trueppm-api 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +27 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +109 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +459 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +178 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +863 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +597 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +528 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +280 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +551 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +566 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +993 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +537 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +541 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +458 -0
- trueppm_api/apps/projects/backfill.py +459 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1244 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1979 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/mc_admission.py +181 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2908 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +446 -0
- trueppm_api/apps/sso/services.py +1473 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +440 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +371 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +254 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/demo_read_only.py +314 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +1221 -0
- trueppm_api/core/password_reset.py +513 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/core/worker_selfheal.py +205 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"""Recording and undo for the schedule outline's structural acts (ADR-0880, #2974, #3006).
|
|
2
|
+
|
|
3
|
+
Six gestures reshape the WBS — indent, outdent, reparent, reorder, group, ungroup — and
|
|
4
|
+
until this module none of them could be reversed. The session trail (#2948) could say what
|
|
5
|
+
happened and not put it back.
|
|
6
|
+
|
|
7
|
+
All six are reversed by **one** function. Parenthood in this tree is ``wbs_path`` and
|
|
8
|
+
nothing else (there is no ``parent_id`` column — see ``models.structural_parent``), so the
|
|
9
|
+
entire outline shape is one string per row and "restore the shape" is "restore N strings"
|
|
10
|
+
regardless of which gesture produced it. ``undo_structural_operation()`` therefore never
|
|
11
|
+
branches on ``StructuralOperation.kind``; a future structural act becomes undoable by
|
|
12
|
+
recording a snapshot, with no new inverse code.
|
|
13
|
+
|
|
14
|
+
Two reuses are wrong here, and both look obvious:
|
|
15
|
+
|
|
16
|
+
**Do not reuse ``task_batch_services._partition_touched``.** It is correct for paste-many
|
|
17
|
+
and cascade, whose fields are independent per row: skipping a touched row leaves the others
|
|
18
|
+
consistent. ``wbs_path`` is a tree and its rows are not independent. Restoring a subset can
|
|
19
|
+
mint a *duplicate* path, and nothing anywhere in the stack would catch it — there is no
|
|
20
|
+
``UniqueConstraint`` on ``(project, wbs_path)``, ``LtreeField`` has no validators, and no
|
|
21
|
+
runtime integrity check exists. The next ``rewrite_level`` over that level then reads a
|
|
22
|
+
collapsed ``_subtree_snapshot`` bucket and writes garbled paths into the *other* twin's
|
|
23
|
+
descendants. Undo here is all-or-nothing: if anything it would write has moved, it refuses.
|
|
24
|
+
|
|
25
|
+
**Do not reuse ``models.cascade_task_children_restore``.** Restoring a container is not the
|
|
26
|
+
same act as restoring a task. That helper resurrects earlier-deleted subtasks — its own
|
|
27
|
+
docstring concedes the tradeoff, which was accepted for a user who explicitly clicked
|
|
28
|
+
Restore on a row they deleted, not for someone reversing a *grouping*. ``perform_ungroup``
|
|
29
|
+
only refuses on *live* subtasks, so a container carrying tombstones ungroups cleanly and a
|
|
30
|
+
cascade on undo would silently un-delete them. Undo restores exactly the ids it recorded.
|
|
31
|
+
(Its missing project scope — an unscoped ``wbs_path__startswith`` that matched the same
|
|
32
|
+
path in every other project — was fixed in #3010; the reuse is still wrong for the reason
|
|
33
|
+
above.)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import uuid
|
|
39
|
+
from typing import TYPE_CHECKING, Any
|
|
40
|
+
|
|
41
|
+
from django.db import transaction
|
|
42
|
+
from django.db.models import Q
|
|
43
|
+
from django.utils import timezone
|
|
44
|
+
|
|
45
|
+
from trueppm_api.apps.projects.refusal_codes import StructuralUndoBlockedReason
|
|
46
|
+
from trueppm_api.apps.projects.restructure_hooks import (
|
|
47
|
+
broadcast_tasks_restructured_and_recalc,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from rest_framework.request import Request
|
|
52
|
+
|
|
53
|
+
from trueppm_api.apps.projects.models import Project, StructuralOperation
|
|
54
|
+
|
|
55
|
+
#: Ceiling on how many rows one operation may record.
|
|
56
|
+
#:
|
|
57
|
+
#: ``MAX_GROUP_SELECTION`` bounds group and ungroup; nothing bounds the other four.
|
|
58
|
+
#: ``_get_descendants`` has no limit, so an outdent near the root pulls its whole
|
|
59
|
+
#: descendant set *plus* every following sibling it adopts, and a root-level reorder is
|
|
60
|
+
#: unbounded in level width. Left uncapped, one reparent on a large project would write two
|
|
61
|
+
#: maps of tens of thousands of entries inside the hot write transaction (ADR-0880 §7).
|
|
62
|
+
#:
|
|
63
|
+
#: Past the cap the act still succeeds — only its *reversal* is refused, and the ledger row
|
|
64
|
+
#: records itself ``undoable=False`` so the trail can say so rather than offering a control
|
|
65
|
+
#: that fails.
|
|
66
|
+
STRUCTURAL_OPERATION_MAX_REGION = 2000
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class StructuralUndoRejected(Exception):
|
|
70
|
+
"""A refusal that maps to a structured 409 rather than a 500.
|
|
71
|
+
|
|
72
|
+
Mirrors ``task_grouping.GroupingRejected``: carries a machine-branchable ``code``
|
|
73
|
+
beside the human sentence, so a client can branch without parsing prose.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, code: str, detail: str, *, extra: dict[str, Any] | None = None) -> None:
|
|
77
|
+
super().__init__(detail)
|
|
78
|
+
self.code = code
|
|
79
|
+
self.detail = detail
|
|
80
|
+
self.extra = extra or {}
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def body(self) -> dict[str, Any]:
|
|
84
|
+
return {"code": self.code, "detail": self.detail, **self.extra}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── Region capture ──────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def reduce_region_roots(paths: set[str]) -> list[str]:
|
|
91
|
+
"""Drop any path that already sits under another in the set.
|
|
92
|
+
|
|
93
|
+
``{"1", "1.2"}`` reduces to ``{"1"}`` — capturing 1's subtree already captures
|
|
94
|
+
1.2's. ``""`` is the project root and absorbs everything.
|
|
95
|
+
"""
|
|
96
|
+
cleaned = {p for p in paths if p is not None}
|
|
97
|
+
if "" in cleaned:
|
|
98
|
+
return [""]
|
|
99
|
+
out: list[str] = []
|
|
100
|
+
for path in sorted(cleaned):
|
|
101
|
+
if any(path == kept or path.startswith(f"{kept}.") for kept in out):
|
|
102
|
+
continue
|
|
103
|
+
out.append(path)
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def capture_shape(
|
|
108
|
+
project_id: Any, region_roots: list[str], *, limit: int | None = None
|
|
109
|
+
) -> dict[str, str]:
|
|
110
|
+
"""Read every live row at-or-under ``region_roots`` as ``{task id: wbs_path}``.
|
|
111
|
+
|
|
112
|
+
The region is the whole subtree of each root, not just the rows a gesture moved.
|
|
113
|
+
That is deliberate: a row someone else *adds* into an affected level is invisible to
|
|
114
|
+
a per-row check, and it is exactly what produces an orphan when the undo puts the
|
|
115
|
+
surrounding paths back (a row whose parent path no longer exists resolves
|
|
116
|
+
``parent_id = NULL`` and silently renders at the project root).
|
|
117
|
+
|
|
118
|
+
``limit`` reads at most ``limit + 1`` rows, so the caller can detect an oversized
|
|
119
|
+
region without materializing it. A root-level gesture reduces to ``[""]``, which is
|
|
120
|
+
the whole project — on a large plan that is tens of thousands of rows pulled into a
|
|
121
|
+
dict inside the hot write transaction, twice, only to be discarded when the size
|
|
122
|
+
cap rejects them. The extra row is what makes "more than the cap" distinguishable
|
|
123
|
+
from "exactly the cap".
|
|
124
|
+
"""
|
|
125
|
+
from trueppm_api.apps.projects.models import Task
|
|
126
|
+
|
|
127
|
+
if not region_roots:
|
|
128
|
+
return {}
|
|
129
|
+
rows = Task.objects.filter(project_id=project_id, is_deleted=False).exclude(wbs_path=None)
|
|
130
|
+
if region_roots != [""]:
|
|
131
|
+
clause = Q()
|
|
132
|
+
for root in region_roots:
|
|
133
|
+
clause |= Q(wbs_path=root) | Q(wbs_path__startswith=f"{root}.")
|
|
134
|
+
rows = rows.filter(clause)
|
|
135
|
+
pairs = rows.values_list("pk", "wbs_path")
|
|
136
|
+
if limit is not None:
|
|
137
|
+
pairs = pairs.order_by("pk")[: limit + 1]
|
|
138
|
+
return {str(pk): str(path) for pk, path in pairs}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class StructuralCapture:
|
|
142
|
+
"""Brackets a structural write so the ledger row can be built from both sides.
|
|
143
|
+
|
|
144
|
+
Usage inside the endpoint's existing ``transaction.atomic()``::
|
|
145
|
+
|
|
146
|
+
capture = StructuralCapture.begin(project.pk, {parent_path})
|
|
147
|
+
... # perform the restructure
|
|
148
|
+
capture.record(project=project, applied_by=request.user, kind=..., anchors=[task.pk])
|
|
149
|
+
|
|
150
|
+
``begin`` must run *before* the write and ``record`` *after* it, both inside the same
|
|
151
|
+
transaction, so ``shape_after`` is exactly what the write produced — the baseline the
|
|
152
|
+
undo precondition compares against later.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
project_id: Any,
|
|
158
|
+
region_roots: list[str],
|
|
159
|
+
shape_before: dict[str, str],
|
|
160
|
+
*,
|
|
161
|
+
oversized: bool = False,
|
|
162
|
+
):
|
|
163
|
+
self.project_id = project_id
|
|
164
|
+
self.region_roots = region_roots
|
|
165
|
+
self.shape_before = shape_before
|
|
166
|
+
self.oversized = oversized
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def begin(cls, project_id: Any, region_roots: set[str]) -> StructuralCapture:
|
|
170
|
+
roots = reduce_region_roots(region_roots)
|
|
171
|
+
# Bounded read. The cap has to be applied HERE, not after both reads: measuring
|
|
172
|
+
# the region by materializing it means an oversized act pays the full cost
|
|
173
|
+
# anyway and then throws the result away, which is the opposite of what a cap
|
|
174
|
+
# is for.
|
|
175
|
+
shape = capture_shape(project_id, roots, limit=STRUCTURAL_OPERATION_MAX_REGION)
|
|
176
|
+
if len(shape) > STRUCTURAL_OPERATION_MAX_REGION:
|
|
177
|
+
return cls(project_id, roots, {}, oversized=True)
|
|
178
|
+
return cls(project_id, roots, shape)
|
|
179
|
+
|
|
180
|
+
def record(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
project: Project,
|
|
184
|
+
applied_by: Any,
|
|
185
|
+
kind: str,
|
|
186
|
+
anchors: list[uuid.UUID] | None = None,
|
|
187
|
+
created_task_ids: list[uuid.UUID] | None = None,
|
|
188
|
+
deleted_task_ids: list[uuid.UUID] | None = None,
|
|
189
|
+
removed_dependency_ids: list[uuid.UUID] | None = None,
|
|
190
|
+
) -> StructuralOperation:
|
|
191
|
+
"""Write the ledger row. Returns it so the endpoint can echo ``operation_id``."""
|
|
192
|
+
from trueppm_api.apps.projects.models import StructuralOperation, Task
|
|
193
|
+
|
|
194
|
+
created = list(created_task_ids or [])
|
|
195
|
+
deleted = list(deleted_task_ids or [])
|
|
196
|
+
# A minted row is not in shape_before and a soft-deleted one drops out of
|
|
197
|
+
# shape_after, so neither is covered by the shape comparison. They are recorded
|
|
198
|
+
# and precondition-checked on their own terms (ADR-0880 §2).
|
|
199
|
+
#
|
|
200
|
+
# An oversized region skips the second read entirely — there is nothing to
|
|
201
|
+
# compare it against and nothing will be persisted.
|
|
202
|
+
shape_after = (
|
|
203
|
+
{}
|
|
204
|
+
if self.oversized
|
|
205
|
+
else capture_shape(
|
|
206
|
+
self.project_id, self.region_roots, limit=STRUCTURAL_OPERATION_MAX_REGION
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
region_size = len(set(self.shape_before) | set(shape_after)) + len(created) + len(deleted)
|
|
210
|
+
undoable = not self.oversized and region_size <= STRUCTURAL_OPERATION_MAX_REGION
|
|
211
|
+
|
|
212
|
+
created_versions: dict[str, int] = {}
|
|
213
|
+
if undoable and created:
|
|
214
|
+
created_versions = {
|
|
215
|
+
str(pk): version
|
|
216
|
+
for pk, version in Task.objects.filter(
|
|
217
|
+
pk__in=created, project_id=self.project_id
|
|
218
|
+
).values_list("pk", "server_version")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return StructuralOperation.objects.create(
|
|
222
|
+
project=project,
|
|
223
|
+
applied_by=applied_by if getattr(applied_by, "is_authenticated", False) else None,
|
|
224
|
+
kind=kind,
|
|
225
|
+
undoable=undoable,
|
|
226
|
+
region_roots=self.region_roots,
|
|
227
|
+
# An oversized region is not worth persisting — the row exists to tell the
|
|
228
|
+
# trail the act happened and cannot be reversed, not to carry a payload
|
|
229
|
+
# nothing will read.
|
|
230
|
+
shape_before=self.shape_before if undoable else {},
|
|
231
|
+
shape_after=shape_after if undoable else {},
|
|
232
|
+
anchor_task_ids=list(anchors or []),
|
|
233
|
+
created_task_ids=created,
|
|
234
|
+
deleted_task_ids=deleted,
|
|
235
|
+
created_task_versions=created_versions,
|
|
236
|
+
removed_dependency_ids=list(removed_dependency_ids or []),
|
|
237
|
+
result_summary={
|
|
238
|
+
"region_size": region_size,
|
|
239
|
+
"moved": sum(
|
|
240
|
+
1 for k, v in shape_after.items() if self.shape_before.get(k) not in (None, v)
|
|
241
|
+
),
|
|
242
|
+
**({} if undoable else {"not_undoable": "region_too_large"}),
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ── Undoability, as a server fact ───────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _newest_active_id(operation: StructuralOperation) -> uuid.UUID | None:
|
|
251
|
+
"""The id of the operation at the top of this actor's stack in this project.
|
|
252
|
+
|
|
253
|
+
Scoped per actor so ⌘Z means "undo *your* last structural act" — a collaborator's
|
|
254
|
+
unrelated act in a disjoint region must not block you (ADR-0880 §8).
|
|
255
|
+
"""
|
|
256
|
+
from trueppm_api.apps.projects.models import StructuralOperation, SyncBatchOperationStatus
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
StructuralOperation.objects.filter(
|
|
260
|
+
project_id=operation.project_id,
|
|
261
|
+
applied_by_id=operation.applied_by_id,
|
|
262
|
+
status=SyncBatchOperationStatus.ACTIVE,
|
|
263
|
+
)
|
|
264
|
+
.order_by("-created_at")
|
|
265
|
+
.values_list("pk", flat=True)
|
|
266
|
+
.first()
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def undo_blocked_reason(operation: StructuralOperation, *, check_shape: bool = True) -> str:
|
|
271
|
+
"""Why this operation cannot be undone right now, or ``""`` if it can.
|
|
272
|
+
|
|
273
|
+
``status`` cannot answer this on its own — its own docstring says the only question
|
|
274
|
+
it answers is whether the row has *already* been undone, while undoability here also
|
|
275
|
+
varies with live shape, stack position and region size. Computing it server-side is
|
|
276
|
+
what stops the web trail and an API client from disagreeing (ADR-0880 §8a).
|
|
277
|
+
|
|
278
|
+
Deliberately excludes the role check: authority depends on the *caller*, and this is
|
|
279
|
+
a property of the operation. The view adds ``forbidden`` for the caller it has.
|
|
280
|
+
|
|
281
|
+
``check_shape=False`` skips the drift comparison — the one arm that costs a
|
|
282
|
+
full-region read. The list endpoint passes it because the answer is racy anyway
|
|
283
|
+
(it can flip between a client reading the list and pressing Undo) and the ``409``
|
|
284
|
+
on the write is authoritative regardless. The detail endpoint and the undo itself
|
|
285
|
+
ask the full question.
|
|
286
|
+
"""
|
|
287
|
+
from trueppm_api.apps.projects.models import SyncBatchOperationStatus
|
|
288
|
+
|
|
289
|
+
if operation.status == SyncBatchOperationStatus.UNDONE:
|
|
290
|
+
return StructuralUndoBlockedReason.ALREADY_UNDONE.value
|
|
291
|
+
if not operation.undoable:
|
|
292
|
+
return StructuralUndoBlockedReason.TOO_LARGE.value
|
|
293
|
+
if _newest_active_id(operation) != operation.pk:
|
|
294
|
+
return StructuralUndoBlockedReason.NOT_TOP_OF_STACK.value
|
|
295
|
+
if check_shape and _describe_shape_drift(operation):
|
|
296
|
+
return StructuralUndoBlockedReason.SHAPE_CHANGED.value
|
|
297
|
+
return StructuralUndoBlockedReason.NONE.value
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _shape_drift(operation: StructuralOperation) -> list[dict[str, Any]]:
|
|
301
|
+
"""Rows that moved, vanished, or appeared inside the affected region."""
|
|
302
|
+
drift: list[dict[str, Any]] = []
|
|
303
|
+
recorded: dict[str, str] = operation.shape_after or {}
|
|
304
|
+
live = capture_shape(operation.project_id, operation.region_roots)
|
|
305
|
+
for task_id, path in recorded.items():
|
|
306
|
+
actual = live.get(task_id)
|
|
307
|
+
if actual is None:
|
|
308
|
+
drift.append({"task_id": task_id, "expected_path": path, "change": "removed"})
|
|
309
|
+
elif actual != path:
|
|
310
|
+
drift.append(
|
|
311
|
+
{
|
|
312
|
+
"task_id": task_id,
|
|
313
|
+
"expected_path": path,
|
|
314
|
+
"actual_path": actual,
|
|
315
|
+
"change": "moved",
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
drift.extend(
|
|
319
|
+
{"task_id": task_id, "actual_path": path, "change": "added"}
|
|
320
|
+
for task_id, path in live.items()
|
|
321
|
+
if task_id not in recorded
|
|
322
|
+
)
|
|
323
|
+
return drift
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _tombstone_drift(operation: StructuralOperation) -> list[dict[str, Any]]:
|
|
327
|
+
"""Rows this act soft-deleted that somebody has since restored by another route."""
|
|
328
|
+
from trueppm_api.apps.projects.models import Task
|
|
329
|
+
|
|
330
|
+
if not operation.deleted_task_ids:
|
|
331
|
+
return []
|
|
332
|
+
still_deleted = set(
|
|
333
|
+
Task.objects.filter(
|
|
334
|
+
pk__in=operation.deleted_task_ids,
|
|
335
|
+
project_id=operation.project_id,
|
|
336
|
+
is_deleted=True,
|
|
337
|
+
).values_list("pk", flat=True)
|
|
338
|
+
)
|
|
339
|
+
return [
|
|
340
|
+
{"task_id": str(task_id), "change": "restored_elsewhere"}
|
|
341
|
+
for task_id in operation.deleted_task_ids
|
|
342
|
+
if task_id not in still_deleted
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _minted_row_drift(operation: StructuralOperation) -> list[dict[str, Any]]:
|
|
347
|
+
"""Rows this act minted that have since been edited or deleted.
|
|
348
|
+
|
|
349
|
+
Undo soft-deletes these, so an edit somebody made to a grouped phase's name — the
|
|
350
|
+
obvious thing to do right after grouping — must block the undo rather than silently
|
|
351
|
+
take the rename with it.
|
|
352
|
+
"""
|
|
353
|
+
from trueppm_api.apps.projects.models import Task
|
|
354
|
+
|
|
355
|
+
if not operation.created_task_ids:
|
|
356
|
+
return []
|
|
357
|
+
current = dict(
|
|
358
|
+
Task.objects.filter(
|
|
359
|
+
pk__in=operation.created_task_ids,
|
|
360
|
+
project_id=operation.project_id,
|
|
361
|
+
is_deleted=False,
|
|
362
|
+
).values_list("pk", "server_version")
|
|
363
|
+
)
|
|
364
|
+
drift: list[dict[str, Any]] = []
|
|
365
|
+
for task_id, recorded_version in (operation.created_task_versions or {}).items():
|
|
366
|
+
actual_version = current.get(uuid.UUID(task_id))
|
|
367
|
+
if actual_version is None:
|
|
368
|
+
drift.append({"task_id": task_id, "change": "deleted"})
|
|
369
|
+
elif actual_version != recorded_version:
|
|
370
|
+
drift.append({"task_id": task_id, "change": "edited"})
|
|
371
|
+
return drift
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _edge_drift(operation: StructuralOperation) -> list[dict[str, Any]]:
|
|
375
|
+
"""Edges this act removed that somebody has since restored by another route."""
|
|
376
|
+
from trueppm_api.apps.projects.models import Dependency
|
|
377
|
+
|
|
378
|
+
if not operation.removed_dependency_ids:
|
|
379
|
+
return []
|
|
380
|
+
still_removed = set(
|
|
381
|
+
Dependency.objects.filter(
|
|
382
|
+
pk__in=operation.removed_dependency_ids, is_deleted=True
|
|
383
|
+
).values_list("pk", flat=True)
|
|
384
|
+
)
|
|
385
|
+
return [
|
|
386
|
+
{"dependency_id": str(dep_id), "change": "restored_elsewhere"}
|
|
387
|
+
for dep_id in operation.removed_dependency_ids
|
|
388
|
+
if dep_id not in still_removed
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _describe_shape_drift(operation: StructuralOperation) -> list[dict[str, Any]]:
|
|
393
|
+
"""Every way the current state differs from what the operation left behind.
|
|
394
|
+
|
|
395
|
+
Covers all four payload dimensions, not just the shape. An earlier draft checked
|
|
396
|
+
``shape_after`` alone and used that guarantee to justify relaxing the role bar — but a
|
|
397
|
+
soft-deleted row is absent from both ``shape_after`` and the live shape, so the
|
|
398
|
+
comparison matches *whatever happened to it*, and undo would have resurrected a
|
|
399
|
+
container and its CPM edges into a region collaborators had since worked in.
|
|
400
|
+
"""
|
|
401
|
+
return [
|
|
402
|
+
*_shape_drift(operation),
|
|
403
|
+
*_tombstone_drift(operation),
|
|
404
|
+
*_minted_row_drift(operation),
|
|
405
|
+
*_edge_drift(operation),
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ── Undo ────────────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _raise_if_undo_blocked(locked: Any) -> None:
|
|
413
|
+
"""Refuse the undo when the recorded act can no longer be reversed safely.
|
|
414
|
+
|
|
415
|
+
Kept together because the three reasons are one decision with three outcomes,
|
|
416
|
+
and each carries operator-facing copy that has to stay beside its condition.
|
|
417
|
+
"""
|
|
418
|
+
reason = undo_blocked_reason(locked)
|
|
419
|
+
if reason == StructuralUndoBlockedReason.TOO_LARGE:
|
|
420
|
+
raise StructuralUndoRejected(
|
|
421
|
+
StructuralUndoBlockedReason.TOO_LARGE.value,
|
|
422
|
+
"This change affected too many rows to be reversed automatically.",
|
|
423
|
+
)
|
|
424
|
+
if reason == StructuralUndoBlockedReason.NOT_TOP_OF_STACK:
|
|
425
|
+
raise StructuralUndoRejected(
|
|
426
|
+
StructuralUndoBlockedReason.NOT_TOP_OF_STACK.value,
|
|
427
|
+
"Undo the more recent change first.",
|
|
428
|
+
extra={"blocking_operation_id": str(_newest_active_id(locked))},
|
|
429
|
+
)
|
|
430
|
+
if reason == StructuralUndoBlockedReason.SHAPE_CHANGED:
|
|
431
|
+
raise StructuralUndoRejected(
|
|
432
|
+
StructuralUndoBlockedReason.SHAPE_CHANGED.value,
|
|
433
|
+
"The outline has changed here since — this can no longer be undone.",
|
|
434
|
+
extra={"changed": _describe_shape_drift(locked)},
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _restore_deleted_rows(locked: Any, task_model: Any) -> int:
|
|
439
|
+
"""Undo step 1 — restore soft-deleted rows: exact ids, project-scoped, no cascade.
|
|
440
|
+
|
|
441
|
+
This is what returns an ungrouped wrapper with its original id, name, notes and
|
|
442
|
+
labels: a soft delete retains them, so nothing needed snapshotting.
|
|
443
|
+
|
|
444
|
+
``on_conflict="repath"`` for the same reason step 2 skips a colliding edge rather
|
|
445
|
+
than failing: an undo that abandons part-way leaves the caller with neither state
|
|
446
|
+
(#3071). ``unique_task_wbs_path_per_project_live`` covers live rows only, so a row
|
|
447
|
+
soft-deleted by this act had its number freed the instant the act ran, and anything
|
|
448
|
+
created since may be sitting on it. The constraint is DEFERRED, so an unguarded
|
|
449
|
+
restore would raise at COMMIT with a constraint name and no row attribution. Step 3
|
|
450
|
+
re-asserts ``shape_before`` immediately after and will move most of these back where
|
|
451
|
+
they belong; re-pathing here only has to make the row legal enough to reach it.
|
|
452
|
+
|
|
453
|
+
Caller holds ``transaction.atomic()`` — ``select_for_update()`` below requires it.
|
|
454
|
+
"""
|
|
455
|
+
restored = 0
|
|
456
|
+
for row in task_model.objects.select_for_update().filter(
|
|
457
|
+
pk__in=locked.deleted_task_ids, project_id=locked.project_id, is_deleted=True
|
|
458
|
+
):
|
|
459
|
+
row.restore(on_conflict="repath")
|
|
460
|
+
restored += 1
|
|
461
|
+
return restored
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _restore_dependencies(locked: Any, task_model: Any, dependency_model: Any) -> tuple[int, int]:
|
|
465
|
+
"""Undo step 2 — restore edges, returning ``(restored, skipped)``.
|
|
466
|
+
|
|
467
|
+
Only where both endpoints are live and the row would not collide on
|
|
468
|
+
``unique_dependency``. ``capture_graph_state`` builds its edge list from tasks
|
|
469
|
+
filtered ``is_deleted=False``, so an edge pointing at a tombstone is not in the graph
|
|
470
|
+
the feasibility guard validates — it structurally cannot catch what this step writes.
|
|
471
|
+
Guarding here is the primary control.
|
|
472
|
+
|
|
473
|
+
Caller holds ``transaction.atomic()`` — ``select_for_update()`` below requires it.
|
|
474
|
+
"""
|
|
475
|
+
restored = 0
|
|
476
|
+
skipped = 0
|
|
477
|
+
for dep in dependency_model.objects.select_for_update().filter(
|
|
478
|
+
pk__in=locked.removed_dependency_ids,
|
|
479
|
+
is_deleted=True,
|
|
480
|
+
predecessor__project_id=locked.project_id,
|
|
481
|
+
):
|
|
482
|
+
endpoints_live = (
|
|
483
|
+
task_model.objects.filter(
|
|
484
|
+
pk__in=[dep.predecessor_id, dep.successor_id],
|
|
485
|
+
project_id=locked.project_id,
|
|
486
|
+
is_deleted=False,
|
|
487
|
+
).count()
|
|
488
|
+
== 2
|
|
489
|
+
)
|
|
490
|
+
collides = (
|
|
491
|
+
dependency_model.objects.filter(
|
|
492
|
+
predecessor_id=dep.predecessor_id,
|
|
493
|
+
successor_id=dep.successor_id,
|
|
494
|
+
dep_type=dep.dep_type,
|
|
495
|
+
is_deleted=False,
|
|
496
|
+
)
|
|
497
|
+
.exclude(pk=dep.pk)
|
|
498
|
+
.exists()
|
|
499
|
+
)
|
|
500
|
+
if not endpoints_live or collides:
|
|
501
|
+
skipped += 1
|
|
502
|
+
continue
|
|
503
|
+
dep.restore()
|
|
504
|
+
restored += 1
|
|
505
|
+
return restored, skipped
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _restore_shape(locked: Any, task_model: Any) -> int:
|
|
509
|
+
"""Undo step 3 — put the outline shape back, returning the number of rows moved.
|
|
510
|
+
|
|
511
|
+
Caller holds ``transaction.atomic()`` — ``select_for_update()`` below requires it.
|
|
512
|
+
"""
|
|
513
|
+
restored = 0
|
|
514
|
+
rows_by_id = {
|
|
515
|
+
str(row.pk): row
|
|
516
|
+
for row in task_model.objects.select_for_update().filter(
|
|
517
|
+
pk__in=[uuid.UUID(k) for k in (locked.shape_before or {})],
|
|
518
|
+
project_id=locked.project_id,
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
for task_id, path in (locked.shape_before or {}).items():
|
|
522
|
+
target = rows_by_id.get(task_id)
|
|
523
|
+
if target is None or str(target.wbs_path) == path:
|
|
524
|
+
continue
|
|
525
|
+
target.wbs_path = path
|
|
526
|
+
target.save(update_fields=["wbs_path"])
|
|
527
|
+
restored += 1
|
|
528
|
+
return restored
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _unmint_created_rows(locked: Any, task_model: Any) -> int:
|
|
532
|
+
"""Undo step 4 — un-mint what the act minted.
|
|
533
|
+
|
|
534
|
+
Caller holds ``transaction.atomic()`` — ``select_for_update()`` below requires it.
|
|
535
|
+
"""
|
|
536
|
+
removed = 0
|
|
537
|
+
for row in task_model.objects.select_for_update().filter(
|
|
538
|
+
pk__in=locked.created_task_ids, project_id=locked.project_id, is_deleted=False
|
|
539
|
+
):
|
|
540
|
+
row.soft_delete()
|
|
541
|
+
removed += 1
|
|
542
|
+
return removed
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def undo_structural_operation(
|
|
546
|
+
operation: StructuralOperation,
|
|
547
|
+
*,
|
|
548
|
+
undone_by: Any = None,
|
|
549
|
+
authorize: Any = None,
|
|
550
|
+
) -> dict[str, int]:
|
|
551
|
+
"""Reverse one structural act. All-or-nothing, idempotent, kind-agnostic.
|
|
552
|
+
|
|
553
|
+
``authorize`` is called with the list of ``Task`` rows the forward act itself gated
|
|
554
|
+
on plus, separately, the rows this undo will delete or restore — the view supplies it
|
|
555
|
+
so the permission vocabulary stays in the view layer. It must raise to refuse.
|
|
556
|
+
|
|
557
|
+
Returns the undo summary. A second call on an already-undone row returns the recorded
|
|
558
|
+
summary unchanged rather than raising: a double ⌘Z is a no-op, not an error, matching
|
|
559
|
+
``undo_paste_many_operation``.
|
|
560
|
+
"""
|
|
561
|
+
from trueppm_api.apps.access.permissions import assert_project_not_archived
|
|
562
|
+
from trueppm_api.apps.projects.models import (
|
|
563
|
+
Dependency,
|
|
564
|
+
StructuralOperation,
|
|
565
|
+
SyncBatchOperationStatus,
|
|
566
|
+
Task,
|
|
567
|
+
)
|
|
568
|
+
from trueppm_api.apps.projects.task_grouping import assert_graph_feasible, capture_graph_state
|
|
569
|
+
|
|
570
|
+
# `StructuralOperationViewSet` already carries `IsProjectNotArchived`, so this is
|
|
571
|
+
# not closing an open hole — it makes the floor uniform across the family after
|
|
572
|
+
# #3354 found three of its five undo paths missing it at the view layer. The
|
|
573
|
+
# distinction that justifies putting it here and NOT the role check the
|
|
574
|
+
# `authorize` callback carries: archived is lifecycle state, a property of the
|
|
575
|
+
# plan, so it holds for every caller; authority is a property of the requester
|
|
576
|
+
# and stays in the view's vocabulary (see this function's docstring).
|
|
577
|
+
assert_project_not_archived(operation.project_id)
|
|
578
|
+
|
|
579
|
+
with transaction.atomic():
|
|
580
|
+
locked = StructuralOperation.objects.select_for_update().get(pk=operation.pk)
|
|
581
|
+
if locked.status == SyncBatchOperationStatus.UNDONE:
|
|
582
|
+
return dict(locked.result_summary.get("undo", {}))
|
|
583
|
+
|
|
584
|
+
_raise_if_undo_blocked(locked)
|
|
585
|
+
|
|
586
|
+
# `graph_before` must be read before step 1 writes any edge: undo restores
|
|
587
|
+
# dependencies, so unlike group/ungroup the differential guard below is live on
|
|
588
|
+
# this path and needs a true pre-undo baseline (ADR-0880 §7a).
|
|
589
|
+
graph_before = capture_graph_state(locked.project_id)
|
|
590
|
+
|
|
591
|
+
if authorize is not None:
|
|
592
|
+
authorize(
|
|
593
|
+
move_rows=list(
|
|
594
|
+
Task.objects.filter(
|
|
595
|
+
pk__in=locked.anchor_task_ids,
|
|
596
|
+
project_id=locked.project_id,
|
|
597
|
+
is_deleted=False,
|
|
598
|
+
)
|
|
599
|
+
),
|
|
600
|
+
# Only rows the act SOFT-DELETED are gated at delete grade. Rows it
|
|
601
|
+
# MINTED are not: `perform_group` creates its container with no
|
|
602
|
+
# assignee and demands no delete authority to make it, so demanding
|
|
603
|
+
# delete authority to un-make it denies a Member the reversal of
|
|
604
|
+
# their own group — the exact asymmetry this design exists to
|
|
605
|
+
# remove, reproduced one layer down. Un-minting is gated by the
|
|
606
|
+
# authority the mint required, which is the anchors above.
|
|
607
|
+
restore_rows=list(
|
|
608
|
+
Task.objects.filter(
|
|
609
|
+
pk__in=locked.deleted_task_ids,
|
|
610
|
+
project_id=locked.project_id,
|
|
611
|
+
)
|
|
612
|
+
),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Steps 1-4 each run inside this transaction; see the helpers for why each
|
|
616
|
+
# guards the way it does.
|
|
617
|
+
deleted_restored = _restore_deleted_rows(locked, Task)
|
|
618
|
+
|
|
619
|
+
deps_restored, deps_skipped = _restore_dependencies(locked, Task, Dependency)
|
|
620
|
+
|
|
621
|
+
restored = _restore_shape(locked, Task)
|
|
622
|
+
|
|
623
|
+
created_removed = _unmint_created_rows(locked, Task)
|
|
624
|
+
|
|
625
|
+
# A row that gained or lost children needs its shadow values re-parked. Every
|
|
626
|
+
# distinct parent path on either side of the restore is a candidate; reading
|
|
627
|
+
# from current state (not replaying the act) means this converges even on the
|
|
628
|
+
# paths where the forward endpoints skip it.
|
|
629
|
+
_resync_shadow_values(locked)
|
|
630
|
+
|
|
631
|
+
assert_graph_feasible(locked.project_id, graph_before)
|
|
632
|
+
|
|
633
|
+
summary = {
|
|
634
|
+
"restored": restored,
|
|
635
|
+
"created_removed": created_removed,
|
|
636
|
+
"deleted_restored": deleted_restored,
|
|
637
|
+
"dependencies_restored": deps_restored,
|
|
638
|
+
"dependencies_skipped": deps_skipped,
|
|
639
|
+
}
|
|
640
|
+
locked.status = SyncBatchOperationStatus.UNDONE
|
|
641
|
+
locked.undone_at = timezone.now()
|
|
642
|
+
locked.undone_by = undone_by if getattr(undone_by, "is_authenticated", False) else None
|
|
643
|
+
locked.result_summary = {**locked.result_summary, "undo": summary}
|
|
644
|
+
locked.save(
|
|
645
|
+
update_fields=["status", "undone_at", "undone_by", "result_summary"],
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# Post-commit hooks mirroring the forward acts' own, so clients need no change.
|
|
649
|
+
# The six structural endpoints all emit ``tasks_restructured`` with an empty
|
|
650
|
+
# payload and enqueue a recalculation; an undo is the same kind of mutation and
|
|
651
|
+
# emits the same event.
|
|
652
|
+
broadcast_tasks_restructured_and_recalc(locked.project_id)
|
|
653
|
+
return summary
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _resync_shadow_values(operation: StructuralOperation) -> None:
|
|
657
|
+
"""Re-park own_status/own_estimate and structure_role on every affected parent.
|
|
658
|
+
|
|
659
|
+
Persists through :func:`resync_container_declarations`. The earlier version called
|
|
660
|
+
``sync_structure_shadow_values`` and stopped there — which mutates the instance and
|
|
661
|
+
returns whether a write is needed, so the whole pass converged in memory and was
|
|
662
|
+
discarded at the end of the loop (#3010). Undo's own tests could not see it: they
|
|
663
|
+
assert the restored tree shape, and the shape was always right.
|
|
664
|
+
"""
|
|
665
|
+
from trueppm_api.apps.projects.models import Task, resync_container_declarations
|
|
666
|
+
|
|
667
|
+
parent_paths = {
|
|
668
|
+
path.rsplit(".", 1)[0]
|
|
669
|
+
for shape in (operation.shape_before or {}, operation.shape_after or {})
|
|
670
|
+
for path in shape.values()
|
|
671
|
+
if "." in path
|
|
672
|
+
}
|
|
673
|
+
if not parent_paths:
|
|
674
|
+
return
|
|
675
|
+
# Lock and read all N parents in ONE statement, then tell the helper not to
|
|
676
|
+
# re-read them. Letting it re-SELECT each row it was just handed made this pass
|
|
677
|
+
# O(N) round trips, and `STRUCTURAL_OPERATION_MAX_REGION` bounds N at 2000.
|
|
678
|
+
# Ordered by wbs_path so concurrent undos in the same region queue rather than
|
|
679
|
+
# deadlock.
|
|
680
|
+
resync_container_declarations(
|
|
681
|
+
*Task.objects.select_for_update()
|
|
682
|
+
.filter(
|
|
683
|
+
project_id=operation.project_id, is_deleted=False, wbs_path__in=sorted(parent_paths)
|
|
684
|
+
)
|
|
685
|
+
.order_by("wbs_path"),
|
|
686
|
+
already_locked=True,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def may_undo(request: Request, operation: StructuralOperation) -> bool:
|
|
691
|
+
"""Whether this caller clears the actor-or-Admin bar. Never raises.
|
|
692
|
+
|
|
693
|
+
Exists so the serializer can report ``forbidden`` rather than advertising an
|
|
694
|
+
enabled Undo on a row that will 403 — the serializer's whole reason for computing
|
|
695
|
+
undoability server-side is that the web trail and an API client must not each
|
|
696
|
+
guess separately, and authority is part of the answer.
|
|
697
|
+
"""
|
|
698
|
+
try:
|
|
699
|
+
require_structural_undo_authority(request, operation)
|
|
700
|
+
except Exception:
|
|
701
|
+
return False
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def require_structural_undo_authority(request: Request, operation: StructuralOperation) -> None:
|
|
706
|
+
"""Gate an undo: your own act at the act's own authority, anyone else's at Admin+.
|
|
707
|
+
|
|
708
|
+
``_require_admin`` (``batch_operation_views``) demands Admin for every batch undo,
|
|
709
|
+
reasoning that undo "removes work other collaborators may already be building on top
|
|
710
|
+
of". That premise does not hold here: the all-or-nothing precondition refuses outright
|
|
711
|
+
if anything the undo would write has moved, so the hazard the Admin bar exists for is
|
|
712
|
+
unreachable. Importing it would re-impose ADR-0773's asymmetry — a Member may indent
|
|
713
|
+
and could not un-indent — with no safety left to justify it. This follows
|
|
714
|
+
``IsProjectAdminOrSessionOwner``'s shape instead: Admin **or** the actor.
|
|
715
|
+
"""
|
|
716
|
+
from rest_framework.exceptions import PermissionDenied
|
|
717
|
+
|
|
718
|
+
from trueppm_api.apps.access.models import Role
|
|
719
|
+
from trueppm_api.apps.access.permissions import _membership_role
|
|
720
|
+
|
|
721
|
+
if operation.applied_by_id == getattr(request.user, "pk", None):
|
|
722
|
+
return
|
|
723
|
+
role = _membership_role(request, str(operation.project_id))
|
|
724
|
+
if role is None or role < Role.ADMIN:
|
|
725
|
+
raise PermissionDenied(
|
|
726
|
+
"You need at least Project Manager role to undo someone else's change."
|
|
727
|
+
)
|