trueppm-api 0.4.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (759) hide show
  1. trueppm_api/__init__.py +14 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +164 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  24. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  25. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  26. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  27. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
  28. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  29. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  30. trueppm_api/apps/access/migrations/__init__.py +0 -0
  31. trueppm_api/apps/access/models.py +588 -0
  32. trueppm_api/apps/access/permissions.py +2837 -0
  33. trueppm_api/apps/access/serializers.py +946 -0
  34. trueppm_api/apps/access/services.py +667 -0
  35. trueppm_api/apps/access/signals.py +103 -0
  36. trueppm_api/apps/access/tasks.py +50 -0
  37. trueppm_api/apps/access/throttles.py +95 -0
  38. trueppm_api/apps/access/urls.py +191 -0
  39. trueppm_api/apps/access/views.py +1931 -0
  40. trueppm_api/apps/agents/__init__.py +6 -0
  41. trueppm_api/apps/agents/apps.py +11 -0
  42. trueppm_api/apps/agents/canonical.py +73 -0
  43. trueppm_api/apps/agents/deferred.py +108 -0
  44. trueppm_api/apps/agents/management/__init__.py +0 -0
  45. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  46. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  47. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  48. trueppm_api/apps/agents/middleware.py +44 -0
  49. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  50. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  51. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  52. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  53. trueppm_api/apps/agents/models.py +371 -0
  54. trueppm_api/apps/agents/refusal.py +103 -0
  55. trueppm_api/apps/agents/serializers.py +85 -0
  56. trueppm_api/apps/agents/services.py +397 -0
  57. trueppm_api/apps/agents/signals.py +30 -0
  58. trueppm_api/apps/agents/urls.py +15 -0
  59. trueppm_api/apps/agents/views.py +108 -0
  60. trueppm_api/apps/csvimport/__init__.py +12 -0
  61. trueppm_api/apps/csvimport/apps.py +11 -0
  62. trueppm_api/apps/csvimport/mapping.py +363 -0
  63. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  64. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  65. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  66. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  67. trueppm_api/apps/csvimport/models.py +111 -0
  68. trueppm_api/apps/csvimport/parser.py +1825 -0
  69. trueppm_api/apps/csvimport/services.py +48 -0
  70. trueppm_api/apps/csvimport/tasks.py +418 -0
  71. trueppm_api/apps/csvimport/template.py +37 -0
  72. trueppm_api/apps/csvimport/urls.py +50 -0
  73. trueppm_api/apps/csvimport/views.py +793 -0
  74. trueppm_api/apps/history/__init__.py +0 -0
  75. trueppm_api/apps/history/apps.py +12 -0
  76. trueppm_api/apps/history/changelog.py +359 -0
  77. trueppm_api/apps/history/diff_policy.py +177 -0
  78. trueppm_api/apps/history/serializers.py +104 -0
  79. trueppm_api/apps/history/signals.py +82 -0
  80. trueppm_api/apps/history/tasks.py +105 -0
  81. trueppm_api/apps/history/urls.py +38 -0
  82. trueppm_api/apps/history/views.py +443 -0
  83. trueppm_api/apps/idempotency/__init__.py +0 -0
  84. trueppm_api/apps/idempotency/apps.py +11 -0
  85. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  86. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  87. trueppm_api/apps/idempotency/mixins.py +226 -0
  88. trueppm_api/apps/idempotency/models.py +75 -0
  89. trueppm_api/apps/idempotency/tasks.py +61 -0
  90. trueppm_api/apps/integrations/__init__.py +13 -0
  91. trueppm_api/apps/integrations/apps.py +59 -0
  92. trueppm_api/apps/integrations/connections.py +806 -0
  93. trueppm_api/apps/integrations/encryption.py +92 -0
  94. trueppm_api/apps/integrations/external_sources.py +837 -0
  95. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  96. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  97. trueppm_api/apps/integrations/http.py +403 -0
  98. trueppm_api/apps/integrations/me_work.py +218 -0
  99. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  100. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  101. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  102. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  103. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  104. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  105. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  106. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  107. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  108. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  109. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  110. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  111. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  112. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  113. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  114. trueppm_api/apps/integrations/models.py +622 -0
  115. trueppm_api/apps/integrations/notification_channels.py +57 -0
  116. trueppm_api/apps/integrations/opengraph.py +229 -0
  117. trueppm_api/apps/integrations/outgoing.py +201 -0
  118. trueppm_api/apps/integrations/providers.py +519 -0
  119. trueppm_api/apps/integrations/registry.py +345 -0
  120. trueppm_api/apps/integrations/serializers.py +453 -0
  121. trueppm_api/apps/integrations/services.py +171 -0
  122. trueppm_api/apps/integrations/tasks.py +731 -0
  123. trueppm_api/apps/integrations/throttles.py +165 -0
  124. trueppm_api/apps/integrations/urls.py +77 -0
  125. trueppm_api/apps/integrations/views.py +1106 -0
  126. trueppm_api/apps/jiraimport/__init__.py +8 -0
  127. trueppm_api/apps/jiraimport/apps.py +11 -0
  128. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  129. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  130. trueppm_api/apps/jiraimport/models.py +70 -0
  131. trueppm_api/apps/jiraimport/parser.py +350 -0
  132. trueppm_api/apps/jiraimport/services.py +46 -0
  133. trueppm_api/apps/jiraimport/tasks.py +277 -0
  134. trueppm_api/apps/jiraimport/urls.py +15 -0
  135. trueppm_api/apps/jiraimport/views.py +153 -0
  136. trueppm_api/apps/msproject/__init__.py +0 -0
  137. trueppm_api/apps/msproject/apps.py +11 -0
  138. trueppm_api/apps/msproject/dataclasses.py +153 -0
  139. trueppm_api/apps/msproject/exporter.py +500 -0
  140. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  141. trueppm_api/apps/msproject/importer.py +1095 -0
  142. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  143. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  144. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  145. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  146. trueppm_api/apps/msproject/models.py +81 -0
  147. trueppm_api/apps/msproject/parser.py +1231 -0
  148. trueppm_api/apps/msproject/serializers.py +136 -0
  149. trueppm_api/apps/msproject/services.py +58 -0
  150. trueppm_api/apps/msproject/tasks.py +436 -0
  151. trueppm_api/apps/msproject/urls.py +39 -0
  152. trueppm_api/apps/msproject/views.py +531 -0
  153. trueppm_api/apps/notifications/__init__.py +7 -0
  154. trueppm_api/apps/notifications/apps.py +15 -0
  155. trueppm_api/apps/notifications/backfill.py +79 -0
  156. trueppm_api/apps/notifications/categories.py +112 -0
  157. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  158. trueppm_api/apps/notifications/digests.py +447 -0
  159. trueppm_api/apps/notifications/email_backend.py +330 -0
  160. trueppm_api/apps/notifications/email_health.py +128 -0
  161. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  162. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
  163. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  164. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  165. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  166. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  167. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  168. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  169. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  170. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  171. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  172. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  173. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  174. trueppm_api/apps/notifications/models.py +1197 -0
  175. trueppm_api/apps/notifications/receivers.py +155 -0
  176. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  177. trueppm_api/apps/notifications/serializers.py +961 -0
  178. trueppm_api/apps/notifications/services.py +1451 -0
  179. trueppm_api/apps/notifications/tasks.py +711 -0
  180. trueppm_api/apps/notifications/throttles.py +98 -0
  181. trueppm_api/apps/notifications/urls.py +97 -0
  182. trueppm_api/apps/notifications/views.py +904 -0
  183. trueppm_api/apps/observability/__init__.py +0 -0
  184. trueppm_api/apps/observability/apps.py +31 -0
  185. trueppm_api/apps/observability/logging.py +213 -0
  186. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  187. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  188. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  189. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  190. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  191. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  192. trueppm_api/apps/observability/models.py +141 -0
  193. trueppm_api/apps/observability/otel/__init__.py +45 -0
  194. trueppm_api/apps/observability/otel/attributes.py +247 -0
  195. trueppm_api/apps/observability/otel/export_health.py +533 -0
  196. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  197. trueppm_api/apps/observability/otel/metrics.py +551 -0
  198. trueppm_api/apps/observability/otel/provider.py +511 -0
  199. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  200. trueppm_api/apps/observability/purge_registry.py +124 -0
  201. trueppm_api/apps/observability/retention.py +139 -0
  202. trueppm_api/apps/observability/selectors.py +969 -0
  203. trueppm_api/apps/observability/serializers.py +142 -0
  204. trueppm_api/apps/observability/services.py +407 -0
  205. trueppm_api/apps/observability/tasks.py +266 -0
  206. trueppm_api/apps/observability/urls.py +27 -0
  207. trueppm_api/apps/observability/views.py +523 -0
  208. trueppm_api/apps/profiles/__init__.py +0 -0
  209. trueppm_api/apps/profiles/apps.py +15 -0
  210. trueppm_api/apps/profiles/constants.py +54 -0
  211. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  212. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  213. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  214. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  215. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  216. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  217. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  218. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  219. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  220. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  221. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  222. trueppm_api/apps/profiles/models.py +276 -0
  223. trueppm_api/apps/profiles/serializers.py +199 -0
  224. trueppm_api/apps/profiles/services.py +466 -0
  225. trueppm_api/apps/profiles/views.py +124 -0
  226. trueppm_api/apps/projects/__init__.py +0 -0
  227. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  228. trueppm_api/apps/projects/amend.py +133 -0
  229. trueppm_api/apps/projects/apps.py +38 -0
  230. trueppm_api/apps/projects/asset_feed.py +459 -0
  231. trueppm_api/apps/projects/asset_views.py +317 -0
  232. trueppm_api/apps/projects/attachment_policy.py +278 -0
  233. trueppm_api/apps/projects/authentication.py +449 -0
  234. trueppm_api/apps/projects/backfill.py +287 -0
  235. trueppm_api/apps/projects/backlog_services.py +324 -0
  236. trueppm_api/apps/projects/backlog_views.py +302 -0
  237. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  238. trueppm_api/apps/projects/blocker_services.py +510 -0
  239. trueppm_api/apps/projects/board_activity.py +462 -0
  240. trueppm_api/apps/projects/board_activity_views.py +186 -0
  241. trueppm_api/apps/projects/board_lanes.py +134 -0
  242. trueppm_api/apps/projects/bulk_settings.py +194 -0
  243. trueppm_api/apps/projects/bundled_templates.py +215 -0
  244. trueppm_api/apps/projects/calendar_settings.py +180 -0
  245. trueppm_api/apps/projects/ceremony_views.py +244 -0
  246. trueppm_api/apps/projects/commit_moment.py +204 -0
  247. trueppm_api/apps/projects/config_notice.py +1325 -0
  248. trueppm_api/apps/projects/custom_field_values.py +266 -0
  249. trueppm_api/apps/projects/decisions_services.py +93 -0
  250. trueppm_api/apps/projects/decisions_views.py +94 -0
  251. trueppm_api/apps/projects/estimation_scale.py +126 -0
  252. trueppm_api/apps/projects/export_bundle.py +353 -0
  253. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
  254. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
  255. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
  256. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
  257. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
  258. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  259. trueppm_api/apps/projects/inbound_sync.py +555 -0
  260. trueppm_api/apps/projects/iteration_label.py +85 -0
  261. trueppm_api/apps/projects/lifecycle.py +191 -0
  262. trueppm_api/apps/projects/management/__init__.py +0 -0
  263. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  264. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  265. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  266. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  267. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  268. trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
  269. trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
  270. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
  271. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  272. trueppm_api/apps/projects/mcp_settings.py +305 -0
  273. trueppm_api/apps/projects/methodology.py +178 -0
  274. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  275. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
  276. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  277. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  278. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  279. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  280. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  281. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  282. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  283. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  284. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  285. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  286. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  287. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  288. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  289. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  290. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  291. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  292. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  293. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  294. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  295. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  296. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  297. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  298. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  299. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  300. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  301. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  302. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  303. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  304. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  305. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  306. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  307. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  308. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  309. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  310. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  311. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  312. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  313. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  314. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  315. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  316. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  317. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  318. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  319. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  320. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  321. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  322. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  323. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  324. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  325. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  326. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  327. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  328. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  329. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  330. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  331. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  332. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  333. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  334. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  335. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  336. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  337. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  338. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  339. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  340. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  341. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  342. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  343. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  344. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  345. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  346. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  347. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  348. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  349. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  350. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  351. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  352. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  353. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  354. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  355. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  356. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  357. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  358. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  359. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  360. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  361. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  362. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  363. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  364. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  365. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  366. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  367. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  368. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
  369. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  370. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  371. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  372. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  373. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  374. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  375. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  376. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  377. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  378. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  379. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
  380. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  381. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  382. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  383. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  384. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  385. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
  386. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  387. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  388. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  389. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  390. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  391. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  392. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  393. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  394. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  395. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  396. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  397. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  398. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  399. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  400. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  401. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  402. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  403. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  404. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  405. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  406. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  407. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  408. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  409. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  410. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  411. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  412. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  413. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  414. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  415. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  416. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  417. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  418. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  419. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  420. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  421. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  422. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  423. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  424. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  425. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  426. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  427. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  428. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  429. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  430. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  431. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  432. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  433. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  434. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  435. trueppm_api/apps/projects/models.py +8946 -0
  436. trueppm_api/apps/projects/poker_services.py +154 -0
  437. trueppm_api/apps/projects/poker_views.py +398 -0
  438. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  439. trueppm_api/apps/projects/program_label_services.py +94 -0
  440. trueppm_api/apps/projects/program_label_views.py +189 -0
  441. trueppm_api/apps/projects/program_rollup.py +575 -0
  442. trueppm_api/apps/projects/program_schedule.py +745 -0
  443. trueppm_api/apps/projects/program_views.py +3220 -0
  444. trueppm_api/apps/projects/project_templates.py +422 -0
  445. trueppm_api/apps/projects/receivers.py +234 -0
  446. trueppm_api/apps/projects/refusal_codes.py +140 -0
  447. trueppm_api/apps/projects/reorder_services.py +145 -0
  448. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  449. trueppm_api/apps/projects/retro_board_services.py +368 -0
  450. trueppm_api/apps/projects/retro_services.py +285 -0
  451. trueppm_api/apps/projects/risk_import.py +396 -0
  452. trueppm_api/apps/projects/row_identity.py +113 -0
  453. trueppm_api/apps/projects/schema_migrations.py +228 -0
  454. trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
  455. trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
  456. trueppm_api/apps/projects/seed/__init__.py +61 -0
  457. trueppm_api/apps/projects/seed/exporter.py +1644 -0
  458. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  459. trueppm_api/apps/projects/seed/importer.py +2390 -0
  460. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  461. trueppm_api/apps/projects/seed/reldates.py +126 -0
  462. trueppm_api/apps/projects/seed/replace.py +126 -0
  463. trueppm_api/apps/projects/seed/replay.py +1691 -0
  464. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  465. trueppm_api/apps/projects/seed/samples.py +427 -0
  466. trueppm_api/apps/projects/seed/validation.py +1149 -0
  467. trueppm_api/apps/projects/serializers.py +12310 -0
  468. trueppm_api/apps/projects/services.py +6966 -0
  469. trueppm_api/apps/projects/share_serializers.py +132 -0
  470. trueppm_api/apps/projects/share_services.py +325 -0
  471. trueppm_api/apps/projects/share_views.py +374 -0
  472. trueppm_api/apps/projects/sharing_settings.py +187 -0
  473. trueppm_api/apps/projects/signal_privacy_services.py +909 -0
  474. trueppm_api/apps/projects/signal_privacy_views.py +476 -0
  475. trueppm_api/apps/projects/signals.py +132 -0
  476. trueppm_api/apps/projects/slip_conflict.py +266 -0
  477. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  478. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  479. trueppm_api/apps/projects/standup.py +304 -0
  480. trueppm_api/apps/projects/standup_views.py +99 -0
  481. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  482. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  483. trueppm_api/apps/projects/surface_visibility.py +126 -0
  484. trueppm_api/apps/projects/task_batch_services.py +379 -0
  485. trueppm_api/apps/projects/task_bulk.py +1152 -0
  486. trueppm_api/apps/projects/task_classification.py +561 -0
  487. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  488. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  489. trueppm_api/apps/projects/task_grouping.py +754 -0
  490. trueppm_api/apps/projects/tasks.py +1917 -0
  491. trueppm_api/apps/projects/template_divergence.py +168 -0
  492. trueppm_api/apps/projects/template_services.py +160 -0
  493. trueppm_api/apps/projects/template_tasks.py +258 -0
  494. trueppm_api/apps/projects/template_views.py +799 -0
  495. trueppm_api/apps/projects/throttles.py +311 -0
  496. trueppm_api/apps/projects/urls.py +1011 -0
  497. trueppm_api/apps/projects/utilization.py +885 -0
  498. trueppm_api/apps/projects/views.py +20572 -0
  499. trueppm_api/apps/projects/wbs_paths.py +119 -0
  500. trueppm_api/apps/resources/__init__.py +0 -0
  501. trueppm_api/apps/resources/apps.py +8 -0
  502. trueppm_api/apps/resources/capacity.py +121 -0
  503. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  504. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  505. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  506. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  507. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  508. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  509. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  510. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  511. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  512. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  513. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  514. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  515. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  516. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  517. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  518. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  519. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  520. trueppm_api/apps/resources/models.py +298 -0
  521. trueppm_api/apps/resources/serializers.py +426 -0
  522. trueppm_api/apps/resources/services.py +465 -0
  523. trueppm_api/apps/resources/urls.py +28 -0
  524. trueppm_api/apps/resources/views.py +1749 -0
  525. trueppm_api/apps/scheduling/__init__.py +0 -0
  526. trueppm_api/apps/scheduling/apps.py +153 -0
  527. trueppm_api/apps/scheduling/calendars.py +119 -0
  528. trueppm_api/apps/scheduling/deadletter.py +91 -0
  529. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  530. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  531. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  532. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  533. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  534. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  535. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  536. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  537. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  538. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  539. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  540. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  541. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  542. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  543. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  544. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  545. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  546. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  547. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  548. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  549. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  550. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  551. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  552. trueppm_api/apps/scheduling/models.py +516 -0
  553. trueppm_api/apps/scheduling/receivers.py +160 -0
  554. trueppm_api/apps/scheduling/risk_premium.py +283 -0
  555. trueppm_api/apps/scheduling/serializers.py +564 -0
  556. trueppm_api/apps/scheduling/services.py +1182 -0
  557. trueppm_api/apps/scheduling/signals.py +56 -0
  558. trueppm_api/apps/scheduling/tasks.py +1984 -0
  559. trueppm_api/apps/scheduling/telemetry.py +80 -0
  560. trueppm_api/apps/scheduling/units.py +35 -0
  561. trueppm_api/apps/scheduling/urls.py +57 -0
  562. trueppm_api/apps/scheduling/views.py +2564 -0
  563. trueppm_api/apps/sso/__init__.py +0 -0
  564. trueppm_api/apps/sso/apps.py +19 -0
  565. trueppm_api/apps/sso/extensions.py +130 -0
  566. trueppm_api/apps/sso/management/__init__.py +0 -0
  567. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  568. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  569. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  570. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  571. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  572. trueppm_api/apps/sso/models.py +148 -0
  573. trueppm_api/apps/sso/serializers.py +437 -0
  574. trueppm_api/apps/sso/services.py +1385 -0
  575. trueppm_api/apps/sso/urls.py +47 -0
  576. trueppm_api/apps/sso/views.py +935 -0
  577. trueppm_api/apps/sync/__init__.py +0 -0
  578. trueppm_api/apps/sync/apps.py +15 -0
  579. trueppm_api/apps/sync/backfill.py +208 -0
  580. trueppm_api/apps/sync/broadcast.py +320 -0
  581. trueppm_api/apps/sync/conflict.py +262 -0
  582. trueppm_api/apps/sync/consumers.py +471 -0
  583. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  584. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  585. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  586. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  587. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  588. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  589. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  590. trueppm_api/apps/sync/models.py +225 -0
  591. trueppm_api/apps/sync/pagination.py +231 -0
  592. trueppm_api/apps/sync/sequence.py +371 -0
  593. trueppm_api/apps/sync/serializers.py +687 -0
  594. trueppm_api/apps/sync/tasks.py +260 -0
  595. trueppm_api/apps/sync/throttles.py +175 -0
  596. trueppm_api/apps/sync/upload.py +501 -0
  597. trueppm_api/apps/sync/urls.py +17 -0
  598. trueppm_api/apps/sync/views.py +930 -0
  599. trueppm_api/apps/sync/ws_auth.py +193 -0
  600. trueppm_api/apps/taskruns/__init__.py +0 -0
  601. trueppm_api/apps/taskruns/apps.py +11 -0
  602. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  603. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  604. trueppm_api/apps/taskruns/models.py +68 -0
  605. trueppm_api/apps/taskruns/serializers.py +92 -0
  606. trueppm_api/apps/taskruns/tasks.py +60 -0
  607. trueppm_api/apps/taskruns/tracker.py +286 -0
  608. trueppm_api/apps/taskruns/urls.py +46 -0
  609. trueppm_api/apps/taskruns/views.py +195 -0
  610. trueppm_api/apps/teams/__init__.py +8 -0
  611. trueppm_api/apps/teams/apps.py +16 -0
  612. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  613. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
  614. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  615. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  616. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  617. trueppm_api/apps/teams/models.py +145 -0
  618. trueppm_api/apps/teams/permissions.py +149 -0
  619. trueppm_api/apps/teams/serializers.py +95 -0
  620. trueppm_api/apps/teams/services.py +434 -0
  621. trueppm_api/apps/teams/signals.py +61 -0
  622. trueppm_api/apps/teams/urls.py +34 -0
  623. trueppm_api/apps/teams/views.py +201 -0
  624. trueppm_api/apps/timetracking/__init__.py +0 -0
  625. trueppm_api/apps/timetracking/apps.py +9 -0
  626. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  627. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  628. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  629. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  630. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  631. trueppm_api/apps/timetracking/models.py +178 -0
  632. trueppm_api/apps/timetracking/serializers.py +175 -0
  633. trueppm_api/apps/timetracking/services.py +117 -0
  634. trueppm_api/apps/timetracking/urls.py +47 -0
  635. trueppm_api/apps/timetracking/views.py +526 -0
  636. trueppm_api/apps/webhooks/__init__.py +0 -0
  637. trueppm_api/apps/webhooks/apps.py +8 -0
  638. trueppm_api/apps/webhooks/backfill.py +90 -0
  639. trueppm_api/apps/webhooks/dispatch.py +144 -0
  640. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  641. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
  642. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  643. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  644. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  645. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  646. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  647. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  648. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  649. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
  650. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  651. trueppm_api/apps/webhooks/models.py +372 -0
  652. trueppm_api/apps/webhooks/serializers.py +318 -0
  653. trueppm_api/apps/webhooks/tasks.py +663 -0
  654. trueppm_api/apps/webhooks/urls.py +22 -0
  655. trueppm_api/apps/webhooks/views.py +383 -0
  656. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  657. trueppm_api/apps/workflow_engine/apps.py +23 -0
  658. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  659. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  660. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  661. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  662. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  663. trueppm_api/apps/workflow_engine/models.py +265 -0
  664. trueppm_api/apps/workflow_engine/tasks.py +368 -0
  665. trueppm_api/apps/workspace/__init__.py +0 -0
  666. trueppm_api/apps/workspace/apps.py +10 -0
  667. trueppm_api/apps/workspace/backfill.py +111 -0
  668. trueppm_api/apps/workspace/export.py +242 -0
  669. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  670. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  671. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  672. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  673. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  674. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  675. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  676. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  677. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  678. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  679. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  680. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  681. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  682. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  683. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  684. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  685. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  686. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  687. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  688. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  689. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  690. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  691. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  692. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  693. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  694. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  695. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  696. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  697. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  698. trueppm_api/apps/workspace/models.py +857 -0
  699. trueppm_api/apps/workspace/permissions.py +187 -0
  700. trueppm_api/apps/workspace/serializers.py +590 -0
  701. trueppm_api/apps/workspace/services.py +919 -0
  702. trueppm_api/apps/workspace/signals.py +37 -0
  703. trueppm_api/apps/workspace/tasks.py +555 -0
  704. trueppm_api/apps/workspace/urls.py +113 -0
  705. trueppm_api/apps/workspace/views.py +1502 -0
  706. trueppm_api/asgi.py +42 -0
  707. trueppm_api/celery.py +31 -0
  708. trueppm_api/core/__init__.py +0 -0
  709. trueppm_api/core/admin_site.py +206 -0
  710. trueppm_api/core/auth_views.py +916 -0
  711. trueppm_api/core/constant_time.py +51 -0
  712. trueppm_api/core/csp.py +54 -0
  713. trueppm_api/core/db.py +87 -0
  714. trueppm_api/core/db_session.py +37 -0
  715. trueppm_api/core/exception_handlers.py +157 -0
  716. trueppm_api/core/export_downloads.py +56 -0
  717. trueppm_api/core/extension_providers.py +56 -0
  718. trueppm_api/core/extension_signals.py +147 -0
  719. trueppm_api/core/idempotent.py +260 -0
  720. trueppm_api/core/metadata.py +42 -0
  721. trueppm_api/core/middleware.py +51 -0
  722. trueppm_api/core/openapi.py +949 -0
  723. trueppm_api/core/password_reset.py +500 -0
  724. trueppm_api/core/protect_conflict.py +100 -0
  725. trueppm_api/core/ratelimit.py +148 -0
  726. trueppm_api/core/redis_throttle.py +82 -0
  727. trueppm_api/core/refresh_cookie_policy.py +156 -0
  728. trueppm_api/core/request_body.py +86 -0
  729. trueppm_api/core/security_checks.py +884 -0
  730. trueppm_api/core/storage_config.py +105 -0
  731. trueppm_api/core/text_decode.py +168 -0
  732. trueppm_api/core/throttling.py +344 -0
  733. trueppm_api/core/valkey.py +214 -0
  734. trueppm_api/core/valkey_cache.py +66 -0
  735. trueppm_api/core/valkey_checks.py +104 -0
  736. trueppm_api/core/valkey_config.py +39 -0
  737. trueppm_api/core/worker_broker_wait.py +149 -0
  738. trueppm_api/core/worker_heartbeat.py +134 -0
  739. trueppm_api/fields.py +36 -0
  740. trueppm_api/permissions.py +30 -0
  741. trueppm_api/py.typed +0 -0
  742. trueppm_api/routing.py +11 -0
  743. trueppm_api/settings/__init__.py +0 -0
  744. trueppm_api/settings/base.py +2455 -0
  745. trueppm_api/settings/dev.py +273 -0
  746. trueppm_api/settings/prod.py +228 -0
  747. trueppm_api/urls.py +153 -0
  748. trueppm_api/workflows/__init__.py +38 -0
  749. trueppm_api/workflows/backends/__init__.py +6 -0
  750. trueppm_api/workflows/backends/default.py +297 -0
  751. trueppm_api/workflows/consumers/__init__.py +11 -0
  752. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  753. trueppm_api/workflows/interface.py +148 -0
  754. trueppm_api/workflows/registry.py +106 -0
  755. trueppm_api/workflows/services.py +90 -0
  756. trueppm_api/wsgi.py +11 -0
  757. trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
  758. trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
  759. trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
