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,829 @@
1
+ """Project template gallery, publish, apply and undo (ADR-0789, #2729).
2
+
3
+ RBAC follows ADR-0773's matrix, which already decided this before any code:
4
+ **"Apply a template" is Admin+** (Owner and Admin; Viewer, Member and Scheduler
5
+ cannot). Publishing is likewise Admin+ on the source project. *Reading* the gallery
6
+ needs only authentication and — for a program-scoped template — live membership in
7
+ that program (#4005); a Member should be able to see what skeletons exist without
8
+ being able to fire one at a project, but only for programs they actually belong to.
9
+ Workspace-wide templates (``program`` null) stay visible to every authenticated
10
+ caller regardless of program membership.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, cast
16
+
17
+ from django.core.exceptions import ValidationError as DjangoValidationError
18
+ from django.db.models import Case, Count, IntegerField, Q, QuerySet, Value, When
19
+ from drf_spectacular.utils import (
20
+ OpenApiResponse,
21
+ extend_schema,
22
+ extend_schema_field,
23
+ inline_serializer,
24
+ )
25
+ from rest_framework import serializers, status, viewsets
26
+ from rest_framework.decorators import action
27
+ from rest_framework.exceptions import PermissionDenied, ValidationError
28
+ from rest_framework.permissions import IsAuthenticated
29
+ from rest_framework.request import Request
30
+ from rest_framework.response import Response
31
+
32
+ from trueppm_api.apps.access.models import ProgramMembership, Role
33
+ from trueppm_api.apps.access.permissions import (
34
+ IsProjectNotArchived,
35
+ _membership_role,
36
+ assert_project_not_archived,
37
+ role_can_undo_batch_operation,
38
+ )
39
+ from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
40
+ from trueppm_api.apps.projects.models import (
41
+ Methodology,
42
+ Project,
43
+ ProjectTemplate,
44
+ TemplateApplication,
45
+ TemplateApplicationStatus,
46
+ TemplateSource,
47
+ )
48
+ from trueppm_api.apps.projects.program_views import SeedImportThrottle
49
+ from trueppm_api.apps.projects.project_templates import (
50
+ TemplateStructureError,
51
+ extract_structure,
52
+ summarize_structure,
53
+ validate_structure,
54
+ )
55
+ from trueppm_api.apps.projects.template_services import (
56
+ enqueue_template_apply,
57
+ undo_template_application,
58
+ )
59
+ from trueppm_api.apps.workspace.models import AuditEventType
60
+ from trueppm_api.apps.workspace.services import record_audit_event
61
+ from trueppm_api.core.openapi import state_refusal_400, undo_summary_schema
62
+ from trueppm_api.core.request_body import object_body
63
+
64
+ # Shared user-facing response details.
65
+ _NO_SUCH_PROJECT_DETAIL = "No such project."
66
+
67
+ #: `apply` and `undo` both refuse for two unrelated reasons that share a status
68
+ #: code. Spelling them out is the difference between a client retrying usefully
69
+ #: and a client retrying forever (#3354).
70
+ _ARCHIVED_403 = (
71
+ "Two causes, and a client must tell them apart: the caller is below Project "
72
+ "Manager (Admin) on the project, **or** the project is archived. The archived "
73
+ "case clears for no role — including Owner — so escalating the caller will "
74
+ "never succeed; the project has to be unarchived (#3354)."
75
+ )
76
+
77
+
78
+ def _publish_inputs(data: Any) -> tuple[Any, str, Any]:
79
+ """Read and type-check ``publish``'s three body fields, before any project read.
80
+
81
+ Split out of ``publish`` only to keep it readable; the order matters and is
82
+ preserved — every check here runs *before* the project lookup and therefore
83
+ before ``_require_project_admin``, so an unauthorized caller still cannot use a
84
+ malformed body to learn whether a project exists.
85
+
86
+ Same type-confusion class as #2785: a JSON list/dict/number is truthy, so
87
+ ``(value or "")`` hands it straight to .strip() / slicing and raises
88
+ AttributeError, and a non-string pk reaches UUIDField.to_python, which raises
89
+ Django's ValidationError — not one of the ValueError/TypeError caught by the
90
+ caller, and not something DRF converts. All three are 500s on input that has to
91
+ be a 400.
92
+
93
+ Returns:
94
+ The raw project pk, the stripped name, and the raw description.
95
+ """
96
+ project_id = data.get("project")
97
+ raw_name = data.get("name")
98
+ raw_description = data.get("description")
99
+ for field, value in (
100
+ ("project", project_id),
101
+ ("name", raw_name),
102
+ ("description", raw_description),
103
+ ):
104
+ if value is not None and not isinstance(value, str):
105
+ raise ValidationError({field: "Must be a string."})
106
+
107
+ name = (raw_name or "").strip()
108
+ if not project_id or not name:
109
+ raise ValidationError({"detail": "Both `project` and `name` are required."})
110
+ return project_id, name, raw_description
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Request declarations for the two hand-parsed bodies (#3319)
115
+ # ---------------------------------------------------------------------------
116
+ #
117
+ # Both handlers read their body themselves — ``object_body(request)`` plus
118
+ # ``_publish_inputs`` — rather than through a serializer, and both were previously
119
+ # declared ``request=None``. That is worse than a missing error branch: the
120
+ # published contract said the endpoint accepts nothing, so a generated client had
121
+ # no way to send the field the endpoint *requires*, and ``apply`` answered
122
+ # ``{"project": "This field is required."}`` naming a field the schema had never
123
+ # mentioned.
124
+ #
125
+ # These serializers are **declaration-only** — nothing calls ``is_valid()`` on
126
+ # them, and the handlers' own checks stay the source of truth for what is
127
+ # accepted. They exist so the schema states the shape those checks enforce. Keep
128
+ # them in step with ``_publish_inputs`` / ``apply``: a field added to one and not
129
+ # the other puts the contract back out of sync, which is the defect this fixes.
130
+ # Declaring a body also brings both operations under #3286's automatic 400
131
+ # injection, so the refusal each already emits is now typed for a client.
132
+ _PublishRequestSerializer = inline_serializer(
133
+ name="ProjectTemplatePublish",
134
+ fields={
135
+ "project": serializers.UUIDField(
136
+ help_text="Project whose shape is frozen into the template. Required."
137
+ ),
138
+ "name": serializers.CharField(
139
+ help_text=(
140
+ "Template name. Required, whitespace-stripped, and truncated to 200 "
141
+ "characters rather than rejected when longer."
142
+ )
143
+ ),
144
+ "description": serializers.CharField(
145
+ required=False,
146
+ allow_blank=True,
147
+ allow_null=True,
148
+ help_text="Truncated to 2000 characters rather than rejected when longer.",
149
+ ),
150
+ "source_kind": serializers.ChoiceField(
151
+ choices=TemplateSource.choices,
152
+ required=False,
153
+ help_text="Provenance chip. Defaults to `workspace`.",
154
+ ),
155
+ "new_version": serializers.BooleanField(
156
+ required=False,
157
+ help_text=(
158
+ "Publish as the next version of an existing same-named template "
159
+ "instead of receiving a 409. Read permissively: the strings "
160
+ '`"true"` and `"1"` are also accepted, anything else is false.'
161
+ ),
162
+ ),
163
+ },
164
+ )
165
+
166
+ _ApplyRequestSerializer = inline_serializer(
167
+ name="ProjectTemplateApply",
168
+ fields={
169
+ "project": serializers.UUIDField(help_text="Project to seed from this template. Required."),
170
+ },
171
+ )
172
+
173
+
174
+ def _flag(value: Any) -> bool:
175
+ """Read a permissive boolean off attacker-shaped ``request.data``.
176
+
177
+ Non-string input is the #2795 container-type class: a JSON list or dict is
178
+ truthy and reaches ``.lower()`` as an AttributeError 500. Coerced through
179
+ ``str`` so every shape lands on False rather than on a stack trace.
180
+ """
181
+ return str(value).lower() in {"true", "1"}
182
+
183
+
184
+ class ProjectTemplateSerializer(serializers.ModelSerializer[ProjectTemplate]):
185
+ """Gallery row. ``structure`` is deliberately **not** exposed.
186
+
187
+ The gallery renders a name, a description, a provenance chip, a methodology,
188
+ and what the template carries — never the document itself. Publishing the
189
+ structure on a list endpoint would put a whole project's shape (task names
190
+ included) in front of anyone who can read the gallery, which is a wider
191
+ audience than the source project's members.
192
+ """
193
+
194
+ task_count = serializers.SerializerMethodField()
195
+ provenance = serializers.SerializerMethodField()
196
+ methodology = serializers.SerializerMethodField()
197
+ counts = serializers.SerializerMethodField()
198
+ usage_count = serializers.SerializerMethodField()
199
+ published_by_name = serializers.SerializerMethodField()
200
+ source_project_name = serializers.SerializerMethodField()
201
+ is_superseded = serializers.SerializerMethodField()
202
+
203
+ class Meta:
204
+ model = ProjectTemplate
205
+ fields = [ # noqa: RUF012
206
+ "id",
207
+ "name",
208
+ "description",
209
+ "source_kind",
210
+ "provenance",
211
+ "carries",
212
+ "methodology",
213
+ "task_count",
214
+ "version",
215
+ "program",
216
+ "is_published",
217
+ "published_at",
218
+ "published_by",
219
+ "published_by_name",
220
+ "source_project",
221
+ "source_project_name",
222
+ "supersedes",
223
+ "is_superseded",
224
+ "counts",
225
+ "usage_count",
226
+ ]
227
+ read_only_fields = [ # noqa: RUF012
228
+ "id",
229
+ "source_kind",
230
+ "provenance",
231
+ "carries",
232
+ "methodology",
233
+ "task_count",
234
+ "version",
235
+ "published_at",
236
+ "published_by",
237
+ "published_by_name",
238
+ "source_project",
239
+ "source_project_name",
240
+ "supersedes",
241
+ "is_superseded",
242
+ "counts",
243
+ "usage_count",
244
+ ]
245
+
246
+ def get_counts(self, obj: ProjectTemplate) -> dict[str, Any]:
247
+ """Tasks, phases, gates and dependencies, off the frozen document (#2909).
248
+
249
+ Server-side because the publish inventory and the gallery's carries
250
+ summary are both screens whose credibility rests on a number, and a
251
+ client-side walk of a filtered tree would produce a different one.
252
+ """
253
+ return summarize_structure(obj.structure or {})
254
+
255
+ def get_usage_count(self, obj: ProjectTemplate) -> int:
256
+ """Projects created from this template — the PMO's only house-standard signal.
257
+
258
+ Annotated on the queryset where one is available; falls back to a count so
259
+ a serializer used outside the gallery (the publish response) still answers.
260
+ Undone applications are excluded: a template whose seeding was reversed
261
+ was not adopted, and counting it would overstate the standard.
262
+ """
263
+ annotated = getattr(obj, "adoption_count", None)
264
+ if annotated is not None:
265
+ return int(annotated)
266
+ return obj.applications.filter(
267
+ status=TemplateApplicationStatus.SUCCESS, undone_at__isnull=True
268
+ ).count()
269
+
270
+ def get_published_by_name(self, obj: ProjectTemplate) -> str:
271
+ user = obj.published_by or obj.owner
272
+ if user is None:
273
+ return ""
274
+ return str(getattr(user, "display_name", "") or getattr(user, "username", ""))
275
+
276
+ def get_source_project_name(self, obj: ProjectTemplate) -> str:
277
+ """Name of the project this was frozen from, or '' once it is gone.
278
+
279
+ Empty rather than absent when the FK has been nulled by a project
280
+ deletion: the provenance line simply drops, and the template still works.
281
+ """
282
+ return obj.source_project.name if obj.source_project else ""
283
+
284
+ def get_is_superseded(self, obj: ProjectTemplate) -> bool:
285
+ """A later version replaced this one. It stays selectable on purpose —
286
+ projects already created from it are why it must remain legible."""
287
+ return obj.superseded_by.exists()
288
+
289
+ def get_task_count(self, obj: ProjectTemplate) -> int:
290
+ """How many rows adopting this would write — the one number that decides adoption.
291
+
292
+ Read off the stored document rather than counted in SQL: the structure is
293
+ frozen at publish, so there is nothing to join against and no count that
294
+ could disagree with what apply will actually write.
295
+ """
296
+ structure = obj.structure or {}
297
+ tasks = structure.get("tasks")
298
+ return len(tasks) if isinstance(tasks, list) else 0
299
+
300
+ def get_methodology(self, obj: ProjectTemplate) -> str:
301
+ """The methodology the source project carried at publish time (ADR-0791).
302
+
303
+ Read off the frozen ``structure`` document, mirroring ``get_task_count`` —
304
+ never re-queries the (possibly since-edited/archived/deleted) source
305
+ project, and can never disagree with what ``apply`` will actually seed.
306
+ Falls back to ``Methodology.HYBRID`` (never null) for a structure written
307
+ before this key existed, or one edited outside the publish path — Hybrid is
308
+ the lossless default everywhere else in the methodology chain
309
+ (``methodology.DEFAULT_METHODOLOGY``), so an unresolved template under-hides
310
+ rather than over-hides tabs.
311
+ """
312
+ structure = obj.structure or {}
313
+ methodology = structure.get("methodology")
314
+ return methodology if methodology in Methodology.values else Methodology.HYBRID
315
+
316
+ def get_provenance(self, obj: ProjectTemplate) -> str:
317
+ """The chip label — "Workspace" / "Community" / "Yours".
318
+
319
+ ``source_kind`` is stored, so two readers agree on Workspace and Community.
320
+ *Yours* is the one label additionally narrowed at read time, because it is
321
+ the only one that is genuinely relative to who is asking (ADR-0789 §2).
322
+ """
323
+ request = self.context.get("request")
324
+ user = getattr(request, "user", None)
325
+ if obj.source_kind == TemplateSource.PERSONAL:
326
+ if user is not None and obj.owner_id == getattr(user, "id", None):
327
+ return "Yours"
328
+ # Somebody else's personal template that has been shared into view:
329
+ # it is not "Yours", and calling it Workspace would overstate its
330
+ # standing. Name the publisher's tier honestly.
331
+ return "Community"
332
+ return TemplateSource(obj.source_kind).label
333
+
334
+
335
+ class TemplateApplicationSerializer(serializers.ModelSerializer[TemplateApplication]):
336
+ """The adoption record — also what the Start sheet polls while seeding runs."""
337
+
338
+ class Meta:
339
+ model = TemplateApplication
340
+ fields = [ # noqa: RUF012
341
+ "id",
342
+ "template",
343
+ "template_name",
344
+ "template_version",
345
+ "project",
346
+ "applied_by",
347
+ "status",
348
+ "result_summary",
349
+ "error_detail",
350
+ "created_at",
351
+ "completed_at",
352
+ "undone_at",
353
+ ]
354
+ read_only_fields = fields
355
+
356
+
357
+ class TemplateApplicationUndoSerializer(TemplateApplicationSerializer):
358
+ """The adoption record **plus** the ``undo`` summary the action returns (#3416).
359
+
360
+ The handler builds its body as ``data = self.get_serializer(application).data``
361
+ then ``data["undo"] = summary``, so the bare ``TemplateApplicationSerializer``
362
+ published as the ``200`` gave a typed client no field for the deleted/kept split
363
+ — and that split is the whole point of the call here, because undo deliberately
364
+ *keeps* rows a person has since edited (ADR-0786 §4). A client that cannot read
365
+ ``kept`` cannot tell a full reversal from a partial one.
366
+ """
367
+
368
+ undo = serializers.SerializerMethodField()
369
+
370
+ class Meta(TemplateApplicationSerializer.Meta):
371
+ fields = [*TemplateApplicationSerializer.Meta.fields, "undo"] # noqa: RUF012
372
+
373
+ @extend_schema_field(
374
+ undo_summary_schema(
375
+ "What the undo did to the rows this application wrote.",
376
+ deleted="Rows removed — written by the application and untouched since.",
377
+ kept=(
378
+ "Rows left in place because someone has edited them since. A non-zero "
379
+ "value means the project still carries part of the template."
380
+ ),
381
+ )
382
+ )
383
+ def get_undo(self, obj: TemplateApplication) -> dict[str, int]:
384
+ """The persisted summary, for a caller that serializes a row outside the action.
385
+
386
+ Not what the action returns: ``undo_template_application`` short-circuits an
387
+ application that created no rows and returns ``{"deleted": 0, "kept": 0}``
388
+ without persisting a summary, so the action's live value is authoritative and
389
+ this is the best a later reader can do.
390
+ """
391
+ return dict((obj.result_summary or {}).get("undo") or {})
392
+
393
+
394
+ class ProjectTemplateViewSet(IdempotencyMixin, viewsets.ReadOnlyModelViewSet[ProjectTemplate]):
395
+ """Templates available to the caller, plus publish and apply.
396
+
397
+ **ReadOnly** on purpose, not for lack of ambition. A ``ModelViewSet`` here would
398
+ inherit create / update / destroy under this class's ``IsAuthenticated``, which
399
+ is the wrong gate for all three: any authenticated user could mint an empty
400
+ template, or rename and delete somebody else's. Publishing has exactly one door
401
+ (``publish``, Admin+ on the source project) and it is the door that runs the
402
+ extraction. Editing and unpublishing are deliberately not in this issue —
403
+ absent is safer than present-and-ungated.
404
+
405
+ Apply is throttled on the existing ``seed_import`` scope rather than a new one
406
+ (ADR-0789 §Context 3). Template apply is a third background seeding path
407
+ alongside seed import and CSV/MS Project import; #2615 remains the issue that
408
+ bounds per-user concurrency for all three. Reusing the scope inherits exactly
409
+ the bound the other paths have today and keeps that fix in one place, instead
410
+ of adding a fourth unbounded path.
411
+ """
412
+
413
+ serializer_class = ProjectTemplateSerializer
414
+ permission_classes = [IsAuthenticated] # noqa: RUF012
415
+
416
+ def get_queryset(self) -> QuerySet[ProjectTemplate]:
417
+ """Published templates in scope: workspace-wide, or a program the caller belongs to.
418
+
419
+ A program-scoped template (``program`` set) is offered only inside that
420
+ program (the invariant stated on :class:`ProjectTemplate`, ADR-0789) — a
421
+ non-member must not be able to list, retrieve, or ``apply`` it (#4005). The
422
+ membership check runs as a subquery (``ProgramMembership.live()`` filtered to
423
+ the caller, projected to ``program_id``), not a Python-evaluated list, so it
424
+ costs one extra join rather than N extra queries. Superusers bypass the
425
+ membership floor, matching the existing admin-visibility precedent elsewhere
426
+ in this app.
427
+
428
+ ``?program=`` further narrows to a program's own templates *plus* the
429
+ workspace-wide ones — a program-scoped gallery that hid the shared skeletons
430
+ would be emptier than the user expects and would push them to republish
431
+ duplicates. It narrows the already-membership-scoped queryset; it cannot
432
+ widen it back out to a program the caller does not belong to.
433
+ """
434
+ qs = (
435
+ ProjectTemplate.objects.filter(is_published=True)
436
+ .select_related("owner", "published_by", "source_project")
437
+ # One aggregate rather than a per-row count in the serializer: the
438
+ # gallery renders every template at once, so a SerializerMethodField
439
+ # doing its own query is N+1 by construction (#2909).
440
+ .annotate(
441
+ adoption_count=Count(
442
+ "applications",
443
+ filter=Q(
444
+ applications__status=TemplateApplicationStatus.SUCCESS,
445
+ applications__undone_at__isnull=True,
446
+ ),
447
+ distinct=True,
448
+ )
449
+ )
450
+ # Provenance is the sort key as well as the chip (#2909): on a
451
+ # workspace that has published its own shapes, those are the ones
452
+ # worth reading first, and the bundled starters sort last. The chip
453
+ # stays on every row regardless of position — order is a nudge, not
454
+ # the information. Explicit rather than leaning on Meta.ordering,
455
+ # which an annotate() drops and which paginates unordered.
456
+ .order_by(
457
+ Case(
458
+ When(source_kind=TemplateSource.COMMUNITY, then=Value(1)),
459
+ default=Value(0),
460
+ output_field=IntegerField(),
461
+ ),
462
+ "name",
463
+ "-version",
464
+ )
465
+ )
466
+ # `getattr(self.request, "user", None)` rather than `self.request.user`: schema
467
+ # introspection (drf-spectacular resolving the `id` path parameter's type)
468
+ # calls `get_queryset()` with `self.request` unset, and `IsAuthenticated`
469
+ # already refuses a real unauthenticated caller before this method runs — the
470
+ # same guard `DependencyViewSet.get_queryset()` (projects/views.py) uses for
471
+ # the identical reason. An unauthenticated/unresolvable caller sees only the
472
+ # workspace-wide pool, the most restrictive branch, never every program's
473
+ # templates.
474
+ user = getattr(self.request, "user", None)
475
+ if user is None or not getattr(user, "is_authenticated", False):
476
+ qs = qs.filter(program__isnull=True)
477
+ elif not user.is_superuser:
478
+ member_program_ids = (
479
+ ProgramMembership.live().filter(user=cast("Any", user)).values("program_id")
480
+ )
481
+ qs = qs.filter(Q(program__isnull=True) | Q(program_id__in=member_program_ids))
482
+ program_id = self.request.query_params.get("program")
483
+ if program_id:
484
+ qs = qs.filter(Q(program_id=program_id) | Q(program__isnull=True))
485
+ return qs
486
+
487
+ def _require_project_admin(self, project: Project) -> None:
488
+ """ADR-0773: publishing and applying are Admin+ on the project in question."""
489
+ role = _membership_role(self.request, project.pk)
490
+ if role is None or role < Role.ADMIN:
491
+ raise PermissionDenied(
492
+ "You need at least Project Manager role to publish or apply a template."
493
+ )
494
+
495
+ @extend_schema(
496
+ request=_PublishRequestSerializer,
497
+ responses={
498
+ 201: ProjectTemplateSerializer,
499
+ 409: OpenApiResponse(
500
+ description=(
501
+ "A template with this name already exists in the pool the caller "
502
+ "can see. Carries ``code: name_taken`` plus ``template``, "
503
+ "``version`` and ``next_version`` — resend with "
504
+ "``new_version: true`` to publish the next version of it."
505
+ )
506
+ ),
507
+ },
508
+ description="Publish a project's shape as a reusable template.",
509
+ )
510
+ @action(detail=False, methods=["post"], url_path="publish")
511
+ def publish(self, request: Request) -> Response:
512
+ """Freeze a project's shape into a new template (or a new version of one).
513
+
514
+ Extraction happens **here**, synchronously: it is a read of one project's
515
+ tasks and edges, bounded by ``MAX_TEMPLATE_NODES``, and the publisher should
516
+ learn immediately whether their project was too large rather than by polling.
517
+ """
518
+ body = object_body(request)
519
+ project_id, name, raw_description = _publish_inputs(body)
520
+ try:
521
+ project = Project.objects.get(pk=project_id, is_deleted=False)
522
+ except (Project.DoesNotExist, ValueError, TypeError) as exc:
523
+ raise ValidationError({"project": _NO_SUCH_PROJECT_DETAIL}) from exc
524
+ self._require_project_admin(project)
525
+
526
+ try:
527
+ structure = extract_structure(project)
528
+ except TemplateStructureError as exc:
529
+ raise ValidationError({"project": str(exc)}) from exc
530
+
531
+ source_kind = body.get("source_kind") or TemplateSource.WORKSPACE
532
+ if source_kind not in TemplateSource.values:
533
+ raise ValidationError({"source_kind": "Unknown provenance."})
534
+
535
+ # Republishing writes a NEW row and leaves the old one selectable, rather
536
+ # than editing in place (#2909). The projects already created from v1 are
537
+ # the only audit trail a PMO has for why they look the way they do, and a
538
+ # version mutated under them turns that trail into a lie. So a name that
539
+ # is already taken is a decision, not an error to swallow: either publish
540
+ # a new version of that template, or pick a different name.
541
+ #
542
+ # The lookup is scoped to the pool the publisher can actually SEE (#3309),
543
+ # matching ``get_queryset``'s own narrowing (this program's templates plus
544
+ # the workspace-wide ones). A name-only lookup reached across programs and
545
+ # broke twice: Program B's republish superseded Program A's v1, so A's card
546
+ # rendered the ``superseded`` chip because of a row A cannot see and cannot
547
+ # find; and without ``new_version`` the 409 named — and gave the version of
548
+ # — a template B has no access to. A supersede must only ever occur inside
549
+ # a pool both parties can read.
550
+ #
551
+ # Own-program rows outrank workspace-wide ones on a tie, so a program that
552
+ # already owns this name extends ITS chain rather than forking whichever
553
+ # row happens to carry the higher version number.
554
+ scope = Q(program=project.program) | Q(program__isnull=True)
555
+ existing = (
556
+ ProjectTemplate.objects.filter(scope, name=name[:200])
557
+ .annotate(
558
+ _shared_pool=Case(
559
+ When(program__isnull=True, then=Value(1)),
560
+ default=Value(0),
561
+ output_field=IntegerField(),
562
+ )
563
+ )
564
+ .order_by("_shared_pool", "-version")
565
+ .first()
566
+ )
567
+ supersede = _flag(body.get("new_version"))
568
+ if existing is not None and not supersede:
569
+ return Response(
570
+ {
571
+ "code": "name_taken",
572
+ "detail": (
573
+ f"“{existing.name}” already exists in this workspace "
574
+ f"(v{existing.version}). Publish a new version of it, or "
575
+ f"give this one a different name."
576
+ ),
577
+ "template": str(existing.pk),
578
+ "version": existing.version,
579
+ "next_version": existing.version + 1,
580
+ },
581
+ status=status.HTTP_409_CONFLICT,
582
+ )
583
+
584
+ template = ProjectTemplate.objects.create(
585
+ name=name[:200],
586
+ description=(raw_description or "")[:2000],
587
+ source_kind=source_kind,
588
+ owner=cast("Any", request.user),
589
+ published_by=cast("Any", request.user),
590
+ program=project.program,
591
+ source_project=project,
592
+ supersedes=existing,
593
+ version=(existing.version + 1) if existing is not None else 1,
594
+ structure=structure,
595
+ carries=structure.get("carries", []),
596
+ )
597
+ # A workspace-visible disclosure act: this puts one project's task names in
598
+ # front of everyone in the workspace, and a new version changes what the
599
+ # house shape resolves to for every future adopter. Written through the
600
+ # ADR-0157 choke point, inside this request's transaction, so the log can
601
+ # never claim a publish that rolled back.
602
+ record_audit_event(
603
+ event_type=AuditEventType.TEMPLATE_PUBLISHED,
604
+ actor=request.user,
605
+ target_type="project_template",
606
+ target_id=template.pk,
607
+ target_label=f"{template.name} v{template.version}",
608
+ metadata={
609
+ "version": template.version,
610
+ "source_project": str(project.pk),
611
+ "supersedes": str(existing.pk) if existing is not None else None,
612
+ "task_count": len(structure.get("tasks", [])),
613
+ "source_kind": source_kind,
614
+ },
615
+ )
616
+ return Response(
617
+ self.get_serializer(template).data,
618
+ status=status.HTTP_201_CREATED,
619
+ )
620
+
621
+ @extend_schema(
622
+ responses={
623
+ 200: OpenApiResponse(
624
+ description=(
625
+ "Dry run: what publishing this project would carry — "
626
+ "{task_count, phase_count, gate_count, milestone_count, "
627
+ "dependency_count, methodology, carries, name_taken, "
628
+ "next_version}. Writes nothing."
629
+ )
630
+ ),
631
+ 403: OpenApiResponse(description="Caller is not Admin+ on the project."),
632
+ },
633
+ description=(
634
+ "Counts for the publish form, computed from the same extraction publish itself runs."
635
+ ),
636
+ )
637
+ @action(detail=False, methods=["get"], url_path="publish-preview")
638
+ def publish_preview(self, request: Request) -> Response:
639
+ """The six counts the Settings page and the confirm step both show (#2909).
640
+
641
+ Runs the **same** ``extract_structure`` the publish action runs, so the
642
+ numbers on the confirm screen are the numbers that will be written — not
643
+ a client-side walk of whatever the Grid currently has loaded, which a
644
+ collapsed or filtered view would get wrong.
645
+
646
+ Also reports whether the proposed name is taken, so the form can offer
647
+ "publish as v3 instead" before the operator fills anything in rather than
648
+ after a 409.
649
+ """
650
+ project_id = request.query_params.get("project")
651
+ if not project_id:
652
+ raise ValidationError({"project": "Required."})
653
+ try:
654
+ project = Project.objects.get(pk=project_id, is_deleted=False)
655
+ except (Project.DoesNotExist, ValueError, TypeError, DjangoValidationError) as exc:
656
+ raise ValidationError({"project": _NO_SUCH_PROJECT_DETAIL}) from exc
657
+ self._require_project_admin(project)
658
+
659
+ try:
660
+ structure = extract_structure(project)
661
+ except TemplateStructureError as exc:
662
+ raise ValidationError({"project": str(exc)}) from exc
663
+
664
+ summary = summarize_structure(structure)
665
+ proposed = (request.query_params.get("name") or "").strip()
666
+ existing = (
667
+ ProjectTemplate.objects.filter(name=proposed[:200]).order_by("-version").first()
668
+ if proposed
669
+ else None
670
+ )
671
+ summary["name_taken"] = existing is not None
672
+ summary["next_version"] = (existing.version + 1) if existing is not None else 1
673
+ summary["existing_template"] = str(existing.pk) if existing is not None else None
674
+ return Response(summary, status=status.HTTP_200_OK)
675
+
676
+ @extend_schema(
677
+ request=_ApplyRequestSerializer,
678
+ responses={
679
+ 202: OpenApiResponse(description='{"queued": true, "application": "<uuid>"}'),
680
+ 403: OpenApiResponse(description=_ARCHIVED_403),
681
+ },
682
+ description="Apply this template to a project. Returns 202; seeding runs async.",
683
+ )
684
+ @action(
685
+ detail=True,
686
+ methods=["post"],
687
+ url_path="apply",
688
+ throttle_classes=[SeedImportThrottle],
689
+ )
690
+ def apply(self, request: Request, pk: str | None = None) -> Response:
691
+ """Queue a seeding job and hand back the application id.
692
+
693
+ Returns ``202 {"queued": true}`` and **not** a Celery task id: dispatch is
694
+ best-effort behind an outbox, so there may be no task id yet (or ever, for
695
+ the delivery that gets dropped and re-dispatched by the drain). The
696
+ application id is the durable handle — it exists the moment the request
697
+ commits, and it is what the Start sheet polls.
698
+ """
699
+ template = self.get_object()
700
+ project_id = object_body(request).get("project")
701
+ if not project_id:
702
+ raise ValidationError({"project": "This field is required."})
703
+ try:
704
+ project = Project.objects.get(pk=cast("Any", project_id), is_deleted=False)
705
+ # `DjangoValidationError` is what `UUIDField.to_python` raises on a malformed
706
+ # pk, and DRF does not convert it — without it in this tuple a non-UUID
707
+ # `project` is a 500, reachable by any authenticated user because this line
708
+ # runs before `_require_project_admin`. `publish_preview` above already caught
709
+ # it; this call site had drifted (the #2785 class).
710
+ except (Project.DoesNotExist, ValueError, TypeError, DjangoValidationError) as exc:
711
+ raise ValidationError({"project": _NO_SUCH_PROJECT_DETAIL}) from exc
712
+ self._require_project_admin(project)
713
+ # The forward half of the same gap (#3354). `IsProjectNotArchived` cannot run
714
+ # on this action: it resolves its target project from the request *body*, so
715
+ # `has_permission` finds no `project_pk` kwarg and passes, and `get_object()`
716
+ # returns the `ProjectTemplate` — which carries no `project_id` — so
717
+ # `has_object_permission` reads None and passes too. Seeding a whole
718
+ # template's worth of rows into an archived plan is the largest write in this
719
+ # family and has to be refused explicitly. `enqueue_template_apply` repeats
720
+ # the check for non-view callers (ADR-0184 defense-in-depth).
721
+ #
722
+ # Deliberately NOT applied to `publish`/`publish_preview` above: those read
723
+ # the source project and write a workspace-level `ProjectTemplate` row. The
724
+ # archived flag makes a plan read-only, and extracting a template from one is
725
+ # a read of it.
726
+ assert_project_not_archived(project)
727
+
728
+ # Re-validate here, not only at publish: `structure` is a JSONB column, so
729
+ # the row can be edited by any path that reaches the database, and this is
730
+ # the request that turns it into rows in somebody's project.
731
+ try:
732
+ validate_structure(template.structure)
733
+ except TemplateStructureError as exc:
734
+ raise ValidationError({"template": str(exc)}) from exc
735
+
736
+ application = enqueue_template_apply(
737
+ template, project, applied_by=cast("Any", request.user)
738
+ )
739
+ return Response(
740
+ {"queued": True, "application": str(application.id)},
741
+ status=status.HTTP_202_ACCEPTED,
742
+ )
743
+
744
+
745
+ class TemplateApplicationViewSet(
746
+ IdempotencyMixin, viewsets.ReadOnlyModelViewSet[TemplateApplication]
747
+ ):
748
+ """Poll an application's progress, and undo it.
749
+
750
+ Read is scoped to projects the caller is a member of — an application row names
751
+ a project and a person, so an unscoped list would leak both.
752
+ """
753
+
754
+ serializer_class = TemplateApplicationSerializer
755
+ # `undo` soft-deletes the task rows this application wrote, so this route is a
756
+ # write path and takes the archived floor — the same gap #3354 found on the two
757
+ # `batch_operation_views` ledgers, which were written to mirror this viewset's
758
+ # shape and inherited its omission along with it. Reads pass untouched
759
+ # (SAFE_METHODS), so the Start sheet can still poll an archived project's
760
+ # application row. The project resolves from the object's `project_id` in
761
+ # `has_object_permission`, not a URL kwarg — see `PasteManyOperationViewSet`.
762
+ permission_classes = [IsAuthenticated, IsProjectNotArchived] # noqa: RUF012
763
+
764
+ def get_queryset(self) -> QuerySet[TemplateApplication]:
765
+ from trueppm_api.apps.access.models import ProjectMembership
766
+
767
+ member_project_ids = ProjectMembership.objects.filter(
768
+ user=cast("Any", self.request.user), is_deleted=False, project__is_deleted=False
769
+ ).values_list("project_id", flat=True)
770
+ qs = TemplateApplication.objects.filter(project_id__in=member_project_ids)
771
+ project_id = self.request.query_params.get("project")
772
+ if project_id:
773
+ qs = qs.filter(project_id=project_id)
774
+ return qs
775
+
776
+ @extend_schema(
777
+ request=None,
778
+ responses={
779
+ 200: TemplateApplicationUndoSerializer,
780
+ 400: state_refusal_400(
781
+ "The application is not in a state that can be undone — it never "
782
+ "reached SUCCESS, or it has already been undone. Verified against "
783
+ "the status guard in ``undo`` (#3319)."
784
+ ),
785
+ 403: OpenApiResponse(description=_ARCHIVED_403),
786
+ },
787
+ description="Undo this application — removes the rows it wrote that nobody has edited.",
788
+ )
789
+ @action(detail=True, methods=["post"], url_path="undo")
790
+ def undo(self, request: Request, pk: str | None = None) -> Response:
791
+ """Reverse one application in a single step.
792
+
793
+ Rows a person has since touched are **kept**, not deleted — see
794
+ ``undo_template_application``. Undoing an application that never succeeded,
795
+ or has already been undone, is a 400 rather than a silent no-op: the caller
796
+ asked to reverse something and deserves to know it did not happen.
797
+ """
798
+ application = self.get_object()
799
+ # Defers to `role_can_undo_batch_operation` rather than testing the ordinal
800
+ # here: this is the same "may you reverse a recorded batch write" rule the
801
+ # paste-many, cascade and CSV-import undos enforce, and three apply endpoints
802
+ # now report it to the client as `can_undo`. An inline copy here is a second
803
+ # implementation of a rule that is itself under revision (#3355) — #3353.
804
+ #
805
+ # `_require_project_admin` above is deliberately NOT folded in: it gates
806
+ # PUBLISHING and APPLYING a template, a different rule that happens to share
807
+ # today's ordinal (ADR-0773). Merging them would make one edit move two floors.
808
+ #
809
+ # Be explicit about what this call DOES couple, so #3355 has to confront it:
810
+ # template apply/undo is the one SYMMETRIC member of the family (Admin+ both
811
+ # ways), so lowering the shared batch-undo floor would loosen undo here without
812
+ # touching apply — a Member could reverse an application they could not apply.
813
+ # Today's behavior is unchanged; the blast radius is not. Whoever resolves
814
+ # #3355 must decide whether this endpoint follows or gets its own predicate.
815
+ role = _membership_role(request, cast("Any", application.project_id))
816
+ if not role_can_undo_batch_operation(role):
817
+ raise PermissionDenied("You need at least Project Manager role to undo a template.")
818
+ if application.status != TemplateApplicationStatus.SUCCESS:
819
+ raise ValidationError(
820
+ {
821
+ "detail": f"Only a completed application can be undone (this one is "
822
+ f"{application.status})."
823
+ }
824
+ )
825
+ summary = undo_template_application(application)
826
+ application.refresh_from_db()
827
+ data: dict[str, Any] = self.get_serializer(application).data
828
+ data["undo"] = summary
829
+ return Response(data, status=status.HTTP_200_OK)