trueppm-api 0.4.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (759) hide show
  1. trueppm_api/__init__.py +14 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +164 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  24. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  25. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  26. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  27. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
  28. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  29. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  30. trueppm_api/apps/access/migrations/__init__.py +0 -0
  31. trueppm_api/apps/access/models.py +588 -0
  32. trueppm_api/apps/access/permissions.py +2837 -0
  33. trueppm_api/apps/access/serializers.py +946 -0
  34. trueppm_api/apps/access/services.py +667 -0
  35. trueppm_api/apps/access/signals.py +103 -0
  36. trueppm_api/apps/access/tasks.py +50 -0
  37. trueppm_api/apps/access/throttles.py +95 -0
  38. trueppm_api/apps/access/urls.py +191 -0
  39. trueppm_api/apps/access/views.py +1931 -0
  40. trueppm_api/apps/agents/__init__.py +6 -0
  41. trueppm_api/apps/agents/apps.py +11 -0
  42. trueppm_api/apps/agents/canonical.py +73 -0
  43. trueppm_api/apps/agents/deferred.py +108 -0
  44. trueppm_api/apps/agents/management/__init__.py +0 -0
  45. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  46. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  47. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  48. trueppm_api/apps/agents/middleware.py +44 -0
  49. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  50. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  51. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  52. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  53. trueppm_api/apps/agents/models.py +371 -0
  54. trueppm_api/apps/agents/refusal.py +103 -0
  55. trueppm_api/apps/agents/serializers.py +85 -0
  56. trueppm_api/apps/agents/services.py +397 -0
  57. trueppm_api/apps/agents/signals.py +30 -0
  58. trueppm_api/apps/agents/urls.py +15 -0
  59. trueppm_api/apps/agents/views.py +108 -0
  60. trueppm_api/apps/csvimport/__init__.py +12 -0
  61. trueppm_api/apps/csvimport/apps.py +11 -0
  62. trueppm_api/apps/csvimport/mapping.py +363 -0
  63. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  64. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  65. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  66. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  67. trueppm_api/apps/csvimport/models.py +111 -0
  68. trueppm_api/apps/csvimport/parser.py +1825 -0
  69. trueppm_api/apps/csvimport/services.py +48 -0
  70. trueppm_api/apps/csvimport/tasks.py +418 -0
  71. trueppm_api/apps/csvimport/template.py +37 -0
  72. trueppm_api/apps/csvimport/urls.py +50 -0
  73. trueppm_api/apps/csvimport/views.py +793 -0
  74. trueppm_api/apps/history/__init__.py +0 -0
  75. trueppm_api/apps/history/apps.py +12 -0
  76. trueppm_api/apps/history/changelog.py +359 -0
  77. trueppm_api/apps/history/diff_policy.py +177 -0
  78. trueppm_api/apps/history/serializers.py +104 -0
  79. trueppm_api/apps/history/signals.py +82 -0
  80. trueppm_api/apps/history/tasks.py +105 -0
  81. trueppm_api/apps/history/urls.py +38 -0
  82. trueppm_api/apps/history/views.py +443 -0
  83. trueppm_api/apps/idempotency/__init__.py +0 -0
  84. trueppm_api/apps/idempotency/apps.py +11 -0
  85. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  86. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  87. trueppm_api/apps/idempotency/mixins.py +226 -0
  88. trueppm_api/apps/idempotency/models.py +75 -0
  89. trueppm_api/apps/idempotency/tasks.py +61 -0
  90. trueppm_api/apps/integrations/__init__.py +13 -0
  91. trueppm_api/apps/integrations/apps.py +59 -0
  92. trueppm_api/apps/integrations/connections.py +806 -0
  93. trueppm_api/apps/integrations/encryption.py +92 -0
  94. trueppm_api/apps/integrations/external_sources.py +837 -0
  95. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  96. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  97. trueppm_api/apps/integrations/http.py +403 -0
  98. trueppm_api/apps/integrations/me_work.py +218 -0
  99. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  100. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  101. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  102. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  103. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  104. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  105. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  106. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  107. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  108. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  109. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  110. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  111. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  112. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  113. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  114. trueppm_api/apps/integrations/models.py +622 -0
  115. trueppm_api/apps/integrations/notification_channels.py +57 -0
  116. trueppm_api/apps/integrations/opengraph.py +229 -0
  117. trueppm_api/apps/integrations/outgoing.py +201 -0
  118. trueppm_api/apps/integrations/providers.py +519 -0
  119. trueppm_api/apps/integrations/registry.py +345 -0
  120. trueppm_api/apps/integrations/serializers.py +453 -0
  121. trueppm_api/apps/integrations/services.py +171 -0
  122. trueppm_api/apps/integrations/tasks.py +731 -0
  123. trueppm_api/apps/integrations/throttles.py +165 -0
  124. trueppm_api/apps/integrations/urls.py +77 -0
  125. trueppm_api/apps/integrations/views.py +1106 -0
  126. trueppm_api/apps/jiraimport/__init__.py +8 -0
  127. trueppm_api/apps/jiraimport/apps.py +11 -0
  128. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  129. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  130. trueppm_api/apps/jiraimport/models.py +70 -0
  131. trueppm_api/apps/jiraimport/parser.py +350 -0
  132. trueppm_api/apps/jiraimport/services.py +46 -0
  133. trueppm_api/apps/jiraimport/tasks.py +277 -0
  134. trueppm_api/apps/jiraimport/urls.py +15 -0
  135. trueppm_api/apps/jiraimport/views.py +153 -0
  136. trueppm_api/apps/msproject/__init__.py +0 -0
  137. trueppm_api/apps/msproject/apps.py +11 -0
  138. trueppm_api/apps/msproject/dataclasses.py +153 -0
  139. trueppm_api/apps/msproject/exporter.py +500 -0
  140. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  141. trueppm_api/apps/msproject/importer.py +1095 -0
  142. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  143. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  144. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  145. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  146. trueppm_api/apps/msproject/models.py +81 -0
  147. trueppm_api/apps/msproject/parser.py +1231 -0
  148. trueppm_api/apps/msproject/serializers.py +136 -0
  149. trueppm_api/apps/msproject/services.py +58 -0
  150. trueppm_api/apps/msproject/tasks.py +436 -0
  151. trueppm_api/apps/msproject/urls.py +39 -0
  152. trueppm_api/apps/msproject/views.py +531 -0
  153. trueppm_api/apps/notifications/__init__.py +7 -0
  154. trueppm_api/apps/notifications/apps.py +15 -0
  155. trueppm_api/apps/notifications/backfill.py +79 -0
  156. trueppm_api/apps/notifications/categories.py +112 -0
  157. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  158. trueppm_api/apps/notifications/digests.py +447 -0
  159. trueppm_api/apps/notifications/email_backend.py +330 -0
  160. trueppm_api/apps/notifications/email_health.py +128 -0
  161. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  162. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
  163. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  164. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  165. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  166. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  167. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  168. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  169. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  170. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  171. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  172. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  173. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  174. trueppm_api/apps/notifications/models.py +1197 -0
  175. trueppm_api/apps/notifications/receivers.py +155 -0
  176. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  177. trueppm_api/apps/notifications/serializers.py +961 -0
  178. trueppm_api/apps/notifications/services.py +1451 -0
  179. trueppm_api/apps/notifications/tasks.py +711 -0
  180. trueppm_api/apps/notifications/throttles.py +98 -0
  181. trueppm_api/apps/notifications/urls.py +97 -0
  182. trueppm_api/apps/notifications/views.py +904 -0
  183. trueppm_api/apps/observability/__init__.py +0 -0
  184. trueppm_api/apps/observability/apps.py +31 -0
  185. trueppm_api/apps/observability/logging.py +213 -0
  186. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  187. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  188. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  189. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  190. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  191. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  192. trueppm_api/apps/observability/models.py +141 -0
  193. trueppm_api/apps/observability/otel/__init__.py +45 -0
  194. trueppm_api/apps/observability/otel/attributes.py +247 -0
  195. trueppm_api/apps/observability/otel/export_health.py +533 -0
  196. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  197. trueppm_api/apps/observability/otel/metrics.py +551 -0
  198. trueppm_api/apps/observability/otel/provider.py +511 -0
  199. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  200. trueppm_api/apps/observability/purge_registry.py +124 -0
  201. trueppm_api/apps/observability/retention.py +139 -0
  202. trueppm_api/apps/observability/selectors.py +969 -0
  203. trueppm_api/apps/observability/serializers.py +142 -0
  204. trueppm_api/apps/observability/services.py +407 -0
  205. trueppm_api/apps/observability/tasks.py +266 -0
  206. trueppm_api/apps/observability/urls.py +27 -0
  207. trueppm_api/apps/observability/views.py +523 -0
  208. trueppm_api/apps/profiles/__init__.py +0 -0
  209. trueppm_api/apps/profiles/apps.py +15 -0
  210. trueppm_api/apps/profiles/constants.py +54 -0
  211. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  212. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  213. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  214. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  215. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  216. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  217. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  218. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  219. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  220. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  221. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  222. trueppm_api/apps/profiles/models.py +276 -0
  223. trueppm_api/apps/profiles/serializers.py +199 -0
  224. trueppm_api/apps/profiles/services.py +466 -0
  225. trueppm_api/apps/profiles/views.py +124 -0
  226. trueppm_api/apps/projects/__init__.py +0 -0
  227. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  228. trueppm_api/apps/projects/amend.py +133 -0
  229. trueppm_api/apps/projects/apps.py +38 -0
  230. trueppm_api/apps/projects/asset_feed.py +459 -0
  231. trueppm_api/apps/projects/asset_views.py +317 -0
  232. trueppm_api/apps/projects/attachment_policy.py +278 -0
  233. trueppm_api/apps/projects/authentication.py +449 -0
  234. trueppm_api/apps/projects/backfill.py +287 -0
  235. trueppm_api/apps/projects/backlog_services.py +324 -0
  236. trueppm_api/apps/projects/backlog_views.py +302 -0
  237. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  238. trueppm_api/apps/projects/blocker_services.py +510 -0
  239. trueppm_api/apps/projects/board_activity.py +462 -0
  240. trueppm_api/apps/projects/board_activity_views.py +186 -0
  241. trueppm_api/apps/projects/board_lanes.py +134 -0
  242. trueppm_api/apps/projects/bulk_settings.py +194 -0
  243. trueppm_api/apps/projects/bundled_templates.py +215 -0
  244. trueppm_api/apps/projects/calendar_settings.py +180 -0
  245. trueppm_api/apps/projects/ceremony_views.py +244 -0
  246. trueppm_api/apps/projects/commit_moment.py +204 -0
  247. trueppm_api/apps/projects/config_notice.py +1325 -0
  248. trueppm_api/apps/projects/custom_field_values.py +266 -0
  249. trueppm_api/apps/projects/decisions_services.py +93 -0
  250. trueppm_api/apps/projects/decisions_views.py +94 -0
  251. trueppm_api/apps/projects/estimation_scale.py +126 -0
  252. trueppm_api/apps/projects/export_bundle.py +353 -0
  253. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
  254. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
  255. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
  256. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
  257. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
  258. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  259. trueppm_api/apps/projects/inbound_sync.py +555 -0
  260. trueppm_api/apps/projects/iteration_label.py +85 -0
  261. trueppm_api/apps/projects/lifecycle.py +191 -0
  262. trueppm_api/apps/projects/management/__init__.py +0 -0
  263. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  264. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  265. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  266. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  267. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  268. trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
  269. trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
  270. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
  271. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  272. trueppm_api/apps/projects/mcp_settings.py +305 -0
  273. trueppm_api/apps/projects/methodology.py +178 -0
  274. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  275. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
  276. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  277. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  278. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  279. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  280. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  281. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  282. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  283. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  284. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  285. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  286. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  287. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  288. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  289. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  290. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  291. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  292. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  293. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  294. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  295. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  296. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  297. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  298. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  299. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  300. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  301. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  302. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  303. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  304. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  305. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  306. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  307. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  308. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  309. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  310. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  311. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  312. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  313. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  314. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  315. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  316. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  317. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  318. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  319. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  320. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  321. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  322. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  323. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  324. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  325. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  326. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  327. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  328. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  329. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  330. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  331. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  332. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  333. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  334. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  335. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  336. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  337. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  338. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  339. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  340. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  341. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  342. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  343. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  344. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  345. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  346. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  347. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  348. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  349. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  350. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  351. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  352. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  353. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  354. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  355. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  356. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  357. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  358. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  359. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  360. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  361. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  362. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  363. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  364. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  365. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  366. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  367. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  368. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
  369. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  370. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  371. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  372. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  373. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  374. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  375. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  376. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  377. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  378. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  379. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
  380. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  381. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  382. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  383. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  384. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  385. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
  386. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  387. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  388. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  389. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  390. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  391. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  392. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  393. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  394. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  395. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  396. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  397. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  398. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  399. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  400. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  401. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  402. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  403. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  404. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  405. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  406. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  407. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  408. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  409. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  410. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  411. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  412. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  413. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  414. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  415. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  416. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  417. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  418. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  419. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  420. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  421. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  422. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  423. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  424. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  425. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  426. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  427. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  428. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  429. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  430. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  431. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  432. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  433. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  434. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  435. trueppm_api/apps/projects/models.py +8946 -0
  436. trueppm_api/apps/projects/poker_services.py +154 -0
  437. trueppm_api/apps/projects/poker_views.py +398 -0
  438. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  439. trueppm_api/apps/projects/program_label_services.py +94 -0
  440. trueppm_api/apps/projects/program_label_views.py +189 -0
  441. trueppm_api/apps/projects/program_rollup.py +575 -0
  442. trueppm_api/apps/projects/program_schedule.py +745 -0
  443. trueppm_api/apps/projects/program_views.py +3220 -0
  444. trueppm_api/apps/projects/project_templates.py +422 -0
  445. trueppm_api/apps/projects/receivers.py +234 -0
  446. trueppm_api/apps/projects/refusal_codes.py +140 -0
  447. trueppm_api/apps/projects/reorder_services.py +145 -0
  448. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  449. trueppm_api/apps/projects/retro_board_services.py +368 -0
  450. trueppm_api/apps/projects/retro_services.py +285 -0
  451. trueppm_api/apps/projects/risk_import.py +396 -0
  452. trueppm_api/apps/projects/row_identity.py +113 -0
  453. trueppm_api/apps/projects/schema_migrations.py +228 -0
  454. trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
  455. trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
  456. trueppm_api/apps/projects/seed/__init__.py +61 -0
  457. trueppm_api/apps/projects/seed/exporter.py +1644 -0
  458. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  459. trueppm_api/apps/projects/seed/importer.py +2390 -0
  460. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  461. trueppm_api/apps/projects/seed/reldates.py +126 -0
  462. trueppm_api/apps/projects/seed/replace.py +126 -0
  463. trueppm_api/apps/projects/seed/replay.py +1691 -0
  464. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  465. trueppm_api/apps/projects/seed/samples.py +427 -0
  466. trueppm_api/apps/projects/seed/validation.py +1149 -0
  467. trueppm_api/apps/projects/serializers.py +12310 -0
  468. trueppm_api/apps/projects/services.py +6966 -0
  469. trueppm_api/apps/projects/share_serializers.py +132 -0
  470. trueppm_api/apps/projects/share_services.py +325 -0
  471. trueppm_api/apps/projects/share_views.py +374 -0
  472. trueppm_api/apps/projects/sharing_settings.py +187 -0
  473. trueppm_api/apps/projects/signal_privacy_services.py +909 -0
  474. trueppm_api/apps/projects/signal_privacy_views.py +476 -0
  475. trueppm_api/apps/projects/signals.py +132 -0
  476. trueppm_api/apps/projects/slip_conflict.py +266 -0
  477. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  478. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  479. trueppm_api/apps/projects/standup.py +304 -0
  480. trueppm_api/apps/projects/standup_views.py +99 -0
  481. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  482. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  483. trueppm_api/apps/projects/surface_visibility.py +126 -0
  484. trueppm_api/apps/projects/task_batch_services.py +379 -0
  485. trueppm_api/apps/projects/task_bulk.py +1152 -0
  486. trueppm_api/apps/projects/task_classification.py +561 -0
  487. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  488. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  489. trueppm_api/apps/projects/task_grouping.py +754 -0
  490. trueppm_api/apps/projects/tasks.py +1917 -0
  491. trueppm_api/apps/projects/template_divergence.py +168 -0
  492. trueppm_api/apps/projects/template_services.py +160 -0
  493. trueppm_api/apps/projects/template_tasks.py +258 -0
  494. trueppm_api/apps/projects/template_views.py +799 -0
  495. trueppm_api/apps/projects/throttles.py +311 -0
  496. trueppm_api/apps/projects/urls.py +1011 -0
  497. trueppm_api/apps/projects/utilization.py +885 -0
  498. trueppm_api/apps/projects/views.py +20572 -0
  499. trueppm_api/apps/projects/wbs_paths.py +119 -0
  500. trueppm_api/apps/resources/__init__.py +0 -0
  501. trueppm_api/apps/resources/apps.py +8 -0
  502. trueppm_api/apps/resources/capacity.py +121 -0
  503. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  504. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  505. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  506. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  507. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  508. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  509. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  510. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  511. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  512. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  513. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  514. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  515. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  516. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  517. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  518. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  519. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  520. trueppm_api/apps/resources/models.py +298 -0
  521. trueppm_api/apps/resources/serializers.py +426 -0
  522. trueppm_api/apps/resources/services.py +465 -0
  523. trueppm_api/apps/resources/urls.py +28 -0
  524. trueppm_api/apps/resources/views.py +1749 -0
  525. trueppm_api/apps/scheduling/__init__.py +0 -0
  526. trueppm_api/apps/scheduling/apps.py +153 -0
  527. trueppm_api/apps/scheduling/calendars.py +119 -0
  528. trueppm_api/apps/scheduling/deadletter.py +91 -0
  529. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  530. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  531. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  532. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  533. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  534. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  535. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  536. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  537. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  538. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  539. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  540. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  541. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  542. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  543. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  544. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  545. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  546. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  547. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  548. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  549. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  550. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  551. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  552. trueppm_api/apps/scheduling/models.py +516 -0
  553. trueppm_api/apps/scheduling/receivers.py +160 -0
  554. trueppm_api/apps/scheduling/risk_premium.py +283 -0
  555. trueppm_api/apps/scheduling/serializers.py +564 -0
  556. trueppm_api/apps/scheduling/services.py +1182 -0
  557. trueppm_api/apps/scheduling/signals.py +56 -0
  558. trueppm_api/apps/scheduling/tasks.py +1984 -0
  559. trueppm_api/apps/scheduling/telemetry.py +80 -0
  560. trueppm_api/apps/scheduling/units.py +35 -0
  561. trueppm_api/apps/scheduling/urls.py +57 -0
  562. trueppm_api/apps/scheduling/views.py +2564 -0
  563. trueppm_api/apps/sso/__init__.py +0 -0
  564. trueppm_api/apps/sso/apps.py +19 -0
  565. trueppm_api/apps/sso/extensions.py +130 -0
  566. trueppm_api/apps/sso/management/__init__.py +0 -0
  567. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  568. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  569. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  570. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  571. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  572. trueppm_api/apps/sso/models.py +148 -0
  573. trueppm_api/apps/sso/serializers.py +437 -0
  574. trueppm_api/apps/sso/services.py +1385 -0
  575. trueppm_api/apps/sso/urls.py +47 -0
  576. trueppm_api/apps/sso/views.py +935 -0
  577. trueppm_api/apps/sync/__init__.py +0 -0
  578. trueppm_api/apps/sync/apps.py +15 -0
  579. trueppm_api/apps/sync/backfill.py +208 -0
  580. trueppm_api/apps/sync/broadcast.py +320 -0
  581. trueppm_api/apps/sync/conflict.py +262 -0
  582. trueppm_api/apps/sync/consumers.py +471 -0
  583. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  584. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  585. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  586. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  587. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  588. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  589. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  590. trueppm_api/apps/sync/models.py +225 -0
  591. trueppm_api/apps/sync/pagination.py +231 -0
  592. trueppm_api/apps/sync/sequence.py +371 -0
  593. trueppm_api/apps/sync/serializers.py +687 -0
  594. trueppm_api/apps/sync/tasks.py +260 -0
  595. trueppm_api/apps/sync/throttles.py +175 -0
  596. trueppm_api/apps/sync/upload.py +501 -0
  597. trueppm_api/apps/sync/urls.py +17 -0
  598. trueppm_api/apps/sync/views.py +930 -0
  599. trueppm_api/apps/sync/ws_auth.py +193 -0
  600. trueppm_api/apps/taskruns/__init__.py +0 -0
  601. trueppm_api/apps/taskruns/apps.py +11 -0
  602. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  603. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  604. trueppm_api/apps/taskruns/models.py +68 -0
  605. trueppm_api/apps/taskruns/serializers.py +92 -0
  606. trueppm_api/apps/taskruns/tasks.py +60 -0
  607. trueppm_api/apps/taskruns/tracker.py +286 -0
  608. trueppm_api/apps/taskruns/urls.py +46 -0
  609. trueppm_api/apps/taskruns/views.py +195 -0
  610. trueppm_api/apps/teams/__init__.py +8 -0
  611. trueppm_api/apps/teams/apps.py +16 -0
  612. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  613. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
  614. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  615. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  616. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  617. trueppm_api/apps/teams/models.py +145 -0
  618. trueppm_api/apps/teams/permissions.py +149 -0
  619. trueppm_api/apps/teams/serializers.py +95 -0
  620. trueppm_api/apps/teams/services.py +434 -0
  621. trueppm_api/apps/teams/signals.py +61 -0
  622. trueppm_api/apps/teams/urls.py +34 -0
  623. trueppm_api/apps/teams/views.py +201 -0
  624. trueppm_api/apps/timetracking/__init__.py +0 -0
  625. trueppm_api/apps/timetracking/apps.py +9 -0
  626. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  627. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  628. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  629. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  630. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  631. trueppm_api/apps/timetracking/models.py +178 -0
  632. trueppm_api/apps/timetracking/serializers.py +175 -0
  633. trueppm_api/apps/timetracking/services.py +117 -0
  634. trueppm_api/apps/timetracking/urls.py +47 -0
  635. trueppm_api/apps/timetracking/views.py +526 -0
  636. trueppm_api/apps/webhooks/__init__.py +0 -0
  637. trueppm_api/apps/webhooks/apps.py +8 -0
  638. trueppm_api/apps/webhooks/backfill.py +90 -0
  639. trueppm_api/apps/webhooks/dispatch.py +144 -0
  640. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  641. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
  642. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  643. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  644. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  645. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  646. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  647. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  648. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  649. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
  650. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  651. trueppm_api/apps/webhooks/models.py +372 -0
  652. trueppm_api/apps/webhooks/serializers.py +318 -0
  653. trueppm_api/apps/webhooks/tasks.py +663 -0
  654. trueppm_api/apps/webhooks/urls.py +22 -0
  655. trueppm_api/apps/webhooks/views.py +383 -0
  656. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  657. trueppm_api/apps/workflow_engine/apps.py +23 -0
  658. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  659. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  660. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  661. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  662. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  663. trueppm_api/apps/workflow_engine/models.py +265 -0
  664. trueppm_api/apps/workflow_engine/tasks.py +368 -0
  665. trueppm_api/apps/workspace/__init__.py +0 -0
  666. trueppm_api/apps/workspace/apps.py +10 -0
  667. trueppm_api/apps/workspace/backfill.py +111 -0
  668. trueppm_api/apps/workspace/export.py +242 -0
  669. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  670. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  671. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  672. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  673. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  674. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  675. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  676. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  677. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  678. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  679. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  680. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  681. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  682. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  683. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  684. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  685. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  686. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  687. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  688. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  689. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  690. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  691. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  692. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  693. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  694. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  695. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  696. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  697. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  698. trueppm_api/apps/workspace/models.py +857 -0
  699. trueppm_api/apps/workspace/permissions.py +187 -0
  700. trueppm_api/apps/workspace/serializers.py +590 -0
  701. trueppm_api/apps/workspace/services.py +919 -0
  702. trueppm_api/apps/workspace/signals.py +37 -0
  703. trueppm_api/apps/workspace/tasks.py +555 -0
  704. trueppm_api/apps/workspace/urls.py +113 -0
  705. trueppm_api/apps/workspace/views.py +1502 -0
  706. trueppm_api/asgi.py +42 -0
  707. trueppm_api/celery.py +31 -0
  708. trueppm_api/core/__init__.py +0 -0
  709. trueppm_api/core/admin_site.py +206 -0
  710. trueppm_api/core/auth_views.py +916 -0
  711. trueppm_api/core/constant_time.py +51 -0
  712. trueppm_api/core/csp.py +54 -0
  713. trueppm_api/core/db.py +87 -0
  714. trueppm_api/core/db_session.py +37 -0
  715. trueppm_api/core/exception_handlers.py +157 -0
  716. trueppm_api/core/export_downloads.py +56 -0
  717. trueppm_api/core/extension_providers.py +56 -0
  718. trueppm_api/core/extension_signals.py +147 -0
  719. trueppm_api/core/idempotent.py +260 -0
  720. trueppm_api/core/metadata.py +42 -0
  721. trueppm_api/core/middleware.py +51 -0
  722. trueppm_api/core/openapi.py +949 -0
  723. trueppm_api/core/password_reset.py +500 -0
  724. trueppm_api/core/protect_conflict.py +100 -0
  725. trueppm_api/core/ratelimit.py +148 -0
  726. trueppm_api/core/redis_throttle.py +82 -0
  727. trueppm_api/core/refresh_cookie_policy.py +156 -0
  728. trueppm_api/core/request_body.py +86 -0
  729. trueppm_api/core/security_checks.py +884 -0
  730. trueppm_api/core/storage_config.py +105 -0
  731. trueppm_api/core/text_decode.py +168 -0
  732. trueppm_api/core/throttling.py +344 -0
  733. trueppm_api/core/valkey.py +214 -0
  734. trueppm_api/core/valkey_cache.py +66 -0
  735. trueppm_api/core/valkey_checks.py +104 -0
  736. trueppm_api/core/valkey_config.py +39 -0
  737. trueppm_api/core/worker_broker_wait.py +149 -0
  738. trueppm_api/core/worker_heartbeat.py +134 -0
  739. trueppm_api/fields.py +36 -0
  740. trueppm_api/permissions.py +30 -0
  741. trueppm_api/py.typed +0 -0
  742. trueppm_api/routing.py +11 -0
  743. trueppm_api/settings/__init__.py +0 -0
  744. trueppm_api/settings/base.py +2455 -0
  745. trueppm_api/settings/dev.py +273 -0
  746. trueppm_api/settings/prod.py +228 -0
  747. trueppm_api/urls.py +153 -0
  748. trueppm_api/workflows/__init__.py +38 -0
  749. trueppm_api/workflows/backends/__init__.py +6 -0
  750. trueppm_api/workflows/backends/default.py +297 -0
  751. trueppm_api/workflows/consumers/__init__.py +11 -0
  752. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  753. trueppm_api/workflows/interface.py +148 -0
  754. trueppm_api/workflows/registry.py +106 -0
  755. trueppm_api/workflows/services.py +90 -0
  756. trueppm_api/wsgi.py +11 -0
  757. trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
  758. trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
  759. trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
