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