trueppm-api 0.4.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (759) hide show
  1. trueppm_api/__init__.py +14 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +164 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  24. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  25. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  26. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  27. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
  28. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  29. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  30. trueppm_api/apps/access/migrations/__init__.py +0 -0
  31. trueppm_api/apps/access/models.py +588 -0
  32. trueppm_api/apps/access/permissions.py +2837 -0
  33. trueppm_api/apps/access/serializers.py +946 -0
  34. trueppm_api/apps/access/services.py +667 -0
  35. trueppm_api/apps/access/signals.py +103 -0
  36. trueppm_api/apps/access/tasks.py +50 -0
  37. trueppm_api/apps/access/throttles.py +95 -0
  38. trueppm_api/apps/access/urls.py +191 -0
  39. trueppm_api/apps/access/views.py +1931 -0
  40. trueppm_api/apps/agents/__init__.py +6 -0
  41. trueppm_api/apps/agents/apps.py +11 -0
  42. trueppm_api/apps/agents/canonical.py +73 -0
  43. trueppm_api/apps/agents/deferred.py +108 -0
  44. trueppm_api/apps/agents/management/__init__.py +0 -0
  45. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  46. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  47. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  48. trueppm_api/apps/agents/middleware.py +44 -0
  49. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  50. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  51. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  52. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  53. trueppm_api/apps/agents/models.py +371 -0
  54. trueppm_api/apps/agents/refusal.py +103 -0
  55. trueppm_api/apps/agents/serializers.py +85 -0
  56. trueppm_api/apps/agents/services.py +397 -0
  57. trueppm_api/apps/agents/signals.py +30 -0
  58. trueppm_api/apps/agents/urls.py +15 -0
  59. trueppm_api/apps/agents/views.py +108 -0
  60. trueppm_api/apps/csvimport/__init__.py +12 -0
  61. trueppm_api/apps/csvimport/apps.py +11 -0
  62. trueppm_api/apps/csvimport/mapping.py +363 -0
  63. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  64. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  65. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  66. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  67. trueppm_api/apps/csvimport/models.py +111 -0
  68. trueppm_api/apps/csvimport/parser.py +1825 -0
  69. trueppm_api/apps/csvimport/services.py +48 -0
  70. trueppm_api/apps/csvimport/tasks.py +418 -0
  71. trueppm_api/apps/csvimport/template.py +37 -0
  72. trueppm_api/apps/csvimport/urls.py +50 -0
  73. trueppm_api/apps/csvimport/views.py +793 -0
  74. trueppm_api/apps/history/__init__.py +0 -0
  75. trueppm_api/apps/history/apps.py +12 -0
  76. trueppm_api/apps/history/changelog.py +359 -0
  77. trueppm_api/apps/history/diff_policy.py +177 -0
  78. trueppm_api/apps/history/serializers.py +104 -0
  79. trueppm_api/apps/history/signals.py +82 -0
  80. trueppm_api/apps/history/tasks.py +105 -0
  81. trueppm_api/apps/history/urls.py +38 -0
  82. trueppm_api/apps/history/views.py +443 -0
  83. trueppm_api/apps/idempotency/__init__.py +0 -0
  84. trueppm_api/apps/idempotency/apps.py +11 -0
  85. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  86. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  87. trueppm_api/apps/idempotency/mixins.py +226 -0
  88. trueppm_api/apps/idempotency/models.py +75 -0
  89. trueppm_api/apps/idempotency/tasks.py +61 -0
  90. trueppm_api/apps/integrations/__init__.py +13 -0
  91. trueppm_api/apps/integrations/apps.py +59 -0
  92. trueppm_api/apps/integrations/connections.py +806 -0
  93. trueppm_api/apps/integrations/encryption.py +92 -0
  94. trueppm_api/apps/integrations/external_sources.py +837 -0
  95. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  96. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  97. trueppm_api/apps/integrations/http.py +403 -0
  98. trueppm_api/apps/integrations/me_work.py +218 -0
  99. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  100. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  101. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  102. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  103. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  104. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  105. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  106. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  107. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  108. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  109. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  110. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  111. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  112. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  113. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  114. trueppm_api/apps/integrations/models.py +622 -0
  115. trueppm_api/apps/integrations/notification_channels.py +57 -0
  116. trueppm_api/apps/integrations/opengraph.py +229 -0
  117. trueppm_api/apps/integrations/outgoing.py +201 -0
  118. trueppm_api/apps/integrations/providers.py +519 -0
  119. trueppm_api/apps/integrations/registry.py +345 -0
  120. trueppm_api/apps/integrations/serializers.py +453 -0
  121. trueppm_api/apps/integrations/services.py +171 -0
  122. trueppm_api/apps/integrations/tasks.py +731 -0
  123. trueppm_api/apps/integrations/throttles.py +165 -0
  124. trueppm_api/apps/integrations/urls.py +77 -0
  125. trueppm_api/apps/integrations/views.py +1106 -0
  126. trueppm_api/apps/jiraimport/__init__.py +8 -0
  127. trueppm_api/apps/jiraimport/apps.py +11 -0
  128. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  129. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  130. trueppm_api/apps/jiraimport/models.py +70 -0
  131. trueppm_api/apps/jiraimport/parser.py +350 -0
  132. trueppm_api/apps/jiraimport/services.py +46 -0
  133. trueppm_api/apps/jiraimport/tasks.py +277 -0
  134. trueppm_api/apps/jiraimport/urls.py +15 -0
  135. trueppm_api/apps/jiraimport/views.py +153 -0
  136. trueppm_api/apps/msproject/__init__.py +0 -0
  137. trueppm_api/apps/msproject/apps.py +11 -0
  138. trueppm_api/apps/msproject/dataclasses.py +153 -0
  139. trueppm_api/apps/msproject/exporter.py +500 -0
  140. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  141. trueppm_api/apps/msproject/importer.py +1095 -0
  142. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  143. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  144. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  145. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  146. trueppm_api/apps/msproject/models.py +81 -0
  147. trueppm_api/apps/msproject/parser.py +1231 -0
  148. trueppm_api/apps/msproject/serializers.py +136 -0
  149. trueppm_api/apps/msproject/services.py +58 -0
  150. trueppm_api/apps/msproject/tasks.py +436 -0
  151. trueppm_api/apps/msproject/urls.py +39 -0
  152. trueppm_api/apps/msproject/views.py +531 -0
  153. trueppm_api/apps/notifications/__init__.py +7 -0
  154. trueppm_api/apps/notifications/apps.py +15 -0
  155. trueppm_api/apps/notifications/backfill.py +79 -0
  156. trueppm_api/apps/notifications/categories.py +112 -0
  157. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  158. trueppm_api/apps/notifications/digests.py +447 -0
  159. trueppm_api/apps/notifications/email_backend.py +330 -0
  160. trueppm_api/apps/notifications/email_health.py +128 -0
  161. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  162. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
  163. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  164. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  165. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  166. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  167. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  168. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  169. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  170. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  171. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  172. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  173. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  174. trueppm_api/apps/notifications/models.py +1197 -0
  175. trueppm_api/apps/notifications/receivers.py +155 -0
  176. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  177. trueppm_api/apps/notifications/serializers.py +961 -0
  178. trueppm_api/apps/notifications/services.py +1451 -0
  179. trueppm_api/apps/notifications/tasks.py +711 -0
  180. trueppm_api/apps/notifications/throttles.py +98 -0
  181. trueppm_api/apps/notifications/urls.py +97 -0
  182. trueppm_api/apps/notifications/views.py +904 -0
  183. trueppm_api/apps/observability/__init__.py +0 -0
  184. trueppm_api/apps/observability/apps.py +31 -0
  185. trueppm_api/apps/observability/logging.py +213 -0
  186. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  187. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  188. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  189. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  190. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  191. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  192. trueppm_api/apps/observability/models.py +141 -0
  193. trueppm_api/apps/observability/otel/__init__.py +45 -0
  194. trueppm_api/apps/observability/otel/attributes.py +247 -0
  195. trueppm_api/apps/observability/otel/export_health.py +533 -0
  196. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  197. trueppm_api/apps/observability/otel/metrics.py +551 -0
  198. trueppm_api/apps/observability/otel/provider.py +511 -0
  199. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  200. trueppm_api/apps/observability/purge_registry.py +124 -0
  201. trueppm_api/apps/observability/retention.py +139 -0
  202. trueppm_api/apps/observability/selectors.py +969 -0
  203. trueppm_api/apps/observability/serializers.py +142 -0
  204. trueppm_api/apps/observability/services.py +407 -0
  205. trueppm_api/apps/observability/tasks.py +266 -0
  206. trueppm_api/apps/observability/urls.py +27 -0
  207. trueppm_api/apps/observability/views.py +523 -0
  208. trueppm_api/apps/profiles/__init__.py +0 -0
  209. trueppm_api/apps/profiles/apps.py +15 -0
  210. trueppm_api/apps/profiles/constants.py +54 -0
  211. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  212. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  213. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  214. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  215. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  216. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  217. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  218. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  219. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  220. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  221. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  222. trueppm_api/apps/profiles/models.py +276 -0
  223. trueppm_api/apps/profiles/serializers.py +199 -0
  224. trueppm_api/apps/profiles/services.py +466 -0
  225. trueppm_api/apps/profiles/views.py +124 -0
  226. trueppm_api/apps/projects/__init__.py +0 -0
  227. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  228. trueppm_api/apps/projects/amend.py +133 -0
  229. trueppm_api/apps/projects/apps.py +38 -0
  230. trueppm_api/apps/projects/asset_feed.py +459 -0
  231. trueppm_api/apps/projects/asset_views.py +317 -0
  232. trueppm_api/apps/projects/attachment_policy.py +278 -0
  233. trueppm_api/apps/projects/authentication.py +449 -0
  234. trueppm_api/apps/projects/backfill.py +287 -0
  235. trueppm_api/apps/projects/backlog_services.py +324 -0
  236. trueppm_api/apps/projects/backlog_views.py +302 -0
  237. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  238. trueppm_api/apps/projects/blocker_services.py +510 -0
  239. trueppm_api/apps/projects/board_activity.py +462 -0
  240. trueppm_api/apps/projects/board_activity_views.py +186 -0
  241. trueppm_api/apps/projects/board_lanes.py +134 -0
  242. trueppm_api/apps/projects/bulk_settings.py +194 -0
  243. trueppm_api/apps/projects/bundled_templates.py +215 -0
  244. trueppm_api/apps/projects/calendar_settings.py +180 -0
  245. trueppm_api/apps/projects/ceremony_views.py +244 -0
  246. trueppm_api/apps/projects/commit_moment.py +204 -0
  247. trueppm_api/apps/projects/config_notice.py +1325 -0
  248. trueppm_api/apps/projects/custom_field_values.py +266 -0
  249. trueppm_api/apps/projects/decisions_services.py +93 -0
  250. trueppm_api/apps/projects/decisions_views.py +94 -0
  251. trueppm_api/apps/projects/estimation_scale.py +126 -0
  252. trueppm_api/apps/projects/export_bundle.py +353 -0
  253. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
  254. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
  255. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
  256. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
  257. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
  258. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  259. trueppm_api/apps/projects/inbound_sync.py +555 -0
  260. trueppm_api/apps/projects/iteration_label.py +85 -0
  261. trueppm_api/apps/projects/lifecycle.py +191 -0
  262. trueppm_api/apps/projects/management/__init__.py +0 -0
  263. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  264. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  265. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  266. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  267. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  268. trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
  269. trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
  270. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
  271. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  272. trueppm_api/apps/projects/mcp_settings.py +305 -0
  273. trueppm_api/apps/projects/methodology.py +178 -0
  274. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  275. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
  276. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  277. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  278. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  279. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  280. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  281. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  282. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  283. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  284. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  285. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  286. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  287. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  288. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  289. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  290. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  291. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  292. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  293. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  294. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  295. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  296. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  297. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  298. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  299. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  300. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  301. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  302. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  303. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  304. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  305. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  306. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  307. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  308. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  309. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  310. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  311. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  312. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  313. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  314. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  315. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  316. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  317. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  318. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  319. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  320. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  321. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  322. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  323. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  324. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  325. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  326. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  327. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  328. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  329. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  330. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  331. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  332. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  333. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  334. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  335. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  336. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  337. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  338. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  339. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  340. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  341. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  342. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  343. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  344. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  345. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  346. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  347. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  348. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  349. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  350. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  351. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  352. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  353. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  354. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  355. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  356. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  357. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  358. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  359. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  360. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  361. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  362. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  363. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  364. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  365. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  366. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  367. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  368. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
  369. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  370. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  371. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  372. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  373. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  374. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  375. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  376. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  377. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  378. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  379. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
  380. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  381. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  382. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  383. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  384. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  385. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
  386. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  387. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  388. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  389. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  390. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  391. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  392. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  393. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  394. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  395. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  396. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  397. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  398. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  399. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  400. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  401. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  402. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  403. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  404. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  405. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  406. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  407. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  408. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  409. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  410. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  411. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  412. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  413. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  414. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  415. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  416. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  417. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  418. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  419. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  420. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  421. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  422. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  423. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  424. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  425. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  426. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  427. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  428. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  429. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  430. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  431. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  432. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  433. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  434. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  435. trueppm_api/apps/projects/models.py +8946 -0
  436. trueppm_api/apps/projects/poker_services.py +154 -0
  437. trueppm_api/apps/projects/poker_views.py +398 -0
  438. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  439. trueppm_api/apps/projects/program_label_services.py +94 -0
  440. trueppm_api/apps/projects/program_label_views.py +189 -0
  441. trueppm_api/apps/projects/program_rollup.py +575 -0
  442. trueppm_api/apps/projects/program_schedule.py +745 -0
  443. trueppm_api/apps/projects/program_views.py +3220 -0
  444. trueppm_api/apps/projects/project_templates.py +422 -0
  445. trueppm_api/apps/projects/receivers.py +234 -0
  446. trueppm_api/apps/projects/refusal_codes.py +140 -0
  447. trueppm_api/apps/projects/reorder_services.py +145 -0
  448. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  449. trueppm_api/apps/projects/retro_board_services.py +368 -0
  450. trueppm_api/apps/projects/retro_services.py +285 -0
  451. trueppm_api/apps/projects/risk_import.py +396 -0
  452. trueppm_api/apps/projects/row_identity.py +113 -0
  453. trueppm_api/apps/projects/schema_migrations.py +228 -0
  454. trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
  455. trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
  456. trueppm_api/apps/projects/seed/__init__.py +61 -0
  457. trueppm_api/apps/projects/seed/exporter.py +1644 -0
  458. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  459. trueppm_api/apps/projects/seed/importer.py +2390 -0
  460. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  461. trueppm_api/apps/projects/seed/reldates.py +126 -0
  462. trueppm_api/apps/projects/seed/replace.py +126 -0
  463. trueppm_api/apps/projects/seed/replay.py +1691 -0
  464. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  465. trueppm_api/apps/projects/seed/samples.py +427 -0
  466. trueppm_api/apps/projects/seed/validation.py +1149 -0
  467. trueppm_api/apps/projects/serializers.py +12310 -0
  468. trueppm_api/apps/projects/services.py +6966 -0
  469. trueppm_api/apps/projects/share_serializers.py +132 -0
  470. trueppm_api/apps/projects/share_services.py +325 -0
  471. trueppm_api/apps/projects/share_views.py +374 -0
  472. trueppm_api/apps/projects/sharing_settings.py +187 -0
  473. trueppm_api/apps/projects/signal_privacy_services.py +909 -0
  474. trueppm_api/apps/projects/signal_privacy_views.py +476 -0
  475. trueppm_api/apps/projects/signals.py +132 -0
  476. trueppm_api/apps/projects/slip_conflict.py +266 -0
  477. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  478. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  479. trueppm_api/apps/projects/standup.py +304 -0
  480. trueppm_api/apps/projects/standup_views.py +99 -0
  481. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  482. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  483. trueppm_api/apps/projects/surface_visibility.py +126 -0
  484. trueppm_api/apps/projects/task_batch_services.py +379 -0
  485. trueppm_api/apps/projects/task_bulk.py +1152 -0
  486. trueppm_api/apps/projects/task_classification.py +561 -0
  487. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  488. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  489. trueppm_api/apps/projects/task_grouping.py +754 -0
  490. trueppm_api/apps/projects/tasks.py +1917 -0
  491. trueppm_api/apps/projects/template_divergence.py +168 -0
  492. trueppm_api/apps/projects/template_services.py +160 -0
  493. trueppm_api/apps/projects/template_tasks.py +258 -0
  494. trueppm_api/apps/projects/template_views.py +799 -0
  495. trueppm_api/apps/projects/throttles.py +311 -0
  496. trueppm_api/apps/projects/urls.py +1011 -0
  497. trueppm_api/apps/projects/utilization.py +885 -0
  498. trueppm_api/apps/projects/views.py +20572 -0
  499. trueppm_api/apps/projects/wbs_paths.py +119 -0
  500. trueppm_api/apps/resources/__init__.py +0 -0
  501. trueppm_api/apps/resources/apps.py +8 -0
  502. trueppm_api/apps/resources/capacity.py +121 -0
  503. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  504. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  505. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  506. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  507. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  508. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  509. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  510. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  511. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  512. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  513. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  514. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  515. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  516. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  517. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  518. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  519. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  520. trueppm_api/apps/resources/models.py +298 -0
  521. trueppm_api/apps/resources/serializers.py +426 -0
  522. trueppm_api/apps/resources/services.py +465 -0
  523. trueppm_api/apps/resources/urls.py +28 -0
  524. trueppm_api/apps/resources/views.py +1749 -0
  525. trueppm_api/apps/scheduling/__init__.py +0 -0
  526. trueppm_api/apps/scheduling/apps.py +153 -0
  527. trueppm_api/apps/scheduling/calendars.py +119 -0
  528. trueppm_api/apps/scheduling/deadletter.py +91 -0
  529. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  530. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  531. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  532. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  533. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  534. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  535. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  536. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  537. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  538. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  539. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  540. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  541. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  542. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  543. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  544. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  545. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  546. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  547. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  548. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  549. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  550. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  551. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  552. trueppm_api/apps/scheduling/models.py +516 -0
  553. trueppm_api/apps/scheduling/receivers.py +160 -0
  554. trueppm_api/apps/scheduling/risk_premium.py +283 -0
  555. trueppm_api/apps/scheduling/serializers.py +564 -0
  556. trueppm_api/apps/scheduling/services.py +1182 -0
  557. trueppm_api/apps/scheduling/signals.py +56 -0
  558. trueppm_api/apps/scheduling/tasks.py +1984 -0
  559. trueppm_api/apps/scheduling/telemetry.py +80 -0
  560. trueppm_api/apps/scheduling/units.py +35 -0
  561. trueppm_api/apps/scheduling/urls.py +57 -0
  562. trueppm_api/apps/scheduling/views.py +2564 -0
  563. trueppm_api/apps/sso/__init__.py +0 -0
  564. trueppm_api/apps/sso/apps.py +19 -0
  565. trueppm_api/apps/sso/extensions.py +130 -0
  566. trueppm_api/apps/sso/management/__init__.py +0 -0
  567. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  568. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  569. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  570. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  571. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  572. trueppm_api/apps/sso/models.py +148 -0
  573. trueppm_api/apps/sso/serializers.py +437 -0
  574. trueppm_api/apps/sso/services.py +1385 -0
  575. trueppm_api/apps/sso/urls.py +47 -0
  576. trueppm_api/apps/sso/views.py +935 -0
  577. trueppm_api/apps/sync/__init__.py +0 -0
  578. trueppm_api/apps/sync/apps.py +15 -0
  579. trueppm_api/apps/sync/backfill.py +208 -0
  580. trueppm_api/apps/sync/broadcast.py +320 -0
  581. trueppm_api/apps/sync/conflict.py +262 -0
  582. trueppm_api/apps/sync/consumers.py +471 -0
  583. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  584. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  585. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  586. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  587. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  588. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  589. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  590. trueppm_api/apps/sync/models.py +225 -0
  591. trueppm_api/apps/sync/pagination.py +231 -0
  592. trueppm_api/apps/sync/sequence.py +371 -0
  593. trueppm_api/apps/sync/serializers.py +687 -0
  594. trueppm_api/apps/sync/tasks.py +260 -0
  595. trueppm_api/apps/sync/throttles.py +175 -0
  596. trueppm_api/apps/sync/upload.py +501 -0
  597. trueppm_api/apps/sync/urls.py +17 -0
  598. trueppm_api/apps/sync/views.py +930 -0
  599. trueppm_api/apps/sync/ws_auth.py +193 -0
  600. trueppm_api/apps/taskruns/__init__.py +0 -0
  601. trueppm_api/apps/taskruns/apps.py +11 -0
  602. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  603. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  604. trueppm_api/apps/taskruns/models.py +68 -0
  605. trueppm_api/apps/taskruns/serializers.py +92 -0
  606. trueppm_api/apps/taskruns/tasks.py +60 -0
  607. trueppm_api/apps/taskruns/tracker.py +286 -0
  608. trueppm_api/apps/taskruns/urls.py +46 -0
  609. trueppm_api/apps/taskruns/views.py +195 -0
  610. trueppm_api/apps/teams/__init__.py +8 -0
  611. trueppm_api/apps/teams/apps.py +16 -0
  612. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  613. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
  614. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  615. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  616. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  617. trueppm_api/apps/teams/models.py +145 -0
  618. trueppm_api/apps/teams/permissions.py +149 -0
  619. trueppm_api/apps/teams/serializers.py +95 -0
  620. trueppm_api/apps/teams/services.py +434 -0
  621. trueppm_api/apps/teams/signals.py +61 -0
  622. trueppm_api/apps/teams/urls.py +34 -0
  623. trueppm_api/apps/teams/views.py +201 -0
  624. trueppm_api/apps/timetracking/__init__.py +0 -0
  625. trueppm_api/apps/timetracking/apps.py +9 -0
  626. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  627. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  628. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  629. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  630. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  631. trueppm_api/apps/timetracking/models.py +178 -0
  632. trueppm_api/apps/timetracking/serializers.py +175 -0
  633. trueppm_api/apps/timetracking/services.py +117 -0
  634. trueppm_api/apps/timetracking/urls.py +47 -0
  635. trueppm_api/apps/timetracking/views.py +526 -0
  636. trueppm_api/apps/webhooks/__init__.py +0 -0
  637. trueppm_api/apps/webhooks/apps.py +8 -0
  638. trueppm_api/apps/webhooks/backfill.py +90 -0
  639. trueppm_api/apps/webhooks/dispatch.py +144 -0
  640. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  641. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
  642. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  643. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  644. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  645. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  646. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  647. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  648. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  649. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
  650. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  651. trueppm_api/apps/webhooks/models.py +372 -0
  652. trueppm_api/apps/webhooks/serializers.py +318 -0
  653. trueppm_api/apps/webhooks/tasks.py +663 -0
  654. trueppm_api/apps/webhooks/urls.py +22 -0
  655. trueppm_api/apps/webhooks/views.py +383 -0
  656. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  657. trueppm_api/apps/workflow_engine/apps.py +23 -0
  658. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  659. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  660. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  661. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  662. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  663. trueppm_api/apps/workflow_engine/models.py +265 -0
  664. trueppm_api/apps/workflow_engine/tasks.py +368 -0
  665. trueppm_api/apps/workspace/__init__.py +0 -0
  666. trueppm_api/apps/workspace/apps.py +10 -0
  667. trueppm_api/apps/workspace/backfill.py +111 -0
  668. trueppm_api/apps/workspace/export.py +242 -0
  669. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  670. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  671. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  672. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  673. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  674. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  675. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  676. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  677. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  678. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  679. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  680. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  681. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  682. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  683. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  684. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  685. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  686. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  687. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  688. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  689. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  690. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  691. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  692. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  693. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  694. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  695. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  696. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  697. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  698. trueppm_api/apps/workspace/models.py +857 -0
  699. trueppm_api/apps/workspace/permissions.py +187 -0
  700. trueppm_api/apps/workspace/serializers.py +590 -0
  701. trueppm_api/apps/workspace/services.py +919 -0
  702. trueppm_api/apps/workspace/signals.py +37 -0
  703. trueppm_api/apps/workspace/tasks.py +555 -0
  704. trueppm_api/apps/workspace/urls.py +113 -0
  705. trueppm_api/apps/workspace/views.py +1502 -0
  706. trueppm_api/asgi.py +42 -0
  707. trueppm_api/celery.py +31 -0
  708. trueppm_api/core/__init__.py +0 -0
  709. trueppm_api/core/admin_site.py +206 -0
  710. trueppm_api/core/auth_views.py +916 -0
  711. trueppm_api/core/constant_time.py +51 -0
  712. trueppm_api/core/csp.py +54 -0
  713. trueppm_api/core/db.py +87 -0
  714. trueppm_api/core/db_session.py +37 -0
  715. trueppm_api/core/exception_handlers.py +157 -0
  716. trueppm_api/core/export_downloads.py +56 -0
  717. trueppm_api/core/extension_providers.py +56 -0
  718. trueppm_api/core/extension_signals.py +147 -0
  719. trueppm_api/core/idempotent.py +260 -0
  720. trueppm_api/core/metadata.py +42 -0
  721. trueppm_api/core/middleware.py +51 -0
  722. trueppm_api/core/openapi.py +949 -0
  723. trueppm_api/core/password_reset.py +500 -0
  724. trueppm_api/core/protect_conflict.py +100 -0
  725. trueppm_api/core/ratelimit.py +148 -0
  726. trueppm_api/core/redis_throttle.py +82 -0
  727. trueppm_api/core/refresh_cookie_policy.py +156 -0
  728. trueppm_api/core/request_body.py +86 -0
  729. trueppm_api/core/security_checks.py +884 -0
  730. trueppm_api/core/storage_config.py +105 -0
  731. trueppm_api/core/text_decode.py +168 -0
  732. trueppm_api/core/throttling.py +344 -0
  733. trueppm_api/core/valkey.py +214 -0
  734. trueppm_api/core/valkey_cache.py +66 -0
  735. trueppm_api/core/valkey_checks.py +104 -0
  736. trueppm_api/core/valkey_config.py +39 -0
  737. trueppm_api/core/worker_broker_wait.py +149 -0
  738. trueppm_api/core/worker_heartbeat.py +134 -0
  739. trueppm_api/fields.py +36 -0
  740. trueppm_api/permissions.py +30 -0
  741. trueppm_api/py.typed +0 -0
  742. trueppm_api/routing.py +11 -0
  743. trueppm_api/settings/__init__.py +0 -0
  744. trueppm_api/settings/base.py +2455 -0
  745. trueppm_api/settings/dev.py +273 -0
  746. trueppm_api/settings/prod.py +228 -0
  747. trueppm_api/urls.py +153 -0
  748. trueppm_api/workflows/__init__.py +38 -0
  749. trueppm_api/workflows/backends/__init__.py +6 -0
  750. trueppm_api/workflows/backends/default.py +297 -0
  751. trueppm_api/workflows/consumers/__init__.py +11 -0
  752. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  753. trueppm_api/workflows/interface.py +148 -0
  754. trueppm_api/workflows/registry.py +106 -0
  755. trueppm_api/workflows/services.py +90 -0
  756. trueppm_api/wsgi.py +11 -0
  757. trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
  758. trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
  759. trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
