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,930 @@
1
+ """REST views for the sync app — offline delta pull endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import partial
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ from django.conf import settings
9
+ from django.db import IntegrityError, transaction
10
+ from django.db.models import BooleanField, Prefetch
11
+ from django.db.models.expressions import RawSQL
12
+ from drf_spectacular.types import OpenApiTypes
13
+ from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
14
+ from rest_framework import serializers, status
15
+ from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
16
+ from rest_framework.permissions import IsAuthenticated
17
+ from rest_framework.request import Request
18
+ from rest_framework.response import Response
19
+ from rest_framework.throttling import BaseThrottle, ScopedRateThrottle
20
+ from rest_framework.views import APIView
21
+
22
+ from trueppm_api.apps.access.models import ProgramMembership, ProjectMembership, Role
23
+ from trueppm_api.apps.access.permissions import _membership_role
24
+ from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
25
+ from trueppm_api.apps.integrations.models import TaskLink
26
+ from trueppm_api.apps.projects.models import (
27
+ Calendar,
28
+ Dependency,
29
+ Label,
30
+ Program,
31
+ Project,
32
+ RetroActionItem,
33
+ RetroVisibility,
34
+ Risk,
35
+ Sprint,
36
+ SprintRetro,
37
+ Task,
38
+ TaskCustomFieldValue,
39
+ TaskRecurrenceRule,
40
+ TaskRelation,
41
+ TaskSuggestedAssignee,
42
+ )
43
+ from trueppm_api.apps.sync.pagination import SyncCursor, SyncSource, paginate_changes
44
+ from trueppm_api.apps.sync.serializers import (
45
+ SyncCalendarSerializer,
46
+ SyncDependencySerializer,
47
+ SyncLabelSerializer,
48
+ SyncMembershipSerializer,
49
+ SyncProgramMembershipSerializer,
50
+ SyncProgramSerializer,
51
+ SyncProjectSerializer,
52
+ SyncPullResponseSerializer,
53
+ SyncRetroActionItemSerializer,
54
+ SyncRiskSerializer,
55
+ SyncSprintRetroSerializer,
56
+ SyncSprintSerializer,
57
+ SyncTaskLinkSerializer,
58
+ SyncTaskRecurrenceRuleSerializer,
59
+ SyncTaskRelationSerializer,
60
+ SyncTaskSerializer,
61
+ SyncTaskSuggestedAssigneeSerializer,
62
+ SyncTimeEntrySerializer,
63
+ SyncUploadRequestSerializer,
64
+ SyncUploadResponseSerializer,
65
+ )
66
+ from trueppm_api.apps.sync.ws_auth import TICKET_TTL_SECONDS, issue_ticket
67
+ from trueppm_api.apps.timetracking.models import TimeEntry
68
+
69
+ if TYPE_CHECKING:
70
+ from django.contrib.auth.models import User as _User
71
+
72
+
73
+ class WebSocketTicketView(IdempotencyMixin, APIView):
74
+ """Mint a short-lived, single-use ticket for the WebSocket handshake (ADR-0141).
75
+
76
+ Browsers cannot set an ``Authorization`` header on a WebSocket upgrade, so the
77
+ credential must ride in the URL. A raw JWT there leaks into every access log
78
+ (#818); a ticket that is single-use and expires in
79
+ :data:`~trueppm_api.apps.sync.ws_auth.TICKET_TTL_SECONDS` seconds is useless
80
+ once logged. The client POSTs here, then opens the socket with ``?ticket=``.
81
+
82
+ Authentication only — authorization (project membership/role) is enforced by
83
+ the consumer on connect. No project scope is required to mint a ticket.
84
+ """
85
+
86
+ permission_classes = [IsAuthenticated]
87
+ throttle_classes = [ScopedRateThrottle]
88
+ throttle_scope = "ws_ticket"
89
+ # Stateless mint — the generic Idempotency-Key path adds nothing.
90
+ idempotency_exempt = True
91
+
92
+ @extend_schema(
93
+ summary="Issue a WebSocket connection ticket",
94
+ responses={
95
+ 200: inline_serializer(
96
+ name="WebSocketTicketResponse",
97
+ fields={
98
+ "ticket": serializers.CharField(
99
+ help_text="Single-use opaque ticket to pass as the "
100
+ "`?ticket=` query parameter when opening the socket.",
101
+ ),
102
+ "expires_in": serializers.IntegerField(
103
+ help_text="Seconds until the ticket expires.",
104
+ ),
105
+ },
106
+ ),
107
+ },
108
+ description="Issue a single-use WebSocket connection ticket (ADR-0141).",
109
+ )
110
+ def post(self, request: Request) -> Response:
111
+ ticket = issue_ticket(str(request.user.pk))
112
+ return Response({"ticket": ticket, "expires_in": TICKET_TTL_SECONDS})
113
+
114
+
115
+ class ProjectSyncView(IdempotencyMixin, APIView):
116
+ """Pull-only delta sync endpoint for the mobile offline store.
117
+
118
+ Returns all rows (live and soft-deleted) whose ``sync_seq`` is strictly
119
+ greater than `since` for the given project, formatted for WatermelonDB's
120
+ synchronize() helper.
121
+
122
+ ``sync_seq`` — not ``server_version`` — is the cursor (ADR-0686, #2491). It is
123
+ drawn from the project's own monotonic sequence, so every synced row in the
124
+ project is totally ordered against every other one and the project-wide
125
+ maximum is a meaningful checkpoint. ``server_version`` counts a single row's
126
+ saves; comparing it against that maximum let a frequently-edited row raise the
127
+ checkpoint above every other row's counter and drop those rows from the delta
128
+ permanently.
129
+
130
+ The response ``timestamp`` is read *before* the delta queries, so a write that
131
+ lands during the pull always draws a cursor above it. Such a row is delivered by
132
+ this response or the next one — never dropped — at the cost of an occasional
133
+ re-delivery, which WatermelonDB's upsert absorbs. Clients therefore need no
134
+ retry or reconciliation logic for a mid-pull write; adopt ``timestamp`` as the
135
+ next ``since`` and the delta is complete. It is read once per *paging session*,
136
+ not per request: continuation pages echo the value pinned in the cursor (#2568).
137
+ See the comment at the watermark read for why the ordering, and not a transaction
138
+ isolation level, is what guarantees this.
139
+
140
+ Usage:
141
+ GET /api/v1/projects/{pk}/sync/?since=0
142
+ """
143
+
144
+ # Exempt from the generic Idempotency-Key path (ADR-0170): the upload POST is already
145
+ # idempotent by client_batch_id (SyncBatch dedup, ADR-0082); the generic header would
146
+ # be redundant and the sync protocol uses its own batch id.
147
+ idempotency_exempt = True
148
+ permission_classes = [IsAuthenticated]
149
+
150
+ @extend_schema(
151
+ summary="Pull the offline sync delta for a project",
152
+ parameters=[
153
+ OpenApiParameter(
154
+ name="since",
155
+ type=OpenApiTypes.INT,
156
+ location=OpenApiParameter.QUERY,
157
+ required=False,
158
+ default=0,
159
+ description=(
160
+ "WatermelonDB high-water mark — an opaque cursor, not a row "
161
+ "version. Returns only rows changed strictly after this point. "
162
+ "Omit (or pass `0`) for a full initial sync; passing the "
163
+ "previous response's `timestamp` fetches just the delta since "
164
+ "then. Omitting it silently triggers a full re-sync."
165
+ ),
166
+ ),
167
+ OpenApiParameter(
168
+ name="cursor",
169
+ type=OpenApiTypes.STR,
170
+ location=OpenApiParameter.QUERY,
171
+ required=False,
172
+ description=(
173
+ "Opaque pagination continuation token (#1013). Omit on the "
174
+ "first request; on each subsequent request pass the previous "
175
+ "response's `next_cursor`. Keep `since` constant for the whole "
176
+ "paging session. Loop until `next_cursor` is null / `has_more` "
177
+ "is false, then adopt `timestamp` as the next `since`."
178
+ ),
179
+ ),
180
+ OpenApiParameter(
181
+ name="page_size",
182
+ type=OpenApiTypes.INT,
183
+ location=OpenApiParameter.QUERY,
184
+ required=False,
185
+ description=(
186
+ "Maximum rows returned across all collections in one page. "
187
+ "Defaults to `TRUEPPM_SYNC_PULL_PAGE_SIZE`; clamped to "
188
+ "`TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE`."
189
+ ),
190
+ ),
191
+ ],
192
+ responses={200: SyncPullResponseSerializer},
193
+ )
194
+ def get(self, request: Request, pk: str) -> Response:
195
+ # Validate `since` parameter.
196
+ since_raw = request.query_params.get("since", "0")
197
+ try:
198
+ since = int(since_raw)
199
+ if since < 0:
200
+ raise ValueError
201
+ except (ValueError, TypeError) as err:
202
+ raise ValidationError({"since": "Must be a non-negative integer."}) from err
203
+
204
+ # Validate the optional continuation cursor and page size (#1013).
205
+ cursor_raw = request.query_params.get("cursor")
206
+ cursor = SyncCursor.decode(cursor_raw) if cursor_raw else None
207
+ page_size = self._resolve_page_size(request)
208
+
209
+ # Resolve project — 404 if missing or soft-deleted.
210
+ try:
211
+ project = Project.objects.get(pk=pk, is_deleted=False)
212
+ except Project.DoesNotExist as err:
213
+ raise NotFound("Project not found.") from err
214
+
215
+ # RBAC — any project member (Viewer+) may pull.
216
+ caller_role = _membership_role(request, project.pk)
217
+ if caller_role is None:
218
+ from rest_framework.exceptions import PermissionDenied
219
+
220
+ raise PermissionDenied("You must be a member of this project.")
221
+
222
+ # Read the high-water mark BEFORE the delta queries. This ORDERING is the
223
+ # whole guarantee — not an isolation level. ATOMIC_REQUESTS does wrap this
224
+ # request in a transaction, but at PostgreSQL's default READ COMMITTED, and
225
+ # nothing here or in settings raises that: every statement below still takes
226
+ # its own fresh snapshot, so being in a transaction pins nothing. REPEATABLE
227
+ # READ would add nothing either, and an earlier version of this comment
228
+ # claimed a wrapper that never existed at all (#2613).
229
+ #
230
+ # Two facts close the TOCTOU window:
231
+ #
232
+ # 1. A write landing after this read draws sync_seq > timestamp, so it is
233
+ # either carried by this response anyway (harmless — upsert) or caught
234
+ # by the next pull, whose `since` is this timestamp. Reading the
235
+ # watermark first errs toward re-delivery; reading it last is lossy.
236
+ # 2. A write that already committed is fully visible. Allocation UPDATEs
237
+ # the project row and holds its write lock to commit (sync/sequence.py),
238
+ # so allocations are totally ordered by commit: observing
239
+ # last_sync_version == W means every allocation <= W has committed —
240
+ # each one together with the row it stamped.
241
+ #
242
+ # Fact 2 rests on a WRITER-side precondition: the allocator bump and the row
243
+ # write must land in one transaction. ATOMIC_REQUESTS supplies it for every
244
+ # HTTP write and the batch writers open an explicit atomic(); a synced write
245
+ # in autocommit would violate it, and no reader isolation level can
246
+ # compensate — a snapshot cannot reveal an uncommitted row whose already-
247
+ # committed allocator bump the reader can see. Tracked in #2617.
248
+ #
249
+ # The value is taken off the Project row fetched above rather than re-read,
250
+ # so the snapshot instant is the RBAC fetch — earlier still, which only
251
+ # widens the margin. Do NOT add a refresh_from_db() here: re-reading the
252
+ # allocator after the delta queries is exactly the lossy order, and would
253
+ # publish a checkpoint over rows this response could not carry.
254
+ #
255
+ # One watermark per *session*, not per request (#2568) — which preserves the
256
+ # ordering above rather than weakening it: a continuation page's checkpoint was
257
+ # read before page 1's delta queries, so it precedes every query in the drain.
258
+ # On a continuation page the checkpoint is read back off the cursor: recomputing
259
+ # it would
260
+ # publish a value above rows written mid-drain into a collection the pager
261
+ # had already passed, and the client adopting the last page's `timestamp`
262
+ # would filter those rows out of every future pull. Pinning page 1's value
263
+ # errs the safe way — such rows simply arrive on the next pull, and rows
264
+ # this drain did deliver above the pin are re-delivered under upsert.
265
+ #
266
+ # The cursor is unsigned, so `w` is also ceiling-checked against the live
267
+ # sequence. `i` and `v` are stream *positions* the pager re-derives and
268
+ # bounds naturally, but `w` is a checkpoint the client is told to persist
269
+ # and echo back as `since` indefinitely — a junk value is permanent for
270
+ # that device rather than transient. The check is free here (the watermark
271
+ # is an attribute of the project row already fetched above) and rejects
272
+ # nothing legitimate, because the sequence only ever increases. It is
273
+ # deliberately NOT mirrored in UserProgramSyncView, whose watermark costs
274
+ # two aggregates over the caller's whole accessible set: paying them per
275
+ # page would reinstate the O(pages x rows) cost this fix removed, to guard
276
+ # a value that can only ever corrupt the tampering client's own cursor.
277
+ if cursor is not None:
278
+ timestamp = cursor.watermark
279
+ if timestamp > self._watermark(project):
280
+ raise ValidationError({"cursor": "Malformed pagination cursor."})
281
+ else:
282
+ timestamp = self._watermark(project)
283
+
284
+ # Retros are visibility-gated per ADR-0071 §3. A VIEWER on a TEAM_ONLY
285
+ # retro does not receive the retro's raw notes — the sync filters them
286
+ # out at the queryset level. The sync protocol delivers tombstones for
287
+ # visibility-removed rows so WatermelonDB can drop them client-side.
288
+ retro_qs = SprintRetro.objects.filter(sprint__project=project, sync_seq__gt=since)
289
+ if caller_role < Role.MEMBER:
290
+ retro_qs = retro_qs.exclude(team_visibility=RetroVisibility.TEAM_ONLY)
291
+
292
+ retro_action_item_qs = RetroActionItem.objects.filter(
293
+ retro__sprint__project=project, sync_seq__gt=since
294
+ ).select_related("retro")
295
+ if caller_role < Role.MEMBER:
296
+ retro_action_item_qs = retro_action_item_qs.exclude(
297
+ retro__team_visibility=RetroVisibility.TEAM_ONLY
298
+ )
299
+
300
+ # Ordered sync sources: (collection, base queryset, serializer). Each base
301
+ # queryset carries the project scope, the `sync_seq__gt=since` floor,
302
+ # and any RBAC/visibility filters. The pager (#1013) drains them in this
303
+ # fixed order, keyset-seeking on (sync_seq, id) within each, so the
304
+ # response is bounded by page_size without skipping or duplicating rows.
305
+ # The order is stable across pages — do not reorder without a protocol bump.
306
+ sources: list[SyncSource] = [
307
+ (
308
+ "projects",
309
+ Project.objects.filter(pk=project.pk, sync_seq__gt=since),
310
+ SyncProjectSerializer,
311
+ ),
312
+ (
313
+ "tasks",
314
+ # is_phase annotation (ADR-0293): EXISTS a direct non-subtask child.
315
+ # Mirrors the projects app's annotate_tasks_queryset is_phase shape so
316
+ # offline clients receive the same computed rollup flag. Static SQL
317
+ # literal, empty params — no user input interpolated; the ltree
318
+ # operator can't be expressed in the ORM.
319
+ Task.objects.filter(project=project, sync_seq__gt=since)
320
+ .annotate(
321
+ # nosemgrep: avoid-raw-sql
322
+ is_phase=RawSQL(
323
+ "EXISTS("
324
+ " SELECT 1 FROM projects_task c"
325
+ " WHERE c.project_id = projects_task.project_id"
326
+ " AND c.is_deleted = false"
327
+ " AND c.is_subtask = false"
328
+ " AND c.id != projects_task.id"
329
+ " AND c.wbs_path IS NOT NULL"
330
+ " AND projects_task.wbs_path IS NOT NULL"
331
+ " AND c.wbs_path ~ (projects_task.wbs_path::text || '.*{1}')::lquery"
332
+ ")",
333
+ [],
334
+ output_field=BooleanField(),
335
+ )
336
+ )
337
+ .prefetch_related(
338
+ # label_ids array (ADR-0400) — filter tombstoned labels so a
339
+ # deleted label never rides a task's sync payload; O(1) per page.
340
+ Prefetch("labels", queryset=Label.objects.filter(is_deleted=False)),
341
+ # custom_fields map (#2143, ADR-0528) rides the task like label_ids;
342
+ # select_related feeds the field-type resolver + person avatar O(1).
343
+ Prefetch(
344
+ "custom_field_values",
345
+ queryset=TaskCustomFieldValue.objects.select_related("field", "value_user"),
346
+ ),
347
+ ),
348
+ SyncTaskSerializer,
349
+ ),
350
+ (
351
+ "dependencies",
352
+ # Same-project edges only. Cross-project edges (ADR-0120) are
353
+ # deliberately excluded from the single-project offline delta:
354
+ # their successor task lives in a project this client may not have
355
+ # synced, and the offline WASM recompute is single-project. Offline
356
+ # cross-project scheduling arrives with the program-sync slice
357
+ # (D3/D4); until then the sync payload stays byte-identical to the
358
+ # pre-cross-project behaviour.
359
+ Dependency.objects.filter(
360
+ predecessor__project=project,
361
+ successor__project=project,
362
+ sync_seq__gt=since,
363
+ ).select_related("predecessor"),
364
+ SyncDependencySerializer,
365
+ ),
366
+ (
367
+ "calendars",
368
+ # prefetch_related("exceptions") feeds the nested read-only
369
+ # exceptions array on SyncCalendarSerializer (ADR-0193) without a
370
+ # per-calendar lazy load — parity with CalendarViewSet and
371
+ # N+1-safe for the future program-sync multi-calendar slice.
372
+ Calendar.objects.filter(
373
+ pk=project.calendar_id, sync_seq__gt=since
374
+ ).prefetch_related("exceptions")
375
+ if project.calendar_id
376
+ else Calendar.objects.none(),
377
+ SyncCalendarSerializer,
378
+ ),
379
+ (
380
+ "memberships",
381
+ ProjectMembership.objects.filter(project=project, sync_seq__gt=since),
382
+ SyncMembershipSerializer,
383
+ ),
384
+ (
385
+ "risks",
386
+ Risk.objects.filter(project=project, sync_seq__gt=since).prefetch_related("tasks"),
387
+ SyncRiskSerializer,
388
+ ),
389
+ (
390
+ "sprints",
391
+ Sprint.objects.filter(project=project, sync_seq__gt=since),
392
+ SyncSprintSerializer,
393
+ ),
394
+ ("sprint_retros", retro_qs, SyncSprintRetroSerializer),
395
+ ("retro_action_items", retro_action_item_qs, SyncRetroActionItemSerializer),
396
+ (
397
+ "task_suggested_assignees",
398
+ TaskSuggestedAssignee.objects.filter(task__project=project, sync_seq__gt=since),
399
+ SyncTaskSuggestedAssigneeSerializer,
400
+ ),
401
+ (
402
+ "task_links",
403
+ TaskLink.objects.filter(task__project=project, sync_seq__gt=since),
404
+ SyncTaskLinkSerializer,
405
+ ),
406
+ (
407
+ "task_recurrence_rules",
408
+ TaskRecurrenceRule.objects.filter(
409
+ task__project=project, sync_seq__gt=since
410
+ ).select_related("task"),
411
+ SyncTaskRecurrenceRuleSerializer,
412
+ ),
413
+ (
414
+ # Time entries are scoped to the CALLER's own rows (ADR-0185 §6): the
415
+ # per-project delta never leaks a colleague's entries, mirroring the
416
+ # /me/ REST surface's non-surveillance discipline. A soft-deleted entry
417
+ # yields a tombstone through the standard _collect path.
418
+ "time_entries",
419
+ TimeEntry.objects.filter(
420
+ task__project=project,
421
+ user=cast("_User", request.user),
422
+ sync_seq__gt=since,
423
+ ).select_related("task"),
424
+ SyncTimeEntrySerializer,
425
+ ),
426
+ (
427
+ # Label catalog (ADR-0400) — appended to preserve the stable protocol
428
+ # order. Assignment is NOT a source here; it rides Task.label_ids.
429
+ "labels",
430
+ Label.objects.filter(project=project, sync_seq__gt=since),
431
+ SyncLabelSerializer,
432
+ ),
433
+ (
434
+ # Informational task relations (ADR-0455) — appended LAST to preserve
435
+ # the stable protocol order. Same-project relations only: a
436
+ # cross-project relation's counterpart task may live in a project
437
+ # this client has not synced (mirrors the Dependency same-project
438
+ # scoping above). Pull-only, like Dependency — never in the upload
439
+ # WRITABLE_COLLECTIONS. The delta joins via ``source`` since
440
+ # TaskRelation carries no direct project FK.
441
+ "task_relations",
442
+ TaskRelation.objects.filter(
443
+ source__project=project,
444
+ target__project=project,
445
+ sync_seq__gt=since,
446
+ ).select_related("source"),
447
+ SyncTaskRelationSerializer,
448
+ ),
449
+ ]
450
+
451
+ changes, next_cursor, has_more = paginate_changes(
452
+ sources,
453
+ cursor=cursor,
454
+ page_size=page_size,
455
+ collect=self._collect,
456
+ watermark=timestamp,
457
+ version_field="sync_seq",
458
+ )
459
+
460
+ return Response(
461
+ {
462
+ "changes": changes,
463
+ "timestamp": timestamp,
464
+ "next_cursor": next_cursor.encode() if next_cursor is not None else None,
465
+ "has_more": has_more,
466
+ }
467
+ )
468
+
469
+ @staticmethod
470
+ def _resolve_page_size(request: Request) -> int:
471
+ """Clamp the requested ``page_size`` to ``[1, MAX]`` with a sane default (#1013).
472
+
473
+ A missing or non-numeric value falls back to the default rather than 400ing:
474
+ the pager is a transport optimization, not a contract the client must satisfy.
475
+ """
476
+ default = getattr(settings, "TRUEPPM_SYNC_PULL_PAGE_SIZE", 1000)
477
+ maximum = getattr(settings, "TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE", 5000)
478
+ raw = request.query_params.get("page_size")
479
+ if raw is None:
480
+ return default
481
+ try:
482
+ requested = int(raw)
483
+ except (ValueError, TypeError):
484
+ return default
485
+ return max(1, min(requested, maximum))
486
+
487
+ def get_throttles(self) -> list[BaseThrottle]:
488
+ """POST gets the dedicated upload throttle; GET keeps the default `user`
489
+ scope (#2939 — same defect class #2878 fixed in four other views: a bare
490
+ list here *replaces* ``DEFAULT_THROTTLE_CLASSES`` instead of adding to it,
491
+ so this branch resolved to no throttle at all on the delta-export pull,
492
+ the largest read surface in the API). Mobile sync is high-frequency by
493
+ design, so it is bounded by the generous default (1000/min per account,
494
+ ``TRUEPPM_THROTTLE_USER_RATE``) rather than a dedicated tighter scope —
495
+ that is a real decision, not an oversight, and now it is enforced.
496
+ """
497
+ if self.request.method == "POST":
498
+ from trueppm_api.apps.sync.throttles import SyncUploadThrottle
499
+
500
+ return [SyncUploadThrottle()]
501
+ return super().get_throttles()
502
+
503
+ @extend_schema(
504
+ request=SyncUploadRequestSerializer,
505
+ responses={
506
+ 200: SyncUploadResponseSerializer,
507
+ 409: OpenApiTypes.OBJECT,
508
+ },
509
+ description=(
510
+ "Upload a WatermelonDB delta batch. Carries a client_batch_id for "
511
+ "all-or-nothing, idempotent replay (ADR-0082)."
512
+ ),
513
+ )
514
+ def post(self, request: Request, pk: str) -> Response:
515
+ """Upload a WatermelonDB delta batch with transactional atomicity (ADR-0082).
516
+
517
+ The batch carries a client-generated ``client_batch_id``. The first
518
+ request to win the unique-constraint race applies the delta and records
519
+ the batch + its response in one atomic transaction (all-or-nothing); a
520
+ retry carrying the same id within the freshness window replays the stored
521
+ response without re-applying. See ADR-0082 §D for the full algorithm.
522
+ """
523
+ from trueppm_api.apps.access.models import Role
524
+ from trueppm_api.apps.sync.models import SyncBatch, SyncBatchStatus
525
+ from trueppm_api.apps.sync.throttles import sync_batch_concurrency_slot
526
+
527
+ envelope = SyncUploadRequestSerializer(data=request.data)
528
+ envelope.is_valid(raise_exception=True)
529
+ client_batch_id = envelope.validated_data["client_batch_id"]
530
+ # The high-water mark the client last pulled — the default per-row base for the
531
+ # field-level conflict guard (#1718). Advisory only: absent → last-writer-wins.
532
+ last_pulled_at = envelope.validated_data.get("last_pulled_at")
533
+ # Feed the RAW changes map (not envelope.validated_data["changes"]) to
534
+ # apply_task_changes so it stays the single authority for the writable-
535
+ # collection whitelist (ADR-0082 §B). The typed nested serializer (#786)
536
+ # documents the shape but ignores unknown collection keys; passing the raw
537
+ # map keeps the explicit "unsupported collection → 400" guard byte-identical
538
+ # for existing clients rather than silently dropping the foreign collection.
539
+ # The envelope serializer above already rejected a non-mapping body with a
540
+ # 400 ("Expected a dictionary, but got list"), so the raw body is provably a
541
+ # mapping by here — narrowed rather than re-guarded.
542
+ changes = cast("dict[str, Any]", request.data)["changes"]
543
+
544
+ try:
545
+ project = Project.objects.get(pk=pk, is_deleted=False)
546
+ except Project.DoesNotExist as err:
547
+ raise NotFound("Project not found.") from err
548
+
549
+ # RBAC: only project members with a write role may push. Per-row
550
+ # ownership (Member edits own tasks; Scheduler cannot edit content) is
551
+ # enforced inside apply_task_changes, mirroring IsProjectMemberWriteOrOwn.
552
+ role = _membership_role(request, project.pk)
553
+ if role is None:
554
+ raise PermissionDenied("You must be a member of this project.")
555
+ if role < Role.MEMBER:
556
+ raise PermissionDenied("You need at least Team Member role to upload changes.")
557
+
558
+ # Archived projects are hard read-only (#530) — mirror IsProjectNotArchived,
559
+ # which gates every REST write. Without this, the upload path would be a
560
+ # back-channel to mutate an archived project.
561
+ if project.is_archived:
562
+ raise PermissionDenied(
563
+ "This project is archived and cannot be modified. Unarchive it first."
564
+ )
565
+
566
+ ttl = getattr(settings, "TRUEPPM_SYNC_BATCH_RETENTION_HOURS", 24)
567
+
568
+ # Fast path: a fresh, completed duplicate replays its stored response.
569
+ # Scoped to (project, actor) so a member can never replay another
570
+ # project's — or another *user's* — batch by reusing its client_batch_id.
571
+ # The stored response carries that actor's task ids, server_versions, and
572
+ # watermark; cross-actor replay would leak it (#894, #887 IDOR guard).
573
+ existing = SyncBatch.objects.filter( # type: ignore[misc]
574
+ project=project, actor_user=request.user, client_batch_id=client_batch_id
575
+ ).first()
576
+ if (
577
+ existing is not None
578
+ and existing.status == SyncBatchStatus.COMPLETED
579
+ and existing.is_fresh(ttl_hours=ttl)
580
+ ):
581
+ return Response(existing.response_body, status=existing.response_status)
582
+
583
+ # Bound simultaneously in-flight heavy batch applies for this user (#1756).
584
+ # Wraps the whole apply — including the IntegrityError re-run — so one
585
+ # logical upload holds exactly one slot. Placed AFTER the fast-path replay
586
+ # so a cheap idempotent retry never consumes a slot. Raises 429 (Throttled)
587
+ # when the per-user concurrency cap is exceeded; fails open on Redis error.
588
+ with sync_batch_concurrency_slot(request.user.pk):
589
+ try:
590
+ return self._apply_and_record(
591
+ request, project, role, client_batch_id, changes, last_pulled_at
592
+ )
593
+ except IntegrityError:
594
+ # A concurrent duplicate committed between our check and create, or a
595
+ # stale row exists. Re-fetch (project + actor-scoped) and resolve.
596
+ existing = SyncBatch.objects.filter( # type: ignore[misc]
597
+ project=project, actor_user=request.user, client_batch_id=client_batch_id
598
+ ).first()
599
+ if existing is None:
600
+ raise # genuinely unexpected — surface it
601
+ if existing.is_fresh(ttl_hours=ttl):
602
+ if existing.status == SyncBatchStatus.COMPLETED:
603
+ return Response(existing.response_body, status=existing.response_status)
604
+ # Fresh but still pending: a true concurrent race whose winner has
605
+ # not committed yet. Ask the client to retry — the next attempt
606
+ # hits the fast-path replay.
607
+ return Response(
608
+ {"detail": "This batch is already being processed; retry shortly."},
609
+ status=status.HTTP_409_CONFLICT,
610
+ )
611
+ # Expired row is blocking a re-run: drop it and apply once more. After
612
+ # the freshness window the same client_batch_id is allowed to re-run.
613
+ existing.delete()
614
+ return self._apply_and_record(
615
+ request, project, role, client_batch_id, changes, last_pulled_at
616
+ )
617
+
618
+ def _apply_and_record(
619
+ self,
620
+ request: Request,
621
+ project: Project,
622
+ role: int,
623
+ client_batch_id: Any,
624
+ changes: dict[str, Any],
625
+ last_pulled_at: int | None = None,
626
+ ) -> Response:
627
+ """Apply the delta and snapshot the response inside one atomic batch.
628
+
629
+ The SyncBatch row is the **first** write so the unique constraint
630
+ serializes concurrent duplicates. Any failure (RBAC, validation, DB)
631
+ rolls back the row writes *and* the batch row together — nothing commits,
632
+ and the client re-uploads the whole batch under the same id.
633
+ """
634
+ from trueppm_api.apps.scheduling.services import enqueue_recalculate
635
+ from trueppm_api.apps.sync.broadcast import broadcast_board_event
636
+ from trueppm_api.apps.sync.models import SyncBatch, SyncBatchStatus
637
+ from trueppm_api.apps.sync.sequence import coalesce_sync_seq
638
+ from trueppm_api.apps.sync.upload import apply_task_changes
639
+
640
+ with transaction.atomic():
641
+ batch = SyncBatch.objects.create(
642
+ client_batch_id=client_batch_id,
643
+ project=project,
644
+ actor_user=request.user, # type: ignore[misc]
645
+ status=SyncBatchStatus.PENDING,
646
+ )
647
+ # Draw one sync_seq per project for the whole batch rather than one
648
+ # per row (#1527, ADR-0686). The allocations run inside this atomic
649
+ # block, so a rollback discards them with the rows.
650
+ with coalesce_sync_seq():
651
+ applied = apply_task_changes(
652
+ project=project,
653
+ request=request,
654
+ role=role,
655
+ changes=changes,
656
+ last_pulled_at=last_pulled_at,
657
+ )
658
+ body = {
659
+ "client_batch_id": str(client_batch_id),
660
+ "applied": {
661
+ "tasks": {
662
+ "created": applied.created,
663
+ "updated": applied.updated,
664
+ "deleted": applied.deleted,
665
+ }
666
+ },
667
+ # Per-row lost-update conflicts (#1718): rows that were NOT applied
668
+ # because a concurrent writer changed an overlapping field. The client
669
+ # reloads these and reapplies; empty on the common no-conflict batch.
670
+ "conflicts": {"tasks": applied.conflicts},
671
+ "timestamp": applied.max_version,
672
+ }
673
+ batch.status = SyncBatchStatus.COMPLETED
674
+ batch.response_body = body
675
+ batch.response_status = status.HTTP_200_OK
676
+ batch.save(update_fields=["status", "response_body", "response_status"])
677
+
678
+ # Side effects on commit, mirroring single-row writes (inbound_sync):
679
+ # one coalesced CPM recalc + one coalesced board event, so web clients
680
+ # react and a rolled-back batch broadcasts nothing.
681
+ #
682
+ # A delta batch can carry up to TRUEPPM_SYNC_BATCH_MAX_ROWS (500) rows.
683
+ # Emitting one broadcast per applied row (the previous behavior) issued
684
+ # up to 500 async_to_sync(group_send) round-trips at commit on a single
685
+ # upload — under a reconnect storm this overflowed the channel-layer
686
+ # inbox and dropped presence/CPM events (#809). Coalesce into a single
687
+ # tasks_bulk_mutated event (same pattern as sprint-close carry-over,
688
+ # projects/tasks.py); the web handler invalidates the whole task query
689
+ # on it, so per-row granularity is not needed.
690
+ project_id = str(project.pk)
691
+ if applied.changed:
692
+ transaction.on_commit(partial(enqueue_recalculate, project_id))
693
+ mutated_ids = [task_id for _event_type, task_id in applied.events]
694
+ if mutated_ids:
695
+ # default-arg binding (not closure capture) so a future branch
696
+ # can't late-bind the ids — matches the carry-over pattern.
697
+ def _broadcast_bulk(pid: str = project_id, ids: list[str] = mutated_ids) -> None:
698
+ broadcast_board_event(pid, "tasks_bulk_mutated", {"task_ids": ids})
699
+
700
+ transaction.on_commit(_broadcast_bulk)
701
+
702
+ return Response(body, status=status.HTTP_200_OK)
703
+
704
+ @staticmethod
705
+ def _collect(qs: Any, serializer_class: Any) -> dict[str, Any]:
706
+ """Split a queryset into WatermelonDB 'updated' and 'deleted' buckets.
707
+
708
+ All changed rows are returned in 'updated' (upsert semantics).
709
+ Soft-deleted rows have their IDs returned in 'deleted' as well.
710
+ 'created' is always empty — WatermelonDB upserts handle insert/update.
711
+ """
712
+ rows = list(qs)
713
+ live = [r for r in rows if not r.is_deleted]
714
+ deleted_ids = [str(r.pk) for r in rows if r.is_deleted]
715
+ return {
716
+ "created": [],
717
+ "updated": serializer_class(live, many=True).data,
718
+ "deleted": deleted_ids,
719
+ }
720
+
721
+ @classmethod
722
+ def _watermark(cls, project: Project) -> int:
723
+ """Return the project's sync cursor high-water mark (ADR-0686, #2491).
724
+
725
+ ``Project.last_sync_version`` is the allocator every synced row in this
726
+ project drew its ``sync_seq`` from (``apps/sync/sequence.py``), so it is
727
+ by construction greater than or equal to every row's cursor — the safe
728
+ upper bound the client adopts as its next ``since``.
729
+
730
+ It used to cache ``MAX(server_version)`` over a 14-table ``UNION ALL``
731
+ (ADR-0142). The cache was faithful; the value was not a valid ordering,
732
+ because ``server_version`` counts one row's own saves. The union, its
733
+ ``SYNC_WATERMARK_USE_COLUMN`` fallback, and the receiver-vs-union
734
+ conformance test are gone with it: there is nothing left to derive the
735
+ column from, and nothing left to drift.
736
+ """
737
+ return int(project.last_sync_version)
738
+
739
+
740
+ class UserProgramSyncView(IdempotencyMixin, APIView):
741
+ """Pull-only delta sync for the caller's Program + ProgramMembership rows.
742
+
743
+ The project-scoped :class:`ProjectSyncView` cannot reach user-scoped Program
744
+ rows (ADR-0070 §Sync): the ``Project.program`` FK syncs offline so the program
745
+ badge renders on cached projects, but the program list itself does not — on a
746
+ job site the program layer vanishes and the user falls back to a flat project
747
+ list. This endpoint closes that gap, delivering Program and ProgramMembership
748
+ deltas for every program the caller is a member of. It is the 0.4 follow-up
749
+ ADR-0070 §Sync flagged; no new ADR (see the updated §Sync note).
750
+
751
+ Scope is derived entirely from the caller's own live memberships, so — unlike
752
+ the project endpoint — there is **no path parameter to tamper with**: no IDOR
753
+ surface. All co-members of the caller's programs are returned (parity with the
754
+ project endpoint, which returns every ``ProjectMembership`` for the project),
755
+ which lets the client enforce offline program RBAC and render the roster.
756
+
757
+ Tombstones: a program soft-deleted while the caller is still a member, and any
758
+ co-member membership soft-deleted, are delivered as ``deleted`` ids.
759
+
760
+ Known v1 limitation (mirrors ``ProjectSyncView``): if the caller's *own*
761
+ membership is soft-deleted (they are removed from a program), that row drops
762
+ out of the accessible set and its tombstone is never delivered — the online
763
+ path 403s on the next access and the client evicts the stale program on a full
764
+ re-sync. Full write-side offline for programs is out of scope for v1 (reads
765
+ first, per #561).
766
+
767
+ Usage:
768
+ GET /api/v1/sync/user/programs/?since=0
769
+ """
770
+
771
+ # Read-only pull; the generic Idempotency-Key path adds nothing (parity with
772
+ # ProjectSyncView.get, ADR-0170).
773
+ idempotency_exempt = True
774
+ permission_classes = [IsAuthenticated]
775
+
776
+ @extend_schema(
777
+ summary="Pull the offline sync delta for the caller's programs",
778
+ parameters=[
779
+ OpenApiParameter(
780
+ name="since",
781
+ type=OpenApiTypes.INT,
782
+ location=OpenApiParameter.QUERY,
783
+ required=False,
784
+ default=0,
785
+ description=(
786
+ "WatermelonDB high-water mark. Returns only Program / "
787
+ "ProgramMembership rows whose `server_version` is strictly "
788
+ "greater than this value. Omit (or pass `0`) for a full initial "
789
+ "sync; adopt the previous response's `timestamp` for the delta."
790
+ ),
791
+ ),
792
+ OpenApiParameter(
793
+ name="cursor",
794
+ type=OpenApiTypes.STR,
795
+ location=OpenApiParameter.QUERY,
796
+ required=False,
797
+ description=(
798
+ "Opaque pagination continuation token (#1013). Omit on the "
799
+ "first request; on each subsequent request pass the previous "
800
+ "response's `next_cursor`. Keep `since` constant for the whole "
801
+ "paging session. Loop until `has_more` is false, then adopt "
802
+ "`timestamp` as the next `since`."
803
+ ),
804
+ ),
805
+ OpenApiParameter(
806
+ name="page_size",
807
+ type=OpenApiTypes.INT,
808
+ location=OpenApiParameter.QUERY,
809
+ required=False,
810
+ description=(
811
+ "Maximum rows returned across both collections in one page. "
812
+ "Defaults to `TRUEPPM_SYNC_PULL_PAGE_SIZE`; clamped to "
813
+ "`TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE`."
814
+ ),
815
+ ),
816
+ ],
817
+ responses={200: SyncPullResponseSerializer},
818
+ )
819
+ def get(self, request: Request) -> Response:
820
+ # Validate `since` — identical contract to ProjectSyncView.
821
+ since_raw = request.query_params.get("since", "0")
822
+ try:
823
+ since = int(since_raw)
824
+ if since < 0:
825
+ raise ValueError
826
+ except (ValueError, TypeError) as err:
827
+ raise ValidationError({"since": "Must be a non-negative integer."}) from err
828
+
829
+ cursor_raw = request.query_params.get("cursor")
830
+ cursor = SyncCursor.decode(cursor_raw) if cursor_raw else None
831
+ page_size = ProjectSyncView._resolve_page_size(request)
832
+
833
+ # Accessible set: programs the caller has a live membership on, kept as a
834
+ # queryset so Postgres evaluates it as a single subquery in the delta reads
835
+ # rather than round-tripping every id into Python.
836
+ accessible_ids = ProgramMembership.objects.filter(
837
+ user=cast("_User", request.user), is_deleted=False
838
+ ).values("program_id")
839
+
840
+ # Snapshot the high-water mark BEFORE the delta queries. server_version is
841
+ # monotonic, so a write landing after this snapshot moves the row forward in
842
+ # (server_version, id) order — delivered this pull (re-applied under upsert)
843
+ # or the next (its version > timestamp = next since); never lost. This is
844
+ # the same concurrency argument pagination.py makes, and the same
845
+ # watermark-before-rows ordering ProjectSyncView relies on. Neither view
846
+ # opens a REPEATABLE READ block; both run at READ COMMITTED, and the
847
+ # ordering — not the isolation level — is what closes the window.
848
+ #
849
+ # This slice keys on sync_seq, drawn from the installation-wide program
850
+ # sequence (ADR-0747, #2498). It used to key on the per-row server_version
851
+ # and carried the ordering defect ADR-0686 fixed for projects: a hot program
852
+ # raised the watermark above a cold program's row counters and those rows
853
+ # dropped out of the delta permanently. The project sequence could not fix
854
+ # it — programs have no single owning row — so program-scoped writes order
855
+ # against one installation counter instead, which restores a scalar cursor's
856
+ # validity without changing the wire contract.
857
+ #
858
+ # As in ProjectSyncView, the watermark is session state: a continuation page
859
+ # echoes the cursor's pinned value instead of recomputing one that could sit
860
+ # above an already-drained collection's mid-drain write (#2568).
861
+ timestamp = cursor.watermark if cursor is not None else self._watermark(accessible_ids)
862
+
863
+ # Program rows include soft-deleted ones so a program deleted while the
864
+ # caller is still a member arrives as a tombstone. Memberships cover every
865
+ # co-member of the caller's programs (offline RBAC + roster), soft-deleted
866
+ # rows included. Order is fixed — do not reorder without a protocol bump.
867
+ sources: list[SyncSource] = [
868
+ (
869
+ "programs",
870
+ Program.objects.filter(id__in=accessible_ids, sync_seq__gt=since),
871
+ SyncProgramSerializer,
872
+ ),
873
+ (
874
+ "program_memberships",
875
+ ProgramMembership.objects.filter(program_id__in=accessible_ids, sync_seq__gt=since),
876
+ SyncProgramMembershipSerializer,
877
+ ),
878
+ ]
879
+
880
+ changes, next_cursor, has_more = paginate_changes(
881
+ sources,
882
+ cursor=cursor,
883
+ page_size=page_size,
884
+ collect=ProjectSyncView._collect,
885
+ watermark=timestamp,
886
+ # Must match the delta filter above: the pager orders on this column
887
+ # and the sources floor on it. Ordering on one while filtering the
888
+ # other pages a stream the floor does not bound (ADR-0747, #2498).
889
+ version_field="sync_seq",
890
+ )
891
+
892
+ return Response(
893
+ {
894
+ "changes": changes,
895
+ "timestamp": timestamp,
896
+ "next_cursor": next_cursor.encode() if next_cursor is not None else None,
897
+ "has_more": has_more,
898
+ }
899
+ )
900
+
901
+ @staticmethod
902
+ def _watermark(accessible_ids: Any) -> int:
903
+ """The installation-wide program sequence's high-water mark (ADR-0747, #2498).
904
+
905
+ ``ProgramSyncSequence.value`` is the allocator every synced ``Program`` and
906
+ ``ProgramMembership`` row drew its ``sync_seq`` from, so it is by
907
+ construction greater than or equal to every row's cursor — the safe upper
908
+ bound the client adopts as its next ``since``. One row read, no aggregate.
909
+
910
+ It used to be ``MAX(server_version)`` across the caller's programs and their
911
+ memberships. That value was faithful and was not a valid ordering:
912
+ ``server_version`` counts a single row's saves, so a frequently-edited
913
+ program raised the checkpoint above every other program's row counter and
914
+ dropped those rows from the delta permanently (#2498) — the same defect
915
+ ADR-0686 fixed for projects, one level up.
916
+
917
+ ``accessible_ids`` is no longer read: the checkpoint is a property of the
918
+ installation's write log, not of the caller's accessible set. Scoping it per
919
+ caller is what made it a maximum-over-many-rows in the first place. The
920
+ parameter is kept so the call site and ``ProjectSyncView._watermark`` keep
921
+ one shape.
922
+ """
923
+ from trueppm_api.apps.sync.models import ProgramSyncSequence
924
+
925
+ value = (
926
+ ProgramSyncSequence.objects.filter(pk=ProgramSyncSequence.SINGLETON_PK)
927
+ .values_list("value", flat=True)
928
+ .first()
929
+ )
930
+ return value or 0