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,1197 @@
|
|
|
1
|
+
"""Notifications data model (ADR-0075).
|
|
2
|
+
|
|
3
|
+
Three entities power the unified @mention surface that #311 ships first and
|
|
4
|
+
#476 (deferred Notes) will inherit unchanged:
|
|
5
|
+
|
|
6
|
+
- Mention — polymorphic append-only @mention record. Source is one of a small
|
|
7
|
+
set of nullable FKs (today: TaskComment; #476 adds TaskNote). Target is
|
|
8
|
+
either a user (direct mention) or a group key (snapshot expanded to per-
|
|
9
|
+
user Notification rows at write time).
|
|
10
|
+
- Notification — per-recipient inbox row. Single feed surface so a future
|
|
11
|
+
#476 note-mention lands in the same UI as today's comment-mention.
|
|
12
|
+
- NotificationPreference — per-user `(event_type, channel)` toggle. ADR-0049's
|
|
13
|
+
NOTIFICATION_CHANNELS registry is *registered but not yet consumed*: channel
|
|
14
|
+
validation runs off the ProjectNotificationChannel TextChoices (see
|
|
15
|
+
serializers._VALID_CHANNELS), so an Enterprise-registered slack_dm/teams_dm/sms
|
|
16
|
+
would currently be rejected. Wiring the registry is #3252.
|
|
17
|
+
- ProjectNotificationPreference — per-(project, user) routing matrix plus
|
|
18
|
+
daily quiet-hours window. Backs the Project > Notifications settings page
|
|
19
|
+
(#522); orthogonal to NotificationPreference, which is the *global* mention
|
|
20
|
+
feed default.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import datetime
|
|
26
|
+
import uuid
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from django.conf import settings
|
|
30
|
+
from django.db import models
|
|
31
|
+
|
|
32
|
+
# Lazy FK target — Django resolves the string, so no migration is produced.
|
|
33
|
+
_PROJECT_MODEL = "projects.Project"
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Enums
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MentionScope(models.TextChoices):
|
|
41
|
+
"""Visibility scope of a @mention's source content.
|
|
42
|
+
|
|
43
|
+
`PROJECT_VISIBLE` is the only value used by #311 (comments); #476 will
|
|
44
|
+
populate `TEAM_ONLY` for team-private Decisions and the visibility gate
|
|
45
|
+
on the Decisions view. The scope field exists on day one so #476's gate
|
|
46
|
+
isn't bolted on as an afterthought.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
PROJECT_VISIBLE = "project_visible", "Project visible"
|
|
50
|
+
TEAM_ONLY = "team_only", "Team only"
|
|
51
|
+
PRIVATE = "private", "Private"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NotificationEventType(models.TextChoices):
|
|
55
|
+
"""Event types that produce notifications.
|
|
56
|
+
|
|
57
|
+
Mapped to NotificationPreference rows so users can independently toggle
|
|
58
|
+
in_app / email per event type. The own-task events (#639, ADR-0085) reuse
|
|
59
|
+
the WebhookEventType string values where one exists (task.assigned,
|
|
60
|
+
task.due_date_changed) so event names share one source of truth across the
|
|
61
|
+
webhook and notification subsystems; comment_on_my_task is notification-only.
|
|
62
|
+
@mentions of me are covered by MENTION_INDIVIDUAL / MENTION_GROUP and the
|
|
63
|
+
existing mention path — task.mentioned is not duplicated here.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
MENTION_INDIVIDUAL = "mention_individual", "Direct @mention"
|
|
67
|
+
MENTION_GROUP = "mention_group", "Group @mention"
|
|
68
|
+
# Own-task events (#639) — a user's reach over work assigned to or owned by them.
|
|
69
|
+
TASK_ASSIGNED = "task.assigned", "Assigned to me"
|
|
70
|
+
TASK_DUE_DATE_CHANGED = "task.due_date_changed", "Due date changed on my task"
|
|
71
|
+
COMMENT_ON_MY_TASK = "comment_on_my_task", "Comment on my task"
|
|
72
|
+
# Contributor signal (#855, #476) — fires when a task I own is flagged blocked
|
|
73
|
+
# by someone else. One of the two events in the SIGNAL_ONLY_EVENTS preset.
|
|
74
|
+
TASK_BLOCKED = "task.blocked", "A task I own is blocked"
|
|
75
|
+
# Sprint-close bridge digest (#861) — the PM cohort is told when a closed
|
|
76
|
+
# sprint's reforecast materially shifts a bound milestone's finish. Email
|
|
77
|
+
# defaults ON for this one event (see DEFAULT_PREFERENCES): the issue's whole
|
|
78
|
+
# point is reaching the PM *outside* her session, which in-app alone can't do.
|
|
79
|
+
MILESTONE_FORECAST_SHIFTED = "milestone.forecast_shifted", "Milestone forecast shifted"
|
|
80
|
+
# Schedule-canvas reschedule of a sprint-mate's task (#497) — the rest of the
|
|
81
|
+
# ACTIVE sprint team learns a committed date moved. In-app only for v1.
|
|
82
|
+
SPRINT_TASK_RESCHEDULED = "sprint.task_rescheduled", "Task in my sprint rescheduled"
|
|
83
|
+
# Signal-privacy ceiling-raise ratification (#1275, ADR-0104 Amendment B) — a
|
|
84
|
+
# team-owned vote to widen a signal's visibility. "Opened" reaches eligible
|
|
85
|
+
# voters so the 72h window is discoverable off the Settings page; "resolved"
|
|
86
|
+
# reaches voters + the proposer with the outcome. In-app ON, email opt-in OFF
|
|
87
|
+
# (Priya's hard-NO was on un-opted email, not on an in-app inbox row).
|
|
88
|
+
SIGNAL_CEILING_PROPOSAL_OPENED = (
|
|
89
|
+
"signal.ceiling_proposal_opened",
|
|
90
|
+
"Team signal visibility proposal opened",
|
|
91
|
+
)
|
|
92
|
+
SIGNAL_CEILING_PROPOSAL_RESOLVED = (
|
|
93
|
+
"signal.ceiling_proposal_resolved",
|
|
94
|
+
"Team signal visibility proposal resolved",
|
|
95
|
+
)
|
|
96
|
+
# Project-delete team notification (#1115, VoC Option C). A project soft-delete
|
|
97
|
+
# is a large destructive write that removes the whole team's workspace; every
|
|
98
|
+
# member (bar the deleter) is told in-app who deleted it, so a project does not
|
|
99
|
+
# simply vanish from under them. In-app ON by default; email strictly opt-in OFF
|
|
100
|
+
# (Priya's hard-NO on un-opted email); NEVER push — the durable inbox row is the
|
|
101
|
+
# only channel, so the notification can never arrive as an unsolicited interrupt.
|
|
102
|
+
PROJECT_DELETED = "project.deleted", "A project I belong to was deleted"
|
|
103
|
+
# Stale-task daily nudge (ADR-0200). A daily scan fans this out to the assignee
|
|
104
|
+
# of a non-terminal task that has sat in its current status past the project's
|
|
105
|
+
# configurable threshold (default 7 days) — the "task I forgot in Review" slip.
|
|
106
|
+
# Distinct from the synchronous is_stalled board-card chip (ADR-0115, 3-day dwell):
|
|
107
|
+
# this is opt-in, configurable, and reaches the owner asynchronously. In-app ON,
|
|
108
|
+
# email opt-in OFF (Priya's un-opted-email hard-NO).
|
|
109
|
+
TASK_STALE = "task.stale", "A task I own has gone stale"
|
|
110
|
+
# Sprint-close carryover reassignment (ADR-0232, #1470). Fires to the assignee
|
|
111
|
+
# when close-time carry-over moves a task they own to the next sprint or the
|
|
112
|
+
# backlog — the "my work hopped sprints over the weekend and nobody told me"
|
|
113
|
+
# gap (Priya's load-bearing 🔴 in the 2026-07-01 agile VoC audit). A durable
|
|
114
|
+
# in-app inbox row, NOT a push: ADR-0102 §6 withholds push for board mechanics,
|
|
115
|
+
# but carryover is an actual reassignment of committed work (closer to
|
|
116
|
+
# task.assigned), so the inbox row reaches the assignee off-session without
|
|
117
|
+
# interrupting. In-app ON, email opt-in OFF (Priya's un-opted-email hard-NO).
|
|
118
|
+
TASK_MOVED_SPRINT = "task.moved_sprint", "A task I own was carried to another sprint"
|
|
119
|
+
# Sprint-membership scope change (ADR-0412, #1946). Fires to the project lead
|
|
120
|
+
# cohort (role >= ADMIN) when a task actually ENTERS or LEAVES an ACTIVE sprint
|
|
121
|
+
# — the "PM/admin silently added a task to the active sprint" audit gap Jordan
|
|
122
|
+
# (PO) and Alex (SM) raised as hard-NOs in the 2026-07-14 activity-streams VoC
|
|
123
|
+
# audit. Distinct from TASK_MOVED_SPRINT above (that reaches the *assignee* on
|
|
124
|
+
# close-time carryover); this reaches the *accountable leads* on a live-scope
|
|
125
|
+
# change. In-app ON, email opt-in OFF (Priya's un-opted-email hard-NO), and
|
|
126
|
+
# deliberately NOT in DND_BYPASS_EVENTS — a scope change is not a hard interrupt.
|
|
127
|
+
SPRINT_MEMBERSHIP_CHANGED = (
|
|
128
|
+
"sprint.membership_changed",
|
|
129
|
+
"A task entered or left an active sprint",
|
|
130
|
+
)
|
|
131
|
+
# Project-finish shift (#1911, third #82 schedule-notification rule — companion
|
|
132
|
+
# to #1668's dependency-slip / became-critical pair). Fires when a
|
|
133
|
+
# ProjectForecastSnapshot's cpm_finish moves by more than the project's
|
|
134
|
+
# configurable Project.end_date_shift_threshold_days versus the immediately
|
|
135
|
+
# prior snapshot. Reaches the PM/Owner cohort (role >= ADMIN), same targeting
|
|
136
|
+
# as MILESTONE_FORECAST_SHIFTED (#861) — see notify_project_end_date_shift.
|
|
137
|
+
PROJECT_END_DATE_SHIFTED = "project.end_date_shifted", "Project end date shifted"
|
|
138
|
+
# Amend on a committed plan (#2964). Fires when a structural edit lands on a
|
|
139
|
+
# project that has been committed (lifecycle=active, ADR-0845 baseline taken).
|
|
140
|
+
# Reaches the people whose work is affected — NEVER a block: the design is
|
|
141
|
+
# explicit that Amend records and tells, it does not gate. In-app ON so the
|
|
142
|
+
# change is visible where decisions get argued about; email opt-in OFF, the
|
|
143
|
+
# same un-opted-email floor every contributor signal keeps.
|
|
144
|
+
PLAN_AMENDED = "plan.amended", "A committed plan I work on was changed"
|
|
145
|
+
# Project configuration change that moves the surface everyone works on
|
|
146
|
+
# (#2972, epic #2743). Removing a board lane, hiding a board column, hiding a
|
|
147
|
+
# leaf surface, or switching the methodology preset re-arranges where every
|
|
148
|
+
# assignee's work appears — and before this, only the person who clicked knew.
|
|
149
|
+
#
|
|
150
|
+
# Two properties this event exists to hold, both easy to erode later:
|
|
151
|
+
# 1. It fires ONLY on a change that moves or hides work. A rename, a color,
|
|
152
|
+
# a WIP limit, an added lane — nothing. "A setting was saved" is exactly
|
|
153
|
+
# the notification the issue exists to refuse, so the trigger is the
|
|
154
|
+
# consequence, not the write.
|
|
155
|
+
# 2. Its body names what the change did to THIS recipient's own items
|
|
156
|
+
# (rendered per recipient via create_event_notifications_batch), which is
|
|
157
|
+
# why the notice is worth an inbox row at all.
|
|
158
|
+
# Reaches everyone with work in the project rather than the whole membership:
|
|
159
|
+
# the claim it makes is about the recipient's own items, and a member with no
|
|
160
|
+
# items has nothing to be told about. In-app ON, email opt-in OFF (Priya's
|
|
161
|
+
# un-opted-email hard-NO). Deliberately NOT in DND_BYPASS_EVENTS — a
|
|
162
|
+
# re-arranged board needs to be seen, not to interrupt.
|
|
163
|
+
PROJECT_CONFIG_CHANGED = (
|
|
164
|
+
"project.config_changed",
|
|
165
|
+
"The board or views I work on were reconfigured",
|
|
166
|
+
)
|
|
167
|
+
# Scheduled digests (ADR-0663, #2407) — the first CLOCK-driven events in this
|
|
168
|
+
# enum. Every other member fires from a domain event; these two fire from Beat
|
|
169
|
+
# on the recipient's own weekly slot (UserNotificationSettings.digest_*), which
|
|
170
|
+
# is why they are the only pair defaulting OFF on BOTH channels: a weekly send
|
|
171
|
+
# nobody asked for is exactly the volume the matrix exists to prevent. Content
|
|
172
|
+
# is read from already-computed facts (compute_program_rollup / _load_band) —
|
|
173
|
+
# a digest never derives a number the UI does not already show.
|
|
174
|
+
PROGRAM_HEALTH_DIGEST = "program.health_digest", "Weekly program health digest"
|
|
175
|
+
RESOURCE_OVERALLOCATION_DIGEST = (
|
|
176
|
+
"resource.overallocation_digest",
|
|
177
|
+
"Weekly resource overallocation digest",
|
|
178
|
+
)
|
|
179
|
+
# Membership grant / role-change (#3645). ProjectMembershipViewSet and
|
|
180
|
+
# ProgramMembershipViewSet could create, change and revoke access with no
|
|
181
|
+
# signal reaching the affected person — their only tell was a project or
|
|
182
|
+
# program appearing in their list. These two fire to the added or
|
|
183
|
+
# role-changed user only (not on revoke, which is audit-only — see the
|
|
184
|
+
# issue). In-app ON, email opt-in OFF, matching every other contributor
|
|
185
|
+
# signal in this table (Priya's un-opted-email hard-NO).
|
|
186
|
+
MEMBERSHIP_GRANTED = "membership.granted", "Added to a project or program"
|
|
187
|
+
MEMBERSHIP_ROLE_CHANGED = "membership.role_changed", "Role changed on a project or program"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class NotificationChannel(models.TextChoices):
|
|
191
|
+
"""Delivery channels (OSS keys only).
|
|
192
|
+
|
|
193
|
+
ADR-0049's NOTIFICATION_CHANNELS registry is *intended* to open this axis to
|
|
194
|
+
Enterprise channels (slack_dm / teams_dm / sms), but nothing reads it yet, so
|
|
195
|
+
registering one does not make it accepted. Wiring it is #3252.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
IN_APP = "in_app", "In-app"
|
|
199
|
+
EMAIL = "email", "Email"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Manager — scope-respecting queryset (ADR-0075 must-fix)
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class MentionManager(models.Manager["Mention"]):
|
|
208
|
+
"""Manager enforcing the `Mention.objects.visible_to(user)` pattern.
|
|
209
|
+
|
|
210
|
+
All read paths must go through this method so #476's TEAM_ONLY scope is
|
|
211
|
+
enforced at a single point. Raw `Mention.objects.filter(...)` in viewsets
|
|
212
|
+
is forbidden — the rule is a review checklist item until a lint can be
|
|
213
|
+
added.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def visible_to(
|
|
217
|
+
self,
|
|
218
|
+
user: Any,
|
|
219
|
+
project_id: uuid.UUID | str | None = None,
|
|
220
|
+
) -> models.QuerySet[Mention]:
|
|
221
|
+
"""Return mentions the user is permitted to see.
|
|
222
|
+
|
|
223
|
+
Rules:
|
|
224
|
+
- Always restricted to projects the user is a member of (anchored by
|
|
225
|
+
ProjectMembership; non-deleted only).
|
|
226
|
+
- Within those projects, PROJECT_VISIBLE mentions are visible to all
|
|
227
|
+
members. TEAM_ONLY mentions (future #476) are visible only to the
|
|
228
|
+
mentioner, the mentioned user, and members of the mentioned group.
|
|
229
|
+
PRIVATE mentions are visible only to mentioner + mentioned_user.
|
|
230
|
+
|
|
231
|
+
NOTE: Currently UNUSED in the 0.2 viewsets — Mention visibility is
|
|
232
|
+
enforced indirectly via NotificationViewSet's `recipient=request.user`
|
|
233
|
+
filter and the absence of a direct Mention list endpoint. This method
|
|
234
|
+
is pre-positioned for #476, which will expose a Decision rollup view
|
|
235
|
+
that queries Mentions directly. Do NOT remove as dead code; it's the
|
|
236
|
+
single point of TEAM_ONLY scope enforcement when that scope is wired.
|
|
237
|
+
"""
|
|
238
|
+
from trueppm_api.apps.access.models import ProjectMembership
|
|
239
|
+
|
|
240
|
+
if not user or not getattr(user, "is_authenticated", False):
|
|
241
|
+
return self.none()
|
|
242
|
+
|
|
243
|
+
member_projects = ProjectMembership.objects.filter(user=user, is_deleted=False).values_list(
|
|
244
|
+
"project_id", flat=True
|
|
245
|
+
)
|
|
246
|
+
qs = self.get_queryset().filter(project_id__in=list(member_projects))
|
|
247
|
+
|
|
248
|
+
if project_id is not None:
|
|
249
|
+
qs = qs.filter(project_id=project_id)
|
|
250
|
+
|
|
251
|
+
# Scope gate. PROJECT_VISIBLE is the only scope used in 0.2 (#311);
|
|
252
|
+
# the other two branches are inert today but the structure exists so
|
|
253
|
+
# #476 can populate them without a schema change or rewrite.
|
|
254
|
+
return qs.filter(
|
|
255
|
+
models.Q(scope=MentionScope.PROJECT_VISIBLE)
|
|
256
|
+
| models.Q(scope=MentionScope.TEAM_ONLY, mentioned_user=user)
|
|
257
|
+
| models.Q(scope=MentionScope.TEAM_ONLY, mentioner=user)
|
|
258
|
+
| models.Q(scope=MentionScope.PRIVATE, mentioned_user=user)
|
|
259
|
+
| models.Q(scope=MentionScope.PRIVATE, mentioner=user)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
# Mention
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class Mention(models.Model):
|
|
269
|
+
"""Append-only @mention record. Polymorphic source via nullable FKs.
|
|
270
|
+
|
|
271
|
+
Exactly one source FK and at least one target (user OR group_key) must be
|
|
272
|
+
set — enforced by CheckConstraints. The `mentioned_group_key` is preserved
|
|
273
|
+
on the row for display ("@scrum-team was mentioned"), independently of
|
|
274
|
+
the per-recipient Notification rows that fan out from group resolution.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
278
|
+
mentioner = models.ForeignKey(
|
|
279
|
+
settings.AUTH_USER_MODEL,
|
|
280
|
+
on_delete=models.SET_NULL,
|
|
281
|
+
null=True,
|
|
282
|
+
related_name="mentions_authored",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Target — direct user OR group_key (or both, if the group resolution
|
|
286
|
+
# happens to include the mentioned user explicitly).
|
|
287
|
+
mentioned_user = models.ForeignKey(
|
|
288
|
+
settings.AUTH_USER_MODEL,
|
|
289
|
+
on_delete=models.SET_NULL,
|
|
290
|
+
null=True,
|
|
291
|
+
blank=True,
|
|
292
|
+
related_name="mentions_received",
|
|
293
|
+
)
|
|
294
|
+
# Empty string = no group key (Django convention DJ001: no nullable CharField).
|
|
295
|
+
mentioned_group_key = models.CharField(max_length=32, blank=True, default="")
|
|
296
|
+
|
|
297
|
+
# Polymorphic source. 0.2 has TaskComment only; #476 will add task_note.
|
|
298
|
+
# CheckConstraint requires exactly one (today: task_comment must be set).
|
|
299
|
+
task_comment = models.ForeignKey(
|
|
300
|
+
"projects.TaskComment",
|
|
301
|
+
on_delete=models.CASCADE,
|
|
302
|
+
null=True,
|
|
303
|
+
blank=True,
|
|
304
|
+
related_name="mentions",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
project = models.ForeignKey(
|
|
308
|
+
_PROJECT_MODEL,
|
|
309
|
+
on_delete=models.CASCADE,
|
|
310
|
+
related_name="mentions",
|
|
311
|
+
)
|
|
312
|
+
scope = models.CharField(
|
|
313
|
+
max_length=32,
|
|
314
|
+
choices=MentionScope.choices,
|
|
315
|
+
default=MentionScope.PROJECT_VISIBLE,
|
|
316
|
+
)
|
|
317
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
318
|
+
|
|
319
|
+
objects = MentionManager()
|
|
320
|
+
|
|
321
|
+
class Meta:
|
|
322
|
+
db_table = "notifications_mention"
|
|
323
|
+
ordering = ["-created_at"]
|
|
324
|
+
constraints = [
|
|
325
|
+
models.CheckConstraint(
|
|
326
|
+
condition=(
|
|
327
|
+
models.Q(mentioned_user__isnull=False) | ~models.Q(mentioned_group_key="")
|
|
328
|
+
),
|
|
329
|
+
name="mention_must_have_target",
|
|
330
|
+
),
|
|
331
|
+
# In 0.2 only task_comment is a valid source; when #476 ships,
|
|
332
|
+
# widen this to allow task_note as well.
|
|
333
|
+
models.CheckConstraint(
|
|
334
|
+
condition=models.Q(task_comment__isnull=False),
|
|
335
|
+
name="mention_must_have_source",
|
|
336
|
+
),
|
|
337
|
+
]
|
|
338
|
+
indexes = [
|
|
339
|
+
models.Index(fields=["mentioned_user", "-created_at"], name="ix_mention_user_recent"),
|
|
340
|
+
models.Index(fields=["project", "-created_at"], name="ix_mention_project_recent"),
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
def __str__(self) -> str:
|
|
344
|
+
target = self.mentioned_user_id or f"@{self.mentioned_group_key}"
|
|
345
|
+
return f"Mention(to={target}, scope={self.scope})"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# Notification — per-recipient inbox row
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class Notification(models.Model):
|
|
354
|
+
"""Per-recipient unread notification. Generated from Mention fan-out (0.2)
|
|
355
|
+
and future event types. Single inbox surface — every #476 mention lands
|
|
356
|
+
here unchanged.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
360
|
+
recipient = models.ForeignKey(
|
|
361
|
+
settings.AUTH_USER_MODEL,
|
|
362
|
+
on_delete=models.CASCADE,
|
|
363
|
+
related_name="notifications",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Source — a notification is EITHER mention-sourced (mention set, rendered
|
|
367
|
+
# from the comment) OR event-sourced (#639, ADR-0085 §3: event_type + the
|
|
368
|
+
# pre-rendered subject/body set, mention null). Both feed the one inbox.
|
|
369
|
+
mention = models.ForeignKey(
|
|
370
|
+
Mention,
|
|
371
|
+
on_delete=models.CASCADE,
|
|
372
|
+
null=True,
|
|
373
|
+
blank=True,
|
|
374
|
+
related_name="notifications",
|
|
375
|
+
)
|
|
376
|
+
# Event-sourced notifications (own-task events) — blank for mention rows.
|
|
377
|
+
event_type = models.CharField(max_length=64, blank=True, default="")
|
|
378
|
+
subject = models.CharField(max_length=255, blank=True, default="")
|
|
379
|
+
body = models.TextField(blank=True, default="")
|
|
380
|
+
|
|
381
|
+
# Optional deep-link target (#497/#861). Mention rows resolve their task via
|
|
382
|
+
# the comment; event rows have no such path, so this FK lets the inbox row
|
|
383
|
+
# link straight to the affected task/milestone in the schedule. SET_NULL +
|
|
384
|
+
# related_name="+" — a deleted task should never cascade-delete the historical
|
|
385
|
+
# notification, and no reverse accessor on Task is needed.
|
|
386
|
+
task = models.ForeignKey(
|
|
387
|
+
"projects.Task",
|
|
388
|
+
on_delete=models.SET_NULL,
|
|
389
|
+
null=True,
|
|
390
|
+
blank=True,
|
|
391
|
+
related_name="+",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Nullable since ADR-0663: a scheduled digest spans the recipient's whole
|
|
395
|
+
# membership set and has no single owning project, so forcing one would mean
|
|
396
|
+
# picking an arbitrary project and mis-scoping the row. Every event- and
|
|
397
|
+
# mention-sourced row still sets it; only the account-scoped digests are NULL.
|
|
398
|
+
project = models.ForeignKey(
|
|
399
|
+
_PROJECT_MODEL,
|
|
400
|
+
on_delete=models.CASCADE,
|
|
401
|
+
null=True,
|
|
402
|
+
blank=True,
|
|
403
|
+
related_name="notifications",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
is_read = models.BooleanField(default=False, db_index=True)
|
|
407
|
+
is_archived = models.BooleanField(default=False)
|
|
408
|
+
# Per-notification snooze (ADR-0216 §1). A row with snoozed_until > now() is
|
|
409
|
+
# hidden from the All/Unread inbox views (and the unread-count query) until
|
|
410
|
+
# the timestamp passes, then reappears — a pure query-time filter, no Celery.
|
|
411
|
+
# Null = not snoozed. Indexed because every non-snoozed list query filters on it.
|
|
412
|
+
snoozed_until = models.DateTimeField(null=True, blank=True, db_index=True)
|
|
413
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
414
|
+
read_at = models.DateTimeField(null=True, blank=True)
|
|
415
|
+
|
|
416
|
+
# Email delivery state (drained via drain_notification_emails Beat task).
|
|
417
|
+
email_pending = models.BooleanField(default=False, db_index=True)
|
|
418
|
+
email_sent_at = models.DateTimeField(null=True, blank=True)
|
|
419
|
+
email_failed_at = models.DateTimeField(null=True, blank=True)
|
|
420
|
+
email_attempts = models.PositiveIntegerField(default=0)
|
|
421
|
+
|
|
422
|
+
class Meta:
|
|
423
|
+
db_table = "notifications_notification"
|
|
424
|
+
ordering = ["-created_at"]
|
|
425
|
+
indexes = [
|
|
426
|
+
models.Index(fields=["recipient", "is_read", "-created_at"], name="ix_notif_unread"),
|
|
427
|
+
models.Index(
|
|
428
|
+
fields=["email_pending", "-created_at"],
|
|
429
|
+
name="ix_notif_email_pending",
|
|
430
|
+
condition=models.Q(email_pending=True),
|
|
431
|
+
),
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
def __str__(self) -> str:
|
|
435
|
+
return f"Notification(to={self.recipient_id}, mention={self.mention_id})"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
# NotificationPreference — per-user (event_type, channel) toggle
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class NotificationPreference(models.Model):
|
|
444
|
+
"""Per-user channel preference, per event type.
|
|
445
|
+
|
|
446
|
+
Defaults backfill on first GET (see services.get_or_create_defaults). The
|
|
447
|
+
channel column is a plain CharField, so the *schema* imposes no channel list —
|
|
448
|
+
but serializer validation does (_VALID_CHANNELS, built from the
|
|
449
|
+
ProjectNotificationChannel TextChoices), so an ADR-0049 Enterprise channel
|
|
450
|
+
(slack_dm, teams_dm, sms) does not yet round-trip. Wiring is #3252.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
user = models.ForeignKey(
|
|
454
|
+
settings.AUTH_USER_MODEL,
|
|
455
|
+
on_delete=models.CASCADE,
|
|
456
|
+
related_name="notification_preferences",
|
|
457
|
+
)
|
|
458
|
+
event_type = models.CharField(max_length=64) # NotificationEventType + future
|
|
459
|
+
channel = models.CharField(max_length=32) # NotificationChannel + Enterprise extensions
|
|
460
|
+
enabled = models.BooleanField()
|
|
461
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
462
|
+
|
|
463
|
+
class Meta:
|
|
464
|
+
db_table = "notifications_preference"
|
|
465
|
+
constraints = [
|
|
466
|
+
models.UniqueConstraint(
|
|
467
|
+
fields=["user", "event_type", "channel"],
|
|
468
|
+
name="uq_notifpref_user_event_channel",
|
|
469
|
+
),
|
|
470
|
+
]
|
|
471
|
+
|
|
472
|
+
def __str__(self) -> str:
|
|
473
|
+
state = "on" if self.enabled else "off"
|
|
474
|
+
return f"NotificationPreference({self.event_type}/{self.channel}={state})"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# ---------------------------------------------------------------------------
|
|
478
|
+
# Defaults (ADR-0075 V2 VoC — Priya's "flip email default OFF")
|
|
479
|
+
# ---------------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
DEFAULT_PREFERENCES: list[tuple[str, str, bool]] = [
|
|
483
|
+
(NotificationEventType.MENTION_INDIVIDUAL, NotificationChannel.IN_APP, True),
|
|
484
|
+
(NotificationEventType.MENTION_INDIVIDUAL, NotificationChannel.EMAIL, False),
|
|
485
|
+
(NotificationEventType.MENTION_GROUP, NotificationChannel.IN_APP, True),
|
|
486
|
+
(NotificationEventType.MENTION_GROUP, NotificationChannel.EMAIL, False),
|
|
487
|
+
# Own-task events (#639): in-app ON, email OFF. Email is strictly opt-in —
|
|
488
|
+
# aggressive email defaults were Priya's VoC blocker (ADR-0085 §1, ADR-0075 V2).
|
|
489
|
+
(NotificationEventType.TASK_ASSIGNED, NotificationChannel.IN_APP, True),
|
|
490
|
+
(NotificationEventType.TASK_ASSIGNED, NotificationChannel.EMAIL, False),
|
|
491
|
+
(NotificationEventType.TASK_DUE_DATE_CHANGED, NotificationChannel.IN_APP, True),
|
|
492
|
+
(NotificationEventType.TASK_DUE_DATE_CHANGED, NotificationChannel.EMAIL, False),
|
|
493
|
+
(NotificationEventType.COMMENT_ON_MY_TASK, NotificationChannel.IN_APP, True),
|
|
494
|
+
(NotificationEventType.COMMENT_ON_MY_TASK, NotificationChannel.EMAIL, False),
|
|
495
|
+
# Blocked signal (#855): in-app ON, email OFF — same opt-in-email rule. This
|
|
496
|
+
# event is one of the two kept ON by the Signal-only preset below.
|
|
497
|
+
(NotificationEventType.TASK_BLOCKED, NotificationChannel.IN_APP, True),
|
|
498
|
+
(NotificationEventType.TASK_BLOCKED, NotificationChannel.EMAIL, False),
|
|
499
|
+
# #861 — the bridge digest is the deliberate exception to the email-OFF
|
|
500
|
+
# default: a PM who is not logged in when the team closes a sprint at 7pm
|
|
501
|
+
# must still be pushed the milestone-confidence shift, or "automatic" still
|
|
502
|
+
# demands she remember to log in (the issue's core blocker). Still fully
|
|
503
|
+
# matrix-overridable — a PM can mute either channel.
|
|
504
|
+
(NotificationEventType.MILESTONE_FORECAST_SHIFTED, NotificationChannel.IN_APP, True),
|
|
505
|
+
(NotificationEventType.MILESTONE_FORECAST_SHIFTED, NotificationChannel.EMAIL, True),
|
|
506
|
+
# #497 — in-app only for v1 (email digest/push explicitly out of scope).
|
|
507
|
+
(NotificationEventType.SPRINT_TASK_RESCHEDULED, NotificationChannel.IN_APP, True),
|
|
508
|
+
(NotificationEventType.SPRINT_TASK_RESCHEDULED, NotificationChannel.EMAIL, False),
|
|
509
|
+
# #1275 / ADR-0104 Amendment B — ceiling-raise proposal discovery. In-app ON so
|
|
510
|
+
# voters see the ask without navigating to Settings; email strictly opt-in OFF
|
|
511
|
+
# (Priya's hard-NO is preserved — un-opted email noise, not the in-app inbox).
|
|
512
|
+
(NotificationEventType.SIGNAL_CEILING_PROPOSAL_OPENED, NotificationChannel.IN_APP, True),
|
|
513
|
+
(NotificationEventType.SIGNAL_CEILING_PROPOSAL_OPENED, NotificationChannel.EMAIL, False),
|
|
514
|
+
(NotificationEventType.SIGNAL_CEILING_PROPOSAL_RESOLVED, NotificationChannel.IN_APP, True),
|
|
515
|
+
(NotificationEventType.SIGNAL_CEILING_PROPOSAL_RESOLVED, NotificationChannel.EMAIL, False),
|
|
516
|
+
# #1115 — project-delete team notification. In-app ON so every member learns
|
|
517
|
+
# their project was deleted; email opt-in OFF (Priya's un-opted-email hard-NO).
|
|
518
|
+
(NotificationEventType.PROJECT_DELETED, NotificationChannel.IN_APP, True),
|
|
519
|
+
(NotificationEventType.PROJECT_DELETED, NotificationChannel.EMAIL, False),
|
|
520
|
+
# ADR-0200 — stale-task daily nudge. In-app ON so the owner sees the reminder in
|
|
521
|
+
# their inbox; email opt-in OFF (Priya's un-opted-email hard-NO). A daily digest
|
|
522
|
+
# of forgotten cards must not become un-asked-for email.
|
|
523
|
+
(NotificationEventType.TASK_STALE, NotificationChannel.IN_APP, True),
|
|
524
|
+
(NotificationEventType.TASK_STALE, NotificationChannel.EMAIL, False),
|
|
525
|
+
# ADR-0232 (#1470) — sprint-close carryover reassignment. In-app ON so the
|
|
526
|
+
# assignee learns their committed work moved across the close→plan seam; email
|
|
527
|
+
# opt-in OFF (Priya's un-opted-email hard-NO).
|
|
528
|
+
(NotificationEventType.TASK_MOVED_SPRINT, NotificationChannel.IN_APP, True),
|
|
529
|
+
(NotificationEventType.TASK_MOVED_SPRINT, NotificationChannel.EMAIL, False),
|
|
530
|
+
# ADR-0412 (#1946) — sprint-membership scope change. In-app ON so the project
|
|
531
|
+
# leads see a mid-sprint injection/removal in their inbox; email opt-in OFF
|
|
532
|
+
# (Priya's un-opted-email hard-NO), matching every other contributor signal.
|
|
533
|
+
(NotificationEventType.SPRINT_MEMBERSHIP_CHANGED, NotificationChannel.IN_APP, True),
|
|
534
|
+
(NotificationEventType.SPRINT_MEMBERSHIP_CHANGED, NotificationChannel.EMAIL, False),
|
|
535
|
+
# #2964 — an amend to committed work. In-app ON so the people whose plan
|
|
536
|
+
# changed see it; email opt-in OFF, matching every other contributor signal.
|
|
537
|
+
(NotificationEventType.PLAN_AMENDED, NotificationChannel.IN_APP, True),
|
|
538
|
+
(NotificationEventType.PLAN_AMENDED, NotificationChannel.EMAIL, False),
|
|
539
|
+
# #1911 — project end-date shift. In-app ON so the PM/Owner cohort sees the
|
|
540
|
+
# slip in their inbox; email opt-in OFF (Priya's un-opted-email hard-NO),
|
|
541
|
+
# matching every other schedule-drift signal in this table.
|
|
542
|
+
(NotificationEventType.PROJECT_END_DATE_SHIFTED, NotificationChannel.IN_APP, True),
|
|
543
|
+
(NotificationEventType.PROJECT_END_DATE_SHIFTED, NotificationChannel.EMAIL, False),
|
|
544
|
+
# #2972 — a board lane removed, a column or view hidden, or the methodology
|
|
545
|
+
# preset switched. In-app ON so the people whose work moved learn it moved
|
|
546
|
+
# without waiting to notice; email opt-in OFF, the same un-opted-email floor
|
|
547
|
+
# every contributor signal in this table keeps.
|
|
548
|
+
(NotificationEventType.PROJECT_CONFIG_CHANGED, NotificationChannel.IN_APP, True),
|
|
549
|
+
(NotificationEventType.PROJECT_CONFIG_CHANGED, NotificationChannel.EMAIL, False),
|
|
550
|
+
# ADR-0663 (#2407) — the scheduled digests are the ONLY rows in this table that
|
|
551
|
+
# default OFF on both channels, and deliberately so. Every entry above fires
|
|
552
|
+
# from something the user did or something that happened to their work, so an
|
|
553
|
+
# in-app row is welcome by default; a weekly clock-driven send is traffic the
|
|
554
|
+
# user has to actually want. Defaulting in_app OFF also makes the feature inert
|
|
555
|
+
# on upgrade: create_event_notifications skips a recipient whose in_app is off,
|
|
556
|
+
# so no digest row exists for anyone until they opt in. Flipping either of these
|
|
557
|
+
# to True is a product decision with a volume cost (ADR-0663 §Risks), not a
|
|
558
|
+
# tidy-up.
|
|
559
|
+
(NotificationEventType.PROGRAM_HEALTH_DIGEST, NotificationChannel.IN_APP, False),
|
|
560
|
+
(NotificationEventType.PROGRAM_HEALTH_DIGEST, NotificationChannel.EMAIL, False),
|
|
561
|
+
(NotificationEventType.RESOURCE_OVERALLOCATION_DIGEST, NotificationChannel.IN_APP, False),
|
|
562
|
+
(NotificationEventType.RESOURCE_OVERALLOCATION_DIGEST, NotificationChannel.EMAIL, False),
|
|
563
|
+
# #3645 — membership grant / role-change. In-app ON so the affected person
|
|
564
|
+
# sees who changed their access and to what; email opt-in OFF, the same
|
|
565
|
+
# un-opted-email floor every contributor signal in this table keeps.
|
|
566
|
+
(NotificationEventType.MEMBERSHIP_GRANTED, NotificationChannel.IN_APP, True),
|
|
567
|
+
(NotificationEventType.MEMBERSHIP_GRANTED, NotificationChannel.EMAIL, False),
|
|
568
|
+
(NotificationEventType.MEMBERSHIP_ROLE_CHANGED, NotificationChannel.IN_APP, True),
|
|
569
|
+
(NotificationEventType.MEMBERSHIP_ROLE_CHANGED, NotificationChannel.EMAIL, False),
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# ---------------------------------------------------------------------------
|
|
574
|
+
# Signal-only preset (#855) — the contributor-friendly minimal profile
|
|
575
|
+
# ---------------------------------------------------------------------------
|
|
576
|
+
# Priya (Team Member) gave a hard-NO on noisy notifications. "Signal-only" is the
|
|
577
|
+
# one-click profile that keeps ON only the two events that mean "something needs
|
|
578
|
+
# my attention" — a task of mine got blocked, or a deadline moved — and turns
|
|
579
|
+
# everything else OFF. It is applied by POST /me/notification-preferences/
|
|
580
|
+
# apply-preset/ (a wholesale write of the per-(event,channel) rows), NOT a new
|
|
581
|
+
# stored model: the matrix stays the single source of truth so the existing
|
|
582
|
+
# data-driven settings page (ADR-0085) keeps rendering whatever rows the server
|
|
583
|
+
# returns. The "everything" preset restores DEFAULT_PREFERENCES.
|
|
584
|
+
SIGNAL_ONLY_EVENTS: frozenset[str] = frozenset(
|
|
585
|
+
{
|
|
586
|
+
NotificationEventType.TASK_BLOCKED,
|
|
587
|
+
NotificationEventType.TASK_DUE_DATE_CHANGED,
|
|
588
|
+
}
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# ---------------------------------------------------------------------------
|
|
593
|
+
# Do-Not-Disturb bypass set (#1707, ADR-0292)
|
|
594
|
+
# ---------------------------------------------------------------------------
|
|
595
|
+
# The account-wide DND switch (UserNotificationSettings.dnd_enabled) silences
|
|
596
|
+
# transient channels (email/push) for routine events. These four events ALWAYS
|
|
597
|
+
# deliver on every channel regardless of DND — the "needs my attention now"
|
|
598
|
+
# signals a muted bell must never swallow: a task of mine got blocked, a team
|
|
599
|
+
# signal-visibility vote opened or resolved, or a milestone forecast shifted.
|
|
600
|
+
#
|
|
601
|
+
# This is deliberately NOT SIGNAL_ONLY_EVENTS: that set is the contributor
|
|
602
|
+
# settings *preset* (task.blocked + task.due_date_changed) and is the wrong
|
|
603
|
+
# membership for a safety bypass — it would let a routine due-date change punch
|
|
604
|
+
# through DND while silencing a signal-ceiling proposal. The two sets are
|
|
605
|
+
# distinct on purpose; keep this membership in step as new critical event types
|
|
606
|
+
# are added (it is the load-bearing safety contract, #1707).
|
|
607
|
+
DND_BYPASS_EVENTS: frozenset[str] = frozenset(
|
|
608
|
+
{
|
|
609
|
+
NotificationEventType.TASK_BLOCKED,
|
|
610
|
+
NotificationEventType.SIGNAL_CEILING_PROPOSAL_OPENED,
|
|
611
|
+
NotificationEventType.SIGNAL_CEILING_PROPOSAL_RESOLVED,
|
|
612
|
+
NotificationEventType.MILESTONE_FORECAST_SHIFTED,
|
|
613
|
+
}
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# ---------------------------------------------------------------------------
|
|
618
|
+
# Project-scoped notification preferences (#522)
|
|
619
|
+
# ---------------------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class ProjectNotificationEventType(models.TextChoices):
|
|
623
|
+
"""Project-scoped notification events surfaced on Project > Notifications.
|
|
624
|
+
|
|
625
|
+
Disjoint from `NotificationEventType` (which today only covers @mention
|
|
626
|
+
fan-out for the inbox surface). The project page covers the broader set
|
|
627
|
+
of routing decisions a PM makes per-project — task assignment, slips,
|
|
628
|
+
risk creation, sprint lifecycle, etc.
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
TASK_ASSIGNED = "task_assigned", "Task assigned to me"
|
|
632
|
+
TASK_OVERDUE = "task_overdue", "Task I own is overdue"
|
|
633
|
+
COMMENT_MENTION = "comment_mention", "Mention (@) in a comment"
|
|
634
|
+
STATUS_CHANGE = "status_change", "Task moves to another column"
|
|
635
|
+
BUDGET_ALERT = "budget_alert", "Budget threshold crossed"
|
|
636
|
+
RISK_CREATED = "risk_created", "Risk created or escalated"
|
|
637
|
+
MILESTONE_REACHED = "milestone_reached", "Milestone reached"
|
|
638
|
+
SPRINT_START = "sprint_start", "Sprint started"
|
|
639
|
+
SPRINT_END = "sprint_end", "Sprint closed"
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
#: Project-scoped events that a delivery path actually consults (#2904).
|
|
643
|
+
#: ``COMMENT_MENTION`` is the only one: ``notifications/services.py`` gates the
|
|
644
|
+
#: mention fan-out on it. Everything else in the enum is declared below.
|
|
645
|
+
PROJECT_NOTIFICATION_DISPATCHED_EVENTS: frozenset[str] = frozenset(
|
|
646
|
+
{ProjectNotificationEventType.COMMENT_MENTION.value}
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
#: Members that exist in the matrix but that **nothing dispatches** — the settings
|
|
650
|
+
#: page renders them, and toggling one has no effect in either direction
|
|
651
|
+
#: (TODO(#3016) wire the dispatchers).
|
|
652
|
+
#:
|
|
653
|
+
#: They are kept rather than deleted because they are a stated product intent, and
|
|
654
|
+
#: the API reports them in ``event_delivery`` as ``false`` so a client can say so
|
|
655
|
+
#: instead of implying a delivery that never happens. Crucially they default **OFF** on
|
|
656
|
+
#: every channel: eight events defaulting ON across in-app, email and Slack told an
|
|
657
|
+
#: admin that eight kinds of notification were being delivered, which made turning
|
|
658
|
+
#: one *off* the only action with any apparent meaning — and it had none either.
|
|
659
|
+
#:
|
|
660
|
+
#: This set and :data:`PROJECT_NOTIFICATION_DISPATCHED_EVENTS` must together cover
|
|
661
|
+
#: the enum exactly; ``test_project_notification_dispatch_coverage`` enforces it, so
|
|
662
|
+
#: a member cannot be added without someone deciding which side it is on.
|
|
663
|
+
PROJECT_NOTIFICATION_UNDISPATCHED_EVENTS: frozenset[str] = frozenset(
|
|
664
|
+
{
|
|
665
|
+
ProjectNotificationEventType.TASK_ASSIGNED.value,
|
|
666
|
+
ProjectNotificationEventType.TASK_OVERDUE.value,
|
|
667
|
+
ProjectNotificationEventType.STATUS_CHANGE.value,
|
|
668
|
+
ProjectNotificationEventType.BUDGET_ALERT.value,
|
|
669
|
+
ProjectNotificationEventType.RISK_CREATED.value,
|
|
670
|
+
ProjectNotificationEventType.MILESTONE_REACHED.value,
|
|
671
|
+
ProjectNotificationEventType.SPRINT_START.value,
|
|
672
|
+
ProjectNotificationEventType.SPRINT_END.value,
|
|
673
|
+
}
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class ProjectNotificationChannel(models.TextChoices):
|
|
678
|
+
"""Columns of the project notification matrix — not a claim that each delivers.
|
|
679
|
+
|
|
680
|
+
Which of these TruePPM actually delivers on is not restated here: it is
|
|
681
|
+
:data:`PROJECT_NOTIFICATION_DELIVERABLE_CHANNELS` and its complement
|
|
682
|
+
:data:`PROJECT_NOTIFICATION_UNDELIVERABLE_CHANNELS` below, the one place that
|
|
683
|
+
classification is made and the pair a coverage test holds against this enum.
|
|
684
|
+
``slack`` and ``mobile_push`` are on the undeliverable side: nothing sends to
|
|
685
|
+
them and **no setting anywhere turns them on** — there is no integration to
|
|
686
|
+
configure, which is what the pre-#3378 wording here claimed. They remain as
|
|
687
|
+
columns so a member can record routing intent that applies once delivery ships.
|
|
688
|
+
|
|
689
|
+
This axis is *not* the ADR-0049 ``NOTIFICATION_CHANNELS`` registry — that is the
|
|
690
|
+
account-wide ``NotificationPreference.channel`` vocabulary, registered but not
|
|
691
|
+
yet consumed here (see the module docstring); whether the two converge is #3252.
|
|
692
|
+
|
|
693
|
+
A toggle in the matrix is therefore user intent, and the API says which columns
|
|
694
|
+
that intent can reach today via ``channel_delivery`` (#3378).
|
|
695
|
+
"""
|
|
696
|
+
|
|
697
|
+
IN_APP = "in_app", "In-app"
|
|
698
|
+
EMAIL = "email", "Email"
|
|
699
|
+
SLACK = "slack", "Slack"
|
|
700
|
+
MOBILE_PUSH = "mobile_push", "Mobile push"
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
#: Matrix columns TruePPM has a delivery path for (#3378).
|
|
704
|
+
#:
|
|
705
|
+
#: A literal, deliberately. The obvious move — deriving it from ADR-0049's
|
|
706
|
+
#: ``NOTIFICATION_CHANNELS`` registrations — is wrong twice over. The registry is
|
|
707
|
+
#: the vocabulary of the *account-wide* ``NotificationPreference.channel`` surface,
|
|
708
|
+
#: not of this matrix, and it is registered but not yet consumed (see the module
|
|
709
|
+
#: docstring); whether the two axes converge is #3252's open question, so pinning
|
|
710
|
+
#: this to it would encode a decision nobody has made. And the registry is
|
|
711
|
+
#: populated at ``AppConfig.ready()``, so a value read from it would make the
|
|
712
|
+
#: published OpenAPI schema depend on which apps happened to be loaded when it was
|
|
713
|
+
#: generated — including Enterprise's, which would change the *OSS* schema.
|
|
714
|
+
#:
|
|
715
|
+
#: What keeps the literal honest is
|
|
716
|
+
#: ``tests/apps/notifications/test_project_notification_channel_coverage.py``: the
|
|
717
|
+
#: two sets must cover :class:`ProjectNotificationChannel` exactly and be disjoint,
|
|
718
|
+
#: and an AST walk asserts a channel declared undeliverable is named by no dispatch
|
|
719
|
+
#: site.
|
|
720
|
+
PROJECT_NOTIFICATION_DELIVERABLE_CHANNELS: frozenset[str] = frozenset(
|
|
721
|
+
{
|
|
722
|
+
ProjectNotificationChannel.IN_APP.value,
|
|
723
|
+
ProjectNotificationChannel.EMAIL.value,
|
|
724
|
+
}
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
#: Columns the matrix renders that **nothing delivers on** (TODO(#3252)).
|
|
728
|
+
#:
|
|
729
|
+
#: Not "the integration is not configured" — there is nothing to configure. No code
|
|
730
|
+
#: in ``apps/notifications`` sends to either, and no setting anywhere turns them on.
|
|
731
|
+
#: They are kept as columns because they are a stated product intent and a member's
|
|
732
|
+
#: stored routing choice survives until delivery ships; the API reports them in
|
|
733
|
+
#: ``channel_delivery`` as ``false`` so a client can label the column instead of
|
|
734
|
+
#: rendering a control that cannot do anything.
|
|
735
|
+
PROJECT_NOTIFICATION_UNDELIVERABLE_CHANNELS: frozenset[str] = frozenset(
|
|
736
|
+
{
|
|
737
|
+
ProjectNotificationChannel.SLACK.value,
|
|
738
|
+
ProjectNotificationChannel.MOBILE_PUSH.value,
|
|
739
|
+
}
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# Default matrix — applied lazily on first GET (no per-user backfill on join).
|
|
744
|
+
# A cell defaults ON only where both halves of the claim hold: the event has a
|
|
745
|
+
# dispatcher (PROJECT_NOTIFICATION_DISPATCHED_EVENTS) and the channel has a
|
|
746
|
+
# delivery path (PROJECT_NOTIFICATION_DELIVERABLE_CHANNELS). Everything else
|
|
747
|
+
# defaults OFF, because a default of True is a claim that something will be
|
|
748
|
+
# delivered.
|
|
749
|
+
_T = ProjectNotificationEventType
|
|
750
|
+
_C = ProjectNotificationChannel
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _ALL_OFF() -> dict[str, bool]:
|
|
754
|
+
"""Every channel off — the default for an event nothing dispatches (#2904)."""
|
|
755
|
+
return {_C.IN_APP: False, _C.EMAIL: False, _C.SLACK: False, _C.MOBILE_PUSH: False}
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
PROJECT_NOTIFICATION_DEFAULT_MATRIX: dict[str, dict[str, bool]] = {
|
|
759
|
+
# Only COMMENT_MENTION has a dispatch path, so it is the only row that may
|
|
760
|
+
# default ON — a default of True is a claim that something will be delivered.
|
|
761
|
+
# Slack and mobile push stay OFF even here: the event IS dispatched, but those
|
|
762
|
+
# two columns are in PROJECT_NOTIFICATION_UNDELIVERABLE_CHANNELS, so ON would be
|
|
763
|
+
# the same false claim one axis over. Flip them ON in the same change that ships
|
|
764
|
+
# their delivery (TODO(#3252)).
|
|
765
|
+
#
|
|
766
|
+
# This changes what a NEW row is seeded with, and nothing else. The JSONField
|
|
767
|
+
# default is materialized in full at row creation and every read path prefers the
|
|
768
|
+
# stored value, so a member who has already opened this page keeps whatever they
|
|
769
|
+
# have — deliberately. Force-clearing a stored cell would destroy a routing
|
|
770
|
+
# intent the whole surface exists to preserve, and it is no longer misleading
|
|
771
|
+
# either way: `channel_delivery` now labels the column on every response.
|
|
772
|
+
_T.COMMENT_MENTION: {
|
|
773
|
+
_C.IN_APP: True,
|
|
774
|
+
_C.EMAIL: True,
|
|
775
|
+
_C.SLACK: False,
|
|
776
|
+
_C.MOBILE_PUSH: False,
|
|
777
|
+
},
|
|
778
|
+
# TODO(#3016): every row below is dispatched by nothing. They default OFF on
|
|
779
|
+
# every channel so the settings matrix does not promise a delivery that never
|
|
780
|
+
# comes; the API reports them in event_delivery as false so a client can label
|
|
781
|
+
# them. Flip a row back to True in the same change that wires its dispatcher.
|
|
782
|
+
_T.TASK_ASSIGNED: _ALL_OFF(),
|
|
783
|
+
_T.TASK_OVERDUE: _ALL_OFF(),
|
|
784
|
+
_T.STATUS_CHANGE: _ALL_OFF(),
|
|
785
|
+
_T.BUDGET_ALERT: _ALL_OFF(),
|
|
786
|
+
_T.RISK_CREATED: _ALL_OFF(),
|
|
787
|
+
_T.MILESTONE_REACHED: _ALL_OFF(),
|
|
788
|
+
_T.SPRINT_START: _ALL_OFF(),
|
|
789
|
+
_T.SPRINT_END: _ALL_OFF(),
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _default_matrix() -> dict[str, dict[str, bool]]:
|
|
794
|
+
"""Deep-copy of the default matrix used as the JSONField default.
|
|
795
|
+
|
|
796
|
+
Django requires a callable; returning the literal would share mutable
|
|
797
|
+
state across rows.
|
|
798
|
+
"""
|
|
799
|
+
return {evt: dict(chans) for evt, chans in PROJECT_NOTIFICATION_DEFAULT_MATRIX.items()}
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
#: Every event type the ``event_delivery`` map reports on, as plain strings.
|
|
803
|
+
#:
|
|
804
|
+
#: The literal key set of :data:`PROJECT_NOTIFICATION_DEFAULT_MATRIX`, which is what
|
|
805
|
+
#: :func:`project_notification_event_delivery` iterates. Named here so the published
|
|
806
|
+
#: OpenAPI schema for ``event_delivery`` can enumerate the same keys from the same
|
|
807
|
+
#: source rather than restating them — a new event type then reaches the declared
|
|
808
|
+
#: contract in the edit that adds it, instead of leaving the schema quietly short a
|
|
809
|
+
#: property (#3396, #3399).
|
|
810
|
+
PROJECT_NOTIFICATION_DELIVERY_REPORTED_EVENTS: tuple[str, ...] = tuple(
|
|
811
|
+
sorted(str(event) for event in PROJECT_NOTIFICATION_DEFAULT_MATRIX)
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def project_notification_event_delivery() -> dict[str, bool]:
|
|
816
|
+
"""``{event_type: is a dispatcher wired}`` for every matrix row (#2904).
|
|
817
|
+
|
|
818
|
+
Derived from :data:`PROJECT_NOTIFICATION_DISPATCHED_EVENTS` rather than restating
|
|
819
|
+
the classification, so the coverage test that pins that set also pins this map.
|
|
820
|
+
|
|
821
|
+
Lives beside the two constants rather than in the view that returns it because
|
|
822
|
+
the serializer that *declares* the response shape needs the same key set, and a
|
|
823
|
+
second copy in the schema layer is exactly the drift #3399 asked to close.
|
|
824
|
+
"""
|
|
825
|
+
return {
|
|
826
|
+
event: event in PROJECT_NOTIFICATION_DISPATCHED_EVENTS
|
|
827
|
+
for event in PROJECT_NOTIFICATION_DELIVERY_REPORTED_EVENTS
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
#: Every channel the ``channel_delivery`` map reports on, as plain strings (#3378).
|
|
832
|
+
#:
|
|
833
|
+
#: The matrix's own column vocabulary — :class:`ProjectNotificationChannel` — which
|
|
834
|
+
#: is a different, closed set from the ADR-0049 registry keys: Enterprise registers
|
|
835
|
+
#: ``slack_dm`` / ``teams_dm`` / ``sms`` there and none of them is a column here.
|
|
836
|
+
#: Named so the published OpenAPI schema enumerates the same keys from the same
|
|
837
|
+
#: source rather than restating them.
|
|
838
|
+
PROJECT_NOTIFICATION_DELIVERY_REPORTED_CHANNELS: tuple[str, ...] = tuple(
|
|
839
|
+
sorted(str(channel.value) for channel in ProjectNotificationChannel)
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def project_notification_channel_delivery() -> dict[str, bool]:
|
|
844
|
+
"""``{channel: does TruePPM deliver on it}`` for every matrix column (#3378).
|
|
845
|
+
|
|
846
|
+
The per-column counterpart to :func:`project_notification_event_delivery`, and
|
|
847
|
+
the reason it exists is the same: the web client was hard-coding
|
|
848
|
+
``['slack', 'mobile_push']`` and its own comment said the copy should not
|
|
849
|
+
survive. Whether a channel delivers is server state — it changes in the release a
|
|
850
|
+
delivery path lands — so a client-side copy drifts the moment one does.
|
|
851
|
+
|
|
852
|
+
Derived from :data:`PROJECT_NOTIFICATION_DELIVERABLE_CHANNELS` rather than
|
|
853
|
+
restating the classification, so the coverage test that pins that set also pins
|
|
854
|
+
this map. The two maps are deliberately independent axes: a cell is live only
|
|
855
|
+
when its event is dispatched **and** its channel delivers, which is why a
|
|
856
|
+
per-row badge could never have covered this — ``comment_mention`` *is*
|
|
857
|
+
dispatched and its Slack cell still delivers nothing.
|
|
858
|
+
|
|
859
|
+
Server-global: the answer does not vary by user or by project. It rides on the
|
|
860
|
+
per-user preference document only so a client can label the grid in one render.
|
|
861
|
+
"""
|
|
862
|
+
return {
|
|
863
|
+
channel: channel in PROJECT_NOTIFICATION_DELIVERABLE_CHANNELS
|
|
864
|
+
for channel in PROJECT_NOTIFICATION_DELIVERY_REPORTED_CHANNELS
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class ProjectNotificationPreference(models.Model):
|
|
869
|
+
"""Per-(project, user) notification routing matrix and quiet-hours window.
|
|
870
|
+
|
|
871
|
+
One row per user per project. The matrix is a `{event_type: {channel: bool}}`
|
|
872
|
+
JSON document — the row count stays small (one per user per project) and
|
|
873
|
+
PATCH is a single UPDATE rather than a 36-row diff. Missing event/channel
|
|
874
|
+
keys fall back to the default matrix on read, so a stale row won't break
|
|
875
|
+
when the event set grows.
|
|
876
|
+
|
|
877
|
+
schema_version tracks the shape of ``matrix`` (ADR-0086 / ADR-0204, #1916 —
|
|
878
|
+
this surface was named by #522 but missed the #645 registry sweep). On read,
|
|
879
|
+
``ProjectNotificationPreferenceSerializer`` runs the payload through the
|
|
880
|
+
forward-migration registry (``schema_migrations.migrate_payload``) keyed on
|
|
881
|
+
this column, mirroring ``BoardSavedView.schema_version``. Rows created before
|
|
882
|
+
this field existed are backfilled to ``schema_version=1`` by the migration
|
|
883
|
+
default; every event type in ``PROJECT_NOTIFICATION_DEFAULT_MATRIX`` shipped
|
|
884
|
+
together at #522, so no existing row is actually missing a key and the v0
|
|
885
|
+
path is a no-op for them today — it exists so a *future* event-type addition
|
|
886
|
+
(or an externally-imported row that skipped ``validate`` at write time) has a
|
|
887
|
+
forward-migration path instead of relying solely on the view-layer
|
|
888
|
+
``_merge_matrix`` overlay.
|
|
889
|
+
"""
|
|
890
|
+
|
|
891
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
892
|
+
project = models.ForeignKey(
|
|
893
|
+
_PROJECT_MODEL,
|
|
894
|
+
on_delete=models.CASCADE,
|
|
895
|
+
related_name="notification_preferences_by_user",
|
|
896
|
+
)
|
|
897
|
+
user = models.ForeignKey(
|
|
898
|
+
settings.AUTH_USER_MODEL,
|
|
899
|
+
on_delete=models.CASCADE,
|
|
900
|
+
related_name="project_notification_preferences",
|
|
901
|
+
)
|
|
902
|
+
matrix = models.JSONField(default=_default_matrix)
|
|
903
|
+
schema_version = models.IntegerField(
|
|
904
|
+
default=1,
|
|
905
|
+
help_text="Matrix shape version; upgraded on read via the migration registry (ADR-0086).",
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
# Per-user-per-project kill-switch (#589). When True, all notification
|
|
909
|
+
# dispatch for this user on this project is suppressed regardless of the
|
|
910
|
+
# matrix — an opt-out path for members who haven't dialed in their
|
|
911
|
+
# routing yet. The matrix remains preserved so unpausing restores prior
|
|
912
|
+
# preferences exactly.
|
|
913
|
+
paused = models.BooleanField(default=False)
|
|
914
|
+
|
|
915
|
+
quiet_hours_enabled = models.BooleanField(default=True)
|
|
916
|
+
quiet_hours_from = models.TimeField(default=datetime.time(20, 0))
|
|
917
|
+
quiet_hours_until = models.TimeField(default=datetime.time(7, 0))
|
|
918
|
+
|
|
919
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
920
|
+
|
|
921
|
+
class Meta:
|
|
922
|
+
db_table = "notifications_project_preference"
|
|
923
|
+
constraints = [
|
|
924
|
+
models.UniqueConstraint(
|
|
925
|
+
fields=["project", "user"],
|
|
926
|
+
name="uq_projnotifpref_project_user",
|
|
927
|
+
),
|
|
928
|
+
]
|
|
929
|
+
indexes = [
|
|
930
|
+
models.Index(fields=["project", "user"], name="ix_projnotifpref_proj_user"),
|
|
931
|
+
]
|
|
932
|
+
|
|
933
|
+
def __str__(self) -> str:
|
|
934
|
+
return f"ProjectNotificationPreference(project={self.project_id}, user={self.user_id})"
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
class UserNotificationSettings(models.Model):
|
|
938
|
+
"""Per-user, account-wide notification settings — the Do-Not-Disturb switch
|
|
939
|
+
(#1707, ADR-0292).
|
|
940
|
+
|
|
941
|
+
One row per user, lazily created on first access
|
|
942
|
+
(:func:`services.get_or_create_notification_settings`) — no backfill on user
|
|
943
|
+
create, mirroring :func:`services.get_or_create_default_preferences`; the
|
|
944
|
+
absence of a row reads as DND off. Distinct in grain from the per-(event,
|
|
945
|
+
channel) :class:`NotificationPreference` matrix and the per-project
|
|
946
|
+
:class:`ProjectNotificationPreference`: this is a single account-wide switch
|
|
947
|
+
that silences transient channels (email/push) across every project, exactly
|
|
948
|
+
as quiet hours gate only transient channels — the durable in-app inbox row is
|
|
949
|
+
never silenced and :data:`DND_BYPASS_EVENTS` always deliver.
|
|
950
|
+
|
|
951
|
+
A plain ``Model`` (no ``server_version``): a personal installation preference,
|
|
952
|
+
never synced or broadcast, mirroring ``ProjectNotificationPreference`` and
|
|
953
|
+
``profiles.UserProfile``. Shaped to grow a scheduled quiet-hours window + a
|
|
954
|
+
per-user timezone later; only the DND switch ships now (ADR-0292 §4).
|
|
955
|
+
"""
|
|
956
|
+
|
|
957
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
958
|
+
user = models.OneToOneField(
|
|
959
|
+
settings.AUTH_USER_MODEL,
|
|
960
|
+
on_delete=models.CASCADE,
|
|
961
|
+
related_name="notification_settings",
|
|
962
|
+
)
|
|
963
|
+
# Account-wide Do-Not-Disturb. When True, transient channels (email/push) are
|
|
964
|
+
# suppressed for all non-bypass events; the durable in-app inbox row is
|
|
965
|
+
# unaffected and DND_BYPASS_EVENTS always deliver on every channel.
|
|
966
|
+
dnd_enabled = models.BooleanField(default=False)
|
|
967
|
+
# Weekly digest slot (ADR-0663, #2407) — the "per-user timezone + scheduled
|
|
968
|
+
# window" this model was documented from the start as being shaped to grow
|
|
969
|
+
# (ADR-0292 §4). ONE slot covers both digest event types: a schedule per event
|
|
970
|
+
# would double the settings surface to serve a case nobody asked for.
|
|
971
|
+
#
|
|
972
|
+
# The slot is only ever *read* by the Beat sweep; it is inert for a user who
|
|
973
|
+
# has not enabled a digest preference, so the defaults below are a starting
|
|
974
|
+
# point for the picker rather than an implied subscription.
|
|
975
|
+
digest_weekday = models.IntegerField(
|
|
976
|
+
default=6,
|
|
977
|
+
choices=[
|
|
978
|
+
(0, "Monday"),
|
|
979
|
+
(1, "Tuesday"),
|
|
980
|
+
(2, "Wednesday"),
|
|
981
|
+
(3, "Thursday"),
|
|
982
|
+
(4, "Friday"),
|
|
983
|
+
(5, "Saturday"),
|
|
984
|
+
(6, "Sunday"),
|
|
985
|
+
],
|
|
986
|
+
help_text="Day of week for scheduled digests, 0=Monday..6=Sunday (Python weekday()).",
|
|
987
|
+
)
|
|
988
|
+
digest_hour = models.IntegerField(
|
|
989
|
+
default=17,
|
|
990
|
+
help_text="Local hour (0-23) at which scheduled digests are sent.",
|
|
991
|
+
)
|
|
992
|
+
# Blank = fall back to settings.TIME_ZONE. Empty string is the single unset
|
|
993
|
+
# state (project DJ001 convention — no nullable CharFields).
|
|
994
|
+
digest_timezone = models.CharField(
|
|
995
|
+
max_length=64,
|
|
996
|
+
blank=True,
|
|
997
|
+
default="",
|
|
998
|
+
help_text="IANA timezone for the digest slot; blank uses the server timezone.",
|
|
999
|
+
)
|
|
1000
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
1001
|
+
|
|
1002
|
+
class Meta:
|
|
1003
|
+
db_table = "notifications_user_settings"
|
|
1004
|
+
verbose_name = "user notification settings"
|
|
1005
|
+
verbose_name_plural = "user notification settings"
|
|
1006
|
+
|
|
1007
|
+
def __str__(self) -> str:
|
|
1008
|
+
state = "on" if self.dnd_enabled else "off"
|
|
1009
|
+
return f"UserNotificationSettings(user={self.user_id}, dnd={state})"
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
class NotificationDigestRun(models.Model):
|
|
1013
|
+
"""Idempotency ledger for scheduled digests (ADR-0663, #2407).
|
|
1014
|
+
|
|
1015
|
+
One row per ``(user, event_type, period_start)``. The Beat sweep
|
|
1016
|
+
``get_or_create``s the row **before** building content and skips when
|
|
1017
|
+
``created`` is False, so a double-fire, a manual re-queue, or an overlapping
|
|
1018
|
+
sweep cannot mail a second copy of the same week's digest. The unique
|
|
1019
|
+
constraint is what makes that check atomic across concurrent workers — the
|
|
1020
|
+
Redis lock on the task is belt-and-braces, not the primary guard.
|
|
1021
|
+
|
|
1022
|
+
Creating the row *before* the content build means a mid-build crash leaves a
|
|
1023
|
+
"sent" marker for a digest that never arrived. That is deliberate and fails
|
|
1024
|
+
closed: a missing weekly digest is recoverable by logging in, whereas a
|
|
1025
|
+
duplicate erodes trust in a channel whose entire value is that it is quiet.
|
|
1026
|
+
|
|
1027
|
+
A plain ``Model`` (no ``server_version``): an operational record, never synced
|
|
1028
|
+
to mobile and never broadcast, mirroring :class:`UserNotificationSettings`.
|
|
1029
|
+
"""
|
|
1030
|
+
|
|
1031
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
1032
|
+
user = models.ForeignKey(
|
|
1033
|
+
settings.AUTH_USER_MODEL,
|
|
1034
|
+
on_delete=models.CASCADE,
|
|
1035
|
+
related_name="digest_runs",
|
|
1036
|
+
)
|
|
1037
|
+
event_type = models.CharField(max_length=64)
|
|
1038
|
+
# User-local week-start date of the send. Date (not datetime) so the ledger
|
|
1039
|
+
# grain matches the weekly cadence and a DST-shifted resend collides.
|
|
1040
|
+
period_start = models.DateField()
|
|
1041
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
1042
|
+
# How many inbox rows the send produced. 0 is a legitimate value — an honest
|
|
1043
|
+
# "nothing at risk this week" digest is still a send (ADR-0663 §4).
|
|
1044
|
+
notification_count = models.PositiveIntegerField(default=0)
|
|
1045
|
+
|
|
1046
|
+
class Meta:
|
|
1047
|
+
db_table = "notifications_digest_run"
|
|
1048
|
+
constraints = [
|
|
1049
|
+
models.UniqueConstraint(
|
|
1050
|
+
fields=["user", "event_type", "period_start"],
|
|
1051
|
+
name="uniq_digest_run_user_event_period",
|
|
1052
|
+
),
|
|
1053
|
+
]
|
|
1054
|
+
indexes = [
|
|
1055
|
+
# Nightly purge scans by age alone.
|
|
1056
|
+
models.Index(fields=["created_at"], name="digestrun_created_idx"),
|
|
1057
|
+
]
|
|
1058
|
+
|
|
1059
|
+
def __str__(self) -> str:
|
|
1060
|
+
return f"NotificationDigestRun(user={self.user_id}, {self.event_type}, {self.period_start})"
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
# ---------------------------------------------------------------------------
|
|
1064
|
+
# Workspace SMTP configuration (#712, ADR-0213)
|
|
1065
|
+
# ---------------------------------------------------------------------------
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
class EmailTransportMode(models.TextChoices):
|
|
1069
|
+
"""Outbound-mail transport for the workspace.
|
|
1070
|
+
|
|
1071
|
+
``CLOUD`` is the unconfigured default — the send path falls back to the
|
|
1072
|
+
process-global Django ``EMAIL_BACKEND`` (Helm env), so a fresh install
|
|
1073
|
+
behaves exactly as it did before #712. The other three build an SMTP
|
|
1074
|
+
connection from this row; ``SENDGRID`` / ``SES`` are SMTP relays with a
|
|
1075
|
+
provider-fixed host so 0.4 needs no new backend class (ADR-0213 §2).
|
|
1076
|
+
"""
|
|
1077
|
+
|
|
1078
|
+
CLOUD = "cloud", "TruePPM cloud (Django settings)"
|
|
1079
|
+
SMTP = "smtp", "Custom SMTP"
|
|
1080
|
+
SENDGRID = "sendgrid", "SendGrid"
|
|
1081
|
+
SES = "ses", "Amazon SES"
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
class EmailSecurity(models.TextChoices):
|
|
1085
|
+
"""SMTP connection security."""
|
|
1086
|
+
|
|
1087
|
+
NONE = "none", "None"
|
|
1088
|
+
TLS = "tls", "STARTTLS"
|
|
1089
|
+
SSL = "ssl", "SSL/TLS"
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
class WorkspaceEmailSettings(models.Model):
|
|
1093
|
+
"""Singleton writable SMTP configuration for the workspace (#712, ADR-0213).
|
|
1094
|
+
|
|
1095
|
+
Installation config, not a synced/board-scoped resource, so there is no
|
|
1096
|
+
``server_version``. Single-row is enforced by the unique ``singleton_key``
|
|
1097
|
+
(the ``workspace.Workspace`` / ADR-0081 precedent) and the row is lazily
|
|
1098
|
+
created on first access via :meth:`load` — no seed migration.
|
|
1099
|
+
|
|
1100
|
+
The SMTP password is encrypted at rest with Fernet (``password_ciphertext``)
|
|
1101
|
+
reusing ``apps.integrations.encryption`` and is **never** returned by the
|
|
1102
|
+
API — the serializer exposes only :attr:`password_is_set`. Decryption
|
|
1103
|
+
happens server-side only, at send time, in ``resolve_email_connection``.
|
|
1104
|
+
"""
|
|
1105
|
+
|
|
1106
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
1107
|
+
# Always 1 — the unique constraint makes a second row impossible.
|
|
1108
|
+
singleton_key = models.PositiveSmallIntegerField(default=1, editable=False, unique=True)
|
|
1109
|
+
|
|
1110
|
+
transport_mode = models.CharField(
|
|
1111
|
+
max_length=16,
|
|
1112
|
+
choices=EmailTransportMode.choices,
|
|
1113
|
+
default=EmailTransportMode.CLOUD,
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
# SMTP transport parameters (unused when transport_mode == CLOUD).
|
|
1117
|
+
host = models.CharField(max_length=255, blank=True, default="")
|
|
1118
|
+
port = models.PositiveIntegerField(default=587)
|
|
1119
|
+
security = models.CharField(
|
|
1120
|
+
max_length=8,
|
|
1121
|
+
choices=EmailSecurity.choices,
|
|
1122
|
+
default=EmailSecurity.TLS,
|
|
1123
|
+
)
|
|
1124
|
+
username = models.CharField(max_length=255, blank=True, default="")
|
|
1125
|
+
# Fernet-encrypted SMTP password / provider API key. Never serialized out.
|
|
1126
|
+
password_ciphertext = models.BinaryField(blank=True, default=b"")
|
|
1127
|
+
|
|
1128
|
+
# From identity + deliverability.
|
|
1129
|
+
from_name = models.CharField(max_length=255, blank=True, default="")
|
|
1130
|
+
from_email = models.EmailField(blank=True, default="")
|
|
1131
|
+
reply_to = models.EmailField(blank=True, default="")
|
|
1132
|
+
dkim_selector = models.CharField(max_length=63, blank=True, default="")
|
|
1133
|
+
|
|
1134
|
+
# Delivery limits. throttle_per_min == 0 means "no throttle".
|
|
1135
|
+
max_recipients = models.PositiveIntegerField(default=50)
|
|
1136
|
+
throttle_per_min = models.PositiveIntegerField(default=0)
|
|
1137
|
+
|
|
1138
|
+
# Bounce webhook — NOT WIRED. Removed from the serializer and the Email
|
|
1139
|
+
# settings UI in #2860: there is no bounce-ingest endpoint anywhere in this
|
|
1140
|
+
# app, and nothing has ever POSTed to this URL or read it back. It persisted,
|
|
1141
|
+
# validated through the SSRF egress guard, rendered back and was documented,
|
|
1142
|
+
# which is exactly what made it look like a working control.
|
|
1143
|
+
#
|
|
1144
|
+
# The column is RETAINED rather than dropped so no operator's stored value is
|
|
1145
|
+
# destroyed; #2872 builds the ingest endpoint and re-exposes the field. Do not
|
|
1146
|
+
# re-add it to the serializer before that lands.
|
|
1147
|
+
bounce_webhook_url = models.URLField(max_length=512, blank=True, default="")
|
|
1148
|
+
|
|
1149
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
1150
|
+
updated_by = models.ForeignKey(
|
|
1151
|
+
settings.AUTH_USER_MODEL,
|
|
1152
|
+
on_delete=models.SET_NULL,
|
|
1153
|
+
null=True,
|
|
1154
|
+
blank=True,
|
|
1155
|
+
related_name="+",
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
class Meta:
|
|
1159
|
+
db_table = "notifications_workspace_email_settings"
|
|
1160
|
+
verbose_name = "workspace email settings"
|
|
1161
|
+
verbose_name_plural = "workspace email settings"
|
|
1162
|
+
|
|
1163
|
+
def __str__(self) -> str:
|
|
1164
|
+
return f"WorkspaceEmailSettings(transport={self.transport_mode})"
|
|
1165
|
+
|
|
1166
|
+
@classmethod
|
|
1167
|
+
def load(cls) -> WorkspaceEmailSettings:
|
|
1168
|
+
"""Return the singleton, creating it on first access (no seed migration)."""
|
|
1169
|
+
obj, _ = cls.objects.get_or_create(singleton_key=1)
|
|
1170
|
+
return obj
|
|
1171
|
+
|
|
1172
|
+
@property
|
|
1173
|
+
def password_is_set(self) -> bool:
|
|
1174
|
+
"""Whether an encrypted SMTP password is stored (safe to expose)."""
|
|
1175
|
+
return bool(self.password_ciphertext)
|
|
1176
|
+
|
|
1177
|
+
def set_password(self, plaintext: str) -> None:
|
|
1178
|
+
"""Fernet-encrypt and store the SMTP password / provider API key.
|
|
1179
|
+
|
|
1180
|
+
An empty ``plaintext`` clears the stored secret. Callers that want the
|
|
1181
|
+
"blank means keep existing" semantics must guard before calling this —
|
|
1182
|
+
the serializer does (#712 rotate-vs-keep).
|
|
1183
|
+
"""
|
|
1184
|
+
from trueppm_api.apps.integrations.encryption import encrypt_secret
|
|
1185
|
+
|
|
1186
|
+
if not plaintext:
|
|
1187
|
+
self.password_ciphertext = b""
|
|
1188
|
+
return
|
|
1189
|
+
self.password_ciphertext = encrypt_secret(plaintext)
|
|
1190
|
+
|
|
1191
|
+
def get_password(self) -> str:
|
|
1192
|
+
"""Decrypt and return the stored password, or "" when none is set."""
|
|
1193
|
+
if not self.password_ciphertext:
|
|
1194
|
+
return ""
|
|
1195
|
+
from trueppm_api.apps.integrations.encryption import decrypt_secret
|
|
1196
|
+
|
|
1197
|
+
return decrypt_secret(bytes(self.password_ciphertext))
|