trueppm-api 0.4.0__py3-none-any.whl

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