@@ -0,0 +1,799 @@
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 — a Member should be able to see what skeletons exist
7
+ without being able to fire one at a project.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, cast
13
+
14
+ from django.core.exceptions import ValidationError as DjangoValidationError
15
+ from django.db.models import Case, Count, IntegerField, Q, QuerySet, Value, When
16
+ from drf_spectacular.utils import (
17
+ OpenApiResponse,
18
+ extend_schema,
19
+ extend_schema_field,
20
+ inline_serializer,
21
+ )
22
+ from rest_framework import serializers, status, viewsets
23
+ from rest_framework.decorators import action
24
+ from rest_framework.exceptions import PermissionDenied, ValidationError
25
+ from rest_framework.permissions import IsAuthenticated
26
+ from rest_framework.request import Request
27
+ from rest_framework.response import Response
28
+
29
+ from trueppm_api.apps.access.models import Role
30
+ from trueppm_api.apps.access.permissions import (
31
+ IsProjectNotArchived,
32
+ _membership_role,
33
+ assert_project_not_archived,
34
+ role_can_undo_batch_operation,
35
+ )
36
+ from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
37
+ from trueppm_api.apps.projects.models import (
38
+ Methodology,
39
+ Project,
40
+ ProjectTemplate,
41
+ TemplateApplication,
42
+ TemplateApplicationStatus,
43
+ TemplateSource,
44
+ )
45
+ from trueppm_api.apps.projects.program_views import SeedImportThrottle
46
+ from trueppm_api.apps.projects.project_templates import (
47
+ TemplateStructureError,
48
+ extract_structure,
49
+ summarize_structure,
50
+ validate_structure,
51
+ )
52
+ from trueppm_api.apps.projects.template_services import (
53
+ enqueue_template_apply,
54
+ undo_template_application,
55
+ )
56
+ from trueppm_api.apps.workspace.models import AuditEventType
57
+ from trueppm_api.apps.workspace.services import record_audit_event
58
+ from trueppm_api.core.openapi import state_refusal_400, undo_summary_schema
59
+ from trueppm_api.core.request_body import object_body
60
+
61
+ # Shared user-facing response details.
62
+ _NO_SUCH_PROJECT_DETAIL = "No such project."
63
+
64
+ #: `apply` and `undo` both refuse for two unrelated reasons that share a status
65
+ #: code. Spelling them out is the difference between a client retrying usefully
66
+ #: and a client retrying forever (#3354).
67
+ _ARCHIVED_403 = (
68
+ "Two causes, and a client must tell them apart: the caller is below Project "
69
+ "Manager (Admin) on the project, **or** the project is archived. The archived "
70
+ "case clears for no role — including Owner — so escalating the caller will "
71
+ "never succeed; the project has to be unarchived (#3354)."
72
+ )
73
+
74
+
75
+ def _publish_inputs(data: Any) -> tuple[Any, str, Any]:
76
+ """Read and type-check ``publish``'s three body fields, before any project read.
77
+
78
+ Split out of ``publish`` only to keep it readable; the order matters and is
79
+ preserved — every check here runs *before* the project lookup and therefore
80
+ before ``_require_project_admin``, so an unauthorized caller still cannot use a
81
+ malformed body to learn whether a project exists.
82
+
83
+ Same type-confusion class as #2785: a JSON list/dict/number is truthy, so
84
+ ``(value or "")`` hands it straight to .strip() / slicing and raises
85
+ AttributeError, and a non-string pk reaches UUIDField.to_python, which raises
86
+ Django's ValidationError — not one of the ValueError/TypeError caught by the
87
+ caller, and not something DRF converts. All three are 500s on input that has to
88
+ be a 400.
89
+
90
+ Returns:
91
+ The raw project pk, the stripped name, and the raw description.
92
+ """
93
+ project_id = data.get("project")
94
+ raw_name = data.get("name")
95
+ raw_description = data.get("description")
96
+ for field, value in (
97
+ ("project", project_id),
98
+ ("name", raw_name),
99
+ ("description", raw_description),
100
+ ):
101
+ if value is not None and not isinstance(value, str):
102
+ raise ValidationError({field: "Must be a string."})
103
+
104
+ name = (raw_name or "").strip()
105
+ if not project_id or not name:
106
+ raise ValidationError({"detail": "Both `project` and `name` are required."})
107
+ return project_id, name, raw_description
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Request declarations for the two hand-parsed bodies (#3319)
112
+ # ---------------------------------------------------------------------------
113
+ #
114
+ # Both handlers read their body themselves — ``object_body(request)`` plus
115
+ # ``_publish_inputs`` — rather than through a serializer, and both were previously
116
+ # declared ``request=None``. That is worse than a missing error branch: the
117
+ # published contract said the endpoint accepts nothing, so a generated client had
118
+ # no way to send the field the endpoint *requires*, and ``apply`` answered
119
+ # ``{"project": "This field is required."}`` naming a field the schema had never
120
+ # mentioned.
121
+ #
122
+ # These serializers are **declaration-only** — nothing calls ``is_valid()`` on
123
+ # them, and the handlers' own checks stay the source of truth for what is
124
+ # accepted. They exist so the schema states the shape those checks enforce. Keep
125
+ # them in step with ``_publish_inputs`` / ``apply``: a field added to one and not
126
+ # the other puts the contract back out of sync, which is the defect this fixes.
127
+ # Declaring a body also brings both operations under #3286's automatic 400
128
+ # injection, so the refusal each already emits is now typed for a client.
129
+ _PublishRequestSerializer = inline_serializer(
130
+ name="ProjectTemplatePublish",
131
+ fields={
132
+ "project": serializers.UUIDField(
133
+ help_text="Project whose shape is frozen into the template. Required."
134
+ ),
135
+ "name": serializers.CharField(
136
+ help_text=(
137
+ "Template name. Required, whitespace-stripped, and truncated to 200 "
138
+ "characters rather than rejected when longer."
139
+ )
140
+ ),
141
+ "description": serializers.CharField(
142
+ required=False,
143
+ allow_blank=True,
144
+ allow_null=True,
145
+ help_text="Truncated to 2000 characters rather than rejected when longer.",
146
+ ),
147
+ "source_kind": serializers.ChoiceField(
148
+ choices=TemplateSource.choices,
149
+ required=False,
150
+ help_text="Provenance chip. Defaults to `workspace`.",
151
+ ),
152
+ "new_version": serializers.BooleanField(
153
+ required=False,
154
+ help_text=(
155
+ "Publish as the next version of an existing same-named template "
156
+ "instead of receiving a 409. Read permissively: the strings "
157
+ '`"true"` and `"1"` are also accepted, anything else is false.'
158
+ ),
159
+ ),
160
+ },
161
+ )
162
+
163
+ _ApplyRequestSerializer = inline_serializer(
164
+ name="ProjectTemplateApply",
165
+ fields={
166
+ "project": serializers.UUIDField(help_text="Project to seed from this template. Required."),
167
+ },
168
+ )
169
+
170
+
171
+ def _flag(value: Any) -> bool:
172
+ """Read a permissive boolean off attacker-shaped ``request.data``.
173
+
174
+ Non-string input is the #2795 container-type class: a JSON list or dict is
175
+ truthy and reaches ``.lower()`` as an AttributeError 500. Coerced through
176
+ ``str`` so every shape lands on False rather than on a stack trace.
177
+ """
178
+ return str(value).lower() in {"true", "1"}
179
+
180
+
181
+ class ProjectTemplateSerializer(serializers.ModelSerializer[ProjectTemplate]):
182
+ """Gallery row. ``structure`` is deliberately **not** exposed.
183
+
184
+ The gallery renders a name, a description, a provenance chip, a methodology,
185
+ and what the template carries — never the document itself. Publishing the
186
+ structure on a list endpoint would put a whole project's shape (task names
187
+ included) in front of anyone who can read the gallery, which is a wider
188
+ audience than the source project's members.
189
+ """
190
+
191
+ task_count = serializers.SerializerMethodField()
192
+ provenance = serializers.SerializerMethodField()
193
+ methodology = serializers.SerializerMethodField()
194
+ counts = serializers.SerializerMethodField()
195
+ usage_count = serializers.SerializerMethodField()
196
+ published_by_name = serializers.SerializerMethodField()
197
+ source_project_name = serializers.SerializerMethodField()
198
+ is_superseded = serializers.SerializerMethodField()
199
+
200
+ class Meta:
201
+ model = ProjectTemplate
202
+ fields = [ # noqa: RUF012
203
+ "id",
204
+ "name",
205
+ "description",
206
+ "source_kind",
207
+ "provenance",
208
+ "carries",
209
+ "methodology",
210
+ "task_count",
211
+ "version",
212
+ "program",
213
+ "is_published",
214
+ "published_at",
215
+ "published_by",
216
+ "published_by_name",
217
+ "source_project",
218
+ "source_project_name",
219
+ "supersedes",
220
+ "is_superseded",
221
+ "counts",
222
+ "usage_count",
223
+ ]
224
+ read_only_fields = [ # noqa: RUF012
225
+ "id",
226
+ "source_kind",
227
+ "provenance",
228
+ "carries",
229
+ "methodology",
230
+ "task_count",
231
+ "version",
232
+ "published_at",
233
+ "published_by",
234
+ "published_by_name",
235
+ "source_project",
236
+ "source_project_name",
237
+ "supersedes",
238
+ "is_superseded",
239
+ "counts",
240
+ "usage_count",
241
+ ]
242
+
243
+ def get_counts(self, obj: ProjectTemplate) -> dict[str, Any]:
244
+ """Tasks, phases, gates and dependencies, off the frozen document (#2909).
245
+
246
+ Server-side because the publish inventory and the gallery's carries
247
+ summary are both screens whose credibility rests on a number, and a
248
+ client-side walk of a filtered tree would produce a different one.
249
+ """
250
+ return summarize_structure(obj.structure or {})
251
+
252
+ def get_usage_count(self, obj: ProjectTemplate) -> int:
253
+ """Projects created from this template — the PMO's only house-standard signal.
254
+
255
+ Annotated on the queryset where one is available; falls back to a count so
256
+ a serializer used outside the gallery (the publish response) still answers.
257
+ Undone applications are excluded: a template whose seeding was reversed
258
+ was not adopted, and counting it would overstate the standard.
259
+ """
260
+ annotated = getattr(obj, "adoption_count", None)
261
+ if annotated is not None:
262
+ return int(annotated)
263
+ return obj.applications.filter(
264
+ status=TemplateApplicationStatus.SUCCESS, undone_at__isnull=True
265
+ ).count()
266
+
267
+ def get_published_by_name(self, obj: ProjectTemplate) -> str:
268
+ user = obj.published_by or obj.owner
269
+ if user is None:
270
+ return ""
271
+ return str(getattr(user, "display_name", "") or getattr(user, "username", ""))
272
+
273
+ def get_source_project_name(self, obj: ProjectTemplate) -> str:
274
+ """Name of the project this was frozen from, or '' once it is gone.
275
+
276
+ Empty rather than absent when the FK has been nulled by a project
277
+ deletion: the provenance line simply drops, and the template still works.
278
+ """
279
+ return obj.source_project.name if obj.source_project else ""
280
+
281
+ def get_is_superseded(self, obj: ProjectTemplate) -> bool:
282
+ """A later version replaced this one. It stays selectable on purpose —
283
+ projects already created from it are why it must remain legible."""
284
+ return obj.superseded_by.exists()
285
+
286
+ def get_task_count(self, obj: ProjectTemplate) -> int:
287
+ """How many rows adopting this would write — the one number that decides adoption.
288
+
289
+ Read off the stored document rather than counted in SQL: the structure is
290
+ frozen at publish, so there is nothing to join against and no count that
291
+ could disagree with what apply will actually write.
292
+ """
293
+ structure = obj.structure or {}
294
+ tasks = structure.get("tasks")
295
+ return len(tasks) if isinstance(tasks, list) else 0
296
+
297
+ def get_methodology(self, obj: ProjectTemplate) -> str:
298
+ """The methodology the source project carried at publish time (ADR-0791).
299
+
300
+ Read off the frozen ``structure`` document, mirroring ``get_task_count`` —
301
+ never re-queries the (possibly since-edited/archived/deleted) source
302
+ project, and can never disagree with what ``apply`` will actually seed.
303
+ Falls back to ``Methodology.HYBRID`` (never null) for a structure written
304
+ before this key existed, or one edited outside the publish path — Hybrid is
305
+ the lossless default everywhere else in the methodology chain
306
+ (``methodology.DEFAULT_METHODOLOGY``), so an unresolved template under-hides
307
+ rather than over-hides tabs.
308
+ """
309
+ structure = obj.structure or {}
310
+ methodology = structure.get("methodology")
311
+ return methodology if methodology in Methodology.values else Methodology.HYBRID
312
+
313
+ def get_provenance(self, obj: ProjectTemplate) -> str:
314
+ """The chip label — "Workspace" / "Community" / "Yours".
315
+
316
+ ``source_kind`` is stored, so two readers agree on Workspace and Community.
317
+ *Yours* is the one label additionally narrowed at read time, because it is
318
+ the only one that is genuinely relative to who is asking (ADR-0789 §2).
319
+ """
320
+ request = self.context.get("request")
321
+ user = getattr(request, "user", None)
322
+ if obj.source_kind == TemplateSource.PERSONAL:
323
+ if user is not None and obj.owner_id == getattr(user, "id", None):
324
+ return "Yours"
325
+ # Somebody else's personal template that has been shared into view:
326
+ # it is not "Yours", and calling it Workspace would overstate its
327
+ # standing. Name the publisher's tier honestly.
328
+ return "Community"
329
+ return TemplateSource(obj.source_kind).label
330
+
331
+
332
+ class TemplateApplicationSerializer(serializers.ModelSerializer[TemplateApplication]):
333
+ """The adoption record — also what the Start sheet polls while seeding runs."""
334
+
335
+ class Meta:
336
+ model = TemplateApplication
337
+ fields = [ # noqa: RUF012
338
+ "id",
339
+ "template",
340
+ "template_name",
341
+ "template_version",
342
+ "project",
343
+ "applied_by",
344
+ "status",
345
+ "result_summary",
346
+ "error_detail",
347
+ "created_at",
348
+ "completed_at",
349
+ "undone_at",
350
+ ]
351
+ read_only_fields = fields
352
+
353
+
354
+ class TemplateApplicationUndoSerializer(TemplateApplicationSerializer):
355
+ """The adoption record **plus** the ``undo`` summary the action returns (#3416).
356
+
357
+ The handler builds its body as ``data = self.get_serializer(application).data``
358
+ then ``data["undo"] = summary``, so the bare ``TemplateApplicationSerializer``
359
+ published as the ``200`` gave a typed client no field for the deleted/kept split
360
+ — and that split is the whole point of the call here, because undo deliberately
361
+ *keeps* rows a person has since edited (ADR-0786 §4). A client that cannot read
362
+ ``kept`` cannot tell a full reversal from a partial one.
363
+ """
364
+
365
+ undo = serializers.SerializerMethodField()
366
+
367
+ class Meta(TemplateApplicationSerializer.Meta):
368
+ fields = [*TemplateApplicationSerializer.Meta.fields, "undo"] # noqa: RUF012
369
+
370
+ @extend_schema_field(
371
+ undo_summary_schema(
372
+ "What the undo did to the rows this application wrote.",
373
+ deleted="Rows removed — written by the application and untouched since.",
374
+ kept=(
375
+ "Rows left in place because someone has edited them since. A non-zero "
376
+ "value means the project still carries part of the template."
377
+ ),
378
+ )
379
+ )
380
+ def get_undo(self, obj: TemplateApplication) -> dict[str, int]:
381
+ """The persisted summary, for a caller that serializes a row outside the action.
382
+
383
+ Not what the action returns: ``undo_template_application`` short-circuits an
384
+ application that created no rows and returns ``{"deleted": 0, "kept": 0}``
385
+ without persisting a summary, so the action's live value is authoritative and
386
+ this is the best a later reader can do.
387
+ """
388
+ return dict((obj.result_summary or {}).get("undo") or {})
389
+
390
+
391
+ class ProjectTemplateViewSet(IdempotencyMixin, viewsets.ReadOnlyModelViewSet[ProjectTemplate]):
392
+ """Templates available to the caller, plus publish and apply.
393
+
394
+ **ReadOnly** on purpose, not for lack of ambition. A ``ModelViewSet`` here would
395
+ inherit create / update / destroy under this class's ``IsAuthenticated``, which
396
+ is the wrong gate for all three: any authenticated user could mint an empty
397
+ template, or rename and delete somebody else's. Publishing has exactly one door
398
+ (``publish``, Admin+ on the source project) and it is the door that runs the
399
+ extraction. Editing and unpublishing are deliberately not in this issue —
400
+ absent is safer than present-and-ungated.
401
+
402
+ Apply is throttled on the existing ``seed_import`` scope rather than a new one
403
+ (ADR-0789 §Context 3). Template apply is a third background seeding path
404
+ alongside seed import and CSV/MS Project import; #2615 remains the issue that
405
+ bounds per-user concurrency for all three. Reusing the scope inherits exactly
406
+ the bound the other paths have today and keeps that fix in one place, instead
407
+ of adding a fourth unbounded path.
408
+ """
409
+
410
+ serializer_class = ProjectTemplateSerializer
411
+ permission_classes = [IsAuthenticated] # noqa: RUF012
412
+
413
+ def get_queryset(self) -> QuerySet[ProjectTemplate]:
414
+ """Published templates in scope: workspace-wide, or this program's.
415
+
416
+ ``?program=`` narrows to a program's own templates *plus* the workspace-wide
417
+ ones — a program-scoped gallery that hid the shared skeletons would be
418
+ emptier than the user expects and would push them to republish duplicates.
419
+ """
420
+ qs = (
421
+ ProjectTemplate.objects.filter(is_published=True)
422
+ .select_related("owner", "published_by", "source_project")
423
+ # One aggregate rather than a per-row count in the serializer: the
424
+ # gallery renders every template at once, so a SerializerMethodField
425
+ # doing its own query is N+1 by construction (#2909).
426
+ .annotate(
427
+ adoption_count=Count(
428
+ "applications",
429
+ filter=Q(
430
+ applications__status=TemplateApplicationStatus.SUCCESS,
431
+ applications__undone_at__isnull=True,
432
+ ),
433
+ distinct=True,
434
+ )
435
+ )
436
+ # Provenance is the sort key as well as the chip (#2909): on a
437
+ # workspace that has published its own shapes, those are the ones
438
+ # worth reading first, and the bundled starters sort last. The chip
439
+ # stays on every row regardless of position — order is a nudge, not
440
+ # the information. Explicit rather than leaning on Meta.ordering,
441
+ # which an annotate() drops and which paginates unordered.
442
+ .order_by(
443
+ Case(
444
+ When(source_kind=TemplateSource.COMMUNITY, then=Value(1)),
445
+ default=Value(0),
446
+ output_field=IntegerField(),
447
+ ),
448
+ "name",
449
+ "-version",
450
+ )
451
+ )
452
+ program_id = self.request.query_params.get("program")
453
+ if program_id:
454
+ qs = qs.filter(Q(program_id=program_id) | Q(program__isnull=True))
455
+ return qs
456
+
457
+ def _require_project_admin(self, project: Project) -> None:
458
+ """ADR-0773: publishing and applying are Admin+ on the project in question."""
459
+ role = _membership_role(self.request, project.pk)
460
+ if role is None or role < Role.ADMIN:
461
+ raise PermissionDenied(
462
+ "You need at least Project Manager role to publish or apply a template."
463
+ )
464
+
465
+ @extend_schema(
466
+ request=_PublishRequestSerializer,
467
+ responses={
468
+ 201: ProjectTemplateSerializer,
469
+ 409: OpenApiResponse(
470
+ description=(
471
+ "A template with this name already exists in the pool the caller "
472
+ "can see. Carries ``code: name_taken`` plus ``template``, "
473
+ "``version`` and ``next_version`` — resend with "
474
+ "``new_version: true`` to publish the next version of it."
475
+ )
476
+ ),
477
+ },
478
+ description="Publish a project's shape as a reusable template.",
479
+ )
480
+ @action(detail=False, methods=["post"], url_path="publish")
481
+ def publish(self, request: Request) -> Response:
482
+ """Freeze a project's shape into a new template (or a new version of one).
483
+
484
+ Extraction happens **here**, synchronously: it is a read of one project's
485
+ tasks and edges, bounded by ``MAX_TEMPLATE_NODES``, and the publisher should
486
+ learn immediately whether their project was too large rather than by polling.
487
+ """
488
+ body = object_body(request)
489
+ project_id, name, raw_description = _publish_inputs(body)
490
+ try:
491
+ project = Project.objects.get(pk=project_id, is_deleted=False)
492
+ except (Project.DoesNotExist, ValueError, TypeError) as exc:
493
+ raise ValidationError({"project": _NO_SUCH_PROJECT_DETAIL}) from exc
494
+ self._require_project_admin(project)
495
+
496
+ try:
497
+ structure = extract_structure(project)
498
+ except TemplateStructureError as exc:
499
+ raise ValidationError({"project": str(exc)}) from exc
500
+
501
+ source_kind = body.get("source_kind") or TemplateSource.WORKSPACE
502
+ if source_kind not in TemplateSource.values:
503
+ raise ValidationError({"source_kind": "Unknown provenance."})
504
+
505
+ # Republishing writes a NEW row and leaves the old one selectable, rather
506
+ # than editing in place (#2909). The projects already created from v1 are
507
+ # the only audit trail a PMO has for why they look the way they do, and a
508
+ # version mutated under them turns that trail into a lie. So a name that
509
+ # is already taken is a decision, not an error to swallow: either publish
510
+ # a new version of that template, or pick a different name.
511
+ #
512
+ # The lookup is scoped to the pool the publisher can actually SEE (#3309),
513
+ # matching ``get_queryset``'s own narrowing (this program's templates plus
514
+ # the workspace-wide ones). A name-only lookup reached across programs and
515
+ # broke twice: Program B's republish superseded Program A's v1, so A's card
516
+ # rendered the ``superseded`` chip because of a row A cannot see and cannot
517
+ # find; and without ``new_version`` the 409 named — and gave the version of
518
+ # — a template B has no access to. A supersede must only ever occur inside
519
+ # a pool both parties can read.
520
+ #
521
+ # Own-program rows outrank workspace-wide ones on a tie, so a program that
522
+ # already owns this name extends ITS chain rather than forking whichever
523
+ # row happens to carry the higher version number.
524
+ scope = Q(program=project.program) | Q(program__isnull=True)
525
+ existing = (
526
+ ProjectTemplate.objects.filter(scope, name=name[:200])
527
+ .annotate(
528
+ _shared_pool=Case(
529
+ When(program__isnull=True, then=Value(1)),
530
+ default=Value(0),
531
+ output_field=IntegerField(),
532
+ )
533
+ )
534
+ .order_by("_shared_pool", "-version")
535
+ .first()
536
+ )
537
+ supersede = _flag(body.get("new_version"))
538
+ if existing is not None and not supersede:
539
+ return Response(
540
+ {
541
+ "code": "name_taken",
542
+ "detail": (
543
+ f"“{existing.name}” already exists in this workspace "
544
+ f"(v{existing.version}). Publish a new version of it, or "
545
+ f"give this one a different name."
546
+ ),
547
+ "template": str(existing.pk),
548
+ "version": existing.version,
549
+ "next_version": existing.version + 1,
550
+ },
551
+ status=status.HTTP_409_CONFLICT,
552
+ )
553
+
554
+ template = ProjectTemplate.objects.create(
555
+ name=name[:200],
556
+ description=(raw_description or "")[:2000],
557
+ source_kind=source_kind,
558
+ owner=cast("Any", request.user),
559
+ published_by=cast("Any", request.user),
560
+ program=project.program,
561
+ source_project=project,
562
+ supersedes=existing,
563
+ version=(existing.version + 1) if existing is not None else 1,
564
+ structure=structure,
565
+ carries=structure.get("carries", []),
566
+ )
567
+ # A workspace-visible disclosure act: this puts one project's task names in
568
+ # front of everyone in the workspace, and a new version changes what the
569
+ # house shape resolves to for every future adopter. Written through the
570
+ # ADR-0157 choke point, inside this request's transaction, so the log can
571
+ # never claim a publish that rolled back.
572
+ record_audit_event(
573
+ event_type=AuditEventType.TEMPLATE_PUBLISHED,
574
+ actor=request.user,
575
+ target_type="project_template",
576
+ target_id=template.pk,
577
+ target_label=f"{template.name} v{template.version}",
578
+ metadata={
579
+ "version": template.version,
580
+ "source_project": str(project.pk),
581
+ "supersedes": str(existing.pk) if existing is not None else None,
582
+ "task_count": len(structure.get("tasks", [])),
583
+ "source_kind": source_kind,
584
+ },
585
+ )
586
+ return Response(
587
+ self.get_serializer(template).data,
588
+ status=status.HTTP_201_CREATED,
589
+ )
590
+
591
+ @extend_schema(
592
+ responses={
593
+ 200: OpenApiResponse(
594
+ description=(
595
+ "Dry run: what publishing this project would carry — "
596
+ "{task_count, phase_count, gate_count, milestone_count, "
597
+ "dependency_count, methodology, carries, name_taken, "
598
+ "next_version}. Writes nothing."
599
+ )
600
+ ),
601
+ 403: OpenApiResponse(description="Caller is not Admin+ on the project."),
602
+ },
603
+ description=(
604
+ "Counts for the publish form, computed from the same extraction publish itself runs."
605
+ ),
606
+ )
607
+ @action(detail=False, methods=["get"], url_path="publish-preview")
608
+ def publish_preview(self, request: Request) -> Response:
609
+ """The six counts the Settings page and the confirm step both show (#2909).
610
+
611
+ Runs the **same** ``extract_structure`` the publish action runs, so the
612
+ numbers on the confirm screen are the numbers that will be written — not
613
+ a client-side walk of whatever the Grid currently has loaded, which a
614
+ collapsed or filtered view would get wrong.
615
+
616
+ Also reports whether the proposed name is taken, so the form can offer
617
+ "publish as v3 instead" before the operator fills anything in rather than
618
+ after a 409.
619
+ """
620
+ project_id = request.query_params.get("project")
621
+ if not project_id:
622
+ raise ValidationError({"project": "Required."})
623
+ try:
624
+ project = Project.objects.get(pk=project_id, is_deleted=False)
625
+ except (Project.DoesNotExist, ValueError, TypeError, DjangoValidationError) as exc:
626
+ raise ValidationError({"project": _NO_SUCH_PROJECT_DETAIL}) from exc
627
+ self._require_project_admin(project)
628
+
629
+ try:
630
+ structure = extract_structure(project)
631
+ except TemplateStructureError as exc:
632
+ raise ValidationError({"project": str(exc)}) from exc
633
+
634
+ summary = summarize_structure(structure)
635
+ proposed = (request.query_params.get("name") or "").strip()
636
+ existing = (
637
+ ProjectTemplate.objects.filter(name=proposed[:200]).order_by("-version").first()
638
+ if proposed
639
+ else None
640
+ )
641
+ summary["name_taken"] = existing is not None
642
+ summary["next_version"] = (existing.version + 1) if existing is not None else 1
643
+ summary["existing_template"] = str(existing.pk) if existing is not None else None
644
+ return Response(summary, status=status.HTTP_200_OK)
645
+
646
+ @extend_schema(
647
+ request=_ApplyRequestSerializer,
648
+ responses={
649
+ 202: OpenApiResponse(description='{"queued": true, "application": "<uuid>"}'),
650
+ 403: OpenApiResponse(description=_ARCHIVED_403),
651
+ },
652
+ description="Apply this template to a project. Returns 202; seeding runs async.",
653
+ )
654
+ @action(
655
+ detail=True,
656
+ methods=["post"],
657
+ url_path="apply",
658
+ throttle_classes=[SeedImportThrottle],
659
+ )
660
+ def apply(self, request: Request, pk: str | None = None) -> Response:
661
+ """Queue a seeding job and hand back the application id.
662
+
663
+ Returns ``202 {"queued": true}`` and **not** a Celery task id: dispatch is
664
+ best-effort behind an outbox, so there may be no task id yet (or ever, for
665
+ the delivery that gets dropped and re-dispatched by the drain). The
666
+ application id is the durable handle — it exists the moment the request
667
+ commits, and it is what the Start sheet polls.
668
+ """
669
+ template = self.get_object()
670
+ project_id = object_body(request).get("project")
671
+ if not project_id:
672
+ raise ValidationError({"project": "This field is required."})
673
+ try:
674
+ project = Project.objects.get(pk=cast("Any", project_id), is_deleted=False)
675
+ # `DjangoValidationError` is what `UUIDField.to_python` raises on a malformed
676
+ # pk, and DRF does not convert it — without it in this tuple a non-UUID
677
+ # `project` is a 500, reachable by any authenticated user because this line
678
+ # runs before `_require_project_admin`. `publish_preview` above already caught
679
+ # it; this call site had drifted (the #2785 class).
680
+ except (Project.DoesNotExist, ValueError, TypeError, DjangoValidationError) as exc:
681
+ raise ValidationError({"project": _NO_SUCH_PROJECT_DETAIL}) from exc
682
+ self._require_project_admin(project)
683
+ # The forward half of the same gap (#3354). `IsProjectNotArchived` cannot run
684
+ # on this action: it resolves its target project from the request *body*, so
685
+ # `has_permission` finds no `project_pk` kwarg and passes, and `get_object()`
686
+ # returns the `ProjectTemplate` — which carries no `project_id` — so
687
+ # `has_object_permission` reads None and passes too. Seeding a whole
688
+ # template's worth of rows into an archived plan is the largest write in this
689
+ # family and has to be refused explicitly. `enqueue_template_apply` repeats
690
+ # the check for non-view callers (ADR-0184 defense-in-depth).
691
+ #
692
+ # Deliberately NOT applied to `publish`/`publish_preview` above: those read
693
+ # the source project and write a workspace-level `ProjectTemplate` row. The
694
+ # archived flag makes a plan read-only, and extracting a template from one is
695
+ # a read of it.
696
+ assert_project_not_archived(project)
697
+
698
+ # Re-validate here, not only at publish: `structure` is a JSONB column, so
699
+ # the row can be edited by any path that reaches the database, and this is
700
+ # the request that turns it into rows in somebody's project.
701
+ try:
702
+ validate_structure(template.structure)
703
+ except TemplateStructureError as exc:
704
+ raise ValidationError({"template": str(exc)}) from exc
705
+
706
+ application = enqueue_template_apply(
707
+ template, project, applied_by=cast("Any", request.user)
708
+ )
709
+ return Response(
710
+ {"queued": True, "application": str(application.id)},
711
+ status=status.HTTP_202_ACCEPTED,
712
+ )
713
+
714
+
715
+ class TemplateApplicationViewSet(
716
+ IdempotencyMixin, viewsets.ReadOnlyModelViewSet[TemplateApplication]
717
+ ):
718
+ """Poll an application's progress, and undo it.
719
+
720
+ Read is scoped to projects the caller is a member of — an application row names
721
+ a project and a person, so an unscoped list would leak both.
722
+ """
723
+
724
+ serializer_class = TemplateApplicationSerializer
725
+ # `undo` soft-deletes the task rows this application wrote, so this route is a
726
+ # write path and takes the archived floor — the same gap #3354 found on the two
727
+ # `batch_operation_views` ledgers, which were written to mirror this viewset's
728
+ # shape and inherited its omission along with it. Reads pass untouched
729
+ # (SAFE_METHODS), so the Start sheet can still poll an archived project's
730
+ # application row. The project resolves from the object's `project_id` in
731
+ # `has_object_permission`, not a URL kwarg — see `PasteManyOperationViewSet`.
732
+ permission_classes = [IsAuthenticated, IsProjectNotArchived] # noqa: RUF012
733
+
734
+ def get_queryset(self) -> QuerySet[TemplateApplication]:
735
+ from trueppm_api.apps.access.models import ProjectMembership
736
+
737
+ member_project_ids = ProjectMembership.objects.filter(
738
+ user=cast("Any", self.request.user), is_deleted=False
739
+ ).values_list("project_id", flat=True)
740
+ qs = TemplateApplication.objects.filter(project_id__in=member_project_ids)
741
+ project_id = self.request.query_params.get("project")
742
+ if project_id:
743
+ qs = qs.filter(project_id=project_id)
744
+ return qs
745
+
746
+ @extend_schema(
747
+ request=None,
748
+ responses={
749
+ 200: TemplateApplicationUndoSerializer,
750
+ 400: state_refusal_400(
751
+ "The application is not in a state that can be undone — it never "
752
+ "reached SUCCESS, or it has already been undone. Verified against "
753
+ "the status guard in ``undo`` (#3319)."
754
+ ),
755
+ 403: OpenApiResponse(description=_ARCHIVED_403),
756
+ },
757
+ description="Undo this application — removes the rows it wrote that nobody has edited.",
758
+ )
759
+ @action(detail=True, methods=["post"], url_path="undo")
760
+ def undo(self, request: Request, pk: str | None = None) -> Response:
761
+ """Reverse one application in a single step.
762
+
763
+ Rows a person has since touched are **kept**, not deleted — see
764
+ ``undo_template_application``. Undoing an application that never succeeded,
765
+ or has already been undone, is a 400 rather than a silent no-op: the caller
766
+ asked to reverse something and deserves to know it did not happen.
767
+ """
768
+ application = self.get_object()
769
+ # Defers to `role_can_undo_batch_operation` rather than testing the ordinal
770
+ # here: this is the same "may you reverse a recorded batch write" rule the
771
+ # paste-many, cascade and CSV-import undos enforce, and three apply endpoints
772
+ # now report it to the client as `can_undo`. An inline copy here is a second
773
+ # implementation of a rule that is itself under revision (#3355) — #3353.
774
+ #
775
+ # `_require_project_admin` above is deliberately NOT folded in: it gates
776
+ # PUBLISHING and APPLYING a template, a different rule that happens to share
777
+ # today's ordinal (ADR-0773). Merging them would make one edit move two floors.
778
+ #
779
+ # Be explicit about what this call DOES couple, so #3355 has to confront it:
780
+ # template apply/undo is the one SYMMETRIC member of the family (Admin+ both
781
+ # ways), so lowering the shared batch-undo floor would loosen undo here without
782
+ # touching apply — a Member could reverse an application they could not apply.
783
+ # Today's behavior is unchanged; the blast radius is not. Whoever resolves
784
+ # #3355 must decide whether this endpoint follows or gets its own predicate.
785
+ role = _membership_role(request, cast("Any", application.project_id))
786
+ if not role_can_undo_batch_operation(role):
787
+ raise PermissionDenied("You need at least Project Manager role to undo a template.")
788
+ if application.status != TemplateApplicationStatus.SUCCESS:
789
+ raise ValidationError(
790
+ {
791
+ "detail": f"Only a completed application can be undone (this one is "
792
+ f"{application.status})."
793
+ }
794
+ )
795
+ summary = undo_template_application(application)
796
+ application.refresh_from_db()
797
+ data: dict[str, Any] = self.get_serializer(application).data
798
+ data["undo"] = summary
799
+ return Response(data, status=status.HTTP_200_OK)