trueppm-api 0.4.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (759) hide show
  1. trueppm_api/__init__.py +14 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +164 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  24. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  25. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  26. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  27. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
  28. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  29. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  30. trueppm_api/apps/access/migrations/__init__.py +0 -0
  31. trueppm_api/apps/access/models.py +588 -0
  32. trueppm_api/apps/access/permissions.py +2837 -0
  33. trueppm_api/apps/access/serializers.py +946 -0
  34. trueppm_api/apps/access/services.py +667 -0
  35. trueppm_api/apps/access/signals.py +103 -0
  36. trueppm_api/apps/access/tasks.py +50 -0
  37. trueppm_api/apps/access/throttles.py +95 -0
  38. trueppm_api/apps/access/urls.py +191 -0
  39. trueppm_api/apps/access/views.py +1931 -0
  40. trueppm_api/apps/agents/__init__.py +6 -0
  41. trueppm_api/apps/agents/apps.py +11 -0
  42. trueppm_api/apps/agents/canonical.py +73 -0
  43. trueppm_api/apps/agents/deferred.py +108 -0
  44. trueppm_api/apps/agents/management/__init__.py +0 -0
  45. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  46. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  47. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  48. trueppm_api/apps/agents/middleware.py +44 -0
  49. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  50. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  51. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  52. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  53. trueppm_api/apps/agents/models.py +371 -0
  54. trueppm_api/apps/agents/refusal.py +103 -0
  55. trueppm_api/apps/agents/serializers.py +85 -0
  56. trueppm_api/apps/agents/services.py +397 -0
  57. trueppm_api/apps/agents/signals.py +30 -0
  58. trueppm_api/apps/agents/urls.py +15 -0
  59. trueppm_api/apps/agents/views.py +108 -0
  60. trueppm_api/apps/csvimport/__init__.py +12 -0
  61. trueppm_api/apps/csvimport/apps.py +11 -0
  62. trueppm_api/apps/csvimport/mapping.py +363 -0
  63. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  64. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  65. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  66. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  67. trueppm_api/apps/csvimport/models.py +111 -0
  68. trueppm_api/apps/csvimport/parser.py +1825 -0
  69. trueppm_api/apps/csvimport/services.py +48 -0
  70. trueppm_api/apps/csvimport/tasks.py +418 -0
  71. trueppm_api/apps/csvimport/template.py +37 -0
  72. trueppm_api/apps/csvimport/urls.py +50 -0
  73. trueppm_api/apps/csvimport/views.py +793 -0
  74. trueppm_api/apps/history/__init__.py +0 -0
  75. trueppm_api/apps/history/apps.py +12 -0
  76. trueppm_api/apps/history/changelog.py +359 -0
  77. trueppm_api/apps/history/diff_policy.py +177 -0
  78. trueppm_api/apps/history/serializers.py +104 -0
  79. trueppm_api/apps/history/signals.py +82 -0
  80. trueppm_api/apps/history/tasks.py +105 -0
  81. trueppm_api/apps/history/urls.py +38 -0
  82. trueppm_api/apps/history/views.py +443 -0
  83. trueppm_api/apps/idempotency/__init__.py +0 -0
  84. trueppm_api/apps/idempotency/apps.py +11 -0
  85. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  86. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  87. trueppm_api/apps/idempotency/mixins.py +226 -0
  88. trueppm_api/apps/idempotency/models.py +75 -0
  89. trueppm_api/apps/idempotency/tasks.py +61 -0
  90. trueppm_api/apps/integrations/__init__.py +13 -0
  91. trueppm_api/apps/integrations/apps.py +59 -0
  92. trueppm_api/apps/integrations/connections.py +806 -0
  93. trueppm_api/apps/integrations/encryption.py +92 -0
  94. trueppm_api/apps/integrations/external_sources.py +837 -0
  95. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  96. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  97. trueppm_api/apps/integrations/http.py +403 -0
  98. trueppm_api/apps/integrations/me_work.py +218 -0
  99. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  100. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  101. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  102. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  103. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  104. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  105. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  106. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  107. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  108. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  109. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  110. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  111. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  112. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  113. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  114. trueppm_api/apps/integrations/models.py +622 -0
  115. trueppm_api/apps/integrations/notification_channels.py +57 -0
  116. trueppm_api/apps/integrations/opengraph.py +229 -0
  117. trueppm_api/apps/integrations/outgoing.py +201 -0
  118. trueppm_api/apps/integrations/providers.py +519 -0
  119. trueppm_api/apps/integrations/registry.py +345 -0
  120. trueppm_api/apps/integrations/serializers.py +453 -0
  121. trueppm_api/apps/integrations/services.py +171 -0
  122. trueppm_api/apps/integrations/tasks.py +731 -0
  123. trueppm_api/apps/integrations/throttles.py +165 -0
  124. trueppm_api/apps/integrations/urls.py +77 -0
  125. trueppm_api/apps/integrations/views.py +1106 -0
  126. trueppm_api/apps/jiraimport/__init__.py +8 -0
  127. trueppm_api/apps/jiraimport/apps.py +11 -0
  128. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  129. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  130. trueppm_api/apps/jiraimport/models.py +70 -0
  131. trueppm_api/apps/jiraimport/parser.py +350 -0
  132. trueppm_api/apps/jiraimport/services.py +46 -0
  133. trueppm_api/apps/jiraimport/tasks.py +277 -0
  134. trueppm_api/apps/jiraimport/urls.py +15 -0
  135. trueppm_api/apps/jiraimport/views.py +153 -0
  136. trueppm_api/apps/msproject/__init__.py +0 -0
  137. trueppm_api/apps/msproject/apps.py +11 -0
  138. trueppm_api/apps/msproject/dataclasses.py +153 -0
  139. trueppm_api/apps/msproject/exporter.py +500 -0
  140. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  141. trueppm_api/apps/msproject/importer.py +1095 -0
  142. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  143. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  144. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  145. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  146. trueppm_api/apps/msproject/models.py +81 -0
  147. trueppm_api/apps/msproject/parser.py +1231 -0
  148. trueppm_api/apps/msproject/serializers.py +136 -0
  149. trueppm_api/apps/msproject/services.py +58 -0
  150. trueppm_api/apps/msproject/tasks.py +436 -0
  151. trueppm_api/apps/msproject/urls.py +39 -0
  152. trueppm_api/apps/msproject/views.py +531 -0
  153. trueppm_api/apps/notifications/__init__.py +7 -0
  154. trueppm_api/apps/notifications/apps.py +15 -0
  155. trueppm_api/apps/notifications/backfill.py +79 -0
  156. trueppm_api/apps/notifications/categories.py +112 -0
  157. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  158. trueppm_api/apps/notifications/digests.py +447 -0
  159. trueppm_api/apps/notifications/email_backend.py +330 -0
  160. trueppm_api/apps/notifications/email_health.py +128 -0
  161. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  162. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
  163. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  164. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  165. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  166. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  167. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  168. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  169. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  170. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  171. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  172. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  173. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  174. trueppm_api/apps/notifications/models.py +1197 -0
  175. trueppm_api/apps/notifications/receivers.py +155 -0
  176. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  177. trueppm_api/apps/notifications/serializers.py +961 -0
  178. trueppm_api/apps/notifications/services.py +1451 -0
  179. trueppm_api/apps/notifications/tasks.py +711 -0
  180. trueppm_api/apps/notifications/throttles.py +98 -0
  181. trueppm_api/apps/notifications/urls.py +97 -0
  182. trueppm_api/apps/notifications/views.py +904 -0
  183. trueppm_api/apps/observability/__init__.py +0 -0
  184. trueppm_api/apps/observability/apps.py +31 -0
  185. trueppm_api/apps/observability/logging.py +213 -0
  186. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  187. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  188. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  189. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  190. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  191. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  192. trueppm_api/apps/observability/models.py +141 -0
  193. trueppm_api/apps/observability/otel/__init__.py +45 -0
  194. trueppm_api/apps/observability/otel/attributes.py +247 -0
  195. trueppm_api/apps/observability/otel/export_health.py +533 -0
  196. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  197. trueppm_api/apps/observability/otel/metrics.py +551 -0
  198. trueppm_api/apps/observability/otel/provider.py +511 -0
  199. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  200. trueppm_api/apps/observability/purge_registry.py +124 -0
  201. trueppm_api/apps/observability/retention.py +139 -0
  202. trueppm_api/apps/observability/selectors.py +969 -0
  203. trueppm_api/apps/observability/serializers.py +142 -0
  204. trueppm_api/apps/observability/services.py +407 -0
  205. trueppm_api/apps/observability/tasks.py +266 -0
  206. trueppm_api/apps/observability/urls.py +27 -0
  207. trueppm_api/apps/observability/views.py +523 -0
  208. trueppm_api/apps/profiles/__init__.py +0 -0
  209. trueppm_api/apps/profiles/apps.py +15 -0
  210. trueppm_api/apps/profiles/constants.py +54 -0
  211. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  212. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  213. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  214. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  215. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  216. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  217. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  218. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  219. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  220. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  221. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  222. trueppm_api/apps/profiles/models.py +276 -0
  223. trueppm_api/apps/profiles/serializers.py +199 -0
  224. trueppm_api/apps/profiles/services.py +466 -0
  225. trueppm_api/apps/profiles/views.py +124 -0
  226. trueppm_api/apps/projects/__init__.py +0 -0
  227. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  228. trueppm_api/apps/projects/amend.py +133 -0
  229. trueppm_api/apps/projects/apps.py +38 -0
  230. trueppm_api/apps/projects/asset_feed.py +459 -0
  231. trueppm_api/apps/projects/asset_views.py +317 -0
  232. trueppm_api/apps/projects/attachment_policy.py +278 -0
  233. trueppm_api/apps/projects/authentication.py +449 -0
  234. trueppm_api/apps/projects/backfill.py +287 -0
  235. trueppm_api/apps/projects/backlog_services.py +324 -0
  236. trueppm_api/apps/projects/backlog_views.py +302 -0
  237. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  238. trueppm_api/apps/projects/blocker_services.py +510 -0
  239. trueppm_api/apps/projects/board_activity.py +462 -0
  240. trueppm_api/apps/projects/board_activity_views.py +186 -0
  241. trueppm_api/apps/projects/board_lanes.py +134 -0
  242. trueppm_api/apps/projects/bulk_settings.py +194 -0
  243. trueppm_api/apps/projects/bundled_templates.py +215 -0
  244. trueppm_api/apps/projects/calendar_settings.py +180 -0
  245. trueppm_api/apps/projects/ceremony_views.py +244 -0
  246. trueppm_api/apps/projects/commit_moment.py +204 -0
  247. trueppm_api/apps/projects/config_notice.py +1325 -0
  248. trueppm_api/apps/projects/custom_field_values.py +266 -0
  249. trueppm_api/apps/projects/decisions_services.py +93 -0
  250. trueppm_api/apps/projects/decisions_views.py +94 -0
  251. trueppm_api/apps/projects/estimation_scale.py +126 -0
  252. trueppm_api/apps/projects/export_bundle.py +353 -0
  253. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
  254. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
  255. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
  256. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
  257. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
  258. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  259. trueppm_api/apps/projects/inbound_sync.py +555 -0
  260. trueppm_api/apps/projects/iteration_label.py +85 -0
  261. trueppm_api/apps/projects/lifecycle.py +191 -0
  262. trueppm_api/apps/projects/management/__init__.py +0 -0
  263. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  264. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  265. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  266. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  267. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  268. trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
  269. trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
  270. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
  271. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  272. trueppm_api/apps/projects/mcp_settings.py +305 -0
  273. trueppm_api/apps/projects/methodology.py +178 -0
  274. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  275. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
  276. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  277. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  278. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  279. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  280. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  281. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  282. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  283. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  284. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  285. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  286. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  287. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  288. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  289. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  290. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  291. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  292. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  293. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  294. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  295. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  296. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  297. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  298. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  299. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  300. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  301. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  302. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  303. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  304. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  305. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  306. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  307. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  308. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  309. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  310. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  311. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  312. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  313. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  314. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  315. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  316. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  317. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  318. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  319. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  320. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  321. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  322. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  323. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  324. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  325. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  326. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  327. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  328. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  329. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  330. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  331. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  332. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  333. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  334. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  335. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  336. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  337. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  338. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  339. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  340. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  341. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  342. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  343. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  344. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  345. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  346. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  347. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  348. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  349. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  350. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  351. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  352. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  353. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  354. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  355. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  356. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  357. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  358. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  359. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  360. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  361. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  362. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  363. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  364. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  365. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  366. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  367. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  368. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
  369. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  370. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  371. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  372. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  373. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  374. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  375. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  376. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  377. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  378. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  379. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
  380. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  381. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  382. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  383. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  384. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  385. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
  386. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  387. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  388. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  389. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  390. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  391. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  392. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  393. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  394. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  395. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  396. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  397. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  398. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  399. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  400. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  401. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  402. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  403. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  404. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  405. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  406. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  407. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  408. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  409. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  410. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  411. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  412. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  413. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  414. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  415. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  416. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  417. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  418. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  419. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  420. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  421. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  422. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  423. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  424. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  425. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  426. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  427. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  428. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  429. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  430. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  431. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  432. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  433. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  434. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  435. trueppm_api/apps/projects/models.py +8946 -0
  436. trueppm_api/apps/projects/poker_services.py +154 -0
  437. trueppm_api/apps/projects/poker_views.py +398 -0
  438. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  439. trueppm_api/apps/projects/program_label_services.py +94 -0
  440. trueppm_api/apps/projects/program_label_views.py +189 -0
  441. trueppm_api/apps/projects/program_rollup.py +575 -0
  442. trueppm_api/apps/projects/program_schedule.py +745 -0
  443. trueppm_api/apps/projects/program_views.py +3220 -0
  444. trueppm_api/apps/projects/project_templates.py +422 -0
  445. trueppm_api/apps/projects/receivers.py +234 -0
  446. trueppm_api/apps/projects/refusal_codes.py +140 -0
  447. trueppm_api/apps/projects/reorder_services.py +145 -0
  448. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  449. trueppm_api/apps/projects/retro_board_services.py +368 -0
  450. trueppm_api/apps/projects/retro_services.py +285 -0
  451. trueppm_api/apps/projects/risk_import.py +396 -0
  452. trueppm_api/apps/projects/row_identity.py +113 -0
  453. trueppm_api/apps/projects/schema_migrations.py +228 -0
  454. trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
  455. trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
  456. trueppm_api/apps/projects/seed/__init__.py +61 -0
  457. trueppm_api/apps/projects/seed/exporter.py +1644 -0
  458. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  459. trueppm_api/apps/projects/seed/importer.py +2390 -0
  460. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  461. trueppm_api/apps/projects/seed/reldates.py +126 -0
  462. trueppm_api/apps/projects/seed/replace.py +126 -0
  463. trueppm_api/apps/projects/seed/replay.py +1691 -0
  464. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  465. trueppm_api/apps/projects/seed/samples.py +427 -0
  466. trueppm_api/apps/projects/seed/validation.py +1149 -0
  467. trueppm_api/apps/projects/serializers.py +12310 -0
  468. trueppm_api/apps/projects/services.py +6966 -0
  469. trueppm_api/apps/projects/share_serializers.py +132 -0
  470. trueppm_api/apps/projects/share_services.py +325 -0
  471. trueppm_api/apps/projects/share_views.py +374 -0
  472. trueppm_api/apps/projects/sharing_settings.py +187 -0
  473. trueppm_api/apps/projects/signal_privacy_services.py +909 -0
  474. trueppm_api/apps/projects/signal_privacy_views.py +476 -0
  475. trueppm_api/apps/projects/signals.py +132 -0
  476. trueppm_api/apps/projects/slip_conflict.py +266 -0
  477. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  478. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  479. trueppm_api/apps/projects/standup.py +304 -0
  480. trueppm_api/apps/projects/standup_views.py +99 -0
  481. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  482. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  483. trueppm_api/apps/projects/surface_visibility.py +126 -0
  484. trueppm_api/apps/projects/task_batch_services.py +379 -0
  485. trueppm_api/apps/projects/task_bulk.py +1152 -0
  486. trueppm_api/apps/projects/task_classification.py +561 -0
  487. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  488. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  489. trueppm_api/apps/projects/task_grouping.py +754 -0
  490. trueppm_api/apps/projects/tasks.py +1917 -0
  491. trueppm_api/apps/projects/template_divergence.py +168 -0
  492. trueppm_api/apps/projects/template_services.py +160 -0
  493. trueppm_api/apps/projects/template_tasks.py +258 -0
  494. trueppm_api/apps/projects/template_views.py +799 -0
  495. trueppm_api/apps/projects/throttles.py +311 -0
  496. trueppm_api/apps/projects/urls.py +1011 -0
  497. trueppm_api/apps/projects/utilization.py +885 -0
  498. trueppm_api/apps/projects/views.py +20572 -0
  499. trueppm_api/apps/projects/wbs_paths.py +119 -0
  500. trueppm_api/apps/resources/__init__.py +0 -0
  501. trueppm_api/apps/resources/apps.py +8 -0
  502. trueppm_api/apps/resources/capacity.py +121 -0
  503. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  504. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  505. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  506. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  507. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  508. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  509. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  510. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  511. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  512. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  513. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  514. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  515. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  516. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  517. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  518. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  519. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  520. trueppm_api/apps/resources/models.py +298 -0
  521. trueppm_api/apps/resources/serializers.py +426 -0
  522. trueppm_api/apps/resources/services.py +465 -0
  523. trueppm_api/apps/resources/urls.py +28 -0
  524. trueppm_api/apps/resources/views.py +1749 -0
  525. trueppm_api/apps/scheduling/__init__.py +0 -0
  526. trueppm_api/apps/scheduling/apps.py +153 -0
  527. trueppm_api/apps/scheduling/calendars.py +119 -0
  528. trueppm_api/apps/scheduling/deadletter.py +91 -0
  529. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  530. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  531. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  532. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  533. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  534. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  535. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  536. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  537. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  538. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  539. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  540. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  541. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  542. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  543. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  544. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  545. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  546. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  547. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  548. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  549. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  550. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  551. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  552. trueppm_api/apps/scheduling/models.py +516 -0
  553. trueppm_api/apps/scheduling/receivers.py +160 -0
  554. trueppm_api/apps/scheduling/risk_premium.py +283 -0
  555. trueppm_api/apps/scheduling/serializers.py +564 -0
  556. trueppm_api/apps/scheduling/services.py +1182 -0
  557. trueppm_api/apps/scheduling/signals.py +56 -0
  558. trueppm_api/apps/scheduling/tasks.py +1984 -0
  559. trueppm_api/apps/scheduling/telemetry.py +80 -0
  560. trueppm_api/apps/scheduling/units.py +35 -0
  561. trueppm_api/apps/scheduling/urls.py +57 -0
  562. trueppm_api/apps/scheduling/views.py +2564 -0
  563. trueppm_api/apps/sso/__init__.py +0 -0
  564. trueppm_api/apps/sso/apps.py +19 -0
  565. trueppm_api/apps/sso/extensions.py +130 -0
  566. trueppm_api/apps/sso/management/__init__.py +0 -0
  567. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  568. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  569. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  570. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  571. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  572. trueppm_api/apps/sso/models.py +148 -0
  573. trueppm_api/apps/sso/serializers.py +437 -0
  574. trueppm_api/apps/sso/services.py +1385 -0
  575. trueppm_api/apps/sso/urls.py +47 -0
  576. trueppm_api/apps/sso/views.py +935 -0
  577. trueppm_api/apps/sync/__init__.py +0 -0
  578. trueppm_api/apps/sync/apps.py +15 -0
  579. trueppm_api/apps/sync/backfill.py +208 -0
  580. trueppm_api/apps/sync/broadcast.py +320 -0
  581. trueppm_api/apps/sync/conflict.py +262 -0
  582. trueppm_api/apps/sync/consumers.py +471 -0
  583. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  584. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  585. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  586. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  587. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  588. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  589. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  590. trueppm_api/apps/sync/models.py +225 -0
  591. trueppm_api/apps/sync/pagination.py +231 -0
  592. trueppm_api/apps/sync/sequence.py +371 -0
  593. trueppm_api/apps/sync/serializers.py +687 -0
  594. trueppm_api/apps/sync/tasks.py +260 -0
  595. trueppm_api/apps/sync/throttles.py +175 -0
  596. trueppm_api/apps/sync/upload.py +501 -0
  597. trueppm_api/apps/sync/urls.py +17 -0
  598. trueppm_api/apps/sync/views.py +930 -0
  599. trueppm_api/apps/sync/ws_auth.py +193 -0
  600. trueppm_api/apps/taskruns/__init__.py +0 -0
  601. trueppm_api/apps/taskruns/apps.py +11 -0
  602. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  603. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  604. trueppm_api/apps/taskruns/models.py +68 -0
  605. trueppm_api/apps/taskruns/serializers.py +92 -0
  606. trueppm_api/apps/taskruns/tasks.py +60 -0
  607. trueppm_api/apps/taskruns/tracker.py +286 -0
  608. trueppm_api/apps/taskruns/urls.py +46 -0
  609. trueppm_api/apps/taskruns/views.py +195 -0
  610. trueppm_api/apps/teams/__init__.py +8 -0
  611. trueppm_api/apps/teams/apps.py +16 -0
  612. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  613. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
  614. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  615. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  616. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  617. trueppm_api/apps/teams/models.py +145 -0
  618. trueppm_api/apps/teams/permissions.py +149 -0
  619. trueppm_api/apps/teams/serializers.py +95 -0
  620. trueppm_api/apps/teams/services.py +434 -0
  621. trueppm_api/apps/teams/signals.py +61 -0
  622. trueppm_api/apps/teams/urls.py +34 -0
  623. trueppm_api/apps/teams/views.py +201 -0
  624. trueppm_api/apps/timetracking/__init__.py +0 -0
  625. trueppm_api/apps/timetracking/apps.py +9 -0
  626. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  627. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  628. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  629. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  630. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  631. trueppm_api/apps/timetracking/models.py +178 -0
  632. trueppm_api/apps/timetracking/serializers.py +175 -0
  633. trueppm_api/apps/timetracking/services.py +117 -0
  634. trueppm_api/apps/timetracking/urls.py +47 -0
  635. trueppm_api/apps/timetracking/views.py +526 -0
  636. trueppm_api/apps/webhooks/__init__.py +0 -0
  637. trueppm_api/apps/webhooks/apps.py +8 -0
  638. trueppm_api/apps/webhooks/backfill.py +90 -0
  639. trueppm_api/apps/webhooks/dispatch.py +144 -0
  640. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  641. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
  642. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  643. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  644. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  645. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  646. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  647. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  648. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  649. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
  650. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  651. trueppm_api/apps/webhooks/models.py +372 -0
  652. trueppm_api/apps/webhooks/serializers.py +318 -0
  653. trueppm_api/apps/webhooks/tasks.py +663 -0
  654. trueppm_api/apps/webhooks/urls.py +22 -0
  655. trueppm_api/apps/webhooks/views.py +383 -0
  656. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  657. trueppm_api/apps/workflow_engine/apps.py +23 -0
  658. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  659. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  660. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  661. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  662. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  663. trueppm_api/apps/workflow_engine/models.py +265 -0
  664. trueppm_api/apps/workflow_engine/tasks.py +368 -0
  665. trueppm_api/apps/workspace/__init__.py +0 -0
  666. trueppm_api/apps/workspace/apps.py +10 -0
  667. trueppm_api/apps/workspace/backfill.py +111 -0
  668. trueppm_api/apps/workspace/export.py +242 -0
  669. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  670. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  671. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  672. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  673. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  674. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  675. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  676. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  677. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  678. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  679. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  680. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  681. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  682. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  683. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  684. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  685. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  686. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  687. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  688. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  689. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  690. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  691. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  692. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  693. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  694. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  695. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  696. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  697. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  698. trueppm_api/apps/workspace/models.py +857 -0
  699. trueppm_api/apps/workspace/permissions.py +187 -0
  700. trueppm_api/apps/workspace/serializers.py +590 -0
  701. trueppm_api/apps/workspace/services.py +919 -0
  702. trueppm_api/apps/workspace/signals.py +37 -0
  703. trueppm_api/apps/workspace/tasks.py +555 -0
  704. trueppm_api/apps/workspace/urls.py +113 -0
  705. trueppm_api/apps/workspace/views.py +1502 -0
  706. trueppm_api/asgi.py +42 -0
  707. trueppm_api/celery.py +31 -0
  708. trueppm_api/core/__init__.py +0 -0
  709. trueppm_api/core/admin_site.py +206 -0
  710. trueppm_api/core/auth_views.py +916 -0
  711. trueppm_api/core/constant_time.py +51 -0
  712. trueppm_api/core/csp.py +54 -0
  713. trueppm_api/core/db.py +87 -0
  714. trueppm_api/core/db_session.py +37 -0
  715. trueppm_api/core/exception_handlers.py +157 -0
  716. trueppm_api/core/export_downloads.py +56 -0
  717. trueppm_api/core/extension_providers.py +56 -0
  718. trueppm_api/core/extension_signals.py +147 -0
  719. trueppm_api/core/idempotent.py +260 -0
  720. trueppm_api/core/metadata.py +42 -0
  721. trueppm_api/core/middleware.py +51 -0
  722. trueppm_api/core/openapi.py +949 -0
  723. trueppm_api/core/password_reset.py +500 -0
  724. trueppm_api/core/protect_conflict.py +100 -0
  725. trueppm_api/core/ratelimit.py +148 -0
  726. trueppm_api/core/redis_throttle.py +82 -0
  727. trueppm_api/core/refresh_cookie_policy.py +156 -0
  728. trueppm_api/core/request_body.py +86 -0
  729. trueppm_api/core/security_checks.py +884 -0
  730. trueppm_api/core/storage_config.py +105 -0
  731. trueppm_api/core/text_decode.py +168 -0
  732. trueppm_api/core/throttling.py +344 -0
  733. trueppm_api/core/valkey.py +214 -0
  734. trueppm_api/core/valkey_cache.py +66 -0
  735. trueppm_api/core/valkey_checks.py +104 -0
  736. trueppm_api/core/valkey_config.py +39 -0
  737. trueppm_api/core/worker_broker_wait.py +149 -0
  738. trueppm_api/core/worker_heartbeat.py +134 -0
  739. trueppm_api/fields.py +36 -0
  740. trueppm_api/permissions.py +30 -0
  741. trueppm_api/py.typed +0 -0
  742. trueppm_api/routing.py +11 -0
  743. trueppm_api/settings/__init__.py +0 -0
  744. trueppm_api/settings/base.py +2455 -0
  745. trueppm_api/settings/dev.py +273 -0
  746. trueppm_api/settings/prod.py +228 -0
  747. trueppm_api/urls.py +153 -0
  748. trueppm_api/workflows/__init__.py +38 -0
  749. trueppm_api/workflows/backends/__init__.py +6 -0
  750. trueppm_api/workflows/backends/default.py +297 -0
  751. trueppm_api/workflows/consumers/__init__.py +11 -0
  752. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  753. trueppm_api/workflows/interface.py +148 -0
  754. trueppm_api/workflows/registry.py +106 -0
  755. trueppm_api/workflows/services.py +90 -0
  756. trueppm_api/wsgi.py +11 -0
  757. trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
  758. trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
  759. trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