@@ -0,0 +1,711 @@
1
+ """Celery tasks for the notifications app (ADR-0075 §F durable-execution).
2
+
3
+ Three Beat-scheduled tasks:
4
+
5
+ - ``drain_notification_emails`` — every 30 s, finds Notification rows with
6
+ ``email_pending=True`` older than the 5-min orphan window, renders an email
7
+ for each, sends via SMTP, and updates delivery state. Best-effort — broker
8
+ outage logs but doesn't propagate. Re-checks the recipient's account and
9
+ project-membership status at drain time, not just at creation time (#3523,
10
+ #3675) — a row queued before an off-boarding or a project removal is
11
+ retired unsent rather than delivered on a stale grant.
12
+
13
+ - ``archive_old_notifications`` — nightly, sets ``is_archived=True`` on any
14
+ Notification older than 90 days that has ``is_read=True``. Keeps the
15
+ index on ``(recipient, is_read, -created_at)`` shallow for the unread
16
+ query path.
17
+
18
+ - ``detect_stale_tasks`` — nightly (ADR-0200), scans every project for
19
+ non-terminal tasks that have sat in their current status past the project's
20
+ ``stale_task_threshold_days`` (default 7) and nudges the assignee via the
21
+ #639 event-notification rail. Idempotent: dedupes against existing unread
22
+ ``task.stale`` notifications, so a re-run creates zero duplicates.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import contextlib
28
+ import html
29
+ import logging
30
+ import textwrap
31
+ from datetime import timedelta
32
+ from typing import TYPE_CHECKING, Any
33
+
34
+ from trueppm_api.core.idempotent import idempotent_task
35
+
36
+ from .delivery_limits import (
37
+ EMAIL_MAX_BATCH_SIZE,
38
+ beat_ticks_per_minute,
39
+ per_tick_cap,
40
+ reserve_send_budget,
41
+ )
42
+
43
+ if TYPE_CHECKING:
44
+ from django.core.mail.backends.base import BaseEmailBackend
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ # Locked constants for email delivery — kept in this module so they're
50
+ # adjacent to the drain task that owns them.
51
+ EMAIL_MAX_RETRIES = 3
52
+ EMAIL_ORPHAN_WINDOW_MINUTES = 5 # ADR-0075 §F durable-execution checklist item 3
53
+ # Fallback cap per drain tick, when no operator limit applies. Re-exported from
54
+ # delivery_limits, which every mail path now shares (#2887 item 3).
55
+ EMAIL_BATCH_SIZE = EMAIL_MAX_BATCH_SIZE
56
+ # Beat task name — also the key the per-minute throttle divisor is derived from, so
57
+ # the cadence and the throttle can no longer disagree (#2887 item 4).
58
+ DRAIN_TASK_NAME = "notifications.drain_notification_emails"
59
+ ARCHIVE_AFTER_DAYS = 90 # ADR-0075 §F item 6 — outbox cleanup
60
+
61
+ # Snippet rendering (#574, security review !306 LOW-1). SNIPPET_MAX_CHARS bounds
62
+ # the raw comment text before it's embedded in the email body; SNIPPET_WRAP_WIDTH
63
+ # hard-wraps it so a long unbroken string (a pasted URL, log line, or base64 blob)
64
+ # can't render as a single unbounded line in the recipient's mail client.
65
+ SNIPPET_MAX_CHARS = 280
66
+ SNIPPET_WRAP_WIDTH = 72 # classic plain-text convention; keeps quoted replies readable
67
+
68
+
69
+ @idempotent_task(
70
+ lock_key_template="drain_notification_emails",
71
+ lock_ttl=60,
72
+ on_contention="skip",
73
+ soft_time_limit=25,
74
+ time_limit=30,
75
+ acks_late=True,
76
+ reject_on_worker_lost=True,
77
+ name="notifications.drain_notification_emails",
78
+ )
79
+ def drain_notification_emails(self: object) -> None:
80
+ """Send queued email notifications.
81
+
82
+ Runs every 30 seconds via Celery Beat. Finds Notification rows where
83
+ ``email_pending=True`` and ``email_sent_at IS NULL`` and ``email_attempts
84
+ < EMAIL_MAX_RETRIES`` and ``created_at < (now - orphan_window)``.
85
+
86
+ The orphan-window filter prevents racing the comment-create transaction —
87
+ a Notification inserted inside a still-open atomic block isn't visible
88
+ to this drain until commit AND has aged past the window. The 5-min value
89
+ matches the webhook drain (ADR-0019).
90
+
91
+ The singleton lock + ``on_contention="skip"`` ensures at most one drain
92
+ runs at a time; the next Beat tick picks up anything missed.
93
+ """
94
+ _do_drain_emails()
95
+
96
+
97
+ @idempotent_task(
98
+ lock_key_template="archive_old_notifications",
99
+ lock_ttl=120,
100
+ on_contention="skip",
101
+ soft_time_limit=55,
102
+ time_limit=90,
103
+ acks_late=True,
104
+ reject_on_worker_lost=True,
105
+ name="notifications.archive_old_notifications",
106
+ )
107
+ def archive_old_notifications(self: object) -> None:
108
+ """Archive read notifications older than 90 days.
109
+
110
+ Runs nightly via Celery Beat. Sets ``is_archived=True`` on Notifications
111
+ where ``is_read=True`` and ``created_at < (now - ARCHIVE_AFTER_DAYS)``.
112
+
113
+ Archive is a soft state — rows stay in the table, are excluded from
114
+ default list queries, and remain reachable via the explicit ``archived``
115
+ filter. A future hard-purge for archived rows older than 365 days is
116
+ operational concern (not in 0.2).
117
+ """
118
+ _do_archive()
119
+
120
+
121
+ @idempotent_task(
122
+ lock_key_template="detect_stale_tasks",
123
+ lock_ttl=300,
124
+ on_contention="skip",
125
+ soft_time_limit=110,
126
+ time_limit=120,
127
+ acks_late=True,
128
+ reject_on_worker_lost=True,
129
+ name="notifications.detect_stale_tasks",
130
+ )
131
+ def detect_stale_tasks(self: object) -> None:
132
+ """Nudge assignees of stale non-terminal tasks (ADR-0200).
133
+
134
+ Runs nightly via Celery Beat. Delegates to
135
+ :func:`notifications.services.create_stale_task_notifications`, which owns the
136
+ per-project threshold query, the unread dedupe, and the preference-gated fan-out.
137
+
138
+ Idempotent by construction: the dedupe against existing unread ``task.stale``
139
+ notifications means a broker retry, a manual re-queue, or the next nightly tick
140
+ all create zero duplicates. The singleton lock (``on_contention="skip"``) also
141
+ prevents two overlapping runs. A failed run is not retried — the tasks are still
142
+ stale tomorrow, so silent re-attempt on the next tick is correct and avoids a
143
+ retry storm (ADR-0200 §Durable Execution item 8).
144
+ """
145
+ from .services import create_stale_task_notifications
146
+
147
+ created = create_stale_task_notifications()
148
+ if created:
149
+ logger.info("detect_stale_tasks: created %d stale-task notification(s)", created)
150
+
151
+
152
+ @idempotent_task(
153
+ lock_key_template="send_scheduled_digests",
154
+ lock_ttl=300,
155
+ on_contention="skip",
156
+ soft_time_limit=280,
157
+ time_limit=300,
158
+ acks_late=True,
159
+ reject_on_worker_lost=True,
160
+ max_retries=0,
161
+ name="notifications.send_scheduled_digests",
162
+ )
163
+ def send_scheduled_digests(self: object) -> None:
164
+ """Send weekly digests whose per-user slot matches this hour (ADR-0663).
165
+
166
+ Runs **hourly** via Beat even though the digests are weekly: the send time is
167
+ a per-user property (``UserNotificationSettings.digest_*``) in the user's own
168
+ timezone, so the sweep has to look every hour and match slots rather than fire
169
+ on one cluster-wide schedule.
170
+
171
+ ``max_retries=0`` — retrying is pointless here. The next tick is 60 minutes
172
+ away and re-evaluates from current state, and the ledger row is already written,
173
+ so a retry would either no-op or mail a stale snapshot. A failed run means one
174
+ skipped weekly digest, which the reader can recover by logging in
175
+ (ADR-0663 §Durable Execution item 8).
176
+ """
177
+ from .digests import send_due_digests
178
+
179
+ created = send_due_digests()
180
+ if created:
181
+ logger.info("send_scheduled_digests: created %d digest notification(s)", created)
182
+
183
+
184
+ @idempotent_task(
185
+ lock_key_template="purge_old_digest_runs",
186
+ lock_ttl=300,
187
+ on_contention="skip",
188
+ soft_time_limit=55,
189
+ time_limit=90,
190
+ acks_late=True,
191
+ reject_on_worker_lost=True,
192
+ name="notifications.purge_old_digest_runs",
193
+ )
194
+ def purge_old_digest_runs(self: object) -> None:
195
+ """Delete digest ledger rows past the retention window (ADR-0663 item 6).
196
+
197
+ Retention is 90 days rather than the 7-day per-request outbox convention: the
198
+ ledger exists to remember a *weekly* send, so a 7-day window would forget a
199
+ fortnight-old run it must not repeat.
200
+ """
201
+ from .digests import purge_old_digest_runs as _purge
202
+
203
+ deleted = _purge()
204
+ if deleted:
205
+ logger.info("purge_old_digest_runs: deleted %d ledger row(s)", deleted)
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Drain — extracted for direct testability without Celery in the loop
210
+ # ---------------------------------------------------------------------------
211
+
212
+
213
+ def _drain_batch_size(email_settings: Any) -> int:
214
+ """Rows to drain this tick, honoring the operator's delivery limits (#2860).
215
+
216
+ Thin wrapper over :func:`delivery_limits.per_tick_cap` that supplies this drain's
217
+ beat cadence. The interpretation of both controls, and the shared per-minute budget
218
+ that spans every mail path, live in ``delivery_limits`` (#2887 item 3).
219
+ """
220
+ return per_tick_cap(
221
+ email_settings,
222
+ ticks_per_minute=beat_ticks_per_minute(DRAIN_TASK_NAME),
223
+ fallback=EMAIL_MAX_BATCH_SIZE,
224
+ )
225
+
226
+
227
+ def _current_project_membership_exists() -> Any:
228
+ """Correlated ``EXISTS(ProjectMembership …)`` for a notification's (recipient, project).
229
+
230
+ Shared by :func:`_retire_pending_for_revoked_membership` and the pending-row
231
+ query in :func:`_do_drain_emails` so both express the same boundary
232
+ ``NotificationSerializer._recipient_can_see_project`` (#3510) enforces on
233
+ read — as one correlated subquery attached to whatever outer
234
+ ``Notification`` queryset calls this, never a per-row Python-side lookup
235
+ (#3675 review note: this drain processes a whole batch per 30 s tick, so an
236
+ N+1 here scales with backlog size).
237
+
238
+ ``is_deleted=False`` matches the serializer predicate exactly: a soft-deleted
239
+ (revoked) ``ProjectMembership`` row does not count as current membership.
240
+ """
241
+ from django.db.models import Exists, OuterRef
242
+
243
+ from trueppm_api.apps.access.models import ProjectMembership
244
+
245
+ return Exists(
246
+ ProjectMembership.objects.filter(
247
+ user_id=OuterRef("recipient_id"),
248
+ project_id=OuterRef("project_id"),
249
+ is_deleted=False,
250
+ )
251
+ )
252
+
253
+
254
+ def _retire_pending_for_revoked_membership() -> int:
255
+ """Retire queued emails whose recipient has since lost project membership (#3675).
256
+
257
+ Mirrors :func:`_retire_pending_for_deactivated_recipients` (#3523) for the
258
+ membership axis rather than the account-active axis. ``_render_email`` /
259
+ ``_send_email_for_notification`` read ``Notification.subject``/``.body``
260
+ straight off the frozen row and mail them via SMTP without ever consulting
261
+ ``NotificationSerializer._recipient_can_see_project`` (#3510) — so a member
262
+ removed from ``obj.project`` during the orphan-window-plus-retry gap between
263
+ a notification's creation and its eventual successful send still received a
264
+ plaintext email naming a project/task they can no longer see through the API.
265
+ Fixing the REST read path (#3510) does not reach this channel because it
266
+ never goes through the serializer at all.
267
+
268
+ A row with no project (``project_id IS NULL`` — the ADR-0663 account-scoped
269
+ digest shape) has no membership boundary to check and is left untouched,
270
+ matching ``_recipient_can_see_project``'s own "falls open with no project"
271
+ rule.
272
+
273
+ **Retired, not failed** — same terminal-state reasoning as #3523: only
274
+ ``email_pending`` is cleared here. ``email_attempts`` and ``email_failed_at``
275
+ are deliberately left untouched, so ``observability.selectors.
276
+ notification_email_signals``'s ``failed_recent`` signal (``email_pending=
277
+ False`` AND ``email_attempts >= EMAIL_MAX_RETRIES`` AND a recent
278
+ ``email_failed_at``) never reports a deliberate privacy suppression as an
279
+ SMTP/mail-relay failure, and the row does not sit in ``queued_aging`` forever
280
+ either (it reaches a terminal state instead of parking in the backlog).
281
+
282
+ Resolved as a single ``UPDATE … WHERE NOT EXISTS (…)`` rather than one
283
+ membership query per row — the drain runs every 30 s over the whole pending
284
+ backlog, so a per-row check would multiply with backlog size.
285
+
286
+ Returns:
287
+ The number of rows retired — 0 on every tick where no pending recipient
288
+ has lost project access, the steady state.
289
+ """
290
+ from .models import Notification
291
+
292
+ retired: int = (
293
+ Notification.objects.filter(
294
+ email_pending=True,
295
+ email_sent_at__isnull=True,
296
+ project_id__isnull=False,
297
+ )
298
+ .exclude(_current_project_membership_exists())
299
+ .update(email_pending=False)
300
+ )
301
+ if retired:
302
+ logger.info(
303
+ "drain_notification_emails: retired %d queued email(s) for recipient(s) "
304
+ "no longer a member of the notification's project",
305
+ retired,
306
+ )
307
+ return retired
308
+
309
+
310
+ def _retire_pending_for_deactivated_recipients() -> int:
311
+ """Retire queued emails whose recipient has since been deactivated (#3523).
312
+
313
+ The digest sweep now excludes deactivated users at the audience (see
314
+ ``digests._opted_in_user_ids``), but rows queued *before* off-boarding are already
315
+ in the outbox — and every notification email in OSS leaves through this one drain,
316
+ so this is the single chokepoint at which the account axis covers the mention,
317
+ stale-task, event and digest rails at once.
318
+
319
+ **Retired, not merely skipped.** Excluding these rows from the candidate query
320
+ alone would leave them ``email_pending=True`` forever, and
321
+ ``observability.selectors.notification_email_signals`` reads exactly that as its
322
+ ``queued_aging`` signal — rows pending past an hour mean "Beat is dead, the worker
323
+ is gone, or the transport cannot be built". A permanent false alarm on the System
324
+ Health card is the over-claiming that selector's docstring exists to prevent, so
325
+ the row must reach a terminal state instead of parking in the backlog.
326
+
327
+ The terminal state written here is ``email_pending=False`` **only**.
328
+ ``email_attempts`` and ``email_failed_at`` are deliberately left untouched: the
329
+ same selector derives ``failed_recent`` from ``email_pending=False`` **and**
330
+ ``email_attempts >= EMAIL_MAX_RETRIES`` **and** a recent ``email_failed_at``, so
331
+ stamping either would report an off-boarding as a mail-relay failure. This is not
332
+ a delivery failure — there is no longer anyone to deliver to.
333
+
334
+ The durable in-app ``Notification`` row is left alone. It is the inbox record, and
335
+ a deactivated account cannot authenticate to read it (JWT, session and PAT all
336
+ close on ``is_active``); the email is the only channel that reaches outward.
337
+
338
+ Reactivation does not resurrect a retired row, which is the intended trade: an
339
+ off-boarding is not expected to reverse, and a week-old digest replayed on
340
+ reinstatement would state program health that has since moved on.
341
+
342
+ Returns:
343
+ The number of rows retired — 0 on every tick of a workspace with no pending
344
+ mail for a deactivated user, which is the steady state.
345
+ """
346
+ from .models import Notification
347
+
348
+ retired: int = Notification.objects.filter(
349
+ email_pending=True,
350
+ email_sent_at__isnull=True,
351
+ recipient__is_active=False,
352
+ ).update(email_pending=False)
353
+ if retired:
354
+ logger.info(
355
+ "drain_notification_emails: retired %d queued email(s) for deactivated "
356
+ "recipient(s) without sending",
357
+ retired,
358
+ )
359
+ return retired
360
+
361
+
362
+ def _do_drain_emails() -> None:
363
+ from django.utils import timezone
364
+
365
+ from .models import Notification, WorkspaceEmailSettings
366
+
367
+ now = timezone.now()
368
+ orphan_cutoff = now - timedelta(minutes=EMAIL_ORPHAN_WINDOW_MINUTES)
369
+
370
+ _retire_pending_for_deactivated_recipients()
371
+ _retire_pending_for_revoked_membership()
372
+
373
+ # Read the operator's delivery limits BEFORE the query, because they bound it
374
+ # (#2860). max_recipients and throttle_per_min persisted, validated and rendered
375
+ # back in Workspace → Email for three releases while this task capped every
376
+ # drain at a hardcoded 50 regardless — a control that looked applied and was not.
377
+ email_settings = WorkspaceEmailSettings.load()
378
+ # Two-stage: the per-tick cap smooths within a minute, the shared budget bounds
379
+ # the minute across *every* mail path (invite drain, password reset, export-ready)
380
+ # so no single path can spend the whole allowance (#2887 item 3).
381
+ budget = reserve_send_budget(
382
+ int(email_settings.throttle_per_min or 0), _drain_batch_size(email_settings)
383
+ )
384
+ granted = budget.granted
385
+ if granted <= 0:
386
+ return
387
+
388
+ from django.db.models import Q
389
+
390
+ pending = list(
391
+ Notification.objects.filter(
392
+ email_pending=True,
393
+ email_sent_at__isnull=True,
394
+ email_attempts__lt=EMAIL_MAX_RETRIES,
395
+ created_at__lt=orphan_cutoff,
396
+ # Fail closed on the account axis (#3523). The retirement pass above has
397
+ # already cleared these rows, so this predicate normally matches nothing;
398
+ # it is here for the row deactivated in the gap between that UPDATE and
399
+ # this SELECT, where the only safe default is not to send.
400
+ recipient__is_active=True,
401
+ )
402
+ # Fail closed on the membership axis (#3675), same reasoning: the retirement
403
+ # pass above already cleared rows whose recipient lost project access, this
404
+ # is the backstop for a revocation landing in the gap between that UPDATE and
405
+ # this SELECT. A row with no project (account-scoped digests) has no
406
+ # membership boundary and always passes.
407
+ .filter(_current_project_membership_exists() | Q(project_id__isnull=True))
408
+ .select_related("recipient", "mention", "mention__task_comment", "mention__mentioner")
409
+ .order_by("created_at")[:granted]
410
+ )
411
+
412
+ # Hand back what this tick will not spend, or an idle drain starves the other
413
+ # paths for the rest of the minute.
414
+ budget.release(granted - len(pending))
415
+
416
+ if not pending:
417
+ return
418
+
419
+ # Resolve the workspace transport + From identity ONCE per batch — the config
420
+ # is constant across the batch, so building it per message would be N SMTP
421
+ # connections + N single-row reads per tick (#712 perf, ADR-0213).
422
+ from .email_backend import (
423
+ EmailTransportError,
424
+ resolve_email_connection,
425
+ resolve_from_email,
426
+ resolve_reply_to,
427
+ )
428
+
429
+ try:
430
+ connection = resolve_email_connection(email_settings)
431
+ except EmailTransportError:
432
+ # The workspace has a transport configured that cannot be built — an
433
+ # undecryptable credential or a host the egress guard rejects (#2886 item 2).
434
+ # Deliberately return WITHOUT touching the rows: this is a configuration fault
435
+ # an operator fix clears, and burning three retries per row would permanently
436
+ # discard notifications for it. The rows stay pending, which is what the
437
+ # System Health card's aging-backlog signal reports on, and
438
+ # ``transport_status`` names the cause directly.
439
+ logger.error(
440
+ "drain_notification_emails: the configured mail transport is unusable — "
441
+ "%d row(s) left pending, no retries burned. Fix the transport in "
442
+ "Workspace -> Email.",
443
+ len(pending),
444
+ )
445
+ budget.release(len(pending))
446
+ return
447
+ from_email = resolve_from_email(email_settings)
448
+ reply_to = resolve_reply_to(email_settings)
449
+
450
+ # Open the shared connection once so all N sends reuse one socket/TLS
451
+ # handshake. Best-effort: if the transport is down, each per-message send
452
+ # still attempts (and fails) on its own, preserving the attempt/retry count.
453
+ try:
454
+ connection.open()
455
+ except Exception:
456
+ logger.warning("drain_notification_emails: could not open mail transport", exc_info=True)
457
+
458
+ sent = 0
459
+ failed = 0
460
+ try:
461
+ for notif in pending:
462
+ try:
463
+ ok = _send_email_for_notification(
464
+ notif,
465
+ connection=connection,
466
+ from_email=from_email,
467
+ reply_to=reply_to,
468
+ )
469
+ except Exception:
470
+ logger.exception(
471
+ "drain_notification_emails: unexpected error for notif %s", notif.pk
472
+ )
473
+ ok = False
474
+
475
+ if ok:
476
+ Notification.objects.filter(pk=notif.pk).update(
477
+ email_pending=False,
478
+ email_sent_at=timezone.now(),
479
+ )
480
+ sent += 1
481
+ else:
482
+ # Increment attempts atomically; on attempt N=max, clear
483
+ # email_pending so the row is no longer eligible for retry.
484
+ from django.db.models import F
485
+
486
+ new_attempts = notif.email_attempts + 1
487
+ update_fields: dict[str, object] = {
488
+ "email_attempts": F("email_attempts") + 1,
489
+ "email_failed_at": timezone.now(),
490
+ }
491
+ if new_attempts >= EMAIL_MAX_RETRIES:
492
+ update_fields["email_pending"] = False
493
+ Notification.objects.filter(pk=notif.pk).update(**update_fields)
494
+ failed += 1
495
+ finally:
496
+ with contextlib.suppress(Exception):
497
+ connection.close()
498
+
499
+ logger.info(
500
+ "drain_notification_emails: sent=%d failed=%d candidates=%d",
501
+ sent,
502
+ failed,
503
+ len(pending),
504
+ )
505
+
506
+
507
+ def _do_archive() -> None:
508
+ from django.utils import timezone
509
+
510
+ from .models import Notification
511
+
512
+ cutoff = timezone.now() - timedelta(days=ARCHIVE_AFTER_DAYS)
513
+ archived = Notification.objects.filter(
514
+ is_read=True,
515
+ is_archived=False,
516
+ created_at__lt=cutoff,
517
+ ).update(is_archived=True)
518
+ if archived:
519
+ logger.info(
520
+ "archive_old_notifications: archived %d row(s) older than %dd",
521
+ archived,
522
+ ARCHIVE_AFTER_DAYS,
523
+ )
524
+
525
+
526
+ # ---------------------------------------------------------------------------
527
+ # Email rendering — minimal Diátaxis-friendly subject + plain-text body
528
+ # ---------------------------------------------------------------------------
529
+
530
+
531
+ def _send_email_for_notification(
532
+ notif: object,
533
+ *,
534
+ connection: BaseEmailBackend | None = None,
535
+ from_email: str | None = None,
536
+ reply_to: list[str] | None = None,
537
+ ) -> bool:
538
+ """Render and send the email for a single Notification.
539
+
540
+ Returns True on successful SMTP delivery, False on any failure (SMTP error,
541
+ missing user email, missing source mention). The caller increments the
542
+ attempt counter — this function is pure-attempt, no state mutation.
543
+
544
+ The workspace transport (``connection``), ``from_email``, and ``reply_to``
545
+ are resolved **once per drain batch** and passed in (#712 perf: the config is
546
+ constant across a batch, so a per-message ``load()`` + SMTP connect would be
547
+ 50 redundant DB reads and SMTP/TLS handshakes per tick). When omitted they are
548
+ resolved here so a direct/one-off caller still works.
549
+ """
550
+ from django.core.mail import EmailMessage
551
+
552
+ from .email_backend import (
553
+ resolve_email_connection,
554
+ resolve_from_email,
555
+ resolve_reply_to,
556
+ )
557
+ from .models import Notification
558
+
559
+ notif_obj: Notification = notif # type: ignore[assignment]
560
+ recipient = notif_obj.recipient
561
+ recipient_email = getattr(recipient, "email", "") or ""
562
+ if not recipient_email:
563
+ logger.warning(
564
+ "drain_notification_emails: recipient %s has no email, skipping notif %s",
565
+ getattr(recipient, "pk", "?"),
566
+ notif_obj.pk,
567
+ )
568
+ return False
569
+
570
+ # A notification is mention-sourced OR event-sourced (#639, ADR-0085 §3);
571
+ # _render_email handles both shapes and returns ("", "") if neither yields
572
+ # renderable content (e.g. the source comment was deleted).
573
+ subject, body = _render_email(notif_obj)
574
+ if not subject or not body:
575
+ return False
576
+
577
+ # Dynamic transport + From identity from the workspace SMTP config (#712,
578
+ # ADR-0213). Falls back to the global backend / DEFAULT_FROM_EMAIL when the
579
+ # workspace transport is unconfigured (cloud), so this is a no-op for installs
580
+ # that never touch the writable config.
581
+ if connection is None:
582
+ connection = resolve_email_connection()
583
+ if from_email is None:
584
+ from_email = resolve_from_email()
585
+ if reply_to is None:
586
+ reply_to = resolve_reply_to()
587
+ msg = EmailMessage(
588
+ subject=subject,
589
+ body=body,
590
+ from_email=from_email,
591
+ to=[recipient_email],
592
+ reply_to=reply_to or None,
593
+ headers=_unsubscribe_headers() or None,
594
+ connection=connection,
595
+ )
596
+ try:
597
+ msg.send(fail_silently=False)
598
+ except Exception:
599
+ logger.warning(
600
+ "drain_notification_emails: SMTP send failed for notif %s",
601
+ notif_obj.pk,
602
+ exc_info=True,
603
+ )
604
+ return False
605
+ return True
606
+
607
+
608
+ def _render_email(notif: object) -> tuple[str, str]:
609
+ """Render a Notification into (subject, body) plain-text strings.
610
+
611
+ Kept deliberately minimal — the UI is the primary surface; email is a
612
+ secondary "ping the inbox" channel for users who opt in (default OFF).
613
+ A richer HTML template lands when the executive-digest issue ships
614
+ (trueppm-enterprise#112).
615
+ """
616
+ from .models import Notification
617
+
618
+ notif_obj: Notification = notif # type: ignore[assignment]
619
+ mention = notif_obj.mention
620
+ if mention is None:
621
+ # Event-sourced notification (#639): the subject/body were rendered at
622
+ # dispatch time and frozen on the row. Empty subject => nothing to send.
623
+ return notif_obj.subject, notif_obj.body
624
+
625
+ mentioner = mention.mentioner
626
+ mentioner_name = (
627
+ mentioner.get_full_name() or mentioner.username if mentioner is not None else "Someone"
628
+ )
629
+
630
+ comment = mention.task_comment
631
+ if comment is None or comment.is_deleted:
632
+ return "", ""
633
+
634
+ task = comment.task
635
+ task_name = task.name if task else "a task"
636
+ snippet = _sanitize_snippet(comment.body or "")
637
+
638
+ if mention.mentioned_group_key:
639
+ subject = f"{mentioner_name} mentioned @{mention.mentioned_group_key} on {task_name}"
640
+ else:
641
+ subject = f"{mentioner_name} mentioned you on {task_name}"
642
+
643
+ body_lines = [
644
+ f'{mentioner_name} mentioned you in a comment on "{task_name}":',
645
+ "",
646
+ snippet,
647
+ "",
648
+ "Open the task in TruePPM to reply or acknowledge.",
649
+ "",
650
+ "—",
651
+ "You can change which notifications send email at /me/settings/notifications/.",
652
+ ]
653
+ body = "\n".join(body_lines)
654
+ return subject, body
655
+
656
+
657
+ def _sanitize_snippet(raw: str) -> str:
658
+ """Bound and format a comment snippet for embedding in the plain-text email body.
659
+
660
+ Truncates to ``SNIPPET_MAX_CHARS``, HTML-escapes as defense-in-depth (the
661
+ body is sent as ``text/plain`` today, so this isn't an XSS vector, but
662
+ escaping now means a future HTML template — e.g. the executive-digest work
663
+ in trueppm-enterprise#112 — can reuse this helper without introducing a new
664
+ escaping gap), and hard-wraps at ``SNIPPET_WRAP_WIDTH`` so a long unbroken
665
+ string (a pasted URL, log line, or base64 blob) can't render as a single
666
+ unbounded line in the recipient's mail client (#574, security review !306
667
+ LOW-1). ``quote=False`` on the escape leaves straight/curly quotes and
668
+ apostrophes untouched — those are common in ordinary prose and carry no
669
+ markup risk in body text, only ``&``/``<``/``>`` do.
670
+ """
671
+ text = (raw or "").strip()
672
+ if len(text) > SNIPPET_MAX_CHARS:
673
+ text = text[:SNIPPET_MAX_CHARS].rstrip() + "..."
674
+ escaped = html.escape(text, quote=False)
675
+ return textwrap.fill(
676
+ escaped,
677
+ width=SNIPPET_WRAP_WIDTH,
678
+ break_long_words=True,
679
+ break_on_hyphens=False,
680
+ )
681
+
682
+
683
+ def _unsubscribe_headers() -> dict[str, str]:
684
+ """Build the RFC 2369 ``List-Unsubscribe`` header.
685
+
686
+ Points at the user's notification-preferences page — TruePPM has no
687
+ per-notification unsubscribe token today, so this is "one click to the
688
+ login-gated preferences page" rather than a true no-auth unsubscribe. That is a
689
+ valid ``List-Unsubscribe`` on its own; RFC 2369 does not require the target to be
690
+ unauthenticated.
691
+
692
+ ``List-Unsubscribe-Post: List-Unsubscribe=One-Click`` is deliberately **not**
693
+ emitted (#2887 item 6). That header is an RFC 8058 promise that an unauthenticated
694
+ ``POST`` to the URL unsubscribes the recipient with no further interaction, and
695
+ there is no such handler anywhere in this app — the URL is a login-gated SPA route.
696
+ Gmail's and Yahoo's bulk-sender requirements are checked by *exercising* the POST,
697
+ so advertising it without a conforming endpoint is a deliverability liability,
698
+ inverting the reason it was added. Re-add it in the same change that ships a signed
699
+ unsubscribe token and its endpoint, never before.
700
+
701
+ Returns an empty dict — omitting the header entirely — when the deployer hasn't
702
+ configured ``FRONTEND_BASE_URL``, since a bare relative path is not a valid header
703
+ value.
704
+ """
705
+ from django.conf import settings
706
+
707
+ base = getattr(settings, "FRONTEND_BASE_URL", "").rstrip("/")
708
+ if not base:
709
+ return {}
710
+ prefs_url = f"{base}/me/settings/notifications"
711
+ return {"List-Unsubscribe": f"<{prefs_url}>"}