trueppm-api 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +27 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +109 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +459 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +178 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +863 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +597 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +528 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +280 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +551 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +566 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +993 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +537 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +541 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +458 -0
- trueppm_api/apps/projects/backfill.py +459 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1244 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1979 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/mc_admission.py +181 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2908 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +446 -0
- trueppm_api/apps/sso/services.py +1473 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +440 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +371 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +254 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/demo_read_only.py +314 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +1221 -0
- trueppm_api/core/password_reset.py +513 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/core/worker_selfheal.py +205 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
"""Service-layer helpers for the access app.
|
|
2
|
+
|
|
3
|
+
The service layer wraps multi-row transactions that must remain atomic — most
|
|
4
|
+
importantly the Program create flow, which inserts a ``Program`` row and an
|
|
5
|
+
OWNER ``ProgramMembership`` row in the same transaction so a Program can never
|
|
6
|
+
exist in the database without at least one Owner.
|
|
7
|
+
|
|
8
|
+
ADR-0070: see §Durable Execution §4 for the atomic-create rationale.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import uuid
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from django.core.exceptions import ValidationError
|
|
18
|
+
from django.db import models, transaction
|
|
19
|
+
from django.utils import timezone
|
|
20
|
+
|
|
21
|
+
from trueppm_api.apps.access.models import ProgramMembership, ProjectMembership, Role
|
|
22
|
+
from trueppm_api.apps.projects.models import Methodology, Program, Project
|
|
23
|
+
|
|
24
|
+
# Shared detail copy for "this user cannot be the transfer target" (#4575).
|
|
25
|
+
#
|
|
26
|
+
# Views that resolve a ``*_user_id`` payload field must return this exact
|
|
27
|
+
# string — not a distinct "User not found." — for an unknown id, a real id
|
|
28
|
+
# with no live membership, and a real id for a deactivated user alike. Prior
|
|
29
|
+
# to #4575 the view returned a 404 "User not found."/"Lead user not found."
|
|
30
|
+
# for an unresolvable id while the service raised a differently-worded 400
|
|
31
|
+
# for a real-but-non-member id. That mismatch was itself an oracle: a caller
|
|
32
|
+
# could distinguish "no such user" from "user exists but isn't a member" (or
|
|
33
|
+
# "exists but is deactivated") by status code and message alone, without ever
|
|
34
|
+
# seeing the target's email. Collapsing both failure modes onto one
|
|
35
|
+
# constant — reused by both the service (which raises it) and the view
|
|
36
|
+
# (which returns it directly when the id does not even resolve to a User) —
|
|
37
|
+
# makes that distinction unobservable by construction instead of by
|
|
38
|
+
# convention.
|
|
39
|
+
PROJECT_OWNER_NOT_MEMBER_DETAIL = (
|
|
40
|
+
"The new owner must already be a member of this project. "
|
|
41
|
+
"Invite them first, then retry the transfer."
|
|
42
|
+
)
|
|
43
|
+
PROGRAM_OWNER_NOT_MEMBER_DETAIL = (
|
|
44
|
+
"The new sponsor must already be a member of this program. "
|
|
45
|
+
"Invite them first, then retry the transfer."
|
|
46
|
+
)
|
|
47
|
+
PROGRAM_LEAD_NOT_MEMBER_DETAIL = (
|
|
48
|
+
"The new lead must already be a member of this program. "
|
|
49
|
+
"Invite them first, then retry the transfer."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _protecting_relations(model: type[models.Model]) -> list[Any]:
|
|
54
|
+
"""Reverse relations into *model* whose FK is ``on_delete=PROTECT``.
|
|
55
|
+
|
|
56
|
+
Derived from ``_meta`` rather than hand-listed. Every hand-maintained version of
|
|
57
|
+
this list in the codebase went stale as new ``PROTECT`` FKs landed — three delete
|
|
58
|
+
paths were each written against the FKs that existed at the time and silently
|
|
59
|
+
started raising ``ProtectedError`` later (#2364). Reflection cannot drift.
|
|
60
|
+
|
|
61
|
+
``include_hidden=True`` is load-bearing, not defensive: a FK declared with
|
|
62
|
+
``related_name="+"`` is absent from ``_meta.related_objects`` but Django's
|
|
63
|
+
deletion collector still enforces its ``PROTECT``. Walking the public
|
|
64
|
+
``related_objects`` would silently miss exactly those — reproducing the bug this
|
|
65
|
+
helper exists to prevent, just one layer down. ``Workspace.calendar`` is a live
|
|
66
|
+
example of a ``+``-named ``PROTECT`` FK in this codebase.
|
|
67
|
+
|
|
68
|
+
``test_protect_relations_snapshot`` pins the resolved set, so adding a new
|
|
69
|
+
``PROTECT`` FK fails a test and forces a deliberate call on whether a hard delete
|
|
70
|
+
should purge that child or refuse — rather than silently purging it here.
|
|
71
|
+
"""
|
|
72
|
+
return [
|
|
73
|
+
rel
|
|
74
|
+
for rel in model._meta.get_fields(include_hidden=True)
|
|
75
|
+
if rel.is_relation
|
|
76
|
+
and rel.auto_created
|
|
77
|
+
and not rel.concrete
|
|
78
|
+
and getattr(rel, "on_delete", None) is models.PROTECT
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _purge_protecting_children(model: type[models.Model], pks: Sequence[Any]) -> None:
|
|
83
|
+
"""Delete every ``PROTECT``-ing child of *pks* so the parent rows can be removed.
|
|
84
|
+
|
|
85
|
+
Only for paths where the caller has already confirmed a permanent, irreversible
|
|
86
|
+
delete (the archive → ``?force=true`` dialog, sample-data teardown, the retention
|
|
87
|
+
purge). Everywhere else a ``PROTECT`` FK should surface as a 409 refusal, not be
|
|
88
|
+
bulldozed — see :mod:`trueppm_api.core.protect_conflict`.
|
|
89
|
+
"""
|
|
90
|
+
if not pks:
|
|
91
|
+
return
|
|
92
|
+
for rel in _protecting_relations(model):
|
|
93
|
+
rel.related_model._base_manager.filter(**{f"{rel.field.name}__in": pks}).delete()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@transaction.atomic
|
|
97
|
+
def hard_delete_projects(project_ids: Sequence[Any], *, broadcast: bool = False) -> int:
|
|
98
|
+
"""Permanently delete projects and the rows whose ``PROTECT`` FKs block them.
|
|
99
|
+
|
|
100
|
+
Returns the number of ``Project`` rows removed. Authorization and audit events
|
|
101
|
+
remain the caller's responsibility, but the ``project_hard_deleted`` sync
|
|
102
|
+
broadcast does not: pass ``broadcast=True`` to have this helper schedule one
|
|
103
|
+
per deleted project via ``transaction.on_commit`` (deferred so a rollback never
|
|
104
|
+
advertises a delete that didn't happen). Every current caller needs this
|
|
105
|
+
broadcast, so it is opt-in rather than silently assumed — a caller that forgets
|
|
106
|
+
the flag gets no broadcast and no event, instead of the inverse bug this helper
|
|
107
|
+
used to invite (#4374): callers were "responsible for... sync broadcasts" in
|
|
108
|
+
name only, and one of five forgot.
|
|
109
|
+
|
|
110
|
+
``project_ids`` are coerced to ``str`` before being used in the on_commit
|
|
111
|
+
closures, so each callback captures a plain string rather than a lazily
|
|
112
|
+
re-evaluated queryset or ORM instance.
|
|
113
|
+
"""
|
|
114
|
+
if not project_ids:
|
|
115
|
+
return 0
|
|
116
|
+
ids = [str(pid) for pid in project_ids]
|
|
117
|
+
_purge_protecting_children(Project, ids)
|
|
118
|
+
_, by_model = Project.objects.filter(pk__in=ids).delete()
|
|
119
|
+
deleted = int(by_model.get(Project._meta.label, 0))
|
|
120
|
+
if broadcast and deleted:
|
|
121
|
+
# A direct, literal call rather than functools.partial: the #1019/#1381
|
|
122
|
+
# AST sweep that keeps FROZEN_WS_EVENT_TYPES honest
|
|
123
|
+
# (tests/apps/sync/ws_event_scan.py) only recognizes a literal event_type
|
|
124
|
+
# reaching broadcast_board_event() directly or through a one-level
|
|
125
|
+
# parameter-forwarding wrapper — partial(broadcast_board_event, ...) is
|
|
126
|
+
# neither, and would make "project_hard_deleted" invisible to that scan.
|
|
127
|
+
# A nested def with a typed default argument (not a bare lambda, which
|
|
128
|
+
# mypy --strict cannot infer a type for here, and not a closure over the
|
|
129
|
+
# bare loop variable, which would let every callback resolve to the last
|
|
130
|
+
# id once the loop finishes) is what keeps each callback bound to its own
|
|
131
|
+
# id.
|
|
132
|
+
from trueppm_api.apps.sync.broadcast import broadcast_board_event
|
|
133
|
+
|
|
134
|
+
for project_id in ids:
|
|
135
|
+
|
|
136
|
+
def _emit_hard_deleted(project_id: str = project_id) -> None:
|
|
137
|
+
broadcast_board_event(project_id, "project_hard_deleted", {"id": project_id})
|
|
138
|
+
|
|
139
|
+
transaction.on_commit(_emit_hard_deleted)
|
|
140
|
+
return deleted
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@transaction.atomic
|
|
144
|
+
def hard_delete_program(program_id: uuid.UUID | str, *, broadcast: bool = False) -> int:
|
|
145
|
+
"""Permanently delete a program, its projects, and all ``PROTECT``-ing children.
|
|
146
|
+
|
|
147
|
+
The program's projects are deleted outright rather than detached — the only
|
|
148
|
+
caller is sample-data teardown, where the whole subtree is disposable. Contrast
|
|
149
|
+
:func:`delete_program_cascade`, which *soft*-deletes a real program and detaches
|
|
150
|
+
its projects so they survive as standalone work.
|
|
151
|
+
|
|
152
|
+
Returns the number of ``Project`` rows removed. Pass ``broadcast=True`` to emit
|
|
153
|
+
the per-project ``project_hard_deleted`` sync broadcast via
|
|
154
|
+
:func:`hard_delete_projects` — the program itself has no deliverable WS channel
|
|
155
|
+
(#836), so a ``program_deleted`` event, if the caller also sends one, reaches no
|
|
156
|
+
client; the per-project broadcast is what lets an open tab on one of the deleted
|
|
157
|
+
projects learn it is gone.
|
|
158
|
+
"""
|
|
159
|
+
project_ids = list(Project.objects.filter(program_id=program_id).values_list("pk", flat=True))
|
|
160
|
+
deleted = hard_delete_projects(project_ids, broadcast=broadcast)
|
|
161
|
+
_purge_protecting_children(Program, [program_id])
|
|
162
|
+
Program.objects.filter(pk=program_id).delete()
|
|
163
|
+
return deleted
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@transaction.atomic
|
|
167
|
+
def create_program(
|
|
168
|
+
*,
|
|
169
|
+
name: str,
|
|
170
|
+
description: str,
|
|
171
|
+
methodology: str | Methodology,
|
|
172
|
+
created_by: Any,
|
|
173
|
+
lead: Any | None = None,
|
|
174
|
+
code: str | None = "",
|
|
175
|
+
) -> Program:
|
|
176
|
+
"""Create a ``Program`` and the creator's OWNER membership in one transaction.
|
|
177
|
+
|
|
178
|
+
Atomicity is the whole point of this helper — if the OWNER ``ProgramMembership``
|
|
179
|
+
INSERT were a second request (or a non-atomic follow-up), a crash between the
|
|
180
|
+
two writes would leave a Program with no Owner, which permission classes treat
|
|
181
|
+
as "nobody can manage this program." The ``@transaction.atomic`` wrapper makes
|
|
182
|
+
that orphan window impossible.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
name: Display name (max 255). No uniqueness enforcement — two PMs can
|
|
186
|
+
both have a program called "Phase 2".
|
|
187
|
+
description: Optional rich text; empty string allowed.
|
|
188
|
+
methodology: One of ``Methodology`` values; default ``HYBRID``.
|
|
189
|
+
created_by: The user creating the program — recorded on the Program row
|
|
190
|
+
and inserted as the OWNER ``ProgramMembership``.
|
|
191
|
+
lead: Optional program lead (#2025). At create time the only member that
|
|
192
|
+
will exist is ``created_by`` (the OWNER row inserted below), so the
|
|
193
|
+
serializer only admits ``lead == created_by`` — the "lead must be a
|
|
194
|
+
member" invariant holds because the OWNER membership is written in the
|
|
195
|
+
same atomic transaction. Defaults to ``None`` (no lead).
|
|
196
|
+
code: The program key (ADR-1237), already validated by the caller.
|
|
197
|
+
``""`` derives one from ``name``. ``None`` assigns no key at all —
|
|
198
|
+
only for the seed importer, which decides the key itself (a slug, a
|
|
199
|
+
suffixed slug, or keys adopted from the program it replaces) and
|
|
200
|
+
must not first burn a derived key into the never-reused namespace.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The persisted ``Program``. The OWNER membership exists by the time the
|
|
204
|
+
function returns.
|
|
205
|
+
"""
|
|
206
|
+
program = Program.objects.create(
|
|
207
|
+
name=name,
|
|
208
|
+
description=description,
|
|
209
|
+
methodology=methodology,
|
|
210
|
+
created_by=created_by,
|
|
211
|
+
lead=lead,
|
|
212
|
+
)
|
|
213
|
+
ProgramMembership.objects.create(
|
|
214
|
+
program=program,
|
|
215
|
+
user=created_by,
|
|
216
|
+
role=Role.OWNER,
|
|
217
|
+
)
|
|
218
|
+
if code is not None:
|
|
219
|
+
from trueppm_api.apps.projects.models import ObjectKeySource
|
|
220
|
+
from trueppm_api.apps.projects.services import assign_key, derive_key
|
|
221
|
+
|
|
222
|
+
source = ObjectKeySource.USER if code else ObjectKeySource.DERIVED
|
|
223
|
+
assign_key(program, code or derive_key(name, "program"), source=source, actor=created_by)
|
|
224
|
+
return program
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@transaction.atomic
|
|
228
|
+
def delete_program_cascade(program_id: uuid.UUID | str) -> None:
|
|
229
|
+
"""Soft-delete all memberships, detach all projects, soft-delete the program.
|
|
230
|
+
|
|
231
|
+
Three things must happen atomically:
|
|
232
|
+
|
|
233
|
+
1. **Memberships are soft-deleted.** ``ProgramMembership.program`` is ``PROTECT``,
|
|
234
|
+
so a naive ``program.delete()`` raises ``ProtectedError`` while any member
|
|
235
|
+
rows exist. The UX brief surfaces this as a combined "Remove all members
|
|
236
|
+
and delete" action; the cascade implements that intent server-side.
|
|
237
|
+
2. **Projects are detached.** Django's ``SET_NULL`` only fires on a hard DELETE,
|
|
238
|
+
not on our soft-delete (``is_deleted=True``). Without an explicit detach
|
|
239
|
+
step, ``Project.program_id`` would continue to reference a soft-deleted
|
|
240
|
+
Program — the UI would display "Phase 2 (deleted)" for those projects
|
|
241
|
+
and the program shell would still try to retrieve a 404'd parent. Setting
|
|
242
|
+
``program=NULL`` here matches the dialog's promise that projects become
|
|
243
|
+
standalone after deletion.
|
|
244
|
+
3. **The program itself is soft-deleted.** This bumps ``server_version`` so
|
|
245
|
+
sync consumers can drop the row from their caches.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
program_id: The program to delete.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
Program.DoesNotExist: if the program is missing or already soft-deleted.
|
|
252
|
+
"""
|
|
253
|
+
# Local import: Project is in the projects app; importing at module top
|
|
254
|
+
# would create a circular dependency through the projects → access path.
|
|
255
|
+
from trueppm_api.apps.projects.models import Project
|
|
256
|
+
|
|
257
|
+
# Lock the program row so concurrent member-add / project-assign requests
|
|
258
|
+
# cannot land between the cascade steps and re-create a reference to the
|
|
259
|
+
# program we're about to mark deleted.
|
|
260
|
+
program = Program.objects.select_for_update().get(pk=program_id, is_deleted=False)
|
|
261
|
+
|
|
262
|
+
for membership in ProgramMembership.objects.select_for_update().filter(
|
|
263
|
+
program=program, is_deleted=False
|
|
264
|
+
):
|
|
265
|
+
membership.soft_delete()
|
|
266
|
+
|
|
267
|
+
# Detach projects — they hold a SET_NULL FK, but SET_NULL only fires on
|
|
268
|
+
# hard delete. soft_delete() on the program would otherwise leave dangling
|
|
269
|
+
# references. We must go through ``Project.save()`` (not ``update()``) so
|
|
270
|
+
# ``server_version`` increments and mobile sync clients pick up the change.
|
|
271
|
+
for project in Project.objects.select_for_update().filter(program=program):
|
|
272
|
+
project.program = None
|
|
273
|
+
project.save(update_fields=["program"])
|
|
274
|
+
|
|
275
|
+
program.soft_delete()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@transaction.atomic
|
|
279
|
+
def transfer_project_ownership(
|
|
280
|
+
*,
|
|
281
|
+
project: Project,
|
|
282
|
+
new_owner: Any,
|
|
283
|
+
actor: Any,
|
|
284
|
+
) -> ProjectMembership:
|
|
285
|
+
"""Atomically promote a project member to OWNER, demote the actor to ADMIN.
|
|
286
|
+
|
|
287
|
+
Why the target must already be a member: requiring an existing membership
|
|
288
|
+
forces an explicit invite step and gives the audit trail a clean
|
|
289
|
+
"joined → promoted" sequence. Auto-creating membership on transfer would
|
|
290
|
+
surprise the target with admin authority on a project they were never
|
|
291
|
+
knowingly added to.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
project: The project whose ownership is being transferred.
|
|
295
|
+
new_owner: The user who will become OWNER.
|
|
296
|
+
actor: The current OWNER initiating the transfer; will be downgraded
|
|
297
|
+
to ADMIN. May be the same user as ``new_owner`` — in that case
|
|
298
|
+
both upserts are no-ops and the function returns the actor's row.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
The new OWNER's ``ProjectMembership`` row.
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
ValidationError: if ``new_owner`` has no existing active membership
|
|
305
|
+
on the project.
|
|
306
|
+
"""
|
|
307
|
+
# Defense in depth — the view layer gates with ``IsProjectOwner``, but the
|
|
308
|
+
# service is reusable (management commands, signals, future endpoints). If
|
|
309
|
+
# the actor is not actually the OWNER, a naive ``.update()`` is a silent
|
|
310
|
+
# no-op that would promote the target without removing anyone — creating
|
|
311
|
+
# an unauthorized OWNER. Assert and fail loudly before any state changes.
|
|
312
|
+
try:
|
|
313
|
+
actor_row = ProjectMembership.objects.select_for_update().get(
|
|
314
|
+
project=project,
|
|
315
|
+
user=actor,
|
|
316
|
+
is_deleted=False,
|
|
317
|
+
)
|
|
318
|
+
except ProjectMembership.DoesNotExist as exc:
|
|
319
|
+
raise ValidationError("Only an existing project Owner can transfer ownership.") from exc
|
|
320
|
+
if actor_row.role != Role.OWNER:
|
|
321
|
+
raise ValidationError("Only an existing project Owner can transfer ownership.")
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
# ``user__is_active=True`` closes the email-harvest class (#4575): a
|
|
325
|
+
# deactivated user must be refused identically to one who was never a
|
|
326
|
+
# member, not promoted to OWNER on an account nobody can log into.
|
|
327
|
+
target = ProjectMembership.objects.select_for_update().get(
|
|
328
|
+
project=project,
|
|
329
|
+
user=new_owner,
|
|
330
|
+
user__is_active=True,
|
|
331
|
+
is_deleted=False,
|
|
332
|
+
)
|
|
333
|
+
except ProjectMembership.DoesNotExist as exc:
|
|
334
|
+
raise ValidationError(PROJECT_OWNER_NOT_MEMBER_DETAIL) from exc
|
|
335
|
+
|
|
336
|
+
if new_owner == actor:
|
|
337
|
+
return target
|
|
338
|
+
|
|
339
|
+
# Demote the actor first so the OWNER count never exceeds the previous
|
|
340
|
+
# state mid-transaction — defensive against future single-owner constraints.
|
|
341
|
+
# role_changed_at is stamped alongside role on both rows so the per-project
|
|
342
|
+
# access-evidence timestamp (#590) stays accurate on this path too, not just
|
|
343
|
+
# on the partial_update endpoint.
|
|
344
|
+
now = timezone.now()
|
|
345
|
+
actor_row.role = Role.ADMIN
|
|
346
|
+
actor_row.role_changed_at = now
|
|
347
|
+
actor_row.save(update_fields=["role", "role_changed_at"])
|
|
348
|
+
|
|
349
|
+
target.role = Role.OWNER
|
|
350
|
+
target.role_changed_at = now
|
|
351
|
+
target.save(update_fields=["role", "role_changed_at"])
|
|
352
|
+
return target
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@transaction.atomic
|
|
356
|
+
def transfer_program_sponsorship(
|
|
357
|
+
*,
|
|
358
|
+
program: Program,
|
|
359
|
+
new_owner: Any,
|
|
360
|
+
actor: Any,
|
|
361
|
+
new_lead: Any | None = None,
|
|
362
|
+
) -> ProgramMembership:
|
|
363
|
+
"""Atomically promote a program member to OWNER and optionally rotate the lead.
|
|
364
|
+
|
|
365
|
+
Programs carry both an OWNER membership (RBAC) and a ``lead`` FK (the
|
|
366
|
+
"Program Manager" displayed in the header). Both can move together in a
|
|
367
|
+
sponsorship transfer so the header chip and the access matrix stay in sync.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
program: The program whose sponsorship is being transferred.
|
|
371
|
+
new_owner: The user who will become OWNER.
|
|
372
|
+
actor: The current OWNER initiating the transfer; will be downgraded
|
|
373
|
+
to ADMIN.
|
|
374
|
+
new_lead: Optional user to set as ``program.lead``. ``None`` leaves
|
|
375
|
+
the existing lead unchanged; pass ``new_owner`` explicitly to keep
|
|
376
|
+
owner+lead in lockstep.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
The new OWNER's ``ProgramMembership`` row.
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
ValidationError: if ``new_owner`` has no existing active program
|
|
383
|
+
membership, or if ``new_lead`` is given and has no existing
|
|
384
|
+
active program membership (#4575).
|
|
385
|
+
"""
|
|
386
|
+
# Defense in depth (see ``transfer_project_ownership`` for full rationale)
|
|
387
|
+
# — the actor must currently hold the OWNER row, otherwise the demote
|
|
388
|
+
# ``.update()`` is a no-op and the target gets promoted without anyone
|
|
389
|
+
# being removed.
|
|
390
|
+
try:
|
|
391
|
+
actor_row = ProgramMembership.objects.select_for_update().get(
|
|
392
|
+
program=program,
|
|
393
|
+
user=actor,
|
|
394
|
+
is_deleted=False,
|
|
395
|
+
)
|
|
396
|
+
except ProgramMembership.DoesNotExist as exc:
|
|
397
|
+
raise ValidationError("Only an existing Program Admin can transfer sponsorship.") from exc
|
|
398
|
+
if actor_row.role != Role.OWNER:
|
|
399
|
+
raise ValidationError("Only an existing Program Admin can transfer sponsorship.")
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
# ``user__is_active=True`` closes the email-harvest class (#4575): a
|
|
403
|
+
# deactivated user must be refused identically to one who was never a
|
|
404
|
+
# member, not promoted to OWNER on an account nobody can log into.
|
|
405
|
+
target = ProgramMembership.objects.select_for_update().get(
|
|
406
|
+
program=program,
|
|
407
|
+
user=new_owner,
|
|
408
|
+
user__is_active=True,
|
|
409
|
+
is_deleted=False,
|
|
410
|
+
)
|
|
411
|
+
except ProgramMembership.DoesNotExist as exc:
|
|
412
|
+
raise ValidationError(PROGRAM_OWNER_NOT_MEMBER_DETAIL) from exc
|
|
413
|
+
|
|
414
|
+
# ``new_lead`` previously flowed straight into ``program.lead`` with no
|
|
415
|
+
# membership check at all — unlike ``new_owner`` above, which has always
|
|
416
|
+
# been membership-gated. That let any authenticated OWNER of ANY program
|
|
417
|
+
# (including a throwaway one they just created) set an arbitrary user id
|
|
418
|
+
# as lead and read their email back off ``lead_detail`` in the 200
|
|
419
|
+
# response: an email-harvest oracle over the full user-id space (#4575).
|
|
420
|
+
# Validate here, in the service, not only in the view — the view is not
|
|
421
|
+
# the only caller and the invariant belongs with the write.
|
|
422
|
+
if new_lead is not None:
|
|
423
|
+
lead_is_member = ProgramMembership.objects.filter(
|
|
424
|
+
program=program,
|
|
425
|
+
user=new_lead,
|
|
426
|
+
user__is_active=True,
|
|
427
|
+
is_deleted=False,
|
|
428
|
+
).exists()
|
|
429
|
+
if not lead_is_member:
|
|
430
|
+
raise ValidationError(PROGRAM_LEAD_NOT_MEMBER_DETAIL)
|
|
431
|
+
|
|
432
|
+
if new_owner != actor:
|
|
433
|
+
# role_changed_at is stamped alongside role on both rows so the
|
|
434
|
+
# per-program access-evidence timestamp (#878) stays accurate on this
|
|
435
|
+
# path too, not just on the partial_update endpoint.
|
|
436
|
+
now = timezone.now()
|
|
437
|
+
actor_row.role = Role.ADMIN
|
|
438
|
+
actor_row.role_changed_at = now
|
|
439
|
+
actor_row.save(update_fields=["role", "role_changed_at"])
|
|
440
|
+
|
|
441
|
+
target.role = Role.OWNER
|
|
442
|
+
target.role_changed_at = now
|
|
443
|
+
target.save(update_fields=["role", "role_changed_at"])
|
|
444
|
+
|
|
445
|
+
if new_lead is not None:
|
|
446
|
+
program.lead = new_lead
|
|
447
|
+
program.save(update_fields=["lead"])
|
|
448
|
+
|
|
449
|
+
return target
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@transaction.atomic
|
|
453
|
+
def split_program(
|
|
454
|
+
*,
|
|
455
|
+
program: Program,
|
|
456
|
+
splits: list[dict[str, Any]],
|
|
457
|
+
actor: Any,
|
|
458
|
+
) -> list[Program]:
|
|
459
|
+
"""Split a program into sub-programs and close the original (ADR-0156, #967).
|
|
460
|
+
|
|
461
|
+
For each entry in ``splits`` a new sub-program is created (owned by ``actor``,
|
|
462
|
+
methodology copied from the parent, all inheritable overrides left NULL so the
|
|
463
|
+
new program inherits workspace defaults), and the entry's projects are moved
|
|
464
|
+
under it. After every split is applied the original program is closed — it
|
|
465
|
+
becomes a read-only shell holding any projects that were not redistributed.
|
|
466
|
+
|
|
467
|
+
Only the ``Project.program`` FK moves; tasks, dependencies, baselines,
|
|
468
|
+
memberships, and history are untouched, so each project keeps its full
|
|
469
|
+
schedule and audit trail under its new program. Reassignment goes through
|
|
470
|
+
``Project.save()`` (not ``.update()``) so ``server_version`` increments and
|
|
471
|
+
mobile sync clients pick up the move — same rationale as
|
|
472
|
+
``delete_program_cascade``.
|
|
473
|
+
|
|
474
|
+
The whole operation is one transaction: either every sub-program, every
|
|
475
|
+
reassignment, and the parent close commit together, or nothing does. A bad
|
|
476
|
+
payload raises before any INSERT so the error message is precise.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
program: The program being split. Must be open — callers gate on
|
|
480
|
+
``IsProgramNotClosed``; once closed by this call a replayed request
|
|
481
|
+
is rejected by that gate, which is what makes a network retry safe.
|
|
482
|
+
splits: ``[{"name": str, "project_ids": [uuid]}, ...]``. Each entry
|
|
483
|
+
becomes one sub-program. ``project_ids`` may be empty (an empty
|
|
484
|
+
sub-program shell, symmetric with ``create_program``).
|
|
485
|
+
actor: The current OWNER initiating the split; becomes OWNER of every
|
|
486
|
+
sub-program.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
The created sub-programs, in input order.
|
|
490
|
+
|
|
491
|
+
Raises:
|
|
492
|
+
ValidationError: if a referenced project is not a live member of this
|
|
493
|
+
program, or if a project appears in more than one split.
|
|
494
|
+
"""
|
|
495
|
+
# Lock the parent row so a concurrent project-assign / delete cannot land
|
|
496
|
+
# between validation and reassignment (mirrors delete_program_cascade).
|
|
497
|
+
program = Program.objects.select_for_update().get(pk=program.pk)
|
|
498
|
+
|
|
499
|
+
member_project_ids = {
|
|
500
|
+
str(pk)
|
|
501
|
+
for pk in Project.objects.filter(program=program, is_deleted=False).values_list(
|
|
502
|
+
"pk", flat=True
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
# Validate the full payload up front: every id must be a live member of this
|
|
507
|
+
# program, and no id may be claimed by two sub-programs.
|
|
508
|
+
seen: set[str] = set()
|
|
509
|
+
normalized: list[tuple[str, list[str]]] = []
|
|
510
|
+
for entry in splits:
|
|
511
|
+
name = entry["name"]
|
|
512
|
+
ids = [str(pid) for pid in entry["project_ids"]]
|
|
513
|
+
for pid in ids:
|
|
514
|
+
if pid not in member_project_ids:
|
|
515
|
+
raise ValidationError(f"Project {pid} is not a project of this program.")
|
|
516
|
+
if pid in seen:
|
|
517
|
+
raise ValidationError(f"Project {pid} is assigned to more than one sub-program.")
|
|
518
|
+
seen.add(pid)
|
|
519
|
+
normalized.append((name, ids))
|
|
520
|
+
|
|
521
|
+
sub_programs: list[Program] = []
|
|
522
|
+
for name, ids in normalized:
|
|
523
|
+
sub = create_program(
|
|
524
|
+
name=name,
|
|
525
|
+
description="",
|
|
526
|
+
methodology=program.methodology,
|
|
527
|
+
created_by=actor,
|
|
528
|
+
)
|
|
529
|
+
# Filter by program=program as well as pk so a project moved out by a
|
|
530
|
+
# racing request (despite the lock) is never silently captured.
|
|
531
|
+
for project in Project.objects.select_for_update().filter(
|
|
532
|
+
program=program, pk__in=ids, is_deleted=False
|
|
533
|
+
):
|
|
534
|
+
project.program = sub
|
|
535
|
+
project.save(update_fields=["program"])
|
|
536
|
+
sub_programs.append(sub)
|
|
537
|
+
|
|
538
|
+
if not program.is_closed:
|
|
539
|
+
program.is_closed = True
|
|
540
|
+
program.closed_at = timezone.now()
|
|
541
|
+
program.closed_by = actor
|
|
542
|
+
program.save(update_fields=["is_closed", "closed_at", "closed_by"])
|
|
543
|
+
|
|
544
|
+
return sub_programs
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def revoke_all_refresh_tokens(user: Any) -> int:
|
|
548
|
+
"""Blacklist every outstanding simplejwt refresh token for ``user``.
|
|
549
|
+
|
|
550
|
+
This is the "sign out every device" primitive (ADR-0209). A password reset
|
|
551
|
+
(and, later, any explicit account-security action) must invalidate all of the
|
|
552
|
+
user's existing sessions, not just the one that triggered the change. The
|
|
553
|
+
SPA's session lives entirely in a JWT access token (in-memory, 15-min TTL) plus
|
|
554
|
+
an httpOnly refresh cookie (#897), so revoking every *refresh* token is the
|
|
555
|
+
complete server-side session revocation: a blacklisted refresh token can no
|
|
556
|
+
longer be exchanged for a new access token, and the old access token self-
|
|
557
|
+
expires within its short TTL.
|
|
558
|
+
|
|
559
|
+
Django sessions are intentionally left untouched — the SPA never authenticates
|
|
560
|
+
via ``django.contrib.sessions`` (that framework backs only the admin site and
|
|
561
|
+
the DRF browsable API), so there is no app session to clear there.
|
|
562
|
+
|
|
563
|
+
Completeness depends on every live token having an ``OutstandingToken`` row.
|
|
564
|
+
simplejwt writes one only in ``RefreshToken.for_user`` (login); rotation mints a
|
|
565
|
+
new jti and records nothing, so before #2999 this function walked a set of
|
|
566
|
+
already-blacklisted rows and left the *live* session usable while returning a
|
|
567
|
+
plausible non-zero count. ``core.auth_views._record_outstanding`` now upserts the
|
|
568
|
+
row on both login and rotation, which is what makes this function's contract
|
|
569
|
+
true. **Tokens minted before that fix shipped still have no row and remain
|
|
570
|
+
unrevokable** — an operator needing a hard cut across those must rotate
|
|
571
|
+
``JWT_SIGNING_KEY``.
|
|
572
|
+
|
|
573
|
+
Idempotent: ``get_or_create`` means re-running blacklists nothing twice, and a
|
|
574
|
+
row already blacklisted by rotation stays blacklisted. Safe to call inside the
|
|
575
|
+
reset transaction.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
user: The account whose refresh tokens should all be revoked.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
The number of tokens newly blacklisted (already-blacklisted tokens are not
|
|
582
|
+
counted). Zero when the account had no outstanding tokens (e.g. it has
|
|
583
|
+
never logged in on this deployment).
|
|
584
|
+
"""
|
|
585
|
+
# Local import: the blacklist app's models are only importable once apps are
|
|
586
|
+
# loaded, and keeping the import lazy lets a lean deploy that removes the
|
|
587
|
+
# token_blacklist app degrade gracefully (the ImportError is swallowed → the
|
|
588
|
+
# password is still reset, sessions just fall back to TTL-only expiry).
|
|
589
|
+
try:
|
|
590
|
+
from rest_framework_simplejwt.token_blacklist.models import (
|
|
591
|
+
BlacklistedToken,
|
|
592
|
+
OutstandingToken,
|
|
593
|
+
)
|
|
594
|
+
except ImportError: # pragma: no cover - only when the app is uninstalled
|
|
595
|
+
return 0
|
|
596
|
+
|
|
597
|
+
revoked = 0
|
|
598
|
+
for token in OutstandingToken.objects.filter(user=user):
|
|
599
|
+
_, created = BlacklistedToken.objects.get_or_create(token=token)
|
|
600
|
+
if created:
|
|
601
|
+
revoked += 1
|
|
602
|
+
return revoked
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def revoke_all_personal_access_tokens(
|
|
606
|
+
user: Any,
|
|
607
|
+
*,
|
|
608
|
+
actor: Any = None,
|
|
609
|
+
reason: str = "",
|
|
610
|
+
source_ip: str | None = None,
|
|
611
|
+
) -> int:
|
|
612
|
+
"""Soft-revoke every active Personal Access Token owned by ``user`` (ADR-0214).
|
|
613
|
+
|
|
614
|
+
Why this exists: a Personal Access Token is a *full-authority* bearer of the
|
|
615
|
+
user's own credentials — a script authenticating with it acts exactly as the
|
|
616
|
+
user. A password change is the user asserting "my credentials may be
|
|
617
|
+
compromised; cut everything off," so it must invalidate not only live sessions
|
|
618
|
+
(``revoke_all_refresh_tokens``) but also every long-lived personal credential
|
|
619
|
+
they minted. Leaving PATs live after a reset would defeat the whole point of
|
|
620
|
+
the reset — an attacker who phished the old password could keep the account via
|
|
621
|
+
a PAT they created. This is the PAT analogue of the refresh-token revocation and
|
|
622
|
+
is called immediately after it in the same atomic block.
|
|
623
|
+
|
|
624
|
+
Scope discipline: only ``owner=user`` (personal) tokens are revoked. Project-
|
|
625
|
+
and program-scoped tokens are *org assets* minted by an Admin/PM — they are not
|
|
626
|
+
the user's personal credentials and are deliberately left untouched, so a
|
|
627
|
+
password reset never breaks a team's CI integration.
|
|
628
|
+
|
|
629
|
+
Idempotent: already-revoked tokens are filtered out, so re-running is a no-op.
|
|
630
|
+
Uses a bulk ``update()`` (not per-row ``save()``) because revocation is a
|
|
631
|
+
single indexed write with no ``server_version`` sync semantics on the audit
|
|
632
|
+
path — the tokens are not mobile-synced resources whose version consumers track.
|
|
633
|
+
|
|
634
|
+
**Every revoked token gets an** ``ApiTokenAuditEntry`` **row (#2878).** It did
|
|
635
|
+
not, and the trail was inverted: routine self-service revocation through
|
|
636
|
+
``MyApiTokenViewSet.destroy`` was logged, while the two *security-relevant*
|
|
637
|
+
revocations — password reset and off-boarding, the two that answer "was this
|
|
638
|
+
account contained, and when?" — wrote nothing, on a feature page that promises
|
|
639
|
+
"every mint and revoke is written to an append-only audit log." The rows are
|
|
640
|
+
``bulk_create``d from the ids the ``update()`` matched, so the cost stays two
|
|
641
|
+
queries regardless of token count and the audit is inside the caller's
|
|
642
|
+
transaction: it commits with the revocation or rolls back with it, never apart.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
user: The account whose personal access tokens should all be revoked.
|
|
646
|
+
actor: The user who caused the revocation, for the audit row. The account
|
|
647
|
+
owner for a password reset; the acting admin for an off-boarding; ``None``
|
|
648
|
+
for an operator running the management command, which has no Django user.
|
|
649
|
+
reason: Short machine-readable cause recorded in ``detail["reason"]``
|
|
650
|
+
(``password_reset``, ``offboarding``, ``operator_bulk_revoke``). It is what
|
|
651
|
+
makes the trail answerable — "revoked" alone cannot distinguish routine
|
|
652
|
+
rotation from incident response.
|
|
653
|
+
source_ip: Client IP when one is available (a request-driven path); ``None``
|
|
654
|
+
from a management command.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
The number of tokens newly revoked (already-revoked tokens are excluded).
|
|
658
|
+
"""
|
|
659
|
+
# Local import: the projects app imports from access, so importing ApiToken at
|
|
660
|
+
# module top would create a circular dependency through the access → projects
|
|
661
|
+
# path (mirrors delete_program_cascade's lazy Project import).
|
|
662
|
+
from trueppm_api.apps.projects.models import (
|
|
663
|
+
ApiToken,
|
|
664
|
+
ApiTokenAuditAction,
|
|
665
|
+
ApiTokenAuditEntry,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
# Materialize before the update: after it, `revoked_at__isnull=True` no longer
|
|
669
|
+
# selects the rows this call is responsible for, and re-reading without the filter
|
|
670
|
+
# would also pick up tokens revoked earlier by someone else and double-audit them.
|
|
671
|
+
#
|
|
672
|
+
# `select_for_update()` is what keeps the split read/write as idempotent as the
|
|
673
|
+
# single `update()` it replaced. Without the row lock, a password reset racing an
|
|
674
|
+
# admin off-boarding both select the same rows, both write, and the second write
|
|
675
|
+
# moves `revoked_at` *forward* — misdating containment on the one field an auditor
|
|
676
|
+
# reads — while producing a second REVOKED row with a contradicting reason and an
|
|
677
|
+
# inflated return count. Holding the lock makes `doomed` exactly the set this call
|
|
678
|
+
# revokes, so the audit rows below cannot describe someone else's revocation. It
|
|
679
|
+
# requires an open transaction, which every caller has (ATOMIC_REQUESTS for the two
|
|
680
|
+
# request paths, an explicit `atomic()` in the management command); a caller without
|
|
681
|
+
# one gets a loud TransactionManagementError rather than a silent race.
|
|
682
|
+
#
|
|
683
|
+
# Expired tokens are excluded deliberately. An expired token already fails
|
|
684
|
+
# authentication (expiry is folded into the authenticator's hash lookup), so
|
|
685
|
+
# revoking it changes nothing — but auditing it would pad the trail with rows
|
|
686
|
+
# claiming N live credentials were contained when the real number was lower. This
|
|
687
|
+
# matches `ApiToken.active_personal_tokens_for`, the canonical definition of
|
|
688
|
+
# "active", which the 10-token cap already uses.
|
|
689
|
+
now = timezone.now()
|
|
690
|
+
doomed = list(
|
|
691
|
+
ApiToken.objects.select_for_update()
|
|
692
|
+
.filter(
|
|
693
|
+
models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=now),
|
|
694
|
+
owner=user,
|
|
695
|
+
is_deleted=False,
|
|
696
|
+
revoked_at__isnull=True,
|
|
697
|
+
)
|
|
698
|
+
.only("pk", "token_prefix", "name")
|
|
699
|
+
)
|
|
700
|
+
if not doomed:
|
|
701
|
+
return 0
|
|
702
|
+
|
|
703
|
+
revoked = ApiToken.objects.filter(
|
|
704
|
+
pk__in=[token.pk for token in doomed], revoked_at__isnull=True
|
|
705
|
+
).update(revoked_at=now)
|
|
706
|
+
ApiTokenAuditEntry.objects.bulk_create(
|
|
707
|
+
[
|
|
708
|
+
ApiTokenAuditEntry(
|
|
709
|
+
owner=user,
|
|
710
|
+
token=token,
|
|
711
|
+
token_prefix=token.token_prefix,
|
|
712
|
+
actor=actor,
|
|
713
|
+
action=ApiTokenAuditAction.REVOKED.value,
|
|
714
|
+
source_ip=source_ip,
|
|
715
|
+
detail={"name": token.name, "reason": reason} if reason else {"name": token.name},
|
|
716
|
+
)
|
|
717
|
+
for token in doomed
|
|
718
|
+
]
|
|
719
|
+
)
|
|
720
|
+
return revoked
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def revoke_personal_durable_grants(user: Any = None, *, actor: Any = None) -> tuple[int, int, int]:
|
|
724
|
+
"""Revoke the non-token durable grants a PAT (or a live session) can mint.
|
|
725
|
+
|
|
726
|
+
``IsNotTokenAuthenticated`` (#2878) now blocks *minting* a ``ShareLink``,
|
|
727
|
+
rotating a ``BoardAutomation`` webhook secret, or registering/repointing an
|
|
728
|
+
outbound ``Webhook`` through a bearer token, but a grant minted **before**
|
|
729
|
+
that guard existed, or minted through a live session by someone who is now
|
|
730
|
+
being contained, is exactly as durable as a personal access token and sits
|
|
731
|
+
outside every existing revocation lever (``revoke_all_personal_access_tokens``,
|
|
732
|
+
password reset, off-boarding). This closes that gap for the three grants
|
|
733
|
+
#2939 and #4461 scoped: a project's public share links, its git-automation
|
|
734
|
+
webhook secret, and any outgoing webhook the user registered. It also revokes
|
|
735
|
+
the pending workspace invites the user sent (#4589, see
|
|
736
|
+
``revoke_pending_workspace_invites``; not reflected in the returned tuple).
|
|
737
|
+
|
|
738
|
+
The webhook case is distinct from the other two: dispatch already stops for
|
|
739
|
+
an off-boarded *registrant* because it re-checks Admin+ membership on every
|
|
740
|
+
delivery (#4325/#4330) — ``created_by`` only, never the editors set below.
|
|
741
|
+
What that membership re-check cannot see at all is a *credential*
|
|
742
|
+
compromise — a leaked PAT or hijacked session belonging to a registrant who
|
|
743
|
+
is still an active Admin — and it cannot see an editor who is not the
|
|
744
|
+
registrant in any case, compromised or not. Deactivating the webhook here
|
|
745
|
+
is belt-and-suspenders for the registrant's own credential compromise, and
|
|
746
|
+
the *only* control that reaches an editor who never registered the row.
|
|
747
|
+
|
|
748
|
+
The webhook sweep matches ``created_by=user`` **or** ``user`` appearing in
|
|
749
|
+
``webhook.editors`` (:class:`~trueppm_api.apps.webhooks.models.WebhookEditor`,
|
|
750
|
+
an additive, append-only record of every user who has ever set ``url`` or
|
|
751
|
+
``secret`` on the row, or re-enabled it while disabled) — not ``created_by``
|
|
752
|
+
alone, and not a single mutable "last modified by" field either. A project
|
|
753
|
+
webhook's update has no
|
|
754
|
+
registrant check — any project Admin may repoint any project webhook's URL
|
|
755
|
+
or rotate its secret (``WebhookViewSet.perform_update``) — so a hijacked
|
|
756
|
+
session belonging to Admin B could repoint Admin A's webhook to an
|
|
757
|
+
attacker's host, and B's own password reset or off-boarding must still
|
|
758
|
+
catch that row even after a later, unrelated edit by A or anyone else. A
|
|
759
|
+
mutable field would lose B's attribution the moment someone else's edit
|
|
760
|
+
overwrote it (including a PUT that only changes ``events`` but still
|
|
761
|
+
carries the unchanged ``url`` in its body); an append-only membership does
|
|
762
|
+
not have that failure mode, because nothing removes a row from it. This is
|
|
763
|
+
exactly the "grant minted through a live session by someone who is now
|
|
764
|
+
being contained" case two paragraphs up, for an update rather than a
|
|
765
|
+
create. Program webhooks record an editor too, for symmetry, but it is
|
|
766
|
+
always the same user as ``created_by`` there because
|
|
767
|
+
``ProgramWebhookViewSet`` already restricts update to the creator
|
|
768
|
+
(ADR-0076/#4310) — so this clause adds real coverage only on the
|
|
769
|
+
project-scoped path.
|
|
770
|
+
|
|
771
|
+
``user=None`` means instance-wide — every active grant, not one person's —
|
|
772
|
+
which is what the ``revoke_api_tokens --all`` full-compromise lever needs;
|
|
773
|
+
a scoped ``user`` is the off-boarding / single-account case.
|
|
774
|
+
|
|
775
|
+
Revoking a share link is cheap for a team to recover from (an Admin mints a
|
|
776
|
+
new one); clearing a webhook secret degrades ``GitWebhookIngestView`` to its
|
|
777
|
+
existing "no secret set" 404 refusal (fail-closed by construction, not a new
|
|
778
|
+
code path) until an Admin rotates a fresh one; deactivating an outgoing
|
|
779
|
+
``Webhook`` stops dispatch immediately (``dispatch_webhooks`` only fans out to
|
|
780
|
+
``is_active=True`` rows) and an Admin re-enables it once the credential issue
|
|
781
|
+
is resolved, which also clears the delivery-health counters (see
|
|
782
|
+
``WebhookSerializer.update``). All three are lower-disruption than the
|
|
783
|
+
project-/program-scoped *API tokens* this module deliberately leaves alone
|
|
784
|
+
on off-boarding (see ``_revoke_offboarded_credentials``) — those are shared
|
|
785
|
+
integration credentials embedded in other people's scripts and CI, not a
|
|
786
|
+
single admin's config action.
|
|
787
|
+
|
|
788
|
+
Returns ``(share_links_revoked, automation_secrets_cleared, webhooks_deactivated)``.
|
|
789
|
+
"""
|
|
790
|
+
# Local import: projects/integrations/webhooks import from access, so importing
|
|
791
|
+
# these at module top would create a circular dependency (mirrors the ApiToken
|
|
792
|
+
# import above).
|
|
793
|
+
from trueppm_api.apps.integrations.models import BoardAutomation
|
|
794
|
+
from trueppm_api.apps.projects.models import ShareLink
|
|
795
|
+
from trueppm_api.apps.webhooks.models import Webhook
|
|
796
|
+
|
|
797
|
+
now = timezone.now()
|
|
798
|
+
|
|
799
|
+
links = ShareLink.objects.filter(revoked_at__isnull=True)
|
|
800
|
+
if user is not None:
|
|
801
|
+
links = links.filter(created_by=user)
|
|
802
|
+
links_revoked = links.update(revoked_at=now, revoked_by=actor)
|
|
803
|
+
|
|
804
|
+
secrets_qs = BoardAutomation.objects.exclude(secret_ciphertext=b"")
|
|
805
|
+
if user is not None:
|
|
806
|
+
secrets_qs = secrets_qs.filter(configured_by=user)
|
|
807
|
+
secrets_cleared = secrets_qs.update(secret_ciphertext=b"", secret_set_at=None)
|
|
808
|
+
|
|
809
|
+
webhooks_qs = Webhook.objects.filter(is_active=True)
|
|
810
|
+
if user is not None:
|
|
811
|
+
webhooks_qs = webhooks_qs.filter(models.Q(created_by=user) | models.Q(editors__user=user))
|
|
812
|
+
# Deliberately neutral: this fires whenever EITHER leg above matches, and the
|
|
813
|
+
# editors leg means the row's registrant may be a different, untouched account
|
|
814
|
+
# (#4461) — "the registrant's credentials were revoked" would misattribute the
|
|
815
|
+
# cause on exactly that path. The UI/docs tell the admin to verify url/secret
|
|
816
|
+
# before re-enabling regardless of which leg fired, so the reason does not need
|
|
817
|
+
# to (and cannot cheaply) distinguish them.
|
|
818
|
+
webhooks_deactivated = webhooks_qs.update(
|
|
819
|
+
is_active=False,
|
|
820
|
+
disabled_at=now,
|
|
821
|
+
disabled_reason="Deactivated: a security sweep revoked a credential tied to this webhook.",
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
# Pending workspace invites the user minted (#4589). Not part of the returned
|
|
825
|
+
# tuple (its shape is relied on by every caller); the sweep audits itself.
|
|
826
|
+
revoke_pending_workspace_invites(user, actor=actor)
|
|
827
|
+
|
|
828
|
+
return links_revoked, secrets_cleared, webhooks_deactivated
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def revoke_pending_workspace_invites(
|
|
832
|
+
user: Any = None, *, actor: Any = None, min_role: int | None = None
|
|
833
|
+
) -> int:
|
|
834
|
+
"""Revoke the not-yet-accepted ``WorkspaceInvite`` rows ``user`` minted (#4589).
|
|
835
|
+
|
|
836
|
+
An invite is a durable, role-carrying bearer grant (7-day token, role up to one
|
|
837
|
+
rank below the inviter) that works for an outside address with no session at
|
|
838
|
+
all. A contained Admin's off-boarding, password reset and
|
|
839
|
+
``revoke_api_tokens`` all left it live, and ``resend`` could re-mint it.
|
|
840
|
+
``FAILED`` (send-exhausted) invites are swept too because resend revives them;
|
|
841
|
+
``REVOKED`` is terminal and never resendable.
|
|
842
|
+
|
|
843
|
+
``min_role`` limits the sweep to invites granting at least that workspace role
|
|
844
|
+
(used on demotion: invites the sender can no longer vouch for).
|
|
845
|
+
|
|
846
|
+
``user=None`` is instance-wide (``revoke_api_tokens --all``). One
|
|
847
|
+
``INVITE_REVOKED`` audit row per invite, written in the caller's transaction.
|
|
848
|
+
Returns the number of invites revoked.
|
|
849
|
+
"""
|
|
850
|
+
from trueppm_api.apps.workspace.models import AuditEventType, InviteStatus, WorkspaceInvite
|
|
851
|
+
from trueppm_api.apps.workspace.services import _actor_label, record_audit_event
|
|
852
|
+
|
|
853
|
+
qs = WorkspaceInvite.objects.select_for_update(of=("self",)).filter(
|
|
854
|
+
status__in=(InviteStatus.PENDING, InviteStatus.FAILED)
|
|
855
|
+
)
|
|
856
|
+
if user is not None:
|
|
857
|
+
qs = qs.filter(invited_by=user)
|
|
858
|
+
if min_role is not None:
|
|
859
|
+
qs = qs.filter(role__gte=min_role)
|
|
860
|
+
invites = list(qs.select_related("invited_by"))
|
|
861
|
+
for invite in invites:
|
|
862
|
+
invite.status = InviteStatus.REVOKED
|
|
863
|
+
invite.email_pending = False
|
|
864
|
+
invite.email_token = ""
|
|
865
|
+
invite.save(update_fields=["status", "email_pending", "email_token"])
|
|
866
|
+
record_audit_event(
|
|
867
|
+
event_type=AuditEventType.INVITE_REVOKED,
|
|
868
|
+
actor=actor,
|
|
869
|
+
target_type="invite",
|
|
870
|
+
target_id=invite.pk,
|
|
871
|
+
target_label=invite.email,
|
|
872
|
+
metadata={
|
|
873
|
+
"role": invite.get_role_display(),
|
|
874
|
+
"invited_by": _actor_label(invite.invited_by),
|
|
875
|
+
"reason": "security_sweep",
|
|
876
|
+
},
|
|
877
|
+
)
|
|
878
|
+
return len(invites)
|