trueppm-api 0.4.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +14 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +164 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +2837 -0
- trueppm_api/apps/access/serializers.py +946 -0
- trueppm_api/apps/access/services.py +667 -0
- trueppm_api/apps/access/signals.py +103 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +95 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1931 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +108 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +418 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +177 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +837 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +403 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +622 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +519 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +731 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1106 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +277 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +500 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1231 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +436 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +447 -0
- trueppm_api/apps/notifications/email_backend.py +330 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1197 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +961 -0
- trueppm_api/apps/notifications/services.py +1451 -0
- trueppm_api/apps/notifications/tasks.py +711 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +904 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +551 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +969 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +523 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +466 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +449 -0
- trueppm_api/apps/projects/backfill.py +287 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +204 -0
- trueppm_api/apps/projects/config_notice.py +1325 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +93 -0
- trueppm_api/apps/projects/decisions_views.py +94 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +353 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +8946 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +575 -0
- trueppm_api/apps/projects/program_schedule.py +745 -0
- trueppm_api/apps/projects/program_views.py +3220 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/exporter.py +1644 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2390 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +126 -0
- trueppm_api/apps/projects/seed/replay.py +1691 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1149 -0
- trueppm_api/apps/projects/serializers.py +12310 -0
- trueppm_api/apps/projects/services.py +6966 -0
- trueppm_api/apps/projects/share_serializers.py +132 -0
- trueppm_api/apps/projects/share_services.py +325 -0
- trueppm_api/apps/projects/share_views.py +374 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +909 -0
- trueppm_api/apps/projects/signal_privacy_views.py +476 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +266 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1152 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1917 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +258 -0
- trueppm_api/apps/projects/template_views.py +799 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1011 -0
- trueppm_api/apps/projects/utilization.py +885 -0
- trueppm_api/apps/projects/views.py +20572 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1749 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +119 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +516 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +283 -0
- trueppm_api/apps/scheduling/serializers.py +564 -0
- trueppm_api/apps/scheduling/services.py +1182 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +1984 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2564 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +437 -0
- trueppm_api/apps/sso/services.py +1385 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +320 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +471 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +225 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +371 -0
- trueppm_api/apps/sync/serializers.py +687 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +501 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +930 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +434 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +117 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +526 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +144 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +372 -0
- trueppm_api/apps/webhooks/serializers.py +318 -0
- trueppm_api/apps/webhooks/tasks.py +663 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +383 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +368 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +242 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +919 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +555 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1502 -0
- trueppm_api/asgi.py +42 -0
- trueppm_api/celery.py +31 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +916 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +949 -0
- trueppm_api/core/password_reset.py +500 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +884 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +344 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2455 -0
- trueppm_api/settings/dev.py +273 -0
- trueppm_api/settings/prod.py +228 -0
- trueppm_api/urls.py +153 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
- trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
- trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Dynamic outbound-mail connection resolution (#712, ADR-0213).
|
|
2
|
+
|
|
3
|
+
All transactional mail is sent through a connection returned by
|
|
4
|
+
:func:`resolve_email_connection`, which reads the writable
|
|
5
|
+
:class:`~trueppm_api.apps.notifications.models.WorkspaceEmailSettings` singleton
|
|
6
|
+
and builds a ``django.core.mail`` SMTP connection from it — overriding the
|
|
7
|
+
process-global ``EMAIL_BACKEND`` when a transport is configured. When the
|
|
8
|
+
workspace transport is ``cloud`` (the unconfigured default) it falls back to the
|
|
9
|
+
global backend, so a fresh install behaves exactly as it did before #712.
|
|
10
|
+
|
|
11
|
+
Security posture (ADR-0213 §4, security review C1/H1/H2/M1):
|
|
12
|
+
- The custom SMTP / SES host is SSRF-checked with ``assert_host_allowed`` both
|
|
13
|
+
at save (serializer) *and* here at send time — closing the DNS-rebinding
|
|
14
|
+
window where a host resolves public at save and private at send.
|
|
15
|
+
- The Fernet-encrypted password is decrypted only here, server-side, never
|
|
16
|
+
logged and never returned by the API.
|
|
17
|
+
- Connection-build failures raise :class:`EmailTransportError` carrying a
|
|
18
|
+
generic message; callers must not surface the underlying ``smtplib``
|
|
19
|
+
exception (which can echo credentials) to clients.
|
|
20
|
+
|
|
21
|
+
Fail-closed on a configured-but-unusable transport (#2886 item 2). This module
|
|
22
|
+
used to log a warning and fall back to the process-global ``EMAIL_BACKEND`` when
|
|
23
|
+
the stored password would not decrypt or the stored host failed the egress guard.
|
|
24
|
+
The intent — one corrupt row must not dead-letter a whole drain batch — was
|
|
25
|
+
sound; the effect was that mail configured for the operator's relay went out over
|
|
26
|
+
an entirely different transport (or into a console/dummy backend and nowhere at
|
|
27
|
+
all) while the Email settings page still reported ``configured_via: "database"``
|
|
28
|
+
and ``password_is_set: true``. Both faults now raise
|
|
29
|
+
:class:`EmailTransportError`, and the queued drains catch it, log an ERROR, and
|
|
30
|
+
leave their rows pending rather than burning retries on a configuration fault
|
|
31
|
+
that an operator fix will clear. :func:`transport_status` exposes the same
|
|
32
|
+
condition as a read-derived signal, which is what the System Health card and the
|
|
33
|
+
``trueppm_email_transport_unavailable`` gauge report on — the fail-closed path is
|
|
34
|
+
therefore observable rather than merely silent-but-safe.
|
|
35
|
+
|
|
36
|
+
This matches ``apps.integrations.encryption``, which has always been fail-closed:
|
|
37
|
+
``decrypt_secret`` raises ``CredentialEncryptionError`` rather than returning a
|
|
38
|
+
usable-looking empty string. Falling open was this module's own deviation from
|
|
39
|
+
the mechanism it borrows.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import logging
|
|
45
|
+
from typing import TYPE_CHECKING
|
|
46
|
+
|
|
47
|
+
from django.conf import settings
|
|
48
|
+
from django.core.mail import get_connection
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from django.core.mail.backends.base import BaseEmailBackend
|
|
52
|
+
|
|
53
|
+
from .models import WorkspaceEmailSettings
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
_SMTP_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
|
58
|
+
|
|
59
|
+
# Provider-fixed relay parameters. SendGrid's SMTP relay uses a constant host and
|
|
60
|
+
# the literal username "apikey" (the API key travels as the password); SES uses a
|
|
61
|
+
# region-specific host that the UI composes into ``host`` (ADR-0213 §2). Both are
|
|
62
|
+
# STARTTLS on 587, so 0.4 needs no backend class beyond Django's SMTP backend.
|
|
63
|
+
SENDGRID_HOST = "smtp.sendgrid.net"
|
|
64
|
+
SENDGRID_PORT = 587
|
|
65
|
+
SENDGRID_USERNAME = "apikey"
|
|
66
|
+
SES_PORT = 587
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EmailTransportError(RuntimeError):
|
|
70
|
+
"""A workspace mail transport could not be built or opened.
|
|
71
|
+
|
|
72
|
+
Carries a client-safe message only — never the underlying ``smtplib``
|
|
73
|
+
exception or the connection kwargs (which include the password).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _effective_smtp(
|
|
78
|
+
*,
|
|
79
|
+
transport_mode: str,
|
|
80
|
+
host: str,
|
|
81
|
+
port: int,
|
|
82
|
+
security: str,
|
|
83
|
+
username: str,
|
|
84
|
+
) -> tuple[str, int, str, str]:
|
|
85
|
+
"""Resolve (host, port, username, security) after applying provider defaults.
|
|
86
|
+
|
|
87
|
+
Each provider's fixed host/port/username is applied so the caller only ever
|
|
88
|
+
supplies the credential and (for SES) the region-composed host.
|
|
89
|
+
"""
|
|
90
|
+
if transport_mode == "sendgrid":
|
|
91
|
+
return SENDGRID_HOST, SENDGRID_PORT, SENDGRID_USERNAME, "tls"
|
|
92
|
+
if transport_mode == "ses":
|
|
93
|
+
# host is the region relay (email-smtp.<region>.amazonaws.com), composed
|
|
94
|
+
# by the UI; only port/security are forced.
|
|
95
|
+
return host, SES_PORT, username, "tls"
|
|
96
|
+
return host, port, username, security
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_smtp_connection(
|
|
100
|
+
*,
|
|
101
|
+
transport_mode: str,
|
|
102
|
+
host: str,
|
|
103
|
+
port: int,
|
|
104
|
+
security: str,
|
|
105
|
+
username: str,
|
|
106
|
+
password: str,
|
|
107
|
+
validate_host: bool = True,
|
|
108
|
+
) -> BaseEmailBackend:
|
|
109
|
+
"""Build (but do not open) an SMTP connection for a non-cloud transport.
|
|
110
|
+
|
|
111
|
+
Used both by the serializer's validate-before-persist probe (with the
|
|
112
|
+
*incoming* candidate values) and by :func:`resolve_email_connection` (with
|
|
113
|
+
the persisted model values). Re-runs the SSRF host check unless a caller has
|
|
114
|
+
already validated the exact same host in the same request.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
EmailTransportError: The host resolves to a non-public address.
|
|
118
|
+
"""
|
|
119
|
+
eff_host, eff_port, eff_username, eff_security = _effective_smtp(
|
|
120
|
+
transport_mode=transport_mode,
|
|
121
|
+
host=host,
|
|
122
|
+
port=port,
|
|
123
|
+
security=security,
|
|
124
|
+
username=username,
|
|
125
|
+
)
|
|
126
|
+
if validate_host:
|
|
127
|
+
_assert_host_public(eff_host, eff_port)
|
|
128
|
+
# Never enable smtplib debuglevel — it prints the base64 AUTH line
|
|
129
|
+
# (username + password) to stderr (security review M1).
|
|
130
|
+
conn = get_connection(
|
|
131
|
+
_SMTP_BACKEND,
|
|
132
|
+
host=eff_host,
|
|
133
|
+
port=eff_port,
|
|
134
|
+
username=eff_username,
|
|
135
|
+
password=password,
|
|
136
|
+
use_tls=eff_security == "tls",
|
|
137
|
+
use_ssl=eff_security == "ssl",
|
|
138
|
+
timeout=getattr(settings, "EMAIL_TIMEOUT", 10),
|
|
139
|
+
)
|
|
140
|
+
return conn
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _assert_host_public(host: str, port: int) -> None:
|
|
144
|
+
"""SSRF guard for the SMTP relay host (ADR-0213 §4). Blocks internal targets.
|
|
145
|
+
|
|
146
|
+
A DNS-resolution failure is allowed through (the host may resolve later and
|
|
147
|
+
the actual connect will fail cleanly), mirroring the webhook-URL validator;
|
|
148
|
+
a host that resolves to a private/loopback/link-local/metadata address is
|
|
149
|
+
rejected outright.
|
|
150
|
+
"""
|
|
151
|
+
from trueppm_api.apps.integrations.http import (
|
|
152
|
+
EgressBlocked,
|
|
153
|
+
EgressError,
|
|
154
|
+
assert_host_allowed,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
assert_host_allowed(host, port)
|
|
159
|
+
except EgressBlocked as exc:
|
|
160
|
+
# Do NOT copy the EgressBlocked text into EmailTransportError: it can
|
|
161
|
+
# embed the DNS-resolved internal address, which the serializer would
|
|
162
|
+
# otherwise surface to the client (SSRF oracle). Log the detail, raise a
|
|
163
|
+
# curated message (security review M1; CodeQL py/stack-trace-exposure).
|
|
164
|
+
logger.info("SMTP host rejected by egress guard (host=%s port=%s): %s", host, port, exc)
|
|
165
|
+
raise EmailTransportError(
|
|
166
|
+
"The mail server host is not permitted — it resolves to a non-public address."
|
|
167
|
+
) from exc
|
|
168
|
+
except EgressError:
|
|
169
|
+
# Unresolvable now; the connect attempt re-checks and fails cleanly.
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def probe_transport(
|
|
174
|
+
*,
|
|
175
|
+
transport_mode: str,
|
|
176
|
+
host: str,
|
|
177
|
+
port: int,
|
|
178
|
+
security: str,
|
|
179
|
+
username: str,
|
|
180
|
+
password: str,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Open and immediately close a candidate SMTP connection, or raise.
|
|
183
|
+
|
|
184
|
+
The validate-before-persist gate (ADR-0213 §3): a bad transport must be
|
|
185
|
+
rejected *before* the row is written so the workspace can't be locked out of
|
|
186
|
+
mail. Raises :class:`EmailTransportError` with a **generic** message on any
|
|
187
|
+
build or connect failure — the underlying ``smtplib`` exception (which can
|
|
188
|
+
echo credentials) is deliberately swallowed (security review M1).
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
conn = build_smtp_connection(
|
|
192
|
+
transport_mode=transport_mode,
|
|
193
|
+
host=host,
|
|
194
|
+
port=port,
|
|
195
|
+
security=security,
|
|
196
|
+
username=username,
|
|
197
|
+
password=password,
|
|
198
|
+
)
|
|
199
|
+
except EmailTransportError:
|
|
200
|
+
raise
|
|
201
|
+
try:
|
|
202
|
+
conn.open()
|
|
203
|
+
conn.close()
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
logger.info(
|
|
206
|
+
"probe_transport: connect failed (transport=%s host=%s port=%s)",
|
|
207
|
+
transport_mode,
|
|
208
|
+
host,
|
|
209
|
+
port,
|
|
210
|
+
)
|
|
211
|
+
raise EmailTransportError(
|
|
212
|
+
"Could not connect to the mail server. Check the host, port, security, and credentials."
|
|
213
|
+
) from exc
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# Read-derived transport-status literals (#2886). Reported by the System Health
|
|
217
|
+
# notification card and by the ``trueppm_email_transport_unavailable`` gauge.
|
|
218
|
+
TRANSPORT_OK = "ok"
|
|
219
|
+
TRANSPORT_GLOBAL = "global" # cloud / unconfigured — the process-global backend
|
|
220
|
+
TRANSPORT_UNDECRYPTABLE = "undecryptable"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def resolve_email_connection(
|
|
224
|
+
settings_obj: WorkspaceEmailSettings | None = None,
|
|
225
|
+
) -> BaseEmailBackend:
|
|
226
|
+
"""Return the mail connection for the workspace's configured transport.
|
|
227
|
+
|
|
228
|
+
``cloud`` / unconfigured → the process-global ``EMAIL_BACKEND`` (today's
|
|
229
|
+
behaviour). Otherwise an SMTP connection built from the singleton, with the
|
|
230
|
+
password decrypted here and the host SSRF-re-checked at send time.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
EmailTransportError: The workspace has a transport configured but it cannot
|
|
234
|
+
be built — the stored password will not decrypt (the encryption key
|
|
235
|
+
rotated out from under the row) or the stored host fails the egress
|
|
236
|
+
guard. This is deliberately **not** a fall back to the global backend:
|
|
237
|
+
sending the operator's mail over a transport they did not configure,
|
|
238
|
+
while the settings page reports the configured one, is a silent reroute
|
|
239
|
+
(#2886 item 2). Callers on a queued path catch this and leave their rows
|
|
240
|
+
pending; callers on a synchronous path surface a generic failure.
|
|
241
|
+
"""
|
|
242
|
+
from .models import EmailTransportMode, WorkspaceEmailSettings
|
|
243
|
+
|
|
244
|
+
obj = settings_obj or WorkspaceEmailSettings.load()
|
|
245
|
+
if obj.transport_mode == EmailTransportMode.CLOUD:
|
|
246
|
+
return get_connection()
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
password = obj.get_password()
|
|
250
|
+
except Exception as exc:
|
|
251
|
+
# ERROR, not WARNING: no mail leaves the workspace until an operator
|
|
252
|
+
# re-enters the password, so this is an outage rather than a degradation.
|
|
253
|
+
logger.error(
|
|
254
|
+
"resolve_email_connection: could not decrypt the stored SMTP password "
|
|
255
|
+
"(transport=%s host=%s) — refusing to send over a different transport. "
|
|
256
|
+
"The encryption key may have rotated; re-enter the password in "
|
|
257
|
+
"Workspace -> Email.",
|
|
258
|
+
obj.transport_mode,
|
|
259
|
+
obj.host,
|
|
260
|
+
)
|
|
261
|
+
raise EmailTransportError(
|
|
262
|
+
"The stored mail credential could not be decrypted. Re-enter the "
|
|
263
|
+
"password for this transport."
|
|
264
|
+
) from exc
|
|
265
|
+
|
|
266
|
+
return build_smtp_connection(
|
|
267
|
+
transport_mode=str(obj.transport_mode),
|
|
268
|
+
host=obj.host,
|
|
269
|
+
port=obj.port,
|
|
270
|
+
security=str(obj.security),
|
|
271
|
+
username=obj.username,
|
|
272
|
+
password=password,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def transport_status(settings_obj: WorkspaceEmailSettings | None = None) -> str:
|
|
277
|
+
"""Classify whether the configured transport's credential can be used, on read.
|
|
278
|
+
|
|
279
|
+
The credential fault in :func:`resolve_email_connection` is a deterministic
|
|
280
|
+
function of committed state (the stored ciphertext) and live configuration (the
|
|
281
|
+
encryption key) — not of anything a worker writes. So the web process can evaluate
|
|
282
|
+
it directly, which is what makes this a *positive* health signal rather than an
|
|
283
|
+
inference from the absence of one: it is true at the instant the credential breaks,
|
|
284
|
+
with no row having to fail first and no window in which anything could clear it.
|
|
285
|
+
|
|
286
|
+
Deliberately does **not** re-run the egress guard. That check resolves DNS, and
|
|
287
|
+
this function is called on a polled health endpoint and a Prometheus scrape — a
|
|
288
|
+
lookup per poll is a real egress and latency cost for a fault the queued-backlog
|
|
289
|
+
signal already catches (a host the guard rejects makes every send raise, so rows
|
|
290
|
+
stay pending and the notification card's aging-backlog signal fires).
|
|
291
|
+
|
|
292
|
+
Returns one of :data:`TRANSPORT_OK`, :data:`TRANSPORT_GLOBAL` (no workspace
|
|
293
|
+
transport configured — the process-global backend is in use, which is the shipped
|
|
294
|
+
default and not a fault), or :data:`TRANSPORT_UNDECRYPTABLE`. Never raises: a
|
|
295
|
+
status probe that can throw is unusable in a health endpoint.
|
|
296
|
+
"""
|
|
297
|
+
from .models import EmailTransportMode, WorkspaceEmailSettings
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
obj = settings_obj or WorkspaceEmailSettings.load()
|
|
301
|
+
if obj.transport_mode == EmailTransportMode.CLOUD:
|
|
302
|
+
return TRANSPORT_GLOBAL
|
|
303
|
+
try:
|
|
304
|
+
obj.get_password()
|
|
305
|
+
except Exception:
|
|
306
|
+
return TRANSPORT_UNDECRYPTABLE
|
|
307
|
+
except Exception: # pragma: no cover — guard, not behavior
|
|
308
|
+
logger.warning("transport_status: could not classify the mail transport", exc_info=True)
|
|
309
|
+
return TRANSPORT_OK
|
|
310
|
+
return TRANSPORT_OK
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def resolve_from_email(settings_obj: WorkspaceEmailSettings | None = None) -> str:
|
|
314
|
+
"""Return the effective From header ("Name <addr>" or the settings default)."""
|
|
315
|
+
from .models import WorkspaceEmailSettings
|
|
316
|
+
|
|
317
|
+
obj = settings_obj or WorkspaceEmailSettings.load()
|
|
318
|
+
addr = obj.from_email.strip()
|
|
319
|
+
if not addr:
|
|
320
|
+
return getattr(settings, "DEFAULT_FROM_EMAIL", "notifications@trueppm.local")
|
|
321
|
+
name = obj.from_name.strip()
|
|
322
|
+
return f"{name} <{addr}>" if name else addr
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def resolve_reply_to(settings_obj: WorkspaceEmailSettings | None = None) -> list[str]:
|
|
326
|
+
"""Return the reply-to list for outbound mail, or [] when unset."""
|
|
327
|
+
from .models import WorkspaceEmailSettings
|
|
328
|
+
|
|
329
|
+
obj = settings_obj or WorkspaceEmailSettings.load()
|
|
330
|
+
return [obj.reply_to.strip()] if obj.reply_to.strip() else []
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Deliverability (SPF / DKIM / DMARC) health checks for the From domain.
|
|
2
|
+
|
|
3
|
+
Live DNS TXT lookups for the workspace From-address domain (#712, ADR-0213 §4).
|
|
4
|
+
Bounded and defensive per the security review (M4): a short per-query timeout, a
|
|
5
|
+
capped number of records, TXT-only lookups (never opens a connection to the
|
|
6
|
+
domain), and the target domain is derived solely from the *persisted, validated*
|
|
7
|
+
``from_email`` — never from request input. The endpoint is operator-gated and
|
|
8
|
+
throttled by the caller.
|
|
9
|
+
|
|
10
|
+
Each check returns ``pass`` / ``warn`` / ``fail``. When the DNS resolver is
|
|
11
|
+
unavailable the whole surface degrades to ``available = False`` rather than
|
|
12
|
+
erroring, so a resolver-less host shows "checks unavailable" instead of a
|
|
13
|
+
failure.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import TypedDict
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Bound every lookup: a slow/hostile authoritative resolver must not hang the
|
|
24
|
+
# request, and an oversized TXT set must not balloon the response.
|
|
25
|
+
_DNS_TIMEOUT_SECONDS = 3.0
|
|
26
|
+
_MAX_TXT_RECORDS = 20
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HealthResult(TypedDict):
|
|
30
|
+
available: bool
|
|
31
|
+
domain: str
|
|
32
|
+
spf: str
|
|
33
|
+
dkim: str
|
|
34
|
+
dmarc: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _empty(domain: str, *, available: bool) -> HealthResult:
|
|
38
|
+
status = "unknown"
|
|
39
|
+
return {
|
|
40
|
+
"available": available,
|
|
41
|
+
"domain": domain,
|
|
42
|
+
"spf": status,
|
|
43
|
+
"dkim": status,
|
|
44
|
+
"dmarc": status,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _txt_records(name: str) -> list[str]:
|
|
49
|
+
"""Return decoded TXT strings for ``name`` (bounded), or [] on any DNS miss."""
|
|
50
|
+
import dns.resolver # local import: optional at runtime, degrades gracefully
|
|
51
|
+
|
|
52
|
+
resolver = dns.resolver.Resolver()
|
|
53
|
+
resolver.lifetime = _DNS_TIMEOUT_SECONDS
|
|
54
|
+
resolver.timeout = _DNS_TIMEOUT_SECONDS
|
|
55
|
+
try:
|
|
56
|
+
answer = resolver.resolve(name, "TXT")
|
|
57
|
+
except Exception:
|
|
58
|
+
# NXDOMAIN, timeout, no-answer, servfail — all mean "record not found".
|
|
59
|
+
return []
|
|
60
|
+
records: list[str] = []
|
|
61
|
+
for rdata in answer:
|
|
62
|
+
# Each TXT rdata is one or more byte strings; join then decode.
|
|
63
|
+
try:
|
|
64
|
+
value = b"".join(getattr(rdata, "strings", [])).decode("utf-8", "replace")
|
|
65
|
+
except Exception: # pragma: no cover — defensive
|
|
66
|
+
continue
|
|
67
|
+
records.append(value)
|
|
68
|
+
if len(records) >= _MAX_TXT_RECORDS:
|
|
69
|
+
break
|
|
70
|
+
return records
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_deliverability(from_email: str, dkim_selector: str = "") -> HealthResult:
|
|
74
|
+
"""Resolve SPF / DKIM / DMARC posture for the From domain.
|
|
75
|
+
|
|
76
|
+
``from_email`` is the persisted, EmailField-validated address; the domain is
|
|
77
|
+
everything after the last ``@``. Returns ``available = False`` when there is
|
|
78
|
+
no From domain or the DNS resolver is not installed.
|
|
79
|
+
"""
|
|
80
|
+
domain = from_email.rsplit("@", 1)[-1].strip().lower() if "@" in from_email else ""
|
|
81
|
+
if not domain:
|
|
82
|
+
return _empty(domain, available=False)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
import dns.resolver # noqa: F401 — availability probe
|
|
86
|
+
except ImportError: # pragma: no cover — dnspython is a hard dependency
|
|
87
|
+
logger.info("check_deliverability: dnspython not installed; degrading")
|
|
88
|
+
return _empty(domain, available=False)
|
|
89
|
+
|
|
90
|
+
# SPF: a v=spf1 TXT record on the apex.
|
|
91
|
+
spf_records = _txt_records(domain)
|
|
92
|
+
spf_terms = [r for r in spf_records if r.lower().startswith("v=spf1")]
|
|
93
|
+
if not spf_terms:
|
|
94
|
+
spf = "fail"
|
|
95
|
+
elif any("-all" in r for r in spf_terms):
|
|
96
|
+
spf = "pass"
|
|
97
|
+
else:
|
|
98
|
+
# ~all / ?all — present but not enforcing.
|
|
99
|
+
spf = "warn"
|
|
100
|
+
|
|
101
|
+
# DMARC: a v=DMARC1 TXT record at _dmarc.<domain>.
|
|
102
|
+
dmarc_records = [
|
|
103
|
+
r for r in _txt_records(f"_dmarc.{domain}") if r.lower().startswith("v=dmarc1")
|
|
104
|
+
]
|
|
105
|
+
if not dmarc_records:
|
|
106
|
+
dmarc = "fail"
|
|
107
|
+
elif any("p=reject" in r.lower() or "p=quarantine" in r.lower() for r in dmarc_records):
|
|
108
|
+
dmarc = "pass"
|
|
109
|
+
else:
|
|
110
|
+
dmarc = "warn" # p=none (monitoring only)
|
|
111
|
+
|
|
112
|
+
# DKIM: only checkable when a selector is configured; look up
|
|
113
|
+
# <selector>._domainkey.<domain> for a v=DKIM1 / k= record.
|
|
114
|
+
selector = dkim_selector.strip()
|
|
115
|
+
if not selector:
|
|
116
|
+
dkim = "warn" # no selector configured — can't verify
|
|
117
|
+
else:
|
|
118
|
+
dkim_records = _txt_records(f"{selector}._domainkey.{domain}")
|
|
119
|
+
has_dkim = any("v=dkim1" in r.lower() or "k=" in r.lower() for r in dkim_records)
|
|
120
|
+
dkim = "pass" if has_dkim else "fail"
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"available": True,
|
|
124
|
+
"domain": domain,
|
|
125
|
+
"spf": spf,
|
|
126
|
+
"dkim": dkim,
|
|
127
|
+
"dmarc": dmarc,
|
|
128
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Generated by Django 6.0.3 on 2026-05-20 02:41
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.db import migrations, models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
("projects", "0038_taskcomment_commentreaction_commentacknowledgement_and_more"),
|
|
15
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name="Mention",
|
|
21
|
+
fields=[
|
|
22
|
+
(
|
|
23
|
+
"id",
|
|
24
|
+
models.UUIDField(
|
|
25
|
+
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
("mentioned_group_key", models.CharField(blank=True, default="", max_length=32)),
|
|
29
|
+
(
|
|
30
|
+
"scope",
|
|
31
|
+
models.CharField(
|
|
32
|
+
choices=[
|
|
33
|
+
("project_visible", "Project visible"),
|
|
34
|
+
("team_only", "Team only"),
|
|
35
|
+
("private", "Private"),
|
|
36
|
+
],
|
|
37
|
+
default="project_visible",
|
|
38
|
+
max_length=32,
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
42
|
+
(
|
|
43
|
+
"mentioned_user",
|
|
44
|
+
models.ForeignKey(
|
|
45
|
+
blank=True,
|
|
46
|
+
null=True,
|
|
47
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
48
|
+
related_name="mentions_received",
|
|
49
|
+
to=settings.AUTH_USER_MODEL,
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
"mentioner",
|
|
54
|
+
models.ForeignKey(
|
|
55
|
+
null=True,
|
|
56
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
57
|
+
related_name="mentions_authored",
|
|
58
|
+
to=settings.AUTH_USER_MODEL,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
(
|
|
62
|
+
"project",
|
|
63
|
+
models.ForeignKey(
|
|
64
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
65
|
+
related_name="mentions",
|
|
66
|
+
to="projects.project",
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
(
|
|
70
|
+
"task_comment",
|
|
71
|
+
models.ForeignKey(
|
|
72
|
+
blank=True,
|
|
73
|
+
null=True,
|
|
74
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
75
|
+
related_name="mentions",
|
|
76
|
+
to="projects.taskcomment",
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
],
|
|
80
|
+
options={
|
|
81
|
+
"db_table": "notifications_mention",
|
|
82
|
+
"ordering": ["-created_at"],
|
|
83
|
+
},
|
|
84
|
+
),
|
|
85
|
+
migrations.CreateModel(
|
|
86
|
+
name="Notification",
|
|
87
|
+
fields=[
|
|
88
|
+
(
|
|
89
|
+
"id",
|
|
90
|
+
models.UUIDField(
|
|
91
|
+
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
("is_read", models.BooleanField(db_index=True, default=False)),
|
|
95
|
+
("is_archived", models.BooleanField(default=False)),
|
|
96
|
+
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
97
|
+
("read_at", models.DateTimeField(blank=True, null=True)),
|
|
98
|
+
("email_pending", models.BooleanField(db_index=True, default=False)),
|
|
99
|
+
("email_sent_at", models.DateTimeField(blank=True, null=True)),
|
|
100
|
+
("email_failed_at", models.DateTimeField(blank=True, null=True)),
|
|
101
|
+
("email_attempts", models.PositiveIntegerField(default=0)),
|
|
102
|
+
(
|
|
103
|
+
"mention",
|
|
104
|
+
models.ForeignKey(
|
|
105
|
+
blank=True,
|
|
106
|
+
null=True,
|
|
107
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
108
|
+
related_name="notifications",
|
|
109
|
+
to="notifications.mention",
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
(
|
|
113
|
+
"project",
|
|
114
|
+
models.ForeignKey(
|
|
115
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
116
|
+
related_name="notifications",
|
|
117
|
+
to="projects.project",
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
(
|
|
121
|
+
"recipient",
|
|
122
|
+
models.ForeignKey(
|
|
123
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
124
|
+
related_name="notifications",
|
|
125
|
+
to=settings.AUTH_USER_MODEL,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
],
|
|
129
|
+
options={
|
|
130
|
+
"db_table": "notifications_notification",
|
|
131
|
+
"ordering": ["-created_at"],
|
|
132
|
+
},
|
|
133
|
+
),
|
|
134
|
+
migrations.CreateModel(
|
|
135
|
+
name="NotificationPreference",
|
|
136
|
+
fields=[
|
|
137
|
+
(
|
|
138
|
+
"id",
|
|
139
|
+
models.BigAutoField(
|
|
140
|
+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
("event_type", models.CharField(max_length=64)),
|
|
144
|
+
("channel", models.CharField(max_length=32)),
|
|
145
|
+
("enabled", models.BooleanField()),
|
|
146
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
147
|
+
(
|
|
148
|
+
"user",
|
|
149
|
+
models.ForeignKey(
|
|
150
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
151
|
+
related_name="notification_preferences",
|
|
152
|
+
to=settings.AUTH_USER_MODEL,
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
],
|
|
156
|
+
options={
|
|
157
|
+
"db_table": "notifications_preference",
|
|
158
|
+
},
|
|
159
|
+
),
|
|
160
|
+
migrations.AddIndex(
|
|
161
|
+
model_name="mention",
|
|
162
|
+
index=models.Index(
|
|
163
|
+
fields=["mentioned_user", "-created_at"], name="ix_mention_user_recent"
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
migrations.AddIndex(
|
|
167
|
+
model_name="mention",
|
|
168
|
+
index=models.Index(fields=["project", "-created_at"], name="ix_mention_project_recent"),
|
|
169
|
+
),
|
|
170
|
+
migrations.AddConstraint(
|
|
171
|
+
model_name="mention",
|
|
172
|
+
constraint=models.CheckConstraint(
|
|
173
|
+
condition=models.Q(
|
|
174
|
+
("mentioned_user__isnull", False),
|
|
175
|
+
models.Q(("mentioned_group_key", ""), _negated=True),
|
|
176
|
+
_connector="OR",
|
|
177
|
+
),
|
|
178
|
+
name="mention_must_have_target",
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
migrations.AddConstraint(
|
|
182
|
+
model_name="mention",
|
|
183
|
+
constraint=models.CheckConstraint(
|
|
184
|
+
condition=models.Q(("task_comment__isnull", False)), name="mention_must_have_source"
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
migrations.AddIndex(
|
|
188
|
+
model_name="notification",
|
|
189
|
+
index=models.Index(
|
|
190
|
+
fields=["recipient", "is_read", "-created_at"], name="ix_notif_unread"
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
migrations.AddIndex(
|
|
194
|
+
model_name="notification",
|
|
195
|
+
index=models.Index(
|
|
196
|
+
condition=models.Q(("email_pending", True)),
|
|
197
|
+
fields=["email_pending", "-created_at"],
|
|
198
|
+
name="ix_notif_email_pending",
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
migrations.AddConstraint(
|
|
202
|
+
model_name="notificationpreference",
|
|
203
|
+
constraint=models.UniqueConstraint(
|
|
204
|
+
fields=("user", "event_type", "channel"), name="uq_notifpref_user_event_channel"
|
|
205
|
+
),
|
|
206
|
+
),
|
|
207
|
+
]
|