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,1007 @@
1
+ """Serializers for the access app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from django.contrib.auth import get_user_model
9
+ from django.db.models import Q, QuerySet
10
+ from drf_spectacular.utils import extend_schema_field, inline_serializer
11
+ from rest_framework import serializers
12
+
13
+ from trueppm_api.apps.access.groups import ALL_AUTO_GROUP_KEYS
14
+ from trueppm_api.apps.access.models import (
15
+ ExternalStakeholder,
16
+ ProgramMembership,
17
+ ProgramUserDefinedMentionGroup,
18
+ ProjectMembership,
19
+ Role,
20
+ UserDefinedMentionGroup,
21
+ program_role_label,
22
+ )
23
+ from trueppm_api.apps.access.permissions import is_workspace_operator as _is_workspace_operator
24
+ from trueppm_api.apps.profiles.models import DateFormat, RoleContext
25
+ from trueppm_api.apps.workspace.models import WorkspaceRole
26
+ from trueppm_api.apps.workspace.permissions import (
27
+ _workspace_membership_role,
28
+ workspace_role_for_user,
29
+ )
30
+ from trueppm_api.apps.workspace.serializers import display_name_for
31
+
32
+ User = get_user_model()
33
+
34
+ # A mention group key must be a valid mention token (the name class of
35
+ # notifications.services._MENTION_RE) so it is actually addressable as @name.
36
+ _GROUP_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
37
+
38
+ # Refusal copy for an unreachable membership target. Deliberately identical for
39
+ # "no such account" and "an account you may not name" so the field leaks no
40
+ # existence oracle — the same reasoning as ``ProjectSerializer.copy_settings_from``.
41
+ _UNREACHABLE_TARGET_ERROR = (
42
+ "No such user, or not someone you can add. You can add people you already share a "
43
+ "project or program with; ask a workspace admin to invite anyone else."
44
+ )
45
+
46
+ _USER_IMMUTABLE_ERROR = (
47
+ "A membership's user cannot be changed. Remove this member and add the other account."
48
+ )
49
+
50
+ _TARGET_HELP_TEXT = (
51
+ "The stock auth.User integer primary key of the account to add. Bounded to accounts "
52
+ "you can already reach: a workspace Admin may name any active account; anyone else "
53
+ "may name themselves or somebody already on a project or program roster they belong "
54
+ "to. Deactivated accounts are never accepted. An id outside that set is refused with "
55
+ "400 and the same message a nonexistent id gets, so this field is not an existence "
56
+ "oracle — use a workspace invite to bring in anyone further out. Accepted on add "
57
+ "only; a membership's user cannot be changed."
58
+ )
59
+
60
+
61
+ def reachable_membership_targets(actor: Any, *, request: Any = None) -> QuerySet[Any]:
62
+ """Accounts ``actor`` may name as the target of a membership write (#3641).
63
+
64
+ Membership writes take a bare ``auth.User`` integer primary key and answer with
65
+ the target's ``user_detail`` — including their email address. Left unbounded that
66
+ turns a self-minted project into an address-lookup oracle over sequential ids
67
+ (``POST /api/v1/projects/`` is ungated and mints the caller as Owner), which is
68
+ the harvest #815 and #891 were fixed to prevent. So the target set is bounded to
69
+ accounts the caller can *already* read, and a membership grant can therefore never
70
+ disclose an account they could not already see.
71
+
72
+ Two tiers, both mirroring rules the install already enforces elsewhere:
73
+
74
+ * **Workspace ADMIN or above** reaches every active account. That is the branch
75
+ ``WorkspaceMemberListView`` already takes for the directory listing, and the
76
+ principal that already owns ``/workspace/invites/`` and
77
+ ``/workspace/groups/{id}/members/``. It is not self-grantable: an implicit role
78
+ is MEMBER (superuser bootstrap aside) and an explicit row is written only by an
79
+ existing admin (``workspace_role_for_user``).
80
+ * **Everyone else** reaches themselves plus the people already introduced to a
81
+ project or program roster they belong to — the same "must already be a member"
82
+ constraint ``validate_lead`` and the mention-group ``_member_user_or_400``
83
+ helper apply, with the caller exempt exactly as in
84
+ ``seed.importer._resolve_accounts`` (#1057).
85
+
86
+ The caller's *own* membership must be live, but the target's need not be: a revoked
87
+ row is still evidence that somebody with reach introduced that account to this
88
+ roster, and requiring a live one would make re-adding a member you just removed
89
+ impossible — the revive path ``_revive_revoked_membership`` exists for (#3410).
90
+
91
+ So "reachable" means **ever introduced to a roster I am on**, not *currently
92
+ readable there*. The deliberate consequence: somebody revoked from a project before
93
+ the caller joined it is nameable even though the caller never saw them on it. That
94
+ is bounded to accounts already adjacent to the caller's own projects, never the
95
+ install, and it is the price of keeping re-add working. Do not silently narrow this
96
+ to the live set without restoring the re-add path some other way.
97
+
98
+ ``is_active=False`` accounts are excluded at every tier, matching ``UserSearchView``:
99
+ the deactivated pool is admin-only state (#1724) and must not be reachable through
100
+ a roster write either.
101
+
102
+ Bringing in somebody outside that set is the invite path
103
+ (``POST /api/v1/workspace/invites/``). It is keyed on an email address the inviter
104
+ must already possess, so it discloses nothing — it is the reason this narrowing does
105
+ not strand onboarding.
106
+
107
+ Args:
108
+ actor: The requesting user. An anonymous or deactivated principal reaches nobody.
109
+ request: The DRF request, when there is one. Resolving the workspace role
110
+ through it populates the per-request cache the permission layer reads, so a
111
+ workspace-scoped permission class added to these viewsets later does not
112
+ silently resolve the same role a second time.
113
+
114
+ Returns:
115
+ A ``User`` queryset of the accounts ``actor`` may name.
116
+ """
117
+ role = (
118
+ _workspace_membership_role(request)
119
+ if request is not None
120
+ else workspace_role_for_user(actor)
121
+ )
122
+ if role is None:
123
+ return User.objects.none()
124
+
125
+ active = User.objects.filter(is_active=True)
126
+ if role >= WorkspaceRole.ADMIN:
127
+ return active
128
+
129
+ # Subqueries rather than a join + ``.distinct()``: a caller on many projects would
130
+ # otherwise fan the join out per shared roster row, and a bare ``.distinct()`` is
131
+ # silently defeated by any model ordering the queryset picks up.
132
+ # The actor's own row must be live and its scope must still exist — a trashed
133
+ # project's roster is unreadable (``_get_project_or_404`` filters it out), so it
134
+ # must not confer reach either. Archived is deliberately NOT excluded: an archived
135
+ # project is read-only, not invisible, and its members still see each other.
136
+ actor_project_ids = ProjectMembership.objects.filter(
137
+ user=actor, is_deleted=False, project__is_deleted=False
138
+ ).values("project_id")
139
+ actor_program_ids = ProgramMembership.objects.filter(
140
+ user=actor, is_deleted=False, program__is_deleted=False
141
+ ).values("program_id")
142
+ return active.filter(
143
+ Q(pk=actor.pk)
144
+ | Q(
145
+ pk__in=ProjectMembership.objects.filter(project_id__in=actor_project_ids).values(
146
+ "user_id"
147
+ )
148
+ )
149
+ | Q(
150
+ pk__in=ProgramMembership.objects.filter(program_id__in=actor_program_ids).values(
151
+ "user_id"
152
+ )
153
+ )
154
+ )
155
+
156
+
157
+ class _ReachableMembershipTargetMixin:
158
+ """Bounds a membership write serializer's ``user`` field to reachable accounts.
159
+
160
+ ``user`` is declared with an **empty** queryset on the concrete serializers, so a
161
+ serializer built without a request context resolves nobody rather than everybody —
162
+ the field is its own IDOR gate, failing closed. Here the real, caller-scoped
163
+ queryset is swapped in (the structural precedent is
164
+ ``ProjectSerializer.__init__``'s ``copy_settings_from`` narrowing).
165
+
166
+ On **update** the field is made read-only and a supplied ``user`` is refused
167
+ outright: reassigning an existing row onto another account is the same harvest
168
+ through a second door, and it also silently rewrites who holds access while
169
+ keeping the row's ``joined_at`` access evidence (#3410). Refusing rather than
170
+ ignoring keeps a 200 from meaning two different things to the caller.
171
+ """
172
+
173
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
174
+ super().__init__(*args, **kwargs)
175
+ field = self.fields.get("user") # type: ignore[attr-defined]
176
+ if not isinstance(field, serializers.PrimaryKeyRelatedField):
177
+ return
178
+ if self.instance is not None: # type: ignore[attr-defined]
179
+ field.read_only = True
180
+ return
181
+ request = self.context.get("request") # type: ignore[attr-defined]
182
+ actor = getattr(request, "user", None)
183
+ if actor is not None and getattr(actor, "is_authenticated", False):
184
+ field.queryset = reachable_membership_targets(actor, request=request)
185
+
186
+ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
187
+ # ``user`` is read-only on update, so DRF drops it silently. Surface the
188
+ # refusal instead — a caller that asked to reassign the row must not read a
189
+ # 200 as "done". Checked against ``initial_data`` because the field never
190
+ # reaches ``attrs``; the body may not be a dict, so do not assume ``in`` works.
191
+ if self.instance is not None: # type: ignore[attr-defined]
192
+ raw = getattr(self, "initial_data", None)
193
+ if isinstance(raw, dict) and "user" in raw:
194
+ raise serializers.ValidationError({"user": _USER_IMMUTABLE_ERROR})
195
+ return super().validate(attrs) # type: ignore[misc,no-any-return]
196
+
197
+
198
+ class _UserSummarySerializer(serializers.ModelSerializer): # type: ignore[type-arg]
199
+ class Meta:
200
+ model = User
201
+ fields = ["id", "username", "email"]
202
+
203
+
204
+ class _MentionGroupMemberSerializer(serializers.ModelSerializer): # type: ignore[type-arg]
205
+ """Member payload for a user-defined @mention group — NO email (#4578, security).
206
+
207
+ Both the project-scoped (``UserDefinedMentionGroupReadSerializer``) and
208
+ program-scoped (``ProgramUserDefinedMentionGroupReadSerializer``) mention
209
+ group read serializers draw ``members`` from across a wider surface than the
210
+ reader's own membership guarantees: a program mention-group's members are
211
+ drawn from any project in the program (ADR-0248 §2 union), so any program
212
+ Viewer could read a project member's email even on a project they hold no
213
+ membership on — a leak across the program/project boundary ADR-0070 exists
214
+ to prevent. Username is sufficient to resolve and render an @mention; email
215
+ is dropped entirely rather than gated, since a per-reader-shared-project
216
+ check would have to run per member on every group read.
217
+ """
218
+
219
+ class Meta:
220
+ model = User
221
+ fields = ["id", "username"]
222
+
223
+
224
+ class ProjectMembershipReadSerializer(serializers.ModelSerializer[ProjectMembership]):
225
+ """Response serializer — includes user_detail and role_label for list/retrieve.
226
+
227
+ role — integer ordinal (canonical wire format; use for comparisons)
228
+ role_label — human-readable label e.g. "Project Manager" (display only)
229
+ joined_at — when this membership row was first created. Per-project access
230
+ evidence, but NOT proof of uninterrupted access: re-adding a
231
+ previously removed member revives their original row and keeps
232
+ this date, so the span may contain one or more revoked intervals
233
+ (#3410). Use reinstated_at as the discriminator — it is null iff
234
+ this membership has never lapsed (#3436).
235
+ role_changed_at — when the role last changed, or null if unchanged since joining
236
+ reinstated_at — when this membership was last revived after a revoke, or null if
237
+ it has never been revoked and re-added. Stamped on every revive
238
+ (advances on a second revive rather than accumulating history) —
239
+ the durable server-side fact that lets a caller, human or machine,
240
+ tell a never-lapsed membership apart from a revived one (#3436).
241
+ other_active_project_count — how many OTHER active (non-archived, non-deleted) projects this
242
+ user belongs to, excluding the current one. A resource-load
243
+ signal for the assigner (#598). The full count is shown; it is a
244
+ number only and leaks no project identities.
245
+ other_active_project_names — names of those other projects, but ONLY the ones the *requesting*
246
+ user is OWNER of (visibility gate — never reveal the name of a
247
+ project the requester cannot already see). Empty for non-OWNERs.
248
+ """
249
+
250
+ user_detail = _UserSummarySerializer(source="user", read_only=True)
251
+ role_label = serializers.SerializerMethodField()
252
+ other_active_project_count = serializers.SerializerMethodField()
253
+ other_active_project_names = serializers.SerializerMethodField()
254
+
255
+ def get_role_label(self, obj: ProjectMembership) -> str:
256
+ """Display name for ``role`` in **project** vocabulary.
257
+
258
+ Ordinal 400 reads "Project Admin" and 300 reads "Project Manager". The
259
+ identically-named field on a *program* membership deliberately names the
260
+ same two ordinals "Program Admin" and "Program Manager" — see
261
+ ``ProgramMembershipReadSerializer.get_role_label``. One ordinal, two
262
+ containers, two names.
263
+ """
264
+ return Role(obj.role).label
265
+
266
+ def get_other_active_project_count(self, obj: ProjectMembership) -> int:
267
+ # list/retrieve annotate this on the queryset (one Subquery, no N+1). create/
268
+ # partial_update serialize a fresh, un-annotated instance — fall back to a
269
+ # single count query there (rare, one row).
270
+ annotated = getattr(obj, "other_active_count", None)
271
+ if annotated is not None:
272
+ return int(annotated)
273
+ return (
274
+ ProjectMembership.objects.filter(
275
+ user_id=obj.user_id,
276
+ is_deleted=False,
277
+ project__is_deleted=False,
278
+ project__is_archived=False,
279
+ )
280
+ .exclude(project_id=obj.project_id)
281
+ .values("project_id")
282
+ .distinct()
283
+ .count()
284
+ )
285
+
286
+ def get_other_active_project_names(self, obj: ProjectMembership) -> list[str]:
287
+ # Names are visibility-gated and prebuilt once per request by the viewset
288
+ # (see ProjectMembershipViewSet._build_other_project_names_map). When the map
289
+ # is absent (create/partial_update responses) return [] — the client re-fetches
290
+ # the list, which carries the gated names.
291
+ names_map: dict[Any, list[str]] = self.context.get("other_project_names_map") or {}
292
+ return names_map.get(obj.user_id, [])
293
+
294
+ class Meta:
295
+ model = ProjectMembership
296
+ fields = [
297
+ "id",
298
+ "server_version",
299
+ "project",
300
+ "user",
301
+ "user_detail",
302
+ "role",
303
+ "role_label",
304
+ "joined_at",
305
+ "role_changed_at",
306
+ "reinstated_at",
307
+ "other_active_project_count",
308
+ "other_active_project_names",
309
+ ]
310
+ read_only_fields = [
311
+ "id",
312
+ "server_version",
313
+ "project",
314
+ "user",
315
+ "user_detail",
316
+ "role_label",
317
+ "joined_at",
318
+ "role_changed_at",
319
+ "reinstated_at",
320
+ "other_active_project_count",
321
+ "other_active_project_names",
322
+ ]
323
+
324
+
325
+ class ProjectMembershipWriteSerializer(
326
+ _ReachableMembershipTargetMixin, serializers.ModelSerializer[ProjectMembership]
327
+ ):
328
+ """Write serializer — accepts user and role; project is injected from URL.
329
+
330
+ ``user`` is the stock ``auth.User`` **integer** primary key (TruePPM has no custom
331
+ user model), and it is bounded to :func:`reachable_membership_targets` — see that
332
+ function for who a caller may name and why. It is accepted on add only; a
333
+ ``partial_update`` that carries it is refused (#3641).
334
+
335
+ ``role`` is optional (ADR-0363, #157): when omitted on add, the viewset falls
336
+ back to the project's ``default_member_role``. It remains required for a role
337
+ *change* (``partial_update`` always sends it), and the model column stays
338
+ non-null — the fallback is resolved before ``save``.
339
+ """
340
+
341
+ # Empty by construction — the mixin swaps in the caller-scoped queryset. Without a
342
+ # request context the field resolves nobody, so it fails closed.
343
+ user = serializers.PrimaryKeyRelatedField(
344
+ queryset=User.objects.none(),
345
+ error_messages={"does_not_exist": _UNREACHABLE_TARGET_ERROR},
346
+ help_text=_TARGET_HELP_TEXT,
347
+ )
348
+ role = serializers.IntegerField(
349
+ required=False,
350
+ help_text=(
351
+ "Optional on add — defaults to the project's default_member_role "
352
+ "(ADR-0363). Required to change an existing member's role."
353
+ ),
354
+ )
355
+
356
+ class Meta:
357
+ model = ProjectMembership
358
+ fields = ["user", "role"]
359
+
360
+ def validate_role(self, value: int) -> int:
361
+ # Role must be a valid Role ordinal.
362
+ valid = {r.value for r in Role}
363
+ if value not in valid:
364
+ raise serializers.ValidationError(f"Invalid role. Choose from {sorted(valid)}.")
365
+ return value
366
+
367
+
368
+ class ProjectMembershipUpdateSerializer(ProjectMembershipWriteSerializer):
369
+ """PATCH body — ``role`` only.
370
+
371
+ A separate class rather than ``read_only_fields`` on the add serializer so the
372
+ published schema tells the truth: ``user`` is genuinely not part of an update
373
+ body, and an integrator reading ``PatchedProjectMembershipWriteRequest`` would
374
+ otherwise still see it advertised as writable. Sending it anyway is refused by
375
+ the inherited ``validate`` rather than silently dropped (#3641).
376
+ """
377
+
378
+ class Meta(ProjectMembershipWriteSerializer.Meta):
379
+ fields = ["role"]
380
+
381
+
382
+ class ProgramMembershipReadSerializer(serializers.ModelSerializer[ProgramMembership]):
383
+ """Response serializer for ProgramMembership — mirrors the project version.
384
+
385
+ ``joined_at`` carries the same caveat as its project twin: it is the date the row
386
+ was first created, and a revoked member who is later re-added keeps it, so the
387
+ span may contain revoked intervals (#3410). It is not proof of uninterrupted
388
+ access — ``reinstated_at`` is the discriminator (null iff never revoked and
389
+ re-added, #3436).
390
+ """
391
+
392
+ user_detail = _UserSummarySerializer(source="user", read_only=True)
393
+ # ``role_label`` is the *access* role's display name (Owner/Admin/…), a
394
+ # computed mirror of the ``role`` ordinal — NOT the freeform functional title.
395
+ # The freeform PO/PM/Tech-Lead label is the distinct ``role_title`` field
396
+ # below (#565); the two never collide.
397
+ role_label = serializers.SerializerMethodField()
398
+
399
+ def get_role_label(self, obj: ProgramMembership) -> str:
400
+ """Display name for ``role`` in **program** vocabulary.
401
+
402
+ Ordinal 400 reads "Program Admin" and 300 reads "Program Manager" — the
403
+ same names ``Program.my_role_label`` uses, from the same map. An ordinal
404
+ outside the five OSS roles has no name and reads ``"Role <ordinal>"``.
405
+ """
406
+ # Why this is not ``Role.label``: the enum's labels are project-scoped, so
407
+ # serializing through them called a *program* membership "Project Admin"
408
+ # here while the program card called the same membership "Program Admin"
409
+ # — one fact, two answers, and only TruePPM's own web client knew which to
410
+ # believe (#3503). ``program_role_label`` is the one definition of that
411
+ # vocabulary and the program card calls it too.
412
+ #
413
+ # The fallback covers an Enterprise custom-band ordinal (ADR-0072), which
414
+ # the OSS edition cannot name. The field is a non-null string in the
415
+ # published schema and the ordinal is already on the wire beside it in
416
+ # ``role``, so echoing it degrades rather than borrowing a neighbouring
417
+ # role's name — and rather than the ``ValueError`` this used to raise,
418
+ # which DRF does not convert and which 500'd the whole response.
419
+ return program_role_label(obj.role) or f"Role {obj.role}"
420
+
421
+ class Meta:
422
+ model = ProgramMembership
423
+ fields = [
424
+ "id",
425
+ "server_version",
426
+ "program",
427
+ "user",
428
+ "user_detail",
429
+ "role",
430
+ "role_label",
431
+ "role_title",
432
+ "joined_at",
433
+ "role_changed_at",
434
+ "reinstated_at",
435
+ ]
436
+ read_only_fields = [
437
+ "id",
438
+ "server_version",
439
+ "program",
440
+ "user",
441
+ "user_detail",
442
+ "role_label",
443
+ "joined_at",
444
+ "role_changed_at",
445
+ "reinstated_at",
446
+ ]
447
+
448
+
449
+ class ProgramMembershipWriteSerializer(
450
+ _ReachableMembershipTargetMixin, serializers.ModelSerializer[ProgramMembership]
451
+ ):
452
+ """Write serializer — accepts user, role, and the freeform role_title.
453
+
454
+ ``user`` is the stock ``auth.User`` **integer** primary key, bounded to
455
+ :func:`reachable_membership_targets` on add and refused entirely on
456
+ ``partial_update`` (#3641) — the project twin carries the same constraint for the
457
+ same reason.
458
+
459
+ ``program`` is injected from the URL. ``role_title`` (#565) is optional; the
460
+ view gates *who* may set it (a role change stays Owner-only, while a
461
+ role_title-only PATCH is allowed at Admin+).
462
+ """
463
+
464
+ # Empty by construction — see the project twin.
465
+ user = serializers.PrimaryKeyRelatedField(
466
+ queryset=User.objects.none(),
467
+ error_messages={"does_not_exist": _UNREACHABLE_TARGET_ERROR},
468
+ help_text=_TARGET_HELP_TEXT,
469
+ )
470
+
471
+ class Meta:
472
+ model = ProgramMembership
473
+ fields = ["user", "role", "role_title"]
474
+
475
+ def validate_role(self, value: int) -> int:
476
+ valid = {r.value for r in Role}
477
+ if value not in valid:
478
+ raise serializers.ValidationError(f"Invalid role. Choose from {sorted(valid)}.")
479
+ return value
480
+
481
+ def validate_role_title(self, value: str | None) -> str:
482
+ # Collapse whitespace-only / empty submissions to "" so "unset" is a single
483
+ # canonical state (empty string, never NULL — per the model's DJ001 default).
484
+ return (value or "").strip()
485
+
486
+
487
+ class ProgramMembershipUpdateSerializer(ProgramMembershipWriteSerializer):
488
+ """PATCH body — ``role`` and ``role_title`` only; see the project twin for why."""
489
+
490
+ class Meta(ProgramMembershipWriteSerializer.Meta):
491
+ fields = ["role", "role_title"]
492
+
493
+
494
+ class UserDefinedMentionGroupReadSerializer(serializers.ModelSerializer[UserDefinedMentionGroup]):
495
+ """Response serializer for a user-defined @mention group (ADR-0212, #515).
496
+
497
+ ``members`` is the curated member set (display-name-only — see
498
+ ``_MentionGroupMemberSerializer``, #4578); ``member_count`` is a convenience
499
+ for the list UI. ``muted_by_me`` reflects whether the *requesting* user has
500
+ muted this group — the per-user override that suppresses its mentions for
501
+ them.
502
+ """
503
+
504
+ members = _MentionGroupMemberSerializer(many=True, read_only=True)
505
+ member_count = serializers.SerializerMethodField()
506
+ muted_by_me = serializers.SerializerMethodField()
507
+
508
+ def get_member_count(self, obj: UserDefinedMentionGroup) -> int:
509
+ # `members` is already prefetched by the viewset (it also backs the
510
+ # ``members`` field), so counting the loaded rows in Python is free. A
511
+ # DB-side ``.count()`` here would be a wasted round trip, not a saving.
512
+ members = obj.members.all()
513
+ return len(members)
514
+
515
+ def get_muted_by_me(self, obj: UserDefinedMentionGroup) -> bool:
516
+ user = getattr(self.context.get("request"), "user", None)
517
+ if user is None or not getattr(user, "is_authenticated", False):
518
+ return False
519
+ # muted_by is prefetched by the viewset.
520
+ return any(m.pk == user.pk for m in obj.muted_by.all())
521
+
522
+ class Meta:
523
+ model = UserDefinedMentionGroup
524
+ fields = [
525
+ "id",
526
+ "server_version",
527
+ "project",
528
+ "name",
529
+ "description",
530
+ "email_default_on",
531
+ "members",
532
+ "member_count",
533
+ "muted_by_me",
534
+ ]
535
+ read_only_fields = fields
536
+
537
+
538
+ def _validated_mention_group_name(value: str) -> str:
539
+ """Normalize and validate a @mention group name, shared by the project- and
540
+ program-scoped write serializers (#3903) — everything except the per-scope
541
+ uniqueness check, which needs each serializer's own queryset/context.
542
+ """
543
+ # Accept a leading @ from the client for convenience; store without it.
544
+ name = value.strip().lstrip("@").strip()
545
+ if not name:
546
+ raise serializers.ValidationError("Group name cannot be empty.")
547
+ if len(name) > 32:
548
+ raise serializers.ValidationError("Group name must be 32 characters or fewer.")
549
+ if not _GROUP_NAME_RE.match(name):
550
+ raise serializers.ValidationError(
551
+ "Group name may only contain letters, digits, and the characters . _ -"
552
+ )
553
+ # An auto-group name (@admins, @scrum-team, @program-pms, …) must never
554
+ # be shadowed — project- and program-scoped keys alike.
555
+ if name.lower() in ALL_AUTO_GROUP_KEYS:
556
+ raise serializers.ValidationError(
557
+ f"'@{name}' is a reserved automatic group and cannot be used."
558
+ )
559
+ return name
560
+
561
+
562
+ class UserDefinedMentionGroupWriteSerializer(serializers.ModelSerializer[UserDefinedMentionGroup]):
563
+ """Write serializer for create/rename/edit — ``project`` is injected from URL.
564
+
565
+ Membership and mute are managed through dedicated viewset actions (different
566
+ RBAC), so this serializer covers only the group's own attributes.
567
+ """
568
+
569
+ class Meta:
570
+ model = UserDefinedMentionGroup
571
+ fields = ["name", "description", "email_default_on"]
572
+
573
+ def validate_name(self, value: str) -> str:
574
+ name = _validated_mention_group_name(value)
575
+ # Case-insensitive project-uniqueness (the DB constraint is the backstop;
576
+ # this returns a friendly field error instead of a 500 on the race loser).
577
+ project_id = str(self.context.get("project_id"))
578
+ qs = UserDefinedMentionGroup.objects.filter(
579
+ project_id=project_id, name__iexact=name, is_deleted=False
580
+ )
581
+ if self.instance is not None:
582
+ qs = qs.exclude(pk=self.instance.pk)
583
+ if qs.exists():
584
+ raise serializers.ValidationError(
585
+ f"A group named '@{name}' already exists in this project."
586
+ )
587
+ return name
588
+
589
+ def validate_description(self, value: str | None) -> str:
590
+ return (value or "").strip()
591
+
592
+
593
+ class ProgramUserDefinedMentionGroupReadSerializer(
594
+ serializers.ModelSerializer[ProgramUserDefinedMentionGroup]
595
+ ):
596
+ """Response serializer for a program-scoped @mention group (ADR-0248, #516).
597
+
598
+ The program-scoped mirror of :class:`UserDefinedMentionGroupReadSerializer`.
599
+ ``members`` is the curated member set, drawn from across the program's
600
+ projects and display-name-only (no email — see
601
+ ``_MentionGroupMemberSerializer``, #4578): a program Viewer reading this
602
+ list is not necessarily a member of every project a listed member actually
603
+ belongs to, so email here would cross the ADR-0070 program/project
604
+ visibility boundary. ``member_count`` is a list-UI convenience;
605
+ ``muted_by_me`` reflects whether the *requesting* user has muted this group.
606
+ """
607
+
608
+ members = _MentionGroupMemberSerializer(many=True, read_only=True)
609
+ member_count = serializers.SerializerMethodField()
610
+ muted_by_me = serializers.SerializerMethodField()
611
+
612
+ def get_member_count(self, obj: ProgramUserDefinedMentionGroup) -> int:
613
+ # members is prefetched by the viewset (it also backs the members field),
614
+ # so counting the loaded rows in Python is free (no extra round trip).
615
+ members = obj.members.all()
616
+ return len(members)
617
+
618
+ def get_muted_by_me(self, obj: ProgramUserDefinedMentionGroup) -> bool:
619
+ user = getattr(self.context.get("request"), "user", None)
620
+ if user is None or not getattr(user, "is_authenticated", False):
621
+ return False
622
+ # muted_by is prefetched by the viewset.
623
+ return any(m.pk == user.pk for m in obj.muted_by.all())
624
+
625
+ class Meta:
626
+ model = ProgramUserDefinedMentionGroup
627
+ fields = [
628
+ "id",
629
+ "server_version",
630
+ "program",
631
+ "name",
632
+ "description",
633
+ "email_default_on",
634
+ "members",
635
+ "member_count",
636
+ "muted_by_me",
637
+ ]
638
+ read_only_fields = fields
639
+
640
+
641
+ class ProgramUserDefinedMentionGroupWriteSerializer(
642
+ serializers.ModelSerializer[ProgramUserDefinedMentionGroup]
643
+ ):
644
+ """Write serializer for program-group create/rename/edit — ``program`` from URL.
645
+
646
+ Membership and mute are managed through dedicated viewset actions (different
647
+ RBAC), so this serializer covers only the group's own attributes.
648
+ """
649
+
650
+ class Meta:
651
+ model = ProgramUserDefinedMentionGroup
652
+ fields = ["name", "description", "email_default_on"]
653
+
654
+ def validate_name(self, value: str) -> str:
655
+ name = _validated_mention_group_name(value)
656
+ # Case-insensitive program-uniqueness (the DB constraint is the backstop;
657
+ # this returns a friendly field error instead of a 500 on the race loser).
658
+ program_id = str(self.context.get("program_id"))
659
+ qs = ProgramUserDefinedMentionGroup.objects.filter(
660
+ program_id=program_id, name__iexact=name, is_deleted=False
661
+ )
662
+ if self.instance is not None:
663
+ qs = qs.exclude(pk=self.instance.pk)
664
+ if qs.exists():
665
+ raise serializers.ValidationError(
666
+ f"A group named '@{name}' already exists in this program."
667
+ )
668
+ return name
669
+
670
+ def validate_description(self, value: str | None) -> str:
671
+ return (value or "").strip()
672
+
673
+
674
+ class ExternalStakeholderSerializer(serializers.ModelSerializer[ExternalStakeholder]):
675
+ """CRUD serializer for a program's external stakeholder registry (#1658, ADR-0264).
676
+
677
+ ``created_by`` is the adder's integer user PK (the wire meaning it has on every
678
+ other schema) and ``created_by_name`` their display name, never the raw user row;
679
+ ``program`` comes from the URL and is never accepted from the body (IDOR-safe —
680
+ the viewset scopes and stamps it). Case-insensitive per-program email uniqueness
681
+ is validated here so the client gets a friendly field error instead of the DB
682
+ constraint's 500 on the race loser.
683
+ """
684
+
685
+ created_by_name = serializers.SerializerMethodField()
686
+
687
+ class Meta:
688
+ model = ExternalStakeholder
689
+ fields = [
690
+ "id",
691
+ "name",
692
+ "email",
693
+ "note",
694
+ "created_by",
695
+ "created_by_name",
696
+ "created_at",
697
+ "updated_at",
698
+ ]
699
+ read_only_fields = ["id", "created_by", "created_by_name", "created_at", "updated_at"]
700
+
701
+ @extend_schema_field(serializers.CharField(allow_null=True))
702
+ def get_created_by_name(self, obj: ExternalStakeholder) -> str | None:
703
+ user = obj.created_by
704
+ if user is None:
705
+ return None
706
+ return display_name_for(user.first_name, user.last_name, user.username)
707
+
708
+ def validate_name(self, value: str) -> str:
709
+ name = value.strip()
710
+ if not name:
711
+ raise serializers.ValidationError("Name cannot be empty.")
712
+ return name
713
+
714
+ def validate_note(self, value: str | None) -> str:
715
+ return (value or "").strip()
716
+
717
+ def validate_email(self, value: str) -> str:
718
+ email = value.strip()
719
+ # Case-insensitive per-program uniqueness across LIVE rows (the DB
720
+ # constraint is the backstop; this returns a friendly 400 on the race loser
721
+ # and on the common re-add-of-existing case).
722
+ program_id = str(self.context.get("program_id"))
723
+ qs = ExternalStakeholder.objects.filter(
724
+ program_id=program_id, email__iexact=email, is_deleted=False
725
+ )
726
+ if self.instance is not None:
727
+ qs = qs.exclude(pk=self.instance.pk)
728
+ if qs.exists():
729
+ raise serializers.ValidationError(
730
+ "A stakeholder with this email already exists in this program."
731
+ )
732
+ return email
733
+
734
+
735
+ def _user_display_name(obj: Any) -> str:
736
+ """First + last name, falling back to ``username`` when both name fields
737
+ are blank — shared by every serializer that surfaces a user's display
738
+ name (#3903)."""
739
+ name = f"{obj.first_name} {obj.last_name}".strip()
740
+ return name if name else obj.username
741
+
742
+
743
+ def _user_initials(obj: Any) -> str:
744
+ """Up to two initials from first/last name, falling back to the first two
745
+ characters of ``username`` when both name fields are blank (#3903)."""
746
+ parts: list[str] = []
747
+ if obj.first_name:
748
+ parts.append(obj.first_name[0].upper())
749
+ if obj.last_name:
750
+ parts.append(obj.last_name[0].upper())
751
+ if parts:
752
+ return "".join(parts[:2])
753
+ return str(obj.username[:2].upper())
754
+
755
+
756
+ class UserSearchResultSerializer(serializers.Serializer[Any]):
757
+ """Read-only serializer for GET /api/v1/users/search/ results (ADR-0061).
758
+
759
+ Deliberately omits ``email`` (#815): returning it let any authenticated caller
760
+ paginate the typeahead to harvest every user's email. Identity for the invite
761
+ typeahead is carried by username + display_name + initials; the endpoint still
762
+ *matches* on email so invite-by-email works, but never echoes the value back.
763
+ """
764
+
765
+ id = serializers.CharField()
766
+ username = serializers.CharField()
767
+ display_name = serializers.SerializerMethodField()
768
+ initials = serializers.SerializerMethodField()
769
+
770
+ def get_display_name(self, obj: Any) -> str:
771
+ return _user_display_name(obj)
772
+
773
+ def get_initials(self, obj: Any) -> str:
774
+ return _user_initials(obj)
775
+
776
+
777
+ class TokenPostureSerializer(serializers.Serializer[Any]):
778
+ """Nested shape of ``MeSerializer.token`` — schema-only, never instantiated
779
+ directly (``MeSerializer.get_token`` returns a plain dict; this class only gives
780
+ ``extend_schema_field`` something faithful to point at, per #2938).
781
+ """
782
+
783
+ scopes = serializers.ListField(
784
+ child=serializers.CharField(), help_text="Scopes this token carries, e.g. ['mcp:read']."
785
+ )
786
+ is_agent = serializers.BooleanField(
787
+ help_text="Whether trueppm_api.apps.projects.models.is_agent_token() treats this "
788
+ "token as MCP/agent traffic — false for a legacy:full personal access token, "
789
+ "which the operator kill switch and team opt-out do not govern."
790
+ )
791
+
792
+
793
+ class MeSerializer(serializers.Serializer[Any]):
794
+ """Read-only serializer for GET /api/v1/auth/me/."""
795
+
796
+ # The user PK is an integer AutoField (django.contrib.auth.models.User), but
797
+ # this endpoint has always emitted it as a decimal STRING ("5"): the field was
798
+ # declared ``UUIDField``, whose ``to_representation`` is a bare ``str(value)``,
799
+ # so the wire value was a string while the schema advertised ``format: uuid``
800
+ # — a claim no value ever satisfied (#2633). The string encoding is kept on
801
+ # purpose: every client keys on it (the web ``CurrentUser.id``, trueppm-mcp's
802
+ # whoami, per-user localStorage keys), and the comment/mention author payloads
803
+ # use the same string form, so flipping it to a number would silently break
804
+ # each ``===`` against those. Declared as what it is, so the schema stops
805
+ # lying; integer FKs elsewhere are compared to it via the web's ``isSameUser``.
806
+ id = serializers.CharField(read_only=True)
807
+ username = serializers.CharField()
808
+ display_name = serializers.SerializerMethodField()
809
+ initials = serializers.SerializerMethodField()
810
+ email = serializers.EmailField()
811
+ # Contributor-tier role signal (#855/#856). The web client gates the admin
812
+ # settings nav and the "Signal-only" notification default on this, instead of
813
+ # re-deriving "am I an admin anywhere" by fanning out per-project membership
814
+ # calls. API-first: the tier verdict is a server fact, MCP-reachable.
815
+ # - max_project_role: highest project Role ordinal across the user's
816
+ # memberships (null if they belong to no projects).
817
+ # - workspace_role: the user's *effective* WorkspaceRole ordinal — an
818
+ # explicit membership if present, else the implicit role every
819
+ # authenticated user holds (OWNER for a Django superuser bootstrapping a
820
+ # fresh install, else MEMBER); null only for a deactivated membership.
821
+ # Resolved by workspace.permissions.workspace_role_for_user so this signal
822
+ # can never drift from what workspace RBAC actually enforces.
823
+ # - can_access_admin_settings: true iff Admin+ in any project OR Admin+ at
824
+ # the workspace (the implicit superuser OWNER counts) — the single boolean
825
+ # the settings shell gates on.
826
+ # - is_workspace_operator: true iff this user is the install operator
827
+ # (Django superuser) — the SAME predicate access.permissions.is_workspace_
828
+ # operator() uses to gate IsWorkspaceOperator-protected endpoints (mail
829
+ # transport, dead-letter queue, the observability app's System Health /
830
+ # Prometheus / retention / telemetry views, #4009). Deliberately narrower
831
+ # than can_access_admin_settings, which also passes any project Admin —
832
+ # a population IsWorkspaceOperator refuses. Added so client code that
833
+ # fetches an operator-only endpoint can gate on the matching signal
834
+ # instead of can_access_admin_settings, whose broader population was
835
+ # producing a guaranteed 403 for every Project Admin polling
836
+ # /health/system/ from RateLimitDisabledBanner (#4219).
837
+ max_project_role = serializers.SerializerMethodField()
838
+ workspace_role = serializers.SerializerMethodField()
839
+ can_access_admin_settings = serializers.SerializerMethodField()
840
+ is_workspace_operator = serializers.SerializerMethodField()
841
+ # Role-based app front door (ADR-0129). The web router reads these and
842
+ # navigates — it holds no role→surface policy itself. API-first: the
843
+ # destination is a server fact, identical for web, mobile, and MCP clients.
844
+ # - default_landing: the user's stored preference ("auto" if unset).
845
+ # - landing: {intent, path, resolved_by} — the resolved front door.
846
+ default_landing = serializers.SerializerMethodField()
847
+ landing = serializers.SerializerMethodField()
848
+ # Per-user nav visibility (ADR-0139). The web shell reads this to hide the
849
+ # view tabs the user opted out of; it is a global per-user list, layered on
850
+ # top of the per-project methodology preset client-side. API-first: the
851
+ # hidden set is a server fact, identical for web, mobile, and MCP clients.
852
+ hidden_views = serializers.SerializerMethodField()
853
+ # Active role-context "lens" (#412, ADR-0162). A presentation-only preference
854
+ # the web shell reads to pick a dual-hat user's default project view and the
855
+ # view-tab emphasis ("pm" / "scrum_master" / "unified"; "unified" if unset).
856
+ # It NEVER gates access — RBAC remains the sole authority; this is read here
857
+ # only so the lens can be reflected without a flash of the wrong view.
858
+ role_context = serializers.SerializerMethodField()
859
+ # Account-wide Do-Not-Disturb (#1707, ADR-0292). Read-only projection so the
860
+ # web current-user query carries the DND state and the bell reflects it with no
861
+ # extra request; the authoritative read/write surface is
862
+ # /api/v1/me/notification-settings/. Non-creating on this hot GET path — the
863
+ # absence of a settings row reads as DND off.
864
+ dnd_enabled = serializers.SerializerMethodField()
865
+ # Personal display frame (#1953, ADR-0410). Read-only projections the web client
866
+ # reads to render timestamps/dates in the viewer's frame. API-first: these are
867
+ # server facts (identical for web, mobile, MCP) but purely presentational — the
868
+ # API itself always emits aware-UTC ISO-8601; an agent ignores them.
869
+ # - timezone: IANA zone for instant timestamps, or "auto" (browser zone).
870
+ # - date_format: style for all displayed dates ("auto"/"iso"/"us"/"eu").
871
+ timezone = serializers.SerializerMethodField()
872
+ date_format = serializers.SerializerMethodField()
873
+ # Caller's own credential posture (#2938). ``is_agent_token()`` decides on every
874
+ # request whether the caller is governed by the MCP control layer (kill switch,
875
+ # team opt-out, row filtering) but reports it nowhere — so a member who points
876
+ # trueppm-mcp at a personal legacy:full token has no way to learn the switches
877
+ # they think are protecting them do not apply. This is the fix: the one endpoint
878
+ # the MCP client already calls at boot (verify_auth) now echoes the caller's own
879
+ # scopes and agent posture. Disclosing it leaks nothing — it is the caller's own
880
+ # credential. Null for a session/JWT caller, which carries no token at all.
881
+ token = serializers.SerializerMethodField()
882
+
883
+ def get_max_project_role(self, obj: Any) -> int | None:
884
+ # Memoized: get_can_access_admin_settings also needs this, so without the
885
+ # cache /auth/me would run the same aggregate twice per response.
886
+ if not hasattr(self, "_max_project_role"):
887
+ from django.db.models import Max
888
+
889
+ value = ProjectMembership.objects.filter(
890
+ user=obj, is_deleted=False, project__is_deleted=False
891
+ ).aggregate(_max=Max("role"))["_max"]
892
+ self._max_project_role: int | None = int(value) if value is not None else None
893
+ return self._max_project_role
894
+
895
+ def get_workspace_role(self, obj: Any) -> int | None:
896
+ # Memoized: get_can_access_admin_settings also reads this. Delegates to the
897
+ # canonical resolver so the superuser-bootstrap and deactivated-status
898
+ # rules match what workspace RBAC enforces (no shadow copy — ADR-0087 §6).
899
+ if not hasattr(self, "_workspace_role"):
900
+ from trueppm_api.apps.workspace.permissions import workspace_role_for_user
901
+
902
+ self._workspace_role: int | None = workspace_role_for_user(obj)
903
+ return self._workspace_role
904
+
905
+ def get_can_access_admin_settings(self, obj: Any) -> bool:
906
+ from trueppm_api.apps.workspace.models import WorkspaceRole
907
+
908
+ proj = self.get_max_project_role(obj)
909
+ ws = self.get_workspace_role(obj)
910
+ return (proj is not None and proj >= Role.ADMIN) or (
911
+ ws is not None and ws >= WorkspaceRole.ADMIN
912
+ )
913
+
914
+ def get_is_workspace_operator(self, obj: Any) -> bool:
915
+ return _is_workspace_operator(obj)
916
+
917
+ def _prefs(self, obj: Any) -> tuple[str, list[str], str, str, str]:
918
+ # Memoized single read of (default_landing, hidden_views, role_context,
919
+ # timezone, date_format): every pref getter needs a UserProfile column, so
920
+ # reading them in one .only() query keeps /auth/me at one profile read
921
+ # regardless of how many fields consume it.
922
+ if not hasattr(self, "_prefs_cache"):
923
+ from trueppm_api.apps.profiles.services import get_profile_prefs
924
+
925
+ self._prefs_cache: tuple[str, list[str], str, str, str] = get_profile_prefs(obj)
926
+ return self._prefs_cache
927
+
928
+ def get_default_landing(self, obj: Any) -> str:
929
+ return self._prefs(obj)[0]
930
+
931
+ @extend_schema_field(
932
+ inline_serializer(
933
+ "Landing",
934
+ {
935
+ "intent": serializers.CharField(),
936
+ "path": serializers.CharField(),
937
+ "resolved_by": serializers.CharField(),
938
+ },
939
+ )
940
+ )
941
+ def get_landing(self, obj: Any) -> dict[str, str]:
942
+ from trueppm_api.apps.profiles.services import resolve_landing
943
+
944
+ # Reuse the already-computed preference and max project role so the
945
+ # resolver doesn't re-query UserProfile / re-aggregate Max(role) — both
946
+ # are memoized above and computed for sibling fields on this same request.
947
+ landing = resolve_landing(
948
+ obj,
949
+ pref=self.get_default_landing(obj),
950
+ max_role=self.get_max_project_role(obj),
951
+ )
952
+ return {
953
+ "intent": landing.intent,
954
+ "path": landing.path,
955
+ "resolved_by": landing.resolved_by,
956
+ }
957
+
958
+ @extend_schema_field(serializers.ListField(child=serializers.CharField()))
959
+ def get_hidden_views(self, obj: Any) -> list[str]:
960
+ return self._prefs(obj)[1]
961
+
962
+ # Reuse the model's own choices (not a hardcoded copy) so this enum is
963
+ # byte-identical to UserProfileSerializer.role_context — drf-spectacular then
964
+ # collapses both into one shared ``RoleContextEnum`` component instead of
965
+ # emitting divergent ``MeRoleContextEnum`` / ``UserProfileRoleContextEnum``
966
+ # duplicates for the same pm/scrum_master/unified set.
967
+ @extend_schema_field(serializers.ChoiceField(choices=RoleContext.choices))
968
+ def get_role_context(self, obj: Any) -> str:
969
+ return self._prefs(obj)[2]
970
+
971
+ @extend_schema_field(serializers.CharField())
972
+ def get_timezone(self, obj: Any) -> str:
973
+ return self._prefs(obj)[3]
974
+
975
+ # Reuse the model's own choices (not a hardcoded copy) so drf-spectacular
976
+ # collapses this and UserProfileSerializer.date_format into one shared
977
+ # ``DateFormatEnum`` component instead of divergent Me/UserProfile duplicates.
978
+ @extend_schema_field(serializers.ChoiceField(choices=DateFormat.choices))
979
+ def get_date_format(self, obj: Any) -> str:
980
+ return self._prefs(obj)[4]
981
+
982
+ @extend_schema_field(serializers.BooleanField())
983
+ def get_dnd_enabled(self, obj: Any) -> bool:
984
+ from trueppm_api.apps.notifications.models import UserNotificationSettings
985
+
986
+ return (
987
+ UserNotificationSettings.objects.filter(user=obj)
988
+ .values_list("dnd_enabled", flat=True)
989
+ .first()
990
+ or False
991
+ )
992
+
993
+ @extend_schema_field(TokenPostureSerializer(allow_null=True))
994
+ def get_token(self, obj: Any) -> dict[str, Any] | None:
995
+ from trueppm_api.apps.projects.models import ApiToken, is_agent_token
996
+
997
+ request = self.context.get("request")
998
+ auth = getattr(request, "auth", None) if request is not None else None
999
+ if not isinstance(auth, ApiToken):
1000
+ return None
1001
+ return {"scopes": list(auth.scopes or []), "is_agent": is_agent_token(auth)}
1002
+
1003
+ def get_display_name(self, obj: Any) -> str:
1004
+ return _user_display_name(obj)
1005
+
1006
+ def get_initials(self, obj: Any) -> str:
1007
+ return _user_initials(obj)