@@ -0,0 +1,946 @@
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.profiles.models import DateFormat, RoleContext
24
+ from trueppm_api.apps.workspace.models import WorkspaceRole
25
+ from trueppm_api.apps.workspace.permissions import (
26
+ _workspace_membership_role,
27
+ workspace_role_for_user,
28
+ )
29
+ from trueppm_api.apps.workspace.serializers import display_name_for
30
+
31
+ User = get_user_model()
32
+
33
+ # A mention group key must be a valid mention token (the name class of
34
+ # notifications.services._MENTION_RE) so it is actually addressable as @name.
35
+ _GROUP_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
36
+
37
+ # Refusal copy for an unreachable membership target. Deliberately identical for
38
+ # "no such account" and "an account you may not name" so the field leaks no
39
+ # existence oracle — the same reasoning as ``ProjectSerializer.copy_settings_from``.
40
+ _UNREACHABLE_TARGET_ERROR = (
41
+ "No such user, or not someone you can add. You can add people you already share a "
42
+ "project or program with; ask a workspace admin to invite anyone else."
43
+ )
44
+
45
+ _USER_IMMUTABLE_ERROR = (
46
+ "A membership's user cannot be changed. Remove this member and add the other account."
47
+ )
48
+
49
+ _TARGET_HELP_TEXT = (
50
+ "The stock auth.User integer primary key of the account to add. Bounded to accounts "
51
+ "you can already reach: a workspace Admin may name any active account; anyone else "
52
+ "may name themselves or somebody already on a project or program roster they belong "
53
+ "to. Deactivated accounts are never accepted. An id outside that set is refused with "
54
+ "400 and the same message a nonexistent id gets, so this field is not an existence "
55
+ "oracle — use a workspace invite to bring in anyone further out. Accepted on add "
56
+ "only; a membership's user cannot be changed."
57
+ )
58
+
59
+
60
+ def reachable_membership_targets(actor: Any, *, request: Any = None) -> QuerySet[Any]:
61
+ """Accounts ``actor`` may name as the target of a membership write (#3641).
62
+
63
+ Membership writes take a bare ``auth.User`` integer primary key and answer with
64
+ the target's ``user_detail`` — including their email address. Left unbounded that
65
+ turns a self-minted project into an address-lookup oracle over sequential ids
66
+ (``POST /api/v1/projects/`` is ungated and mints the caller as Owner), which is
67
+ the harvest #815 and #891 were fixed to prevent. So the target set is bounded to
68
+ accounts the caller can *already* read, and a membership grant can therefore never
69
+ disclose an account they could not already see.
70
+
71
+ Two tiers, both mirroring rules the install already enforces elsewhere:
72
+
73
+ * **Workspace ADMIN or above** reaches every active account. That is the branch
74
+ ``WorkspaceMemberListView`` already takes for the directory listing, and the
75
+ principal that already owns ``/workspace/invites/`` and
76
+ ``/workspace/groups/{id}/members/``. It is not self-grantable: an implicit role
77
+ is MEMBER (superuser bootstrap aside) and an explicit row is written only by an
78
+ existing admin (``workspace_role_for_user``).
79
+ * **Everyone else** reaches themselves plus the people already introduced to a
80
+ project or program roster they belong to — the same "must already be a member"
81
+ constraint ``validate_lead`` and the mention-group ``_member_user_or_400``
82
+ helper apply, with the caller exempt exactly as in
83
+ ``seed.importer._resolve_accounts`` (#1057).
84
+
85
+ The caller's *own* membership must be live, but the target's need not be: a revoked
86
+ row is still evidence that somebody with reach introduced that account to this
87
+ roster, and requiring a live one would make re-adding a member you just removed
88
+ impossible — the revive path ``_revive_revoked_membership`` exists for (#3410).
89
+
90
+ So "reachable" means **ever introduced to a roster I am on**, not *currently
91
+ readable there*. The deliberate consequence: somebody revoked from a project before
92
+ the caller joined it is nameable even though the caller never saw them on it. That
93
+ is bounded to accounts already adjacent to the caller's own projects, never the
94
+ install, and it is the price of keeping re-add working. Do not silently narrow this
95
+ to the live set without restoring the re-add path some other way.
96
+
97
+ ``is_active=False`` accounts are excluded at every tier, matching ``UserSearchView``:
98
+ the deactivated pool is admin-only state (#1724) and must not be reachable through
99
+ a roster write either.
100
+
101
+ Bringing in somebody outside that set is the invite path
102
+ (``POST /api/v1/workspace/invites/``). It is keyed on an email address the inviter
103
+ must already possess, so it discloses nothing — it is the reason this narrowing does
104
+ not strand onboarding.
105
+
106
+ Args:
107
+ actor: The requesting user. An anonymous or deactivated principal reaches nobody.
108
+ request: The DRF request, when there is one. Resolving the workspace role
109
+ through it populates the per-request cache the permission layer reads, so a
110
+ workspace-scoped permission class added to these viewsets later does not
111
+ silently resolve the same role a second time.
112
+
113
+ Returns:
114
+ A ``User`` queryset of the accounts ``actor`` may name.
115
+ """
116
+ role = (
117
+ _workspace_membership_role(request)
118
+ if request is not None
119
+ else workspace_role_for_user(actor)
120
+ )
121
+ if role is None:
122
+ return User.objects.none()
123
+
124
+ active = User.objects.filter(is_active=True)
125
+ if role >= WorkspaceRole.ADMIN:
126
+ return active
127
+
128
+ # Subqueries rather than a join + ``.distinct()``: a caller on many projects would
129
+ # otherwise fan the join out per shared roster row, and a bare ``.distinct()`` is
130
+ # silently defeated by any model ordering the queryset picks up.
131
+ # The actor's own row must be live and its scope must still exist — a trashed
132
+ # project's roster is unreadable (``_get_project_or_404`` filters it out), so it
133
+ # must not confer reach either. Archived is deliberately NOT excluded: an archived
134
+ # project is read-only, not invisible, and its members still see each other.
135
+ actor_project_ids = ProjectMembership.objects.filter(
136
+ user=actor, is_deleted=False, project__is_deleted=False
137
+ ).values("project_id")
138
+ actor_program_ids = ProgramMembership.objects.filter(
139
+ user=actor, is_deleted=False, program__is_deleted=False
140
+ ).values("program_id")
141
+ return active.filter(
142
+ Q(pk=actor.pk)
143
+ | Q(
144
+ pk__in=ProjectMembership.objects.filter(project_id__in=actor_project_ids).values(
145
+ "user_id"
146
+ )
147
+ )
148
+ | Q(
149
+ pk__in=ProgramMembership.objects.filter(program_id__in=actor_program_ids).values(
150
+ "user_id"
151
+ )
152
+ )
153
+ )
154
+
155
+
156
+ class _ReachableMembershipTargetMixin:
157
+ """Bounds a membership write serializer's ``user`` field to reachable accounts.
158
+
159
+ ``user`` is declared with an **empty** queryset on the concrete serializers, so a
160
+ serializer built without a request context resolves nobody rather than everybody —
161
+ the field is its own IDOR gate, failing closed. Here the real, caller-scoped
162
+ queryset is swapped in (the structural precedent is
163
+ ``ProjectSerializer.__init__``'s ``copy_settings_from`` narrowing).
164
+
165
+ On **update** the field is made read-only and a supplied ``user`` is refused
166
+ outright: reassigning an existing row onto another account is the same harvest
167
+ through a second door, and it also silently rewrites who holds access while
168
+ keeping the row's ``joined_at`` access evidence (#3410). Refusing rather than
169
+ ignoring keeps a 200 from meaning two different things to the caller.
170
+ """
171
+
172
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
173
+ super().__init__(*args, **kwargs)
174
+ field = self.fields.get("user") # type: ignore[attr-defined]
175
+ if not isinstance(field, serializers.PrimaryKeyRelatedField):
176
+ return
177
+ if self.instance is not None: # type: ignore[attr-defined]
178
+ field.read_only = True
179
+ return
180
+ request = self.context.get("request") # type: ignore[attr-defined]
181
+ actor = getattr(request, "user", None)
182
+ if actor is not None and getattr(actor, "is_authenticated", False):
183
+ field.queryset = reachable_membership_targets(actor, request=request)
184
+
185
+ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
186
+ # ``user`` is read-only on update, so DRF drops it silently. Surface the
187
+ # refusal instead — a caller that asked to reassign the row must not read a
188
+ # 200 as "done". Checked against ``initial_data`` because the field never
189
+ # reaches ``attrs``; the body may not be a dict, so do not assume ``in`` works.
190
+ if self.instance is not None: # type: ignore[attr-defined]
191
+ raw = getattr(self, "initial_data", None)
192
+ if isinstance(raw, dict) and "user" in raw:
193
+ raise serializers.ValidationError({"user": _USER_IMMUTABLE_ERROR})
194
+ return super().validate(attrs) # type: ignore[misc,no-any-return]
195
+
196
+
197
+ class _UserSummarySerializer(serializers.ModelSerializer): # type: ignore[type-arg]
198
+ class Meta:
199
+ model = User
200
+ fields = ["id", "username", "email"]
201
+
202
+
203
+ class ProjectMembershipReadSerializer(serializers.ModelSerializer[ProjectMembership]):
204
+ """Response serializer — includes user_detail and role_label for list/retrieve.
205
+
206
+ role — integer ordinal (canonical wire format; use for comparisons)
207
+ role_label — human-readable label e.g. "Project Manager" (display only)
208
+ joined_at — when this membership row was first created. Per-project access
209
+ evidence, but NOT proof of uninterrupted access: re-adding a
210
+ previously removed member revives their original row and keeps
211
+ this date, so the span may contain one or more revoked intervals
212
+ (#3410). Use reinstated_at as the discriminator — it is null iff
213
+ this membership has never lapsed (#3436).
214
+ role_changed_at — when the role last changed, or null if unchanged since joining
215
+ reinstated_at — when this membership was last revived after a revoke, or null if
216
+ it has never been revoked and re-added. Stamped on every revive
217
+ (advances on a second revive rather than accumulating history) —
218
+ the durable server-side fact that lets a caller, human or machine,
219
+ tell a never-lapsed membership apart from a revived one (#3436).
220
+ other_active_project_count — how many OTHER active (non-archived, non-deleted) projects this
221
+ user belongs to, excluding the current one. A resource-load
222
+ signal for the assigner (#598). The full count is shown; it is a
223
+ number only and leaks no project identities.
224
+ other_active_project_names — names of those other projects, but ONLY the ones the *requesting*
225
+ user is OWNER of (visibility gate — never reveal the name of a
226
+ project the requester cannot already see). Empty for non-OWNERs.
227
+ """
228
+
229
+ user_detail = _UserSummarySerializer(source="user", read_only=True)
230
+ role_label = serializers.SerializerMethodField()
231
+ other_active_project_count = serializers.SerializerMethodField()
232
+ other_active_project_names = serializers.SerializerMethodField()
233
+
234
+ def get_role_label(self, obj: ProjectMembership) -> str:
235
+ """Display name for ``role`` in **project** vocabulary.
236
+
237
+ Ordinal 400 reads "Project Admin" and 300 reads "Project Manager". The
238
+ identically-named field on a *program* membership deliberately names the
239
+ same two ordinals "Program Admin" and "Program Manager" — see
240
+ ``ProgramMembershipReadSerializer.get_role_label``. One ordinal, two
241
+ containers, two names.
242
+ """
243
+ return Role(obj.role).label
244
+
245
+ def get_other_active_project_count(self, obj: ProjectMembership) -> int:
246
+ # list/retrieve annotate this on the queryset (one Subquery, no N+1). create/
247
+ # partial_update serialize a fresh, un-annotated instance — fall back to a
248
+ # single count query there (rare, one row).
249
+ annotated = getattr(obj, "other_active_count", None)
250
+ if annotated is not None:
251
+ return int(annotated)
252
+ return (
253
+ ProjectMembership.objects.filter(
254
+ user_id=obj.user_id,
255
+ is_deleted=False,
256
+ project__is_deleted=False,
257
+ project__is_archived=False,
258
+ )
259
+ .exclude(project_id=obj.project_id)
260
+ .values("project_id")
261
+ .distinct()
262
+ .count()
263
+ )
264
+
265
+ def get_other_active_project_names(self, obj: ProjectMembership) -> list[str]:
266
+ # Names are visibility-gated and prebuilt once per request by the viewset
267
+ # (see ProjectMembershipViewSet._build_other_project_names_map). When the map
268
+ # is absent (create/partial_update responses) return [] — the client re-fetches
269
+ # the list, which carries the gated names.
270
+ names_map: dict[Any, list[str]] = self.context.get("other_project_names_map") or {}
271
+ return names_map.get(obj.user_id, [])
272
+
273
+ class Meta:
274
+ model = ProjectMembership
275
+ fields = [
276
+ "id",
277
+ "server_version",
278
+ "project",
279
+ "user",
280
+ "user_detail",
281
+ "role",
282
+ "role_label",
283
+ "joined_at",
284
+ "role_changed_at",
285
+ "reinstated_at",
286
+ "other_active_project_count",
287
+ "other_active_project_names",
288
+ ]
289
+ read_only_fields = [
290
+ "id",
291
+ "server_version",
292
+ "project",
293
+ "user",
294
+ "user_detail",
295
+ "role_label",
296
+ "joined_at",
297
+ "role_changed_at",
298
+ "reinstated_at",
299
+ "other_active_project_count",
300
+ "other_active_project_names",
301
+ ]
302
+
303
+
304
+ class ProjectMembershipWriteSerializer(
305
+ _ReachableMembershipTargetMixin, serializers.ModelSerializer[ProjectMembership]
306
+ ):
307
+ """Write serializer — accepts user and role; project is injected from URL.
308
+
309
+ ``user`` is the stock ``auth.User`` **integer** primary key (TruePPM has no custom
310
+ user model), and it is bounded to :func:`reachable_membership_targets` — see that
311
+ function for who a caller may name and why. It is accepted on add only; a
312
+ ``partial_update`` that carries it is refused (#3641).
313
+
314
+ ``role`` is optional (ADR-0363, #157): when omitted on add, the viewset falls
315
+ back to the project's ``default_member_role``. It remains required for a role
316
+ *change* (``partial_update`` always sends it), and the model column stays
317
+ non-null — the fallback is resolved before ``save``.
318
+ """
319
+
320
+ # Empty by construction — the mixin swaps in the caller-scoped queryset. Without a
321
+ # request context the field resolves nobody, so it fails closed.
322
+ user = serializers.PrimaryKeyRelatedField(
323
+ queryset=User.objects.none(),
324
+ error_messages={"does_not_exist": _UNREACHABLE_TARGET_ERROR},
325
+ help_text=_TARGET_HELP_TEXT,
326
+ )
327
+ role = serializers.IntegerField(
328
+ required=False,
329
+ help_text=(
330
+ "Optional on add — defaults to the project's default_member_role "
331
+ "(ADR-0363). Required to change an existing member's role."
332
+ ),
333
+ )
334
+
335
+ class Meta:
336
+ model = ProjectMembership
337
+ fields = ["user", "role"]
338
+
339
+ def validate_role(self, value: int) -> int:
340
+ # Role must be a valid Role ordinal.
341
+ valid = {r.value for r in Role}
342
+ if value not in valid:
343
+ raise serializers.ValidationError(f"Invalid role. Choose from {sorted(valid)}.")
344
+ return value
345
+
346
+
347
+ class ProjectMembershipUpdateSerializer(ProjectMembershipWriteSerializer):
348
+ """PATCH body — ``role`` only.
349
+
350
+ A separate class rather than ``read_only_fields`` on the add serializer so the
351
+ published schema tells the truth: ``user`` is genuinely not part of an update
352
+ body, and an integrator reading ``PatchedProjectMembershipWriteRequest`` would
353
+ otherwise still see it advertised as writable. Sending it anyway is refused by
354
+ the inherited ``validate`` rather than silently dropped (#3641).
355
+ """
356
+
357
+ class Meta(ProjectMembershipWriteSerializer.Meta):
358
+ fields = ["role"]
359
+
360
+
361
+ class ProgramMembershipReadSerializer(serializers.ModelSerializer[ProgramMembership]):
362
+ """Response serializer for ProgramMembership — mirrors the project version.
363
+
364
+ ``joined_at`` carries the same caveat as its project twin: it is the date the row
365
+ was first created, and a revoked member who is later re-added keeps it, so the
366
+ span may contain revoked intervals (#3410). It is not proof of uninterrupted
367
+ access — ``reinstated_at`` is the discriminator (null iff never revoked and
368
+ re-added, #3436).
369
+ """
370
+
371
+ user_detail = _UserSummarySerializer(source="user", read_only=True)
372
+ # ``role_label`` is the *access* role's display name (Owner/Admin/…), a
373
+ # computed mirror of the ``role`` ordinal — NOT the freeform functional title.
374
+ # The freeform PO/PM/Tech-Lead label is the distinct ``role_title`` field
375
+ # below (#565); the two never collide.
376
+ role_label = serializers.SerializerMethodField()
377
+
378
+ def get_role_label(self, obj: ProgramMembership) -> str:
379
+ """Display name for ``role`` in **program** vocabulary.
380
+
381
+ Ordinal 400 reads "Program Admin" and 300 reads "Program Manager" — the
382
+ same names ``Program.my_role_label`` uses, from the same map. An ordinal
383
+ outside the five OSS roles has no name and reads ``"Role <ordinal>"``.
384
+ """
385
+ # Why this is not ``Role.label``: the enum's labels are project-scoped, so
386
+ # serializing through them called a *program* membership "Project Admin"
387
+ # here while the program card called the same membership "Program Admin"
388
+ # — one fact, two answers, and only TruePPM's own web client knew which to
389
+ # believe (#3503). ``program_role_label`` is the one definition of that
390
+ # vocabulary and the program card calls it too.
391
+ #
392
+ # The fallback covers an Enterprise custom-band ordinal (ADR-0072), which
393
+ # the OSS edition cannot name. The field is a non-null string in the
394
+ # published schema and the ordinal is already on the wire beside it in
395
+ # ``role``, so echoing it degrades rather than borrowing a neighbouring
396
+ # role's name — and rather than the ``ValueError`` this used to raise,
397
+ # which DRF does not convert and which 500'd the whole response.
398
+ return program_role_label(obj.role) or f"Role {obj.role}"
399
+
400
+ class Meta:
401
+ model = ProgramMembership
402
+ fields = [
403
+ "id",
404
+ "server_version",
405
+ "program",
406
+ "user",
407
+ "user_detail",
408
+ "role",
409
+ "role_label",
410
+ "role_title",
411
+ "joined_at",
412
+ "role_changed_at",
413
+ "reinstated_at",
414
+ ]
415
+ read_only_fields = [
416
+ "id",
417
+ "server_version",
418
+ "program",
419
+ "user",
420
+ "user_detail",
421
+ "role_label",
422
+ "joined_at",
423
+ "role_changed_at",
424
+ "reinstated_at",
425
+ ]
426
+
427
+
428
+ class ProgramMembershipWriteSerializer(
429
+ _ReachableMembershipTargetMixin, serializers.ModelSerializer[ProgramMembership]
430
+ ):
431
+ """Write serializer — accepts user, role, and the freeform role_title.
432
+
433
+ ``user`` is the stock ``auth.User`` **integer** primary key, bounded to
434
+ :func:`reachable_membership_targets` on add and refused entirely on
435
+ ``partial_update`` (#3641) — the project twin carries the same constraint for the
436
+ same reason.
437
+
438
+ ``program`` is injected from the URL. ``role_title`` (#565) is optional; the
439
+ view gates *who* may set it (a role change stays Owner-only, while a
440
+ role_title-only PATCH is allowed at Admin+).
441
+ """
442
+
443
+ # Empty by construction — see the project twin.
444
+ user = serializers.PrimaryKeyRelatedField(
445
+ queryset=User.objects.none(),
446
+ error_messages={"does_not_exist": _UNREACHABLE_TARGET_ERROR},
447
+ help_text=_TARGET_HELP_TEXT,
448
+ )
449
+
450
+ class Meta:
451
+ model = ProgramMembership
452
+ fields = ["user", "role", "role_title"]
453
+
454
+ def validate_role(self, value: int) -> int:
455
+ valid = {r.value for r in Role}
456
+ if value not in valid:
457
+ raise serializers.ValidationError(f"Invalid role. Choose from {sorted(valid)}.")
458
+ return value
459
+
460
+ def validate_role_title(self, value: str | None) -> str:
461
+ # Collapse whitespace-only / empty submissions to "" so "unset" is a single
462
+ # canonical state (empty string, never NULL — per the model's DJ001 default).
463
+ return (value or "").strip()
464
+
465
+
466
+ class ProgramMembershipUpdateSerializer(ProgramMembershipWriteSerializer):
467
+ """PATCH body — ``role`` and ``role_title`` only; see the project twin for why."""
468
+
469
+ class Meta(ProgramMembershipWriteSerializer.Meta):
470
+ fields = ["role", "role_title"]
471
+
472
+
473
+ class UserDefinedMentionGroupReadSerializer(serializers.ModelSerializer[UserDefinedMentionGroup]):
474
+ """Response serializer for a user-defined @mention group (ADR-0212, #515).
475
+
476
+ ``members`` is the curated member set (summary detail); ``member_count`` is a
477
+ convenience for the list UI. ``muted_by_me`` reflects whether the *requesting*
478
+ user has muted this group — the per-user override that suppresses its
479
+ mentions for them.
480
+ """
481
+
482
+ members = _UserSummarySerializer(many=True, read_only=True)
483
+ member_count = serializers.SerializerMethodField()
484
+ muted_by_me = serializers.SerializerMethodField()
485
+
486
+ def get_member_count(self, obj: UserDefinedMentionGroup) -> int:
487
+ # `members` is already prefetched by the viewset (it also backs the
488
+ # ``members`` field), so counting the loaded rows in Python is free. A
489
+ # DB-side ``.count()`` here would be a wasted round trip, not a saving.
490
+ members = obj.members.all()
491
+ return len(members)
492
+
493
+ def get_muted_by_me(self, obj: UserDefinedMentionGroup) -> bool:
494
+ user = getattr(self.context.get("request"), "user", None)
495
+ if user is None or not getattr(user, "is_authenticated", False):
496
+ return False
497
+ # muted_by is prefetched by the viewset.
498
+ return any(m.pk == user.pk for m in obj.muted_by.all())
499
+
500
+ class Meta:
501
+ model = UserDefinedMentionGroup
502
+ fields = [
503
+ "id",
504
+ "server_version",
505
+ "project",
506
+ "name",
507
+ "description",
508
+ "email_default_on",
509
+ "members",
510
+ "member_count",
511
+ "muted_by_me",
512
+ ]
513
+ read_only_fields = fields
514
+
515
+
516
+ def _validated_mention_group_name(value: str) -> str:
517
+ """Normalize and validate a @mention group name, shared by the project- and
518
+ program-scoped write serializers (#3903) — everything except the per-scope
519
+ uniqueness check, which needs each serializer's own queryset/context.
520
+ """
521
+ # Accept a leading @ from the client for convenience; store without it.
522
+ name = value.strip().lstrip("@").strip()
523
+ if not name:
524
+ raise serializers.ValidationError("Group name cannot be empty.")
525
+ if len(name) > 32:
526
+ raise serializers.ValidationError("Group name must be 32 characters or fewer.")
527
+ if not _GROUP_NAME_RE.match(name):
528
+ raise serializers.ValidationError(
529
+ "Group name may only contain letters, digits, and the characters . _ -"
530
+ )
531
+ # An auto-group name (@admins, @scrum-team, @program-pms, …) must never
532
+ # be shadowed — project- and program-scoped keys alike.
533
+ if name.lower() in ALL_AUTO_GROUP_KEYS:
534
+ raise serializers.ValidationError(
535
+ f"'@{name}' is a reserved automatic group and cannot be used."
536
+ )
537
+ return name
538
+
539
+
540
+ class UserDefinedMentionGroupWriteSerializer(serializers.ModelSerializer[UserDefinedMentionGroup]):
541
+ """Write serializer for create/rename/edit — ``project`` is injected from URL.
542
+
543
+ Membership and mute are managed through dedicated viewset actions (different
544
+ RBAC), so this serializer covers only the group's own attributes.
545
+ """
546
+
547
+ class Meta:
548
+ model = UserDefinedMentionGroup
549
+ fields = ["name", "description", "email_default_on"]
550
+
551
+ def validate_name(self, value: str) -> str:
552
+ name = _validated_mention_group_name(value)
553
+ # Case-insensitive project-uniqueness (the DB constraint is the backstop;
554
+ # this returns a friendly field error instead of a 500 on the race loser).
555
+ project_id = str(self.context.get("project_id"))
556
+ qs = UserDefinedMentionGroup.objects.filter(
557
+ project_id=project_id, name__iexact=name, is_deleted=False
558
+ )
559
+ if self.instance is not None:
560
+ qs = qs.exclude(pk=self.instance.pk)
561
+ if qs.exists():
562
+ raise serializers.ValidationError(
563
+ f"A group named '@{name}' already exists in this project."
564
+ )
565
+ return name
566
+
567
+ def validate_description(self, value: str | None) -> str:
568
+ return (value or "").strip()
569
+
570
+
571
+ class ProgramUserDefinedMentionGroupReadSerializer(
572
+ serializers.ModelSerializer[ProgramUserDefinedMentionGroup]
573
+ ):
574
+ """Response serializer for a program-scoped @mention group (ADR-0248, #516).
575
+
576
+ The program-scoped mirror of :class:`UserDefinedMentionGroupReadSerializer`.
577
+ ``members`` is the curated member set (drawn from across the program's
578
+ projects); ``member_count`` is a list-UI convenience; ``muted_by_me`` reflects
579
+ whether the *requesting* user has muted this group.
580
+ """
581
+
582
+ members = _UserSummarySerializer(many=True, read_only=True)
583
+ member_count = serializers.SerializerMethodField()
584
+ muted_by_me = serializers.SerializerMethodField()
585
+
586
+ def get_member_count(self, obj: ProgramUserDefinedMentionGroup) -> int:
587
+ # members is prefetched by the viewset (it also backs the members field),
588
+ # so counting the loaded rows in Python is free (no extra round trip).
589
+ members = obj.members.all()
590
+ return len(members)
591
+
592
+ def get_muted_by_me(self, obj: ProgramUserDefinedMentionGroup) -> bool:
593
+ user = getattr(self.context.get("request"), "user", None)
594
+ if user is None or not getattr(user, "is_authenticated", False):
595
+ return False
596
+ # muted_by is prefetched by the viewset.
597
+ return any(m.pk == user.pk for m in obj.muted_by.all())
598
+
599
+ class Meta:
600
+ model = ProgramUserDefinedMentionGroup
601
+ fields = [
602
+ "id",
603
+ "server_version",
604
+ "program",
605
+ "name",
606
+ "description",
607
+ "email_default_on",
608
+ "members",
609
+ "member_count",
610
+ "muted_by_me",
611
+ ]
612
+ read_only_fields = fields
613
+
614
+
615
+ class ProgramUserDefinedMentionGroupWriteSerializer(
616
+ serializers.ModelSerializer[ProgramUserDefinedMentionGroup]
617
+ ):
618
+ """Write serializer for program-group create/rename/edit — ``program`` from URL.
619
+
620
+ Membership and mute are managed through dedicated viewset actions (different
621
+ RBAC), so this serializer covers only the group's own attributes.
622
+ """
623
+
624
+ class Meta:
625
+ model = ProgramUserDefinedMentionGroup
626
+ fields = ["name", "description", "email_default_on"]
627
+
628
+ def validate_name(self, value: str) -> str:
629
+ name = _validated_mention_group_name(value)
630
+ # Case-insensitive program-uniqueness (the DB constraint is the backstop;
631
+ # this returns a friendly field error instead of a 500 on the race loser).
632
+ program_id = str(self.context.get("program_id"))
633
+ qs = ProgramUserDefinedMentionGroup.objects.filter(
634
+ program_id=program_id, name__iexact=name, is_deleted=False
635
+ )
636
+ if self.instance is not None:
637
+ qs = qs.exclude(pk=self.instance.pk)
638
+ if qs.exists():
639
+ raise serializers.ValidationError(
640
+ f"A group named '@{name}' already exists in this program."
641
+ )
642
+ return name
643
+
644
+ def validate_description(self, value: str | None) -> str:
645
+ return (value or "").strip()
646
+
647
+
648
+ class ExternalStakeholderSerializer(serializers.ModelSerializer[ExternalStakeholder]):
649
+ """CRUD serializer for a program's external stakeholder registry (#1658, ADR-0264).
650
+
651
+ ``created_by`` is echoed as the adder's display name (never the raw user row);
652
+ ``program`` comes from the URL and is never accepted from the body (IDOR-safe —
653
+ the viewset scopes and stamps it). Case-insensitive per-program email uniqueness
654
+ is validated here so the client gets a friendly field error instead of the DB
655
+ constraint's 500 on the race loser.
656
+ """
657
+
658
+ created_by = serializers.SerializerMethodField()
659
+
660
+ class Meta:
661
+ model = ExternalStakeholder
662
+ fields = ["id", "name", "email", "note", "created_by", "created_at", "updated_at"]
663
+ read_only_fields = ["id", "created_by", "created_at", "updated_at"]
664
+
665
+ @extend_schema_field(serializers.CharField(allow_null=True))
666
+ def get_created_by(self, obj: ExternalStakeholder) -> str | None:
667
+ user = obj.created_by
668
+ if user is None:
669
+ return None
670
+ return display_name_for(user.first_name, user.last_name, user.username)
671
+
672
+ def validate_name(self, value: str) -> str:
673
+ name = value.strip()
674
+ if not name:
675
+ raise serializers.ValidationError("Name cannot be empty.")
676
+ return name
677
+
678
+ def validate_note(self, value: str | None) -> str:
679
+ return (value or "").strip()
680
+
681
+ def validate_email(self, value: str) -> str:
682
+ email = value.strip()
683
+ # Case-insensitive per-program uniqueness across LIVE rows (the DB
684
+ # constraint is the backstop; this returns a friendly 400 on the race loser
685
+ # and on the common re-add-of-existing case).
686
+ program_id = str(self.context.get("program_id"))
687
+ qs = ExternalStakeholder.objects.filter(
688
+ program_id=program_id, email__iexact=email, is_deleted=False
689
+ )
690
+ if self.instance is not None:
691
+ qs = qs.exclude(pk=self.instance.pk)
692
+ if qs.exists():
693
+ raise serializers.ValidationError(
694
+ "A stakeholder with this email already exists in this program."
695
+ )
696
+ return email
697
+
698
+
699
+ def _user_display_name(obj: Any) -> str:
700
+ """First + last name, falling back to ``username`` when both name fields
701
+ are blank — shared by every serializer that surfaces a user's display
702
+ name (#3903)."""
703
+ name = f"{obj.first_name} {obj.last_name}".strip()
704
+ return name if name else obj.username
705
+
706
+
707
+ def _user_initials(obj: Any) -> str:
708
+ """Up to two initials from first/last name, falling back to the first two
709
+ characters of ``username`` when both name fields are blank (#3903)."""
710
+ parts: list[str] = []
711
+ if obj.first_name:
712
+ parts.append(obj.first_name[0].upper())
713
+ if obj.last_name:
714
+ parts.append(obj.last_name[0].upper())
715
+ if parts:
716
+ return "".join(parts[:2])
717
+ return str(obj.username[:2].upper())
718
+
719
+
720
+ class UserSearchResultSerializer(serializers.Serializer[Any]):
721
+ """Read-only serializer for GET /api/v1/users/search/ results (ADR-0061).
722
+
723
+ Deliberately omits ``email`` (#815): returning it let any authenticated caller
724
+ paginate the typeahead to harvest every user's email. Identity for the invite
725
+ typeahead is carried by username + display_name + initials; the endpoint still
726
+ *matches* on email so invite-by-email works, but never echoes the value back.
727
+ """
728
+
729
+ id = serializers.CharField()
730
+ username = serializers.CharField()
731
+ display_name = serializers.SerializerMethodField()
732
+ initials = serializers.SerializerMethodField()
733
+
734
+ def get_display_name(self, obj: Any) -> str:
735
+ return _user_display_name(obj)
736
+
737
+ def get_initials(self, obj: Any) -> str:
738
+ return _user_initials(obj)
739
+
740
+
741
+ class TokenPostureSerializer(serializers.Serializer[Any]):
742
+ """Nested shape of ``MeSerializer.token`` — schema-only, never instantiated
743
+ directly (``MeSerializer.get_token`` returns a plain dict; this class only gives
744
+ ``extend_schema_field`` something faithful to point at, per #2938).
745
+ """
746
+
747
+ scopes = serializers.ListField(
748
+ child=serializers.CharField(), help_text="Scopes this token carries, e.g. ['mcp:read']."
749
+ )
750
+ is_agent = serializers.BooleanField(
751
+ help_text="Whether trueppm_api.apps.projects.models.is_agent_token() treats this "
752
+ "token as MCP/agent traffic — false for a legacy:full personal access token, "
753
+ "which the operator kill switch and team opt-out do not govern."
754
+ )
755
+
756
+
757
+ class MeSerializer(serializers.Serializer[Any]):
758
+ """Read-only serializer for GET /api/v1/auth/me/."""
759
+
760
+ id = serializers.UUIDField()
761
+ username = serializers.CharField()
762
+ display_name = serializers.SerializerMethodField()
763
+ initials = serializers.SerializerMethodField()
764
+ email = serializers.EmailField()
765
+ # Contributor-tier role signal (#855/#856). The web client gates the admin
766
+ # settings nav and the "Signal-only" notification default on this, instead of
767
+ # re-deriving "am I an admin anywhere" by fanning out per-project membership
768
+ # calls. API-first: the tier verdict is a server fact, MCP-reachable.
769
+ # - max_project_role: highest project Role ordinal across the user's
770
+ # memberships (null if they belong to no projects).
771
+ # - workspace_role: the user's *effective* WorkspaceRole ordinal — an
772
+ # explicit membership if present, else the implicit role every
773
+ # authenticated user holds (OWNER for a Django superuser bootstrapping a
774
+ # fresh install, else MEMBER); null only for a deactivated membership.
775
+ # Resolved by workspace.permissions.workspace_role_for_user so this signal
776
+ # can never drift from what workspace RBAC actually enforces.
777
+ # - can_access_admin_settings: true iff Admin+ in any project OR Admin+ at
778
+ # the workspace (the implicit superuser OWNER counts) — the single boolean
779
+ # the settings shell gates on.
780
+ max_project_role = serializers.SerializerMethodField()
781
+ workspace_role = serializers.SerializerMethodField()
782
+ can_access_admin_settings = serializers.SerializerMethodField()
783
+ # Role-based app front door (ADR-0129). The web router reads these and
784
+ # navigates — it holds no role→surface policy itself. API-first: the
785
+ # destination is a server fact, identical for web, mobile, and MCP clients.
786
+ # - default_landing: the user's stored preference ("auto" if unset).
787
+ # - landing: {intent, path, resolved_by} — the resolved front door.
788
+ default_landing = serializers.SerializerMethodField()
789
+ landing = serializers.SerializerMethodField()
790
+ # Per-user nav visibility (ADR-0139). The web shell reads this to hide the
791
+ # view tabs the user opted out of; it is a global per-user list, layered on
792
+ # top of the per-project methodology preset client-side. API-first: the
793
+ # hidden set is a server fact, identical for web, mobile, and MCP clients.
794
+ hidden_views = serializers.SerializerMethodField()
795
+ # Active role-context "lens" (#412, ADR-0162). A presentation-only preference
796
+ # the web shell reads to pick a dual-hat user's default project view and the
797
+ # view-tab emphasis ("pm" / "scrum_master" / "unified"; "unified" if unset).
798
+ # It NEVER gates access — RBAC remains the sole authority; this is read here
799
+ # only so the lens can be reflected without a flash of the wrong view.
800
+ role_context = serializers.SerializerMethodField()
801
+ # Account-wide Do-Not-Disturb (#1707, ADR-0292). Read-only projection so the
802
+ # web current-user query carries the DND state and the bell reflects it with no
803
+ # extra request; the authoritative read/write surface is
804
+ # /api/v1/me/notification-settings/. Non-creating on this hot GET path — the
805
+ # absence of a settings row reads as DND off.
806
+ dnd_enabled = serializers.SerializerMethodField()
807
+ # Personal display frame (#1953, ADR-0410). Read-only projections the web client
808
+ # reads to render timestamps/dates in the viewer's frame. API-first: these are
809
+ # server facts (identical for web, mobile, MCP) but purely presentational — the
810
+ # API itself always emits aware-UTC ISO-8601; an agent ignores them.
811
+ # - timezone: IANA zone for instant timestamps, or "auto" (browser zone).
812
+ # - date_format: style for all displayed dates ("auto"/"iso"/"us"/"eu").
813
+ timezone = serializers.SerializerMethodField()
814
+ date_format = serializers.SerializerMethodField()
815
+ # Caller's own credential posture (#2938). ``is_agent_token()`` decides on every
816
+ # request whether the caller is governed by the MCP control layer (kill switch,
817
+ # team opt-out, row filtering) but reports it nowhere — so a member who points
818
+ # trueppm-mcp at a personal legacy:full token has no way to learn the switches
819
+ # they think are protecting them do not apply. This is the fix: the one endpoint
820
+ # the MCP client already calls at boot (verify_auth) now echoes the caller's own
821
+ # scopes and agent posture. Disclosing it leaks nothing — it is the caller's own
822
+ # credential. Null for a session/JWT caller, which carries no token at all.
823
+ token = serializers.SerializerMethodField()
824
+
825
+ def get_max_project_role(self, obj: Any) -> int | None:
826
+ # Memoized: get_can_access_admin_settings also needs this, so without the
827
+ # cache /auth/me would run the same aggregate twice per response.
828
+ if not hasattr(self, "_max_project_role"):
829
+ from django.db.models import Max
830
+
831
+ value = ProjectMembership.objects.filter(user=obj, is_deleted=False).aggregate(
832
+ _max=Max("role")
833
+ )["_max"]
834
+ self._max_project_role: int | None = int(value) if value is not None else None
835
+ return self._max_project_role
836
+
837
+ def get_workspace_role(self, obj: Any) -> int | None:
838
+ # Memoized: get_can_access_admin_settings also reads this. Delegates to the
839
+ # canonical resolver so the superuser-bootstrap and deactivated-status
840
+ # rules match what workspace RBAC enforces (no shadow copy — ADR-0087 §6).
841
+ if not hasattr(self, "_workspace_role"):
842
+ from trueppm_api.apps.workspace.permissions import workspace_role_for_user
843
+
844
+ self._workspace_role: int | None = workspace_role_for_user(obj)
845
+ return self._workspace_role
846
+
847
+ def get_can_access_admin_settings(self, obj: Any) -> bool:
848
+ from trueppm_api.apps.workspace.models import WorkspaceRole
849
+
850
+ proj = self.get_max_project_role(obj)
851
+ ws = self.get_workspace_role(obj)
852
+ return (proj is not None and proj >= Role.ADMIN) or (
853
+ ws is not None and ws >= WorkspaceRole.ADMIN
854
+ )
855
+
856
+ def _prefs(self, obj: Any) -> tuple[str, list[str], str, str, str]:
857
+ # Memoized single read of (default_landing, hidden_views, role_context,
858
+ # timezone, date_format): every pref getter needs a UserProfile column, so
859
+ # reading them in one .only() query keeps /auth/me at one profile read
860
+ # regardless of how many fields consume it.
861
+ if not hasattr(self, "_prefs_cache"):
862
+ from trueppm_api.apps.profiles.services import get_profile_prefs
863
+
864
+ self._prefs_cache: tuple[str, list[str], str, str, str] = get_profile_prefs(obj)
865
+ return self._prefs_cache
866
+
867
+ def get_default_landing(self, obj: Any) -> str:
868
+ return self._prefs(obj)[0]
869
+
870
+ @extend_schema_field(
871
+ inline_serializer(
872
+ "Landing",
873
+ {
874
+ "intent": serializers.CharField(),
875
+ "path": serializers.CharField(),
876
+ "resolved_by": serializers.CharField(),
877
+ },
878
+ )
879
+ )
880
+ def get_landing(self, obj: Any) -> dict[str, str]:
881
+ from trueppm_api.apps.profiles.services import resolve_landing
882
+
883
+ # Reuse the already-computed preference and max project role so the
884
+ # resolver doesn't re-query UserProfile / re-aggregate Max(role) — both
885
+ # are memoized above and computed for sibling fields on this same request.
886
+ landing = resolve_landing(
887
+ obj,
888
+ pref=self.get_default_landing(obj),
889
+ max_role=self.get_max_project_role(obj),
890
+ )
891
+ return {
892
+ "intent": landing.intent,
893
+ "path": landing.path,
894
+ "resolved_by": landing.resolved_by,
895
+ }
896
+
897
+ @extend_schema_field(serializers.ListField(child=serializers.CharField()))
898
+ def get_hidden_views(self, obj: Any) -> list[str]:
899
+ return self._prefs(obj)[1]
900
+
901
+ # Reuse the model's own choices (not a hardcoded copy) so this enum is
902
+ # byte-identical to UserProfileSerializer.role_context — drf-spectacular then
903
+ # collapses both into one shared ``RoleContextEnum`` component instead of
904
+ # emitting divergent ``MeRoleContextEnum`` / ``UserProfileRoleContextEnum``
905
+ # duplicates for the same pm/scrum_master/unified set.
906
+ @extend_schema_field(serializers.ChoiceField(choices=RoleContext.choices))
907
+ def get_role_context(self, obj: Any) -> str:
908
+ return self._prefs(obj)[2]
909
+
910
+ @extend_schema_field(serializers.CharField())
911
+ def get_timezone(self, obj: Any) -> str:
912
+ return self._prefs(obj)[3]
913
+
914
+ # Reuse the model's own choices (not a hardcoded copy) so drf-spectacular
915
+ # collapses this and UserProfileSerializer.date_format into one shared
916
+ # ``DateFormatEnum`` component instead of divergent Me/UserProfile duplicates.
917
+ @extend_schema_field(serializers.ChoiceField(choices=DateFormat.choices))
918
+ def get_date_format(self, obj: Any) -> str:
919
+ return self._prefs(obj)[4]
920
+
921
+ @extend_schema_field(serializers.BooleanField())
922
+ def get_dnd_enabled(self, obj: Any) -> bool:
923
+ from trueppm_api.apps.notifications.models import UserNotificationSettings
924
+
925
+ return (
926
+ UserNotificationSettings.objects.filter(user=obj)
927
+ .values_list("dnd_enabled", flat=True)
928
+ .first()
929
+ or False
930
+ )
931
+
932
+ @extend_schema_field(TokenPostureSerializer(allow_null=True))
933
+ def get_token(self, obj: Any) -> dict[str, Any] | None:
934
+ from trueppm_api.apps.projects.models import ApiToken, is_agent_token
935
+
936
+ request = self.context.get("request")
937
+ auth = getattr(request, "auth", None) if request is not None else None
938
+ if not isinstance(auth, ApiToken):
939
+ return None
940
+ return {"scopes": list(auth.scopes or []), "is_agent": is_agent_token(auth)}
941
+
942
+ def get_display_name(self, obj: Any) -> str:
943
+ return _user_display_name(obj)
944
+
945
+ def get_initials(self, obj: Any) -> str:
946
+ return _user_initials(obj)