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,903 @@
|
|
|
1
|
+
"""Program rollup-KPI consumer (ADR-0088, #713).
|
|
2
|
+
|
|
3
|
+
Reads the per-program rollup config shipped by #527 (``rollup_enabled_kpis`` +
|
|
4
|
+
``rollup_aggregation_policy`` on :class:`Program`) and computes the actual
|
|
5
|
+
rolled-up KPI values across the program's *own* projects. Intra-program only —
|
|
6
|
+
cross-program aggregation is Enterprise (ADR-0070).
|
|
7
|
+
|
|
8
|
+
Design (ADR-0088):
|
|
9
|
+
|
|
10
|
+
- **7 KPIs are computable today.** ``cost_variance`` / ``budget_utilization`` are
|
|
11
|
+
deferred to the cost/EVM model (#754) and ``p80_completion`` to a persistent
|
|
12
|
+
Monte Carlo store (#753); those return ``{"available": False, "reason": ...}``
|
|
13
|
+
so the UI can explain *why* a toggled KPI shows nothing rather than fabricating
|
|
14
|
+
a zero.
|
|
15
|
+
- **The aggregation policy applies where it is meaningful.** ``worst`` / ``average``
|
|
16
|
+
/ ``task_weighted`` govern the health bands (``schedule_health``,
|
|
17
|
+
``milestone_health``), the day-variances (``baseline_variance``,
|
|
18
|
+
``schedule_variance``), and the headline program health dot. Counts
|
|
19
|
+
(``critical_tasks``, ``at_risk_tasks``) and risk exposure (``risk_score``) roll
|
|
20
|
+
up as program **totals** regardless of policy — the sum is the only PM-useful
|
|
21
|
+
number for an additive metric.
|
|
22
|
+
- ``weighted_by_budget`` has no weight to use until #754, so it falls back to
|
|
23
|
+
``average`` and the response flags ``policy_available: False``; the rollup is
|
|
24
|
+
never blanked just because the chosen policy cannot be honored.
|
|
25
|
+
|
|
26
|
+
Implemented with a fixed, small number of grouped (``values().annotate()``)
|
|
27
|
+
queries across the program's projects — never a per-project ``ProjectOverviewView``
|
|
28
|
+
call — to hold the ≤200 ms p95 budget (ADR-0030/0088). Pure read; no writes, no
|
|
29
|
+
async side effects (ADR-0088 Durable Execution: all N/A).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import datetime
|
|
35
|
+
from collections.abc import Collection
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from django.db.models import BooleanField, Count, F, Max, Q, Sum
|
|
39
|
+
from django.db.models.expressions import RawSQL
|
|
40
|
+
from django.utils import timezone
|
|
41
|
+
|
|
42
|
+
from trueppm_api.apps.projects.models import (
|
|
43
|
+
AggregationPolicy,
|
|
44
|
+
Baseline,
|
|
45
|
+
BaselineTask,
|
|
46
|
+
Program,
|
|
47
|
+
Project,
|
|
48
|
+
Risk,
|
|
49
|
+
RiskStatus,
|
|
50
|
+
RollupKpi,
|
|
51
|
+
Task,
|
|
52
|
+
TaskStatus,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# KPIs without a per-project source yet (ADR-0088). The reason string is stable
|
|
56
|
+
# so the web layer and #673's preview can branch on it.
|
|
57
|
+
#
|
|
58
|
+
# This map is the single source of truth for "can this KPI ever produce a value
|
|
59
|
+
# today?" and is published on the rollup-config endpoint as ``unavailable_kpis``
|
|
60
|
+
# (#2404). It has to be a *server* fact rather than a hard-coded client list:
|
|
61
|
+
# the settings picker offered all ten toggles as if they were live, so enabling
|
|
62
|
+
# one of these three saved a config that could never render — a control that
|
|
63
|
+
# lies. When #753 (Monte Carlo store) and #754 (cost/EVM model) land, deleting
|
|
64
|
+
# the corresponding entry here is the only change needed to light the toggle up.
|
|
65
|
+
DEFERRED_KPI_REASONS = {
|
|
66
|
+
RollupKpi.COST_VARIANCE.value: "no_cost_data",
|
|
67
|
+
RollupKpi.BUDGET_UTILIZATION.value: "no_cost_data",
|
|
68
|
+
RollupKpi.P80_COMPLETION.value: "no_montecarlo_store",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Active task statuses (exclude COMPLETE) — "open work" for count KPIs. Mirrors
|
|
72
|
+
# the at-risk/critical definitions in ProjectViewSet.status_summary so the
|
|
73
|
+
# program rollup and the per-project StatusBar agree.
|
|
74
|
+
_ACTIVE_EXCLUDE = {TaskStatus.COMPLETE}
|
|
75
|
+
|
|
76
|
+
# Health band → ordinal for reducing (higher = healthier). ``unknown`` has no
|
|
77
|
+
# ordinal: it is excluded from the reduce rather than dragging the result down,
|
|
78
|
+
# which would punish projects that simply have no baseline yet.
|
|
79
|
+
_HEALTH_ORDINAL = {"critical": 0, "at_risk": 1, "on_track": 2}
|
|
80
|
+
_ORDINAL_TO_HEALTH = {0: "critical", 1: "at_risk", 2: "on_track"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_program_rollup(
|
|
84
|
+
program: Program,
|
|
85
|
+
*,
|
|
86
|
+
exclude_project_ids: Collection[Any] | None = None,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""Compute the rolled-up KPI block for a program's overview (ADR-0088).
|
|
89
|
+
|
|
90
|
+
Returns a dict with the program health dot, the active aggregation policy
|
|
91
|
+
(and whether it could be honored), the contributing project count, and a
|
|
92
|
+
``kpis`` map keyed by the program's enabled :class:`RollupKpi` values. Each
|
|
93
|
+
entry is either ``{"available": True, "value": ..., ["unit": ...]}`` (value
|
|
94
|
+
may be ``None`` when a built KPI has no data yet, e.g. no active baseline) or
|
|
95
|
+
``{"available": False, "reason": ...}`` for a KPI whose per-project source is
|
|
96
|
+
not built yet (#753/#754).
|
|
97
|
+
|
|
98
|
+
Pure read: deterministic for a given DB state, no writes, safe to call on
|
|
99
|
+
every GET.
|
|
100
|
+
"""
|
|
101
|
+
today = timezone.localdate()
|
|
102
|
+
policy = program.rollup_aggregation_policy or AggregationPolicy.WORST.value
|
|
103
|
+
enabled = list(program.rollup_enabled_kpis or [])
|
|
104
|
+
|
|
105
|
+
# Drafts are excluded (#2962): a plan nobody has committed to must not move a
|
|
106
|
+
# number a PMO puts in front of a CEO. `visible_projects` is the single
|
|
107
|
+
# definition of that exclusion — see lifecycle.py for why it is one helper
|
|
108
|
+
# and not a filter repeated per surface.
|
|
109
|
+
from trueppm_api.apps.projects.lifecycle import visible_projects
|
|
110
|
+
|
|
111
|
+
project_qs = visible_projects(Project.objects.filter(program=program, is_deleted=False))
|
|
112
|
+
if exclude_project_ids is not None:
|
|
113
|
+
# ADR-0678 (#2482): a project that opted out of agent reads does not feed the
|
|
114
|
+
# rolled-up KPIs an agent sees. ``contributing_project_count`` derives from this
|
|
115
|
+
# set, so the response stays internally consistent — the agent is told how many
|
|
116
|
+
# projects contributed, never that any were withheld.
|
|
117
|
+
project_qs = project_qs.exclude(pk__in=exclude_project_ids)
|
|
118
|
+
project_ids = list(project_qs.values_list("id", flat=True))
|
|
119
|
+
|
|
120
|
+
# weighted_by_budget cannot be honored until the cost model (#754) lands, so
|
|
121
|
+
# it degrades to AVERAGE and the caller flags policy_available=False.
|
|
122
|
+
effective_policy = policy
|
|
123
|
+
policy_available = True
|
|
124
|
+
if policy == AggregationPolicy.WEIGHTED_BY_BUDGET.value:
|
|
125
|
+
effective_policy = AggregationPolicy.AVERAGE.value
|
|
126
|
+
policy_available = False
|
|
127
|
+
|
|
128
|
+
# Per-project committed task counts — the weight for task_weighted. Computed
|
|
129
|
+
# once and shared by every policy-governed KPI. Excludes BACKLOG (uncommitted)
|
|
130
|
+
# so a large grooming backlog does not skew the weight.
|
|
131
|
+
task_weights = _committed_task_counts(project_ids)
|
|
132
|
+
|
|
133
|
+
# schedule_health per project is always needed: it is both a KPI and the
|
|
134
|
+
# source of the headline program health dot (drives Program.health=AUTO).
|
|
135
|
+
schedule_health_by_project = _schedule_health_by_project(project_ids, today)
|
|
136
|
+
program_health = _reduce_health(
|
|
137
|
+
list(schedule_health_by_project.values()), effective_policy, task_weights, project_ids
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
kpis: dict[str, dict[str, Any]] = {}
|
|
141
|
+
for kpi in enabled:
|
|
142
|
+
if kpi in DEFERRED_KPI_REASONS:
|
|
143
|
+
kpis[kpi] = {"available": False, "reason": DEFERRED_KPI_REASONS[kpi]}
|
|
144
|
+
elif kpi == RollupKpi.SCHEDULE_HEALTH.value:
|
|
145
|
+
kpis[kpi] = {
|
|
146
|
+
"available": True,
|
|
147
|
+
"value": _reduce_health(
|
|
148
|
+
list(schedule_health_by_project.values()),
|
|
149
|
+
effective_policy,
|
|
150
|
+
task_weights,
|
|
151
|
+
project_ids,
|
|
152
|
+
),
|
|
153
|
+
}
|
|
154
|
+
elif kpi == RollupKpi.MILESTONE_HEALTH.value:
|
|
155
|
+
bands = list(_milestone_health_by_project(project_ids, today).values())
|
|
156
|
+
kpis[kpi] = {
|
|
157
|
+
"available": True,
|
|
158
|
+
"value": _reduce_health(bands, effective_policy, task_weights, project_ids),
|
|
159
|
+
}
|
|
160
|
+
elif kpi == RollupKpi.CRITICAL_TASKS.value:
|
|
161
|
+
kpis[kpi] = {"available": True, "value": _critical_task_total(project_ids)}
|
|
162
|
+
elif kpi == RollupKpi.AT_RISK_TASKS.value:
|
|
163
|
+
kpis[kpi] = {"available": True, "value": _at_risk_task_total(project_ids)}
|
|
164
|
+
elif kpi == RollupKpi.RISK_SCORE.value:
|
|
165
|
+
kpis[kpi] = {"available": True, "value": _risk_score_total(project_ids)}
|
|
166
|
+
elif kpi == RollupKpi.BASELINE_VARIANCE.value:
|
|
167
|
+
kpis[kpi] = {
|
|
168
|
+
"available": True,
|
|
169
|
+
"unit": "calendar_days",
|
|
170
|
+
"value": _reduce_variance(
|
|
171
|
+
_baseline_variance_by_project(project_ids),
|
|
172
|
+
effective_policy,
|
|
173
|
+
task_weights,
|
|
174
|
+
),
|
|
175
|
+
}
|
|
176
|
+
elif kpi == RollupKpi.SCHEDULE_VARIANCE.value:
|
|
177
|
+
kpis[kpi] = {
|
|
178
|
+
"available": True,
|
|
179
|
+
"unit": "calendar_days",
|
|
180
|
+
"value": _reduce_variance(
|
|
181
|
+
_schedule_variance_by_project(project_ids),
|
|
182
|
+
effective_policy,
|
|
183
|
+
task_weights,
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"aggregation_policy": policy,
|
|
189
|
+
"policy_available": policy_available,
|
|
190
|
+
"project_count": len(project_ids),
|
|
191
|
+
"program_health": program_health,
|
|
192
|
+
"kpis": kpis,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def top_contributing_project(program: Program) -> dict[str, Any] | None:
|
|
197
|
+
"""Return the project dragging ``program``'s health down, or ``None``.
|
|
198
|
+
|
|
199
|
+
"Top contributing" = worst :func:`_schedule_health_by_project` band, tie-broken
|
|
200
|
+
by the larger count of open critical-path tasks, then by name for stability.
|
|
201
|
+
Lives here rather than in the digest builder (ADR-0663) so it reuses the *same*
|
|
202
|
+
per-project bands the Program Overview renders — a digest that named a different
|
|
203
|
+
project than the screen it deep-links to would be worse than no digest at all.
|
|
204
|
+
|
|
205
|
+
Returns ``{"id", "name", "health"}`` for the worst project, or ``None`` when the
|
|
206
|
+
program has no projects or every project's band is ``unknown`` (no data to
|
|
207
|
+
attribute the program's state to).
|
|
208
|
+
"""
|
|
209
|
+
today = timezone.localdate()
|
|
210
|
+
# Drafts are excluded (#2962/#3128) for the same reason as compute_program_rollup
|
|
211
|
+
# above, and one more: this function names ONE project in the weekly program-health
|
|
212
|
+
# email. Attributing a program's health to a plan nobody has committed to would
|
|
213
|
+
# deep-link a PMO into a half-built schedule and call it the cause.
|
|
214
|
+
from trueppm_api.apps.projects.lifecycle import visible_projects
|
|
215
|
+
|
|
216
|
+
project_ids = list(
|
|
217
|
+
visible_projects(Project.objects.filter(program=program, is_deleted=False)).values_list(
|
|
218
|
+
"id", flat=True
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
if not project_ids:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
bands = _schedule_health_by_project(project_ids, today)
|
|
225
|
+
ranked = [(pid, band) for pid, band in bands.items() if band in _HEALTH_ORDINAL]
|
|
226
|
+
if not ranked:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# Leaf rows only (#4276): a phase's `is_critical` is a rollup of its children
|
|
230
|
+
# (ADR-0024/ADR-0293, `_rollup_one_summary`), not a fact about the phase row
|
|
231
|
+
# itself, so counting the phase on top of the leaf whose value it rolled up
|
|
232
|
+
# double-counts that leaf and can tip the worst-project tie-break. See
|
|
233
|
+
# task_is_phase_expr's docstring.
|
|
234
|
+
critical_counts = {
|
|
235
|
+
r["project_id"]: r["c"]
|
|
236
|
+
for r in (
|
|
237
|
+
Task.objects.filter(project_id__in=project_ids, is_deleted=False, is_critical=True)
|
|
238
|
+
.exclude(status__in=_ACTIVE_EXCLUDE)
|
|
239
|
+
.annotate(_is_phase=task_is_phase_expr())
|
|
240
|
+
.filter(_is_phase=False)
|
|
241
|
+
.values("project_id")
|
|
242
|
+
.annotate(c=Count("id"))
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
names = dict(Project.objects.filter(id__in=project_ids).values_list("id", "name"))
|
|
246
|
+
|
|
247
|
+
# Sort by (health ordinal asc = worst first, critical count desc, name asc).
|
|
248
|
+
ranked.sort(
|
|
249
|
+
key=lambda pair: (
|
|
250
|
+
_HEALTH_ORDINAL[pair[1]],
|
|
251
|
+
-critical_counts.get(pair[0], 0),
|
|
252
|
+
names.get(pair[0], ""),
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
worst_id, worst_band = ranked[0]
|
|
256
|
+
return {"id": worst_id, "name": names.get(worst_id, ""), "health": worst_band}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Per-project metric maps (grouped queries — no per-project loop)
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _committed_task_counts(project_ids: list[Any]) -> dict[Any, int]:
|
|
265
|
+
"""project_id → committed (non-BACKLOG, non-deleted) task count, for weighting."""
|
|
266
|
+
if not project_ids:
|
|
267
|
+
return {}
|
|
268
|
+
rows = (
|
|
269
|
+
Task.objects.filter(project_id__in=project_ids, is_deleted=False)
|
|
270
|
+
.exclude(status=TaskStatus.BACKLOG)
|
|
271
|
+
.values("project_id")
|
|
272
|
+
.annotate(c=Count("id"))
|
|
273
|
+
)
|
|
274
|
+
return {r["project_id"]: r["c"] for r in rows}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def task_is_phase_expr() -> RawSQL:
|
|
278
|
+
"""Boolean expression: this task row is a phase (has a structural child).
|
|
279
|
+
|
|
280
|
+
A phase's status, percent, and dates are a rollup of its children and are
|
|
281
|
+
never set directly (ADR-0293), so the *stored* ``status`` on a phase row is
|
|
282
|
+
not a fact about the work — a seeded or imported phase sits at
|
|
283
|
+
``NOT_STARTED`` while every child is done. Any count that reads ``status``
|
|
284
|
+
must therefore count leaf work only, or a phase whose children are all
|
|
285
|
+
complete reads as late (#4238). Same ltree shape as the ``is_phase``
|
|
286
|
+
annotation on the task list; subtask children do not make a phase.
|
|
287
|
+
"""
|
|
288
|
+
# nosemgrep: avoid-raw-sql — static SQL literal, no params interpolated.
|
|
289
|
+
return RawSQL(
|
|
290
|
+
"EXISTS("
|
|
291
|
+
" SELECT 1 FROM projects_task c"
|
|
292
|
+
" WHERE c.project_id = projects_task.project_id"
|
|
293
|
+
" AND c.is_deleted = false"
|
|
294
|
+
" AND c.is_subtask = false"
|
|
295
|
+
" AND c.id != projects_task.id"
|
|
296
|
+
" AND c.wbs_path IS NOT NULL"
|
|
297
|
+
" AND projects_task.wbs_path IS NOT NULL"
|
|
298
|
+
" AND c.wbs_path ~ (projects_task.wbs_path::text || '.*{1}')::lquery"
|
|
299
|
+
")",
|
|
300
|
+
[],
|
|
301
|
+
output_field=BooleanField(),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def drift_counts_by_project(
|
|
306
|
+
project_ids: Collection[Any], today: datetime.date
|
|
307
|
+
) -> dict[Any, tuple[int, int]]:
|
|
308
|
+
"""project_id → ``(overdue_count, at_risk_count)`` for the program's project roster.
|
|
309
|
+
|
|
310
|
+
Same semantics as ``ProgramViewSet.projects`` (#560): incomplete tasks only
|
|
311
|
+
(``status`` != COMPLETE), leaf rows only. A phase's ``total_float`` and
|
|
312
|
+
``early_finish`` are rollups of its children
|
|
313
|
+
(``scheduling.services._rollup_one_summary``) — not facts about the phase row
|
|
314
|
+
itself (ADR-0024/ADR-0293) — so counting the phase on top of the leaf whose
|
|
315
|
+
value it rolled up double-counts that leaf (#4276). See :func:`task_is_phase_expr`.
|
|
316
|
+
|
|
317
|
+
Built as a grouped query over ``Task`` rather than a reverse-FK
|
|
318
|
+
``Count("tasks", filter=...)`` annotation — or a per-row correlated
|
|
319
|
+
``Subquery`` — on a ``Project`` queryset, for the same reason as
|
|
320
|
+
:func:`risk_counts_by_project`: :func:`task_is_phase_expr`'s ``RawSQL`` names
|
|
321
|
+
its own FROM table (``projects_task``) literally, and that bare name is not
|
|
322
|
+
guaranteed once more than one reference to ``Task`` shares the same compiled
|
|
323
|
+
statement. A *joined* reverse-FK Count aliases the join; a *second*
|
|
324
|
+
correlated ``Subquery`` over ``Task`` in the same ``.annotate()`` call hits
|
|
325
|
+
the identical problem the other way (Django assigns ``U0``/``U1`` to
|
|
326
|
+
disambiguate the two subqueries' own FROM clauses) — confirmed by a failing
|
|
327
|
+
``invalid reference to FROM-clause entry for table "projects_task"`` when
|
|
328
|
+
``overdue_count`` and ``at_risk_count`` were each tried as their own
|
|
329
|
+
Subquery here. Keeping ``Task`` as the base model of one standalone query,
|
|
330
|
+
same shape as :func:`spi_counts_by_project`/:func:`risk_counts_by_project`,
|
|
331
|
+
side-steps both failure modes.
|
|
332
|
+
|
|
333
|
+
Projects with no qualifying leaf work are absent from the dict — callers
|
|
334
|
+
read with ``.get(pid, (0, 0))``.
|
|
335
|
+
"""
|
|
336
|
+
if not project_ids:
|
|
337
|
+
return {}
|
|
338
|
+
incomplete = ~Q(status=TaskStatus.COMPLETE)
|
|
339
|
+
rows = (
|
|
340
|
+
Task.objects.filter(project_id__in=list(project_ids), is_deleted=False)
|
|
341
|
+
.filter(incomplete)
|
|
342
|
+
# Narrow before the correlated is_phase EXISTS runs (same reasoning as
|
|
343
|
+
# risk_counts_by_project's round-2 fix, #4250): a row that cannot
|
|
344
|
+
# contribute to either Count below no matter what _is_phase turns out
|
|
345
|
+
# to be is excluded first, so the per-row ltree EXISTS only runs for
|
|
346
|
+
# rows whose answer can matter.
|
|
347
|
+
.filter(Q(early_finish__lt=today) | (Q(total_float__isnull=False) & Q(total_float__lte=5)))
|
|
348
|
+
.annotate(_is_phase=task_is_phase_expr())
|
|
349
|
+
.filter(_is_phase=False)
|
|
350
|
+
.values("project_id")
|
|
351
|
+
.annotate(
|
|
352
|
+
overdue=Count("id", filter=Q(early_finish__lt=today)),
|
|
353
|
+
at_risk=Count("id", filter=Q(total_float__isnull=False) & Q(total_float__lte=5)),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
return {r["project_id"]: (r["overdue"], r["at_risk"]) for r in rows}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def spi_counts_by_project(
|
|
360
|
+
project_ids: Collection[Any], today: datetime.date
|
|
361
|
+
) -> dict[Any, tuple[int, int]]:
|
|
362
|
+
"""project_id → ``(planned, planned_complete)`` for the SPI proxy.
|
|
363
|
+
|
|
364
|
+
The single source for both the project overview card
|
|
365
|
+
(``_project_spi_and_health``) and the program rollup, so the two can never
|
|
366
|
+
disagree about the same project.
|
|
367
|
+
|
|
368
|
+
**Baseline path — EVM semantics (#398), ``has_cpm_dates=True`` only (#4242).**
|
|
369
|
+
``planned`` is the active baseline's rows with ``finish <= today``;
|
|
370
|
+
``planned_complete`` is *every* task complete by today. The numerator is
|
|
371
|
+
deliberately wider than the denominator: work finished ahead of its baselined
|
|
372
|
+
date is earned value, so SPI can exceed 1.0. This path requires the active
|
|
373
|
+
baseline to carry real dates: a baseline captured with ``has_cpm_dates=False``
|
|
374
|
+
(snapshot taken before the CPM engine first ran, see :class:`Baseline`) has
|
|
375
|
+
mostly-or-entirely null ``BaselineTask.finish`` rows, which collapses
|
|
376
|
+
``planned`` toward zero while ``planned_complete`` still counts every real
|
|
377
|
+
completion project-wide — an on-track-looking ratio over a near-empty
|
|
378
|
+
denominator next to a project that also reports late tasks (the hosted demo's
|
|
379
|
+
"GTM Readiness"/"Platform Core" symptom). A ``has_cpm_dates=False`` baseline is
|
|
380
|
+
therefore excluded from this path entirely (see the ``active_baseline`` query
|
|
381
|
+
below) and such a project falls through to the no-baseline path instead, per
|
|
382
|
+
the maintainer decision recorded on #4242: there is no fixed plan to be ahead
|
|
383
|
+
of until a baseline has real CPM dates.
|
|
384
|
+
|
|
385
|
+
**No-baseline path — one task set (#4238).** Both counts come from the *same*
|
|
386
|
+
rows: live leaf tasks (phase rows excluded, see :func:`task_is_phase_expr`)
|
|
387
|
+
whose CPM ``early_finish <= today``, and of those, the ones complete by today.
|
|
388
|
+
Without a (usable) baseline there is no fixed plan to be ahead of — the
|
|
389
|
+
reference is the live CPM forecast, and a COMPLETE task is pinned to its
|
|
390
|
+
actuals on the next run. A complete task *outside* that window therefore has a
|
|
391
|
+
null or stale ``early_finish``, not an ahead-of-plan finish, and counting it in
|
|
392
|
+
the numerator let it cancel out genuinely late rows: the overview read
|
|
393
|
+
``on_track`` / ``spi=1.0`` in a project that also reported late tasks. Scoped
|
|
394
|
+
this way the ratio cannot exceed 1.0 and drops for exactly the rows
|
|
395
|
+
``tasks_late_count`` counts (the same ``early_finish`` reference). A project
|
|
396
|
+
whose active baseline has ``has_cpm_dates=False`` is routed here too (#4242),
|
|
397
|
+
so the same cap applies to it.
|
|
398
|
+
|
|
399
|
+
A null ``actual_finish`` on a COMPLETE task still counts as complete by today —
|
|
400
|
+
keying on ``actual_finish`` stops a late completion masquerading as on-time,
|
|
401
|
+
but a missing stamp is not evidence of lateness.
|
|
402
|
+
|
|
403
|
+
Projects with no planned-by-today work are absent from the result.
|
|
404
|
+
"""
|
|
405
|
+
if not project_ids:
|
|
406
|
+
return {}
|
|
407
|
+
complete_by_today = Q(status=TaskStatus.COMPLETE) & (
|
|
408
|
+
Q(actual_finish__lte=today) | Q(actual_finish__isnull=True)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Active baseline per project (at most one — is_active is a per-project flag).
|
|
412
|
+
# has_cpm_dates=True only (#4242): a baseline snapshotted before CPM first ran
|
|
413
|
+
# has mostly-or-entirely null BaselineTask.finish, so it is not a usable EVM
|
|
414
|
+
# plan — see the docstring above. A has_cpm_dates=False active baseline is
|
|
415
|
+
# therefore treated as if the project had no active baseline at all, which
|
|
416
|
+
# routes it into the no_baseline path below.
|
|
417
|
+
active_baseline = dict(
|
|
418
|
+
Baseline.objects.filter(
|
|
419
|
+
project_id__in=list(project_ids),
|
|
420
|
+
is_active=True,
|
|
421
|
+
is_deleted=False,
|
|
422
|
+
has_cpm_dates=True,
|
|
423
|
+
).values_list("project_id", "id")
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
out: dict[Any, tuple[int, int]] = {}
|
|
427
|
+
|
|
428
|
+
if active_baseline:
|
|
429
|
+
baseline_to_project = {bid: pid for pid, bid in active_baseline.items()}
|
|
430
|
+
planned_by_project: dict[Any, int] = {}
|
|
431
|
+
for br in (
|
|
432
|
+
BaselineTask.objects.filter(
|
|
433
|
+
baseline_id__in=list(active_baseline.values()), finish__lte=today
|
|
434
|
+
)
|
|
435
|
+
.values("baseline_id")
|
|
436
|
+
.annotate(c=Count("id"))
|
|
437
|
+
):
|
|
438
|
+
planned_by_project[baseline_to_project[br["baseline_id"]]] = br["c"]
|
|
439
|
+
if planned_by_project:
|
|
440
|
+
completed_by_project = {
|
|
441
|
+
cr["project_id"]: cr["c"]
|
|
442
|
+
for cr in (
|
|
443
|
+
Task.objects.filter(project_id__in=list(planned_by_project), is_deleted=False)
|
|
444
|
+
.filter(complete_by_today)
|
|
445
|
+
.values("project_id")
|
|
446
|
+
.annotate(c=Count("id"))
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
for pid, planned in planned_by_project.items():
|
|
450
|
+
out[pid] = (planned, completed_by_project.get(pid, 0))
|
|
451
|
+
|
|
452
|
+
no_baseline = [pid for pid in project_ids if pid not in active_baseline]
|
|
453
|
+
if no_baseline:
|
|
454
|
+
for fr in (
|
|
455
|
+
Task.objects.filter(
|
|
456
|
+
project_id__in=no_baseline, is_deleted=False, early_finish__lte=today
|
|
457
|
+
)
|
|
458
|
+
.annotate(_is_phase=task_is_phase_expr())
|
|
459
|
+
.filter(_is_phase=False)
|
|
460
|
+
.values("project_id")
|
|
461
|
+
.annotate(planned=Count("id"), complete=Count("id", filter=complete_by_today))
|
|
462
|
+
):
|
|
463
|
+
out[fr["project_id"]] = (fr["planned"], fr["complete"])
|
|
464
|
+
|
|
465
|
+
return out
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def risk_counts_by_project(project_ids: Collection[Any]) -> dict[Any, tuple[int, int]]:
|
|
469
|
+
"""project_id → ``(at_risk_count, critical_count)`` for the status/health triage counts.
|
|
470
|
+
|
|
471
|
+
Same semantics as ``ProjectViewSet.status_summary``: incomplete tasks only
|
|
472
|
+
(``status`` != COMPLETE), leaf rows only. A phase's ``total_float`` and
|
|
473
|
+
``is_critical`` are rollups of its children
|
|
474
|
+
(``scheduling.services._rollup_one_summary``: ``total_float = min(child floats)``,
|
|
475
|
+
``is_critical = any(child.is_critical)``) — not facts about the phase row itself
|
|
476
|
+
(ADR-0024/ADR-0293) — so counting the phase on top of the leaf whose value it
|
|
477
|
+
rolled up double-counts that leaf (#4250). See :func:`task_is_phase_expr`.
|
|
478
|
+
|
|
479
|
+
Built as a grouped query over ``Task`` rather than a reverse-FK
|
|
480
|
+
``Count("tasks", filter=...)`` annotation on a ``Project`` queryset, because
|
|
481
|
+
:func:`task_is_phase_expr`'s ``RawSQL`` names its own FROM table
|
|
482
|
+
(``projects_task``) literally. On a ``Project`` queryset the join Django adds
|
|
483
|
+
for ``tasks`` is not guaranteed to resolve to that bare table name — it can be
|
|
484
|
+
aliased, particularly once a second filtered aggregate over the same relation
|
|
485
|
+
is added — so the exclusion could silently match against the wrong joined row
|
|
486
|
+
instead of raising. Keeping ``Task`` as the base model side-steps that
|
|
487
|
+
entirely; same shape as :func:`spi_counts_by_project`.
|
|
488
|
+
|
|
489
|
+
Projects with no incomplete leaf work are absent from the dict — callers read
|
|
490
|
+
with ``.get(pid, (0, 0))``.
|
|
491
|
+
"""
|
|
492
|
+
if not project_ids:
|
|
493
|
+
return {}
|
|
494
|
+
incomplete = ~Q(status=TaskStatus.COMPLETE)
|
|
495
|
+
rows = (
|
|
496
|
+
Task.objects.filter(project_id__in=list(project_ids), is_deleted=False)
|
|
497
|
+
# Narrow before the correlated is_phase EXISTS runs: a row that is
|
|
498
|
+
# complete, or incomplete but neither critical nor low-float, cannot
|
|
499
|
+
# contribute to either Count below no matter what _is_phase turns out
|
|
500
|
+
# to be, so excluding it here is semantics-preserving (NULL total_float
|
|
501
|
+
# and NULL is_critical already fail both conditions in SQL) and avoids
|
|
502
|
+
# evaluating the per-row ltree EXISTS for rows whose answer can't matter.
|
|
503
|
+
.filter(incomplete)
|
|
504
|
+
.filter(Q(is_critical=True) | Q(total_float__lte=5))
|
|
505
|
+
.annotate(_is_phase=task_is_phase_expr())
|
|
506
|
+
.filter(_is_phase=False)
|
|
507
|
+
.values("project_id")
|
|
508
|
+
.annotate(
|
|
509
|
+
at_risk=Count(
|
|
510
|
+
"id",
|
|
511
|
+
filter=Q(total_float__isnull=False) & Q(total_float__lte=5),
|
|
512
|
+
),
|
|
513
|
+
critical=Count("id", filter=Q(is_critical=True)),
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
return {r["project_id"]: (r["at_risk"], r["critical"]) for r in rows}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def spi_health_band(spi: float) -> str:
|
|
520
|
+
"""Map an SPI proxy onto the three health bands (shared with the overview card)."""
|
|
521
|
+
if spi >= 0.95:
|
|
522
|
+
return "on_track"
|
|
523
|
+
if spi >= 0.85:
|
|
524
|
+
return "at_risk"
|
|
525
|
+
return "critical"
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _schedule_health_by_project(project_ids: list[Any], today: datetime.date) -> dict[Any, str]:
|
|
529
|
+
"""project_id → SPI-proxy health band, matching ProjectOverviewView semantics.
|
|
530
|
+
|
|
531
|
+
Counts come from :func:`spi_counts_by_project`, the same function the project
|
|
532
|
+
overview uses. A project with no due-by-today work is ``unknown`` and is
|
|
533
|
+
excluded from the program reduce.
|
|
534
|
+
"""
|
|
535
|
+
counts = spi_counts_by_project(project_ids, today)
|
|
536
|
+
out: dict[Any, str] = {}
|
|
537
|
+
for pid in project_ids:
|
|
538
|
+
planned, complete = counts.get(pid, (0, 0))
|
|
539
|
+
out[pid] = "unknown" if planned <= 0 else spi_health_band(complete / planned)
|
|
540
|
+
return out
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _milestone_band(milestones: list[Any], today: datetime.date, soon: datetime.date) -> str:
|
|
544
|
+
"""Worst health band across one project's milestones.
|
|
545
|
+
|
|
546
|
+
``critical`` dominates ``at_risk``, so an overdue milestone short-circuits the
|
|
547
|
+
scan — no later milestone can improve the band. A milestone that is complete
|
|
548
|
+
or has no finish date contributes nothing either way.
|
|
549
|
+
"""
|
|
550
|
+
if not milestones:
|
|
551
|
+
return "unknown"
|
|
552
|
+
band = "on_track"
|
|
553
|
+
for m in milestones:
|
|
554
|
+
done = m["status"] == TaskStatus.COMPLETE or (m["percent_complete"] or 0) >= 100
|
|
555
|
+
ef = m["early_finish"]
|
|
556
|
+
if done or ef is None:
|
|
557
|
+
continue
|
|
558
|
+
if ef < today:
|
|
559
|
+
return "critical"
|
|
560
|
+
if ef <= soon:
|
|
561
|
+
band = "at_risk"
|
|
562
|
+
return band
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _milestone_health_by_project(project_ids: list[Any], today: datetime.date) -> dict[Any, str]:
|
|
566
|
+
"""project_id → milestone health band from milestone task dates.
|
|
567
|
+
|
|
568
|
+
critical if any milestone is overdue and not complete; at_risk if any
|
|
569
|
+
milestone is due within 7 days and not yet complete; on_track if the project
|
|
570
|
+
has milestones and none are late/at-risk; unknown if it has no milestones
|
|
571
|
+
(excluded from the reduce).
|
|
572
|
+
"""
|
|
573
|
+
if not project_ids:
|
|
574
|
+
return {}
|
|
575
|
+
grouped: dict[Any, list[Any]] = {pid: [] for pid in project_ids}
|
|
576
|
+
for r in Task.objects.filter(
|
|
577
|
+
project_id__in=project_ids, is_deleted=False, is_milestone=True
|
|
578
|
+
).values("project_id", "early_finish", "status", "percent_complete"):
|
|
579
|
+
grouped[r["project_id"]].append(r)
|
|
580
|
+
|
|
581
|
+
soon = today + datetime.timedelta(days=7)
|
|
582
|
+
return {pid: _milestone_band(ms, today, soon) for pid, ms in grouped.items()}
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _baseline_variance_by_project(project_ids: list[Any]) -> dict[Any, float]:
|
|
586
|
+
"""project_id → projected-end drift in calendar days vs the active baseline.
|
|
587
|
+
|
|
588
|
+
The current projected end (the latest ``Task.early_finish``) minus the baseline
|
|
589
|
+
end (the latest ``BaselineTask.finish``). Positive = the project is trending
|
|
590
|
+
later than baseline. Projects without an active baseline are absent from the
|
|
591
|
+
map (no comparison basis).
|
|
592
|
+
|
|
593
|
+
Measured in working time (#4197): each end is read with the edge of its day it
|
|
594
|
+
sits on (:func:`~trueppm_api.apps.scheduling.finish_reading.latest_finish`), so
|
|
595
|
+
a plan whose finishing milestone moves from the end of a Friday to the start of
|
|
596
|
+
the next Monday reports 0, not +3. Only the rows *on* each end day are read for
|
|
597
|
+
that — the max itself stays a grouped aggregate.
|
|
598
|
+
|
|
599
|
+
``has_cpm_dates=True`` only (#4242): a baseline snapshotted before CPM first
|
|
600
|
+
ran has mostly-or-entirely null ``BaselineTask.finish``, so ``Max("finish")``
|
|
601
|
+
is either ``None`` (already excluded below) or a date computed from an
|
|
602
|
+
incomplete subset of rows — not the baseline's real projected end. Same
|
|
603
|
+
exclusion as :func:`spi_counts_by_project`: such a project is treated as
|
|
604
|
+
having no active baseline, and is absent from the returned map.
|
|
605
|
+
"""
|
|
606
|
+
from trueppm_api.apps.scheduling.calendars import project_sched_calendars
|
|
607
|
+
from trueppm_api.apps.scheduling.finish_reading import (
|
|
608
|
+
FINISH_READING_FIELDS,
|
|
609
|
+
finish_shift_days,
|
|
610
|
+
latest_finish,
|
|
611
|
+
task_finish_at_day_start,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if not project_ids:
|
|
615
|
+
return {}
|
|
616
|
+
active_baseline = dict(
|
|
617
|
+
Baseline.objects.filter(
|
|
618
|
+
project_id__in=project_ids,
|
|
619
|
+
is_active=True,
|
|
620
|
+
is_deleted=False,
|
|
621
|
+
has_cpm_dates=True,
|
|
622
|
+
).values_list("project_id", "id")
|
|
623
|
+
)
|
|
624
|
+
if not active_baseline:
|
|
625
|
+
return {}
|
|
626
|
+
|
|
627
|
+
current_end = {
|
|
628
|
+
r["project_id"]: r["end"]
|
|
629
|
+
for r in Task.objects.filter(project_id__in=list(active_baseline.keys()), is_deleted=False)
|
|
630
|
+
.values("project_id")
|
|
631
|
+
.annotate(end=Max("early_finish"))
|
|
632
|
+
if r["end"] is not None
|
|
633
|
+
}
|
|
634
|
+
baseline_to_project = {bid: pid for pid, bid in active_baseline.items()}
|
|
635
|
+
baseline_end = {
|
|
636
|
+
baseline_to_project[r["baseline_id"]]: r["end"]
|
|
637
|
+
for r in BaselineTask.objects.filter(baseline_id__in=list(active_baseline.values()))
|
|
638
|
+
.values("baseline_id")
|
|
639
|
+
.annotate(end=Max("finish"))
|
|
640
|
+
if r["end"] is not None
|
|
641
|
+
}
|
|
642
|
+
compared = [pid for pid in active_baseline if pid in current_end and pid in baseline_end]
|
|
643
|
+
if not compared:
|
|
644
|
+
return {}
|
|
645
|
+
|
|
646
|
+
# The rows ON each end day, with their reading (#4197). ``__in`` over the end
|
|
647
|
+
# days can over-select another project's day; the equality check drops those.
|
|
648
|
+
current_rows: dict[Any, list[tuple[Any, bool, Any]]] = {}
|
|
649
|
+
for t in Task.objects.filter(
|
|
650
|
+
project_id__in=compared,
|
|
651
|
+
is_deleted=False,
|
|
652
|
+
early_finish__in={current_end[pid] for pid in compared},
|
|
653
|
+
).only("project_id", "early_finish", "wbs_path", *FINISH_READING_FIELDS):
|
|
654
|
+
if t.early_finish == current_end[t.project_id]:
|
|
655
|
+
current_rows.setdefault(t.project_id, []).append(
|
|
656
|
+
(t.early_finish, task_finish_at_day_start(t), t.wbs_path)
|
|
657
|
+
)
|
|
658
|
+
base_rows_raw = [
|
|
659
|
+
r
|
|
660
|
+
for r in BaselineTask.objects.filter(
|
|
661
|
+
baseline_id__in=[active_baseline[pid] for pid in compared],
|
|
662
|
+
finish__in={baseline_end[pid] for pid in compared},
|
|
663
|
+
).values("baseline_id", "task_id", "finish", "finish_at_day_start")
|
|
664
|
+
if r["finish"] == baseline_end[baseline_to_project[r["baseline_id"]]]
|
|
665
|
+
]
|
|
666
|
+
# A baseline row carries no WBS path; the task's (live or soft-deleted) path is
|
|
667
|
+
# what lets latest_finish drop a summary row sharing its leaf's end day.
|
|
668
|
+
wbs_by_task = dict(
|
|
669
|
+
Task.objects.filter(id__in={r["task_id"] for r in base_rows_raw}).values_list(
|
|
670
|
+
"id", "wbs_path"
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
base_rows: dict[Any, list[tuple[Any, bool, Any]]] = {}
|
|
674
|
+
base_unknown: set[Any] = set()
|
|
675
|
+
for r in base_rows_raw:
|
|
676
|
+
pid = baseline_to_project[r["baseline_id"]]
|
|
677
|
+
if r["finish_at_day_start"] is None:
|
|
678
|
+
base_unknown.add(pid)
|
|
679
|
+
base_rows.setdefault(pid, []).append(
|
|
680
|
+
(r["finish"], bool(r["finish_at_day_start"]), wbs_by_task.get(r["task_id"]))
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
readings: dict[Any, tuple[Any, Any]] = {}
|
|
684
|
+
for pid in compared:
|
|
685
|
+
cur = latest_finish(current_rows.get(pid, [])) or (current_end[pid], False)
|
|
686
|
+
base = latest_finish(base_rows.get(pid, [])) or (baseline_end[pid], None)
|
|
687
|
+
if pid in base_unknown:
|
|
688
|
+
# A baseline captured before the reading was recorded (#4197): unknown.
|
|
689
|
+
base = (base[0], None)
|
|
690
|
+
readings[pid] = (base, cur)
|
|
691
|
+
calendars = project_sched_calendars(
|
|
692
|
+
pid for pid, (base, cur) in readings.items() if base[1] or cur[1]
|
|
693
|
+
)
|
|
694
|
+
return {
|
|
695
|
+
pid: float(finish_shift_days(base, cur, calendars.get(str(pid))))
|
|
696
|
+
for pid, (base, cur) in readings.items()
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _schedule_variance_by_project(project_ids: list[Any]) -> dict[Any, float]:
|
|
701
|
+
"""project_id → mean lateness of completed work, in calendar days.
|
|
702
|
+
|
|
703
|
+
Per-task ``actual_finish − baseline_finish`` (the quantity serializers
|
|
704
|
+
``get_schedule_variance_days`` reports per task) averaged over the project's
|
|
705
|
+
completed tasks that exist in the active baseline. Distinct from
|
|
706
|
+
``baseline_variance``: SV measures *how late finished work landed*, not where
|
|
707
|
+
the project end is heading. Absent for projects without an active baseline or
|
|
708
|
+
with no matched completed work.
|
|
709
|
+
|
|
710
|
+
Each delta is measured in working time (#4197): an actual finish is a recorded
|
|
711
|
+
day, read as its end, and a baselined start-of-day milestone finish is read as
|
|
712
|
+
the end of the working day before it — so a milestone baselined at the start
|
|
713
|
+
of a Monday and hit on the Friday before landed on time, not three days early.
|
|
714
|
+
|
|
715
|
+
``has_cpm_dates=True`` only (#4242): same exclusion as
|
|
716
|
+
:func:`spi_counts_by_project` and :func:`_baseline_variance_by_project` — a
|
|
717
|
+
baseline snapshotted before CPM first ran has mostly-or-entirely null
|
|
718
|
+
``BaselineTask.finish`` and is not a usable plan to measure lateness against.
|
|
719
|
+
"""
|
|
720
|
+
from trueppm_api.apps.scheduling.calendars import project_sched_calendars
|
|
721
|
+
from trueppm_api.apps.scheduling.finish_reading import finish_shift_days
|
|
722
|
+
|
|
723
|
+
if not project_ids:
|
|
724
|
+
return {}
|
|
725
|
+
active_baseline = dict(
|
|
726
|
+
Baseline.objects.filter(
|
|
727
|
+
project_id__in=project_ids,
|
|
728
|
+
is_active=True,
|
|
729
|
+
is_deleted=False,
|
|
730
|
+
has_cpm_dates=True,
|
|
731
|
+
).values_list("project_id", "id")
|
|
732
|
+
)
|
|
733
|
+
if not active_baseline:
|
|
734
|
+
return {}
|
|
735
|
+
|
|
736
|
+
# Baseline finish, with its edge of the day, per (project, task_id).
|
|
737
|
+
baseline_to_project = {bid: pid for pid, bid in active_baseline.items()}
|
|
738
|
+
baseline_finish: dict[tuple[Any, Any], tuple[datetime.date, bool | None]] = {}
|
|
739
|
+
for bt in BaselineTask.objects.filter(
|
|
740
|
+
baseline_id__in=list(active_baseline.values()), finish__isnull=False
|
|
741
|
+
).values("baseline_id", "task_id", "finish", "finish_at_day_start"):
|
|
742
|
+
if bt["finish"] is None: # narrows for mypy; excluded by the filter
|
|
743
|
+
continue
|
|
744
|
+
baseline_finish[(baseline_to_project[bt["baseline_id"]], bt["task_id"])] = (
|
|
745
|
+
bt["finish"],
|
|
746
|
+
bt["finish_at_day_start"],
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Completed tasks with an actual finish.
|
|
750
|
+
pairs: dict[Any, list[tuple[tuple[datetime.date, bool | None], datetime.date]]] = {}
|
|
751
|
+
for t in Task.objects.filter(
|
|
752
|
+
project_id__in=list(active_baseline.keys()),
|
|
753
|
+
is_deleted=False,
|
|
754
|
+
status=TaskStatus.COMPLETE,
|
|
755
|
+
actual_finish__isnull=False,
|
|
756
|
+
).values("id", "project_id", "actual_finish"):
|
|
757
|
+
base = baseline_finish.get((t["project_id"], t["id"]))
|
|
758
|
+
actual = t["actual_finish"]
|
|
759
|
+
if base is not None and actual is not None:
|
|
760
|
+
pairs.setdefault(t["project_id"], []).append((base, actual))
|
|
761
|
+
|
|
762
|
+
# A calendar only for the projects with a start-of-day baseline finish; an
|
|
763
|
+
# end-of-day pair diffs identically without one.
|
|
764
|
+
calendars = project_sched_calendars(
|
|
765
|
+
pid for pid, ps in pairs.items() if any(base[1] for base, _ in ps)
|
|
766
|
+
)
|
|
767
|
+
deltas = {
|
|
768
|
+
pid: [
|
|
769
|
+
finish_shift_days(base, (actual, False), calendars.get(str(pid))) for base, actual in ps
|
|
770
|
+
]
|
|
771
|
+
for pid, ps in pairs.items()
|
|
772
|
+
}
|
|
773
|
+
return {pid: sum(ds) / len(ds) for pid, ds in deltas.items() if ds}
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
# ---------------------------------------------------------------------------
|
|
777
|
+
# Program-total KPIs (policy-independent)
|
|
778
|
+
# ---------------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _critical_task_total(project_ids: list[Any]) -> int:
|
|
782
|
+
"""Total open critical-path tasks across the program.
|
|
783
|
+
|
|
784
|
+
Leaf rows only (#4276): a phase's `is_critical` is a rollup of its children
|
|
785
|
+
(ADR-0024/ADR-0293, `_rollup_one_summary`: `is_critical = any(child.is_critical)`),
|
|
786
|
+
not a fact about the phase row itself, so counting the phase on top of the
|
|
787
|
+
leaf whose value it rolled up double-counts that leaf and inflates this
|
|
788
|
+
program-total KPI. See :func:`task_is_phase_expr`.
|
|
789
|
+
"""
|
|
790
|
+
if not project_ids:
|
|
791
|
+
return 0
|
|
792
|
+
return (
|
|
793
|
+
Task.objects.filter(project_id__in=project_ids, is_deleted=False, is_critical=True)
|
|
794
|
+
.exclude(status__in=_ACTIVE_EXCLUDE)
|
|
795
|
+
.annotate(_is_phase=task_is_phase_expr())
|
|
796
|
+
.filter(_is_phase=False)
|
|
797
|
+
.count()
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _at_risk_task_total(project_ids: list[Any]) -> int:
|
|
802
|
+
"""Total open tasks with ≤ 5 working days of float across the program.
|
|
803
|
+
|
|
804
|
+
Leaf rows only (#4276), same rollup reasoning as :func:`_critical_task_total`:
|
|
805
|
+
a phase's `total_float` is `min(child floats)` (ADR-0024/ADR-0293), not a fact
|
|
806
|
+
about the phase row itself.
|
|
807
|
+
"""
|
|
808
|
+
if not project_ids:
|
|
809
|
+
return 0
|
|
810
|
+
return (
|
|
811
|
+
Task.objects.filter(
|
|
812
|
+
project_id__in=project_ids,
|
|
813
|
+
is_deleted=False,
|
|
814
|
+
total_float__isnull=False,
|
|
815
|
+
total_float__lte=5,
|
|
816
|
+
)
|
|
817
|
+
.exclude(status__in=_ACTIVE_EXCLUDE)
|
|
818
|
+
.annotate(_is_phase=task_is_phase_expr())
|
|
819
|
+
.filter(_is_phase=False)
|
|
820
|
+
.count()
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _risk_score_total(project_ids: list[Any]) -> int:
|
|
825
|
+
"""Total open-risk exposure (Σ probability × impact) across the program."""
|
|
826
|
+
if not project_ids:
|
|
827
|
+
return 0
|
|
828
|
+
agg = Risk.objects.filter(
|
|
829
|
+
project_id__in=project_ids,
|
|
830
|
+
status__in=[RiskStatus.OPEN, RiskStatus.MITIGATING],
|
|
831
|
+
).aggregate(score=Sum(F("probability") * F("impact")))
|
|
832
|
+
return int(agg["score"] or 0)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
# ---------------------------------------------------------------------------
|
|
836
|
+
# Reducers
|
|
837
|
+
# ---------------------------------------------------------------------------
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _reduce_health(
|
|
841
|
+
bands: list[str], policy: str, task_weights: dict[Any, int], project_ids: list[Any]
|
|
842
|
+
) -> str:
|
|
843
|
+
"""Combine per-project health bands into one band under the policy.
|
|
844
|
+
|
|
845
|
+
``unknown`` bands are dropped before reducing (a project with no data must
|
|
846
|
+
not make the program look worse). Returns ``unknown`` when every project is
|
|
847
|
+
unknown or there are no projects.
|
|
848
|
+
|
|
849
|
+
Note: task-weighting health requires aligning weights to bands positionally,
|
|
850
|
+
so the caller passes bands in ``project_ids`` order for ``task_weighted``.
|
|
851
|
+
"""
|
|
852
|
+
ordinals = [_HEALTH_ORDINAL[b] for b in bands if b in _HEALTH_ORDINAL]
|
|
853
|
+
if not ordinals:
|
|
854
|
+
return "unknown"
|
|
855
|
+
|
|
856
|
+
if policy == AggregationPolicy.WORST.value:
|
|
857
|
+
return _ORDINAL_TO_HEALTH[min(ordinals)]
|
|
858
|
+
if policy == AggregationPolicy.TASK_WEIGHTED.value:
|
|
859
|
+
# Weight each project's band by its committed task count. bands is in
|
|
860
|
+
# project_ids order; pair them and skip unknowns.
|
|
861
|
+
weighted_sum = 0.0
|
|
862
|
+
weight_total = 0.0
|
|
863
|
+
for pid, band in zip(project_ids, bands, strict=False):
|
|
864
|
+
if band not in _HEALTH_ORDINAL:
|
|
865
|
+
continue
|
|
866
|
+
w = task_weights.get(pid, 0) or 1 # a zero-task project still counts once
|
|
867
|
+
weighted_sum += _HEALTH_ORDINAL[band] * w
|
|
868
|
+
weight_total += w
|
|
869
|
+
if weight_total == 0:
|
|
870
|
+
return _ORDINAL_TO_HEALTH[round(sum(ordinals) / len(ordinals))]
|
|
871
|
+
return _ORDINAL_TO_HEALTH[round(weighted_sum / weight_total)]
|
|
872
|
+
# AVERAGE (and the weighted_by_budget fallback).
|
|
873
|
+
return _ORDINAL_TO_HEALTH[round(sum(ordinals) / len(ordinals))]
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _reduce_variance(
|
|
877
|
+
by_project: dict[Any, float], policy: str, task_weights: dict[Any, int]
|
|
878
|
+
) -> float | None:
|
|
879
|
+
"""Combine per-project day-variances under the policy.
|
|
880
|
+
|
|
881
|
+
``worst`` = the largest slip (the project dragging the program). ``average``
|
|
882
|
+
= arithmetic mean. ``task_weighted`` = mean weighted by committed task count.
|
|
883
|
+
Returns ``None`` when no project has a value (e.g. no active baselines), which
|
|
884
|
+
the serializer renders as "—".
|
|
885
|
+
"""
|
|
886
|
+
if not by_project:
|
|
887
|
+
return None
|
|
888
|
+
values = list(by_project.values())
|
|
889
|
+
|
|
890
|
+
if policy == AggregationPolicy.WORST.value:
|
|
891
|
+
return round(max(values), 1)
|
|
892
|
+
if policy == AggregationPolicy.TASK_WEIGHTED.value:
|
|
893
|
+
weighted_sum = 0.0
|
|
894
|
+
weight_total = 0.0
|
|
895
|
+
for pid, val in by_project.items():
|
|
896
|
+
w = task_weights.get(pid, 0) or 1
|
|
897
|
+
weighted_sum += val * w
|
|
898
|
+
weight_total += w
|
|
899
|
+
if weight_total == 0:
|
|
900
|
+
return round(sum(values) / len(values), 1)
|
|
901
|
+
return round(weighted_sum / weight_total, 1)
|
|
902
|
+
# AVERAGE (and the weighted_by_budget fallback).
|
|
903
|
+
return round(sum(values) / len(values), 1)
|