trueppm-api 0.4.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +14 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +164 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +2837 -0
- trueppm_api/apps/access/serializers.py +946 -0
- trueppm_api/apps/access/services.py +667 -0
- trueppm_api/apps/access/signals.py +103 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +95 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1931 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +108 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +418 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +177 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +837 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +403 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +622 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +519 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +731 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1106 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +277 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +500 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1231 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +436 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +447 -0
- trueppm_api/apps/notifications/email_backend.py +330 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1197 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +961 -0
- trueppm_api/apps/notifications/services.py +1451 -0
- trueppm_api/apps/notifications/tasks.py +711 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +904 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +551 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +969 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +523 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +466 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +449 -0
- trueppm_api/apps/projects/backfill.py +287 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +204 -0
- trueppm_api/apps/projects/config_notice.py +1325 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +93 -0
- trueppm_api/apps/projects/decisions_views.py +94 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +353 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +8946 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +575 -0
- trueppm_api/apps/projects/program_schedule.py +745 -0
- trueppm_api/apps/projects/program_views.py +3220 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/exporter.py +1644 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2390 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +126 -0
- trueppm_api/apps/projects/seed/replay.py +1691 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1149 -0
- trueppm_api/apps/projects/serializers.py +12310 -0
- trueppm_api/apps/projects/services.py +6966 -0
- trueppm_api/apps/projects/share_serializers.py +132 -0
- trueppm_api/apps/projects/share_services.py +325 -0
- trueppm_api/apps/projects/share_views.py +374 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +909 -0
- trueppm_api/apps/projects/signal_privacy_views.py +476 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +266 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1152 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1917 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +258 -0
- trueppm_api/apps/projects/template_views.py +799 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1011 -0
- trueppm_api/apps/projects/utilization.py +885 -0
- trueppm_api/apps/projects/views.py +20572 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1749 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +119 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +516 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +283 -0
- trueppm_api/apps/scheduling/serializers.py +564 -0
- trueppm_api/apps/scheduling/services.py +1182 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +1984 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2564 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +437 -0
- trueppm_api/apps/sso/services.py +1385 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +320 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +471 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +225 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +371 -0
- trueppm_api/apps/sync/serializers.py +687 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +501 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +930 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +434 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +117 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +526 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +144 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +372 -0
- trueppm_api/apps/webhooks/serializers.py +318 -0
- trueppm_api/apps/webhooks/tasks.py +663 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +383 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +368 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +242 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +919 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +555 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1502 -0
- trueppm_api/asgi.py +42 -0
- trueppm_api/celery.py +31 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +916 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +949 -0
- trueppm_api/core/password_reset.py +500 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +884 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +344 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2455 -0
- trueppm_api/settings/dev.py +273 -0
- trueppm_api/settings/prod.py +228 -0
- trueppm_api/urls.py +153 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
- trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
- trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Generated by Django 5.2.16 on 2026-08-23 13:09
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("integrations", "0012_boardautomation_last_delivery_at_and_more"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="externalsyncrequest",
|
|
14
|
+
name="attempt_count",
|
|
15
|
+
field=models.PositiveIntegerField(default=0),
|
|
16
|
+
),
|
|
17
|
+
migrations.AddField(
|
|
18
|
+
model_name="externalsyncrequest",
|
|
19
|
+
name="next_attempt_at",
|
|
20
|
+
field=models.DateTimeField(
|
|
21
|
+
blank=True,
|
|
22
|
+
help_text="Earliest time the drain may re-dispatch this row (rate-limit backoff).",
|
|
23
|
+
null=True,
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"""Data model for external integrations (ADR-0049 §3).
|
|
2
|
+
|
|
3
|
+
This module owns two tables:
|
|
4
|
+
|
|
5
|
+
- ``IntegrationCredential`` — per-user PAT storage that the User → Settings →
|
|
6
|
+
Connected Accounts page (#587) lists and ``TaskLink`` refresh consumes.
|
|
7
|
+
- ``TaskLink`` (#637) — a GitLab/GitHub/generic URL attached to a task, with a
|
|
8
|
+
cached status (open / draft / merged / closed / unknown) refreshed on demand.
|
|
9
|
+
|
|
10
|
+
Sync note: ``IntegrationCredential`` does **not** inherit ``VersionedModel`` —
|
|
11
|
+
PATs are intentionally never synced to (or serialized back to) any client,
|
|
12
|
+
including the owner (ADR-0049 §3). ``TaskLink`` **does** inherit
|
|
13
|
+
``VersionedModel`` for offline-sync parity with ``Task``, so link adds/removes
|
|
14
|
+
and status changes reach the mobile client through the project sync delta.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import uuid
|
|
20
|
+
from typing import Any, ClassVar
|
|
21
|
+
|
|
22
|
+
from django.conf import settings
|
|
23
|
+
from django.contrib.postgres.fields import ArrayField
|
|
24
|
+
from django.db import models
|
|
25
|
+
from django.utils import timezone
|
|
26
|
+
|
|
27
|
+
from trueppm_api.apps.projects.models import VersionedModel
|
|
28
|
+
|
|
29
|
+
from .encryption import encrypt_secret
|
|
30
|
+
from .registry import (
|
|
31
|
+
LINK_STATUS_CHOICES,
|
|
32
|
+
LINK_STATUS_UNKNOWN,
|
|
33
|
+
PREVIEW_TYPE_CHOICES,
|
|
34
|
+
TASK_LINK_PROVIDERS,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class IntegrationCredential(models.Model):
|
|
39
|
+
"""A user's encrypted PAT for one integration provider (ADR-0049 §3).
|
|
40
|
+
|
|
41
|
+
The ``provider`` field is a free-form ``CharField`` validated against
|
|
42
|
+
``TASK_LINK_PROVIDERS.keys()`` at write time so Enterprise can register
|
|
43
|
+
additional providers (``jira``, ``servicenow``, ``bitbucket``,
|
|
44
|
+
``azure_devops``) without an OSS migration. ``secret_ciphertext`` is
|
|
45
|
+
Fernet-encrypted with ``settings.INTEGRATION_ENCRYPTION_KEY`` and is
|
|
46
|
+
**never** returned to any client — the serializer exposes only an
|
|
47
|
+
``exists`` flag, the ``base_url``, and timestamps.
|
|
48
|
+
|
|
49
|
+
The ``(user, provider)`` pair is unique: each user has at most one
|
|
50
|
+
credential per provider. Connecting a second time rotates the secret
|
|
51
|
+
via update-or-create rather than appending a row, which keeps the
|
|
52
|
+
credentials list a flat one-per-provider read on the page.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
56
|
+
user = models.ForeignKey(
|
|
57
|
+
settings.AUTH_USER_MODEL,
|
|
58
|
+
on_delete=models.CASCADE,
|
|
59
|
+
related_name="integration_credentials",
|
|
60
|
+
)
|
|
61
|
+
provider = models.CharField(max_length=32)
|
|
62
|
+
# Encrypted PAT — Fernet-encrypted bytes blob, never returned to clients.
|
|
63
|
+
secret_ciphertext = models.BinaryField()
|
|
64
|
+
# Self-hosted GitLab / GitHub Enterprise base URL. Empty string for
|
|
65
|
+
# gitlab.com / github.com so we can distinguish "not provided" from
|
|
66
|
+
# "explicitly cleared" without making the column NULL-able.
|
|
67
|
+
base_url = models.CharField(max_length=512, blank=True, default="")
|
|
68
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
69
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
70
|
+
# Stamped by the git-aware tasks refresh endpoint when the credential
|
|
71
|
+
# was last used to fetch link metadata (#637). Null until then.
|
|
72
|
+
last_used_at = models.DateTimeField(null=True, blank=True)
|
|
73
|
+
# Optional expiration the user can record so the UI can warn before a
|
|
74
|
+
# PAT lapses. Not enforced server-side — PAT validity is enforced by
|
|
75
|
+
# the upstream provider — this is purely a UX hint.
|
|
76
|
+
expires_at = models.DateTimeField(null=True, blank=True)
|
|
77
|
+
# Per-connection settings for a user-scoped external task source (ADR-0097 §2).
|
|
78
|
+
# For an EXTERNAL_TASK_SOURCES connection (e.g. ``jira``) this holds
|
|
79
|
+
# ``{"account_email", "jql", "project_keys"}``; for a plain git-PAT
|
|
80
|
+
# TASK_LINK_PROVIDERS credential it stays ``{}``. Extending the existing
|
|
81
|
+
# credential row (rather than a new model) keeps one row per (user, provider)
|
|
82
|
+
# and reuses the Fernet-encrypted ``secret_ciphertext`` store — the ADR-0097
|
|
83
|
+
# §2 "reuse IntegrationCredential" decision. Never carries the secret itself.
|
|
84
|
+
config = models.JSONField(default=dict, blank=True)
|
|
85
|
+
|
|
86
|
+
class Meta:
|
|
87
|
+
constraints = [
|
|
88
|
+
# The unique constraint creates an implicit btree on
|
|
89
|
+
# ``(user, provider)`` in that column order — Postgres uses it
|
|
90
|
+
# for the per-user list lookups too. An explicit ``models.Index``
|
|
91
|
+
# would be a redundant copy that doubles write cost.
|
|
92
|
+
models.UniqueConstraint(
|
|
93
|
+
fields=("user", "provider"),
|
|
94
|
+
name="integrations_credential_unique_per_user_provider",
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
ordering = ("provider",)
|
|
98
|
+
verbose_name = "Integration credential"
|
|
99
|
+
verbose_name_plural = "Integration credentials"
|
|
100
|
+
|
|
101
|
+
def __str__(self) -> str: # pragma: no cover — debugging aid only
|
|
102
|
+
return f"{self.provider} for user {self.user_id}"
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def upsert(
|
|
106
|
+
cls,
|
|
107
|
+
*,
|
|
108
|
+
user: object,
|
|
109
|
+
provider: str,
|
|
110
|
+
secret: str,
|
|
111
|
+
base_url: str = "",
|
|
112
|
+
expires_at: object | None = None,
|
|
113
|
+
) -> IntegrationCredential:
|
|
114
|
+
"""Encrypt and store a PAT, replacing any existing row for the pair.
|
|
115
|
+
|
|
116
|
+
Connect and rotate are the same operation — both produce one row
|
|
117
|
+
per ``(user, provider)`` — so callers don't need to branch on
|
|
118
|
+
whether a row already exists. The plaintext ``secret`` is encrypted
|
|
119
|
+
before the row touches the database.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If ``provider`` is not registered in
|
|
123
|
+
``TASK_LINK_PROVIDERS``. Validation happens here rather
|
|
124
|
+
than in the serializer so the model is the single source
|
|
125
|
+
of truth for the provider-validation rule.
|
|
126
|
+
"""
|
|
127
|
+
if TASK_LINK_PROVIDERS.get(provider) is None:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"Unknown provider {provider!r}. Known: {', '.join(TASK_LINK_PROVIDERS.keys())}"
|
|
130
|
+
)
|
|
131
|
+
ciphertext = encrypt_secret(secret)
|
|
132
|
+
obj, _ = cls.objects.update_or_create(
|
|
133
|
+
user=user,
|
|
134
|
+
provider=provider,
|
|
135
|
+
defaults={
|
|
136
|
+
"secret_ciphertext": ciphertext,
|
|
137
|
+
"base_url": base_url or "",
|
|
138
|
+
"expires_at": expires_at,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
return obj
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TaskLink(VersionedModel):
|
|
145
|
+
"""A GitLab/GitHub/generic URL attached to a task (ADR-0049 §3, #637).
|
|
146
|
+
|
|
147
|
+
Inherits ``VersionedModel`` (UUID PK, ``server_version``, ``is_deleted``,
|
|
148
|
+
``deleted_version``) so link adds/removes and status changes flow to the
|
|
149
|
+
mobile client through the project sync delta, exactly like ``Task`` and
|
|
150
|
+
``Dependency``.
|
|
151
|
+
|
|
152
|
+
``provider`` is a free-form ``CharField`` validated against
|
|
153
|
+
``TASK_LINK_PROVIDERS.keys()`` at write time (Enterprise can register
|
|
154
|
+
``jira``/``bitbucket``/… without an OSS migration). ``status`` is a cached
|
|
155
|
+
classification refreshed on demand via the provider's API — there is **no**
|
|
156
|
+
background polling; it goes stale until the user (or mobile) hits refresh,
|
|
157
|
+
which is why ``fetched_at`` is surfaced so the UI can show "as of …".
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
# Explicit default manager. The django-stubs plugin does not inject the
|
|
161
|
+
# implicit ``objects`` manager for this model (a cross-app abstract-base
|
|
162
|
+
# quirk: ``VersionedModel`` lives in ``projects``), so we declare it so
|
|
163
|
+
# ``TaskLink.objects`` type-checks. Runtime behavior is identical to the
|
|
164
|
+
# implicit default manager.
|
|
165
|
+
objects: ClassVar[models.Manager[TaskLink]] = models.Manager()
|
|
166
|
+
|
|
167
|
+
task = models.ForeignKey(
|
|
168
|
+
"projects.Task",
|
|
169
|
+
on_delete=models.CASCADE,
|
|
170
|
+
related_name="links",
|
|
171
|
+
)
|
|
172
|
+
url = models.URLField(max_length=2048)
|
|
173
|
+
# Validated against TASK_LINK_PROVIDERS at write time (see clean()); not a
|
|
174
|
+
# DB-level choices constraint so Enterprise providers need no OSS migration.
|
|
175
|
+
provider = models.CharField(max_length=32)
|
|
176
|
+
# Human title the provider reported (PR/MR/issue title). Blank until the
|
|
177
|
+
# first successful refresh; the UI falls back to ``custom_title`` then the URL.
|
|
178
|
+
title = models.CharField(max_length=512, blank=True, default="")
|
|
179
|
+
# User-supplied display name (#970). Distinct from the provider-fetched
|
|
180
|
+
# ``title`` so a refresh updates ``title`` only and never clobbers what the
|
|
181
|
+
# user typed — and so a *generic* link (no provider title) can still carry a
|
|
182
|
+
# human name. Display precedence is ``custom_title or title or url``.
|
|
183
|
+
custom_title = models.CharField(max_length=512, blank=True, default="")
|
|
184
|
+
# Free-text categorization tags (#970), e.g. ["spec", "design"]. OSS-simple:
|
|
185
|
+
# no shared per-project taxonomy and no colors — the serializer trims,
|
|
186
|
+
# de-dupes, and caps the list. A plain text-array column (not a join table)
|
|
187
|
+
# because labels are read with the link and never queried independently.
|
|
188
|
+
labels = ArrayField(
|
|
189
|
+
models.CharField(max_length=40),
|
|
190
|
+
blank=True,
|
|
191
|
+
default=list,
|
|
192
|
+
)
|
|
193
|
+
# Cached status — one of LINK_STATUS_VALUES. Starts "unknown" on create
|
|
194
|
+
# (nothing fetched yet) and is updated by the refresh endpoint.
|
|
195
|
+
status = models.CharField(
|
|
196
|
+
max_length=16,
|
|
197
|
+
choices=LINK_STATUS_CHOICES,
|
|
198
|
+
default=LINK_STATUS_UNKNOWN,
|
|
199
|
+
)
|
|
200
|
+
# When the cached status/title was last fetched from the provider. Null
|
|
201
|
+
# until the first refresh — drives the "as of …" / "never refreshed" hint.
|
|
202
|
+
fetched_at = models.DateTimeField(null=True, blank=True)
|
|
203
|
+
# --- Cloud-file preview cache (#571, ADR-0163) ----------------------------
|
|
204
|
+
# Populated by a file provider's OpenGraph unfurl on refresh; empty for git
|
|
205
|
+
# and generic links. Stored here (on the synced row) rather than a side cache
|
|
206
|
+
# so the preview card renders offline on the mobile client from the sync delta.
|
|
207
|
+
# OpenGraph/Twitter description of a previewed file.
|
|
208
|
+
description = models.CharField(max_length=1024, blank=True, default="")
|
|
209
|
+
# Safe https preview-image URL (server drops non-https / blocked candidates).
|
|
210
|
+
thumbnail_url = models.URLField(max_length=2048, blank=True, default="")
|
|
211
|
+
# File class for the card's glyph/chip — one of PREVIEW_TYPE_VALUES, or "" for
|
|
212
|
+
# a link with no file preview (git, generic, not-yet-refreshed file).
|
|
213
|
+
preview_type = models.CharField(
|
|
214
|
+
max_length=16,
|
|
215
|
+
choices=PREVIEW_TYPE_CHOICES,
|
|
216
|
+
blank=True,
|
|
217
|
+
default="",
|
|
218
|
+
)
|
|
219
|
+
# Manual ordering within a task's link list (lower sorts first).
|
|
220
|
+
display_order = models.PositiveIntegerField(default=0)
|
|
221
|
+
# Creation timestamp (#971, ADR-0215). VersionedModel deliberately carries no
|
|
222
|
+
# created/updated timestamps (sync uses ``server_version``), but the unified
|
|
223
|
+
# Assets feed merges links with ``TaskAttachment`` on a shared ``(created_at,
|
|
224
|
+
# id)`` keyset — links need a real creation time to place chronologically.
|
|
225
|
+
# ``default=timezone.now`` (not ``auto_now_add``) so the additive migration
|
|
226
|
+
# backfills existing rows without an interactive one-off default; the value is
|
|
227
|
+
# write-once in practice (nothing mutates it) and never exposed for client
|
|
228
|
+
# write. Not added to ``TaskLinkSerializer`` — the sync/mobile shape is
|
|
229
|
+
# unchanged.
|
|
230
|
+
created_at = models.DateTimeField(default=timezone.now, editable=False)
|
|
231
|
+
|
|
232
|
+
class Meta:
|
|
233
|
+
ordering = ("display_order", "id")
|
|
234
|
+
indexes = [
|
|
235
|
+
# The link list is always read per-task; index the FK + ordering so
|
|
236
|
+
# the section query is a single index range scan.
|
|
237
|
+
models.Index(fields=("task", "display_order"), name="integrations_link_task_order"),
|
|
238
|
+
# Sync delta pull joins via task then filters server_version (#810).
|
|
239
|
+
models.Index(fields=("task", "server_version"), name="tasklink_serverver_idx"),
|
|
240
|
+
models.Index(fields=["task", "sync_seq"], name="tasklink_syncseq_idx"),
|
|
241
|
+
# Assets feed (#971, ADR-0215): each source is scanned newest-first with
|
|
242
|
+
# a small LIMIT before the Python keyset merge — index the sort key so
|
|
243
|
+
# the per-source page is an index range scan, not a filesort.
|
|
244
|
+
models.Index(fields=("-created_at", "-id"), name="tasklink_created_idx"),
|
|
245
|
+
]
|
|
246
|
+
verbose_name = "Task link"
|
|
247
|
+
verbose_name_plural = "Task links"
|
|
248
|
+
|
|
249
|
+
def __str__(self) -> str: # pragma: no cover — debugging aid only
|
|
250
|
+
return f"{self.provider} link on task {self.task_id}"
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def project_id(self) -> Any:
|
|
254
|
+
"""Surface the parent project's id for the RBAC resolver.
|
|
255
|
+
|
|
256
|
+
``access.permissions._get_project_id_from_obj`` traverses ``obj.project_id``
|
|
257
|
+
to gate project-scoped permissions. A task-nested model has no direct
|
|
258
|
+
project FK, so we expose the parent task's project id here — the same
|
|
259
|
+
pattern ``TaskAttachment`` uses, so ``IsProjectMember``/``IsProjectMemberWrite``
|
|
260
|
+
resolve the project for object-level checks (create/destroy/refresh).
|
|
261
|
+
"""
|
|
262
|
+
return self.task.project_id
|
|
263
|
+
|
|
264
|
+
def clean(self) -> None:
|
|
265
|
+
"""Validate ``provider`` against the live registry.
|
|
266
|
+
|
|
267
|
+
Mirrors ``IntegrationCredential.upsert``'s rule so the model is the
|
|
268
|
+
single source of truth for provider validation — the serializer calls
|
|
269
|
+
``full_clean`` so an unknown key is a 400, not a 500 at read time.
|
|
270
|
+
"""
|
|
271
|
+
super().clean()
|
|
272
|
+
from django.core.exceptions import ValidationError
|
|
273
|
+
|
|
274
|
+
if TASK_LINK_PROVIDERS.get(self.provider) is None:
|
|
275
|
+
raise ValidationError({"provider": f"Unknown provider {self.provider!r}."})
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class BoardAutomation(models.Model):
|
|
279
|
+
"""Per-project Git-event card automation config (#329, ADR-0158).
|
|
280
|
+
|
|
281
|
+
Holds the off-by-default toggle and the shared webhook secret for a single
|
|
282
|
+
project's inbound Git-event auto-move. When ``enabled`` and a matching
|
|
283
|
+
``TaskLink`` exists, a verified ``pull_request``/``merge_request`` webhook
|
|
284
|
+
advances that task's board status forward (opened → REVIEW, merged →
|
|
285
|
+
COMPLETE) through the normal ``TaskSerializer`` write path.
|
|
286
|
+
|
|
287
|
+
Plain ``models.Model`` (not ``VersionedModel``) — like ``IntegrationCredential``
|
|
288
|
+
this is project configuration that is **never** synced to the mobile offline
|
|
289
|
+
client; it carries a secret and has no place in the sync delta.
|
|
290
|
+
|
|
291
|
+
The secret is **Fernet-encrypted, not hashed.** Signature verification needs
|
|
292
|
+
the plaintext at request time — GitLab compares ``X-Gitlab-Token`` directly and
|
|
293
|
+
GitHub recomputes ``HMAC-SHA256`` over the raw body — so the one-way SHA-256
|
|
294
|
+
pattern used for ``ApiToken`` cannot work here. We reuse ``IntegrationCredential``'s
|
|
295
|
+
at-rest pattern (``BinaryField`` + ``encryption.encrypt_secret``). The plaintext
|
|
296
|
+
is shown once on rotation and never returned again.
|
|
297
|
+
|
|
298
|
+
``configured_by`` is the admin who enabled automation; it becomes the
|
|
299
|
+
accountable ``request.user`` for the status write (mirrors ADR-0148 using
|
|
300
|
+
``token.created_by``) so RBAC and ``django-simple-history`` see a real user,
|
|
301
|
+
while the activity timeline still classifies the edit as automation via the
|
|
302
|
+
``history_change_reason`` (ADR-0096).
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
306
|
+
project = models.OneToOneField(
|
|
307
|
+
"projects.Project",
|
|
308
|
+
on_delete=models.CASCADE,
|
|
309
|
+
related_name="git_automation",
|
|
310
|
+
)
|
|
311
|
+
# Off by default (AC1): automation never fires until an admin opts in.
|
|
312
|
+
enabled = models.BooleanField(default=False)
|
|
313
|
+
# Fernet-encrypted webhook secret; never returned to any client. Empty until
|
|
314
|
+
# an admin mints one via the rotate-secret endpoint.
|
|
315
|
+
secret_ciphertext = models.BinaryField(blank=True, default=b"")
|
|
316
|
+
# When the current secret was minted/rotated — drives the "secret set" hint.
|
|
317
|
+
secret_set_at = models.DateTimeField(null=True, blank=True)
|
|
318
|
+
configured_by = models.ForeignKey(
|
|
319
|
+
settings.AUTH_USER_MODEL,
|
|
320
|
+
on_delete=models.SET_NULL,
|
|
321
|
+
null=True,
|
|
322
|
+
blank=True,
|
|
323
|
+
related_name="+",
|
|
324
|
+
)
|
|
325
|
+
# --- Delivery diagnostics (#2882) --------------------------------------------
|
|
326
|
+
# A failed webhook used to be invisible to everyone: every non-auth outcome was
|
|
327
|
+
# a 200, the receiver logged nothing, and the only diagnostic surface was the
|
|
328
|
+
# provider's own Recent Deliveries tab — outside the operator's reach. An
|
|
329
|
+
# operator could complete every documented setup step, get a green check from
|
|
330
|
+
# GitHub, and have zero cards ever move with nothing anywhere saying why (a
|
|
331
|
+
# missing TaskLink is the most common cause and looks identical to success).
|
|
332
|
+
#
|
|
333
|
+
# These fields are that missing consumer, read back by the admin-only config GET
|
|
334
|
+
# and rendered on the settings card. Deliberately the *last* outcome on the
|
|
335
|
+
# existing config row rather than an append-only attempt table: the receiver is
|
|
336
|
+
# anonymous and rate-bounded, so an unbounded history table on that path is a
|
|
337
|
+
# storage-growth surface an attacker controls, while a single overwritten row
|
|
338
|
+
# answers the question an operator actually asks ("what happened to the delivery
|
|
339
|
+
# I just triggered?"). Writes go through a queryset ``.update()`` so they never
|
|
340
|
+
# touch ``updated_at`` — that field means "config last changed" and a webhook
|
|
341
|
+
# does not change config.
|
|
342
|
+
#
|
|
343
|
+
# **Two independent slots, and the split is a security property, not tidiness.**
|
|
344
|
+
# A refusal is recorded before the signature is verified, so anyone holding the
|
|
345
|
+
# project's UUID can force one with two forged headers — and the UUID is not a
|
|
346
|
+
# secret; every project Viewer reads it out of the SPA URL, which is the premise
|
|
347
|
+
# of #2881. One shared slot would let that caller erase the genuine ``no_link``
|
|
348
|
+
# an admin is mid-diagnosis on, and leave the card telling the admin their secret
|
|
349
|
+
# is wrong and to rotate it — an unauthenticated party talking a working
|
|
350
|
+
# integration down. Split, hostile traffic can only reach the slot already
|
|
351
|
+
# labeled "unauthenticated, may not be your provider".
|
|
352
|
+
|
|
353
|
+
# Verified: the caller proved it holds the secret. Trustworthy.
|
|
354
|
+
last_delivery_at = models.DateTimeField(null=True, blank=True)
|
|
355
|
+
# A ``GIT_DELIVERY_*`` / service outcome token. Free-text CharField rather than
|
|
356
|
+
# ``choices``: the vocabulary is owned by the receiver and grows with the event
|
|
357
|
+
# taxonomy, and a stale ``choices`` list would turn adding an outcome into a
|
|
358
|
+
# migration. The API *publishes* the vocabulary through the serializer instead.
|
|
359
|
+
last_delivery_outcome = models.CharField(max_length=32, blank=True, default="")
|
|
360
|
+
last_delivery_provider = models.CharField(max_length=16, blank=True, default="")
|
|
361
|
+
|
|
362
|
+
# Refused before verification: anyone can write here. Diagnostic, not evidence.
|
|
363
|
+
last_refusal_at = models.DateTimeField(null=True, blank=True)
|
|
364
|
+
last_refusal_outcome = models.CharField(max_length=32, blank=True, default="")
|
|
365
|
+
last_refusal_provider = models.CharField(max_length=16, blank=True, default="")
|
|
366
|
+
|
|
367
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
368
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
369
|
+
|
|
370
|
+
class Meta:
|
|
371
|
+
verbose_name = "board automation"
|
|
372
|
+
verbose_name_plural = "board automations"
|
|
373
|
+
|
|
374
|
+
def __str__(self) -> str:
|
|
375
|
+
state = "on" if self.enabled else "off"
|
|
376
|
+
return f"BoardAutomation(project={self.project_id}, {state})"
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def has_secret(self) -> bool:
|
|
380
|
+
"""Whether a webhook secret has been minted (drives ``secret_set`` in the API)."""
|
|
381
|
+
return bool(self.secret_ciphertext)
|
|
382
|
+
|
|
383
|
+
def set_secret(self, plaintext: str) -> None:
|
|
384
|
+
"""Fernet-encrypt and store ``plaintext`` as the webhook secret.
|
|
385
|
+
|
|
386
|
+
Does not save — the caller persists inside its own transaction so the
|
|
387
|
+
secret and ``secret_set_at`` land atomically.
|
|
388
|
+
"""
|
|
389
|
+
from django.utils import timezone
|
|
390
|
+
|
|
391
|
+
# ASCII is a *correctness* invariant, not a style preference, and it is
|
|
392
|
+
# enforced here because here is the only place it can be. Signature
|
|
393
|
+
# verification compares the stored secret against a request header, and the
|
|
394
|
+
# header has to be encoded from text back to bytes to make that comparison
|
|
395
|
+
# safe (see ``core.constant_time.constant_time_equal``, #2881/#2929). That
|
|
396
|
+
# encode falls back from latin-1 to utf-8, and the two codecs agree only on
|
|
397
|
+
# ASCII. #2929 made the primitive encode *both* sides the same way, which
|
|
398
|
+
# closed the confusable-match this guard originally existed to prevent
|
|
399
|
+
# (``"é"`` under latin-1 is the same bytes as ``"é"`` under utf-8); what a
|
|
400
|
+
# non-ASCII secret would now do is fail to verify at all, because the wire
|
|
401
|
+
# bytes of the header round-trip through latin-1 and never match the
|
|
402
|
+
# secret's own latin-1 encoding. Refusing it at write time is still the
|
|
403
|
+
# right call — a silently-never-authenticating webhook is a worse bug report
|
|
404
|
+
# than a rejected secret. Every secret today comes from
|
|
405
|
+
# ``secrets.token_urlsafe`` and is ASCII by construction; this guard is what
|
|
406
|
+
# keeps that true the day someone adds a "paste your existing provider
|
|
407
|
+
# secret" path, which is a routine request for a feature shaped like this
|
|
408
|
+
# one.
|
|
409
|
+
if not plaintext.isascii():
|
|
410
|
+
raise ValueError("webhook secret must be ASCII")
|
|
411
|
+
|
|
412
|
+
self.secret_ciphertext = encrypt_secret(plaintext)
|
|
413
|
+
self.secret_set_at = timezone.now()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class ExternalWorkItem(models.Model):
|
|
417
|
+
"""A per-user cached snapshot of one remote work item (ADR-0097 §2).
|
|
418
|
+
|
|
419
|
+
Populated read-only by the #1419 sync worker from an ``EXTERNAL_TASK_SOURCES``
|
|
420
|
+
source (e.g. the user's assigned Jira issues) and surfaced in My Work. It is
|
|
421
|
+
a **personal cache, not project data.**
|
|
422
|
+
|
|
423
|
+
**Isolation invariant (ADR-0097 §2, test-enforced in ``tests/``):** this is a
|
|
424
|
+
plain ``models.Model``, deliberately **not** ``VersionedModel``. It carries no
|
|
425
|
+
``server_version``, never enters the WebSocket board broadcast, never joins
|
|
426
|
+
the project sync delta / offline tombstone protocol, and the pull can **never**
|
|
427
|
+
mint a ``Task``. That line is what keeps the feature OSS-and-read-only: the day
|
|
428
|
+
someone makes this a ``VersionedModel`` "to get mobile offline," the Apache-2.0
|
|
429
|
+
boundary silently breaks, so a regression test asserts every clause here.
|
|
430
|
+
|
|
431
|
+
Strictly personal visibility: every read filters ``user=request.user``; no
|
|
432
|
+
project member, Admin, or Owner can see another user's items (ADR-0097 §3).
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
436
|
+
user = models.ForeignKey(
|
|
437
|
+
settings.AUTH_USER_MODEL,
|
|
438
|
+
on_delete=models.CASCADE,
|
|
439
|
+
related_name="external_work_items",
|
|
440
|
+
)
|
|
441
|
+
# Source key, validated against EXTERNAL_TASK_SOURCES at write time (clean()).
|
|
442
|
+
# Free-form CharField (not DB choices) so Enterprise sources need no OSS
|
|
443
|
+
# migration — mirrors the IntegrationCredential.provider pattern.
|
|
444
|
+
source = models.CharField(max_length=32)
|
|
445
|
+
# Provider-side identifier — the human key for Jira ("RIV-482").
|
|
446
|
+
external_id = models.CharField(max_length=255)
|
|
447
|
+
external_url = models.URLField(max_length=2000, blank=True, default="")
|
|
448
|
+
title = models.CharField(max_length=512, blank=True, default="")
|
|
449
|
+
# Raw provider status ("In Review") preserved for display; the coarse
|
|
450
|
+
# grouping bucket is ``display_bucket``.
|
|
451
|
+
external_status = models.CharField(max_length=64, blank=True, default="")
|
|
452
|
+
# One of external_sources.DISPLAY_BUCKETS (todo / in_progress / done).
|
|
453
|
+
display_bucket = models.CharField(max_length=12, default="todo")
|
|
454
|
+
# Provider-side due date, or null when the item has none. Drives the My Work
|
|
455
|
+
# ordering (``due_date asc nulls last``, #1422) and today/upcoming grouping;
|
|
456
|
+
# the source already fetches it (Jira ``duedate``) and the DTO carries it.
|
|
457
|
+
due_date = models.DateField(null=True, blank=True)
|
|
458
|
+
last_synced_at = models.DateTimeField(null=True, blank=True)
|
|
459
|
+
# Soft-remove: set True when an item stops being returned by a *successful*
|
|
460
|
+
# pull (ADR-0097 §5), so a transient partial response never wipes the list.
|
|
461
|
+
# My Work hides stale items; the nightly purge hard-deletes old ones (#1419).
|
|
462
|
+
is_stale = models.BooleanField(default=False)
|
|
463
|
+
|
|
464
|
+
class Meta:
|
|
465
|
+
constraints = [
|
|
466
|
+
# One cached row per (user, source, external_id). The unique index it
|
|
467
|
+
# creates leads with ``user``, so the per-user My Work read
|
|
468
|
+
# (``WHERE user_id = X``) uses its prefix — no separate user index.
|
|
469
|
+
models.UniqueConstraint(
|
|
470
|
+
fields=("user", "source", "external_id"),
|
|
471
|
+
name="integrations_external_item_unique",
|
|
472
|
+
),
|
|
473
|
+
]
|
|
474
|
+
indexes = [
|
|
475
|
+
# My Work groups a user's live items by bucket; index the group key so
|
|
476
|
+
# the section read is an index range scan over the bounded (≤500) set.
|
|
477
|
+
models.Index(
|
|
478
|
+
fields=("user", "is_stale", "display_bucket"),
|
|
479
|
+
name="external_item_user_bucket_idx",
|
|
480
|
+
),
|
|
481
|
+
# The nightly purge reaps soft-removed items by age with no user
|
|
482
|
+
# predicate (``is_stale=True AND last_synced_at < cutoff``, #1419).
|
|
483
|
+
# Without a leading-(is_stale) index that is a full-table seq scan
|
|
484
|
+
# whose cost grows with install-wide tenant count; this makes it an
|
|
485
|
+
# index range scan over just the stale rows.
|
|
486
|
+
models.Index(
|
|
487
|
+
fields=("is_stale", "last_synced_at"),
|
|
488
|
+
name="external_item_stale_age_idx",
|
|
489
|
+
),
|
|
490
|
+
]
|
|
491
|
+
ordering = ("display_bucket", "external_id")
|
|
492
|
+
verbose_name = "External work item"
|
|
493
|
+
verbose_name_plural = "External work items"
|
|
494
|
+
|
|
495
|
+
def __str__(self) -> str: # pragma: no cover — debugging aid only
|
|
496
|
+
return f"{self.source}:{self.external_id} for user {self.user_id}"
|
|
497
|
+
|
|
498
|
+
def clean(self) -> None:
|
|
499
|
+
"""Validate ``source`` against the live EXTERNAL_TASK_SOURCES registry.
|
|
500
|
+
|
|
501
|
+
Defense in depth — the #1419 worker only writes source keys that came
|
|
502
|
+
from a validated connection, but validating here keeps the model the
|
|
503
|
+
single source of truth (mirrors ``IntegrationCredential``/``TaskLink``).
|
|
504
|
+
"""
|
|
505
|
+
super().clean()
|
|
506
|
+
from django.core.exceptions import ValidationError
|
|
507
|
+
|
|
508
|
+
from .external_sources import EXTERNAL_TASK_SOURCES
|
|
509
|
+
|
|
510
|
+
if EXTERNAL_TASK_SOURCES.get(self.source) is None:
|
|
511
|
+
raise ValidationError({"source": f"Unknown external task source {self.source!r}."})
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class ExternalSyncRequestStatus(models.TextChoices):
|
|
515
|
+
"""Lifecycle of a transactional outbox row for an external-source pull.
|
|
516
|
+
|
|
517
|
+
Mirrors ``scheduling.ScheduleRequestStatus`` (ADR-0019 outbox shape) so the
|
|
518
|
+
drain/purge machinery reads the same across the codebase: ``done`` is the
|
|
519
|
+
ADR-0097 §Durable Execution "COMPLETED" terminal, ``dead`` is its "FAILED".
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
PENDING = "pending", "Pending"
|
|
523
|
+
DISPATCHED = "dispatched", "Dispatched"
|
|
524
|
+
DONE = "done", "Done"
|
|
525
|
+
DEAD = "dead", "Dead"
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class ExternalSyncRequestReason(models.TextChoices):
|
|
529
|
+
"""Why a pull was enqueued — forensics only, does not change dispatch."""
|
|
530
|
+
|
|
531
|
+
# User hit "Refresh" on the connection (POST .../sync/).
|
|
532
|
+
MANUAL = "manual", "Manual"
|
|
533
|
+
# My Work opened and the cache was past the staleness floor (refresh-if-stale).
|
|
534
|
+
ON_OPEN = "on_open", "On Open"
|
|
535
|
+
# The opt-in low-frequency Beat poll fired for an active connection.
|
|
536
|
+
POLL = "poll", "Poll"
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class ExternalSyncRequest(models.Model):
|
|
540
|
+
"""Transactional outbox record for a user-scoped external-source pull (ADR-0097 §4).
|
|
541
|
+
|
|
542
|
+
One row per queued pull of a ``(user, source)`` connection. Written inside the
|
|
543
|
+
request/task transaction; a best-effort ``transaction.on_commit`` dispatch
|
|
544
|
+
fires ``external_sync.delay`` and the 300 s ``drain-external-sync`` Beat task
|
|
545
|
+
recovers any row a broker blip left ``PENDING`` (ADR-0017/0019 outbox shape).
|
|
546
|
+
|
|
547
|
+
The two partial-unique constraints below enforce at-most-one ``PENDING`` and
|
|
548
|
+
at-most-one ``DISPATCHED`` row per ``(user, source)``, so a burst of manual
|
|
549
|
+
refreshes coalesces onto one in-flight pull rather than stacking N Jira
|
|
550
|
+
fetches — the per-user cooldown (ADR-0097 §Resolution #5) is the outer bound;
|
|
551
|
+
this constraint is the inner idempotency guard (mirrors ``ScheduleRequest``).
|
|
552
|
+
|
|
553
|
+
Deliberately **not** ``VersionedModel`` — an outbox row is server-side
|
|
554
|
+
dispatch state, never synced to a client (same rule as ``ScheduleRequest``).
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
558
|
+
user = models.ForeignKey(
|
|
559
|
+
settings.AUTH_USER_MODEL,
|
|
560
|
+
on_delete=models.CASCADE,
|
|
561
|
+
related_name="external_sync_requests",
|
|
562
|
+
)
|
|
563
|
+
# Source key (an EXTERNAL_TASK_SOURCES key, e.g. ``jira``). Free-form CharField
|
|
564
|
+
# to match the credential/work-item pattern — Enterprise sources need no OSS
|
|
565
|
+
# migration. The enqueue service validates it against the live registry.
|
|
566
|
+
source = models.CharField(max_length=32)
|
|
567
|
+
status = models.CharField(
|
|
568
|
+
max_length=16,
|
|
569
|
+
choices=ExternalSyncRequestStatus.choices,
|
|
570
|
+
default=ExternalSyncRequestStatus.PENDING,
|
|
571
|
+
)
|
|
572
|
+
reason = models.CharField(
|
|
573
|
+
max_length=16,
|
|
574
|
+
choices=ExternalSyncRequestReason.choices,
|
|
575
|
+
default=ExternalSyncRequestReason.MANUAL,
|
|
576
|
+
)
|
|
577
|
+
requested_at = models.DateTimeField(auto_now_add=True)
|
|
578
|
+
dispatched_at = models.DateTimeField(null=True, blank=True)
|
|
579
|
+
celery_task_id = models.CharField(max_length=255, blank=True, default="")
|
|
580
|
+
# Last failure detail for a DEAD row — surfaced only in ops/forensics, never
|
|
581
|
+
# to the client. Scrubbed of the PAT/Authorization by the worker before it
|
|
582
|
+
# lands here (ADR-0097 §Resolution #2 "never logged").
|
|
583
|
+
last_error = models.CharField(max_length=512, blank=True, default="")
|
|
584
|
+
# The retry clock for a rate-limited pull (#2924). A 429 is the source telling
|
|
585
|
+
# us when to come back, so the row is re-queued PENDING against this time
|
|
586
|
+
# rather than dead-lettered as "unreachable". Null is the ordinary case — the
|
|
587
|
+
# drain treats a null clock as "eligible now", so every pre-existing row and
|
|
588
|
+
# every non-429 path is unaffected.
|
|
589
|
+
next_attempt_at = models.DateTimeField(
|
|
590
|
+
null=True,
|
|
591
|
+
blank=True,
|
|
592
|
+
help_text="Earliest time the drain may re-dispatch this row (rate-limit backoff).",
|
|
593
|
+
)
|
|
594
|
+
# Bounds the re-queue loop so a source that 429s indefinitely retires instead
|
|
595
|
+
# of cycling forever. Counts rate-limit re-queues only, not dispatches.
|
|
596
|
+
attempt_count = models.PositiveIntegerField(default=0)
|
|
597
|
+
|
|
598
|
+
class Meta:
|
|
599
|
+
ordering = ("requested_at",)
|
|
600
|
+
constraints = [
|
|
601
|
+
models.UniqueConstraint(
|
|
602
|
+
fields=("user", "source"),
|
|
603
|
+
condition=models.Q(status="pending"),
|
|
604
|
+
name="external_sync_one_pending_per_user_source",
|
|
605
|
+
),
|
|
606
|
+
models.UniqueConstraint(
|
|
607
|
+
fields=("user", "source"),
|
|
608
|
+
condition=models.Q(status="dispatched"),
|
|
609
|
+
name="external_sync_one_dispatched_per_user_source",
|
|
610
|
+
),
|
|
611
|
+
]
|
|
612
|
+
indexes = [
|
|
613
|
+
# The drain scans PENDING (+ orphaned DISPATCHED) oldest-first; the
|
|
614
|
+
# purge scans DONE/DEAD by age. Index the shared (status, requested_at)
|
|
615
|
+
# sort key so both are index range scans, not filesorts.
|
|
616
|
+
models.Index(fields=("status", "requested_at"), name="external_sync_status_age_idx"),
|
|
617
|
+
]
|
|
618
|
+
verbose_name = "External sync request"
|
|
619
|
+
verbose_name_plural = "External sync requests"
|
|
620
|
+
|
|
621
|
+
def __str__(self) -> str: # pragma: no cover — debugging aid only
|
|
622
|
+
return f"ExternalSyncRequest({self.source} for user {self.user_id}, {self.status})"
|