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,191 @@
|
|
|
1
|
+
"""The draft exclusion list (#2962).
|
|
2
|
+
|
|
3
|
+
A draft is a plan nobody has agreed to yet. It is fully writable — that is the
|
|
4
|
+
difference from ``is_archived`` — but it must not appear in anything that
|
|
5
|
+
*aggregates*, because a half-built plan inside a rollup makes that rollup a
|
|
6
|
+
guess with a chart around it.
|
|
7
|
+
|
|
8
|
+
**One module, not N filters.** The exclusion has to hold across program rollup,
|
|
9
|
+
portfolio health, search and notifications, and those live in different modules
|
|
10
|
+
maintained at different times. A `.exclude(lifecycle=DRAFT)` written out per
|
|
11
|
+
surface is a place to forget it per surface; this module is one place to get it
|
|
12
|
+
right and one place to grep for.
|
|
13
|
+
|
|
14
|
+
It says *module* rather than *helper* because #3128 found the single-helper
|
|
15
|
+
framing was itself part of why the list leaked. :func:`visible_projects` takes a
|
|
16
|
+
``Project`` queryset, and half the surfaces that must honor the exclusion never
|
|
17
|
+
build one — omni-search and the cross-project dependency picker filter ``Task``
|
|
18
|
+
rows, and the Programs directory counts projects inside a ``Count(filter=...)``
|
|
19
|
+
annotation on ``Program``. Faced with a helper that did not fit, those sites
|
|
20
|
+
wrote nothing at all. So there are three shapes of the same predicate —
|
|
21
|
+
:func:`visible_projects`, :func:`exclude_draft_projects`, :func:`not_draft_q` —
|
|
22
|
+
and the rule is: **no surface writes its own draft-exclusion predicate.** If a
|
|
23
|
+
new one fits none of the three, add a fourth here rather than inlining it there.
|
|
24
|
+
|
|
25
|
+
Be exact about what that rule does and does not say, because the tempting
|
|
26
|
+
stronger version ("``ProjectLifecycle.DRAFT`` appears in this file and nowhere
|
|
27
|
+
else") is *false* and would be a trap to write down. ``commit_moment.py`` reads
|
|
28
|
+
it to decide whether a project is committable, and ``amend.py`` reads its
|
|
29
|
+
sibling ``ACTIVE`` — those are state *transitions*, which are none of this
|
|
30
|
+
module's business. The rule covers exclusion filters only, and nothing enforces
|
|
31
|
+
it mechanically today: a gate would have to tell a transition check apart from
|
|
32
|
+
an exclusion filter, which is not a grep. Treat it as a convention this module
|
|
33
|
+
argues for, not a guarantee something checks (#3128).
|
|
34
|
+
|
|
35
|
+
**The MCP read surface is deliberately NOT on this list**, which is a correction
|
|
36
|
+
to the design rather than an omission. Hiding a project from an agent's reads is
|
|
37
|
+
indistinguishable from a 404 or a token-scope miss: the agent cannot tell "not
|
|
38
|
+
committed yet" from "does not exist" and has no way to debug the difference.
|
|
39
|
+
``lifecycle`` is returned as an ordinary field instead, so a client can filter on
|
|
40
|
+
a stated fact. Filtering is something a caller can do; recovering from silence is
|
|
41
|
+
not.
|
|
42
|
+
|
|
43
|
+
**Two more surfaces are deliberately NOT on this list** (#3144). The #3128 audit
|
|
44
|
+
found both leaking and left both, because filtering them is a decision about
|
|
45
|
+
people and about their own files rather than a correctness fix about plans. Both
|
|
46
|
+
decisions are KEEP, and the reasoning is recorded here so the next audit reads it
|
|
47
|
+
instead of re-deriving it.
|
|
48
|
+
|
|
49
|
+
*The mention fan-out* (``access.groups._program_membership_base_qs``). The
|
|
50
|
+
strongest reason is structural rather than a values call. That queryset is the
|
|
51
|
+
one base **all four** ``@program-*`` keys narrow, and its own docstring forbids a
|
|
52
|
+
per-key filter. So a draft exclusion has no legal placement: written where it
|
|
53
|
+
belongs it also drops the draft's Admin out of ``@program-pms`` and its Scheduler
|
|
54
|
+
out of ``@program-schedulers`` — the people *authoring the draft*, cut out of
|
|
55
|
+
program-wide coordination — and written per key it breaks the invariant the
|
|
56
|
+
helper was extracted to hold. The values argument agrees: draft-ness is a
|
|
57
|
+
property of the plan someone was added to, not of their standing in the program,
|
|
58
|
+
and the failure mode of dropping them is silence neither they nor the sender can
|
|
59
|
+
see, exactly as for the MCP surface above. Two further consequences, both
|
|
60
|
+
deliberate:
|
|
61
|
+
|
|
62
|
+
* ``@program-stakeholders`` has a second arm. ``ExternalStakeholder`` is
|
|
63
|
+
program-owned with **no project FK**, so no project-level narrowing can reach
|
|
64
|
+
it. Excluding the internal arm would report ``viewer_member_count: 0`` beside a
|
|
65
|
+
non-zero external count on a program whose projects are all drafts — an
|
|
66
|
+
incoherence between the arms that does not exist today.
|
|
67
|
+
* A mention *written in* a draft project still fans out program-wide. That is the
|
|
68
|
+
other leak direction, and it is kept too: #3128's three notification sites were
|
|
69
|
+
unattended machine sweeps over a project population, whereas a PM typing
|
|
70
|
+
``@program-stakeholders`` into a draft is deliberately asking for review, which
|
|
71
|
+
is what a draft is for.
|
|
72
|
+
|
|
73
|
+
The cost of keeping, named so a later report traces here: draft memberships
|
|
74
|
+
consume ``@program-all``'s ``ALL_GROUP_HARD_CAP`` slots, so enough drafts can trip
|
|
75
|
+
``GroupTooLargeError`` on the strength of plans nobody committed to. Accepted —
|
|
76
|
+
the cap is a blast-radius guard over real people, who are genuinely reached.
|
|
77
|
+
|
|
78
|
+
Keeping also holds ``count_program_stakeholder_reach`` in lockstep with the
|
|
79
|
+
resolver (ADR-0697) for free: the count and the fan-out cannot drift if neither
|
|
80
|
+
moves. The program-settings docs say the alias reaches "members holding the Viewer
|
|
81
|
+
role on any project inside the program", which stays true under KEEP; that
|
|
82
|
+
sentence becomes wrong, and must gain the gap, only if this decision flips.
|
|
83
|
+
|
|
84
|
+
*The cross-project asset feeds* (``projects.asset_feed``, behind
|
|
85
|
+
``GET /programs/{pk}/assets/`` and ``GET /assets/``) are the member's own browse
|
|
86
|
+
list, which this module exempts, at two scopes — the workspace view is literally
|
|
87
|
+
the program view with the ``project__program=`` clause dropped, and both narrow to
|
|
88
|
+
the caller's own ``ProjectMembership``. ``?q=`` makes them look like the
|
|
89
|
+
omni-search palette this list does name, but the line under omni-search and the
|
|
90
|
+
dependency picker is that a returned row is *acted on across a boundary*: the
|
|
91
|
+
palette navigates into a project, the picker mints an edge that moves a real
|
|
92
|
+
project's dates. Nothing downstream of an asset row changes a schedule, and the
|
|
93
|
+
feed emits no count, health band or ranking that an exclusion would make truer.
|
|
94
|
+
What exclusion would produce instead is a visibility discontinuity at the commit
|
|
95
|
+
moment: a file you attached to your own draft disappears from the surface built to
|
|
96
|
+
find it and returns when somebody else commits the plan.
|
|
97
|
+
|
|
98
|
+
``GET /assets/`` is additionally a declared MCP read surface
|
|
99
|
+
(``McpReadableViewMixin``), which the paragraph above exempts outright; note this
|
|
100
|
+
covers only the workspace tier — ``ProgramAssetsView`` is not MCP-readable, and
|
|
101
|
+
the browse-list argument is what carries it. An operator who wants a project out
|
|
102
|
+
of an agent's index already has ``mcp_settings.mcp_excluded_project_ids``, a
|
|
103
|
+
stated and auditable lever orthogonal to lifecycle.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
from __future__ import annotations
|
|
107
|
+
|
|
108
|
+
from typing import Any, TypeVar
|
|
109
|
+
|
|
110
|
+
from django.db.models import Model, Q, QuerySet
|
|
111
|
+
|
|
112
|
+
from .models import Project, ProjectLifecycle
|
|
113
|
+
|
|
114
|
+
_M = TypeVar("_M", bound=Model)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def visible_projects(qs: QuerySet[Project]) -> QuerySet[Project]:
|
|
118
|
+
"""Drop drafts from an aggregate.
|
|
119
|
+
|
|
120
|
+
Apply to any queryset feeding a rollup, a health figure, a search result or an
|
|
121
|
+
**unattended** notification fan-out over a project population. Do **not** apply
|
|
122
|
+
it to a direct read of one project by id, to the project list a member browses,
|
|
123
|
+
to the MCP read surface, or to a mention resolver, where a human named the
|
|
124
|
+
audience at write time (#3144) — a draft is not a secret, it is merely
|
|
125
|
+
uncommitted.
|
|
126
|
+
"""
|
|
127
|
+
return qs.exclude(lifecycle=ProjectLifecycle.DRAFT)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def exclude_draft_projects(qs: QuerySet[_M], path: str = "project") -> QuerySet[_M]:
|
|
131
|
+
"""Drop rows whose owning project is a draft, from a *non*-Project queryset.
|
|
132
|
+
|
|
133
|
+
The same exclusion as :func:`visible_projects`, reached across a relation.
|
|
134
|
+
Two of the four surfaces the exclusion list names — omni-search and the
|
|
135
|
+
cross-project dependency picker — never build a ``Project`` queryset at all;
|
|
136
|
+
they filter ``Task`` rows by their project. Without this, those sites would
|
|
137
|
+
each have to hand-write ``exclude(project__lifecycle="draft")``, which is
|
|
138
|
+
exactly the "four places to forget it" the module docstring rejects.
|
|
139
|
+
|
|
140
|
+
``path`` must be **single-valued** (a chain of forward FKs / one-to-ones), and
|
|
141
|
+
that is enforced below rather than documented and hoped for. On a single-valued
|
|
142
|
+
path Django appends a bare column predicate to a join it already has. On a
|
|
143
|
+
multi-valued one (``Program.projects``) the same call compiles to a correlated
|
|
144
|
+
``NOT EXISTS`` and, far worse, silently *inverts the question*: it drops every
|
|
145
|
+
row whose related set contains **any** draft, rather than dropping draft rows.
|
|
146
|
+
That is a wrong answer, not a slow one, and nothing downstream would show it —
|
|
147
|
+
so it fails loudly here instead. For an aggregate over a multi-valued relation
|
|
148
|
+
use :func:`not_draft_q` inside the aggregate's own ``filter=``.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
qs: Any queryset with a single-valued relation reaching ``Project``.
|
|
152
|
+
path: The ORM lookup path from ``qs``'s model to that ``Project``
|
|
153
|
+
(``"project"`` for ``Task``, ``"task__project"`` for a task's children).
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If ``path`` traverses a multi-valued relation.
|
|
157
|
+
"""
|
|
158
|
+
model: Any = qs.model
|
|
159
|
+
for part in path.split("__"):
|
|
160
|
+
field = model._meta.get_field(part)
|
|
161
|
+
if field.many_to_many or field.one_to_many:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"exclude_draft_projects: path {path!r} traverses the multi-valued "
|
|
164
|
+
f"relation {part!r} on {model.__name__}. That would drop every row "
|
|
165
|
+
"whose related set contains ANY draft, not the draft rows — use "
|
|
166
|
+
"not_draft_q() inside the aggregate's filter= instead."
|
|
167
|
+
)
|
|
168
|
+
model = field.related_model
|
|
169
|
+
lookup: dict[str, Any] = {f"{path}__lifecycle": ProjectLifecycle.DRAFT}
|
|
170
|
+
return qs.exclude(**lookup)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def not_draft_q(path: str = "") -> Q:
|
|
174
|
+
"""The same exclusion as a ``Q``, for an aggregate's ``filter=`` argument.
|
|
175
|
+
|
|
176
|
+
``Count(..., filter=...)`` takes a ``Q``, not a queryset, so the one place
|
|
177
|
+
that counts projects inside an annotation on ``Program`` cannot call either
|
|
178
|
+
helper above. It gets this instead of hand-writing the predicate, so the
|
|
179
|
+
exclusion still has one definition rather than one per surface.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
path: The ORM lookup path to the ``Project``, or ``""`` when the
|
|
183
|
+
queryset's own model is ``Project`` (``"projects"`` from ``Program``).
|
|
184
|
+
"""
|
|
185
|
+
field = f"{path}__lifecycle" if path else "lifecycle"
|
|
186
|
+
return ~Q(**{field: ProjectLifecycle.DRAFT})
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def is_draft(project: Project) -> bool:
|
|
190
|
+
"""True when nobody has committed to this plan yet."""
|
|
191
|
+
return project.lifecycle == ProjectLifecycle.DRAFT
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Backfill the date-gated NOT_STARTED → IN_PROGRESS auto-transition (#336).
|
|
2
|
+
|
|
3
|
+
Before #336 landed, setting `planned_start` on a NOT_STARTED task did not
|
|
4
|
+
auto-transition status across most entry points (only the gutter-promote
|
|
5
|
+
action did, briefly, on the frontend). This command finds rows that *should*
|
|
6
|
+
have been IN_PROGRESS under the new rule and corrects them in one pass so
|
|
7
|
+
the unified data model holds for existing data, not just new mutations.
|
|
8
|
+
|
|
9
|
+
Targeted rows: ``status=NOT_STARTED`` AND ``planned_start <= today``.
|
|
10
|
+
|
|
11
|
+
For each, the command:
|
|
12
|
+
|
|
13
|
+
- Sets ``status = IN_PROGRESS``.
|
|
14
|
+
- Sets ``actual_start = planned_start`` (preserving the historical date —
|
|
15
|
+
same logic as ``TaskSerializer.update`` for past-dated planned_start).
|
|
16
|
+
|
|
17
|
+
Idempotent: a second run finds zero matches because the first run moved them
|
|
18
|
+
out of NOT_STARTED. Safe to run repeatedly during deployment rollout.
|
|
19
|
+
|
|
20
|
+
Usage::
|
|
21
|
+
|
|
22
|
+
python manage.py backfill_in_progress_status # apply
|
|
23
|
+
python manage.py backfill_in_progress_status --dry-run # preview only
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from django.core.management.base import BaseCommand
|
|
32
|
+
from django.db import transaction
|
|
33
|
+
from django.utils import timezone
|
|
34
|
+
|
|
35
|
+
from trueppm_api.apps.projects.models import Task, TaskStatus
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Command(BaseCommand):
|
|
41
|
+
help = (
|
|
42
|
+
"Backfill NOT_STARTED tasks with planned_start ≤ today to IN_PROGRESS, "
|
|
43
|
+
"pinning actual_start to planned_start (#336)."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def add_arguments(self, parser: Any) -> None:
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--dry-run",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Print the rows that would be updated without writing.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def handle(self, *args: Any, **options: Any) -> None:
|
|
54
|
+
today = timezone.localdate()
|
|
55
|
+
candidates = Task.objects.filter(
|
|
56
|
+
status=TaskStatus.NOT_STARTED,
|
|
57
|
+
planned_start__lte=today,
|
|
58
|
+
).only("id", "name", "planned_start", "actual_start")
|
|
59
|
+
|
|
60
|
+
count = candidates.count()
|
|
61
|
+
if count == 0:
|
|
62
|
+
self.stdout.write(self.style.SUCCESS("No tasks to backfill."))
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
self.stdout.write(
|
|
66
|
+
f"Found {count} NOT_STARTED task(s) with planned_start ≤ {today.isoformat()}."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if options["dry_run"]:
|
|
70
|
+
for t in candidates[:50]:
|
|
71
|
+
self.stdout.write(f" • {t.id} '{t.name}' planned_start={t.planned_start}")
|
|
72
|
+
if count > 50:
|
|
73
|
+
self.stdout.write(f" … and {count - 50} more")
|
|
74
|
+
self.stdout.write(self.style.WARNING("--dry-run: no changes written."))
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Bulk update inside a transaction; preserve actual_start when it's
|
|
78
|
+
# already set (don't trample historical data) and never overwrite a
|
|
79
|
+
# status that has somehow drifted out of NOT_STARTED between the
|
|
80
|
+
# candidate query and the write.
|
|
81
|
+
updated = 0
|
|
82
|
+
with transaction.atomic():
|
|
83
|
+
for t in candidates.select_for_update():
|
|
84
|
+
if t.status != TaskStatus.NOT_STARTED:
|
|
85
|
+
continue # raced; another writer moved this row already
|
|
86
|
+
t.status = TaskStatus.IN_PROGRESS
|
|
87
|
+
if not t.actual_start:
|
|
88
|
+
t.actual_start = t.planned_start
|
|
89
|
+
t.save(update_fields=["status", "actual_start"])
|
|
90
|
+
updated += 1
|
|
91
|
+
|
|
92
|
+
self.stdout.write(self.style.SUCCESS(f"Backfilled {updated} task(s)."))
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Mint (or pin) the public read-only demo share links (issues #1487, #2440).
|
|
2
|
+
|
|
3
|
+
The public demo (``try.trueppm.com``) dogfoods the product's own tokenized,
|
|
4
|
+
read-only share links (#283, extended to schedules by #1486) rather than a
|
|
5
|
+
bespoke read-only mode: no login, no write path, near-zero abuse surface. This
|
|
6
|
+
command binds :class:`~trueppm_api.apps.projects.models.ShareLink` rows to the
|
|
7
|
+
seeded demo project and prints their public URLs, so both the demo compose stack
|
|
8
|
+
(``docker-compose.demo.yml``) and the Helm demo-seed hook (ADR-0658) can log
|
|
9
|
+
ready-to-share links on startup.
|
|
10
|
+
|
|
11
|
+
**Two content kinds.** A ``SCHEDULE`` link is always minted. A ``BOARD`` link is
|
|
12
|
+
minted *only when a board token is supplied* (``--token-board`` /
|
|
13
|
+
``TRUEPPM_DEMO_SHARE_TOKEN_BOARD``). That asymmetry is deliberate back-compat:
|
|
14
|
+
``docker-compose.demo.yml`` predates board sharing and supplies only the schedule
|
|
15
|
+
token, so it keeps its existing single-link behaviour untouched, while the Helm
|
|
16
|
+
path (which requires both tokens) gets both links. A board link is only worth
|
|
17
|
+
having if it is stable, so there is no generated-token mode for it.
|
|
18
|
+
|
|
19
|
+
Each kind needs its *own* token — ``ShareLink.token_hash`` is globally unique, so
|
|
20
|
+
one raw token cannot back two links.
|
|
21
|
+
|
|
22
|
+
**Two modes** (schedule only):
|
|
23
|
+
|
|
24
|
+
* **Pinned (required for a hosted demo).** Set ``TRUEPPM_DEMO_SHARE_TOKEN``
|
|
25
|
+
(or pass ``--token``) to a fixed value. The command upserts a share link whose
|
|
26
|
+
hash matches that token — idempotent and, crucially, *reprintable*: the same
|
|
27
|
+
stable URL is emitted on every restart, so the demo has one deep-linkable
|
|
28
|
+
address that survives redeploys. Under Helm this is not optional: the seeder is
|
|
29
|
+
destructive and ``ShareLink.project`` is ``on_delete=CASCADE``, so an unpinned
|
|
30
|
+
link changes its URL on every upgrade.
|
|
31
|
+
* **Generated (one-shot).** With no token supplied, a random token is minted
|
|
32
|
+
once and its URL printed a single time (the raw token is never stored, only its
|
|
33
|
+
hash — so it cannot be reprinted). Re-running without a pinned token reuses the
|
|
34
|
+
existing link and reminds the operator to pin a token for a stable URL.
|
|
35
|
+
|
|
36
|
+
Read-only posture: this never touches the ``--with-personas`` /
|
|
37
|
+
``TRUEPPM_DEMO_PASSWORD`` path (#1350). The demo is reachable *only* through the
|
|
38
|
+
unauthenticated share endpoints.
|
|
39
|
+
|
|
40
|
+
**Minting and serving are two different gates** (#2781). ``mint_share_link()``
|
|
41
|
+
writes a row; the *read* additionally requires the ADR-0135 "Public sharing"
|
|
42
|
+
policy to resolve true for the project. Because ``Workspace.public_sharing``
|
|
43
|
+
defaults to ``False``, a demo built from default state minted links that every
|
|
44
|
+
recipient saw as ``410 Gone`` — while this command printed a confident URL. So
|
|
45
|
+
the command now turns sharing on for the project it publishes, and verifies the
|
|
46
|
+
*effective* policy before printing, rather than assuming the mint implies a
|
|
47
|
+
working link.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import os
|
|
53
|
+
from typing import Any
|
|
54
|
+
|
|
55
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
56
|
+
|
|
57
|
+
from trueppm_api.apps.projects.authentication import sha256_hex
|
|
58
|
+
from trueppm_api.apps.projects.models import Project, ShareContentKind, ShareLink
|
|
59
|
+
from trueppm_api.apps.projects.share_services import mint_share_link
|
|
60
|
+
from trueppm_api.apps.projects.sharing_settings import (
|
|
61
|
+
public_sharing_instance_enabled,
|
|
62
|
+
resolve_effective_sharing,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# The project this publishes by default. It used to be imported from
|
|
66
|
+
# ``seed_demo_project``, which #3098 retired when the Python seeders converged
|
|
67
|
+
# into the JSON fixture format; the hosted demo now loads the bundled Atlas
|
|
68
|
+
# sample (``DEFAULT_SAMPLE``) and publishes its flagship project. Overridable
|
|
69
|
+
# with ``--project``.
|
|
70
|
+
PROJECT_NAME = "Platform Core"
|
|
71
|
+
|
|
72
|
+
# Idempotency key: a demo link is matched (and reused) by its exact label so
|
|
73
|
+
# repeated runs never sprawl a fresh link per restart. The two kinds carry
|
|
74
|
+
# *different* labels because the generated-mode lookup filters on label — a shared
|
|
75
|
+
# label would make the two kinds collide on it.
|
|
76
|
+
#
|
|
77
|
+
# The SCHEDULE label is frozen at its original #1487 text on purpose: existing demo
|
|
78
|
+
# deployments already hold rows carrying it, and renaming it would make the
|
|
79
|
+
# generated-mode lookup miss them and mint a duplicate on the next run.
|
|
80
|
+
DEMO_LABEL = "Public read-only demo (#1487)"
|
|
81
|
+
DEMO_LABEL_BOARD = "Public read-only demo — board (#2440)"
|
|
82
|
+
|
|
83
|
+
LABEL_BY_KIND = {
|
|
84
|
+
ShareContentKind.SCHEDULE: DEMO_LABEL,
|
|
85
|
+
ShareContentKind.BOARD: DEMO_LABEL_BOARD,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# URL path segment per kind — mirrors the frontend routes /share/schedule/<token>
|
|
89
|
+
# and /share/board/<token>, and the API's own share/ URL patterns.
|
|
90
|
+
PATH_BY_KIND = {
|
|
91
|
+
ShareContentKind.SCHEDULE: "schedule",
|
|
92
|
+
ShareContentKind.BOARD: "board",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
TOKEN_ENV = "TRUEPPM_DEMO_SHARE_TOKEN"
|
|
96
|
+
TOKEN_BOARD_ENV = "TRUEPPM_DEMO_SHARE_TOKEN_BOARD"
|
|
97
|
+
BASE_URL_ENV = "TRUEPPM_DEMO_BASE_URL"
|
|
98
|
+
# The printed URL is the demo's only entry point (there are no accounts), so the
|
|
99
|
+
# fallback must name a host the operator running the command can actually open.
|
|
100
|
+
# It is therefore the loopback origin the local `docker-compose.demo.yml` stack
|
|
101
|
+
# publishes on — NOT the hosted demo's domain, which would hand a local evaluator
|
|
102
|
+
# a link that resolves to somebody else's instance where their token does not
|
|
103
|
+
# exist (#2801). Every hosted path sets the env var explicitly; the Helm chart
|
|
104
|
+
# fails its render when `demo.baseUrl` is empty rather than falling back here.
|
|
105
|
+
DEFAULT_BASE_URL = "http://localhost"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Command(BaseCommand):
|
|
109
|
+
"""Mint or pin the public read-only demo share links (#1487, #2440)."""
|
|
110
|
+
|
|
111
|
+
help = "Create (or pin) the public read-only demo share links and print their URLs."
|
|
112
|
+
|
|
113
|
+
def add_arguments(self, parser: Any) -> None:
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--project",
|
|
116
|
+
default=PROJECT_NAME,
|
|
117
|
+
help=(
|
|
118
|
+
f"Demo project name to share (default: {PROJECT_NAME!r}, "
|
|
119
|
+
"seeded by load_sample_project)."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--token",
|
|
124
|
+
default=None,
|
|
125
|
+
help=(
|
|
126
|
+
f"Pin a fixed raw token for a stable, reprintable SCHEDULE URL. "
|
|
127
|
+
f"Falls back to the {TOKEN_ENV} env var. When omitted a random "
|
|
128
|
+
"token is minted once."
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--token-board",
|
|
133
|
+
default=None,
|
|
134
|
+
help=(
|
|
135
|
+
f"Pin a fixed raw token for a stable BOARD URL. Falls back to the "
|
|
136
|
+
f"{TOKEN_BOARD_ENV} env var. When omitted no board link is minted "
|
|
137
|
+
"(a board link is only useful if stable, so there is no generated "
|
|
138
|
+
"mode). Must differ from the schedule token."
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--base-url",
|
|
143
|
+
default=None,
|
|
144
|
+
help=(
|
|
145
|
+
f"Public base URL of the demo host (default: {BASE_URL_ENV} env "
|
|
146
|
+
f"var, else {DEFAULT_BASE_URL!r})."
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def handle(self, *args: object, **options: object) -> None:
|
|
151
|
+
project_name = str(options["project"])
|
|
152
|
+
base_url_opt = options.get("base_url")
|
|
153
|
+
base_url = (
|
|
154
|
+
str(base_url_opt) if base_url_opt else os.environ.get(BASE_URL_ENV) or DEFAULT_BASE_URL
|
|
155
|
+
).rstrip("/")
|
|
156
|
+
|
|
157
|
+
token_opt = options.get("token")
|
|
158
|
+
schedule_token = str(token_opt) if token_opt else os.environ.get(TOKEN_ENV)
|
|
159
|
+
token_board_opt = options.get("token_board")
|
|
160
|
+
board_token = str(token_board_opt) if token_board_opt else os.environ.get(TOKEN_BOARD_ENV)
|
|
161
|
+
|
|
162
|
+
# Guard the collision early with an actionable message. Without this the
|
|
163
|
+
# second upsert would silently resolve the *first* kind's row by token_hash
|
|
164
|
+
# and report success while minting nothing.
|
|
165
|
+
if schedule_token and board_token and schedule_token == board_token:
|
|
166
|
+
raise CommandError(
|
|
167
|
+
"The schedule and board tokens are identical. ShareLink.token_hash is "
|
|
168
|
+
"globally unique, so each kind needs its own token — generate a second "
|
|
169
|
+
"one with: openssl rand -base64 32 | tr -d '=+/'"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
# Scoped to is_sample so this publishes the seeder's project and never
|
|
174
|
+
# an operator's same-named real one — Project.name is non-unique, and
|
|
175
|
+
# since #2476 the seeder no longer deletes a real project that
|
|
176
|
+
# collides with the demo name, so it can legitimately coexist here.
|
|
177
|
+
project = Project.objects.get(name=project_name, is_sample=True, is_deleted=False)
|
|
178
|
+
except Project.DoesNotExist as exc:
|
|
179
|
+
raise CommandError(
|
|
180
|
+
f"Demo project {project_name!r} not found. Run "
|
|
181
|
+
"`python manage.py load_sample_project` first."
|
|
182
|
+
) from exc
|
|
183
|
+
|
|
184
|
+
# Turn sharing on *before* minting, so the links this run prints resolve
|
|
185
|
+
# for the recipient rather than 410-ing (#2781).
|
|
186
|
+
self._enable_public_sharing(project)
|
|
187
|
+
|
|
188
|
+
results: list[tuple[str, ShareLink, str]] = []
|
|
189
|
+
|
|
190
|
+
scheduled = self._resolve_link(project, ShareContentKind.SCHEDULE, schedule_token)
|
|
191
|
+
if scheduled is not None:
|
|
192
|
+
results.append(scheduled)
|
|
193
|
+
|
|
194
|
+
# Board is opt-in via its token — see the module docstring for why.
|
|
195
|
+
if board_token:
|
|
196
|
+
board = self._resolve_link(project, ShareContentKind.BOARD, board_token)
|
|
197
|
+
if board is not None:
|
|
198
|
+
results.append(board)
|
|
199
|
+
|
|
200
|
+
if not results:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Re-read the posture the *recipient* will hit. Enabling sharing above is
|
|
204
|
+
# necessary but not sufficient: the operator kill switch sits above every
|
|
205
|
+
# policy, and an Enterprise ENFORCE lock outranks the project override. If
|
|
206
|
+
# either still says no, say so instead of printing a URL that 404s/410s.
|
|
207
|
+
self._warn_if_links_will_not_resolve(project)
|
|
208
|
+
|
|
209
|
+
self.stdout.write(self.style.SUCCESS("=" * 60))
|
|
210
|
+
self.stdout.write(self.style.SUCCESS(" Public read-only demo share links"))
|
|
211
|
+
self.stdout.write(self.style.SUCCESS(f" Project: {project.name!r}"))
|
|
212
|
+
for action, link, token in results:
|
|
213
|
+
kind_path = PATH_BY_KIND[ShareContentKind(link.content_kind)]
|
|
214
|
+
url = f"{base_url}/share/{kind_path}/{token}"
|
|
215
|
+
self.stdout.write(self.style.SUCCESS(""))
|
|
216
|
+
self.stdout.write(self.style.SUCCESS(f" {action} {link.content_kind} link"))
|
|
217
|
+
self.stdout.write(self.style.SUCCESS(f" Link id: {link.id}"))
|
|
218
|
+
self.stdout.write(self.style.SUCCESS(f" URL: {url}"))
|
|
219
|
+
self.stdout.write(self.style.SUCCESS("=" * 60))
|
|
220
|
+
|
|
221
|
+
def _enable_public_sharing(self, project: Any) -> None:
|
|
222
|
+
"""Set the demo project's own ADR-0135 ``public_sharing`` override to True.
|
|
223
|
+
|
|
224
|
+
The override is written at **project** scope, not on the workspace, and
|
|
225
|
+
that is the whole point of the choice: ``Workspace.public_sharing`` is the
|
|
226
|
+
admin-facing default for *every* project on the instance, and a management
|
|
227
|
+
command has no business flipping it. The demo project is public by
|
|
228
|
+
definition; nothing else on the instance is, and a self-hoster who seeds
|
|
229
|
+
the sample onto a real install must not find their workspace default
|
|
230
|
+
silently loosened as a side effect.
|
|
231
|
+
|
|
232
|
+
Safe by scoping: ``handle()`` resolves the project with ``is_sample=True``,
|
|
233
|
+
so the only row this can ever touch is one the seeder created.
|
|
234
|
+
|
|
235
|
+
Idempotent — a second run finds the override already true and writes
|
|
236
|
+
nothing, which keeps ``server_version`` (and the history row each save
|
|
237
|
+
writes) from advancing once per container restart.
|
|
238
|
+
"""
|
|
239
|
+
if project.public_sharing:
|
|
240
|
+
return
|
|
241
|
+
project.public_sharing = True
|
|
242
|
+
project.save(update_fields=["public_sharing"])
|
|
243
|
+
self.stdout.write(
|
|
244
|
+
self.style.WARNING(
|
|
245
|
+
f"Enabled public sharing on demo project {project.name!r} "
|
|
246
|
+
"(project-scoped override; the workspace default is untouched)."
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _warn_if_links_will_not_resolve(self, project: Any) -> None:
|
|
251
|
+
"""Warn when the printed URLs will not actually serve, and say why.
|
|
252
|
+
|
|
253
|
+
The mint and the read are separate gates, and this command historically
|
|
254
|
+
only exercised the first — which is how a demo shipped links that every
|
|
255
|
+
recipient saw as ``410 Gone`` while startup logs showed a confident URL
|
|
256
|
+
(#2781). Two conditions survive :meth:`_enable_public_sharing`:
|
|
257
|
+
|
|
258
|
+
* the operator kill switch (``TRUEPPM_PUBLIC_BOARD_SHARING_ENABLED=false``),
|
|
259
|
+
which outranks every policy and makes the share endpoints answer 404;
|
|
260
|
+
* an Enterprise ``ENFORCE`` sharing lock with the workspace value off,
|
|
261
|
+
under which the project override is ignored and the read answers 410.
|
|
262
|
+
|
|
263
|
+
Neither is this command's to override — an operator or a workspace admin
|
|
264
|
+
set them deliberately. Reporting them is the honest response.
|
|
265
|
+
"""
|
|
266
|
+
if not public_sharing_instance_enabled():
|
|
267
|
+
self.stdout.write(
|
|
268
|
+
self.style.ERROR(
|
|
269
|
+
"TRUEPPM_PUBLIC_BOARD_SHARING_ENABLED is false — this instance "
|
|
270
|
+
"serves no public share links, so the URLs below will 404. Set it "
|
|
271
|
+
"to true to run a public demo."
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
if not resolve_effective_sharing(project, "public_sharing"):
|
|
276
|
+
self.stdout.write(
|
|
277
|
+
self.style.ERROR(
|
|
278
|
+
"Public sharing still resolves to off for this project despite the "
|
|
279
|
+
"project-scoped override — a workspace ENFORCE policy is locking it. "
|
|
280
|
+
"The URLs below will return 410 until a workspace admin turns "
|
|
281
|
+
"'Public sharing' on or relaxes the override policy."
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _resolve_link(
|
|
286
|
+
self,
|
|
287
|
+
project: Any,
|
|
288
|
+
kind: ShareContentKind,
|
|
289
|
+
pinned_token: str | None,
|
|
290
|
+
) -> tuple[str, ShareLink, str] | None:
|
|
291
|
+
"""Return ``(action, link, raw_token)`` for one content kind.
|
|
292
|
+
|
|
293
|
+
Returns ``None`` when there is nothing printable — today that is only the
|
|
294
|
+
generated-mode case where a prior link exists but its raw token cannot be
|
|
295
|
+
recovered, which is a warning rather than an error.
|
|
296
|
+
"""
|
|
297
|
+
if pinned_token:
|
|
298
|
+
link, created = self._upsert_pinned_link(project, kind, pinned_token)
|
|
299
|
+
return ("Created" if created else "Reused"), link, pinned_token
|
|
300
|
+
|
|
301
|
+
existing = (
|
|
302
|
+
ShareLink.objects.filter(
|
|
303
|
+
project=project,
|
|
304
|
+
content_kind=kind,
|
|
305
|
+
label=LABEL_BY_KIND[kind],
|
|
306
|
+
revoked_at__isnull=True,
|
|
307
|
+
)
|
|
308
|
+
.order_by("-created_at")
|
|
309
|
+
.first()
|
|
310
|
+
)
|
|
311
|
+
if existing is not None:
|
|
312
|
+
env_var = TOKEN_ENV if kind == ShareContentKind.SCHEDULE else TOKEN_BOARD_ENV
|
|
313
|
+
self.stdout.write(
|
|
314
|
+
self.style.WARNING(
|
|
315
|
+
f"A generated demo {kind} share link already exists but its raw token "
|
|
316
|
+
f"cannot be recovered. Set {env_var} to pin a stable, "
|
|
317
|
+
"reprintable URL, or revoke it in the sharing UI and re-run."
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
link, token = mint_share_link(
|
|
323
|
+
project,
|
|
324
|
+
user=None,
|
|
325
|
+
label=LABEL_BY_KIND[kind],
|
|
326
|
+
show_assignees=False,
|
|
327
|
+
content_kind=kind,
|
|
328
|
+
)
|
|
329
|
+
return "Created", link, token
|
|
330
|
+
|
|
331
|
+
def _upsert_pinned_link(
|
|
332
|
+
self,
|
|
333
|
+
project: Any,
|
|
334
|
+
kind: ShareContentKind,
|
|
335
|
+
token: str,
|
|
336
|
+
) -> tuple[ShareLink, bool]:
|
|
337
|
+
"""Return ``(link, created)`` for the pinned token, creating it if absent.
|
|
338
|
+
|
|
339
|
+
Matched by ``token_hash`` (the only stored representation of the token),
|
|
340
|
+
so a fixed token always resolves the same row and the URL is reprintable
|
|
341
|
+
across restarts — and across the destructive re-seed that a Helm upgrade
|
|
342
|
+
performs, which cascades the previous rows away entirely.
|
|
343
|
+
"""
|
|
344
|
+
token_hash = sha256_hex(token)
|
|
345
|
+
link = ShareLink.objects.filter(token_hash=token_hash).first()
|
|
346
|
+
if link is not None:
|
|
347
|
+
# A token already bound to the *other* kind must not be silently
|
|
348
|
+
# reported as reused: the caller would see "Reused" and exit 0 while
|
|
349
|
+
# the link they asked for was never minted. The same-run guard in
|
|
350
|
+
# handle() only catches both-tokens-equal; this catches the cross-run
|
|
351
|
+
# case (reusing a previously-pinned schedule token as a board token).
|
|
352
|
+
if link.content_kind != kind:
|
|
353
|
+
raise CommandError(
|
|
354
|
+
f"That token is already bound to a {link.content_kind} share link "
|
|
355
|
+
f"(id {link.id}); it cannot also back a {kind} link because "
|
|
356
|
+
"ShareLink.token_hash is globally unique. Use a different token."
|
|
357
|
+
)
|
|
358
|
+
return link, False
|
|
359
|
+
link = ShareLink.objects.create(
|
|
360
|
+
project=project,
|
|
361
|
+
content_kind=kind,
|
|
362
|
+
token_prefix=token[:12],
|
|
363
|
+
token_hash=token_hash,
|
|
364
|
+
label=LABEL_BY_KIND[kind],
|
|
365
|
+
show_assignees=False,
|
|
366
|
+
created_by=None,
|
|
367
|
+
)
|
|
368
|
+
return link, True
|