trueppm-api 0.4.0__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 +27 -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 +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -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_squashed_0019_release_0_4.py +302 -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 +143 -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/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -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/0001_squashed_0003_release_0_4.py +344 -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 +109 -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/0001_squashed_0003_release_0_4.py +90 -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 +459 -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 +178 -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 +863 -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 +597 -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_squashed_0014_release_0_4.py +225 -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/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -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 +528 -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 +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -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 +280 -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 +551 -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 +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -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 +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -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 +299 -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_squashed_0011_release_0_4.py +204 -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 +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -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 +566 -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 +993 -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 +537 -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_squashed_0009_release_0_4.py +128 -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 +541 -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 +458 -0
- trueppm_api/apps/projects/backfill.py +459 -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 +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -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/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -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 +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -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/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -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 +59 -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 +75 -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 +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -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/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -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 +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -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 +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -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 +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -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 +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -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 +1244 -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 +1979 -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 +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -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/0014_squashed_0015_release_0_4.py +47 -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 +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -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/mc_admission.py +181 -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/0010_squashed_0016_release_0_4.py +106 -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/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -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 +2908 -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 +446 -0
- trueppm_api/apps/sso/services.py +1473 -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 +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -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/0003_squashed_0005_release_0_4.py +77 -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/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -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 +159 -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/0004_backfill_default_teams_on_upgrade.py +49 -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 +440 -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/0001_squashed_0004_release_0_4.py +167 -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 +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -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 +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -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 +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -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 +371 -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 +254 -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_squashed_0027_release_0_4.py +199 -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 +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -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/demo_read_only.py +314 -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 +1221 -0
- trueppm_api/core/password_reset.py +513 -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 +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -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/core/worker_selfheal.py +205 -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 +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -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.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Django signals for the workspace app.
|
|
2
|
+
|
|
3
|
+
The ``audit_event_created`` signal is the OSS extension point for the Enterprise
|
|
4
|
+
operational-audit layer (immutable/signed storage, retention policy, SOC-2
|
|
5
|
+
export). Enterprise receivers connect via their own ``AppConfig.ready()`` without
|
|
6
|
+
modifying OSS code — OSS never imports ``trueppm_enterprise`` (ADR-0157, #859).
|
|
7
|
+
|
|
8
|
+
Usage (Enterprise side, never imported by OSS)::
|
|
9
|
+
|
|
10
|
+
# trueppm_enterprise/audit/apps.py
|
|
11
|
+
def ready(self):
|
|
12
|
+
from trueppm_api.apps.workspace.signals import audit_event_created
|
|
13
|
+
audit_event_created.connect(immutable_audit_receiver)
|
|
14
|
+
|
|
15
|
+
.. warning::
|
|
16
|
+
The signal is emitted from ``transaction.on_commit()`` so a receiver only
|
|
17
|
+
ever sees a committed ``AuditEvent`` row (no phantom events on rollback).
|
|
18
|
+
Receivers that perform further I/O (HTTP calls, cross-DB writes) own the
|
|
19
|
+
durability of that downstream work.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import django.dispatch
|
|
25
|
+
|
|
26
|
+
# Sent after an AuditEvent row is committed (ADR-0157, #859). This is the stable
|
|
27
|
+
# seam Enterprise registers against to layer immutable/signed/SOC-2 storage on
|
|
28
|
+
# top of the OSS event stream.
|
|
29
|
+
#
|
|
30
|
+
# Keyword arguments (payload schema is STABLE — do not change):
|
|
31
|
+
# sender — the AuditEvent class
|
|
32
|
+
# audit_event — the committed AuditEvent instance
|
|
33
|
+
#
|
|
34
|
+
# Fired via record_audit_event() using transaction.on_commit + send_robust, so a
|
|
35
|
+
# raising enterprise receiver is swallowed-and-logged and can never break the OSS
|
|
36
|
+
# write path. OSS connects no receiver.
|
|
37
|
+
audit_event_created = django.dispatch.Signal()
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""Celery tasks for the workspace app (ADR-0087 §Durable Execution).
|
|
2
|
+
|
|
3
|
+
- ``drain_invite_emails`` — every 30 s, sends queued invite emails via the
|
|
4
|
+
transactional-outbox pattern (mirrors ``notifications.drain_notification_emails``).
|
|
5
|
+
A new invite is drain-eligible as soon as it commits and ``create_invite`` nudges
|
|
6
|
+
the drain from ``on_commit``, so first-send latency is seconds (#4191).
|
|
7
|
+
- ``purge_stale_invites`` — nightly, marks expired pending invites and deletes
|
|
8
|
+
accepted/revoked/expired invites older than the retention window.
|
|
9
|
+
- ``run_workspace_export`` — builds a full-workspace archive for one job and emails
|
|
10
|
+
the owner; idempotent + bounded retries (ADR-0174).
|
|
11
|
+
- ``drain_workspace_exports`` — every 30 s, re-dispatches export jobs orphaned by a
|
|
12
|
+
broker outage at ``on_commit`` (5-min orphan window).
|
|
13
|
+
- ``purge_expired_exports`` — nightly, deletes export jobs past their link expiry and
|
|
14
|
+
their stored archives.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from datetime import timedelta
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from celery import shared_task
|
|
24
|
+
from celery.exceptions import SoftTimeLimitExceeded
|
|
25
|
+
|
|
26
|
+
from trueppm_api.core.idempotent import idempotent_task
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from django.core.mail.backends.base import BaseEmailBackend
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
EMAIL_MAX_RETRIES = 3
|
|
34
|
+
# Minimum gap between a failed attempt and the next one (#4191). First sends are
|
|
35
|
+
# immediate, but on_commit nudges run the drain far more often than the 30 s Beat
|
|
36
|
+
# tick, so without this floor a short relay outage would burn all three attempts in
|
|
37
|
+
# seconds and mark the invite FAILED. Keeps retries about one Beat tick apart.
|
|
38
|
+
# Slightly under the 30 s tick so a Beat-only retry lands on the NEXT tick rather
|
|
39
|
+
# racing it (a floor equal to the tick would skip one about half the time).
|
|
40
|
+
EMAIL_RETRY_SPACING = timedelta(seconds=25)
|
|
41
|
+
INVITE_RETENTION_DAYS = 30 # ADR-0087 §Durable item 6
|
|
42
|
+
# Beat task name — also the key the per-minute throttle divisor is derived from
|
|
43
|
+
# (#2887 item 4). This drain shares the workspace delivery limits with the
|
|
44
|
+
# notification drain; it used to carry its own hardcoded EMAIL_BATCH_SIZE = 50 and
|
|
45
|
+
# consult neither operator control, so an invite burst could double the configured
|
|
46
|
+
# rate on the same 30 s cadence (#2887 item 3).
|
|
47
|
+
DRAIN_TASK_NAME = "workspace.drain_invite_emails"
|
|
48
|
+
|
|
49
|
+
EXPORT_MAX_RETRIES = 3 # ADR-0174 §Durable item 8
|
|
50
|
+
EXPORT_ORPHAN_WINDOW_MINUTES = 5 # ADR-0174 §Durable item 3
|
|
51
|
+
EXPORT_DRAIN_BATCH_SIZE = 10
|
|
52
|
+
DEFAULT_EXPORT_RETENTION_DAYS = 7 # ADR-0174 §Durable item 6
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@idempotent_task(
|
|
56
|
+
lock_key_template="drain_invite_emails",
|
|
57
|
+
lock_ttl=60,
|
|
58
|
+
on_contention="skip",
|
|
59
|
+
soft_time_limit=25,
|
|
60
|
+
time_limit=30,
|
|
61
|
+
acks_late=True,
|
|
62
|
+
reject_on_worker_lost=True,
|
|
63
|
+
name="workspace.drain_invite_emails",
|
|
64
|
+
)
|
|
65
|
+
def drain_invite_emails(self: object) -> None:
|
|
66
|
+
"""Send queued workspace-invite emails (runs every 30 s via Beat)."""
|
|
67
|
+
_do_drain_invite_emails()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@idempotent_task(
|
|
71
|
+
lock_key_template="purge_stale_invites",
|
|
72
|
+
lock_ttl=120,
|
|
73
|
+
on_contention="skip",
|
|
74
|
+
soft_time_limit=55,
|
|
75
|
+
time_limit=90,
|
|
76
|
+
acks_late=True,
|
|
77
|
+
reject_on_worker_lost=True,
|
|
78
|
+
name="workspace.purge_stale_invites",
|
|
79
|
+
)
|
|
80
|
+
def purge_stale_invites(self: object) -> None:
|
|
81
|
+
"""Expire overdue pending invites; delete terminal invites past retention."""
|
|
82
|
+
_do_purge_stale_invites()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Drain — extracted for direct testability without Celery in the loop
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _do_drain_invite_emails() -> None:
|
|
91
|
+
from django.db.models import Q
|
|
92
|
+
from django.utils import timezone
|
|
93
|
+
|
|
94
|
+
from trueppm_api.apps.notifications.delivery_limits import (
|
|
95
|
+
EMAIL_MAX_BATCH_SIZE,
|
|
96
|
+
beat_ticks_per_minute,
|
|
97
|
+
per_tick_cap,
|
|
98
|
+
reserve_send_budget,
|
|
99
|
+
)
|
|
100
|
+
from trueppm_api.apps.notifications.email_backend import (
|
|
101
|
+
EmailTransportError,
|
|
102
|
+
resolve_email_connection,
|
|
103
|
+
resolve_from_email,
|
|
104
|
+
)
|
|
105
|
+
from trueppm_api.apps.notifications.models import WorkspaceEmailSettings
|
|
106
|
+
|
|
107
|
+
from .models import WorkspaceInvite
|
|
108
|
+
|
|
109
|
+
now = timezone.now()
|
|
110
|
+
|
|
111
|
+
# Same two-stage bound as the notification drain: a per-tick cap for smoothing,
|
|
112
|
+
# then a reservation against the per-minute budget every mail path shares.
|
|
113
|
+
email_settings = WorkspaceEmailSettings.load()
|
|
114
|
+
budget = reserve_send_budget(
|
|
115
|
+
int(email_settings.throttle_per_min or 0),
|
|
116
|
+
per_tick_cap(
|
|
117
|
+
email_settings,
|
|
118
|
+
ticks_per_minute=beat_ticks_per_minute(DRAIN_TASK_NAME),
|
|
119
|
+
fallback=EMAIL_MAX_BATCH_SIZE,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
granted = budget.granted
|
|
123
|
+
if granted <= 0:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
pending = list(
|
|
127
|
+
WorkspaceInvite.objects.filter(
|
|
128
|
+
email_pending=True,
|
|
129
|
+
email_sent_at__isnull=True,
|
|
130
|
+
email_attempts__lt=EMAIL_MAX_RETRIES,
|
|
131
|
+
# No ``created_at`` age floor (#4191, ADR-0087 §Durable item 3 amendment).
|
|
132
|
+
# This drain is the only code that sends an invite, and overlapping drains
|
|
133
|
+
# are serialized by the singleton lock on ``drain_invite_emails`` — not by
|
|
134
|
+
# row age — so a 5-min floor excluded no in-flight owner and only delayed
|
|
135
|
+
# every first send. The drain cannot see an uncommitted row either way.
|
|
136
|
+
)
|
|
137
|
+
# Retries stay one tick apart even when nudges run the drain more often.
|
|
138
|
+
.filter(Q(email_failed_at__isnull=True) | Q(email_failed_at__lt=now - EMAIL_RETRY_SPACING))
|
|
139
|
+
.select_related("invited_by")
|
|
140
|
+
.order_by("created_at")[:granted]
|
|
141
|
+
)
|
|
142
|
+
budget.release(granted - len(pending))
|
|
143
|
+
if not pending:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# Resolve the transport ONCE per batch, and bail out without touching any row when
|
|
147
|
+
# it cannot be built (#2886 item 2). Resolving per invite would both re-connect N
|
|
148
|
+
# times and — now that an unusable transport raises rather than silently rerouting
|
|
149
|
+
# — burn all three retries on every queued invite and mark them FAILED for what is
|
|
150
|
+
# a configuration fault an operator fix clears.
|
|
151
|
+
try:
|
|
152
|
+
connection = resolve_email_connection(email_settings)
|
|
153
|
+
except EmailTransportError:
|
|
154
|
+
logger.error(
|
|
155
|
+
"drain_invite_emails: the configured mail transport is unusable — "
|
|
156
|
+
"%d invite(s) left pending, no retries burned. Fix the transport in "
|
|
157
|
+
"Workspace -> Email.",
|
|
158
|
+
len(pending),
|
|
159
|
+
)
|
|
160
|
+
budget.release(len(pending))
|
|
161
|
+
return
|
|
162
|
+
from_email = resolve_from_email(email_settings)
|
|
163
|
+
|
|
164
|
+
sent = 0
|
|
165
|
+
failed = 0
|
|
166
|
+
for invite in pending:
|
|
167
|
+
try:
|
|
168
|
+
ok = _send_invite_email(invite, connection=connection, from_email=from_email)
|
|
169
|
+
except SoftTimeLimitExceeded:
|
|
170
|
+
raise
|
|
171
|
+
except Exception:
|
|
172
|
+
logger.exception("drain_invite_emails: unexpected error for invite %s", invite.pk)
|
|
173
|
+
ok = False
|
|
174
|
+
|
|
175
|
+
if ok:
|
|
176
|
+
# Clearing email_token consumes the at-rest raw token — after a
|
|
177
|
+
# successful send only the SHA-256 hash remains (ADR-0087 §4).
|
|
178
|
+
WorkspaceInvite.objects.filter(pk=invite.pk).update(
|
|
179
|
+
email_pending=False,
|
|
180
|
+
email_sent_at=timezone.now(),
|
|
181
|
+
email_token="",
|
|
182
|
+
)
|
|
183
|
+
sent += 1
|
|
184
|
+
else:
|
|
185
|
+
from django.db.models import F
|
|
186
|
+
|
|
187
|
+
from .models import InviteStatus
|
|
188
|
+
|
|
189
|
+
new_attempts = invite.email_attempts + 1
|
|
190
|
+
update_fields: dict[str, object] = {
|
|
191
|
+
"email_attempts": F("email_attempts") + 1,
|
|
192
|
+
"email_failed_at": timezone.now(),
|
|
193
|
+
}
|
|
194
|
+
if new_attempts >= EMAIL_MAX_RETRIES:
|
|
195
|
+
# Terminal failure: stop draining, clear the raw token regardless so
|
|
196
|
+
# it does not linger at rest once the send path has given up, and
|
|
197
|
+
# mark the invite FAILED so it is visibly dead (an admin re-invites).
|
|
198
|
+
# The previous code left email_token populated forever on a row that
|
|
199
|
+
# would never send — exactly the at-rest retention the hash design
|
|
200
|
+
# exists to avoid (ADR-0087 §4).
|
|
201
|
+
update_fields["email_pending"] = False
|
|
202
|
+
update_fields["email_token"] = ""
|
|
203
|
+
update_fields["status"] = InviteStatus.FAILED
|
|
204
|
+
# Only a still-PENDING row: a concurrent revoke (#4589) must not be
|
|
205
|
+
# flipped back to FAILED, which is resendable.
|
|
206
|
+
WorkspaceInvite.objects.filter(pk=invite.pk, status=InviteStatus.PENDING).update(
|
|
207
|
+
**update_fields
|
|
208
|
+
)
|
|
209
|
+
failed += 1
|
|
210
|
+
|
|
211
|
+
logger.info("drain_invite_emails: sent=%d failed=%d candidates=%d", sent, failed, len(pending))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Workspace export (ADR-0174, #641)
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@shared_task( # type: ignore[untyped-decorator]
|
|
220
|
+
bind=True,
|
|
221
|
+
max_retries=EXPORT_MAX_RETRIES,
|
|
222
|
+
soft_time_limit=600,
|
|
223
|
+
time_limit=660,
|
|
224
|
+
acks_late=True,
|
|
225
|
+
reject_on_worker_lost=True,
|
|
226
|
+
name="workspace.run_workspace_export",
|
|
227
|
+
)
|
|
228
|
+
def run_workspace_export(self: object, job_id: str) -> None:
|
|
229
|
+
"""Build the full-workspace archive for ``job_id`` and email the owner.
|
|
230
|
+
|
|
231
|
+
Idempotent: claims the job under ``select_for_update`` and no-ops unless it is
|
|
232
|
+
``pending``/``running``, so a duplicate delivery (broker retry, drain re-dispatch)
|
|
233
|
+
cannot produce two archives. Transient failures retry up to ``EXPORT_MAX_RETRIES``
|
|
234
|
+
(the job stays ``running`` so a retry is allowed); on exhaustion the job is marked
|
|
235
|
+
``failed`` and the owner can request a fresh export.
|
|
236
|
+
"""
|
|
237
|
+
# Claim the job (idempotency gate).
|
|
238
|
+
from django.db import transaction
|
|
239
|
+
from django.utils import timezone
|
|
240
|
+
|
|
241
|
+
from .export import build_and_store_archive
|
|
242
|
+
from .models import ExportJobStatus, WorkspaceExportJob
|
|
243
|
+
|
|
244
|
+
with transaction.atomic():
|
|
245
|
+
job = WorkspaceExportJob.objects.select_for_update().filter(pk=job_id).first()
|
|
246
|
+
if job is None:
|
|
247
|
+
logger.warning("run_workspace_export: job %s not found", job_id)
|
|
248
|
+
return
|
|
249
|
+
if job.status not in (ExportJobStatus.PENDING, ExportJobStatus.RUNNING):
|
|
250
|
+
logger.info("run_workspace_export: job %s already %s, skipping", job_id, job.status)
|
|
251
|
+
return
|
|
252
|
+
job.status = ExportJobStatus.RUNNING
|
|
253
|
+
job.started_at = timezone.now()
|
|
254
|
+
job.celery_task_id = getattr(getattr(self, "request", None), "id", "") or ""
|
|
255
|
+
job.save(update_fields=["status", "started_at", "celery_task_id"])
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
storage_path, size = build_and_store_archive(job_id)
|
|
259
|
+
except Exception as exc:
|
|
260
|
+
retries = getattr(getattr(self, "request", None), "retries", 0)
|
|
261
|
+
if retries < EXPORT_MAX_RETRIES:
|
|
262
|
+
logger.warning("run_workspace_export: job %s failed, retrying", job_id, exc_info=True)
|
|
263
|
+
raise self.retry(exc=exc, countdown=10 * (2**retries)) from exc # type: ignore[attr-defined]
|
|
264
|
+
logger.exception("run_workspace_export: job %s failed permanently", job_id)
|
|
265
|
+
WorkspaceExportJob.objects.filter(pk=job_id).update(
|
|
266
|
+
status=ExportJobStatus.FAILED,
|
|
267
|
+
error_detail=str(exc)[:2000],
|
|
268
|
+
completed_at=timezone.now(),
|
|
269
|
+
)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
retention = _export_retention_days()
|
|
273
|
+
expires_at = timezone.now() + timedelta(days=retention) if retention is not None else None
|
|
274
|
+
WorkspaceExportJob.objects.filter(pk=job_id).update(
|
|
275
|
+
status=ExportJobStatus.SUCCESS,
|
|
276
|
+
file_path=storage_path,
|
|
277
|
+
file_size=size,
|
|
278
|
+
expires_at=expires_at,
|
|
279
|
+
completed_at=timezone.now(),
|
|
280
|
+
error_detail="",
|
|
281
|
+
)
|
|
282
|
+
_send_export_ready_email(job_id)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@idempotent_task(
|
|
286
|
+
lock_key_template="drain_workspace_exports",
|
|
287
|
+
lock_ttl=60,
|
|
288
|
+
on_contention="skip",
|
|
289
|
+
soft_time_limit=25,
|
|
290
|
+
time_limit=30,
|
|
291
|
+
acks_late=True,
|
|
292
|
+
reject_on_worker_lost=True,
|
|
293
|
+
name="workspace.drain_workspace_exports",
|
|
294
|
+
)
|
|
295
|
+
def drain_workspace_exports(self: object) -> None:
|
|
296
|
+
"""Re-dispatch export jobs stuck in ``pending`` (broker down at on_commit)."""
|
|
297
|
+
_do_drain_workspace_exports()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@idempotent_task(
|
|
301
|
+
lock_key_template="purge_expired_exports",
|
|
302
|
+
lock_ttl=120,
|
|
303
|
+
on_contention="skip",
|
|
304
|
+
soft_time_limit=55,
|
|
305
|
+
time_limit=90,
|
|
306
|
+
acks_late=True,
|
|
307
|
+
reject_on_worker_lost=True,
|
|
308
|
+
name="workspace.purge_expired_exports",
|
|
309
|
+
)
|
|
310
|
+
def purge_expired_exports(self: object) -> None:
|
|
311
|
+
"""Delete export jobs past their link expiry, and their stored archives."""
|
|
312
|
+
_do_purge_expired_exports()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _export_retention_days() -> int | None:
|
|
316
|
+
from django.conf import settings
|
|
317
|
+
|
|
318
|
+
return getattr(settings, "TRUEPPM_EXPORT_RETENTION_DAYS", DEFAULT_EXPORT_RETENTION_DAYS)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _do_drain_workspace_exports() -> None:
|
|
322
|
+
from django.utils import timezone
|
|
323
|
+
|
|
324
|
+
from .models import ExportJobStatus, WorkspaceExportJob
|
|
325
|
+
|
|
326
|
+
orphan_cutoff = timezone.now() - timedelta(minutes=EXPORT_ORPHAN_WINDOW_MINUTES)
|
|
327
|
+
stuck = list(
|
|
328
|
+
WorkspaceExportJob.objects.filter(
|
|
329
|
+
status=ExportJobStatus.PENDING,
|
|
330
|
+
celery_task_id="",
|
|
331
|
+
created_at__lt=orphan_cutoff,
|
|
332
|
+
).order_by("created_at")[:EXPORT_DRAIN_BATCH_SIZE]
|
|
333
|
+
)
|
|
334
|
+
if not stuck:
|
|
335
|
+
return
|
|
336
|
+
for job in stuck:
|
|
337
|
+
try:
|
|
338
|
+
run_workspace_export.delay(str(job.id))
|
|
339
|
+
except SoftTimeLimitExceeded:
|
|
340
|
+
raise
|
|
341
|
+
except Exception: # pragma: no cover - broker still down, next tick retries
|
|
342
|
+
logger.warning("drain_workspace_exports: broker still unavailable for %s", job.id)
|
|
343
|
+
break
|
|
344
|
+
logger.info("drain_workspace_exports: re-dispatched %d job(s)", len(stuck))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _do_purge_expired_exports() -> None:
|
|
348
|
+
from django.core.files.storage import default_storage
|
|
349
|
+
from django.utils import timezone
|
|
350
|
+
|
|
351
|
+
from .models import WorkspaceExportJob
|
|
352
|
+
|
|
353
|
+
retention = _export_retention_days()
|
|
354
|
+
if retention is None: # retention disabled — keep archives indefinitely
|
|
355
|
+
return
|
|
356
|
+
expired = WorkspaceExportJob.objects.filter(expires_at__lt=timezone.now())
|
|
357
|
+
count = 0
|
|
358
|
+
for job in expired.iterator():
|
|
359
|
+
if job.file_path:
|
|
360
|
+
try:
|
|
361
|
+
default_storage.delete(job.file_path)
|
|
362
|
+
except OSError: # pragma: no cover - storage drift, still drop the row
|
|
363
|
+
logger.warning("purge_expired_exports: could not delete file for %s", job.id)
|
|
364
|
+
job.delete()
|
|
365
|
+
count += 1
|
|
366
|
+
if count:
|
|
367
|
+
logger.info("purge_expired_exports: deleted %d expired export(s)", count)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _send_export_ready_email(job_id: str) -> bool:
|
|
371
|
+
"""Notify the owner their export is ready. Best-effort (returns success).
|
|
372
|
+
|
|
373
|
+
Deactivated accounts are excluded (#3585). Off-boarding is a **second,
|
|
374
|
+
independent revocation axis** from membership (``access/signals.py``): it writes
|
|
375
|
+
``WorkspaceMembership.status = DEACTIVATED`` and ``User.is_active = False`` and
|
|
376
|
+
touches neither the export job nor the project/program membership rows, so a job
|
|
377
|
+
queued while the requester was still active and completed after off-boarding kept
|
|
378
|
+
mailing them — announcing that a **full workspace data export** is ready to
|
|
379
|
+
download, at an address the account no longer controls. Every *credential* path
|
|
380
|
+
already closes on ``is_active`` (JWT, session, PAT, live sockets); this notice is
|
|
381
|
+
*pushed*, so nothing was closing it.
|
|
382
|
+
|
|
383
|
+
The floor deliberately sits **above** the transport and the budget, which is how
|
|
384
|
+
the terminal-state discipline of !2399 (#3523) maps onto this path. There is no
|
|
385
|
+
outbox row to retire here: the export notice is a single-shot send off the back of
|
|
386
|
+
``run_workspace_export``, and its terminal state is the job's own ``SUCCESS``,
|
|
387
|
+
already written before this function is called. What carries over is the rule that
|
|
388
|
+
a *suppression must not be recorded as a delivery failure* — returning before
|
|
389
|
+
``resolve_email_connection`` and ``note_unbudgeted_send`` means an off-boarding
|
|
390
|
+
logs no "mail transport unusable" warning, charges nothing against the shared
|
|
391
|
+
per-minute budget every mail rail draws on, and leaves the job ``SUCCESS`` with an
|
|
392
|
+
empty ``error_detail``. No operator-facing signal reports a personnel change as a
|
|
393
|
+
broken mail relay, and nothing re-queues, so the notice cannot retry forever.
|
|
394
|
+
"""
|
|
395
|
+
from django.core.mail import EmailMessage
|
|
396
|
+
|
|
397
|
+
from .models import WorkspaceExportJob
|
|
398
|
+
|
|
399
|
+
job = WorkspaceExportJob.objects.select_related("requested_by").filter(pk=job_id).first()
|
|
400
|
+
if job is None or job.requested_by is None or not job.requested_by.email:
|
|
401
|
+
return False
|
|
402
|
+
if not job.requested_by.is_active:
|
|
403
|
+
logger.info("export ready email: recipient for job %s is deactivated, not sending", job_id)
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
subject, body = _render_export_email(job)
|
|
407
|
+
from trueppm_api.apps.notifications.delivery_limits import (
|
|
408
|
+
note_unbudgeted_send,
|
|
409
|
+
workspace_throttle_per_min,
|
|
410
|
+
)
|
|
411
|
+
from trueppm_api.apps.notifications.email_backend import (
|
|
412
|
+
resolve_email_connection,
|
|
413
|
+
resolve_from_email,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Send on the workspace SMTP transport (#712, ADR-0213); no-op fall back to
|
|
417
|
+
# the global backend when unconfigured. A single transactional message, so it
|
|
418
|
+
# charges the shared per-minute budget without being refused by it (#2887 item 3)
|
|
419
|
+
# — the queued drains yield to it rather than the reverse.
|
|
420
|
+
try:
|
|
421
|
+
connection = resolve_email_connection()
|
|
422
|
+
except Exception:
|
|
423
|
+
logger.warning("export ready email: mail transport unusable for job %s", job_id)
|
|
424
|
+
return False
|
|
425
|
+
note_unbudgeted_send(workspace_throttle_per_min())
|
|
426
|
+
msg = EmailMessage(
|
|
427
|
+
subject=subject,
|
|
428
|
+
body=body,
|
|
429
|
+
from_email=resolve_from_email(),
|
|
430
|
+
to=[job.requested_by.email],
|
|
431
|
+
connection=connection,
|
|
432
|
+
)
|
|
433
|
+
try:
|
|
434
|
+
msg.send(fail_silently=False)
|
|
435
|
+
except Exception:
|
|
436
|
+
logger.warning("export ready email failed for job %s", job_id, exc_info=True)
|
|
437
|
+
return False
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _render_export_email(job: object) -> tuple[str, str]:
|
|
442
|
+
"""Render the export-ready (subject, body). Degrades to prose, never a bad link.
|
|
443
|
+
|
|
444
|
+
When ``FRONTEND_BASE_URL`` is unset, a relative ``/settings/workspace/danger``
|
|
445
|
+
path is not a valid URL and resolves nowhere for a mail client — so it is
|
|
446
|
+
omitted rather than emitted, and the body instead tells the recipient where to
|
|
447
|
+
navigate once signed in. Unlike the invite email below, this degrades safely:
|
|
448
|
+
the recipient already has an account (this email addresses ``job.requested_by``,
|
|
449
|
+
ADR-0174), so "sign in and find it" is a real fallback rather than a dead end.
|
|
450
|
+
"""
|
|
451
|
+
from django.conf import settings
|
|
452
|
+
|
|
453
|
+
from .models import Workspace
|
|
454
|
+
|
|
455
|
+
workspace_name = Workspace.load().name
|
|
456
|
+
base = (getattr(settings, "FRONTEND_BASE_URL", "") or "").rstrip("/")
|
|
457
|
+
subject = f"Your {workspace_name} export is ready"
|
|
458
|
+
if base:
|
|
459
|
+
# The link lands on the danger page, which fetches the job and offers an
|
|
460
|
+
# authenticated download — we never email a raw, unauthenticated archive URL.
|
|
461
|
+
download_lines = [
|
|
462
|
+
"Download it from the workspace danger zone:",
|
|
463
|
+
f"{base}/settings/workspace/danger",
|
|
464
|
+
]
|
|
465
|
+
else:
|
|
466
|
+
download_lines = [
|
|
467
|
+
"Sign in to TruePPM and open Settings -> Workspace -> Danger Zone to download it.",
|
|
468
|
+
]
|
|
469
|
+
body = "\n".join(
|
|
470
|
+
[
|
|
471
|
+
f"Your full export of {workspace_name} has finished and is ready to download.",
|
|
472
|
+
"",
|
|
473
|
+
*download_lines,
|
|
474
|
+
"",
|
|
475
|
+
"The download link expires after a few days; request a new export if it lapses.",
|
|
476
|
+
]
|
|
477
|
+
)
|
|
478
|
+
return subject, body
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _do_purge_stale_invites() -> None:
|
|
482
|
+
from django.utils import timezone
|
|
483
|
+
|
|
484
|
+
from .models import InviteStatus, WorkspaceInvite
|
|
485
|
+
|
|
486
|
+
now = timezone.now()
|
|
487
|
+
expired = WorkspaceInvite.objects.filter(
|
|
488
|
+
status=InviteStatus.PENDING, expires_at__lt=now
|
|
489
|
+
).update(status=InviteStatus.EXPIRED, email_pending=False, email_token="")
|
|
490
|
+
|
|
491
|
+
cutoff = now - timedelta(days=INVITE_RETENTION_DAYS)
|
|
492
|
+
deleted, _ = WorkspaceInvite.objects.filter(
|
|
493
|
+
status__in=[
|
|
494
|
+
InviteStatus.ACCEPTED,
|
|
495
|
+
InviteStatus.REVOKED,
|
|
496
|
+
InviteStatus.EXPIRED,
|
|
497
|
+
InviteStatus.FAILED,
|
|
498
|
+
],
|
|
499
|
+
created_at__lt=cutoff,
|
|
500
|
+
).delete()
|
|
501
|
+
if expired or deleted:
|
|
502
|
+
logger.info("purge_stale_invites: expired=%d deleted=%d", expired, deleted)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ---------------------------------------------------------------------------
|
|
506
|
+
# Email rendering
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _send_invite_email(
|
|
511
|
+
invite: object,
|
|
512
|
+
*,
|
|
513
|
+
connection: BaseEmailBackend | None = None,
|
|
514
|
+
from_email: str | None = None,
|
|
515
|
+
) -> bool:
|
|
516
|
+
"""Render and send the invitation email. Returns True on SMTP success.
|
|
517
|
+
|
|
518
|
+
The transport and From identity are resolved **once per drain batch** and passed
|
|
519
|
+
in; when omitted they are resolved here so a direct/one-off caller still works. A
|
|
520
|
+
caller that resolves them itself is also the one that can distinguish an unusable
|
|
521
|
+
transport (a configuration fault, no retries burned) from a failed send.
|
|
522
|
+
|
|
523
|
+
Deliberately **not** floored on ``User.is_active``, unlike
|
|
524
|
+
:func:`_send_export_ready_email` (#3585). An invite addresses
|
|
525
|
+
``WorkspaceInvite.email``, a raw address for someone who by definition has no
|
|
526
|
+
account yet — there is no ``User`` row to read ``is_active`` from, so the account
|
|
527
|
+
axis is not the right one here. The invite's own revocation axis is its
|
|
528
|
+
``status``/``expires_at``, enforced by the drain's candidate query and by
|
|
529
|
+
``purge_stale_invites``. Adding an ``is_active`` check would either be dead code
|
|
530
|
+
or, worse, would silently drop invites to addresses that happen to collide with a
|
|
531
|
+
deactivated account's — re-inviting an off-boarded person is a legitimate act.
|
|
532
|
+
"""
|
|
533
|
+
from django.core.mail import EmailMessage
|
|
534
|
+
|
|
535
|
+
from .models import WorkspaceInvite
|
|
536
|
+
|
|
537
|
+
inv: WorkspaceInvite = invite # type: ignore[assignment]
|
|
538
|
+
if not inv.email or not inv.email_token:
|
|
539
|
+
return False
|
|
540
|
+
|
|
541
|
+
subject, body = _render_invite_email(inv)
|
|
542
|
+
from trueppm_api.apps.notifications.email_backend import (
|
|
543
|
+
resolve_email_connection,
|
|
544
|
+
resolve_from_email,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Send on the workspace SMTP transport (#712, ADR-0213); no-op fall back to
|
|
548
|
+
# the global backend when unconfigured.
|
|
549
|
+
msg = EmailMessage(
|
|
550
|
+
subject=subject,
|
|
551
|
+
body=body,
|
|
552
|
+
from_email=resolve_from_email() if from_email is None else from_email,
|
|
553
|
+
to=[inv.email],
|
|
554
|
+
connection=resolve_email_connection() if connection is None else connection,
|
|
555
|
+
)
|
|
556
|
+
try:
|
|
557
|
+
msg.send(fail_silently=False)
|
|
558
|
+
# TODO(#4421): swallows SoftTimeLimitExceeded; the drain-loop guard
|
|
559
|
+
# is ineffective until this is fixed
|
|
560
|
+
except Exception:
|
|
561
|
+
logger.warning("drain_invite_emails: SMTP send failed for invite %s", inv.pk, exc_info=True)
|
|
562
|
+
return False
|
|
563
|
+
return True
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _render_invite_email(invite: object) -> tuple[str, str]:
|
|
567
|
+
"""Render the invite (subject, body). Degrades to prose, never a bad link.
|
|
568
|
+
|
|
569
|
+
When ``FRONTEND_BASE_URL`` is unset, a relative ``/invite/accept?token=...``
|
|
570
|
+
path is not a valid URL and resolves nowhere for a mail client, so it is
|
|
571
|
+
omitted rather than emitted. Unlike the export-ready email above, there is no
|
|
572
|
+
in-app fallback to point at: the recipient has no account yet, and the raw
|
|
573
|
+
token is cleared from ``email_token`` the moment this send is recorded as
|
|
574
|
+
successful (ADR-0087 §4) — so once this body goes out without the link, that
|
|
575
|
+
token cannot be recovered. The only path forward is fixing the setting and
|
|
576
|
+
resending (``WorkspaceInviteResendView`` mints a fresh token), which the copy
|
|
577
|
+
says explicitly.
|
|
578
|
+
"""
|
|
579
|
+
from django.conf import settings
|
|
580
|
+
|
|
581
|
+
from .models import Workspace, WorkspaceInvite, WorkspaceRole
|
|
582
|
+
|
|
583
|
+
inv: WorkspaceInvite = invite # type: ignore[assignment]
|
|
584
|
+
workspace_name = Workspace.load().name
|
|
585
|
+
base = (getattr(settings, "FRONTEND_BASE_URL", "") or "").rstrip("/")
|
|
586
|
+
|
|
587
|
+
inviter = inv.invited_by
|
|
588
|
+
inviter_name = (
|
|
589
|
+
(inviter.get_full_name() or inviter.username) if inviter is not None else "An admin"
|
|
590
|
+
)
|
|
591
|
+
role_label = WorkspaceRole(inv.role).label
|
|
592
|
+
|
|
593
|
+
subject = f"You've been invited to {workspace_name} on TruePPM"
|
|
594
|
+
if base:
|
|
595
|
+
accept_lines = [
|
|
596
|
+
"Accept your invitation:",
|
|
597
|
+
f"{base}/invite/accept?token={inv.email_token}",
|
|
598
|
+
]
|
|
599
|
+
else:
|
|
600
|
+
accept_lines = [
|
|
601
|
+
"This TruePPM deployment has no public URL configured, so this email "
|
|
602
|
+
"cannot include an invitation link. Ask whoever invited you to set "
|
|
603
|
+
"TRUEPPM_FRONTEND_BASE_URL and resend the invitation.",
|
|
604
|
+
]
|
|
605
|
+
body = "\n".join(
|
|
606
|
+
[
|
|
607
|
+
f"{inviter_name} has invited you to join {workspace_name} with the {role_label} role.",
|
|
608
|
+
"",
|
|
609
|
+
*accept_lines,
|
|
610
|
+
"",
|
|
611
|
+
f"This invitation expires on {inv.expires_at:%Y-%m-%d}.",
|
|
612
|
+
"",
|
|
613
|
+
"If you weren't expecting this, you can ignore this email.",
|
|
614
|
+
]
|
|
615
|
+
)
|
|
616
|
+
return subject, body
|