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,588 @@
|
|
|
1
|
+
"""RBAC models — ProjectMembership, ProgramMembership, and Role."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.db import models
|
|
9
|
+
from django.db.models.functions import Lower
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
|
|
12
|
+
from trueppm_api.apps.projects.models import VersionedModel
|
|
13
|
+
|
|
14
|
+
# The three program-scoped models below (ProgramMembership,
|
|
15
|
+
# ProgramUserDefinedMentionGroup, ExternalStakeholder) all point their `program`
|
|
16
|
+
# ForeignKey at the same target. Naming it once keeps the app-label reference in
|
|
17
|
+
# a single place. Django serializes the FK target to this resolved string, so
|
|
18
|
+
# using the constant produces no migration change.
|
|
19
|
+
_PROGRAM_MODEL = "projects.Program"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Role(models.IntegerChoices):
|
|
23
|
+
"""Project-scoped roles, ordered by privilege level (ADR-0072).
|
|
24
|
+
|
|
25
|
+
Ordinals are spaced in 100-unit bands so Enterprise can register custom roles
|
|
26
|
+
at intermediate values (e.g., a "Senior Scheduler" at 250) without forcing an
|
|
27
|
+
OSS renumber. The band-boundary contract:
|
|
28
|
+
|
|
29
|
+
- role >= Role.X (inequality / threshold) — "at least the X-band";
|
|
30
|
+
Enterprise custom roles at intermediate ordinals DO inherit this band's
|
|
31
|
+
capabilities.
|
|
32
|
+
- role == Role.X (singular-tier equality) — "specifically the OSS X tier";
|
|
33
|
+
custom roles do NOT silently absorb these matches. If Enterprise wants
|
|
34
|
+
override semantics, it goes through the slot-registration pattern
|
|
35
|
+
(ADR-0029), not OSS code changes.
|
|
36
|
+
|
|
37
|
+
Code name │ Ordinal │ Issue #11 label │ Reserved band for Enterprise
|
|
38
|
+
───────────┼─────────┼──────────────────┼─────────────────────────────────
|
|
39
|
+
VIEWER │ 1 │ Viewer │ the floor; 0 is deliberately unused
|
|
40
|
+
│ 2–99 │ │ read-augmented roles (e.g. Auditor)
|
|
41
|
+
MEMBER │ 100 │ Team Member │ edit own assigned tasks
|
|
42
|
+
│ 101–199 │ │ contributor extensions
|
|
43
|
+
SCHEDULER │ 200 │ Resource Manager │ assign resources; no task edit
|
|
44
|
+
│ 201–299 │ │ resource-management extensions
|
|
45
|
+
ADMIN │ 300 │ Project Manager │ full task/dep edit; create baseline
|
|
46
|
+
│ 301–399 │ │ project-lead extensions
|
|
47
|
+
OWNER │ 400 │ Project Admin │ delete project; manage membership
|
|
48
|
+
│ 401+ │ (RESERVED) │ no role above Owner; OSS contract
|
|
49
|
+
───────────┴─────────┴──────────────────┴─────────────────────────────────
|
|
50
|
+
|
|
51
|
+
VIEWER is 1, not 0 (#2489). The ordinal is client-visible — it ships in
|
|
52
|
+
membership payloads, invites, and MCP reads — and JavaScript treats 0 as
|
|
53
|
+
falsy, so a single ``role || DEFAULT`` on any consumer silently promotes a
|
|
54
|
+
Viewer to whatever the default is. Every role ordinal is now truthy, which
|
|
55
|
+
removes the failure mode rather than relying on every consumer to remember
|
|
56
|
+
``??``. It costs one slot from the read-augmented band (2–99, still 98).
|
|
57
|
+
|
|
58
|
+
"No membership" is expressed as ``None`` (a distinct type), never as an
|
|
59
|
+
ordinal, so no sentinel value below VIEWER is needed or reserved.
|
|
60
|
+
|
|
61
|
+
OWNER is kept as the code name (not renamed to PROJECT_ADMIN) because it
|
|
62
|
+
carries the last-Owner guard invariant throughout the codebase. The human-
|
|
63
|
+
readable label is "Project Admin" for API consumers.
|
|
64
|
+
|
|
65
|
+
The labels above are **project-scoped**. A program names the same two top
|
|
66
|
+
ordinals differently — see :data:`PROGRAM_ROLE_LABELS` below, which is the
|
|
67
|
+
single source of that vocabulary; any new member added here must be added
|
|
68
|
+
there too or the mapping test fails.
|
|
69
|
+
|
|
70
|
+
NEVER compare against a raw integer literal (e.g. ``if role < 1``) — always
|
|
71
|
+
use the symbolic name (``if role < Role.MEMBER``) so the comparison stays
|
|
72
|
+
correct if ordinals change.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
VIEWER = 1, "Viewer"
|
|
76
|
+
MEMBER = 100, "Team Member"
|
|
77
|
+
SCHEDULER = 200, "Resource Manager"
|
|
78
|
+
ADMIN = 300, "Project Manager"
|
|
79
|
+
OWNER = 400, "Project Admin"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Program-context labels for the shared ``Role`` enum (#1794, #3503). The enum
|
|
83
|
+
# labels are project-scoped ("Project Admin", "Project Manager"); a program
|
|
84
|
+
# surface must read the role as it applies to the *program*, not to some
|
|
85
|
+
# project. Every role the enum defines is mapped so a future enum value can't
|
|
86
|
+
# silently fall back to a project label without a matching test failing. The
|
|
87
|
+
# project-side ``Role.label`` values are intentionally left unchanged.
|
|
88
|
+
#
|
|
89
|
+
# This lives beside ``Role`` rather than in a serializer module because two
|
|
90
|
+
# apps read it — ``access`` (membership rows) and ``projects`` (the program
|
|
91
|
+
# card) — and one rule rendered twice must have one definition (ADR-0133).
|
|
92
|
+
PROGRAM_ROLE_LABELS: dict[Role, str] = {
|
|
93
|
+
Role.VIEWER: "Viewer",
|
|
94
|
+
Role.MEMBER: "Team Member",
|
|
95
|
+
Role.SCHEDULER: "Resource Manager",
|
|
96
|
+
Role.ADMIN: "Program Manager",
|
|
97
|
+
Role.OWNER: "Program Admin",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def program_role_label(role: int | None) -> str | None:
|
|
102
|
+
"""Program-context display label for a role ordinal.
|
|
103
|
+
|
|
104
|
+
Returns ``None`` for ``None`` (no membership) and for any ordinal the
|
|
105
|
+
``Role`` enum does not define. That second branch is not defensive padding:
|
|
106
|
+
ADR-0072 reserves the 2-99, 101-199, 201-299 and 301-399 bands for
|
|
107
|
+
Enterprise custom roles, ``ProgramMembership.role`` is a plain
|
|
108
|
+
``IntegerField`` whose ``choices`` PostgreSQL does not enforce, and a bare
|
|
109
|
+
``Role(350)`` raises ``ValueError`` — which DRF does not convert, so it
|
|
110
|
+
500s the WHOLE response rather than just this field (the failure mode
|
|
111
|
+
#3419 fixed on the project side). ``None`` is honest: the OSS edition
|
|
112
|
+
genuinely has no name for that ordinal, and every caller decides for itself
|
|
113
|
+
how to render the gap.
|
|
114
|
+
"""
|
|
115
|
+
if role is None or role not in Role.values:
|
|
116
|
+
return None
|
|
117
|
+
# ``.get``, not ``[]``: the map is exhaustive over ``Role`` and
|
|
118
|
+
# ``test_every_role_has_a_program_label`` is what keeps it that way, but a
|
|
119
|
+
# ``KeyError`` raised here would 500 the whole response — reintroducing, for a
|
|
120
|
+
# new-enum-member input, exactly the failure shape the guard above removes.
|
|
121
|
+
return PROGRAM_ROLE_LABELS.get(Role(role))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ProjectMembership(VersionedModel):
|
|
125
|
+
"""Through table linking a user to a project with a specific role.
|
|
126
|
+
|
|
127
|
+
Deliberately kept as a standalone model (not a ManyToManyField through=)
|
|
128
|
+
so that it participates in the offline sync protocol via VersionedModel
|
|
129
|
+
and can be queried directly in permission checks without a join through Project.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
project = models.ForeignKey(
|
|
133
|
+
"projects.Project",
|
|
134
|
+
on_delete=models.PROTECT,
|
|
135
|
+
related_name="memberships",
|
|
136
|
+
)
|
|
137
|
+
user = models.ForeignKey(
|
|
138
|
+
settings.AUTH_USER_MODEL,
|
|
139
|
+
on_delete=models.CASCADE,
|
|
140
|
+
related_name="memberships",
|
|
141
|
+
)
|
|
142
|
+
role = models.IntegerField(choices=Role.choices)
|
|
143
|
+
# Per-project access evidence (#590): the OSS surface needs a minimum-viable
|
|
144
|
+
# "who has access and since when" answer for compliance questionnaires.
|
|
145
|
+
# VersionedModel deliberately omits created_at/updated_at (sync uses
|
|
146
|
+
# server_version), so these are explicit columns rather than inherited.
|
|
147
|
+
# Scope of the evidence (#3410): joined_at is the *first* join. Re-adding a
|
|
148
|
+
# revoked member revives their original row and keeps this date, so the span
|
|
149
|
+
# it describes may contain revoked intervals — use reinstated_at below as the
|
|
150
|
+
# discriminator between "joined once, never left" and "was away and came
|
|
151
|
+
# back". Do not present joined_at alone as proof of uninterrupted access.
|
|
152
|
+
# joined_at uses default=timezone.now (not auto_now_add) so the AddField
|
|
153
|
+
# migration backfills existing rows non-interactively at migration time.
|
|
154
|
+
joined_at = models.DateTimeField(default=timezone.now, editable=False)
|
|
155
|
+
# NULL means the role has never changed since the member joined; it is
|
|
156
|
+
# stamped with timezone.now() only on an actual role change (the viewset
|
|
157
|
+
# partial_update and the ownership-transfer service). The UI shows the
|
|
158
|
+
# "role changed" line only when this is set.
|
|
159
|
+
role_changed_at = models.DateTimeField(null=True, blank=True, editable=False)
|
|
160
|
+
# Per-project reinstatement evidence (#3436). NULL means this membership has
|
|
161
|
+
# never been revoked and re-added — a fresh add leaves it null. Stamped with
|
|
162
|
+
# timezone.now() only in _revive_revoked_membership, each time a revoked row
|
|
163
|
+
# is revived (a second revive advances it, it does not accumulate history).
|
|
164
|
+
# This is the durable server-side fact #3410 shipped without: neither model
|
|
165
|
+
# carries HistoricalRecords, deleted_version is cleared on revive, and the
|
|
166
|
+
# access app writes no AuditEvent, so before this field there was no trace of
|
|
167
|
+
# a revocation surviving past TRUEPPM_BOARD_EVENT_RETENTION_HOURS (24h
|
|
168
|
+
# default). A machine caller reading joined_at alone cannot tell a
|
|
169
|
+
# never-lapsed membership from a revived one; reinstated_at is the
|
|
170
|
+
# discriminator.
|
|
171
|
+
reinstated_at = models.DateTimeField(null=True, blank=True, editable=False)
|
|
172
|
+
# Set when this membership was materialized by a workspace Group→project
|
|
173
|
+
# cascade (ADR-0087 §5) rather than a direct invite. A direct grant
|
|
174
|
+
# (source_group IS NULL) always wins: group reconciliation never alters or
|
|
175
|
+
# revokes a direct membership, and only removes rows it created itself.
|
|
176
|
+
source_group = models.ForeignKey(
|
|
177
|
+
"workspace.Group",
|
|
178
|
+
on_delete=models.SET_NULL,
|
|
179
|
+
null=True,
|
|
180
|
+
blank=True,
|
|
181
|
+
related_name="granted_memberships",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
class Meta:
|
|
185
|
+
db_table = "access_project_membership"
|
|
186
|
+
constraints = [
|
|
187
|
+
models.UniqueConstraint(
|
|
188
|
+
fields=["project", "user"], name="uniq_project_membership_project_user"
|
|
189
|
+
),
|
|
190
|
+
]
|
|
191
|
+
indexes = [
|
|
192
|
+
# Sync delta pull: WHERE project_id = X AND server_version > since (#810).
|
|
193
|
+
models.Index(fields=["project", "server_version"], name="pm_proj_serverver_idx"),
|
|
194
|
+
models.Index(fields=["project", "sync_seq"], name="pm_proj_syncseq_idx"),
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def live(cls) -> models.QuerySet[ProjectMembership]:
|
|
199
|
+
"""Membership rows that have not been revoked — the soft-delete floor, in one place.
|
|
200
|
+
|
|
201
|
+
It floors the *membership* only. A row on a soft-deleted **project** passes this
|
|
202
|
+
and confers nothing, so a caller that does not already scope to a live project
|
|
203
|
+
needs ``project__is_deleted=False`` as well (as
|
|
204
|
+
``notifications.digests.build_resource_overallocation_digest`` does). All current
|
|
205
|
+
callers supply their own project scope.
|
|
206
|
+
|
|
207
|
+
The uniqueness constraint above is **unconditional**: revoking access soft-deletes
|
|
208
|
+
the row rather than removing it, and re-adding the member revives that same row
|
|
209
|
+
(#3410). So a ``(project, user)`` lookup that omits ``is_deleted`` resolves a
|
|
210
|
+
*revoked* member to their old role and reads as if they were still present — the
|
|
211
|
+
defect class fixed on the write gates in #3386 and on the read paths in #3411.
|
|
212
|
+
|
|
213
|
+
**Scope, stated narrowly on purpose.** This is a convenience for a *direct*
|
|
214
|
+
``ProjectMembership`` lookup; it is not, and does not claim to be, the only place
|
|
215
|
+
the predicate lives. Being a classmethod it cannot compose through a related
|
|
216
|
+
manager or a join, so the ~30 ``memberships__is_deleted=False`` join filters and
|
|
217
|
+
the ``obj.memberships.filter(is_deleted=False)`` reads elsewhere necessarily
|
|
218
|
+
restate it and are correct as they stand. Making it a queryset method on a custom
|
|
219
|
+
manager would let ``project.memberships.live()`` and ``Prefetch`` compose too;
|
|
220
|
+
that is the natural next step and is deliberately not taken in this fix.
|
|
221
|
+
|
|
222
|
+
Since #3458 a new direct read that omits the floor **is** caught:
|
|
223
|
+
``scripts/check-membership-live-floor.py`` (``make pre-push`` and the
|
|
224
|
+
``api:membership-live-floor`` CI job) rejects an unfloored read of either
|
|
225
|
+
membership manager outside a short allowlist. It does not require the read to go
|
|
226
|
+
through *this* method — an inline ``is_deleted=False`` satisfies it — so this
|
|
227
|
+
stays a shared definition to prefer rather than an invariant. Note what the gate
|
|
228
|
+
cannot see: a defect *inside* an allowlisted module, and a join filter, which is
|
|
229
|
+
why the paragraph above still matters.
|
|
230
|
+
|
|
231
|
+
Some direct reads must see revoked rows and stay on ``objects`` by design: the
|
|
232
|
+
``pre_save`` receiver in :mod:`~trueppm_api.apps.access.signals`, which reads the
|
|
233
|
+
prior row precisely to detect the revocation transition; the add-member path in
|
|
234
|
+
:mod:`~trueppm_api.apps.access.views`, which has to find a revoked row to revive
|
|
235
|
+
it against the unconditional constraint, and to narrow its ``IntegrityError``
|
|
236
|
+
against that same constraint; the sync delta, which ships tombstones by protocol
|
|
237
|
+
so a client can learn the membership went away; and the workspace group-cascade
|
|
238
|
+
reconciliation. Writes (``create``, ``get_or_create``, seeds, data migrations)
|
|
239
|
+
are not reads and are equally out of scope.
|
|
240
|
+
"""
|
|
241
|
+
return cls.objects.filter(is_deleted=False)
|
|
242
|
+
|
|
243
|
+
def __str__(self) -> str:
|
|
244
|
+
return f"{self.user} — {self.project} ({Role(self.role).label})"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ProgramMembership(VersionedModel):
|
|
248
|
+
"""Through table linking a user to a program with a specific role (ADR-0070).
|
|
249
|
+
|
|
250
|
+
Mirrors :class:`ProjectMembership` exactly — standalone model (not M2M
|
|
251
|
+
``through=``) so it participates in the offline sync protocol and supports
|
|
252
|
+
direct permission checks without joining through ``Program``.
|
|
253
|
+
|
|
254
|
+
Program membership controls access to program-level views (backlog, projects
|
|
255
|
+
list, members). It does **not** automatically grant or modify project-level
|
|
256
|
+
access — a user must be invited to each project separately. This is the
|
|
257
|
+
deliberate "explicit grants only" boundary called out in ADR-0070 §RBAC.
|
|
258
|
+
|
|
259
|
+
Uses the same :class:`Role` enum as :class:`ProjectMembership`; the role
|
|
260
|
+
ordinals share semantics: VIEWER reads, MEMBER edits, SCHEDULER assigns,
|
|
261
|
+
ADMIN manages member/projects, OWNER deletes program.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
program = models.ForeignKey(
|
|
265
|
+
_PROGRAM_MODEL,
|
|
266
|
+
on_delete=models.PROTECT,
|
|
267
|
+
related_name="memberships",
|
|
268
|
+
)
|
|
269
|
+
user = models.ForeignKey(
|
|
270
|
+
settings.AUTH_USER_MODEL,
|
|
271
|
+
on_delete=models.CASCADE,
|
|
272
|
+
related_name="program_memberships",
|
|
273
|
+
)
|
|
274
|
+
role = models.IntegerField(choices=Role.choices)
|
|
275
|
+
# Per-program access evidence (#878): mirrors ProjectMembership exactly so
|
|
276
|
+
# ADR-0070's "mirrors ProjectMembership" claim holds and the program members
|
|
277
|
+
# view can answer "who has access and since when". VersionedModel omits
|
|
278
|
+
# created_at/updated_at (sync uses server_version), so these are explicit.
|
|
279
|
+
# The same scope caveat applies as on the project side (#3410): joined_at is
|
|
280
|
+
# the first join and survives a revoke-then-re-add, so it is not proof of
|
|
281
|
+
# uninterrupted access — reinstated_at below is the discriminator.
|
|
282
|
+
# joined_at uses default=timezone.now (not auto_now_add) so the AddField
|
|
283
|
+
# migration backfills existing rows non-interactively at migration time.
|
|
284
|
+
joined_at = models.DateTimeField(default=timezone.now, editable=False)
|
|
285
|
+
# NULL means the role has never changed since the member joined; it is
|
|
286
|
+
# stamped with timezone.now() only on an actual role change (the viewset
|
|
287
|
+
# partial_update and transfer_program_sponsorship).
|
|
288
|
+
role_changed_at = models.DateTimeField(null=True, blank=True, editable=False)
|
|
289
|
+
# Per-program reinstatement evidence (#3436) — mirrors ProjectMembership's
|
|
290
|
+
# field exactly. NULL means never revoked and re-added; stamped with
|
|
291
|
+
# timezone.now() each time _revive_revoked_membership revives this row.
|
|
292
|
+
reinstated_at = models.DateTimeField(null=True, blank=True, editable=False)
|
|
293
|
+
# Freeform functional-role label (#565), e.g. "Product Owner" / "Tech Lead" /
|
|
294
|
+
# "Scrum Master" — distinct from, and orthogonal to, the access ``role`` enum
|
|
295
|
+
# above. It is purely descriptive: not enforced anywhere, it anchors the
|
|
296
|
+
# PO-vs-PM sovereignty signals #501 will surface (a PM-labeled member dragging
|
|
297
|
+
# a story into an active sprint). Empty string is the single "unset" state
|
|
298
|
+
# (no nullable string, per project DJ001 convention); the serializer strips a
|
|
299
|
+
# whitespace-only submission back to "".
|
|
300
|
+
role_title = models.CharField(max_length=50, blank=True, default="")
|
|
301
|
+
|
|
302
|
+
class Meta:
|
|
303
|
+
db_table = "access_program_membership"
|
|
304
|
+
constraints = [
|
|
305
|
+
models.UniqueConstraint(
|
|
306
|
+
fields=["program", "user"], name="uniq_program_membership_program_user"
|
|
307
|
+
),
|
|
308
|
+
]
|
|
309
|
+
indexes = [
|
|
310
|
+
# Sync delta pull: WHERE program_id = X AND server_version > since
|
|
311
|
+
# (ADR-0070 §Sync). Mirrors ProjectMembership's pm_proj_serverver_idx
|
|
312
|
+
# so the standard offline-sync query pattern is index-backed on the
|
|
313
|
+
# program side too — introduced with the #561 user-scoped program
|
|
314
|
+
# sync endpoint.
|
|
315
|
+
models.Index(fields=["program", "server_version"], name="progm_serverver_idx"),
|
|
316
|
+
models.Index(fields=["program", "sync_seq"], name="progm_syncseq_idx"),
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def live(cls) -> models.QuerySet[ProgramMembership]:
|
|
321
|
+
"""Program membership rows that have not been revoked — the soft-delete floor.
|
|
322
|
+
|
|
323
|
+
The program-axis twin of :meth:`ProjectMembership.live`, added in #3458. The
|
|
324
|
+
class docstring above says this model "mirrors ``ProjectMembership`` exactly";
|
|
325
|
+
until this method existed that claim was false in the one place it mattered
|
|
326
|
+
most, and every program-axis read restated its own ``is_deleted=False`` or
|
|
327
|
+
forgot to. Two of the five known instances of the class were program-axis
|
|
328
|
+
(#3456, the weekly program-health digest; #3457, the seed exporter), and both
|
|
329
|
+
were found by a gate sweep on an unrelated MR rather than by anything that runs
|
|
330
|
+
on a branch.
|
|
331
|
+
|
|
332
|
+
It floors the *membership* only. A row on a soft-deleted **program** passes this
|
|
333
|
+
and confers nothing, so a caller that does not already scope to a live program
|
|
334
|
+
needs ``program__is_deleted=False`` as well (as
|
|
335
|
+
``notifications.digests.build_program_health_digest`` does). Same division as on
|
|
336
|
+
the project side.
|
|
337
|
+
|
|
338
|
+
The mechanism is the same too: ``uniq_program_membership_program_user`` is
|
|
339
|
+
**unconditional**, so revoking access soft-deletes the row rather than removing
|
|
340
|
+
it and re-adding the member revives that same row (#3410). A ``(program, user)``
|
|
341
|
+
lookup that omits ``is_deleted`` therefore resolves a *revoked* member to their
|
|
342
|
+
old role and reads as if they were still present.
|
|
343
|
+
|
|
344
|
+
**Scope, stated as narrowly as the project-side twin.** This is a convenience
|
|
345
|
+
for a *direct* ``ProgramMembership`` lookup. Being a classmethod it cannot
|
|
346
|
+
compose through a related manager or a join, so ``memberships__is_deleted=False``
|
|
347
|
+
join filters elsewhere necessarily restate the predicate and are correct as they
|
|
348
|
+
stand. Migrating every inline filter to it was deliberately **not** done in
|
|
349
|
+
#3458: the project axis carries 95 correctly-floored reads and only 4 of them go
|
|
350
|
+
through ``ProjectMembership.live()``, so "mirrors exactly" means the helper
|
|
351
|
+
exists and is preferred, not that every call site was rewritten.
|
|
352
|
+
|
|
353
|
+
What is new in #3458 and is *not* on the project side is that the floor is now
|
|
354
|
+
checked rather than merely preferred: ``scripts/check-membership-live-floor.py``
|
|
355
|
+
(``make pre-push`` and the ``api:membership-live-floor`` CI job) fails any
|
|
356
|
+
unfloored read of either membership manager outside a short allowlist of modules
|
|
357
|
+
that must see revoked rows. The reads that must stay unfloored are named there,
|
|
358
|
+
with a reason each, rather than restated here.
|
|
359
|
+
"""
|
|
360
|
+
return cls.objects.filter(is_deleted=False)
|
|
361
|
+
|
|
362
|
+
def __str__(self) -> str:
|
|
363
|
+
# Program vocabulary, not ``Role.label`` — this row is a membership of a
|
|
364
|
+
# *program*, and the admin/log line that renders it should not call the
|
|
365
|
+
# holder a "Project Admin" (#3503). Falls back to the raw ordinal for an
|
|
366
|
+
# Enterprise custom-band role the OSS enum cannot name (ADR-0072).
|
|
367
|
+
label = program_role_label(self.role) or f"Role {self.role}"
|
|
368
|
+
return f"{self.user} — {self.program} ({label})"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class UserDefinedMentionGroup(VersionedModel):
|
|
372
|
+
"""Admin-curated, project-scoped ``@mention`` group (ADR-0212, #515).
|
|
373
|
+
|
|
374
|
+
Complements the RBAC-derived auto-groups (``@admins``, ``@scrum-team``, …)
|
|
375
|
+
resolved in ``access/groups.py`` with workflow-shaped groupings a PM defines
|
|
376
|
+
by hand — e.g. ``@subcontractors``, ``@inspectors``, ``@team-private`` — that
|
|
377
|
+
do not map onto a role band.
|
|
378
|
+
|
|
379
|
+
``name`` is the mention key *without* the leading ``@`` and is bounded to 32
|
|
380
|
+
chars so it fits ``notifications.Mention.mentioned_group_key``. It is
|
|
381
|
+
case-insensitively unique per project and may not collide with an auto-group
|
|
382
|
+
key (validated in the serializer against ``ALL_AUTO_GROUP_KEYS`` — project-
|
|
383
|
+
and program-scoped keys alike).
|
|
384
|
+
|
|
385
|
+
Mention resolution snapshots the member list at write time — the same
|
|
386
|
+
semantics as the auto-group resolver — so members added after a mention are
|
|
387
|
+
not retroactively notified and departed members are not re-pinged.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
project = models.ForeignKey(
|
|
391
|
+
"projects.Project",
|
|
392
|
+
on_delete=models.PROTECT,
|
|
393
|
+
related_name="mention_groups",
|
|
394
|
+
)
|
|
395
|
+
# The mention key without the leading @ (e.g. "subcontractors").
|
|
396
|
+
name = models.CharField(max_length=32)
|
|
397
|
+
# Optional one-line purpose shown in the manager UI. DJ001: "" not NULL.
|
|
398
|
+
description = models.CharField(max_length=140, blank=True, default="")
|
|
399
|
+
# Per-group email default (ADR-0212 §5). Default OFF preserves the un-opted-
|
|
400
|
+
# email hard-NO (ADR-0075 V2); the group manager flips it on.
|
|
401
|
+
email_default_on = models.BooleanField(default=False)
|
|
402
|
+
created_by = models.ForeignKey(
|
|
403
|
+
settings.AUTH_USER_MODEL,
|
|
404
|
+
on_delete=models.SET_NULL,
|
|
405
|
+
null=True,
|
|
406
|
+
blank=True,
|
|
407
|
+
related_name="created_mention_groups",
|
|
408
|
+
)
|
|
409
|
+
# Curated members. Plain M2M (no sync stream): resolution is server-side at
|
|
410
|
+
# comment-write time, so offline clients never resolve groups.
|
|
411
|
+
members = models.ManyToManyField(
|
|
412
|
+
settings.AUTH_USER_MODEL,
|
|
413
|
+
related_name="mention_groups",
|
|
414
|
+
blank=True,
|
|
415
|
+
)
|
|
416
|
+
# Per-user override / per-group mute (ADR-0212 §5). A member who mutes a group
|
|
417
|
+
# receives neither in-app nor email for that group's mentions; a direct
|
|
418
|
+
# @user mention still reaches them (mute is group-scoped).
|
|
419
|
+
muted_by = models.ManyToManyField(
|
|
420
|
+
settings.AUTH_USER_MODEL,
|
|
421
|
+
related_name="muted_mention_groups",
|
|
422
|
+
blank=True,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
class Meta:
|
|
426
|
+
db_table = "access_user_defined_mention_group"
|
|
427
|
+
constraints = [
|
|
428
|
+
# Case-insensitive project-unique name, enforced only across LIVE rows
|
|
429
|
+
# (condition mirrors the serializer's is_deleted=False uniqueness check).
|
|
430
|
+
# A soft-deleted group therefore frees its name for reuse rather than
|
|
431
|
+
# reserving it — without the condition, re-creating a name after delete
|
|
432
|
+
# would pass serializer validation but hit the DB constraint as a 500.
|
|
433
|
+
models.UniqueConstraint(
|
|
434
|
+
"project",
|
|
435
|
+
Lower("name"),
|
|
436
|
+
condition=models.Q(is_deleted=False),
|
|
437
|
+
name="uniq_mention_group_project_name_ci",
|
|
438
|
+
),
|
|
439
|
+
]
|
|
440
|
+
indexes = [
|
|
441
|
+
# Sync delta pull: WHERE project_id = X AND server_version > since.
|
|
442
|
+
models.Index(fields=["project", "server_version"], name="udmg_proj_serverver_idx"),
|
|
443
|
+
models.Index(fields=["project", "sync_seq"], name="udmg_proj_syncseq_idx"),
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
def __str__(self) -> str:
|
|
447
|
+
return f"@{self.name} ({self.project_id})"
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class ProgramUserDefinedMentionGroup(VersionedModel):
|
|
451
|
+
"""Owner-curated, program-scoped ``@mention`` group (ADR-0248, #516).
|
|
452
|
+
|
|
453
|
+
The program-scoped parallel of :class:`UserDefinedMentionGroup`: a program
|
|
454
|
+
manager hand-curates a collection of members drawn from across the program's
|
|
455
|
+
projects — e.g. ``@program-tech-leads``, ``@program-vendor-x`` — that does not
|
|
456
|
+
map onto a role band (the role-banded cases are the ADR-0240 auto-groups).
|
|
457
|
+
|
|
458
|
+
Mentioned as a plain ``@name`` (no ``program-`` prefix — that prefix is
|
|
459
|
+
reserved for the ADR-0240 auto-groups) from a comment on any task in a project
|
|
460
|
+
of the program. Resolution precedence is member → project group → program
|
|
461
|
+
group, so a program group is the widest, least-specific match (ADR-0248 §4).
|
|
462
|
+
|
|
463
|
+
``members`` are selectable across *all* projects in the program (the union of
|
|
464
|
+
``ProjectMembership``, matching ADR-0240's program-membership semantics), and
|
|
465
|
+
resolution snapshots the member list at write time — members added after a
|
|
466
|
+
mention are not retroactively notified, departed members are not re-pinged.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
program = models.ForeignKey(
|
|
470
|
+
_PROGRAM_MODEL,
|
|
471
|
+
on_delete=models.PROTECT,
|
|
472
|
+
related_name="mention_groups",
|
|
473
|
+
)
|
|
474
|
+
# The mention key without the leading @ (e.g. "tech-leads").
|
|
475
|
+
name = models.CharField(max_length=32)
|
|
476
|
+
# Optional one-line purpose shown in the manager UI. DJ001: "" not NULL.
|
|
477
|
+
description = models.CharField(max_length=140, blank=True, default="")
|
|
478
|
+
# Per-group email default (ADR-0248 §1, mirrors ADR-0212 §5). Default OFF
|
|
479
|
+
# preserves the un-opted-email hard-NO (ADR-0075 V2); the manager flips it on.
|
|
480
|
+
email_default_on = models.BooleanField(default=False)
|
|
481
|
+
created_by = models.ForeignKey(
|
|
482
|
+
settings.AUTH_USER_MODEL,
|
|
483
|
+
on_delete=models.SET_NULL,
|
|
484
|
+
null=True,
|
|
485
|
+
blank=True,
|
|
486
|
+
related_name="created_program_mention_groups",
|
|
487
|
+
)
|
|
488
|
+
# Curated members. Plain M2M (no sync stream): resolution is server-side at
|
|
489
|
+
# comment-write time, so offline clients never resolve groups.
|
|
490
|
+
members = models.ManyToManyField(
|
|
491
|
+
settings.AUTH_USER_MODEL,
|
|
492
|
+
related_name="program_mention_groups",
|
|
493
|
+
blank=True,
|
|
494
|
+
)
|
|
495
|
+
# Per-user override / per-group mute (ADR-0248 §1). A member who mutes a group
|
|
496
|
+
# receives neither in-app nor email for that group's mentions; a direct
|
|
497
|
+
# @user mention still reaches them (mute is group-scoped).
|
|
498
|
+
muted_by = models.ManyToManyField(
|
|
499
|
+
settings.AUTH_USER_MODEL,
|
|
500
|
+
related_name="muted_program_mention_groups",
|
|
501
|
+
blank=True,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
class Meta:
|
|
505
|
+
db_table = "access_program_user_defined_mention_group"
|
|
506
|
+
constraints = [
|
|
507
|
+
# Case-insensitive program-unique name across LIVE rows only — mirrors
|
|
508
|
+
# the ADR-0212 project constraint so a soft-deleted group frees its name.
|
|
509
|
+
models.UniqueConstraint(
|
|
510
|
+
"program",
|
|
511
|
+
Lower("name"),
|
|
512
|
+
condition=models.Q(is_deleted=False),
|
|
513
|
+
name="uniq_program_mention_group_program_name_ci",
|
|
514
|
+
),
|
|
515
|
+
]
|
|
516
|
+
indexes = [
|
|
517
|
+
# Sync delta pull: WHERE program_id = X AND server_version > since.
|
|
518
|
+
models.Index(fields=["program", "server_version"], name="pudmg_prog_serverver_idx"),
|
|
519
|
+
models.Index(fields=["program", "sync_seq"], name="pudmg_prog_syncseq_idx"),
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
def __str__(self) -> str:
|
|
523
|
+
return f"@{self.name} ({self.program_id})"
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class ExternalStakeholder(models.Model):
|
|
527
|
+
"""A non-account external stakeholder registered against a program (#1658, ADR-0264).
|
|
528
|
+
|
|
529
|
+
A snapshot-resolved registry of people who are *not* TruePPM users — client
|
|
530
|
+
sponsors, vendor contacts, external reviewers — whom a program manager wants
|
|
531
|
+
reachable through the ``@program-stakeholders`` mention fan-out alongside the
|
|
532
|
+
program's Viewer-role members. The resolver
|
|
533
|
+
(:func:`trueppm_api.apps.access.groups.resolve_external_stakeholders`) reads
|
|
534
|
+
this table at comment-write time; later membership edits are not retroactive,
|
|
535
|
+
matching the auto-group snapshot semantics.
|
|
536
|
+
|
|
537
|
+
Registry/config, **not** sync state: like ``ShareLink`` and ``ApiToken`` this is
|
|
538
|
+
a plain :class:`django.db.models.Model` (no ``server_version``) and is never part
|
|
539
|
+
of the mobile offline delta — external stakeholders are a server-side program
|
|
540
|
+
setting, not a board object a client edits offline.
|
|
541
|
+
|
|
542
|
+
Email delivery to these addresses is **deferred to #1675**: #1658 ships the
|
|
543
|
+
registry model + resolver so the recipient count can be surfaced, but no
|
|
544
|
+
outbound mail is sent to an external stakeholder yet.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
548
|
+
program = models.ForeignKey(
|
|
549
|
+
_PROGRAM_MODEL,
|
|
550
|
+
on_delete=models.CASCADE,
|
|
551
|
+
related_name="external_stakeholders",
|
|
552
|
+
)
|
|
553
|
+
name = models.CharField(max_length=200)
|
|
554
|
+
email = models.EmailField()
|
|
555
|
+
note = models.TextField(blank=True, default="")
|
|
556
|
+
created_by = models.ForeignKey(
|
|
557
|
+
settings.AUTH_USER_MODEL,
|
|
558
|
+
on_delete=models.SET_NULL,
|
|
559
|
+
null=True,
|
|
560
|
+
related_name="+",
|
|
561
|
+
)
|
|
562
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
563
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
564
|
+
# Soft-delete so a removed stakeholder's email frees up for re-add and the
|
|
565
|
+
# case-insensitive unique constraint only binds live rows (see Meta).
|
|
566
|
+
is_deleted = models.BooleanField(default=False)
|
|
567
|
+
|
|
568
|
+
class Meta:
|
|
569
|
+
db_table = "access_external_stakeholder"
|
|
570
|
+
constraints = [
|
|
571
|
+
# Case-insensitive (program, email) uniqueness across LIVE rows only —
|
|
572
|
+
# mirrors the mention-group constraint pattern so a soft-deleted row
|
|
573
|
+
# frees its email for re-add rather than reserving it (a re-add would
|
|
574
|
+
# otherwise pass the serializer check but hit the DB as a 500).
|
|
575
|
+
models.UniqueConstraint(
|
|
576
|
+
"program",
|
|
577
|
+
Lower("email"),
|
|
578
|
+
condition=models.Q(is_deleted=False),
|
|
579
|
+
name="uniq_external_stakeholder_program_email_ci",
|
|
580
|
+
),
|
|
581
|
+
]
|
|
582
|
+
indexes = [
|
|
583
|
+
# List query: WHERE program_id = X AND is_deleted = False.
|
|
584
|
+
models.Index(fields=["program", "is_deleted"], name="extstake_prog_deleted_idx"),
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
def __str__(self) -> str:
|
|
588
|
+
return f"{self.name} <{self.email}> ({self.program_id})"
|