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,806 @@
|
|
|
1
|
+
"""User-scoped external-source connection endpoints (ADR-0097 §3).
|
|
2
|
+
|
|
3
|
+
``GET/PUT/PATCH/DELETE /api/v1/me/connections/<source>/`` — the personal, self-scoped
|
|
4
|
+
surface for connecting a read-only external task source (e.g. the user's own
|
|
5
|
+
Jira Cloud account) so its assigned items appear in My Work.
|
|
6
|
+
|
|
7
|
+
Isolation & security posture (ADR-0097 §3 / §Threat Model → Resolution):
|
|
8
|
+
- **Strictly personal.** Every read/write filters ``user=request.user``; no
|
|
9
|
+
project member, Admin, or Owner can see or touch another user's connection.
|
|
10
|
+
- **Secret never serialized.** The Fernet-encrypted PAT (``secret_ciphertext``)
|
|
11
|
+
is never returned — the summary exposes only ``{source, exists, base_url,
|
|
12
|
+
account_email, status, last_synced_at, jql, project_keys, poll_enabled,
|
|
13
|
+
last_sync}``. The stored ``config`` is never echoed wholesale; every key it
|
|
14
|
+
contributes is projected explicitly (see :func:`last_sync_summary`).
|
|
15
|
+
- **SSRF collapsed at connect.** ``base_url`` is Jira-Cloud-allow-listed
|
|
16
|
+
(``providers.assert_base_url_allowed`` → ``*.atlassian.net``, https) *before*
|
|
17
|
+
the token is ever put on the wire in the verify ping (#902 ordering).
|
|
18
|
+
|
|
19
|
+
This surface reuses ``IntegrationCredential`` (ADR-0097 §2) with the row's
|
|
20
|
+
``config`` carrying ``{account_email, jql, project_keys, poll_enabled, status}``
|
|
21
|
+
— both ``jql`` and ``project_keys`` are read by the source at pull time, the
|
|
22
|
+
latter ANDed onto the query so it can only narrow what leaves the provider
|
|
23
|
+
(#2888). It does
|
|
24
|
+
**not** call ``IntegrationCredential.upsert`` — that validates ``provider``
|
|
25
|
+
against ``TASK_LINK_PROVIDERS`` (where ``jira`` is reserved Enterprise), whereas
|
|
26
|
+
an external source validates against the distinct ``EXTERNAL_TASK_SOURCES``
|
|
27
|
+
registry. Persistence therefore goes through this module's own
|
|
28
|
+
``update_or_create`` + ``encrypt_secret``.
|
|
29
|
+
|
|
30
|
+
Scope note (#1418): this ticket ships the data layer + connection management.
|
|
31
|
+
The actual pull worker (Celery + ``ExternalSyncRequest`` outbox), the
|
|
32
|
+
``POST .../sync/`` trigger, and the My Work augmentation ride on top in #1419;
|
|
33
|
+
this module deliberately does not enqueue a sync.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
39
|
+
from urllib.parse import urlparse
|
|
40
|
+
|
|
41
|
+
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
|
42
|
+
from rest_framework import generics, pagination, serializers, status
|
|
43
|
+
from rest_framework.permissions import BasePermission, IsAuthenticated
|
|
44
|
+
from rest_framework.response import Response
|
|
45
|
+
from rest_framework.throttling import BaseThrottle, ScopedRateThrottle
|
|
46
|
+
from rest_framework.views import APIView
|
|
47
|
+
|
|
48
|
+
from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
|
|
49
|
+
from trueppm_api.core.openapi import state_refusal_400
|
|
50
|
+
|
|
51
|
+
from . import providers
|
|
52
|
+
from .encryption import encrypt_secret
|
|
53
|
+
from .external_sources import (
|
|
54
|
+
DEPLOYMENT_CLOUD,
|
|
55
|
+
EXTERNAL_TASK_SOURCES,
|
|
56
|
+
JIRA_DEPLOYMENTS,
|
|
57
|
+
JIRA_PROJECT_KEY_RE,
|
|
58
|
+
ExternalTaskSource,
|
|
59
|
+
JqlNotWellFormed,
|
|
60
|
+
scan_jql,
|
|
61
|
+
)
|
|
62
|
+
from .models import ExternalWorkItem, IntegrationCredential
|
|
63
|
+
from .services import SyncCooldownActive, enqueue_external_sync
|
|
64
|
+
|
|
65
|
+
if TYPE_CHECKING:
|
|
66
|
+
from django.contrib.auth.models import User
|
|
67
|
+
from rest_framework.request import Request
|
|
68
|
+
|
|
69
|
+
# Connection lifecycle status surfaced to the owner (ADR-0097 §5). Written by the
|
|
70
|
+
# #1419 pull worker into ``config["status"]``; this module only reads it and
|
|
71
|
+
# defaults a freshly-connected row to ``connected``.
|
|
72
|
+
STATUS_CONNECTED = "connected"
|
|
73
|
+
STATUS_NOT_CONNECTED = "not_connected"
|
|
74
|
+
STATUS_AUTH_FAILED = "auth_failed"
|
|
75
|
+
# The credential is fine but its stored filter cannot be scoped safely, so the
|
|
76
|
+
# worker refuses to pull rather than pull wider than the owner asked for (#2888).
|
|
77
|
+
# A separate state from ``auth_failed`` because the remedy is the same wizard but
|
|
78
|
+
# the cause is not the token — telling the user their token expired would send
|
|
79
|
+
# them to re-issue a credential that was never the problem.
|
|
80
|
+
STATUS_INVALID_FILTER = "invalid_filter"
|
|
81
|
+
|
|
82
|
+
# ``config["last_sync"]["reason"]`` vocabulary (#2925), written by the pull worker
|
|
83
|
+
# (``tasks.py``) and validated on the way out by :func:`last_sync_summary`. A
|
|
84
|
+
# **fixed token set**, not a formatted message, and that is a security constraint
|
|
85
|
+
# rather than a style one: ``config`` is owner-readable over this module's ``GET``,
|
|
86
|
+
# and the exception text on those paths can carry the request URL (which embeds
|
|
87
|
+
# the owner's JQL, and on some deployments the credential) or provider-echoed PII.
|
|
88
|
+
# A closed vocabulary cannot leak either — the web maps the token to user-facing
|
|
89
|
+
# copy. Never interpolate an exception, URL, or response body into this field.
|
|
90
|
+
#
|
|
91
|
+
# Defined *here*, not beside the writer, so the read surface can fail closed
|
|
92
|
+
# against it without importing ``tasks`` (which imports this module). Writer
|
|
93
|
+
# discipline alone would leave the guarantee unenforced at the one boundary that
|
|
94
|
+
# serves it, and ``config`` is a schemaless column an Enterprise integration also
|
|
95
|
+
# writes to.
|
|
96
|
+
SYNC_REASON_AUTH_FAILED = "auth_failed"
|
|
97
|
+
SYNC_REASON_INVALID_FILTER = "invalid_filter"
|
|
98
|
+
SYNC_REASON_UNREACHABLE = "unreachable"
|
|
99
|
+
SYNC_REASON_RATE_LIMITED = "rate_limited"
|
|
100
|
+
SYNC_REASON_DECRYPT_FAILED = "credential_unreadable"
|
|
101
|
+
SYNC_FAILURE_REASONS: frozenset[str] = frozenset(
|
|
102
|
+
{
|
|
103
|
+
SYNC_REASON_AUTH_FAILED,
|
|
104
|
+
SYNC_REASON_INVALID_FILTER,
|
|
105
|
+
SYNC_REASON_UNREACHABLE,
|
|
106
|
+
SYNC_REASON_RATE_LIMITED,
|
|
107
|
+
SYNC_REASON_DECRYPT_FAILED,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Human-readable 422 detail per ``VerifyResult.reason`` (mirrors the credentials
|
|
112
|
+
# viewset map, plus the Jira-specific ``missing_email``).
|
|
113
|
+
_VERIFY_FAILURE_DETAIL: dict[str | None, str] = {
|
|
114
|
+
"invalid_token": (
|
|
115
|
+
"The source rejected this credential. Check the API token is valid, "
|
|
116
|
+
"unexpired, and paired with the correct account email."
|
|
117
|
+
),
|
|
118
|
+
"missing_email": "This source needs the account email that owns the API token.",
|
|
119
|
+
"provider_unreachable": "Could not reach the source to verify this credential. Try again.",
|
|
120
|
+
"provider_timeout": "Verifying this credential with the source timed out. Try again.",
|
|
121
|
+
"blocked_host": (
|
|
122
|
+
"The host URL could not be reached — check it is correct, reachable over "
|
|
123
|
+
"https, and not an internal address."
|
|
124
|
+
),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ExternalConnectionUpsertSerializer(serializers.Serializer[Any]):
|
|
129
|
+
"""Payload for ``PUT /me/connections/{source}/`` (connect or update).
|
|
130
|
+
|
|
131
|
+
``secret`` is the user's own API token / PAT (write-only, never echoed).
|
|
132
|
+
``base_url`` is the source host — Jira Cloud ``https://<tenant>.atlassian.net``
|
|
133
|
+
or a self-hosted Jira Data Center / Server host — allow-listed downstream.
|
|
134
|
+
``deployment`` (``cloud`` default | ``server``) selects the Cloud vs DC/Server
|
|
135
|
+
API + auth shape. ``account_email`` (Cloud Basic auth only) + ``jql`` +
|
|
136
|
+
``project_keys`` are stored in the credential's ``config`` — the source reads
|
|
137
|
+
them at pull time. ``jql`` selects what to pull; ``project_keys`` narrows it
|
|
138
|
+
and is **ANDed onto** the query, whether that query is the default or a
|
|
139
|
+
custom ``jql``, so the project filter can only ever restrict the pull.
|
|
140
|
+
|
|
141
|
+
``poll_enabled`` opts the connection into the background poll (ADR-0097 §4).
|
|
142
|
+
It is declared **without** a ``default`` on purpose: a PUT rebuilds ``config``
|
|
143
|
+
wholesale, so a defaulted field would silently switch polling off every time
|
|
144
|
+
the owner re-connected to rotate a token. Absent from the payload therefore
|
|
145
|
+
means "keep what this connection already had" — see
|
|
146
|
+
:meth:`ExternalConnectionView.put`.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
secret = serializers.CharField(
|
|
150
|
+
write_only=True, min_length=1, max_length=4096, trim_whitespace=False
|
|
151
|
+
)
|
|
152
|
+
base_url = serializers.CharField(max_length=512)
|
|
153
|
+
# Which Jira deployment this connection targets. Cloud is the default so a
|
|
154
|
+
# payload from before the field existed (or any non-Jira source that ignores
|
|
155
|
+
# it) keeps working unchanged.
|
|
156
|
+
deployment = serializers.ChoiceField(
|
|
157
|
+
choices=JIRA_DEPLOYMENTS, required=False, default=DEPLOYMENT_CLOUD
|
|
158
|
+
)
|
|
159
|
+
account_email = serializers.EmailField(required=False, allow_blank=True, default="")
|
|
160
|
+
jql = serializers.CharField(
|
|
161
|
+
required=False,
|
|
162
|
+
allow_blank=True,
|
|
163
|
+
max_length=1024,
|
|
164
|
+
default="",
|
|
165
|
+
help_text=(
|
|
166
|
+
"Optional JQL selecting what to pull. Blank uses 'assigned to me and "
|
|
167
|
+
"not done'. Parentheses and quotes must balance — the project filter "
|
|
168
|
+
"is ANDed onto this query, which is only a narrowing on a "
|
|
169
|
+
"well-formed one."
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
project_keys = serializers.ListField(
|
|
173
|
+
child=serializers.CharField(max_length=64),
|
|
174
|
+
required=False,
|
|
175
|
+
default=list,
|
|
176
|
+
max_length=50,
|
|
177
|
+
help_text=(
|
|
178
|
+
"Optional Jira project keys to restrict the pull to. Each must be a "
|
|
179
|
+
"Jira project key (a letter followed by letters, digits or "
|
|
180
|
+
"underscores); stored upper-cased and de-duplicated. Composed into "
|
|
181
|
+
"the query as 'AND project IN (...)' on top of jql, so it can only "
|
|
182
|
+
"narrow the pull. Empty means every project you can see."
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
poll_enabled = serializers.BooleanField(
|
|
186
|
+
required=False,
|
|
187
|
+
help_text=(
|
|
188
|
+
"Opt this connection into the background poll (ADR-0097 §4). Omit to "
|
|
189
|
+
"leave the current setting untouched — a re-connect must not silently "
|
|
190
|
+
"switch polling off. Default off for a connection that never set it."
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def validate_secret(self, value: str) -> str:
|
|
195
|
+
if not value.strip():
|
|
196
|
+
raise serializers.ValidationError("Secret must not be blank.")
|
|
197
|
+
return value
|
|
198
|
+
|
|
199
|
+
def validate_jql(self, value: str) -> str:
|
|
200
|
+
"""Reject a JQL whose parentheses or quotes do not balance.
|
|
201
|
+
|
|
202
|
+
Not style policing. ``project_keys`` narrows the pull by wrapping this
|
|
203
|
+
query in one pair of parentheses and ANDing a ``project IN (...)`` clause
|
|
204
|
+
onto it, and an unbalanced query turns that wrap into a no-op that pulls
|
|
205
|
+
from projects the owner did not select. Jira would reject a malformed query
|
|
206
|
+
outright anyway, so the only thing this forbids is input that is already
|
|
207
|
+
broken — and it surfaces as an inline message on the field instead of a
|
|
208
|
+
connection that verifies, stores, and then fails on its first pull.
|
|
209
|
+
"""
|
|
210
|
+
text = value.strip()
|
|
211
|
+
if not text:
|
|
212
|
+
return value
|
|
213
|
+
try:
|
|
214
|
+
scan_jql(text)
|
|
215
|
+
except JqlNotWellFormed as exc:
|
|
216
|
+
raise serializers.ValidationError(
|
|
217
|
+
f"This filter is not valid JQL ({exc}). Check the parentheses and quotes."
|
|
218
|
+
) from exc
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
def validate_project_keys(self, value: list[str]) -> list[str]:
|
|
222
|
+
"""Normalize + reject anything that is not a Jira project key (#2888).
|
|
223
|
+
|
|
224
|
+
These keys are composed into the JQL the pull worker sends, so a value
|
|
225
|
+
carrying a quote or a parenthesis could rewrite the filter it is meant to
|
|
226
|
+
narrow. Rejecting at mint time gives the user an inline message on the
|
|
227
|
+
connect wizard rather than a connection that fails on its first pull.
|
|
228
|
+
Upper-cased and de-duplicated so the stored value matches what the wizard
|
|
229
|
+
shows and what the query will carry.
|
|
230
|
+
"""
|
|
231
|
+
cleaned: dict[str, None] = {}
|
|
232
|
+
for raw in value:
|
|
233
|
+
key = raw.strip()
|
|
234
|
+
if not key:
|
|
235
|
+
continue
|
|
236
|
+
if not JIRA_PROJECT_KEY_RE.match(key):
|
|
237
|
+
raise serializers.ValidationError(
|
|
238
|
+
f"{key!r} is not a valid project key. Use the short key Jira "
|
|
239
|
+
"shows on an issue (letters, digits and underscores, starting "
|
|
240
|
+
"with a letter) — for example RIV."
|
|
241
|
+
)
|
|
242
|
+
cleaned.setdefault(key.upper(), None)
|
|
243
|
+
return list(cleaned)
|
|
244
|
+
|
|
245
|
+
def validate_base_url(self, value: str) -> str:
|
|
246
|
+
if "://" not in value:
|
|
247
|
+
raise serializers.ValidationError(
|
|
248
|
+
"Host URL must include a scheme (https://tenant.atlassian.net)."
|
|
249
|
+
)
|
|
250
|
+
parsed = urlparse(value)
|
|
251
|
+
# https-only (ADR-0097 §Resolution #1): an external source ships a personal
|
|
252
|
+
# token to base_url, so the transport must be encrypted. ``_jira_origin``
|
|
253
|
+
# also forces https at request time; rejecting here keeps the stored value
|
|
254
|
+
# spec-compliant rather than silently upgrading a cosmetic ``http://``.
|
|
255
|
+
if parsed.scheme.lower() != "https":
|
|
256
|
+
raise serializers.ValidationError(
|
|
257
|
+
f"Host URL scheme {parsed.scheme.lower()!r} is not allowed. Use https."
|
|
258
|
+
)
|
|
259
|
+
if parsed.query or parsed.fragment:
|
|
260
|
+
raise serializers.ValidationError(
|
|
261
|
+
"Host URL must not contain a query string or fragment."
|
|
262
|
+
)
|
|
263
|
+
return value
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class ExternalConnectionLastSyncSerializer(serializers.Serializer[Any]):
|
|
267
|
+
"""What the last pull actually did (#2925) — the outcome, not just the clock.
|
|
268
|
+
|
|
269
|
+
Written by the pull worker into ``config["last_sync"]`` (``tasks.py``). Before
|
|
270
|
+
this, a connection reported ``status`` + ``last_synced_at`` and nothing else,
|
|
271
|
+
so "Connected, last synced 5 minutes ago" read identically whether the pull
|
|
272
|
+
returned 200 items, zero, or failed — and a contributor past the source's page
|
|
273
|
+
size got a silently partial My Work.
|
|
274
|
+
|
|
275
|
+
``truncated`` is the load-bearing field: it says the source had more than this
|
|
276
|
+
pull stored, so the owner is looking at the first ``stored`` of their assigned
|
|
277
|
+
work. ``total_available`` is the provider's own count when it reported one and
|
|
278
|
+
``null`` when it did not — ``null`` means *unknown*, never *zero*, and a client
|
|
279
|
+
must not render a denominator from it.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
at = serializers.DateTimeField(allow_null=True)
|
|
283
|
+
ok = serializers.BooleanField()
|
|
284
|
+
# A fixed token from ``tasks.SYNC_FAILURE_REASONS`` (blank on success), never a
|
|
285
|
+
# formatted message — see that constant for why the vocabulary is closed.
|
|
286
|
+
reason = serializers.CharField(allow_blank=True)
|
|
287
|
+
fetched = serializers.IntegerField()
|
|
288
|
+
stored = serializers.IntegerField()
|
|
289
|
+
total_available = serializers.IntegerField(allow_null=True)
|
|
290
|
+
truncated = serializers.BooleanField()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ExternalConnectionSummarySerializer(serializers.Serializer[Any]):
|
|
294
|
+
"""Owner-facing summary of a connection — **never** the secret (ADR-0097 §3).
|
|
295
|
+
|
|
296
|
+
The ``source`` key is intentionally not echoed: the caller already has it from
|
|
297
|
+
the ``/me/connections/{source}/`` request path, and a serializer field literally
|
|
298
|
+
named ``source`` collides with DRF's internal ``Field.source`` attribute.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
name = serializers.CharField()
|
|
302
|
+
exists = serializers.BooleanField()
|
|
303
|
+
base_url = serializers.CharField(allow_blank=True)
|
|
304
|
+
deployment = serializers.CharField()
|
|
305
|
+
account_email = serializers.CharField(allow_blank=True)
|
|
306
|
+
status = serializers.CharField()
|
|
307
|
+
last_synced_at = serializers.DateTimeField(allow_null=True)
|
|
308
|
+
jql = serializers.CharField(allow_blank=True)
|
|
309
|
+
project_keys = serializers.ListField(child=serializers.CharField())
|
|
310
|
+
# Whether the background poll picks this connection up (ADR-0097 §4). A
|
|
311
|
+
# reviewed, owner-visible key — it is the owner's own setting, carries no
|
|
312
|
+
# provider data, and the toggle that writes it has to be able to read its
|
|
313
|
+
# current state back. Never inferred client-side from anything else.
|
|
314
|
+
poll_enabled = serializers.BooleanField()
|
|
315
|
+
# ``null`` until the connection's first pull completes — a freshly-connected
|
|
316
|
+
# row has a status but no outcome yet, which is a different thing from a pull
|
|
317
|
+
# that returned nothing.
|
|
318
|
+
last_sync = ExternalConnectionLastSyncSerializer(allow_null=True)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class ExternalConnectionPollSerializer(serializers.Serializer[Any]):
|
|
322
|
+
"""Payload for ``PATCH /me/connections/{source}/`` — the poll opt-in, alone.
|
|
323
|
+
|
|
324
|
+
A separate, one-field serializer rather than a partial
|
|
325
|
+
:class:`ExternalConnectionUpsertSerializer`: flipping the background-poll
|
|
326
|
+
switch must not require the owner to re-enter their API token (``secret`` is
|
|
327
|
+
required on the upsert and is never readable back), and a partial upsert would
|
|
328
|
+
also make every other connection field silently writable from a control whose
|
|
329
|
+
entire job is one boolean. Anything else in the body is ignored, so this
|
|
330
|
+
endpoint can never rewrite a credential, host, or filter.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
poll_enabled = serializers.BooleanField()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class ExternalConnectionErrorSerializer(serializers.Serializer[Any]):
|
|
337
|
+
"""422 body when credential verification against the source fails."""
|
|
338
|
+
|
|
339
|
+
detail = serializers.CharField()
|
|
340
|
+
code = serializers.CharField()
|
|
341
|
+
reason = serializers.CharField(allow_null=True)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def last_sync_summary(config: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
345
|
+
"""Project ``config["last_sync"]`` onto the owner-facing outcome shape (#2925).
|
|
346
|
+
|
|
347
|
+
Field-by-field with per-field type coercion rather than passing the stored
|
|
348
|
+
dict through. ``config`` is a schemaless ``JSONField`` that also holds the
|
|
349
|
+
connection's filter and (for other providers on the same model) whatever an
|
|
350
|
+
Enterprise integration put there — echoing it wholesale would turn any future
|
|
351
|
+
key into an unreviewed client-visible field, which is exactly the
|
|
352
|
+
over-exposure the summary's "never the secret" contract exists to prevent.
|
|
353
|
+
Anything absent or of the wrong type degrades to the neutral value, so a
|
|
354
|
+
hand-edited or pre-#2925 row renders as "no outcome recorded" instead of
|
|
355
|
+
raising.
|
|
356
|
+
|
|
357
|
+
Returns ``None`` when no pull has ever completed — distinct from a pull that
|
|
358
|
+
completed and stored nothing.
|
|
359
|
+
"""
|
|
360
|
+
raw = (config or {}).get("last_sync")
|
|
361
|
+
if not isinstance(raw, dict):
|
|
362
|
+
return None
|
|
363
|
+
total = raw.get("total_available")
|
|
364
|
+
at = raw.get("at")
|
|
365
|
+
# Fail closed on ``reason``: only a token from the closed vocabulary leaves
|
|
366
|
+
# this boundary. Type-checking it would enforce nothing — the whole point of
|
|
367
|
+
# the vocabulary is that a *formatted* string must never reach a client, and
|
|
368
|
+
# a formatted string is a ``str``. Anything unrecognized degrades to blank,
|
|
369
|
+
# which the web already renders as its generic "didn't complete" sentence.
|
|
370
|
+
raw_reason = raw.get("reason")
|
|
371
|
+
reason = raw_reason if raw_reason in SYNC_FAILURE_REASONS else ""
|
|
372
|
+
# ``None`` is meaningful for the total ("the source did not say"), so it is
|
|
373
|
+
# not collapsed to 0 the way the two counts are.
|
|
374
|
+
total_ok = isinstance(total, int) and not isinstance(total, bool)
|
|
375
|
+
return {
|
|
376
|
+
"at": at if isinstance(at, str) else None,
|
|
377
|
+
"ok": bool(raw.get("ok")),
|
|
378
|
+
"reason": reason,
|
|
379
|
+
"fetched": _as_count(raw.get("fetched")),
|
|
380
|
+
"stored": _as_count(raw.get("stored")),
|
|
381
|
+
"total_available": total if total_ok else None,
|
|
382
|
+
"truncated": bool(raw.get("truncated")),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _as_count(value: Any) -> int:
|
|
387
|
+
"""Narrow an untrusted stored value to a non-negative count (0 if it is not one).
|
|
388
|
+
|
|
389
|
+
``bool`` is excluded explicitly because it is an ``int`` subclass, so ``True``
|
|
390
|
+
would otherwise read back as a count of 1.
|
|
391
|
+
"""
|
|
392
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
393
|
+
return 0
|
|
394
|
+
return max(int(value), 0)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _summary(label: str, row: IntegrationCredential | None) -> dict[str, Any]:
|
|
398
|
+
"""Build the owner-facing summary dict for one source (secret excluded)."""
|
|
399
|
+
cfg = (row.config if row else None) or {}
|
|
400
|
+
return {
|
|
401
|
+
"name": label,
|
|
402
|
+
"exists": row is not None,
|
|
403
|
+
"base_url": row.base_url if row else "",
|
|
404
|
+
# Cloud is the default for a row stored before the discriminant existed.
|
|
405
|
+
"deployment": cfg.get("deployment", DEPLOYMENT_CLOUD),
|
|
406
|
+
"account_email": cfg.get("account_email", ""),
|
|
407
|
+
# Freshly-connected rows read ``connected``; the #1419 worker flips this to
|
|
408
|
+
# ``auth_failed`` on a 401/403 so My Work can show a "Reconnect" prompt.
|
|
409
|
+
"status": cfg.get("status") or (STATUS_CONNECTED if row else STATUS_NOT_CONNECTED),
|
|
410
|
+
# ``last_used_at`` is stamped by the pull worker when it last used the token.
|
|
411
|
+
"last_synced_at": row.last_used_at if row else None,
|
|
412
|
+
"jql": cfg.get("jql", ""),
|
|
413
|
+
"project_keys": cfg.get("project_keys", []),
|
|
414
|
+
# ``is True``, not ``bool()``: ``config`` is a schemaless column, and the
|
|
415
|
+
# poll selects on ``config__poll_enabled=True`` — strict JSON equality. A
|
|
416
|
+
# row carrying the string ``"false"`` is truthy in Python and *not* what
|
|
417
|
+
# the poll matches, so a ``bool()`` coercion would render the switch on for
|
|
418
|
+
# a connection that is never polled. Report what the poll acts on.
|
|
419
|
+
"poll_enabled": cfg.get("poll_enabled") is True,
|
|
420
|
+
# What the last pull did — counts, and whether a cap truncated it (#2925).
|
|
421
|
+
"last_sync": last_sync_summary(cfg),
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@extend_schema(tags=["me"])
|
|
426
|
+
class ExternalConnectionView(IdempotencyMixin, APIView):
|
|
427
|
+
"""Manage the authenticated user's connection to one external task source.
|
|
428
|
+
|
|
429
|
+
Routes (``<source>`` is an ``EXTERNAL_TASK_SOURCES`` key, e.g. ``jira``):
|
|
430
|
+
GET /api/v1/me/connections/{source}/ summary (exists / status / config)
|
|
431
|
+
PUT /api/v1/me/connections/{source}/ connect or update (verify then store)
|
|
432
|
+
PATCH /api/v1/me/connections/{source}/ flip the background-poll opt-in
|
|
433
|
+
DELETE /api/v1/me/connections/{source}/ disconnect (hard-remove ciphertext)
|
|
434
|
+
|
|
435
|
+
All actions require authentication and are self-scoped to ``request.user`` —
|
|
436
|
+
the ``(user, provider)`` filter is the single IDOR boundary, so another
|
|
437
|
+
user's connection is invisible by construction.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
# Plain (non-ClassVar) annotations: DRF's APIView declares these as instance
|
|
441
|
+
# attributes, so overriding them with a ClassVar trips mypy; noqa the RUF012
|
|
442
|
+
# mutable-default lint that the ``views.py`` per-file ignore doesn't reach here.
|
|
443
|
+
permission_classes: list[type[BasePermission]] = [IsAuthenticated] # noqa: RUF012
|
|
444
|
+
# Share the credential-store rate bucket — connect verifies against the source
|
|
445
|
+
# (an outbound call) and all actions touch the encrypted credential store.
|
|
446
|
+
throttle_classes: list[type[BaseThrottle]] = [ScopedRateThrottle] # noqa: RUF012
|
|
447
|
+
throttle_scope = "credential_rotate"
|
|
448
|
+
|
|
449
|
+
def _resolve_source(self, source: str) -> type[ExternalTaskSource] | None:
|
|
450
|
+
return EXTERNAL_TASK_SOURCES.get(source)
|
|
451
|
+
|
|
452
|
+
def _user(self, request: Request) -> User:
|
|
453
|
+
"""Narrow ``request.user`` to the authenticated user for the ORM filter.
|
|
454
|
+
|
|
455
|
+
``IsAuthenticated`` guarantees a real user before any handler runs, so the
|
|
456
|
+
``AnonymousUser`` arm of ``request.user`` is unreachable here — the cast
|
|
457
|
+
just tells the type-checker what the permission already enforced.
|
|
458
|
+
"""
|
|
459
|
+
return cast("User", request.user)
|
|
460
|
+
|
|
461
|
+
def _row(self, request: Request, source: str) -> IntegrationCredential | None:
|
|
462
|
+
return IntegrationCredential.objects.filter(
|
|
463
|
+
user=self._user(request), provider=source
|
|
464
|
+
).first()
|
|
465
|
+
|
|
466
|
+
@extend_schema(responses={200: ExternalConnectionSummarySerializer})
|
|
467
|
+
def get(self, request: Request, source: str) -> Response:
|
|
468
|
+
source_cls = self._resolve_source(source)
|
|
469
|
+
if source_cls is None:
|
|
470
|
+
return Response(
|
|
471
|
+
{"detail": f"Unknown external task source {source!r}."},
|
|
472
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
473
|
+
)
|
|
474
|
+
row = self._row(request, source)
|
|
475
|
+
payload = _summary(getattr(source_cls, "label", source), row)
|
|
476
|
+
return Response(ExternalConnectionSummarySerializer(payload).data)
|
|
477
|
+
|
|
478
|
+
@extend_schema(
|
|
479
|
+
request=ExternalConnectionUpsertSerializer,
|
|
480
|
+
responses={
|
|
481
|
+
200: ExternalConnectionSummarySerializer,
|
|
482
|
+
# Two different 400 bodies reach a caller here, so this is declared as
|
|
483
|
+
# a description rather than a serializer: an unknown source key or a
|
|
484
|
+
# disallowed ``base_url`` returns ``{detail, code}``, while DRF field
|
|
485
|
+
# validation (``secret``, ``base_url``, ``jql``, ``project_keys``)
|
|
486
|
+
# returns its own field-keyed ``{field: [message]}`` shape. Declaring
|
|
487
|
+
# either serializer would document one and misdescribe the other.
|
|
488
|
+
400: OpenApiResponse(
|
|
489
|
+
description=(
|
|
490
|
+
"Unknown source, disallowed host URL, or a field that failed "
|
|
491
|
+
"validation (an unparseable JQL filter, or a project key that "
|
|
492
|
+
"is not a Jira project key)."
|
|
493
|
+
)
|
|
494
|
+
),
|
|
495
|
+
422: ExternalConnectionErrorSerializer,
|
|
496
|
+
},
|
|
497
|
+
)
|
|
498
|
+
def put(self, request: Request, source: str) -> Response:
|
|
499
|
+
"""Connect or update the connection: allow-list host, verify, then store.
|
|
500
|
+
|
|
501
|
+
Order matters (#902): the ``base_url`` host is allow-listed *before*
|
|
502
|
+
``verify_credential`` ships the token to it, so an attacker-controlled
|
|
503
|
+
host is rejected before the PAT is on the wire. On a failed verify the
|
|
504
|
+
plaintext is never written — ``encrypt_secret`` is not reached.
|
|
505
|
+
"""
|
|
506
|
+
source_cls = self._resolve_source(source)
|
|
507
|
+
if source_cls is None:
|
|
508
|
+
return Response(
|
|
509
|
+
{"detail": f"Unknown external task source {source!r}."},
|
|
510
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
511
|
+
)
|
|
512
|
+
serializer = ExternalConnectionUpsertSerializer(data=request.data)
|
|
513
|
+
serializer.is_valid(raise_exception=True)
|
|
514
|
+
data = serializer.validated_data
|
|
515
|
+
secret: str = data["secret"]
|
|
516
|
+
base_url: str = data["base_url"]
|
|
517
|
+
|
|
518
|
+
# Gate the host BEFORE verify: verify sends the token to base_url.
|
|
519
|
+
try:
|
|
520
|
+
providers.assert_base_url_allowed(source, base_url)
|
|
521
|
+
except providers.BaseUrlNotAllowed as exc:
|
|
522
|
+
# False positive: BaseUrlNotAllowed carries a curated, user-facing
|
|
523
|
+
# message (#902); the only dynamic part is the caller's own submitted
|
|
524
|
+
# host, and it is an allow-list decision, not a DNS-resolved address.
|
|
525
|
+
return Response(
|
|
526
|
+
{
|
|
527
|
+
"detail": str(exc), # codeql[py/stack-trace-exposure]
|
|
528
|
+
"code": "base_url_not_allowed",
|
|
529
|
+
},
|
|
530
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Rebuilt from the payload, so a re-connect deliberately drops any stored
|
|
534
|
+
# ``last_sync``: the outcome described a pull made with the *previous*
|
|
535
|
+
# token and filter, and carrying it onto a connection whose filter just
|
|
536
|
+
# changed would report a truncation (or a count) that no longer applies.
|
|
537
|
+
existing = self._row(request, source)
|
|
538
|
+
# ``poll_enabled`` is the one key a rebuild must *not* drop. The rest of
|
|
539
|
+
# ``config`` describes the credential and its filter, both of which this
|
|
540
|
+
# payload replaces; the poll opt-in is an independent standing preference
|
|
541
|
+
# the owner set elsewhere (the Connected Accounts toggle → PATCH below).
|
|
542
|
+
# Rotating a token would otherwise silently stop a connection polling,
|
|
543
|
+
# with nothing in the UI to say it had happened.
|
|
544
|
+
existing_cfg = (existing.config if existing else None) or {}
|
|
545
|
+
current_poll = bool(existing_cfg.get("poll_enabled", False))
|
|
546
|
+
config: dict[str, Any] = {
|
|
547
|
+
"deployment": data.get("deployment", DEPLOYMENT_CLOUD),
|
|
548
|
+
"account_email": data.get("account_email", ""),
|
|
549
|
+
"jql": data.get("jql", ""),
|
|
550
|
+
"project_keys": data.get("project_keys", []),
|
|
551
|
+
"poll_enabled": bool(data.get("poll_enabled", current_poll)),
|
|
552
|
+
"status": STATUS_CONNECTED,
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
result = source_cls().verify_credential(base_url=base_url, secret=secret, config=config)
|
|
556
|
+
if not result.ok:
|
|
557
|
+
return Response(
|
|
558
|
+
{
|
|
559
|
+
"detail": _VERIFY_FAILURE_DETAIL.get(
|
|
560
|
+
result.reason, f"Could not verify the credential with {source}."
|
|
561
|
+
),
|
|
562
|
+
"code": "source_verification_failed",
|
|
563
|
+
"reason": result.reason,
|
|
564
|
+
},
|
|
565
|
+
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Persist directly (not IntegrationCredential.upsert — that validates the
|
|
569
|
+
# provider against TASK_LINK_PROVIDERS, which reserves ``jira`` for
|
|
570
|
+
# Enterprise; external sources use the EXTERNAL_TASK_SOURCES namespace).
|
|
571
|
+
row, _ = IntegrationCredential.objects.update_or_create(
|
|
572
|
+
user=self._user(request),
|
|
573
|
+
provider=source,
|
|
574
|
+
defaults={
|
|
575
|
+
"secret_ciphertext": encrypt_secret(secret),
|
|
576
|
+
"base_url": base_url,
|
|
577
|
+
"config": config,
|
|
578
|
+
},
|
|
579
|
+
)
|
|
580
|
+
payload = _summary(getattr(source_cls, "label", source), row)
|
|
581
|
+
return Response(
|
|
582
|
+
ExternalConnectionSummarySerializer(payload).data, status=status.HTTP_200_OK
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
@extend_schema(
|
|
586
|
+
# A raw schema, not ``ExternalConnectionPollSerializer``: drf-spectacular
|
|
587
|
+
# rewrites any serializer on a PATCH into a ``Patched…`` component with
|
|
588
|
+
# every field optional, which would document ``poll_enabled`` as omittable
|
|
589
|
+
# when the handler answers 400 without it. The serializer still does the
|
|
590
|
+
# validating — this only stops the schema from describing a shape the
|
|
591
|
+
# endpoint rejects. (``COMPONENT_SPLIT_PATCH`` is the global lever and is
|
|
592
|
+
# deliberately left alone; this is one endpoint, not a project-wide policy.)
|
|
593
|
+
request={
|
|
594
|
+
"application/json": {
|
|
595
|
+
"type": "object",
|
|
596
|
+
"properties": {
|
|
597
|
+
"poll_enabled": {
|
|
598
|
+
"type": "boolean",
|
|
599
|
+
"description": (
|
|
600
|
+
"Whether the background poll (ADR-0097 §4) should pick this "
|
|
601
|
+
"connection up. Required — there is no partial form of this "
|
|
602
|
+
"request."
|
|
603
|
+
),
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
"required": ["poll_enabled"],
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
responses={
|
|
610
|
+
200: ExternalConnectionSummarySerializer,
|
|
611
|
+
400: OpenApiResponse(
|
|
612
|
+
description="Unknown source, or poll_enabled missing / not a boolean."
|
|
613
|
+
),
|
|
614
|
+
404: OpenApiResponse(description="No connection to this source for this user."),
|
|
615
|
+
},
|
|
616
|
+
)
|
|
617
|
+
def patch(self, request: Request, source: str) -> Response:
|
|
618
|
+
"""Turn the background poll on or off for this connection (ADR-0097 §4).
|
|
619
|
+
|
|
620
|
+
The only mutation on a *stored* connection that does not go through the
|
|
621
|
+
connect wizard, because it is the only one that needs no credential. The
|
|
622
|
+
IDOR boundary is the same single ``(user, provider)`` filter every other
|
|
623
|
+
action uses (``self._row``): a source another user connected simply does
|
|
624
|
+
not exist for this caller, so a 404 is a "you have no such connection",
|
|
625
|
+
never a leak that someone else does.
|
|
626
|
+
|
|
627
|
+
``config`` is copied-then-reassigned rather than mutated in place — the
|
|
628
|
+
JSONField's value is a live dict, and mutating it leaves Django unable to
|
|
629
|
+
tell the field changed if anything later relies on the loaded state.
|
|
630
|
+
"""
|
|
631
|
+
source_cls = self._resolve_source(source)
|
|
632
|
+
if source_cls is None:
|
|
633
|
+
return Response(
|
|
634
|
+
{"detail": f"Unknown external task source {source!r}."},
|
|
635
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
636
|
+
)
|
|
637
|
+
serializer = ExternalConnectionPollSerializer(data=request.data)
|
|
638
|
+
serializer.is_valid(raise_exception=True)
|
|
639
|
+
row = self._row(request, source)
|
|
640
|
+
if row is None:
|
|
641
|
+
return Response(
|
|
642
|
+
{"detail": f"No {source} connection to configure — connect it first."},
|
|
643
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
644
|
+
)
|
|
645
|
+
config = dict(row.config or {})
|
|
646
|
+
config["poll_enabled"] = bool(serializer.validated_data["poll_enabled"])
|
|
647
|
+
row.config = config
|
|
648
|
+
# ``updated_at`` is ``auto_now``; naming it keeps the stamp truthful, which
|
|
649
|
+
# an ``update_fields`` list omitting it would silently skip.
|
|
650
|
+
row.save(update_fields=["config", "updated_at"])
|
|
651
|
+
payload = _summary(getattr(source_cls, "label", source), row)
|
|
652
|
+
return Response(ExternalConnectionSummarySerializer(payload).data)
|
|
653
|
+
|
|
654
|
+
@extend_schema(
|
|
655
|
+
responses={
|
|
656
|
+
204: None,
|
|
657
|
+
400: state_refusal_400(
|
|
658
|
+
"``source`` does not name a registered external task source. The "
|
|
659
|
+
"path segment is free text, so an unregistered value reaches the "
|
|
660
|
+
"handler and is refused here rather than 404ing (#3319)."
|
|
661
|
+
),
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
def delete(self, request: Request, source: str) -> Response:
|
|
665
|
+
"""Disconnect: hard-remove the ciphertext, config, and cached items.
|
|
666
|
+
|
|
667
|
+
Idempotent — a 204 whether or not a row existed (ADR-0097 §Resolution #2:
|
|
668
|
+
"owner-only delete hard-removes ciphertext"). ``ExternalWorkItem`` FKs to
|
|
669
|
+
``user`` (not the credential), so deleting the credential does not cascade
|
|
670
|
+
the cache — the cached items are removed explicitly here so a disconnect
|
|
671
|
+
leaves no residual external data in My Work.
|
|
672
|
+
"""
|
|
673
|
+
source_cls = self._resolve_source(source)
|
|
674
|
+
if source_cls is None:
|
|
675
|
+
return Response(
|
|
676
|
+
{"detail": f"Unknown external task source {source!r}."},
|
|
677
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
678
|
+
)
|
|
679
|
+
user = self._user(request)
|
|
680
|
+
IntegrationCredential.objects.filter(user=user, provider=source).delete()
|
|
681
|
+
ExternalWorkItem.objects.filter(user=user, source=source).delete()
|
|
682
|
+
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class ExternalSyncQueuedSerializer(serializers.Serializer[Any]):
|
|
686
|
+
"""202 body for a queued pull — ``{"queued": true}`` (not a task id)."""
|
|
687
|
+
|
|
688
|
+
queued = serializers.BooleanField()
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class ExternalSyncCooldownSerializer(serializers.Serializer[Any]):
|
|
692
|
+
"""429 body when a manual refresh lands inside the per-connection cooldown."""
|
|
693
|
+
|
|
694
|
+
detail = serializers.CharField()
|
|
695
|
+
code = serializers.CharField()
|
|
696
|
+
retry_after = serializers.IntegerField()
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@extend_schema(tags=["me"])
|
|
700
|
+
class ExternalConnectionSyncView(IdempotencyMixin, APIView):
|
|
701
|
+
"""Trigger a read-only pull of one external-source connection (ADR-0097 §4).
|
|
702
|
+
|
|
703
|
+
``POST /api/v1/me/connections/{source}/sync/`` → ``202 {"queued": true}``.
|
|
704
|
+
Self-scoped: only the owner can trigger their own pull. The pull runs through
|
|
705
|
+
the ``ExternalSyncRequest`` outbox (never a direct ``.delay()``), so a broker
|
|
706
|
+
outage degrades to the drain rather than dropping the request.
|
|
707
|
+
"""
|
|
708
|
+
|
|
709
|
+
permission_classes: list[type[BasePermission]] = [IsAuthenticated] # noqa: RUF012
|
|
710
|
+
throttle_classes: list[type[BaseThrottle]] = [ScopedRateThrottle] # noqa: RUF012
|
|
711
|
+
throttle_scope = "external_sync"
|
|
712
|
+
|
|
713
|
+
@extend_schema(
|
|
714
|
+
request=None,
|
|
715
|
+
responses={
|
|
716
|
+
202: ExternalSyncQueuedSerializer,
|
|
717
|
+
400: state_refusal_400(
|
|
718
|
+
"``source`` does not name a registered external task source. The "
|
|
719
|
+
"path segment is free text, so an unregistered value reaches the "
|
|
720
|
+
"handler and is refused here rather than 404ing (#3319)."
|
|
721
|
+
),
|
|
722
|
+
429: ExternalSyncCooldownSerializer,
|
|
723
|
+
},
|
|
724
|
+
)
|
|
725
|
+
def post(self, request: Request, source: str) -> Response:
|
|
726
|
+
if EXTERNAL_TASK_SOURCES.get(source) is None:
|
|
727
|
+
return Response(
|
|
728
|
+
{"detail": f"Unknown external task source {source!r}."},
|
|
729
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
730
|
+
)
|
|
731
|
+
user = cast("User", request.user)
|
|
732
|
+
# A pull needs a stored connection to read the token + config from.
|
|
733
|
+
if not IntegrationCredential.objects.filter(user=user, provider=source).exists():
|
|
734
|
+
return Response(
|
|
735
|
+
{"detail": f"No {source} connection to sync — connect it first."},
|
|
736
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
737
|
+
)
|
|
738
|
+
try:
|
|
739
|
+
enqueue_external_sync(user.pk, source)
|
|
740
|
+
except SyncCooldownActive as exc:
|
|
741
|
+
resp = Response(
|
|
742
|
+
{
|
|
743
|
+
"detail": (
|
|
744
|
+
"This connection was refreshed a moment ago — "
|
|
745
|
+
f"try again in {exc.retry_after}s."
|
|
746
|
+
),
|
|
747
|
+
"code": "sync_cooldown",
|
|
748
|
+
"retry_after": exc.retry_after,
|
|
749
|
+
},
|
|
750
|
+
status=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
751
|
+
)
|
|
752
|
+
# Standard header so a client can honor the cooldown without parsing
|
|
753
|
+
# the body (RFC 9110 §10.2.3).
|
|
754
|
+
resp["Retry-After"] = str(exc.retry_after)
|
|
755
|
+
return resp
|
|
756
|
+
return Response({"queued": True}, status=status.HTTP_202_ACCEPTED)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class ExternalWorkItemSerializer(serializers.Serializer[Any]):
|
|
760
|
+
"""One read-only external work item row for My Work (ADR-0097 §4).
|
|
761
|
+
|
|
762
|
+
Deliberately CPM-free: an external item is a personal cache row, never a
|
|
763
|
+
``Task``, so it carries no schedule/board fields — only what My Work needs to
|
|
764
|
+
render a labeled, read-only link that opens in the provider.
|
|
765
|
+
"""
|
|
766
|
+
|
|
767
|
+
id = serializers.UUIDField(read_only=True)
|
|
768
|
+
# ``source_key`` (not ``source``): a serializer field literally named
|
|
769
|
+
# ``source`` collides with DRF's internal ``Field.source`` attribute (same
|
|
770
|
+
# reason the connection summary omits it). ``source="source"`` maps this
|
|
771
|
+
# differently-named field back onto the model's ``source`` column.
|
|
772
|
+
source_key = serializers.CharField(source="source", read_only=True)
|
|
773
|
+
external_id = serializers.CharField(read_only=True)
|
|
774
|
+
external_url = serializers.CharField(read_only=True, allow_blank=True)
|
|
775
|
+
title = serializers.CharField(read_only=True, allow_blank=True)
|
|
776
|
+
external_status = serializers.CharField(read_only=True, allow_blank=True)
|
|
777
|
+
display_bucket = serializers.CharField(read_only=True)
|
|
778
|
+
last_synced_at = serializers.DateTimeField(read_only=True, allow_null=True)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
class ExternalWorkItemPagination(pagination.LimitOffsetPagination):
|
|
782
|
+
"""Limit/offset pagination for the personal external-items list."""
|
|
783
|
+
|
|
784
|
+
default_limit = 100
|
|
785
|
+
max_limit = 200
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@extend_schema(tags=["me"])
|
|
789
|
+
class ExternalWorkItemListView(generics.ListAPIView[ExternalWorkItem]):
|
|
790
|
+
"""List the authenticated user's cached external work items (ADR-0097 §3).
|
|
791
|
+
|
|
792
|
+
``GET /api/v1/me/external-items/`` — the read-only items pulled from the
|
|
793
|
+
user's connected sources, for the My Work external section. Strictly personal:
|
|
794
|
+
the queryset filters ``user=request.user`` and hides soft-removed
|
|
795
|
+
(``is_stale``) rows, so no other user (member, Admin, or Owner) can ever see
|
|
796
|
+
another user's external items. Ordering comes from the model ``Meta``
|
|
797
|
+
(``display_bucket``, ``external_id``) so items group by bucket in My Work.
|
|
798
|
+
"""
|
|
799
|
+
|
|
800
|
+
permission_classes: list[type[BasePermission]] = [IsAuthenticated] # noqa: RUF012
|
|
801
|
+
serializer_class = ExternalWorkItemSerializer
|
|
802
|
+
pagination_class = ExternalWorkItemPagination
|
|
803
|
+
|
|
804
|
+
def get_queryset(self) -> Any:
|
|
805
|
+
user = cast("User", self.request.user)
|
|
806
|
+
return ExternalWorkItem.objects.filter(user=user, is_stale=False)
|