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,561 @@
|
|
|
1
|
+
"""Subtree classification cascade for ``PATCH /projects/{pk}/tasks/classification/``.
|
|
2
|
+
|
|
3
|
+
ADR-0790, #2735. Applies the two orthogonal hybrid axes — ``governance_class`` and
|
|
4
|
+
``delivery_mode`` — across a task subtree in one call.
|
|
5
|
+
|
|
6
|
+
The two axes are **not** one choice, and the difference is load-bearing rather than
|
|
7
|
+
cosmetic. ``governance_class`` selects which overlay governs the subtree and carries an
|
|
8
|
+
inherit bit (``parent_governance_inherited``); ``delivery_mode`` selects how work is
|
|
9
|
+
executed, estimated and rolled up, and carries none. ``scrum`` and ``kanban`` are not
|
|
10
|
+
interchangeable either — the rollup engine reads point burndown for one and item
|
|
11
|
+
throughput for the other, and the agile-aware Monte Carlo samples the team velocity
|
|
12
|
+
distribution only for ``scrum``. Collapsing the axes into a single toggle mis-tags every
|
|
13
|
+
Kanban team.
|
|
14
|
+
|
|
15
|
+
Three invariants this module exists to hold:
|
|
16
|
+
|
|
17
|
+
- **Milestones survive a cascade.** ``is_milestone ⟺ delivery_mode='milestone' ⟺
|
|
18
|
+
duration=0`` is normalized on every write path (``TaskSerializer.
|
|
19
|
+
_reconcile_milestone_signals``). Writing an agile delivery mode across a phase would
|
|
20
|
+
silently dissolve every gate inside it, taking the rollup SQL — which keys on
|
|
21
|
+
``delivery_mode`` — with it. That is never done, under any request flag; see
|
|
22
|
+
:func:`_classify_row`.
|
|
23
|
+
- **Explicit governance survives a cascade.** ``preserve_governance_overrides``
|
|
24
|
+
defaults true. That default is what makes cascading safe to try: a planner who has
|
|
25
|
+
hand-tuned one compliance branch inside an otherwise agile phase can re-declare the
|
|
26
|
+
phase without losing the branch.
|
|
27
|
+
- **The write reaches offline clients.** Every touched row goes through
|
|
28
|
+
``Task.save()``, never ``QuerySet.update()`` — ``server_version`` and ``sync_seq``
|
|
29
|
+
advance only inside ``VersionedModel.save()`` (ADR-0686), and a bulk update would
|
|
30
|
+
leave the classification permanently invisible to the delta pull (the #2491 class).
|
|
31
|
+
|
|
32
|
+
This is a service function rather than view code so an MCP, management-command, or
|
|
33
|
+
agent caller reaches identical semantics without going through DRF — the "human and
|
|
34
|
+
agent principals are governed identically" claim holds on this path by construction.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import dataclasses
|
|
40
|
+
import re
|
|
41
|
+
import uuid
|
|
42
|
+
from typing import TYPE_CHECKING, Any
|
|
43
|
+
|
|
44
|
+
from django.db.models import Q
|
|
45
|
+
|
|
46
|
+
from trueppm_api.apps.access.permissions import can_user_edit_task
|
|
47
|
+
from trueppm_api.apps.projects.refusal_codes import BulkRefusalCode
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from rest_framework.request import Request
|
|
51
|
+
|
|
52
|
+
from trueppm_api.apps.projects.models import Project, Task
|
|
53
|
+
|
|
54
|
+
#: A milestone row was left unclassified on at least one axis. Reported as a *skip*,
|
|
55
|
+
#: never a rejection: a classification crossing a gate is a documented no-op, so one
|
|
56
|
+
#: milestone inside a phase cannot fail the rows around it.
|
|
57
|
+
#:
|
|
58
|
+
#: Bound to the shared enum rather than mirroring ``task_bulk``'s constant by hand
|
|
59
|
+
#: (#3037). The two batch write paths are meant to speak one vocabulary, and the old
|
|
60
|
+
#: comment said so — but a second string literal asserting sameness is exactly what
|
|
61
|
+
#: drifts, and nothing would have failed if it had.
|
|
62
|
+
CODE_MILESTONE_GATE = BulkRefusalCode.MILESTONE_GATE.value
|
|
63
|
+
|
|
64
|
+
#: Maximum rows one cascade may resolve.
|
|
65
|
+
#:
|
|
66
|
+
#: Deliberately **not** shared with ``task_bulk.TASK_BULK_MAX_OPERATIONS`` (500): that
|
|
67
|
+
#: cap bounds rows a human typed into a grid, this one bounds rows the server resolved
|
|
68
|
+
#: from a single click, and 500 sits below a realistic top-level phase. A cap is still
|
|
69
|
+
#: required because §7 of ADR-0790 forces one ``save()`` per changed row, so an
|
|
70
|
+
#: uncapped subtree is an unbounded write inside one transaction. Exceeding it is a 400
|
|
71
|
+
#: naming the count — never a silent truncation, which would report a subtree as
|
|
72
|
+
#: classified when its tail is not.
|
|
73
|
+
TASK_CLASSIFY_MAX_SUBTREE = 2000
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SubtreeTooLarge(Exception):
|
|
77
|
+
"""The resolved subtree exceeds :data:`TASK_CLASSIFY_MAX_SUBTREE`.
|
|
78
|
+
|
|
79
|
+
Carries the counts so the view can render an actionable 400 rather than a bare cap
|
|
80
|
+
violation the caller cannot act on.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, matched: int, cap: int) -> None:
|
|
84
|
+
self.matched = matched
|
|
85
|
+
self.cap = cap
|
|
86
|
+
super().__init__(f"Subtree resolves {matched} tasks, above the {cap}-task cap.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SubtreeNotAuthorable(Exception):
|
|
90
|
+
"""The caller may not edit every row in the resolved subtree.
|
|
91
|
+
|
|
92
|
+
Permission on this endpoint is all-or-nothing, which diverges deliberately from
|
|
93
|
+
``tasks/bulk/``'s per-row rejection. There, the caller typed 38 things and one bad
|
|
94
|
+
row must not discard the other 37. Here they declared *one* thing about a subtree,
|
|
95
|
+
and applying it to the 60% of rows they happen to be assigned leaves the plan
|
|
96
|
+
asserting a split that is not true — worse than refusing (ADR-0790 §6).
|
|
97
|
+
|
|
98
|
+
Carries the count only. No ids: the count is what makes the 403 explainable, and
|
|
99
|
+
enumerating rows adds nothing the caller can act on.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, forbidden: int, matched: int, *, detail: str | None = None) -> None:
|
|
103
|
+
self.forbidden = forbidden
|
|
104
|
+
self.matched = matched
|
|
105
|
+
#: ``detail`` exists for the root-only pre-check (#3305), which refuses before
|
|
106
|
+
#: the subtree is resolved and therefore cannot honestly say "n of m tasks" —
|
|
107
|
+
#: it does not know m, and inventing one would re-open the count disclosure
|
|
108
|
+
#: this ordering was changed to close.
|
|
109
|
+
super().__init__(
|
|
110
|
+
detail or f"Your role cannot author {forbidden} of the {matched} tasks in this subtree."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclasses.dataclass(frozen=True)
|
|
115
|
+
class ClassificationSpec:
|
|
116
|
+
"""One validated cascade request. Built by ``TaskClassificationSerializer``."""
|
|
117
|
+
|
|
118
|
+
subtree_id: uuid.UUID
|
|
119
|
+
cascade: bool
|
|
120
|
+
governance_class: str | None
|
|
121
|
+
delivery_mode: str | None
|
|
122
|
+
preserve_governance_overrides: bool
|
|
123
|
+
skip_milestones: bool
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclasses.dataclass
|
|
127
|
+
class _AxisTally:
|
|
128
|
+
"""Per-axis counters. ``overrides_kept`` stays ``None`` on an axis without an
|
|
129
|
+
inherit bit — see :func:`_axis_report`."""
|
|
130
|
+
|
|
131
|
+
applied: int = 0
|
|
132
|
+
unchanged: int = 0
|
|
133
|
+
overrides_kept: int = 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def resolve_subtree(project: Project, spec: ClassificationSpec) -> list[Task]:
|
|
137
|
+
"""Lock and return the subtree root plus, when cascading, its descendants.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
Task.DoesNotExist: The root is not a live task in this project. The view maps
|
|
141
|
+
this to a 404 rather than leaking whether the id exists elsewhere.
|
|
142
|
+
SubtreeTooLarge: More rows resolved than :data:`TASK_CLASSIFY_MAX_SUBTREE`.
|
|
143
|
+
|
|
144
|
+
Rows are locked with ``select_for_update()`` in ``wbs_path`` order, which is both
|
|
145
|
+
deterministic (so two cascades on overlapping subtrees serialize rather than
|
|
146
|
+
deadlock) and the order the change detection in :func:`cascade_task_classification`
|
|
147
|
+
wants anyway — the root sorts before its own descendants.
|
|
148
|
+
|
|
149
|
+
The root is locked first, in its own statement, before its ``wbs_path`` is read.
|
|
150
|
+
A concurrent reparent moves the path, so reading it unlocked would resolve a
|
|
151
|
+
descendant set for a tree shape that no longer exists by the time the write lands.
|
|
152
|
+
"""
|
|
153
|
+
from trueppm_api.apps.projects.models import Task
|
|
154
|
+
|
|
155
|
+
root = Task.objects.select_for_update().get(
|
|
156
|
+
pk=spec.subtree_id, project=project, is_deleted=False
|
|
157
|
+
)
|
|
158
|
+
if not spec.cascade:
|
|
159
|
+
return [root]
|
|
160
|
+
|
|
161
|
+
scope = Q(pk=root.pk)
|
|
162
|
+
if root.wbs_path:
|
|
163
|
+
# A descendant is any live task one or more levels deeper, i.e. whose path
|
|
164
|
+
# starts with "<root path>.". ``__regex`` is the lookup the rest of the
|
|
165
|
+
# codebase uses for ltree paths (see ``serializers.py`` summary detection), so
|
|
166
|
+
# this needs no extra lookup registration. Segments are digits; the escape is
|
|
167
|
+
# a safety net, not a load-bearing guard.
|
|
168
|
+
scope |= Q(wbs_path__regex=rf"^{re.escape(str(root.wbs_path))}\.")
|
|
169
|
+
|
|
170
|
+
matched = Task.objects.filter(scope, project=project, is_deleted=False).count()
|
|
171
|
+
if matched > TASK_CLASSIFY_MAX_SUBTREE:
|
|
172
|
+
raise SubtreeTooLarge(matched, TASK_CLASSIFY_MAX_SUBTREE)
|
|
173
|
+
|
|
174
|
+
return list(
|
|
175
|
+
Task.objects.select_for_update()
|
|
176
|
+
.filter(scope, project=project, is_deleted=False)
|
|
177
|
+
.order_by("wbs_path", "pk")
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def assert_graph_feasible(project_id: Any) -> None:
|
|
182
|
+
"""Run the ADR-0259 graph guard over the project's edges before recalculation.
|
|
183
|
+
|
|
184
|
+
This endpoint writes no dependency edges, so the guard cannot reject anything the
|
|
185
|
+
cascade itself did. It runs because the cascade calls ``enqueue_recalculate``, and
|
|
186
|
+
ADR-0259's rule is that the guard runs before a bulk write and before
|
|
187
|
+
recalculation.
|
|
188
|
+
|
|
189
|
+
That rule is non-vacuous today rather than defensive ceremony: the seed importer
|
|
190
|
+
still bypasses the guard (**#2589, open**) and project templates seed dependency
|
|
191
|
+
edges through it, so a cyclic graph can genuinely exist in a project — and a
|
|
192
|
+
cascade is very often the first authoring act after a template applies. Catching it
|
|
193
|
+
here surfaces an actionable 400 instead of a CPM worker crash nobody sees.
|
|
194
|
+
|
|
195
|
+
Project-scoped, not subtree-scoped: a cycle can leave the subtree and re-enter it,
|
|
196
|
+
and ``enqueue_recalculate``'s own unit is the project.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
InfeasibleGraphError: Self-referential or cyclic edges, re-worded to name the
|
|
200
|
+
offending tasks (see :func:`_offending_task_labels`).
|
|
201
|
+
InvalidScheduleInput: The graph is malformed rather than cyclic.
|
|
202
|
+
"""
|
|
203
|
+
from trueppm_api.apps.projects.models import Dependency
|
|
204
|
+
from trueppm_api.apps.projects.serializers import _load_project_tasks_and_children_map
|
|
205
|
+
from trueppm_api.apps.scheduling.graph_guard import (
|
|
206
|
+
InfeasibleGraphError,
|
|
207
|
+
validate_task_graph,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
task_ids, children_map = _load_project_tasks_and_children_map(project_id)
|
|
211
|
+
edges = [
|
|
212
|
+
(str(p), str(s))
|
|
213
|
+
for p, s in Dependency.objects.filter(
|
|
214
|
+
is_deleted=False, predecessor_id__in=[uuid.UUID(t) for t in task_ids]
|
|
215
|
+
).values_list("predecessor_id", "successor_id")
|
|
216
|
+
]
|
|
217
|
+
try:
|
|
218
|
+
validate_task_graph(edges, children_map=children_map)
|
|
219
|
+
except InfeasibleGraphError as exc:
|
|
220
|
+
# The guard reasons in opaque node ids; this is the layer that knows they are
|
|
221
|
+
# task pks. Relabelling here rather than passing labels *in* keeps the cost on
|
|
222
|
+
# the refusal path only — the guard blames a handful of ids, and the feasible
|
|
223
|
+
# cascade (every request that is not refused) reads no extra row.
|
|
224
|
+
raise exc.with_labels(_offending_task_labels(project_id, exc.offending)) from exc
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _offending_task_labels(project_id: Any, offending: list[str]) -> dict[str, str]:
|
|
228
|
+
"""Resolve blamed task ids to the ``"<WBS> — <name>"`` form the plan shows.
|
|
229
|
+
|
|
230
|
+
That form is not invented here: it is the reference the Schedule already uses
|
|
231
|
+
wherever it names a task outside its own row — the dependency picker, the
|
|
232
|
+
dependencies tab, the task drawer title — so the tasks a refusal names are
|
|
233
|
+
findable by searching for exactly the string it printed.
|
|
234
|
+
|
|
235
|
+
Private on purpose. ``validate_task_graph`` explicitly invites callers to run
|
|
236
|
+
the guard in their own *external*-id space (Jira keys, MS Project uids), and
|
|
237
|
+
relabelling one of those refusals through here would hand a non-UUID to a
|
|
238
|
+
``UUIDField`` lookup — a Django ``ValidationError`` DRF does not convert, i.e.
|
|
239
|
+
a 500 on what should be a 400. This is only correct for the pk-space guard run
|
|
240
|
+
directly above it.
|
|
241
|
+
|
|
242
|
+
Not a disclosure — verified against the gate, not assumed. The guard is
|
|
243
|
+
project-scoped, and this endpoint requires ``IsProjectMemberWrite`` +
|
|
244
|
+
``IsProjectPlanAuthor`` on the project, a strict subset of the ``IsProjectMember``
|
|
245
|
+
that already grants ``list``/``retrieve`` on every task in it. So a caller who can
|
|
246
|
+
trip this guard could already have read every name and WBS code it can print; only
|
|
247
|
+
the wording changes. That holds **while** ``Task`` has no row-level visibility gate
|
|
248
|
+
— the dormant ``Visibility`` enum lives on ``Project``/``Program`` only. If per-task
|
|
249
|
+
visibility ever lands, this function becomes a cross-boundary read and must be
|
|
250
|
+
re-scoped; nothing in the pipeline would catch that on its own.
|
|
251
|
+
|
|
252
|
+
Ids that no longer resolve (a row deleted between the refusal and this read) are
|
|
253
|
+
simply absent; the message falls back to the raw id for those alone.
|
|
254
|
+
|
|
255
|
+
Scoped to live rows in *this* project rather than resolving a bare pk. A blamed
|
|
256
|
+
node is always both today — the guard reads only in-project live tasks — but that
|
|
257
|
+
is an invariant of the edge query two functions away, and this lookup is what
|
|
258
|
+
would print the answer if it ever stopped holding.
|
|
259
|
+
"""
|
|
260
|
+
from trueppm_api.apps.projects.models import Task
|
|
261
|
+
from trueppm_api.apps.scheduling.graph_guard import nodes_to_label
|
|
262
|
+
|
|
263
|
+
# Only the ids the sentence will actually print. A 5000-task cycle names four of
|
|
264
|
+
# its members, so resolving all 5000 would materialize 4996 rows to discard them.
|
|
265
|
+
rows = Task.objects.filter(
|
|
266
|
+
project_id=project_id, is_deleted=False, pk__in=set(nodes_to_label(offending))
|
|
267
|
+
).values("id", "name", "wbs_path")
|
|
268
|
+
labels: dict[str, str] = {}
|
|
269
|
+
for row in rows:
|
|
270
|
+
name = (row["name"] or "").strip()
|
|
271
|
+
wbs = str(row["wbs_path"] or "").strip()
|
|
272
|
+
if name and wbs:
|
|
273
|
+
labels[str(row["id"])] = f"{wbs} — {name}"
|
|
274
|
+
elif name:
|
|
275
|
+
labels[str(row["id"])] = name
|
|
276
|
+
elif wbs:
|
|
277
|
+
# A row with no name is still findable by its WBS code, which is the
|
|
278
|
+
# handle the message exists to give — so say so rather than print a
|
|
279
|
+
# dangling separator.
|
|
280
|
+
labels[str(row["id"])] = f"{wbs} — (unnamed task)"
|
|
281
|
+
return labels
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def assert_authorable(request: Request, rows: list[Task]) -> None:
|
|
285
|
+
"""All-or-nothing per-row edit check (ADR-0790 §6).
|
|
286
|
+
|
|
287
|
+
``IsProjectPlanAuthor`` decides whether the caller may enter Author mode at all;
|
|
288
|
+
``can_user_edit_task`` remains authoritative for *which rows* they may touch
|
|
289
|
+
(ADR-0773 §3). Both apply — the project-level gate does not subsume the row-level
|
|
290
|
+
one, which is why a plain Member (who may edit only their assigned tasks) cannot
|
|
291
|
+
re-classify a phase they do not own.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
SubtreeNotAuthorable: One or more rows fail the per-row bar.
|
|
295
|
+
"""
|
|
296
|
+
forbidden = sum(1 for task in rows if not can_user_edit_task(request, task, method="PATCH"))
|
|
297
|
+
if forbidden:
|
|
298
|
+
raise SubtreeNotAuthorable(forbidden, len(rows))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _apply_governance(
|
|
302
|
+
task: Task,
|
|
303
|
+
*,
|
|
304
|
+
is_root: bool,
|
|
305
|
+
spec: ClassificationSpec,
|
|
306
|
+
tally: _AxisTally,
|
|
307
|
+
) -> tuple[list[str], list[str]]:
|
|
308
|
+
"""Resolve the ``governance_class`` axis for one row.
|
|
309
|
+
|
|
310
|
+
Returns ``(changed, withheld)`` and mutates ``task`` in memory only — saving is the
|
|
311
|
+
caller's decision, exactly as in :func:`_classify_row`.
|
|
312
|
+
"""
|
|
313
|
+
changed: list[str] = []
|
|
314
|
+
withheld: list[str] = []
|
|
315
|
+
|
|
316
|
+
if spec.governance_class is not None:
|
|
317
|
+
if task.is_milestone and spec.skip_milestones:
|
|
318
|
+
withheld.append("governance_class")
|
|
319
|
+
elif (
|
|
320
|
+
not is_root
|
|
321
|
+
and spec.preserve_governance_overrides
|
|
322
|
+
and (not task.parent_governance_inherited)
|
|
323
|
+
):
|
|
324
|
+
# An explicit override: this node declared its own governance rather than
|
|
325
|
+
# taking its parent's. Preserving it is the default, and it is counted so
|
|
326
|
+
# the caller can see what the cascade deliberately did not touch.
|
|
327
|
+
tally.overrides_kept += 1
|
|
328
|
+
else:
|
|
329
|
+
# The root is the declaration point — declaring a subtree's governance IS
|
|
330
|
+
# breaking inheritance from whatever sits above it. Cascaded descendants
|
|
331
|
+
# inherit from the root. A descendant overwritten because the caller sent
|
|
332
|
+
# preserve_governance_overrides=false is no longer an override, so its bit
|
|
333
|
+
# resets to True with everything else.
|
|
334
|
+
target_bit = not is_root
|
|
335
|
+
if (
|
|
336
|
+
task.governance_class != spec.governance_class
|
|
337
|
+
or task.parent_governance_inherited != target_bit
|
|
338
|
+
):
|
|
339
|
+
task.governance_class = spec.governance_class
|
|
340
|
+
task.parent_governance_inherited = target_bit
|
|
341
|
+
changed += ["governance_class", "parent_governance_inherited"]
|
|
342
|
+
tally.applied += 1
|
|
343
|
+
else:
|
|
344
|
+
tally.unchanged += 1
|
|
345
|
+
|
|
346
|
+
return changed, withheld
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _apply_delivery(
|
|
350
|
+
task: Task,
|
|
351
|
+
*,
|
|
352
|
+
spec: ClassificationSpec,
|
|
353
|
+
tally: _AxisTally,
|
|
354
|
+
) -> tuple[list[str], list[str]]:
|
|
355
|
+
"""Resolve the ``delivery_mode`` axis for one row.
|
|
356
|
+
|
|
357
|
+
Returns ``(changed, withheld)`` and mutates ``task`` in memory only — saving is the
|
|
358
|
+
caller's decision, exactly as in :func:`_classify_row`.
|
|
359
|
+
"""
|
|
360
|
+
changed: list[str] = []
|
|
361
|
+
withheld: list[str] = []
|
|
362
|
+
|
|
363
|
+
if spec.delivery_mode is not None:
|
|
364
|
+
if task.is_milestone:
|
|
365
|
+
# Unconditional, and not a flag the caller can waive. is_milestone,
|
|
366
|
+
# delivery_mode='milestone' and duration=0 are three encodings of one
|
|
367
|
+
# fact; writing a non-milestone mode here produces a row the rollup SQL
|
|
368
|
+
# and the CPM engine disagree about. skip_milestones governs the OTHER
|
|
369
|
+
# axis (governance on a gate is invariant-safe — that is what an overlay
|
|
370
|
+
# is); it has no say over this one.
|
|
371
|
+
withheld.append("delivery_mode")
|
|
372
|
+
elif task.delivery_mode != spec.delivery_mode:
|
|
373
|
+
task.delivery_mode = spec.delivery_mode
|
|
374
|
+
changed.append("delivery_mode")
|
|
375
|
+
tally.applied += 1
|
|
376
|
+
else:
|
|
377
|
+
tally.unchanged += 1
|
|
378
|
+
|
|
379
|
+
return changed, withheld
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _classify_row(
|
|
383
|
+
task: Task,
|
|
384
|
+
*,
|
|
385
|
+
is_root: bool,
|
|
386
|
+
spec: ClassificationSpec,
|
|
387
|
+
governance: _AxisTally,
|
|
388
|
+
delivery: _AxisTally,
|
|
389
|
+
skipped: list[dict[str, Any]],
|
|
390
|
+
) -> list[str]:
|
|
391
|
+
"""Compute one row's new classification; return the model fields it changed.
|
|
392
|
+
|
|
393
|
+
Mutates ``task`` in memory only — the caller decides whether to save, so a row
|
|
394
|
+
already at the requested value costs no write, no version bump, and no history row.
|
|
395
|
+
|
|
396
|
+
The two axes are resolved independently and their results concatenated, which is
|
|
397
|
+
what keeps ``changed`` and the skip report's ``axes`` in governance-then-delivery
|
|
398
|
+
order.
|
|
399
|
+
"""
|
|
400
|
+
governance_changed, governance_withheld = _apply_governance(
|
|
401
|
+
task, is_root=is_root, spec=spec, tally=governance
|
|
402
|
+
)
|
|
403
|
+
delivery_changed, delivery_withheld = _apply_delivery(task, spec=spec, tally=delivery)
|
|
404
|
+
|
|
405
|
+
changed = governance_changed + delivery_changed
|
|
406
|
+
withheld = governance_withheld + delivery_withheld
|
|
407
|
+
|
|
408
|
+
if withheld:
|
|
409
|
+
skipped.append(
|
|
410
|
+
{
|
|
411
|
+
"id": str(task.pk),
|
|
412
|
+
"code": CODE_MILESTONE_GATE,
|
|
413
|
+
"axes": withheld,
|
|
414
|
+
"message": (
|
|
415
|
+
"Milestone left unclassified on "
|
|
416
|
+
+ " and ".join(withheld)
|
|
417
|
+
+ " — a classification does not re-type a gate."
|
|
418
|
+
),
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return changed
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _axis_report(
|
|
426
|
+
requested: str | None, tally: _AxisTally, *, has_inherit_bit: bool
|
|
427
|
+
) -> dict[str, Any] | None:
|
|
428
|
+
"""One axis's block in the response, or ``None`` if the request omitted the axis.
|
|
429
|
+
|
|
430
|
+
``overrides_kept`` is ``null`` on an axis with no inherit bit — **not** ``0``.
|
|
431
|
+
Zero reads as "there were none", which is a claim about the data; ``null`` states
|
|
432
|
+
that the count is not computable on this axis, which is a claim about the model,
|
|
433
|
+
and it is the true one. Only ``governance_class`` carries the bit, so only
|
|
434
|
+
governance can report the count (issue #2735 §3).
|
|
435
|
+
"""
|
|
436
|
+
if requested is None:
|
|
437
|
+
return None
|
|
438
|
+
return {
|
|
439
|
+
"requested": requested,
|
|
440
|
+
"applied": tally.applied,
|
|
441
|
+
"unchanged": tally.unchanged,
|
|
442
|
+
"overrides_kept": tally.overrides_kept if has_inherit_bit else None,
|
|
443
|
+
"has_inherit_bit": has_inherit_bit,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
#: Fields _classify_row can change — also consulted by task_batch_services.py's
|
|
448
|
+
#: undo (the two modules deliberately don't import from each other; see that
|
|
449
|
+
#: module's own copy of this tuple for why).
|
|
450
|
+
CLASSIFICATION_FIELDS = ("governance_class", "parent_governance_inherited", "delivery_mode")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def cascade_task_classification(
|
|
454
|
+
project: Project, request: Request, spec: ClassificationSpec
|
|
455
|
+
) -> tuple[dict[str, Any], list[uuid.UUID], dict[str, dict[str, Any]]]:
|
|
456
|
+
"""Apply one classification cascade.
|
|
457
|
+
|
|
458
|
+
Returns ``(report, written_task_ids, before_after)``. ``before_after`` maps
|
|
459
|
+
``str(task_id) -> {"before": {...}, "after": {...}}`` for exactly the rows
|
|
460
|
+
written — the ADR-0810 undo ledger's input, built here because this is the
|
|
461
|
+
only point that sees both a row's pre-cascade values and its post-cascade
|
|
462
|
+
``changed`` fields in the same place.
|
|
463
|
+
|
|
464
|
+
Must be called inside ``transaction.atomic()`` — ``resolve_subtree`` takes row
|
|
465
|
+
locks, and the caller registers the recalculation and broadcast with
|
|
466
|
+
``transaction.on_commit``.
|
|
467
|
+
|
|
468
|
+
Raises:
|
|
469
|
+
Task.DoesNotExist: the root is not a live task in this project — raised by the
|
|
470
|
+
unlocked root read below, or by :func:`resolve_subtree`.
|
|
471
|
+
SubtreeTooLarge: from :func:`resolve_subtree`.
|
|
472
|
+
SubtreeNotAuthorable: from the root pre-check below, or :func:`assert_authorable`.
|
|
473
|
+
InfeasibleGraphError / InvalidScheduleInput: from :func:`assert_graph_feasible`.
|
|
474
|
+
"""
|
|
475
|
+
from trueppm_api.apps.projects.models import Task
|
|
476
|
+
|
|
477
|
+
# Authorization first, so an unauthorized caller never pays for — or learns
|
|
478
|
+
# anything from — the work below. Permission here is all-or-nothing and the root
|
|
479
|
+
# is in every resolved subtree, so authoring the root is a *necessary* condition:
|
|
480
|
+
# refusing on it alone is sound, and it is the only check that can run before the
|
|
481
|
+
# subtree is resolved. Deliberately unlocked and ahead of resolve_subtree, because
|
|
482
|
+
# that call locks the root, resolves descendants and evaluates the size cap — so
|
|
483
|
+
# running it first made a caller with no authorship on the subtree pay for up to
|
|
484
|
+
# 2000 row locks and receive a 400 disclosing the subtree's task count instead of
|
|
485
|
+
# a 403 (#3305).
|
|
486
|
+
#
|
|
487
|
+
# Residual, and narrower by construction: a caller who *can* author the root but
|
|
488
|
+
# not every descendant is still resolved under lock before assert_authorable
|
|
489
|
+
# refuses them below. Closing that too would mean authorizing the descendant set
|
|
490
|
+
# before locking it, which reintroduces a TOCTOU — a row inserted between the
|
|
491
|
+
# unlocked authorization and the locked write would be written unauthorized.
|
|
492
|
+
root_only = Task.objects.get(pk=spec.subtree_id, project=project, is_deleted=False)
|
|
493
|
+
if not can_user_edit_task(request, root_only, method="PATCH"):
|
|
494
|
+
raise SubtreeNotAuthorable(1, 1, detail="Your role cannot author the root of this subtree.")
|
|
495
|
+
|
|
496
|
+
rows = resolve_subtree(project, spec)
|
|
497
|
+
assert_authorable(request, rows)
|
|
498
|
+
|
|
499
|
+
governance = _AxisTally()
|
|
500
|
+
delivery = _AxisTally()
|
|
501
|
+
skipped: list[dict[str, Any]] = []
|
|
502
|
+
written: list[uuid.UUID] = []
|
|
503
|
+
before_after: dict[str, dict[str, Any]] = {}
|
|
504
|
+
|
|
505
|
+
# Two passes. The first mutates instances in memory only and collects what would
|
|
506
|
+
# be written; nothing touches the database. That is what lets the graph guard be
|
|
507
|
+
# skipped entirely when the cascade turns out to be a no-op — its cost scales with
|
|
508
|
+
# PROJECT size (every task row plus every edge), not subtree size, so a repeated
|
|
509
|
+
# identical cascade would otherwise pay a full project scan to write nothing.
|
|
510
|
+
#
|
|
511
|
+
# The "before" snapshot is taken here, ahead of _classify_row, because that
|
|
512
|
+
# function mutates `task` in memory before the caller decides to save — by the
|
|
513
|
+
# time a row reaches `pending` below, its pre-cascade values are already gone
|
|
514
|
+
# from the instance itself.
|
|
515
|
+
pending: list[tuple[Task, list[str]]] = []
|
|
516
|
+
for task in rows:
|
|
517
|
+
before = {field: getattr(task, field) for field in CLASSIFICATION_FIELDS}
|
|
518
|
+
changed = _classify_row(
|
|
519
|
+
task,
|
|
520
|
+
is_root=task.pk == spec.subtree_id,
|
|
521
|
+
spec=spec,
|
|
522
|
+
governance=governance,
|
|
523
|
+
delivery=delivery,
|
|
524
|
+
skipped=skipped,
|
|
525
|
+
)
|
|
526
|
+
if changed:
|
|
527
|
+
pending.append((task, changed))
|
|
528
|
+
before_after[str(task.pk)] = {"before": before}
|
|
529
|
+
|
|
530
|
+
if pending:
|
|
531
|
+
assert_graph_feasible(project.pk)
|
|
532
|
+
for task, changed in pending:
|
|
533
|
+
# known_exists=True: the rows were just SELECTed under a lock, so the
|
|
534
|
+
# INSERT/UPDATE disambiguation probe VersionedModel.save() otherwise runs
|
|
535
|
+
# per row is a guaranteed-True query — 2000 of them for a large phase.
|
|
536
|
+
task.save(update_fields=changed, known_exists=True)
|
|
537
|
+
written.append(task.pk)
|
|
538
|
+
before_after[str(task.pk)]["after"] = {
|
|
539
|
+
field: getattr(task, field) for field in CLASSIFICATION_FIELDS
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
report: dict[str, Any] = {
|
|
543
|
+
"subtree": str(spec.subtree_id),
|
|
544
|
+
"matched": len(rows),
|
|
545
|
+
# Rows actually saved (#3306). Not derivable from the per-axis tallies and
|
|
546
|
+
# not the same number: ``applied`` increments once per row PER AXIS, so a
|
|
547
|
+
# both-axes cascade counts the same row twice, and a milestone withheld on
|
|
548
|
+
# one axis but written on the other is one row appearing in one total only.
|
|
549
|
+
# The governance branch also writes two model columns per increment, so the
|
|
550
|
+
# sum is neither rows nor fields. This is the one count the caller can hold
|
|
551
|
+
# against what they selected and check against the grid.
|
|
552
|
+
"rows_written": len(written),
|
|
553
|
+
"skipped": skipped,
|
|
554
|
+
}
|
|
555
|
+
governance_block = _axis_report(spec.governance_class, governance, has_inherit_bit=True)
|
|
556
|
+
if governance_block is not None:
|
|
557
|
+
report["governance"] = governance_block
|
|
558
|
+
delivery_block = _axis_report(spec.delivery_mode, delivery, has_inherit_bit=False)
|
|
559
|
+
if delivery_block is not None:
|
|
560
|
+
report["delivery_mode"] = delivery_block
|
|
561
|
+
return report, written, before_after
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Methodology-aware defaults for a new task's governance/delivery taxonomy (#2667).
|
|
2
|
+
|
|
3
|
+
``Task.governance_class`` and ``Task.delivery_mode`` (ADR-0036, #407) are purely
|
|
4
|
+
additive fields with model-level defaults of ``FLOW`` / ``WATERFALL`` — chosen so
|
|
5
|
+
every pre-existing row's semantics stayed unchanged when the taxonomy shipped.
|
|
6
|
+
Those literals are also what a brand-new task got on *every* project regardless of
|
|
7
|
+
its own resolved methodology, which produces a self-contradictory dialog on a
|
|
8
|
+
``WATERFALL`` project (opens on Flow governance — "Agile, sprint- or
|
|
9
|
+
kanban-governed work" — for a project with no sprints or board governance) and a
|
|
10
|
+
correctness bug on ``AGILE`` (delivery mode stays Waterfall, so the task never
|
|
11
|
+
engages point-burndown rollup or agile-aware Monte Carlo).
|
|
12
|
+
|
|
13
|
+
This module resolves the *project-aware* default, consulted only when the client
|
|
14
|
+
omits the field on create (:meth:`TaskSerializer.validate`) — an explicit value
|
|
15
|
+
always wins, on create and update alike, so the documented hybrid seam (a single
|
|
16
|
+
project deliberately holding tasks in different delivery modes/governance
|
|
17
|
+
classes) is untouched. ``HYBRID`` keeps today's literal default (``flow`` /
|
|
18
|
+
``waterfall``) since it is the preset that already mixes both models.
|
|
19
|
+
|
|
20
|
+
Mirrored client-side in ``packages/web/src/lib/taskClassificationDefaults.ts`` so
|
|
21
|
+
the create dialog opens on the value this module would pick — the two are
|
|
22
|
+
independently maintained (there is no "preview the server default" endpoint) and
|
|
23
|
+
must be changed together. Keep them in sync.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from trueppm_api.apps.projects.models import Project
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_default_governance_class(effective_methodology: str) -> str:
|
|
35
|
+
"""The governance class a new task should default to for a project's preset.
|
|
36
|
+
|
|
37
|
+
``WATERFALL`` → ``gated`` (phase-gate governance matches a project with no
|
|
38
|
+
sprints or board governance). Every other preset (``AGILE``, ``HYBRID``)
|
|
39
|
+
keeps the existing ``flow`` default — ``HYBRID`` deliberately, because it is
|
|
40
|
+
the preset that mixes both governance overlays and has no single "right"
|
|
41
|
+
class to steer toward.
|
|
42
|
+
"""
|
|
43
|
+
from trueppm_api.apps.projects.models import GovernanceClass, Methodology
|
|
44
|
+
|
|
45
|
+
if effective_methodology == Methodology.WATERFALL:
|
|
46
|
+
return str(GovernanceClass.GATED)
|
|
47
|
+
return str(GovernanceClass.FLOW)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_default_delivery_mode(effective_methodology: str, board_cadence: str) -> str:
|
|
51
|
+
"""The delivery mode a new task should default to for a project's preset.
|
|
52
|
+
|
|
53
|
+
- ``WATERFALL`` / ``HYBRID`` → ``waterfall`` (HYBRID keeps today's literal
|
|
54
|
+
default — the lossless choice for a preset that mixes delivery modes).
|
|
55
|
+
- ``AGILE`` → ``scrum``, unless the project's board runs continuous-flow
|
|
56
|
+
Kanban with no sprint cadence (``board_cadence == 'continuous'``), in which
|
|
57
|
+
case → ``kanban``.
|
|
58
|
+
|
|
59
|
+
The scrum/kanban split is resolved from ``board_cadence`` rather than an
|
|
60
|
+
extra board-config lookup: ADR-0164 already models "sprint cadence vs.
|
|
61
|
+
continuous flow" as that exact field, and ``scrum`` is the load-bearing
|
|
62
|
+
choice for a brand-new agile project with nothing configured yet — an
|
|
63
|
+
unpointed ``scrum`` task degrades to identical rollup behavior as a
|
|
64
|
+
``waterfall`` one (``COALESCE(story_points, duration)``), while defaulting to
|
|
65
|
+
``kanban`` would immediately binarize percent-complete and equal-weight every
|
|
66
|
+
task regardless of size (see #2667 for the full analysis).
|
|
67
|
+
"""
|
|
68
|
+
from trueppm_api.apps.projects.models import BoardCadence, DeliveryMode, Methodology
|
|
69
|
+
|
|
70
|
+
if effective_methodology == Methodology.AGILE:
|
|
71
|
+
if board_cadence == BoardCadence.CONTINUOUS:
|
|
72
|
+
return str(DeliveryMode.KANBAN)
|
|
73
|
+
return str(DeliveryMode.SCRUM)
|
|
74
|
+
return str(DeliveryMode.WATERFALL)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def resolve_default_classification(
|
|
78
|
+
project: Project, *, workspace: object | None = None
|
|
79
|
+
) -> tuple[str, str]:
|
|
80
|
+
"""``(governance_class, delivery_mode)`` defaults for a new task on ``project``.
|
|
81
|
+
|
|
82
|
+
Resolves ``project``'s ``effective_methodology`` (workspace → program →
|
|
83
|
+
project precedence, ADR-0107) once and derives both fields from it, so a
|
|
84
|
+
caller needing both never resolves methodology twice. ``workspace`` is
|
|
85
|
+
accepted so a caller iterating many tasks against the same project can pass
|
|
86
|
+
the already-loaded singleton (mirrors the other resolvers in this app);
|
|
87
|
+
otherwise it is loaded here.
|
|
88
|
+
"""
|
|
89
|
+
from trueppm_api.apps.projects.methodology import resolve_effective_methodology
|
|
90
|
+
|
|
91
|
+
effective_methodology = resolve_effective_methodology(project, workspace=workspace) # type: ignore[arg-type]
|
|
92
|
+
return (
|
|
93
|
+
resolve_default_governance_class(effective_methodology),
|
|
94
|
+
resolve_default_delivery_mode(effective_methodology, project.board_cadence),
|
|
95
|
+
)
|