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,754 @@
|
|
|
1
|
+
"""Transactional Group / Ungroup primitives for the schedule outline (#2955).
|
|
2
|
+
|
|
3
|
+
Group is ``create container`` + ``reparent N rows``; ungroup is its mirror. Both are
|
|
4
|
+
composable client-side out of the existing single-row ``reparent`` endpoint, and
|
|
5
|
+
composing them that way is the defect already filed as #2914: N+1 un-transacted calls
|
|
6
|
+
whose partial failure leaves a half-made phase with some rows moved and some not, and
|
|
7
|
+
no way to reverse it. The design requires each operation to be **one undo step**, so
|
|
8
|
+
each is one request that either applies whole or writes nothing.
|
|
9
|
+
|
|
10
|
+
Two invariants this module exists to hold that the naive composition cannot:
|
|
11
|
+
|
|
12
|
+
1. **Nothing is written on a rejection.** Every guard raises :class:`GroupingRejected`
|
|
13
|
+
*out* of the caller's ``atomic()`` block rather than returning a response from
|
|
14
|
+
inside it, so the rollback is the transaction's, not a hand-rolled compensation.
|
|
15
|
+
That includes the ADR-0259 graph guard, which runs over the tree the operation
|
|
16
|
+
*would* leave behind. See :func:`assert_graph_feasible` for why that guard is a
|
|
17
|
+
tripwire rather than a gate — and why its verdict is differential.
|
|
18
|
+
|
|
19
|
+
2. **A level's subtree survives its renumbering.** Removing a row from the middle of
|
|
20
|
+
a WBS level shifts every later sibling's number, and a sibling's descendants carry
|
|
21
|
+
that number as a path *prefix*. :func:`rewrite_level` therefore snapshots the whole
|
|
22
|
+
affected subtree into memory before the first write and computes every new path
|
|
23
|
+
from that snapshot — a sibling shifting into a position another sibling still
|
|
24
|
+
occupies cannot corrupt the prefix match that moves its children.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import uuid
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import TYPE_CHECKING, Any
|
|
32
|
+
|
|
33
|
+
from django.db.models import Q
|
|
34
|
+
|
|
35
|
+
from trueppm_api.apps.projects.models import StructuralOperationKind
|
|
36
|
+
from trueppm_api.apps.projects.structural_operation_services import StructuralCapture
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from rest_framework.request import Request
|
|
40
|
+
|
|
41
|
+
from trueppm_api.apps.projects.models import Project, Task
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
#: Placeholder name for a freshly wrapped phase. The design names the phase *last* —
|
|
45
|
+
#: the user selects rows, wraps them, and the UI focuses the name field — so the
|
|
46
|
+
#: container is minted with a placeholder rather than demanding a name up front.
|
|
47
|
+
DEFAULT_CONTAINER_NAME = "New phase"
|
|
48
|
+
|
|
49
|
+
#: Upper bound on one selection. Not a performance ceiling so much as a blast-radius
|
|
50
|
+
#: one: past this size the caller is restructuring the plan, not wrapping a phase, and
|
|
51
|
+
#: a mis-clicked ⌘A should not silently rewrite every path in the project.
|
|
52
|
+
MAX_GROUP_SELECTION = 500
|
|
53
|
+
|
|
54
|
+
#: Why a selected row was not wrapped. Both are reported to the caller — the UI has to
|
|
55
|
+
#: tell the user which rows it left alone, or a group that quietly dropped half the
|
|
56
|
+
#: selection reads as a bug.
|
|
57
|
+
REASON_ANCESTOR_SELECTED = "ancestor_selected"
|
|
58
|
+
REASON_DIFFERENT_PARENT = "different_parent"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GroupingRejected(Exception):
|
|
62
|
+
"""A group/ungroup request the server refuses, carrying its 4xx body.
|
|
63
|
+
|
|
64
|
+
Raised rather than returned so that it unwinds the caller's ``transaction.atomic()``
|
|
65
|
+
block: a guard that fires after some rows have already moved must leave nothing
|
|
66
|
+
behind, and the transaction is the only mechanism that guarantees that without a
|
|
67
|
+
hand-written inverse for every partial state.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
code: str,
|
|
73
|
+
detail: str,
|
|
74
|
+
*,
|
|
75
|
+
status_code: int = 400,
|
|
76
|
+
extra: dict[str, Any] | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
super().__init__(detail)
|
|
79
|
+
self.code = code
|
|
80
|
+
self.detail = detail
|
|
81
|
+
self.status_code = status_code
|
|
82
|
+
self.body: dict[str, Any] = {"code": code, "detail": detail}
|
|
83
|
+
if extra:
|
|
84
|
+
self.body.update(extra)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class LeftAlone:
|
|
89
|
+
"""One selected row the operation deliberately did not wrap."""
|
|
90
|
+
|
|
91
|
+
task_id: str
|
|
92
|
+
reason: str
|
|
93
|
+
ancestor_id: str | None = None
|
|
94
|
+
|
|
95
|
+
def as_dict(self) -> dict[str, Any]:
|
|
96
|
+
return {"id": self.task_id, "reason": self.reason, "ancestor_id": self.ancestor_id}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class Selection:
|
|
101
|
+
"""The outcome of applying the design's selection rules to a raw ``task_ids``."""
|
|
102
|
+
|
|
103
|
+
kept: list[Task]
|
|
104
|
+
left_alone: list[LeftAlone]
|
|
105
|
+
parent_path: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── Selection ───────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_selection(selected: list[Task]) -> Selection:
|
|
112
|
+
"""Reduce a raw multi-select to the rows one container can actually wrap.
|
|
113
|
+
|
|
114
|
+
Two rules, in order, both from the design:
|
|
115
|
+
|
|
116
|
+
1. **Drop any row whose own ancestor is also selected.** You cannot wrap a phase
|
|
117
|
+
together with the work inside it — the work is already inside the phase, and
|
|
118
|
+
moving both would either duplicate the subtree or flatten it. The *nearest*
|
|
119
|
+
selected ancestor is reported so the UI can say which phase covered the row.
|
|
120
|
+
2. **Group on the parent shared by most of the remainder.** A selection spanning
|
|
121
|
+
levels is a normal consequence of shift-clicking down a tree, not a mistake to
|
|
122
|
+
reject; the majority parent is the one the user was working in. Ties break
|
|
123
|
+
toward the parent of the first row in *request* order, which is the row the
|
|
124
|
+
selection started from.
|
|
125
|
+
|
|
126
|
+
``selected`` must be in request order — the tie-break depends on it.
|
|
127
|
+
"""
|
|
128
|
+
paths = {str(t.pk): str(t.wbs_path) for t in selected}
|
|
129
|
+
|
|
130
|
+
left_alone: list[LeftAlone] = []
|
|
131
|
+
remainder: list[Task] = []
|
|
132
|
+
for task in selected:
|
|
133
|
+
own = paths[str(task.pk)]
|
|
134
|
+
# Nearest selected ancestor = the longest selected path that is a strict
|
|
135
|
+
# prefix of this row's path. Longest wins so the reported ancestor is the
|
|
136
|
+
# phase the row is actually inside, not its grandparent.
|
|
137
|
+
ancestors = [
|
|
138
|
+
other_id
|
|
139
|
+
for other_id, other_path in paths.items()
|
|
140
|
+
if other_id != str(task.pk) and own.startswith(f"{other_path}.")
|
|
141
|
+
]
|
|
142
|
+
if ancestors:
|
|
143
|
+
nearest = max(ancestors, key=lambda oid: len(paths[oid]))
|
|
144
|
+
left_alone.append(
|
|
145
|
+
LeftAlone(str(task.pk), REASON_ANCESTOR_SELECTED, ancestor_id=nearest)
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
remainder.append(task)
|
|
149
|
+
|
|
150
|
+
if not remainder:
|
|
151
|
+
# Only reachable when every row was covered by another selected row, which
|
|
152
|
+
# means the caller selected a phase and its contents and nothing else.
|
|
153
|
+
raise GroupingRejected(
|
|
154
|
+
"nothing_to_group",
|
|
155
|
+
"Every selected row sits inside another selected row, so there is nothing to wrap.",
|
|
156
|
+
extra={"left_alone": [entry.as_dict() for entry in left_alone]},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
parent_of = {str(t.pk): parent_path_of(str(t.wbs_path)) for t in remainder}
|
|
160
|
+
counts: dict[str, int] = {}
|
|
161
|
+
for task in remainder:
|
|
162
|
+
counts[parent_of[str(task.pk)]] = counts.get(parent_of[str(task.pk)], 0) + 1
|
|
163
|
+
best = max(counts.values())
|
|
164
|
+
# Tie-break on request order: the first row in the request whose parent is tied
|
|
165
|
+
# for the majority. `remainder` preserves request order, so the first hit wins.
|
|
166
|
+
chosen = next(parent_of[str(t.pk)] for t in remainder if counts[parent_of[str(t.pk)]] == best)
|
|
167
|
+
|
|
168
|
+
kept: list[Task] = []
|
|
169
|
+
for task in remainder:
|
|
170
|
+
if parent_of[str(task.pk)] == chosen:
|
|
171
|
+
kept.append(task)
|
|
172
|
+
else:
|
|
173
|
+
left_alone.append(LeftAlone(str(task.pk), REASON_DIFFERENT_PARENT))
|
|
174
|
+
|
|
175
|
+
return Selection(kept=kept, left_alone=left_alone, parent_path=chosen)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def parent_path_of(wbs_path: str) -> str:
|
|
179
|
+
"""The parent's ``wbs_path``, or ``""`` for a root-level row."""
|
|
180
|
+
parent, _, _ = wbs_path.rpartition(".")
|
|
181
|
+
return parent
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── WBS rewriting ───────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _subtree_snapshot(project_id: Any, roots: list[Task]) -> dict[str, list[Task]]:
|
|
188
|
+
"""Load every live descendant of ``roots``, bucketed by the root it sits under.
|
|
189
|
+
|
|
190
|
+
One query for the whole set, and taken **before** any write in the operation, for
|
|
191
|
+
the reason in the module docstring: once the first path is rewritten, a prefix
|
|
192
|
+
match against the database no longer describes the tree the caller started from.
|
|
193
|
+
"""
|
|
194
|
+
from trueppm_api.apps.projects.models import Task as TaskModel
|
|
195
|
+
|
|
196
|
+
if not roots:
|
|
197
|
+
return {}
|
|
198
|
+
predicate = Q()
|
|
199
|
+
for root in roots:
|
|
200
|
+
predicate |= Q(wbs_path__startswith=f"{root.wbs_path}.")
|
|
201
|
+
rows = list(
|
|
202
|
+
TaskModel.objects.select_for_update()
|
|
203
|
+
.filter(project_id=project_id, is_deleted=False)
|
|
204
|
+
.filter(predicate)
|
|
205
|
+
.order_by("wbs_path")
|
|
206
|
+
)
|
|
207
|
+
buckets: dict[str, list[Task]] = {str(root.wbs_path): [] for root in roots}
|
|
208
|
+
for row in rows:
|
|
209
|
+
# Longest matching prefix, so a descendant is filed under its nearest
|
|
210
|
+
# snapshotted root rather than an outer one when roots nest.
|
|
211
|
+
owner = max(
|
|
212
|
+
(p for p in buckets if str(row.wbs_path).startswith(f"{p}.")),
|
|
213
|
+
key=len,
|
|
214
|
+
default=None,
|
|
215
|
+
)
|
|
216
|
+
if owner is not None:
|
|
217
|
+
buckets[owner].append(row)
|
|
218
|
+
return buckets
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _move_root(
|
|
222
|
+
root: Task, descendants: list[Task], new_path: str, updated: list[dict[str, str]]
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Move one row and its snapshotted subtree to ``new_path``, recording the changes."""
|
|
225
|
+
old_path = str(root.wbs_path)
|
|
226
|
+
if old_path != new_path:
|
|
227
|
+
root.wbs_path = new_path
|
|
228
|
+
root.save(update_fields=["wbs_path"])
|
|
229
|
+
updated.append({"id": str(root.pk), "wbs_path": new_path})
|
|
230
|
+
for descendant in descendants:
|
|
231
|
+
rewritten = new_path + str(descendant.wbs_path)[len(old_path) :]
|
|
232
|
+
if str(descendant.wbs_path) != rewritten:
|
|
233
|
+
descendant.wbs_path = rewritten
|
|
234
|
+
descendant.save(update_fields=["wbs_path"])
|
|
235
|
+
updated.append({"id": str(descendant.pk), "wbs_path": rewritten})
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def rewrite_level(project_id: Any, parent_path: str, ordered: list[Task]) -> list[dict[str, str]]:
|
|
239
|
+
"""Renumber a whole WBS level to ``ordered``, carrying every subtree with it.
|
|
240
|
+
|
|
241
|
+
The counterpart to ``views._renumber_siblings``, which rewrites sibling paths only.
|
|
242
|
+
That is sufficient where the caller has already handled descendants separately, and
|
|
243
|
+
silently wrong here: group and ungroup both *insert into* and *remove from* the
|
|
244
|
+
middle of a level, so later siblings shift and their children must shift with them.
|
|
245
|
+
"""
|
|
246
|
+
from trueppm_api.apps.projects.views import _build_wbs_path
|
|
247
|
+
|
|
248
|
+
snapshot = _subtree_snapshot(project_id, ordered)
|
|
249
|
+
updated: list[dict[str, str]] = []
|
|
250
|
+
for position, task in enumerate(ordered, start=1):
|
|
251
|
+
_move_root(
|
|
252
|
+
task,
|
|
253
|
+
snapshot.get(str(task.wbs_path), []),
|
|
254
|
+
_build_wbs_path(parent_path, position),
|
|
255
|
+
updated,
|
|
256
|
+
)
|
|
257
|
+
return updated
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def staging_base(siblings: list[Task]) -> int:
|
|
261
|
+
"""The first level position guaranteed not to collide with an existing sibling.
|
|
262
|
+
|
|
263
|
+
``len(siblings) + 1`` is the obvious answer and is wrong: a level is not necessarily
|
|
264
|
+
numbered contiguously (a delete leaves ``1, 3``, and ``len + 1 == 3`` collides with
|
|
265
|
+
a live row). Both operations stage rows at out-of-range positions before the level
|
|
266
|
+
is renumbered, so the slot has to be free against the *actual* labels, not the count.
|
|
267
|
+
"""
|
|
268
|
+
highest = 0
|
|
269
|
+
for sibling in siblings:
|
|
270
|
+
_, _, label = str(sibling.wbs_path).rpartition(".")
|
|
271
|
+
if label.isdigit():
|
|
272
|
+
highest = max(highest, int(label))
|
|
273
|
+
return highest + 1
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _park_level(project_id: Any, parent_path: str, rows: list[Task], base: int) -> None:
|
|
277
|
+
"""Move ``rows`` to reserved, out-of-range positions at ``parent_path``.
|
|
278
|
+
|
|
279
|
+
Both operations need to hold rows *somewhere* at the destination level while the
|
|
280
|
+
level is renumbered around them. Parking them past the level's highest live label
|
|
281
|
+
keeps every intermediate path collision-free without needing a second numbering
|
|
282
|
+
scheme — ``rewrite_level`` then assigns their real positions in the same pass as
|
|
283
|
+
everyone else's.
|
|
284
|
+
"""
|
|
285
|
+
from trueppm_api.apps.projects.views import _build_wbs_path
|
|
286
|
+
|
|
287
|
+
snapshot = _subtree_snapshot(project_id, rows)
|
|
288
|
+
sink: list[dict[str, str]] = []
|
|
289
|
+
for offset, row in enumerate(rows):
|
|
290
|
+
_move_root(
|
|
291
|
+
row,
|
|
292
|
+
snapshot.get(str(row.wbs_path), []),
|
|
293
|
+
_build_wbs_path(parent_path, base + offset),
|
|
294
|
+
sink,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ── Graph guard ─────────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
#: A project's dependency edges and WBS children-map — the two inputs the ADR-0259
|
|
302
|
+
#: guard reads. Captured before a restructure so a verdict on the post-state can be
|
|
303
|
+
#: compared against the pre-state rather than blamed on this operation blindly.
|
|
304
|
+
GraphState = tuple[list[tuple[str, str]], dict[str, list[str]]]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def capture_graph_state(project_id: Any) -> GraphState:
|
|
308
|
+
"""Read the guard's two inputs. Two queries; no cycle detection."""
|
|
309
|
+
from trueppm_api.apps.projects.models import Dependency
|
|
310
|
+
from trueppm_api.apps.projects.serializers import _load_project_tasks_and_children_map
|
|
311
|
+
|
|
312
|
+
task_ids, children_map = _load_project_tasks_and_children_map(project_id)
|
|
313
|
+
if not task_ids:
|
|
314
|
+
return [], {}
|
|
315
|
+
edges = [
|
|
316
|
+
(str(pred), str(succ))
|
|
317
|
+
for pred, succ in Dependency.objects.filter(
|
|
318
|
+
is_deleted=False, predecessor_id__in=[uuid.UUID(t) for t in task_ids]
|
|
319
|
+
).values_list("predecessor_id", "successor_id")
|
|
320
|
+
]
|
|
321
|
+
return edges, children_map
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _guard_errors() -> tuple[type[Exception], ...]:
|
|
325
|
+
"""The exception types the ADR-0259 guard raises, as a single ``except`` target."""
|
|
326
|
+
from trueppm_scheduler import InvalidScheduleInput
|
|
327
|
+
|
|
328
|
+
from trueppm_api.apps.scheduling.graph_guard import InfeasibleGraphError
|
|
329
|
+
|
|
330
|
+
return (InfeasibleGraphError, InvalidScheduleInput)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _assert_schedulable(state: GraphState) -> None:
|
|
334
|
+
"""Return cleanly when this graph is schedulable, else re-raise the guard's error.
|
|
335
|
+
|
|
336
|
+
The exception propagates rather than being returned, so a caller that wants to
|
|
337
|
+
chain it into its own refusal binds it with ``except ... as`` — the cause of a
|
|
338
|
+
``raise ... from`` is then an exception by construction rather than by a nullable
|
|
339
|
+
return the reader (and every static analyzer) has to prove non-``None`` themselves.
|
|
340
|
+
"""
|
|
341
|
+
from trueppm_api.apps.scheduling.graph_guard import validate_task_graph
|
|
342
|
+
|
|
343
|
+
edges, children_map = state
|
|
344
|
+
if not edges:
|
|
345
|
+
# No edges, no cycle — and no reason to expand a whole project's children_map
|
|
346
|
+
# to prove it. A plan with no dependencies is the common case for the outline
|
|
347
|
+
# this endpoint serves, so the cheap exit is the usual one.
|
|
348
|
+
return
|
|
349
|
+
validate_task_graph(edges, children_map=children_map)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _is_schedulable(state: GraphState) -> bool:
|
|
353
|
+
"""Whether ``state`` passes the guard, discarding the reason it does not."""
|
|
354
|
+
try:
|
|
355
|
+
_assert_schedulable(state)
|
|
356
|
+
except _guard_errors():
|
|
357
|
+
return False
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def assert_graph_feasible(project_id: Any, before: GraphState) -> None:
|
|
362
|
+
"""Run the ADR-0259 graph guard over the tree this operation left behind.
|
|
363
|
+
|
|
364
|
+
Called after the restructure is written and before the transaction commits, because
|
|
365
|
+
the thing being validated is the post-move tree. The guard is the same one
|
|
366
|
+
``tasks/bulk`` runs, applied for the same reason: a write must not hand the CPM
|
|
367
|
+
engine a graph it cannot schedule.
|
|
368
|
+
|
|
369
|
+
**The verdict is differential, and that is load-bearing.** A project can already be
|
|
370
|
+
infeasible without this caller's help — the importers validate their own edges but
|
|
371
|
+
a project assembled across several of them can still arrive cyclic — and rejecting
|
|
372
|
+
every subsequent restructure of it would leave the user unable to touch the plan
|
|
373
|
+
they need to restructure in order to *fix* it. So an infeasibility that was already
|
|
374
|
+
there is not this operation's to refuse, exactly as ``tasks/bulk`` terminates rather
|
|
375
|
+
than rejecting its own rows over a pre-existing cycle (ADR-0772 §4). The pre-state
|
|
376
|
+
check runs only when the post-state fails, so the happy path pays two queries and
|
|
377
|
+
one cycle detection rather than two of each.
|
|
378
|
+
|
|
379
|
+
**Worth stating plainly: on today's expansion rules this cannot fire.**
|
|
380
|
+
``find_cycle`` resolves every edge endpoint to its *leaf* descendants, and neither
|
|
381
|
+
operation changes any existing summary's leaf set — group inserts a container
|
|
382
|
+
between a parent and rows that were already under it, so the parent's leaves are
|
|
383
|
+
the same tasks reached one level deeper, and ungroup removes such a container
|
|
384
|
+
again. Neither writes an edge. The guard is retained as a tripwire rather than
|
|
385
|
+
deleted as dead code: it is one detection over a graph the caller has already
|
|
386
|
+
loaded, and the property protecting it ("a restructure preserves every leaf set")
|
|
387
|
+
is exactly the kind that a later change to what a container may carry would break
|
|
388
|
+
silently.
|
|
389
|
+
"""
|
|
390
|
+
after = capture_graph_state(project_id)
|
|
391
|
+
try:
|
|
392
|
+
_assert_schedulable(after)
|
|
393
|
+
except _guard_errors() as failure:
|
|
394
|
+
if not _is_schedulable(before):
|
|
395
|
+
# Already broken when we arrived. Not ours to refuse.
|
|
396
|
+
return
|
|
397
|
+
offending = getattr(failure, "offending", None)
|
|
398
|
+
reason = getattr(failure, "reason", "invalid_graph_input")
|
|
399
|
+
raise GroupingRejected(
|
|
400
|
+
reason,
|
|
401
|
+
"This restructure would make the dependency graph infeasible. No rows were moved.",
|
|
402
|
+
extra={"offending": offending} if offending is not None else None,
|
|
403
|
+
) from failure
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ── Guards shared by both operations ────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def load_selection(project: Project, raw_ids: Any) -> list[Task]:
|
|
410
|
+
"""Parse and resolve a ``task_ids`` body field into locked, live, in-project rows.
|
|
411
|
+
|
|
412
|
+
Order is preserved: :func:`resolve_selection`'s tie-break is defined on request
|
|
413
|
+
order, so the caller must not be handed a set.
|
|
414
|
+
"""
|
|
415
|
+
from trueppm_api.apps.projects.models import Task as TaskModel
|
|
416
|
+
|
|
417
|
+
if not isinstance(raw_ids, list) or not raw_ids:
|
|
418
|
+
raise GroupingRejected("invalid_task_ids", "`task_ids` must be a non-empty array.")
|
|
419
|
+
if len(raw_ids) > MAX_GROUP_SELECTION:
|
|
420
|
+
raise GroupingRejected(
|
|
421
|
+
"selection_too_large",
|
|
422
|
+
f"A group may wrap at most {MAX_GROUP_SELECTION} rows in one request.",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
parsed: list[uuid.UUID] = []
|
|
426
|
+
seen: set[uuid.UUID] = set()
|
|
427
|
+
for raw in raw_ids:
|
|
428
|
+
# str() first: a non-string, non-UUID member (a dict, a list, an int) must be a
|
|
429
|
+
# 400 naming the field, never a 500 out of uuid.UUID's TypeError (#2785/#2795).
|
|
430
|
+
if not isinstance(raw, str):
|
|
431
|
+
raise GroupingRejected("invalid_task_ids", "`task_ids` members must be UUID strings.")
|
|
432
|
+
try:
|
|
433
|
+
parsed_id = uuid.UUID(raw)
|
|
434
|
+
except ValueError as exc:
|
|
435
|
+
raise GroupingRejected(
|
|
436
|
+
"invalid_task_ids", f"`task_ids` contains a value that is not a UUID: {raw!r}."
|
|
437
|
+
) from exc
|
|
438
|
+
if parsed_id in seen:
|
|
439
|
+
continue # a duplicated id is the same row, not two rows
|
|
440
|
+
seen.add(parsed_id)
|
|
441
|
+
parsed.append(parsed_id)
|
|
442
|
+
|
|
443
|
+
rows = {
|
|
444
|
+
row.pk: row
|
|
445
|
+
for row in TaskModel.objects.select_for_update().filter(
|
|
446
|
+
pk__in=parsed, project_id=project.pk, is_deleted=False
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
missing = [str(pid) for pid in parsed if pid not in rows]
|
|
450
|
+
if missing:
|
|
451
|
+
raise GroupingRejected(
|
|
452
|
+
"unknown_task",
|
|
453
|
+
"One or more task ids do not name a live task in this project.",
|
|
454
|
+
status_code=404,
|
|
455
|
+
extra={"unknown": missing},
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
ordered = [rows[pid] for pid in parsed]
|
|
459
|
+
if any(row.is_subtask for row in ordered):
|
|
460
|
+
raise GroupingRejected(
|
|
461
|
+
"cannot_group_subtasks",
|
|
462
|
+
(
|
|
463
|
+
"Subtasks belong to their parent task and cannot be wrapped in a phase. "
|
|
464
|
+
"Remove them from the selection."
|
|
465
|
+
),
|
|
466
|
+
)
|
|
467
|
+
if any(not row.wbs_path for row in ordered):
|
|
468
|
+
raise GroupingRejected(
|
|
469
|
+
"task_without_wbs_path", "A selected task has no WBS path and cannot be moved."
|
|
470
|
+
)
|
|
471
|
+
return ordered
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _require_restructure(request: Request, tasks: list[Task]) -> None:
|
|
475
|
+
"""Gate every row the operation will move at field-edit authority (ADR-0133).
|
|
476
|
+
|
|
477
|
+
Group and ungroup both rewrite ``wbs_path`` on rows the caller did not name
|
|
478
|
+
individually, so the per-row check is the load-bearing one — the view-level
|
|
479
|
+
permission classes only answer "may this caller author this project's plan at all".
|
|
480
|
+
"""
|
|
481
|
+
from trueppm_api.apps.projects.views import _require_wbs_restructure_permission
|
|
482
|
+
|
|
483
|
+
for task in tasks:
|
|
484
|
+
_require_wbs_restructure_permission(request, task)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# ── Group ───────────────────────────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def perform_group(project: Project, request: Request, raw_ids: Any, name: Any) -> dict[str, Any]:
|
|
491
|
+
"""Wrap a selection in a new declared container. Caller supplies ``atomic()``.
|
|
492
|
+
|
|
493
|
+
Returns the response body. Raises :class:`GroupingRejected` or ``PermissionDenied``
|
|
494
|
+
on any refusal, both of which unwind the caller's transaction so a rejection after
|
|
495
|
+
a partial move leaves nothing written.
|
|
496
|
+
"""
|
|
497
|
+
from trueppm_api.apps.projects.models import StructureRole, resync_container_declarations
|
|
498
|
+
from trueppm_api.apps.projects.views import (
|
|
499
|
+
_build_wbs_path,
|
|
500
|
+
_get_siblings,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
if name is not None and not isinstance(name, str):
|
|
504
|
+
raise GroupingRejected("invalid_name", "`name` must be a string or null.")
|
|
505
|
+
container_name = (name or "").strip() or DEFAULT_CONTAINER_NAME
|
|
506
|
+
if len(container_name) > 512:
|
|
507
|
+
raise GroupingRejected("invalid_name", "`name` must be 512 characters or fewer.")
|
|
508
|
+
|
|
509
|
+
selected = load_selection(project, raw_ids)
|
|
510
|
+
selection = resolve_selection(selected)
|
|
511
|
+
_require_restructure(request, selection.kept)
|
|
512
|
+
|
|
513
|
+
project_id = str(project.pk)
|
|
514
|
+
graph_before = capture_graph_state(project_id)
|
|
515
|
+
parent_path = selection.parent_path
|
|
516
|
+
capture = StructuralCapture.begin(project.pk, {parent_path})
|
|
517
|
+
|
|
518
|
+
parent: Task | None = None
|
|
519
|
+
if parent_path:
|
|
520
|
+
from trueppm_api.apps.projects.models import Task as TaskModel
|
|
521
|
+
|
|
522
|
+
parent = (
|
|
523
|
+
TaskModel.objects.select_for_update()
|
|
524
|
+
.filter(project_id=project_id, wbs_path=parent_path, is_deleted=False)
|
|
525
|
+
.first()
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
siblings = _get_siblings(project_id, parent_path, lock=True)
|
|
529
|
+
kept_ids = {task.pk for task in selection.kept}
|
|
530
|
+
# Tree order, not request order: the container's contents read top-to-bottom the way
|
|
531
|
+
# the outline already showed them, so wrapping never silently reorders the plan.
|
|
532
|
+
kept_in_tree_order = [s for s in siblings if s.pk in kept_ids]
|
|
533
|
+
remaining = [s for s in siblings if s.pk not in kept_ids]
|
|
534
|
+
first_index = next(i for i, s in enumerate(siblings) if s.pk in kept_ids)
|
|
535
|
+
|
|
536
|
+
# Phase 1 — mint the container past the end of the level, where no path can
|
|
537
|
+
# collide, and move the kept subtrees under it.
|
|
538
|
+
from trueppm_api.apps.projects.models import Task as TaskModel
|
|
539
|
+
|
|
540
|
+
staging_path = _build_wbs_path(parent_path, staging_base(siblings))
|
|
541
|
+
container = TaskModel.objects.create(
|
|
542
|
+
project=project,
|
|
543
|
+
name=container_name,
|
|
544
|
+
wbs_path=staging_path,
|
|
545
|
+
# A grouped container is *declared*, not inferred (#2950). `auto_container`
|
|
546
|
+
# stays False on purpose: the row did not drift into container-ness by
|
|
547
|
+
# acquiring a child, so losing its last child must not silently demote it
|
|
548
|
+
# back to work. The user asked for a phase; it stays a phase until they say
|
|
549
|
+
# otherwise.
|
|
550
|
+
structure_role=StructureRole.CONTAINER,
|
|
551
|
+
auto_container=False,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
updated: list[dict[str, str]] = []
|
|
555
|
+
inner_snapshot = _subtree_snapshot(project_id, kept_in_tree_order)
|
|
556
|
+
for position, task in enumerate(kept_in_tree_order, start=1):
|
|
557
|
+
_move_root(
|
|
558
|
+
task,
|
|
559
|
+
inner_snapshot.get(str(task.wbs_path), []),
|
|
560
|
+
_build_wbs_path(staging_path, position),
|
|
561
|
+
updated,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Phase 2 — put the container where the first wrapped row used to be and renumber
|
|
565
|
+
# the level around it.
|
|
566
|
+
new_level = [*remaining[:first_index], container, *remaining[first_index:]]
|
|
567
|
+
level_updates = rewrite_level(project_id, parent_path, new_level)
|
|
568
|
+
|
|
569
|
+
by_id: dict[str, str] = {entry["id"]: entry["wbs_path"] for entry in updated}
|
|
570
|
+
for entry in level_updates:
|
|
571
|
+
by_id[entry["id"]] = entry["wbs_path"]
|
|
572
|
+
|
|
573
|
+
# The container has children the moment it is created, so park its own status and
|
|
574
|
+
# estimate the same way any row promoted over the container line does. Without this
|
|
575
|
+
# the placeholder duration would read as the phase's own estimate rather than as a
|
|
576
|
+
# parked value the rollup shadows.
|
|
577
|
+
resync_container_declarations(container)
|
|
578
|
+
|
|
579
|
+
assert_graph_feasible(project_id, graph_before)
|
|
580
|
+
|
|
581
|
+
warning: str | None = None
|
|
582
|
+
if parent is not None:
|
|
583
|
+
from trueppm_api.apps.resources.models import TaskResource
|
|
584
|
+
|
|
585
|
+
# Mirrors reparent's warning: the level's parent has not changed shape here,
|
|
586
|
+
# but a *newly* childless-to-parent transition on it would surface the same
|
|
587
|
+
# "assignments on a summary" condition. Only fires when the container is the
|
|
588
|
+
# parent's first structural child, which happens when the whole level was
|
|
589
|
+
# wrapped.
|
|
590
|
+
if not remaining and TaskResource.objects.filter(task=parent).exists():
|
|
591
|
+
warning = "has_assignments"
|
|
592
|
+
|
|
593
|
+
operation = capture.record(
|
|
594
|
+
project=project,
|
|
595
|
+
applied_by=request.user,
|
|
596
|
+
kind=StructuralOperationKind.GROUP,
|
|
597
|
+
anchors=[task.pk for task in selection.kept],
|
|
598
|
+
created_task_ids=[container.pk],
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
"container": {
|
|
603
|
+
"id": str(container.pk),
|
|
604
|
+
"name": container.name,
|
|
605
|
+
"wbs_path": by_id.get(str(container.pk), str(container.wbs_path)),
|
|
606
|
+
"structure_role": container.structure_role,
|
|
607
|
+
"parent_id": str(parent.pk) if parent is not None else None,
|
|
608
|
+
},
|
|
609
|
+
"grouped_ids": [str(task.pk) for task in kept_in_tree_order],
|
|
610
|
+
"left_alone": [entry.as_dict() for entry in selection.left_alone],
|
|
611
|
+
"updated": [{"id": tid, "wbs_path": path} for tid, path in by_id.items()],
|
|
612
|
+
"warning": warning,
|
|
613
|
+
"operation_id": str(operation.pk),
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# ── Ungroup ─────────────────────────────────────────────────────────────────────
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def perform_ungroup(project: Project, request: Request, raw_id: Any) -> dict[str, Any]:
|
|
621
|
+
"""Dissolve a container, lifting its children one level. Caller supplies ``atomic()``.
|
|
622
|
+
|
|
623
|
+
"Only the wrapper goes": the children keep their ids, their dependency edges, their
|
|
624
|
+
resource assignments and their estimates, because nothing about them is rewritten
|
|
625
|
+
except ``wbs_path``. The wrapper's *own* edges go with the wrapper — that is what
|
|
626
|
+
``Task.soft_delete`` does to any task — and are reported back so the UI can say so
|
|
627
|
+
rather than letting the links vanish silently.
|
|
628
|
+
"""
|
|
629
|
+
from trueppm_api.apps.access.permissions import can_user_edit_task
|
|
630
|
+
from trueppm_api.apps.projects.models import (
|
|
631
|
+
Dependency,
|
|
632
|
+
resync_container_declarations,
|
|
633
|
+
)
|
|
634
|
+
from trueppm_api.apps.projects.models import (
|
|
635
|
+
Task as TaskModel,
|
|
636
|
+
)
|
|
637
|
+
from trueppm_api.apps.projects.views import _get_siblings
|
|
638
|
+
|
|
639
|
+
if not isinstance(raw_id, str):
|
|
640
|
+
raise GroupingRejected("invalid_task_id", "`task_id` must be a UUID string.")
|
|
641
|
+
try:
|
|
642
|
+
container_id = uuid.UUID(raw_id)
|
|
643
|
+
except ValueError as exc:
|
|
644
|
+
raise GroupingRejected("invalid_task_id", "`task_id` must be a UUID string.") from exc
|
|
645
|
+
|
|
646
|
+
project_id = str(project.pk)
|
|
647
|
+
container = (
|
|
648
|
+
TaskModel.objects.select_for_update()
|
|
649
|
+
.filter(pk=container_id, project_id=project_id, is_deleted=False)
|
|
650
|
+
.first()
|
|
651
|
+
)
|
|
652
|
+
if container is None:
|
|
653
|
+
raise GroupingRejected(
|
|
654
|
+
"unknown_task", "No live task with that id in this project.", status_code=404
|
|
655
|
+
)
|
|
656
|
+
if not container.wbs_path:
|
|
657
|
+
raise GroupingRejected(
|
|
658
|
+
"task_without_wbs_path", "This task has no WBS path and cannot be dissolved."
|
|
659
|
+
)
|
|
660
|
+
if container.is_subtask:
|
|
661
|
+
raise GroupingRejected(
|
|
662
|
+
"cannot_ungroup_subtask", "A subtask is not a phase and cannot be dissolved."
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
container_path = str(container.wbs_path)
|
|
666
|
+
parent_path = parent_path_of(container_path)
|
|
667
|
+
|
|
668
|
+
# A phase should never carry drawer subtasks ("phases group work — add tasks inside
|
|
669
|
+
# the phase, not subtasks"), but if one somehow does, `Task.soft_delete` would
|
|
670
|
+
# cascade-delete them along with the wrapper. Refusing is the only outcome that
|
|
671
|
+
# keeps "only the wrapper goes" true.
|
|
672
|
+
if TaskModel.objects.filter(
|
|
673
|
+
project_id=project_id,
|
|
674
|
+
is_deleted=False,
|
|
675
|
+
is_subtask=True,
|
|
676
|
+
wbs_path__startswith=f"{container_path}.",
|
|
677
|
+
).exists():
|
|
678
|
+
raise GroupingRejected(
|
|
679
|
+
"container_has_subtasks",
|
|
680
|
+
(
|
|
681
|
+
"This phase carries subtasks, which would be deleted with it. "
|
|
682
|
+
"Move or delete them first."
|
|
683
|
+
),
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
children = _get_siblings(project_id, container_path, lock=True)
|
|
687
|
+
_require_restructure(request, [container, *children])
|
|
688
|
+
if not can_user_edit_task(request, container, method="DELETE"):
|
|
689
|
+
from rest_framework.exceptions import PermissionDenied
|
|
690
|
+
|
|
691
|
+
raise PermissionDenied("You do not have permission to delete this phase.")
|
|
692
|
+
|
|
693
|
+
siblings = _get_siblings(project_id, parent_path, lock=True)
|
|
694
|
+
container_index = next(i for i, s in enumerate(siblings) if s.pk == container.pk)
|
|
695
|
+
graph_before = capture_graph_state(project_id)
|
|
696
|
+
capture = StructuralCapture.begin(project.pk, {parent_path})
|
|
697
|
+
|
|
698
|
+
removed_dependency_ids = [
|
|
699
|
+
str(dep_id)
|
|
700
|
+
for dep_id in Dependency.objects.filter(is_deleted=False)
|
|
701
|
+
.filter(Q(predecessor_id=container.pk) | Q(successor_id=container.pk))
|
|
702
|
+
.values_list("pk", flat=True)
|
|
703
|
+
]
|
|
704
|
+
from trueppm_api.apps.resources.models import TaskResource
|
|
705
|
+
|
|
706
|
+
warning = "has_assignments" if TaskResource.objects.filter(task=container).exists() else None
|
|
707
|
+
|
|
708
|
+
# Park the children at the destination level before the wrapper goes, so the
|
|
709
|
+
# renumbering pass below sees one flat level with no path collisions.
|
|
710
|
+
_park_level(project_id, parent_path, children, staging_base(siblings))
|
|
711
|
+
|
|
712
|
+
container.soft_delete()
|
|
713
|
+
|
|
714
|
+
new_level = siblings[:container_index] + children + siblings[container_index + 1 :]
|
|
715
|
+
updated = rewrite_level(project_id, parent_path, new_level)
|
|
716
|
+
|
|
717
|
+
if parent_path:
|
|
718
|
+
parent = (
|
|
719
|
+
TaskModel.objects.select_for_update()
|
|
720
|
+
.filter(project_id=project_id, wbs_path=parent_path, is_deleted=False)
|
|
721
|
+
.first()
|
|
722
|
+
)
|
|
723
|
+
# Dissolving an *empty* phase can take its parent's last structural child, which
|
|
724
|
+
# is the transition that restores the parent's parked status and estimate.
|
|
725
|
+
# Through the shared helper, which picks the update_fields the transition
|
|
726
|
+
# actually needs. This site hand-listed the PROMOTION columns on a branch that
|
|
727
|
+
# only ever DE-promotes, so `status` and `duration` were restored in memory and
|
|
728
|
+
# dropped by the save while `own_status` / `own_estimate` were cleared — the
|
|
729
|
+
# parked values were destroyed rather than handed back (#3010).
|
|
730
|
+
resync_container_declarations(parent)
|
|
731
|
+
|
|
732
|
+
assert_graph_feasible(project_id, graph_before)
|
|
733
|
+
|
|
734
|
+
# This row is what makes #3006 answerable: the wrapper is soft-deleted, so restoring
|
|
735
|
+
# it returns the *original* id, name, notes and labels rather than the new container
|
|
736
|
+
# a re-group would mint. Nothing about the wrapper needs snapshotting — the soft
|
|
737
|
+
# delete retains all of it; only the pointer does.
|
|
738
|
+
operation = capture.record(
|
|
739
|
+
project=project,
|
|
740
|
+
applied_by=request.user,
|
|
741
|
+
kind=StructuralOperationKind.UNGROUP,
|
|
742
|
+
anchors=[container.pk, *[child.pk for child in children]],
|
|
743
|
+
deleted_task_ids=[container.pk],
|
|
744
|
+
removed_dependency_ids=[uuid.UUID(dep_id) for dep_id in removed_dependency_ids],
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
"container_id": str(container.pk),
|
|
749
|
+
"lifted_ids": [str(child.pk) for child in children],
|
|
750
|
+
"removed_dependency_ids": removed_dependency_ids,
|
|
751
|
+
"updated": updated,
|
|
752
|
+
"warning": warning,
|
|
753
|
+
"operation_id": str(operation.pk),
|
|
754
|
+
}
|