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,878 @@
1
+ """Service-layer helpers for the access app.
2
+
3
+ The service layer wraps multi-row transactions that must remain atomic — most
4
+ importantly the Program create flow, which inserts a ``Program`` row and an
5
+ OWNER ``ProgramMembership`` row in the same transaction so a Program can never
6
+ exist in the database without at least one Owner.
7
+
8
+ ADR-0070: see §Durable Execution §4 for the atomic-create rationale.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import uuid
14
+ from collections.abc import Sequence
15
+ from typing import Any
16
+
17
+ from django.core.exceptions import ValidationError
18
+ from django.db import models, transaction
19
+ from django.utils import timezone
20
+
21
+ from trueppm_api.apps.access.models import ProgramMembership, ProjectMembership, Role
22
+ from trueppm_api.apps.projects.models import Methodology, Program, Project
23
+
24
+ # Shared detail copy for "this user cannot be the transfer target" (#4575).
25
+ #
26
+ # Views that resolve a ``*_user_id`` payload field must return this exact
27
+ # string — not a distinct "User not found." — for an unknown id, a real id
28
+ # with no live membership, and a real id for a deactivated user alike. Prior
29
+ # to #4575 the view returned a 404 "User not found."/"Lead user not found."
30
+ # for an unresolvable id while the service raised a differently-worded 400
31
+ # for a real-but-non-member id. That mismatch was itself an oracle: a caller
32
+ # could distinguish "no such user" from "user exists but isn't a member" (or
33
+ # "exists but is deactivated") by status code and message alone, without ever
34
+ # seeing the target's email. Collapsing both failure modes onto one
35
+ # constant — reused by both the service (which raises it) and the view
36
+ # (which returns it directly when the id does not even resolve to a User) —
37
+ # makes that distinction unobservable by construction instead of by
38
+ # convention.
39
+ PROJECT_OWNER_NOT_MEMBER_DETAIL = (
40
+ "The new owner must already be a member of this project. "
41
+ "Invite them first, then retry the transfer."
42
+ )
43
+ PROGRAM_OWNER_NOT_MEMBER_DETAIL = (
44
+ "The new sponsor must already be a member of this program. "
45
+ "Invite them first, then retry the transfer."
46
+ )
47
+ PROGRAM_LEAD_NOT_MEMBER_DETAIL = (
48
+ "The new lead must already be a member of this program. "
49
+ "Invite them first, then retry the transfer."
50
+ )
51
+
52
+
53
+ def _protecting_relations(model: type[models.Model]) -> list[Any]:
54
+ """Reverse relations into *model* whose FK is ``on_delete=PROTECT``.
55
+
56
+ Derived from ``_meta`` rather than hand-listed. Every hand-maintained version of
57
+ this list in the codebase went stale as new ``PROTECT`` FKs landed — three delete
58
+ paths were each written against the FKs that existed at the time and silently
59
+ started raising ``ProtectedError`` later (#2364). Reflection cannot drift.
60
+
61
+ ``include_hidden=True`` is load-bearing, not defensive: a FK declared with
62
+ ``related_name="+"`` is absent from ``_meta.related_objects`` but Django's
63
+ deletion collector still enforces its ``PROTECT``. Walking the public
64
+ ``related_objects`` would silently miss exactly those — reproducing the bug this
65
+ helper exists to prevent, just one layer down. ``Workspace.calendar`` is a live
66
+ example of a ``+``-named ``PROTECT`` FK in this codebase.
67
+
68
+ ``test_protect_relations_snapshot`` pins the resolved set, so adding a new
69
+ ``PROTECT`` FK fails a test and forces a deliberate call on whether a hard delete
70
+ should purge that child or refuse — rather than silently purging it here.
71
+ """
72
+ return [
73
+ rel
74
+ for rel in model._meta.get_fields(include_hidden=True)
75
+ if rel.is_relation
76
+ and rel.auto_created
77
+ and not rel.concrete
78
+ and getattr(rel, "on_delete", None) is models.PROTECT
79
+ ]
80
+
81
+
82
+ def _purge_protecting_children(model: type[models.Model], pks: Sequence[Any]) -> None:
83
+ """Delete every ``PROTECT``-ing child of *pks* so the parent rows can be removed.
84
+
85
+ Only for paths where the caller has already confirmed a permanent, irreversible
86
+ delete (the archive → ``?force=true`` dialog, sample-data teardown, the retention
87
+ purge). Everywhere else a ``PROTECT`` FK should surface as a 409 refusal, not be
88
+ bulldozed — see :mod:`trueppm_api.core.protect_conflict`.
89
+ """
90
+ if not pks:
91
+ return
92
+ for rel in _protecting_relations(model):
93
+ rel.related_model._base_manager.filter(**{f"{rel.field.name}__in": pks}).delete()
94
+
95
+
96
+ @transaction.atomic
97
+ def hard_delete_projects(project_ids: Sequence[Any], *, broadcast: bool = False) -> int:
98
+ """Permanently delete projects and the rows whose ``PROTECT`` FKs block them.
99
+
100
+ Returns the number of ``Project`` rows removed. Authorization and audit events
101
+ remain the caller's responsibility, but the ``project_hard_deleted`` sync
102
+ broadcast does not: pass ``broadcast=True`` to have this helper schedule one
103
+ per deleted project via ``transaction.on_commit`` (deferred so a rollback never
104
+ advertises a delete that didn't happen). Every current caller needs this
105
+ broadcast, so it is opt-in rather than silently assumed — a caller that forgets
106
+ the flag gets no broadcast and no event, instead of the inverse bug this helper
107
+ used to invite (#4374): callers were "responsible for... sync broadcasts" in
108
+ name only, and one of five forgot.
109
+
110
+ ``project_ids`` are coerced to ``str`` before being used in the on_commit
111
+ closures, so each callback captures a plain string rather than a lazily
112
+ re-evaluated queryset or ORM instance.
113
+ """
114
+ if not project_ids:
115
+ return 0
116
+ ids = [str(pid) for pid in project_ids]
117
+ _purge_protecting_children(Project, ids)
118
+ _, by_model = Project.objects.filter(pk__in=ids).delete()
119
+ deleted = int(by_model.get(Project._meta.label, 0))
120
+ if broadcast and deleted:
121
+ # A direct, literal call rather than functools.partial: the #1019/#1381
122
+ # AST sweep that keeps FROZEN_WS_EVENT_TYPES honest
123
+ # (tests/apps/sync/ws_event_scan.py) only recognizes a literal event_type
124
+ # reaching broadcast_board_event() directly or through a one-level
125
+ # parameter-forwarding wrapper — partial(broadcast_board_event, ...) is
126
+ # neither, and would make "project_hard_deleted" invisible to that scan.
127
+ # A nested def with a typed default argument (not a bare lambda, which
128
+ # mypy --strict cannot infer a type for here, and not a closure over the
129
+ # bare loop variable, which would let every callback resolve to the last
130
+ # id once the loop finishes) is what keeps each callback bound to its own
131
+ # id.
132
+ from trueppm_api.apps.sync.broadcast import broadcast_board_event
133
+
134
+ for project_id in ids:
135
+
136
+ def _emit_hard_deleted(project_id: str = project_id) -> None:
137
+ broadcast_board_event(project_id, "project_hard_deleted", {"id": project_id})
138
+
139
+ transaction.on_commit(_emit_hard_deleted)
140
+ return deleted
141
+
142
+
143
+ @transaction.atomic
144
+ def hard_delete_program(program_id: uuid.UUID | str, *, broadcast: bool = False) -> int:
145
+ """Permanently delete a program, its projects, and all ``PROTECT``-ing children.
146
+
147
+ The program's projects are deleted outright rather than detached — the only
148
+ caller is sample-data teardown, where the whole subtree is disposable. Contrast
149
+ :func:`delete_program_cascade`, which *soft*-deletes a real program and detaches
150
+ its projects so they survive as standalone work.
151
+
152
+ Returns the number of ``Project`` rows removed. Pass ``broadcast=True`` to emit
153
+ the per-project ``project_hard_deleted`` sync broadcast via
154
+ :func:`hard_delete_projects` — the program itself has no deliverable WS channel
155
+ (#836), so a ``program_deleted`` event, if the caller also sends one, reaches no
156
+ client; the per-project broadcast is what lets an open tab on one of the deleted
157
+ projects learn it is gone.
158
+ """
159
+ project_ids = list(Project.objects.filter(program_id=program_id).values_list("pk", flat=True))
160
+ deleted = hard_delete_projects(project_ids, broadcast=broadcast)
161
+ _purge_protecting_children(Program, [program_id])
162
+ Program.objects.filter(pk=program_id).delete()
163
+ return deleted
164
+
165
+
166
+ @transaction.atomic
167
+ def create_program(
168
+ *,
169
+ name: str,
170
+ description: str,
171
+ methodology: str | Methodology,
172
+ created_by: Any,
173
+ lead: Any | None = None,
174
+ code: str | None = "",
175
+ ) -> Program:
176
+ """Create a ``Program`` and the creator's OWNER membership in one transaction.
177
+
178
+ Atomicity is the whole point of this helper — if the OWNER ``ProgramMembership``
179
+ INSERT were a second request (or a non-atomic follow-up), a crash between the
180
+ two writes would leave a Program with no Owner, which permission classes treat
181
+ as "nobody can manage this program." The ``@transaction.atomic`` wrapper makes
182
+ that orphan window impossible.
183
+
184
+ Args:
185
+ name: Display name (max 255). No uniqueness enforcement — two PMs can
186
+ both have a program called "Phase 2".
187
+ description: Optional rich text; empty string allowed.
188
+ methodology: One of ``Methodology`` values; default ``HYBRID``.
189
+ created_by: The user creating the program — recorded on the Program row
190
+ and inserted as the OWNER ``ProgramMembership``.
191
+ lead: Optional program lead (#2025). At create time the only member that
192
+ will exist is ``created_by`` (the OWNER row inserted below), so the
193
+ serializer only admits ``lead == created_by`` — the "lead must be a
194
+ member" invariant holds because the OWNER membership is written in the
195
+ same atomic transaction. Defaults to ``None`` (no lead).
196
+ code: The program key (ADR-1237), already validated by the caller.
197
+ ``""`` derives one from ``name``. ``None`` assigns no key at all —
198
+ only for the seed importer, which decides the key itself (a slug, a
199
+ suffixed slug, or keys adopted from the program it replaces) and
200
+ must not first burn a derived key into the never-reused namespace.
201
+
202
+ Returns:
203
+ The persisted ``Program``. The OWNER membership exists by the time the
204
+ function returns.
205
+ """
206
+ program = Program.objects.create(
207
+ name=name,
208
+ description=description,
209
+ methodology=methodology,
210
+ created_by=created_by,
211
+ lead=lead,
212
+ )
213
+ ProgramMembership.objects.create(
214
+ program=program,
215
+ user=created_by,
216
+ role=Role.OWNER,
217
+ )
218
+ if code is not None:
219
+ from trueppm_api.apps.projects.models import ObjectKeySource
220
+ from trueppm_api.apps.projects.services import assign_key, derive_key
221
+
222
+ source = ObjectKeySource.USER if code else ObjectKeySource.DERIVED
223
+ assign_key(program, code or derive_key(name, "program"), source=source, actor=created_by)
224
+ return program
225
+
226
+
227
+ @transaction.atomic
228
+ def delete_program_cascade(program_id: uuid.UUID | str) -> None:
229
+ """Soft-delete all memberships, detach all projects, soft-delete the program.
230
+
231
+ Three things must happen atomically:
232
+
233
+ 1. **Memberships are soft-deleted.** ``ProgramMembership.program`` is ``PROTECT``,
234
+ so a naive ``program.delete()`` raises ``ProtectedError`` while any member
235
+ rows exist. The UX brief surfaces this as a combined "Remove all members
236
+ and delete" action; the cascade implements that intent server-side.
237
+ 2. **Projects are detached.** Django's ``SET_NULL`` only fires on a hard DELETE,
238
+ not on our soft-delete (``is_deleted=True``). Without an explicit detach
239
+ step, ``Project.program_id`` would continue to reference a soft-deleted
240
+ Program — the UI would display "Phase 2 (deleted)" for those projects
241
+ and the program shell would still try to retrieve a 404'd parent. Setting
242
+ ``program=NULL`` here matches the dialog's promise that projects become
243
+ standalone after deletion.
244
+ 3. **The program itself is soft-deleted.** This bumps ``server_version`` so
245
+ sync consumers can drop the row from their caches.
246
+
247
+ Args:
248
+ program_id: The program to delete.
249
+
250
+ Raises:
251
+ Program.DoesNotExist: if the program is missing or already soft-deleted.
252
+ """
253
+ # Local import: Project is in the projects app; importing at module top
254
+ # would create a circular dependency through the projects → access path.
255
+ from trueppm_api.apps.projects.models import Project
256
+
257
+ # Lock the program row so concurrent member-add / project-assign requests
258
+ # cannot land between the cascade steps and re-create a reference to the
259
+ # program we're about to mark deleted.
260
+ program = Program.objects.select_for_update().get(pk=program_id, is_deleted=False)
261
+
262
+ for membership in ProgramMembership.objects.select_for_update().filter(
263
+ program=program, is_deleted=False
264
+ ):
265
+ membership.soft_delete()
266
+
267
+ # Detach projects — they hold a SET_NULL FK, but SET_NULL only fires on
268
+ # hard delete. soft_delete() on the program would otherwise leave dangling
269
+ # references. We must go through ``Project.save()`` (not ``update()``) so
270
+ # ``server_version`` increments and mobile sync clients pick up the change.
271
+ for project in Project.objects.select_for_update().filter(program=program):
272
+ project.program = None
273
+ project.save(update_fields=["program"])
274
+
275
+ program.soft_delete()
276
+
277
+
278
+ @transaction.atomic
279
+ def transfer_project_ownership(
280
+ *,
281
+ project: Project,
282
+ new_owner: Any,
283
+ actor: Any,
284
+ ) -> ProjectMembership:
285
+ """Atomically promote a project member to OWNER, demote the actor to ADMIN.
286
+
287
+ Why the target must already be a member: requiring an existing membership
288
+ forces an explicit invite step and gives the audit trail a clean
289
+ "joined → promoted" sequence. Auto-creating membership on transfer would
290
+ surprise the target with admin authority on a project they were never
291
+ knowingly added to.
292
+
293
+ Args:
294
+ project: The project whose ownership is being transferred.
295
+ new_owner: The user who will become OWNER.
296
+ actor: The current OWNER initiating the transfer; will be downgraded
297
+ to ADMIN. May be the same user as ``new_owner`` — in that case
298
+ both upserts are no-ops and the function returns the actor's row.
299
+
300
+ Returns:
301
+ The new OWNER's ``ProjectMembership`` row.
302
+
303
+ Raises:
304
+ ValidationError: if ``new_owner`` has no existing active membership
305
+ on the project.
306
+ """
307
+ # Defense in depth — the view layer gates with ``IsProjectOwner``, but the
308
+ # service is reusable (management commands, signals, future endpoints). If
309
+ # the actor is not actually the OWNER, a naive ``.update()`` is a silent
310
+ # no-op that would promote the target without removing anyone — creating
311
+ # an unauthorized OWNER. Assert and fail loudly before any state changes.
312
+ try:
313
+ actor_row = ProjectMembership.objects.select_for_update().get(
314
+ project=project,
315
+ user=actor,
316
+ is_deleted=False,
317
+ )
318
+ except ProjectMembership.DoesNotExist as exc:
319
+ raise ValidationError("Only an existing project Owner can transfer ownership.") from exc
320
+ if actor_row.role != Role.OWNER:
321
+ raise ValidationError("Only an existing project Owner can transfer ownership.")
322
+
323
+ try:
324
+ # ``user__is_active=True`` closes the email-harvest class (#4575): a
325
+ # deactivated user must be refused identically to one who was never a
326
+ # member, not promoted to OWNER on an account nobody can log into.
327
+ target = ProjectMembership.objects.select_for_update().get(
328
+ project=project,
329
+ user=new_owner,
330
+ user__is_active=True,
331
+ is_deleted=False,
332
+ )
333
+ except ProjectMembership.DoesNotExist as exc:
334
+ raise ValidationError(PROJECT_OWNER_NOT_MEMBER_DETAIL) from exc
335
+
336
+ if new_owner == actor:
337
+ return target
338
+
339
+ # Demote the actor first so the OWNER count never exceeds the previous
340
+ # state mid-transaction — defensive against future single-owner constraints.
341
+ # role_changed_at is stamped alongside role on both rows so the per-project
342
+ # access-evidence timestamp (#590) stays accurate on this path too, not just
343
+ # on the partial_update endpoint.
344
+ now = timezone.now()
345
+ actor_row.role = Role.ADMIN
346
+ actor_row.role_changed_at = now
347
+ actor_row.save(update_fields=["role", "role_changed_at"])
348
+
349
+ target.role = Role.OWNER
350
+ target.role_changed_at = now
351
+ target.save(update_fields=["role", "role_changed_at"])
352
+ return target
353
+
354
+
355
+ @transaction.atomic
356
+ def transfer_program_sponsorship(
357
+ *,
358
+ program: Program,
359
+ new_owner: Any,
360
+ actor: Any,
361
+ new_lead: Any | None = None,
362
+ ) -> ProgramMembership:
363
+ """Atomically promote a program member to OWNER and optionally rotate the lead.
364
+
365
+ Programs carry both an OWNER membership (RBAC) and a ``lead`` FK (the
366
+ "Program Manager" displayed in the header). Both can move together in a
367
+ sponsorship transfer so the header chip and the access matrix stay in sync.
368
+
369
+ Args:
370
+ program: The program whose sponsorship is being transferred.
371
+ new_owner: The user who will become OWNER.
372
+ actor: The current OWNER initiating the transfer; will be downgraded
373
+ to ADMIN.
374
+ new_lead: Optional user to set as ``program.lead``. ``None`` leaves
375
+ the existing lead unchanged; pass ``new_owner`` explicitly to keep
376
+ owner+lead in lockstep.
377
+
378
+ Returns:
379
+ The new OWNER's ``ProgramMembership`` row.
380
+
381
+ Raises:
382
+ ValidationError: if ``new_owner`` has no existing active program
383
+ membership, or if ``new_lead`` is given and has no existing
384
+ active program membership (#4575).
385
+ """
386
+ # Defense in depth (see ``transfer_project_ownership`` for full rationale)
387
+ # — the actor must currently hold the OWNER row, otherwise the demote
388
+ # ``.update()`` is a no-op and the target gets promoted without anyone
389
+ # being removed.
390
+ try:
391
+ actor_row = ProgramMembership.objects.select_for_update().get(
392
+ program=program,
393
+ user=actor,
394
+ is_deleted=False,
395
+ )
396
+ except ProgramMembership.DoesNotExist as exc:
397
+ raise ValidationError("Only an existing Program Admin can transfer sponsorship.") from exc
398
+ if actor_row.role != Role.OWNER:
399
+ raise ValidationError("Only an existing Program Admin can transfer sponsorship.")
400
+
401
+ try:
402
+ # ``user__is_active=True`` closes the email-harvest class (#4575): a
403
+ # deactivated user must be refused identically to one who was never a
404
+ # member, not promoted to OWNER on an account nobody can log into.
405
+ target = ProgramMembership.objects.select_for_update().get(
406
+ program=program,
407
+ user=new_owner,
408
+ user__is_active=True,
409
+ is_deleted=False,
410
+ )
411
+ except ProgramMembership.DoesNotExist as exc:
412
+ raise ValidationError(PROGRAM_OWNER_NOT_MEMBER_DETAIL) from exc
413
+
414
+ # ``new_lead`` previously flowed straight into ``program.lead`` with no
415
+ # membership check at all — unlike ``new_owner`` above, which has always
416
+ # been membership-gated. That let any authenticated OWNER of ANY program
417
+ # (including a throwaway one they just created) set an arbitrary user id
418
+ # as lead and read their email back off ``lead_detail`` in the 200
419
+ # response: an email-harvest oracle over the full user-id space (#4575).
420
+ # Validate here, in the service, not only in the view — the view is not
421
+ # the only caller and the invariant belongs with the write.
422
+ if new_lead is not None:
423
+ lead_is_member = ProgramMembership.objects.filter(
424
+ program=program,
425
+ user=new_lead,
426
+ user__is_active=True,
427
+ is_deleted=False,
428
+ ).exists()
429
+ if not lead_is_member:
430
+ raise ValidationError(PROGRAM_LEAD_NOT_MEMBER_DETAIL)
431
+
432
+ if new_owner != actor:
433
+ # role_changed_at is stamped alongside role on both rows so the
434
+ # per-program access-evidence timestamp (#878) stays accurate on this
435
+ # path too, not just on the partial_update endpoint.
436
+ now = timezone.now()
437
+ actor_row.role = Role.ADMIN
438
+ actor_row.role_changed_at = now
439
+ actor_row.save(update_fields=["role", "role_changed_at"])
440
+
441
+ target.role = Role.OWNER
442
+ target.role_changed_at = now
443
+ target.save(update_fields=["role", "role_changed_at"])
444
+
445
+ if new_lead is not None:
446
+ program.lead = new_lead
447
+ program.save(update_fields=["lead"])
448
+
449
+ return target
450
+
451
+
452
+ @transaction.atomic
453
+ def split_program(
454
+ *,
455
+ program: Program,
456
+ splits: list[dict[str, Any]],
457
+ actor: Any,
458
+ ) -> list[Program]:
459
+ """Split a program into sub-programs and close the original (ADR-0156, #967).
460
+
461
+ For each entry in ``splits`` a new sub-program is created (owned by ``actor``,
462
+ methodology copied from the parent, all inheritable overrides left NULL so the
463
+ new program inherits workspace defaults), and the entry's projects are moved
464
+ under it. After every split is applied the original program is closed — it
465
+ becomes a read-only shell holding any projects that were not redistributed.
466
+
467
+ Only the ``Project.program`` FK moves; tasks, dependencies, baselines,
468
+ memberships, and history are untouched, so each project keeps its full
469
+ schedule and audit trail under its new program. Reassignment goes through
470
+ ``Project.save()`` (not ``.update()``) so ``server_version`` increments and
471
+ mobile sync clients pick up the move — same rationale as
472
+ ``delete_program_cascade``.
473
+
474
+ The whole operation is one transaction: either every sub-program, every
475
+ reassignment, and the parent close commit together, or nothing does. A bad
476
+ payload raises before any INSERT so the error message is precise.
477
+
478
+ Args:
479
+ program: The program being split. Must be open — callers gate on
480
+ ``IsProgramNotClosed``; once closed by this call a replayed request
481
+ is rejected by that gate, which is what makes a network retry safe.
482
+ splits: ``[{"name": str, "project_ids": [uuid]}, ...]``. Each entry
483
+ becomes one sub-program. ``project_ids`` may be empty (an empty
484
+ sub-program shell, symmetric with ``create_program``).
485
+ actor: The current OWNER initiating the split; becomes OWNER of every
486
+ sub-program.
487
+
488
+ Returns:
489
+ The created sub-programs, in input order.
490
+
491
+ Raises:
492
+ ValidationError: if a referenced project is not a live member of this
493
+ program, or if a project appears in more than one split.
494
+ """
495
+ # Lock the parent row so a concurrent project-assign / delete cannot land
496
+ # between validation and reassignment (mirrors delete_program_cascade).
497
+ program = Program.objects.select_for_update().get(pk=program.pk)
498
+
499
+ member_project_ids = {
500
+ str(pk)
501
+ for pk in Project.objects.filter(program=program, is_deleted=False).values_list(
502
+ "pk", flat=True
503
+ )
504
+ }
505
+
506
+ # Validate the full payload up front: every id must be a live member of this
507
+ # program, and no id may be claimed by two sub-programs.
508
+ seen: set[str] = set()
509
+ normalized: list[tuple[str, list[str]]] = []
510
+ for entry in splits:
511
+ name = entry["name"]
512
+ ids = [str(pid) for pid in entry["project_ids"]]
513
+ for pid in ids:
514
+ if pid not in member_project_ids:
515
+ raise ValidationError(f"Project {pid} is not a project of this program.")
516
+ if pid in seen:
517
+ raise ValidationError(f"Project {pid} is assigned to more than one sub-program.")
518
+ seen.add(pid)
519
+ normalized.append((name, ids))
520
+
521
+ sub_programs: list[Program] = []
522
+ for name, ids in normalized:
523
+ sub = create_program(
524
+ name=name,
525
+ description="",
526
+ methodology=program.methodology,
527
+ created_by=actor,
528
+ )
529
+ # Filter by program=program as well as pk so a project moved out by a
530
+ # racing request (despite the lock) is never silently captured.
531
+ for project in Project.objects.select_for_update().filter(
532
+ program=program, pk__in=ids, is_deleted=False
533
+ ):
534
+ project.program = sub
535
+ project.save(update_fields=["program"])
536
+ sub_programs.append(sub)
537
+
538
+ if not program.is_closed:
539
+ program.is_closed = True
540
+ program.closed_at = timezone.now()
541
+ program.closed_by = actor
542
+ program.save(update_fields=["is_closed", "closed_at", "closed_by"])
543
+
544
+ return sub_programs
545
+
546
+
547
+ def revoke_all_refresh_tokens(user: Any) -> int:
548
+ """Blacklist every outstanding simplejwt refresh token for ``user``.
549
+
550
+ This is the "sign out every device" primitive (ADR-0209). A password reset
551
+ (and, later, any explicit account-security action) must invalidate all of the
552
+ user's existing sessions, not just the one that triggered the change. The
553
+ SPA's session lives entirely in a JWT access token (in-memory, 15-min TTL) plus
554
+ an httpOnly refresh cookie (#897), so revoking every *refresh* token is the
555
+ complete server-side session revocation: a blacklisted refresh token can no
556
+ longer be exchanged for a new access token, and the old access token self-
557
+ expires within its short TTL.
558
+
559
+ Django sessions are intentionally left untouched — the SPA never authenticates
560
+ via ``django.contrib.sessions`` (that framework backs only the admin site and
561
+ the DRF browsable API), so there is no app session to clear there.
562
+
563
+ Completeness depends on every live token having an ``OutstandingToken`` row.
564
+ simplejwt writes one only in ``RefreshToken.for_user`` (login); rotation mints a
565
+ new jti and records nothing, so before #2999 this function walked a set of
566
+ already-blacklisted rows and left the *live* session usable while returning a
567
+ plausible non-zero count. ``core.auth_views._record_outstanding`` now upserts the
568
+ row on both login and rotation, which is what makes this function's contract
569
+ true. **Tokens minted before that fix shipped still have no row and remain
570
+ unrevokable** — an operator needing a hard cut across those must rotate
571
+ ``JWT_SIGNING_KEY``.
572
+
573
+ Idempotent: ``get_or_create`` means re-running blacklists nothing twice, and a
574
+ row already blacklisted by rotation stays blacklisted. Safe to call inside the
575
+ reset transaction.
576
+
577
+ Args:
578
+ user: The account whose refresh tokens should all be revoked.
579
+
580
+ Returns:
581
+ The number of tokens newly blacklisted (already-blacklisted tokens are not
582
+ counted). Zero when the account had no outstanding tokens (e.g. it has
583
+ never logged in on this deployment).
584
+ """
585
+ # Local import: the blacklist app's models are only importable once apps are
586
+ # loaded, and keeping the import lazy lets a lean deploy that removes the
587
+ # token_blacklist app degrade gracefully (the ImportError is swallowed → the
588
+ # password is still reset, sessions just fall back to TTL-only expiry).
589
+ try:
590
+ from rest_framework_simplejwt.token_blacklist.models import (
591
+ BlacklistedToken,
592
+ OutstandingToken,
593
+ )
594
+ except ImportError: # pragma: no cover - only when the app is uninstalled
595
+ return 0
596
+
597
+ revoked = 0
598
+ for token in OutstandingToken.objects.filter(user=user):
599
+ _, created = BlacklistedToken.objects.get_or_create(token=token)
600
+ if created:
601
+ revoked += 1
602
+ return revoked
603
+
604
+
605
+ def revoke_all_personal_access_tokens(
606
+ user: Any,
607
+ *,
608
+ actor: Any = None,
609
+ reason: str = "",
610
+ source_ip: str | None = None,
611
+ ) -> int:
612
+ """Soft-revoke every active Personal Access Token owned by ``user`` (ADR-0214).
613
+
614
+ Why this exists: a Personal Access Token is a *full-authority* bearer of the
615
+ user's own credentials — a script authenticating with it acts exactly as the
616
+ user. A password change is the user asserting "my credentials may be
617
+ compromised; cut everything off," so it must invalidate not only live sessions
618
+ (``revoke_all_refresh_tokens``) but also every long-lived personal credential
619
+ they minted. Leaving PATs live after a reset would defeat the whole point of
620
+ the reset — an attacker who phished the old password could keep the account via
621
+ a PAT they created. This is the PAT analogue of the refresh-token revocation and
622
+ is called immediately after it in the same atomic block.
623
+
624
+ Scope discipline: only ``owner=user`` (personal) tokens are revoked. Project-
625
+ and program-scoped tokens are *org assets* minted by an Admin/PM — they are not
626
+ the user's personal credentials and are deliberately left untouched, so a
627
+ password reset never breaks a team's CI integration.
628
+
629
+ Idempotent: already-revoked tokens are filtered out, so re-running is a no-op.
630
+ Uses a bulk ``update()`` (not per-row ``save()``) because revocation is a
631
+ single indexed write with no ``server_version`` sync semantics on the audit
632
+ path — the tokens are not mobile-synced resources whose version consumers track.
633
+
634
+ **Every revoked token gets an** ``ApiTokenAuditEntry`` **row (#2878).** It did
635
+ not, and the trail was inverted: routine self-service revocation through
636
+ ``MyApiTokenViewSet.destroy`` was logged, while the two *security-relevant*
637
+ revocations — password reset and off-boarding, the two that answer "was this
638
+ account contained, and when?" — wrote nothing, on a feature page that promises
639
+ "every mint and revoke is written to an append-only audit log." The rows are
640
+ ``bulk_create``d from the ids the ``update()`` matched, so the cost stays two
641
+ queries regardless of token count and the audit is inside the caller's
642
+ transaction: it commits with the revocation or rolls back with it, never apart.
643
+
644
+ Args:
645
+ user: The account whose personal access tokens should all be revoked.
646
+ actor: The user who caused the revocation, for the audit row. The account
647
+ owner for a password reset; the acting admin for an off-boarding; ``None``
648
+ for an operator running the management command, which has no Django user.
649
+ reason: Short machine-readable cause recorded in ``detail["reason"]``
650
+ (``password_reset``, ``offboarding``, ``operator_bulk_revoke``). It is what
651
+ makes the trail answerable — "revoked" alone cannot distinguish routine
652
+ rotation from incident response.
653
+ source_ip: Client IP when one is available (a request-driven path); ``None``
654
+ from a management command.
655
+
656
+ Returns:
657
+ The number of tokens newly revoked (already-revoked tokens are excluded).
658
+ """
659
+ # Local import: the projects app imports from access, so importing ApiToken at
660
+ # module top would create a circular dependency through the access → projects
661
+ # path (mirrors delete_program_cascade's lazy Project import).
662
+ from trueppm_api.apps.projects.models import (
663
+ ApiToken,
664
+ ApiTokenAuditAction,
665
+ ApiTokenAuditEntry,
666
+ )
667
+
668
+ # Materialize before the update: after it, `revoked_at__isnull=True` no longer
669
+ # selects the rows this call is responsible for, and re-reading without the filter
670
+ # would also pick up tokens revoked earlier by someone else and double-audit them.
671
+ #
672
+ # `select_for_update()` is what keeps the split read/write as idempotent as the
673
+ # single `update()` it replaced. Without the row lock, a password reset racing an
674
+ # admin off-boarding both select the same rows, both write, and the second write
675
+ # moves `revoked_at` *forward* — misdating containment on the one field an auditor
676
+ # reads — while producing a second REVOKED row with a contradicting reason and an
677
+ # inflated return count. Holding the lock makes `doomed` exactly the set this call
678
+ # revokes, so the audit rows below cannot describe someone else's revocation. It
679
+ # requires an open transaction, which every caller has (ATOMIC_REQUESTS for the two
680
+ # request paths, an explicit `atomic()` in the management command); a caller without
681
+ # one gets a loud TransactionManagementError rather than a silent race.
682
+ #
683
+ # Expired tokens are excluded deliberately. An expired token already fails
684
+ # authentication (expiry is folded into the authenticator's hash lookup), so
685
+ # revoking it changes nothing — but auditing it would pad the trail with rows
686
+ # claiming N live credentials were contained when the real number was lower. This
687
+ # matches `ApiToken.active_personal_tokens_for`, the canonical definition of
688
+ # "active", which the 10-token cap already uses.
689
+ now = timezone.now()
690
+ doomed = list(
691
+ ApiToken.objects.select_for_update()
692
+ .filter(
693
+ models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=now),
694
+ owner=user,
695
+ is_deleted=False,
696
+ revoked_at__isnull=True,
697
+ )
698
+ .only("pk", "token_prefix", "name")
699
+ )
700
+ if not doomed:
701
+ return 0
702
+
703
+ revoked = ApiToken.objects.filter(
704
+ pk__in=[token.pk for token in doomed], revoked_at__isnull=True
705
+ ).update(revoked_at=now)
706
+ ApiTokenAuditEntry.objects.bulk_create(
707
+ [
708
+ ApiTokenAuditEntry(
709
+ owner=user,
710
+ token=token,
711
+ token_prefix=token.token_prefix,
712
+ actor=actor,
713
+ action=ApiTokenAuditAction.REVOKED.value,
714
+ source_ip=source_ip,
715
+ detail={"name": token.name, "reason": reason} if reason else {"name": token.name},
716
+ )
717
+ for token in doomed
718
+ ]
719
+ )
720
+ return revoked
721
+
722
+
723
+ def revoke_personal_durable_grants(user: Any = None, *, actor: Any = None) -> tuple[int, int, int]:
724
+ """Revoke the non-token durable grants a PAT (or a live session) can mint.
725
+
726
+ ``IsNotTokenAuthenticated`` (#2878) now blocks *minting* a ``ShareLink``,
727
+ rotating a ``BoardAutomation`` webhook secret, or registering/repointing an
728
+ outbound ``Webhook`` through a bearer token, but a grant minted **before**
729
+ that guard existed, or minted through a live session by someone who is now
730
+ being contained, is exactly as durable as a personal access token and sits
731
+ outside every existing revocation lever (``revoke_all_personal_access_tokens``,
732
+ password reset, off-boarding). This closes that gap for the three grants
733
+ #2939 and #4461 scoped: a project's public share links, its git-automation
734
+ webhook secret, and any outgoing webhook the user registered. It also revokes
735
+ the pending workspace invites the user sent (#4589, see
736
+ ``revoke_pending_workspace_invites``; not reflected in the returned tuple).
737
+
738
+ The webhook case is distinct from the other two: dispatch already stops for
739
+ an off-boarded *registrant* because it re-checks Admin+ membership on every
740
+ delivery (#4325/#4330) — ``created_by`` only, never the editors set below.
741
+ What that membership re-check cannot see at all is a *credential*
742
+ compromise — a leaked PAT or hijacked session belonging to a registrant who
743
+ is still an active Admin — and it cannot see an editor who is not the
744
+ registrant in any case, compromised or not. Deactivating the webhook here
745
+ is belt-and-suspenders for the registrant's own credential compromise, and
746
+ the *only* control that reaches an editor who never registered the row.
747
+
748
+ The webhook sweep matches ``created_by=user`` **or** ``user`` appearing in
749
+ ``webhook.editors`` (:class:`~trueppm_api.apps.webhooks.models.WebhookEditor`,
750
+ an additive, append-only record of every user who has ever set ``url`` or
751
+ ``secret`` on the row, or re-enabled it while disabled) — not ``created_by``
752
+ alone, and not a single mutable "last modified by" field either. A project
753
+ webhook's update has no
754
+ registrant check — any project Admin may repoint any project webhook's URL
755
+ or rotate its secret (``WebhookViewSet.perform_update``) — so a hijacked
756
+ session belonging to Admin B could repoint Admin A's webhook to an
757
+ attacker's host, and B's own password reset or off-boarding must still
758
+ catch that row even after a later, unrelated edit by A or anyone else. A
759
+ mutable field would lose B's attribution the moment someone else's edit
760
+ overwrote it (including a PUT that only changes ``events`` but still
761
+ carries the unchanged ``url`` in its body); an append-only membership does
762
+ not have that failure mode, because nothing removes a row from it. This is
763
+ exactly the "grant minted through a live session by someone who is now
764
+ being contained" case two paragraphs up, for an update rather than a
765
+ create. Program webhooks record an editor too, for symmetry, but it is
766
+ always the same user as ``created_by`` there because
767
+ ``ProgramWebhookViewSet`` already restricts update to the creator
768
+ (ADR-0076/#4310) — so this clause adds real coverage only on the
769
+ project-scoped path.
770
+
771
+ ``user=None`` means instance-wide — every active grant, not one person's —
772
+ which is what the ``revoke_api_tokens --all`` full-compromise lever needs;
773
+ a scoped ``user`` is the off-boarding / single-account case.
774
+
775
+ Revoking a share link is cheap for a team to recover from (an Admin mints a
776
+ new one); clearing a webhook secret degrades ``GitWebhookIngestView`` to its
777
+ existing "no secret set" 404 refusal (fail-closed by construction, not a new
778
+ code path) until an Admin rotates a fresh one; deactivating an outgoing
779
+ ``Webhook`` stops dispatch immediately (``dispatch_webhooks`` only fans out to
780
+ ``is_active=True`` rows) and an Admin re-enables it once the credential issue
781
+ is resolved, which also clears the delivery-health counters (see
782
+ ``WebhookSerializer.update``). All three are lower-disruption than the
783
+ project-/program-scoped *API tokens* this module deliberately leaves alone
784
+ on off-boarding (see ``_revoke_offboarded_credentials``) — those are shared
785
+ integration credentials embedded in other people's scripts and CI, not a
786
+ single admin's config action.
787
+
788
+ Returns ``(share_links_revoked, automation_secrets_cleared, webhooks_deactivated)``.
789
+ """
790
+ # Local import: projects/integrations/webhooks import from access, so importing
791
+ # these at module top would create a circular dependency (mirrors the ApiToken
792
+ # import above).
793
+ from trueppm_api.apps.integrations.models import BoardAutomation
794
+ from trueppm_api.apps.projects.models import ShareLink
795
+ from trueppm_api.apps.webhooks.models import Webhook
796
+
797
+ now = timezone.now()
798
+
799
+ links = ShareLink.objects.filter(revoked_at__isnull=True)
800
+ if user is not None:
801
+ links = links.filter(created_by=user)
802
+ links_revoked = links.update(revoked_at=now, revoked_by=actor)
803
+
804
+ secrets_qs = BoardAutomation.objects.exclude(secret_ciphertext=b"")
805
+ if user is not None:
806
+ secrets_qs = secrets_qs.filter(configured_by=user)
807
+ secrets_cleared = secrets_qs.update(secret_ciphertext=b"", secret_set_at=None)
808
+
809
+ webhooks_qs = Webhook.objects.filter(is_active=True)
810
+ if user is not None:
811
+ webhooks_qs = webhooks_qs.filter(models.Q(created_by=user) | models.Q(editors__user=user))
812
+ # Deliberately neutral: this fires whenever EITHER leg above matches, and the
813
+ # editors leg means the row's registrant may be a different, untouched account
814
+ # (#4461) — "the registrant's credentials were revoked" would misattribute the
815
+ # cause on exactly that path. The UI/docs tell the admin to verify url/secret
816
+ # before re-enabling regardless of which leg fired, so the reason does not need
817
+ # to (and cannot cheaply) distinguish them.
818
+ webhooks_deactivated = webhooks_qs.update(
819
+ is_active=False,
820
+ disabled_at=now,
821
+ disabled_reason="Deactivated: a security sweep revoked a credential tied to this webhook.",
822
+ )
823
+
824
+ # Pending workspace invites the user minted (#4589). Not part of the returned
825
+ # tuple (its shape is relied on by every caller); the sweep audits itself.
826
+ revoke_pending_workspace_invites(user, actor=actor)
827
+
828
+ return links_revoked, secrets_cleared, webhooks_deactivated
829
+
830
+
831
+ def revoke_pending_workspace_invites(
832
+ user: Any = None, *, actor: Any = None, min_role: int | None = None
833
+ ) -> int:
834
+ """Revoke the not-yet-accepted ``WorkspaceInvite`` rows ``user`` minted (#4589).
835
+
836
+ An invite is a durable, role-carrying bearer grant (7-day token, role up to one
837
+ rank below the inviter) that works for an outside address with no session at
838
+ all. A contained Admin's off-boarding, password reset and
839
+ ``revoke_api_tokens`` all left it live, and ``resend`` could re-mint it.
840
+ ``FAILED`` (send-exhausted) invites are swept too because resend revives them;
841
+ ``REVOKED`` is terminal and never resendable.
842
+
843
+ ``min_role`` limits the sweep to invites granting at least that workspace role
844
+ (used on demotion: invites the sender can no longer vouch for).
845
+
846
+ ``user=None`` is instance-wide (``revoke_api_tokens --all``). One
847
+ ``INVITE_REVOKED`` audit row per invite, written in the caller's transaction.
848
+ Returns the number of invites revoked.
849
+ """
850
+ from trueppm_api.apps.workspace.models import AuditEventType, InviteStatus, WorkspaceInvite
851
+ from trueppm_api.apps.workspace.services import _actor_label, record_audit_event
852
+
853
+ qs = WorkspaceInvite.objects.select_for_update(of=("self",)).filter(
854
+ status__in=(InviteStatus.PENDING, InviteStatus.FAILED)
855
+ )
856
+ if user is not None:
857
+ qs = qs.filter(invited_by=user)
858
+ if min_role is not None:
859
+ qs = qs.filter(role__gte=min_role)
860
+ invites = list(qs.select_related("invited_by"))
861
+ for invite in invites:
862
+ invite.status = InviteStatus.REVOKED
863
+ invite.email_pending = False
864
+ invite.email_token = ""
865
+ invite.save(update_fields=["status", "email_pending", "email_token"])
866
+ record_audit_event(
867
+ event_type=AuditEventType.INVITE_REVOKED,
868
+ actor=actor,
869
+ target_type="invite",
870
+ target_id=invite.pk,
871
+ target_label=invite.email,
872
+ metadata={
873
+ "role": invite.get_role_display(),
874
+ "invited_by": _actor_label(invite.invited_by),
875
+ "reason": "security_sweep",
876
+ },
877
+ )
878
+ return len(invites)