@@ -0,0 +1,745 @@
1
+ """Program-scoped CPM read surface (#1117 / ADR-0120 D3 read side, enables D6).
2
+
3
+ Computes the **program-true** critical path across a program's member projects
4
+ *on read*: every member project's tasks and every accepted cross-project edge are
5
+ merged into one engine graph and the deterministic CPM runs once, so an upstream
6
+ task gating another project's milestone reports ``is_critical`` and floats against
7
+ the whole program — the honest cross-project critical path. This is the
8
+ render-don't-derive (ADR-0115) source the #1118 ``ProgramSchedulePage`` consumes;
9
+ no client-side program math.
10
+
11
+ It deliberately does **not** persist. There is no write-back to per-project
12
+ tasks, no ``schedule_lock:program:{id}``, and no outbox coalescing — those belong
13
+ to the deferred D3 *dispatch* pass (ADR-0120). Until that lands, per-project
14
+ surfaces keep their per-project CPM and this endpoint is the only program-true
15
+ view. Computing on read avoids the lock/coalesce/fan-out machinery entirely at
16
+ the cost of one CPM per request, which the ``MAX_PROGRAM_TASKS`` guard bounds.
17
+
18
+ Visibility (ADR-0120 D5): a requester gated in by ``IsProgramMember`` sees full
19
+ schedule rows for member projects they belong to and the redacted
20
+ ``ExternalTaskCard`` shape (title + CPM dates only, no description/assignee/
21
+ status) for member projects they cannot read — never a bare "blocked by
22
+ [redacted]".
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from collections.abc import Callable, Collection
29
+ from dataclasses import dataclass, field
30
+ from datetime import timedelta
31
+ from typing import TYPE_CHECKING, Any, NoReturn
32
+
33
+ from rest_framework.exceptions import APIException
34
+
35
+ if TYPE_CHECKING:
36
+ from trueppm_api.apps.projects.models import Program
37
+
38
+ # Engine ``SchedulerError`` messages embed the offending task as ``Task '<uuid>'``
39
+ # (e.g. the #1069 three-point-ordering check). Extract the id to attribute the
40
+ # failure back to its member project for the #1981 structured 422; unmatched
41
+ # messages fall back to a project-less "some task" response.
42
+ _TASK_ID_RE = re.compile(
43
+ r"Task '([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
44
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12})'"
45
+ )
46
+
47
+ # Soft upper bound on the merged leaf-task count for an on-read CPM. A
48
+ # compute-on-read pass is O(tasks + edges); for a pathologically large program
49
+ # the persisted D3 dispatch pass (with caching) is the right path, so we fail
50
+ # loud with a 422 rather than serving a slow request. Chosen far above any
51
+ # realistic single program so it only fires on degenerate input.
52
+ MAX_PROGRAM_TASKS = 5000
53
+
54
+
55
+ class ProgramScheduleTooLarge(APIException):
56
+ """Raised when a program exceeds ``MAX_PROGRAM_TASKS`` for an on-read CPM."""
57
+
58
+ status_code = 422
59
+ default_detail = (
60
+ "This program has too many tasks for an on-read schedule computation; "
61
+ "the persisted program-schedule pass is required for programs this large."
62
+ )
63
+ default_code = "program_schedule_too_large"
64
+
65
+
66
+ class ProgramScheduleInvalidInput(APIException):
67
+ """Raised when a member project's task data makes the merged CPM unschedulable.
68
+
69
+ The program schedule is compute-on-read, so a single degenerate task in *any*
70
+ member project (invalid three-point estimate #1069, unknown-task dependency,
71
+ unresolvable calendar, cycle) would otherwise surface as an unhandled 500 that
72
+ blanks the whole cross-project view for every member (#1981). We instead map
73
+ the engine's ``SchedulerError`` to a structured, non-500 response that names
74
+ the offending project (and task where identifiable), so the frontend can point
75
+ the user at the project to fix rather than dead-ending on a retry that can
76
+ never succeed. The body carries ``code`` so the client distinguishes this from
77
+ the sibling 422 (``program_schedule_too_large``).
78
+ """
79
+
80
+ status_code = 422
81
+ default_code = "program_schedule_invalid_input"
82
+
83
+ def __init__(
84
+ self,
85
+ *,
86
+ reason: str | None = None,
87
+ project_id: str | None = None,
88
+ project_name: str | None = None,
89
+ task_id: str | None = None,
90
+ task_name: str | None = None,
91
+ ) -> None:
92
+ if project_name:
93
+ detail_msg = (
94
+ f"A task in “{project_name}” has data the schedule engine "
95
+ "cannot compute. Open that project's schedule to fix it."
96
+ )
97
+ else:
98
+ detail_msg = (
99
+ "A task in one of this program's projects has data the schedule "
100
+ "engine cannot compute. Open the offending project's schedule to "
101
+ "fix it."
102
+ )
103
+ # Only non-null keys are included: DRF coerces every scalar leaf of an
104
+ # APIException detail into an ``ErrorDetail`` string, so a literal ``None``
105
+ # would serialize as the string ``"None"`` rather than JSON ``null``.
106
+ # Omitting the key instead gives the client a clean "absent" (undefined).
107
+ body: dict[str, Any] = {"code": self.default_code, "detail": detail_msg}
108
+ if reason is not None:
109
+ # Carries the offending task's estimate values; present only for an
110
+ # accessible offending project (ADR-0120 D5 — see the raise site).
111
+ body["reason"] = reason
112
+ if project_id:
113
+ body["project"] = {"id": project_id, "name": project_name}
114
+ if task_id:
115
+ body["task"] = {"id": task_id, "name": task_name}
116
+ super().__init__(body)
117
+
118
+
119
+ @dataclass
120
+ class ProgramScheduleGraph:
121
+ """The merged program graph plus its program-true CPM result (ADR-0120 D3).
122
+
123
+ The single output of :func:`gather_program_schedule`, shared by the on-read
124
+ endpoint (:func:`compute_program_schedule`) and the persisted dispatch pass
125
+ (``scheduling.tasks.recalculate_program_schedule``) so the gather + merged CPM
126
+ runs through one code path and the two surfaces can never drift (the #1185
127
+ failure class). ``result``/``result_map`` are ``None``/empty when the program
128
+ has no schedulable work.
129
+
130
+ ``result_map`` holds the *leaf* CPM rows only (summary tasks are excluded from
131
+ the pass, exactly as the single-project engine does); the persisted write-back
132
+ rolls summary dates up separately via ``apply_summary_rollups``.
133
+ """
134
+
135
+ member_projects: list[Any]
136
+ project_by_id: dict[Any, Any]
137
+ db_task_by_id: dict[str, Any]
138
+ children_map: dict[str, list[str]]
139
+ db_deps: list[Any]
140
+ # The expanded *leaf-level* dependency edges the CPM actually ran on (summary
141
+ # edges resolved to their leaves). The D4 firewall walks this adjacency to
142
+ # attribute downstream slips to a cross-project edge. Empty on the no-work path.
143
+ expanded_deps: list[Any]
144
+ start_date: Any
145
+ result: Any | None
146
+ result_map: dict[str, Any]
147
+ leaf_ids: set[str]
148
+ # Composed engine calendar per member project, keyed by ``str(project.id)`` —
149
+ # the same map the merged ``SchedProject`` runs on (ADR-0120 D3). The persisted
150
+ # write-back needs it to express each summary's rolled-up span in that
151
+ # project's own working days (#3530); the on-read endpoint ignores it.
152
+ calendars: dict[str, Any] = field(default_factory=dict)
153
+
154
+ @property
155
+ def summary_ids(self) -> set[str]:
156
+ """Task ids that have ≥1 child in the merged WBS — the summary nodes."""
157
+ return set(self.children_map.keys())
158
+
159
+
160
+ def _schedulable_tasks(project: Any) -> list[Any]:
161
+ """The member project's committed tasks, replicating the single-project filter.
162
+
163
+ Drops recurring templates, EPIC grouping nodes, BACKLOG cards, and soft-deleted
164
+ tombstones — matching ``CommittedTaskManager`` (#1772). Anything else would put
165
+ work in the merged CPM that the single-project pass never schedules, and the two
166
+ surfaces would drift (the #1185 failure class).
167
+ """
168
+ from trueppm_api.apps.projects.models import TaskStatus, TaskType
169
+
170
+ return [
171
+ t
172
+ for t in project.tasks.all()
173
+ if not t.is_recurring
174
+ and t.type != TaskType.EPIC
175
+ and t.status != TaskStatus.BACKLOG
176
+ and not t.is_deleted
177
+ ]
178
+
179
+
180
+ def _project_sched_tasks(project: Any, db_tasks: list[Any], *, suggest_approve: bool) -> list[Any]:
181
+ """Convert one member project's tasks to engine tasks, tagged with its calendar.
182
+
183
+ The ``calendar_id`` tag is the ADR-0120 D3 engine substrate: the merged pass
184
+ holds one calendar per member project rather than a single program-wide one, so
185
+ each task's working-day math matches its own project's composed calendar. The
186
+ engine falls back to the project-level default for an unknown id (it never will
187
+ here — every id is registered by the caller).
188
+ """
189
+ from trueppm_api.apps.scheduling.services import build_sched_tasks
190
+
191
+ sched_tasks = build_sched_tasks(db_tasks, suggest_approve=suggest_approve)
192
+ for st in sched_tasks:
193
+ st.calendar_id = str(project.id)
194
+ return sched_tasks
195
+
196
+
197
+ def _merge_children_map(db_tasks: list[Any], children_map: dict[str, list[str]]) -> None:
198
+ """Add ``db_tasks``' parent→children edges to the merged WBS map, in place.
199
+
200
+ Built per project from ``wbs_path`` then merged — keys are globally-unique task
201
+ ids, so no namespacing is needed, but the *paths* are only unique within a
202
+ project, which is why this merges one project at a time instead of calling
203
+ ``scheduling.services.build_children_map`` over the whole merged set.
204
+
205
+ It no longer "mirrors ``_run_schedule`` exactly" as this docstring used to claim:
206
+ since #3527 that function builds its map through ``build_children_map`` (the O(N)
207
+ index from #1011), while this is still the O(N^2) parent scan that predates it.
208
+ The two produce identical output — ``tests/apps/scheduling/test_children_map.py``
209
+ pins that equivalence — so nothing is wrong today; it is a fourth copy of one
210
+ rule, and collapsing it into ``build_children_map`` is follow-up work rather than
211
+ part of the forecast fix.
212
+ """
213
+ for t in db_tasks:
214
+ if not t.wbs_path:
215
+ continue
216
+ parent_path = str(t.wbs_path).rsplit(".", 1)
217
+ if len(parent_path) < 2:
218
+ continue
219
+ for candidate in db_tasks:
220
+ if candidate.wbs_path and str(candidate.wbs_path) == parent_path[0]:
221
+ children_map.setdefault(str(candidate.id), []).append(str(t.id))
222
+ break
223
+
224
+
225
+ def _raise_invalid_input(
226
+ exc: Exception,
227
+ *,
228
+ db_task_by_id: dict[str, Any],
229
+ project_by_id: dict[Any, Any],
230
+ can_access_project: Callable[[Any], bool] | None,
231
+ ) -> NoReturn:
232
+ """Map a merged-CPM ``SchedulerError`` to the structured 422 (#1981).
233
+
234
+ Compute-on-read means a single degenerate task in any member project would
235
+ otherwise become an unhandled 500 that blanks the whole program view. Best-effort
236
+ attribution: a cycle carries its task ids directly; other invalid inputs embed the
237
+ offending id in the message.
238
+
239
+ The raw ``reason`` embeds the offending task's estimate day-values, which ADR-0120
240
+ D5 withholds for member projects the requester cannot fully read. It is included
241
+ only when the offending project is both attributable AND accessible; otherwise the
242
+ client still gets the project/task identity (already public via lane metadata and
243
+ the redacted ExternalTaskCard) but not the withheld estimate values.
244
+ """
245
+ from trueppm_scheduler.engine import CyclicDependencyError
246
+
247
+ if isinstance(exc, CyclicDependencyError):
248
+ offending_ids = [tid for tid in exc.cycle if tid in db_task_by_id]
249
+ else:
250
+ offending_ids = [tid for tid in _TASK_ID_RE.findall(str(exc)) if tid in db_task_by_id]
251
+ db_task = db_task_by_id.get(offending_ids[0]) if offending_ids else None
252
+ project = project_by_id.get(db_task.project_id) if db_task is not None else None
253
+ reason_visible = (
254
+ project is not None and can_access_project is not None and can_access_project(project.id)
255
+ )
256
+ raise ProgramScheduleInvalidInput(
257
+ reason=str(exc) if reason_visible else None,
258
+ project_id=str(project.id) if project is not None else None,
259
+ project_name=project.name if project is not None else None,
260
+ task_id=str(db_task.id) if db_task is not None else None,
261
+ task_name=db_task.name if db_task is not None else None,
262
+ ) from exc
263
+
264
+
265
+ def gather_program_schedule(
266
+ program: Program,
267
+ *,
268
+ enforce_max: bool = True,
269
+ can_access_project: Callable[[Any], bool] | None = None,
270
+ exclude_project_ids: Collection[Any] | None = None,
271
+ ) -> ProgramScheduleGraph:
272
+ """Merge every member project's tasks + accepted cross edges and run CPM once.
273
+
274
+ The shared substrate of ADR-0120 D3: every member project's committed tasks
275
+ and every ACCEPTED cross-project dependency (both endpoints in member projects)
276
+ are merged into one engine ``Project`` — each task tagged with its own
277
+ project's calendar (the per-task-calendar substrate) — and the deterministic
278
+ CPM runs a single time, so floats and criticality are program-true across the
279
+ boundary.
280
+
281
+ Args:
282
+ program: The program whose member projects are merged.
283
+ enforce_max: When True (the on-read path), raise ``ProgramScheduleTooLarge``
284
+ above ``MAX_PROGRAM_TASKS`` rather than serving a slow request. The
285
+ persisted background pass passes False — it is allowed to be slow and a
286
+ 422 has no caller there.
287
+ can_access_project: Optional ``project_id -> bool`` predicate used only when
288
+ attributing a ``ProgramScheduleInvalidInput`` (#1981). The raw engine
289
+ ``reason`` string embeds the offending task's estimate day-values, which
290
+ ADR-0120 D5 withholds for member projects the requester cannot read; the
291
+ ``reason`` is therefore included only when the offending project is
292
+ accessible. Absent (the background path never raises the 422), the
293
+ reason is omitted.
294
+
295
+ Raises:
296
+ ProgramScheduleTooLarge: When ``enforce_max`` and the merged leaf-task
297
+ count exceeds ``MAX_PROGRAM_TASKS``.
298
+ ProgramScheduleInvalidInput: When ``enforce_max`` and a member project's
299
+ task data makes the merged CPM unschedulable.
300
+ """
301
+ from trueppm_scheduler.engine import (
302
+ SchedulerError,
303
+ expand_summary_dependencies,
304
+ schedule,
305
+ )
306
+ from trueppm_scheduler.models import Calendar as SchedCalendar
307
+ from trueppm_scheduler.models import Dependency as SchedDependency
308
+ from trueppm_scheduler.models import DependencyType
309
+ from trueppm_scheduler.models import Project as SchedProject
310
+
311
+ from trueppm_api.apps.projects.lifecycle import visible_projects
312
+ from trueppm_api.apps.projects.models import Dependency, EstimationMode, Project
313
+ from trueppm_api.apps.scheduling.calendars import compose_project_calendar
314
+
315
+ # Drafts are excluded (#2962/#3128), and this one is not obvious enough to
316
+ # leave implicit. It is tempting to argue a draft is harmless here because CPM
317
+ # on disjoint components is independent, so a draft with no accepted
318
+ # cross-project edge cannot reach a committed project. That argument is WRONG.
319
+ # `engine.schedule()` anchors the backward pass on a single
320
+ # `project_finish = max(t.early_finish for t in tasks)` across the WHOLE merged
321
+ # set, and `start_date` on `min(p.start_date ...)` above. So a draft that
322
+ # simply runs later than everything else raises `project_finish`, which
323
+ # inflates `total_float` for every committed task in the program and can flip
324
+ # `is_critical` off across the board — with no dependency between them at all.
325
+ # This is the rollup case in its sharpest form: a plan nobody agreed to
326
+ # silently rewriting the critical path a PM is managing to.
327
+ member_qs = visible_projects(Project.objects.filter(program=program, is_deleted=False))
328
+ if exclude_project_ids is not None:
329
+ # ADR-0678 (#2482): a project that opted out of agent reads is DROPPED from
330
+ # the merged graph, not redacted. The ExternalTaskCard redaction that
331
+ # ``can_access_project`` drives still leaks task titles and program-true CPM
332
+ # dates — acceptable for a project the caller merely lacks membership on,
333
+ # and NOT acceptable for one that explicitly withheld consent.
334
+ member_qs = member_qs.exclude(pk__in=exclude_project_ids)
335
+ member_projects = list(
336
+ member_qs.select_related("calendar", "program__calendar")
337
+ # calendar__exceptions + calendar_layers__calendar__exceptions:
338
+ # compose_project_calendar reads each member project's base calendar
339
+ # exceptions (#1491) AND its applied overlays (#906); prefetch both to
340
+ # avoid an N+1 across the whole program (this loop iterates every member).
341
+ # program__calendar__exceptions: a member project that sets no calendar of
342
+ # its own inherits the program calendar (ADR-0441), whose exceptions the
343
+ # resolver reads — prefetch to keep the whole-program pass N+1-free.
344
+ .prefetch_related(
345
+ "tasks",
346
+ "tasks__sprint",
347
+ "calendar__exceptions",
348
+ "calendar_layers__calendar__exceptions",
349
+ "program__calendar__exceptions",
350
+ )
351
+ .order_by("start_date", "name")
352
+ )
353
+ project_by_id = {p.id: p for p in member_projects}
354
+
355
+ # --- Per-project conversion, replicating the single-project _run_schedule
356
+ # filter (drop recurring, EPIC grouping nodes, BACKLOG cards, and soft-deleted
357
+ # tombstones — matching CommittedTaskManager, #1772) and tagging each task with
358
+ # its own project's calendar so the merged pass uses per-task calendars
359
+ # (ADR-0120 D3 engine substrate). Task ids are globally-unique UUIDs, so
360
+ # merging the per-project task lists needs no namespacing.
361
+ all_sched_tasks: list[Any] = []
362
+ all_children_map: dict[str, list[str]] = {}
363
+ calendars: dict[str, SchedCalendar] = {}
364
+ db_task_by_id: dict[str, Any] = {}
365
+ total_tasks = 0
366
+
367
+ for p in member_projects:
368
+ db_tasks = _schedulable_tasks(p)
369
+ if not db_tasks:
370
+ continue
371
+ total_tasks += len(db_tasks)
372
+ if enforce_max and total_tasks > MAX_PROGRAM_TASKS:
373
+ raise ProgramScheduleTooLarge
374
+
375
+ # Shared composer (#906/#1491): overlays each member project's base +
376
+ # applied calendars into one non-working mask, including every
377
+ # CalendarException holiday/shutdown range — so the merged program-scoped
378
+ # CPM pass honors the same composed calendar as the single-project pass.
379
+ calendars[str(p.id)] = compose_project_calendar(p)
380
+ all_sched_tasks.extend(
381
+ _project_sched_tasks(
382
+ p,
383
+ db_tasks,
384
+ suggest_approve=p.estimation_mode == EstimationMode.SUGGEST_APPROVE,
385
+ )
386
+ )
387
+ db_task_by_id.update({str(t.id): t for t in db_tasks})
388
+ _merge_children_map(db_tasks, all_children_map)
389
+
390
+ # --- Dependencies: every within-project edge plus every ACCEPTED cross-project
391
+ # edge whose endpoints both sit in member projects. Pending (unaccepted) cross
392
+ # edges are excluded — an unconsented edge is not yet a modeled constraint
393
+ # (ADR-0120 D2). Within-project edges are never pending, so the single
394
+ # ``exclude(pending_acceptance=True)`` keeps them all. Computed even on the
395
+ # empty path so the (cheap) query result is uniform; the empty graph carries it.
396
+ member_ids = list(project_by_id.keys())
397
+ db_deps = list(
398
+ Dependency.live.filter(
399
+ predecessor__project_id__in=member_ids,
400
+ successor__project_id__in=member_ids,
401
+ )
402
+ .exclude(pending_acceptance=True)
403
+ .select_related("predecessor", "successor")
404
+ )
405
+
406
+ if not all_sched_tasks:
407
+ return ProgramScheduleGraph(
408
+ member_projects=member_projects,
409
+ project_by_id=project_by_id,
410
+ db_task_by_id=db_task_by_id,
411
+ children_map=all_children_map,
412
+ db_deps=db_deps,
413
+ expanded_deps=[],
414
+ start_date=None,
415
+ result=None,
416
+ result_map={},
417
+ leaf_ids=set(),
418
+ calendars=calendars,
419
+ )
420
+
421
+ included_ids = set(db_task_by_id.keys())
422
+ sched_deps = [
423
+ SchedDependency(
424
+ predecessor_id=str(d.predecessor_id),
425
+ successor_id=str(d.successor_id),
426
+ dep_type=DependencyType(d.dep_type),
427
+ lag=timedelta(days=d.lag),
428
+ )
429
+ for d in db_deps
430
+ if str(d.predecessor_id) in included_ids and str(d.successor_id) in included_ids
431
+ ]
432
+
433
+ leaf_tasks, expanded_deps = expand_summary_dependencies(
434
+ all_sched_tasks, sched_deps, all_children_map
435
+ )
436
+
437
+ start_date = min(p.start_date for p in member_projects)
438
+ merged = SchedProject(
439
+ id=str(program.pk),
440
+ name=program.name,
441
+ start_date=start_date,
442
+ tasks=leaf_tasks,
443
+ dependencies=expanded_deps,
444
+ # Per-task calendar_id drives each task's working-day math; this default
445
+ # is only a fallback for tasks with no calendar_id (none here).
446
+ calendar=SchedCalendar(),
447
+ calendars=calendars,
448
+ # Earliest-possible dates: per-project status_dates differ across the
449
+ # program, so a single merged data date would be arbitrary. The program
450
+ # forecast view shows the earliest honest schedule (ADR-0132 parity with a
451
+ # null status_date), not a data-date floor.
452
+ status_date=None,
453
+ )
454
+
455
+ try:
456
+ result = schedule(merged)
457
+ except SchedulerError as exc:
458
+ # Only the on-read HTTP path (``enforce_max``) converts a degenerate-data
459
+ # failure into a structured response. The persisted background pass
460
+ # (``enforce_max=False``) must let the original ``SchedulerError``
461
+ # propagate to its ``TaskRunTracker``, which marks the run FAILED — a DRF
462
+ # ``APIException`` has no meaning in a Celery context and would corrupt
463
+ # that telemetry.
464
+ if not enforce_max:
465
+ raise
466
+ _raise_invalid_input(
467
+ exc,
468
+ db_task_by_id=db_task_by_id,
469
+ project_by_id=project_by_id,
470
+ can_access_project=can_access_project,
471
+ )
472
+ result_map = {st.id: st for st in result.tasks}
473
+ leaf_ids = set(result_map.keys())
474
+
475
+ return ProgramScheduleGraph(
476
+ member_projects=member_projects,
477
+ project_by_id=project_by_id,
478
+ db_task_by_id=db_task_by_id,
479
+ children_map=all_children_map,
480
+ db_deps=db_deps,
481
+ expanded_deps=expanded_deps,
482
+ start_date=start_date,
483
+ result=result,
484
+ result_map=result_map,
485
+ leaf_ids=leaf_ids,
486
+ calendars=calendars,
487
+ )
488
+
489
+
490
+ def _lane_working_day_durations(graph: ProgramScheduleGraph) -> dict[str, int]:
491
+ """Working-day span of each member project's lane box (#3597).
492
+
493
+ A lane box is the synthetic per-project rollup the #1118 program-schedule
494
+ view draws under each project — the client's ``laneSummaryTask`` spans it
495
+ over ``[min(early_start), max(early_finish)]`` across every task in that
496
+ project. Before #3597 that box's *displayed* duration was a raw inclusive
497
+ calendar-day count derived client-side (the same unit mistake #3530 fixed
498
+ for summary rows): correct here means the same working-day count, against
499
+ that project's own composed calendar, that ``summary_working_day_durations``
500
+ (#3530) already computes for a single project's WBS summary rows — a lane
501
+ is the same rollup shape, one level up, over the *whole* project rather
502
+ than one WBS subtree.
503
+
504
+ Spans every task in the lane (full or ADR-0120 D5 externally-redacted) —
505
+ the rollup box is drawn regardless of the caller's own per-project
506
+ visibility, matching the pre-#3597 client computation exactly.
507
+
508
+ Args:
509
+ graph: The merged program graph from :func:`gather_program_schedule`.
510
+ ``result_map``/``calendars`` are empty on the no-schedulable-work
511
+ path, so this returns ``{}`` and every lane falls back to 0.
512
+
513
+ Returns:
514
+ ``{project_id: working_day_duration}``, omitting any project with no
515
+ scheduled leaf task or no composed calendar.
516
+ """
517
+ from trueppm_scheduler.engine import _WorkingDayCounter
518
+
519
+ ranges: dict[str, tuple[Any, Any]] = {}
520
+ for tid, sched in graph.result_map.items():
521
+ db_task = graph.db_task_by_id.get(tid)
522
+ if db_task is None or sched.early_start is None or sched.early_finish is None:
523
+ continue
524
+ project_key = str(db_task.project_id)
525
+ lo, hi = ranges.get(project_key, (sched.early_start, sched.early_finish))
526
+ ranges[project_key] = (min(lo, sched.early_start), max(hi, sched.early_finish))
527
+
528
+ durations: dict[str, int] = {}
529
+ for project_key, (lo, hi) in ranges.items():
530
+ cal = graph.calendars.get(project_key)
531
+ if cal is None:
532
+ continue
533
+ counter = _WorkingDayCounter.build(lo, hi, cal)
534
+ # Same half-open-plus-conditional-finish correction as
535
+ # `summary_working_day_durations` (#3530): `between` counts
536
+ # `[lo, hi)`, and the finish day is added back only when it is itself
537
+ # a working day.
538
+ working = counter.between(lo, hi)
539
+ if cal.is_working_day(hi):
540
+ working += 1
541
+ durations[project_key] = max(1, working)
542
+ return durations
543
+
544
+
545
+ def program_has_accepted_cross_edges(program_id: Any) -> bool:
546
+ """Whether ``program_id`` has ≥1 accepted cross-project dependency (ADR-0120 D3).
547
+
548
+ The escalation predicate: a project's CPM recompute escalates to a merged,
549
+ program-scoped pass only when this is True. Cheap and index-friendly — an
550
+ EXISTS over the program's accepted (non-pending, non-deleted) edges whose two
551
+ endpoints sit in two *different* member projects. Cross-PROGRAM edges can't
552
+ match (both endpoints are required to share this ``program_id``), so the
553
+ ADR-0070 boundary is preserved here too.
554
+ """
555
+ from django.db.models import F
556
+
557
+ from trueppm_api.apps.projects.models import Dependency
558
+
559
+ return (
560
+ Dependency.live.filter(
561
+ pending_acceptance=False,
562
+ predecessor__project__program_id=program_id,
563
+ successor__project__program_id=program_id,
564
+ )
565
+ .exclude(predecessor__project_id=F("successor__project_id"))
566
+ .exists()
567
+ )
568
+
569
+
570
+ def compute_program_schedule(
571
+ program: Program,
572
+ *,
573
+ can_access_project: Callable[[Any], bool],
574
+ exclude_project_ids: Collection[Any] | None = None,
575
+ ) -> dict[str, Any]:
576
+ """Compute the merged, program-true schedule for ``program`` on read.
577
+
578
+ Args:
579
+ program: The program whose member projects are merged into one CPM graph.
580
+ can_access_project: Predicate ``project_id -> bool`` deciding whether the
581
+ requester may read a member project's tasks in full. Tasks of projects
582
+ for which this returns ``False`` are redacted to the ExternalTaskCard
583
+ shape. Injected by the view so this service stays request-agnostic.
584
+
585
+ Returns:
586
+ A JSON-serializable dict: ``program_id``, ``start_date``, ``finish_date``,
587
+ ``projects`` (lane metadata with per-project ``accessible`` and the lane's
588
+ rolled-up ``duration`` in working days, #3597), ``tasks`` (full — including
589
+ each task's own working-day ``duration`` — or redacted, discriminated by
590
+ ``is_external``), ``links`` (leaf-level edges with ``is_cross_project``),
591
+ ``critical_path`` (program-true task-id order), and
592
+ ``cross_project_edge_count``.
593
+
594
+ Raises:
595
+ ProgramScheduleTooLarge: When the merged leaf-task count exceeds the guard.
596
+ ProgramScheduleInvalidInput: When a member project's task data makes the
597
+ merged CPM unschedulable (#1981); the engine ``reason`` is included only
598
+ for an accessible offending project (ADR-0120 D5).
599
+ """
600
+ graph = gather_program_schedule(
601
+ program,
602
+ enforce_max=True,
603
+ can_access_project=can_access_project,
604
+ exclude_project_ids=exclude_project_ids,
605
+ )
606
+
607
+ # Lane metadata is independent of whether the program has any schedulable
608
+ # work, so build it from the member set and return it even on the empty path.
609
+ # Project has no accent color (only Program/Task do), so lane colors are
610
+ # assigned client-side by the #1118 Gantt; the lane metadata carries identity,
611
+ # the per-project access flag, and the lane's own rolled-up duration.
612
+ lane_durations = _lane_working_day_durations(graph)
613
+ lanes = [
614
+ {
615
+ "id": str(p.id),
616
+ "name": p.name,
617
+ "accessible": bool(can_access_project(p.id)),
618
+ # Working days across the lane's own rolled-up span (#3597) — 0 for a
619
+ # project with no scheduled task, matching the pre-fix client fallback.
620
+ "duration": lane_durations.get(str(p.id), 0),
621
+ }
622
+ for p in graph.member_projects
623
+ ]
624
+
625
+ if graph.result is None:
626
+ return {
627
+ "program_id": str(program.pk),
628
+ "start_date": None,
629
+ "finish_date": None,
630
+ "projects": lanes,
631
+ "tasks": [],
632
+ "links": [],
633
+ "critical_path": [],
634
+ "cross_project_edge_count": 0,
635
+ }
636
+
637
+ result = graph.result
638
+ result_by_id = graph.result_map
639
+ leaf_ids = graph.leaf_ids
640
+ db_task_by_id = graph.db_task_by_id
641
+ project_by_id = graph.project_by_id
642
+
643
+ tasks_payload = [
644
+ _task_payload(tid, st, db_task_by_id[tid], project_by_id, can_access_project)
645
+ for tid, st in result_by_id.items()
646
+ if tid in db_task_by_id
647
+ ]
648
+
649
+ links_payload = []
650
+ cross_count = 0
651
+ for d in graph.db_deps:
652
+ pid = str(d.predecessor_id)
653
+ sid = str(d.successor_id)
654
+ if pid not in leaf_ids or sid not in leaf_ids:
655
+ # Edge touches a summary task (expanded away) or an excluded task —
656
+ # not a renderable leaf-level link.
657
+ continue
658
+ is_cross = d.predecessor.project_id != d.successor.project_id
659
+ if is_cross:
660
+ cross_count += 1
661
+ links_payload.append(
662
+ {
663
+ "predecessor_id": pid,
664
+ "successor_id": sid,
665
+ "dep_type": d.dep_type,
666
+ "lag_days": d.lag,
667
+ "is_cross_project": is_cross,
668
+ }
669
+ )
670
+
671
+ return {
672
+ "program_id": str(program.pk),
673
+ "start_date": result.project_start,
674
+ "finish_date": result.project_finish,
675
+ "projects": lanes,
676
+ "tasks": tasks_payload,
677
+ "links": links_payload,
678
+ "critical_path": list(result.critical_path),
679
+ "cross_project_edge_count": cross_count,
680
+ }
681
+
682
+
683
+ def _task_payload(
684
+ tid: str,
685
+ sched_task: Any,
686
+ db_task: Any,
687
+ project_by_id: dict[Any, Any],
688
+ can_access_project: Callable[[Any], bool],
689
+ ) -> dict[str, Any]:
690
+ """Build one task row — full when the requester can read the project, else the
691
+ ADR-0120 D5 ExternalTaskCard shape (title + CPM dates only). The CPM values
692
+ come from the *merged* result, so a redacted card still shows program-true
693
+ early dates and criticality, never the stale per-project numbers.
694
+
695
+ ``duration`` (full branch only, #3597) is read straight off the ``Task`` row,
696
+ not derived from the merged dates: a leaf task's ``duration`` is its own
697
+ working-day estimate, an *input* to CPM rather than an output of it, so it is
698
+ identical whether the row was last scheduled by its own project's
699
+ single-project pass or by this merged one — reading it here is the same value
700
+ the project schedule shows for "the same task" (the #3597 acceptance bar),
701
+ with nothing to recompute. Summary/WBS-parent tasks never reach this
702
+ function — ``gather_program_schedule`` excludes them from the merged leaf set
703
+ (docstring on ``ProgramScheduleGraph.result_map``) — so there is no working-day
704
+ rollup to do here the way ``_apply_cpm_results`` does for a summary row.
705
+ """
706
+ project = project_by_id.get(db_task.project_id)
707
+ if can_access_project(db_task.project_id):
708
+ return {
709
+ "id": tid,
710
+ "name": db_task.name,
711
+ "hex_id": db_task.short_id,
712
+ "project_id": str(db_task.project_id),
713
+ "is_milestone": db_task.is_milestone,
714
+ "is_external": False,
715
+ "wbs_path": str(db_task.wbs_path) if db_task.wbs_path else None,
716
+ "early_start": sched_task.early_start,
717
+ "early_finish": sched_task.early_finish,
718
+ "late_start": sched_task.late_start,
719
+ "late_finish": sched_task.late_finish,
720
+ "total_float_days": sched_task.total_float.days,
721
+ "is_critical": sched_task.is_critical,
722
+ "duration": db_task.duration,
723
+ }
724
+ # Redacted — mirrors ExternalTaskCardSerializer's field set exactly so the two
725
+ # cross-project read surfaces stay in lockstep. The text key here is ``title``
726
+ # (NOT ``name`` as in the full branch above) deliberately: that is the
727
+ # ExternalTaskCard contract D5 already ships. Do not "align" the two branches
728
+ # to a single key — the client discriminates the redacted shape by it, and the
729
+ # divergence is the contract, not an oversight. ``duration`` is deliberately
730
+ # NOT added here: ExternalTaskCardSerializer has no such field, and D5's own
731
+ # withholding list is scoped to non-schedule facts (description, assignee,
732
+ # status, points) — widening it is a redaction-policy decision this fix does
733
+ # not make. The redacted card keeps its pre-#3597 client-derived approximation.
734
+ return {
735
+ "id": tid,
736
+ "title": db_task.name,
737
+ "hex_id": db_task.short_id,
738
+ "project_id": str(db_task.project_id),
739
+ "project_name": project.name if project else "",
740
+ "is_milestone": db_task.is_milestone,
741
+ "is_external": True,
742
+ "early_start": sched_task.early_start,
743
+ "early_finish": sched_task.early_finish,
744
+ "is_critical": sched_task.is_critical,
745
+ }