trueppm-api 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (799) hide show
  1. trueppm_api/__init__.py +27 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +346 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
  24. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  25. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  26. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  27. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  28. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
  29. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  30. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  31. trueppm_api/apps/access/migrations/__init__.py +0 -0
  32. trueppm_api/apps/access/minter_role.py +38 -0
  33. trueppm_api/apps/access/models.py +588 -0
  34. trueppm_api/apps/access/permissions.py +3263 -0
  35. trueppm_api/apps/access/serializers.py +1007 -0
  36. trueppm_api/apps/access/services.py +878 -0
  37. trueppm_api/apps/access/signals.py +171 -0
  38. trueppm_api/apps/access/tasks.py +50 -0
  39. trueppm_api/apps/access/throttles.py +96 -0
  40. trueppm_api/apps/access/urls.py +191 -0
  41. trueppm_api/apps/access/views.py +1965 -0
  42. trueppm_api/apps/agents/__init__.py +6 -0
  43. trueppm_api/apps/agents/apps.py +11 -0
  44. trueppm_api/apps/agents/canonical.py +73 -0
  45. trueppm_api/apps/agents/deferred.py +108 -0
  46. trueppm_api/apps/agents/management/__init__.py +0 -0
  47. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  48. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  49. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  50. trueppm_api/apps/agents/middleware.py +44 -0
  51. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  52. trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
  53. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  54. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  55. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  56. trueppm_api/apps/agents/models.py +371 -0
  57. trueppm_api/apps/agents/refusal.py +103 -0
  58. trueppm_api/apps/agents/serializers.py +85 -0
  59. trueppm_api/apps/agents/services.py +397 -0
  60. trueppm_api/apps/agents/signals.py +30 -0
  61. trueppm_api/apps/agents/urls.py +15 -0
  62. trueppm_api/apps/agents/views.py +109 -0
  63. trueppm_api/apps/csvimport/__init__.py +12 -0
  64. trueppm_api/apps/csvimport/apps.py +11 -0
  65. trueppm_api/apps/csvimport/mapping.py +363 -0
  66. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  67. trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
  68. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  69. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  70. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  71. trueppm_api/apps/csvimport/models.py +111 -0
  72. trueppm_api/apps/csvimport/parser.py +1825 -0
  73. trueppm_api/apps/csvimport/services.py +48 -0
  74. trueppm_api/apps/csvimport/tasks.py +459 -0
  75. trueppm_api/apps/csvimport/template.py +37 -0
  76. trueppm_api/apps/csvimport/urls.py +50 -0
  77. trueppm_api/apps/csvimport/views.py +793 -0
  78. trueppm_api/apps/history/__init__.py +0 -0
  79. trueppm_api/apps/history/apps.py +12 -0
  80. trueppm_api/apps/history/changelog.py +359 -0
  81. trueppm_api/apps/history/diff_policy.py +178 -0
  82. trueppm_api/apps/history/serializers.py +104 -0
  83. trueppm_api/apps/history/signals.py +82 -0
  84. trueppm_api/apps/history/tasks.py +105 -0
  85. trueppm_api/apps/history/urls.py +38 -0
  86. trueppm_api/apps/history/views.py +443 -0
  87. trueppm_api/apps/idempotency/__init__.py +0 -0
  88. trueppm_api/apps/idempotency/apps.py +11 -0
  89. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  90. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  91. trueppm_api/apps/idempotency/mixins.py +226 -0
  92. trueppm_api/apps/idempotency/models.py +75 -0
  93. trueppm_api/apps/idempotency/tasks.py +61 -0
  94. trueppm_api/apps/integrations/__init__.py +13 -0
  95. trueppm_api/apps/integrations/apps.py +59 -0
  96. trueppm_api/apps/integrations/connections.py +806 -0
  97. trueppm_api/apps/integrations/encryption.py +92 -0
  98. trueppm_api/apps/integrations/external_sources.py +863 -0
  99. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  100. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  101. trueppm_api/apps/integrations/http.py +597 -0
  102. trueppm_api/apps/integrations/me_work.py +218 -0
  103. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  104. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  105. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  106. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  107. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  108. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  109. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  110. trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
  111. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  112. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  113. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  114. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  115. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  116. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  117. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  118. trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
  119. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  120. trueppm_api/apps/integrations/models.py +635 -0
  121. trueppm_api/apps/integrations/notification_channels.py +57 -0
  122. trueppm_api/apps/integrations/opengraph.py +229 -0
  123. trueppm_api/apps/integrations/outgoing.py +201 -0
  124. trueppm_api/apps/integrations/providers.py +528 -0
  125. trueppm_api/apps/integrations/registry.py +345 -0
  126. trueppm_api/apps/integrations/serializers.py +453 -0
  127. trueppm_api/apps/integrations/services.py +171 -0
  128. trueppm_api/apps/integrations/tasks.py +734 -0
  129. trueppm_api/apps/integrations/throttles.py +165 -0
  130. trueppm_api/apps/integrations/urls.py +77 -0
  131. trueppm_api/apps/integrations/views.py +1158 -0
  132. trueppm_api/apps/jiraimport/__init__.py +8 -0
  133. trueppm_api/apps/jiraimport/apps.py +11 -0
  134. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  135. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  136. trueppm_api/apps/jiraimport/models.py +70 -0
  137. trueppm_api/apps/jiraimport/parser.py +350 -0
  138. trueppm_api/apps/jiraimport/services.py +46 -0
  139. trueppm_api/apps/jiraimport/tasks.py +280 -0
  140. trueppm_api/apps/jiraimport/urls.py +15 -0
  141. trueppm_api/apps/jiraimport/views.py +153 -0
  142. trueppm_api/apps/msproject/__init__.py +0 -0
  143. trueppm_api/apps/msproject/apps.py +11 -0
  144. trueppm_api/apps/msproject/dataclasses.py +153 -0
  145. trueppm_api/apps/msproject/exporter.py +551 -0
  146. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  147. trueppm_api/apps/msproject/importer.py +1095 -0
  148. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  149. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  150. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  151. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  152. trueppm_api/apps/msproject/models.py +81 -0
  153. trueppm_api/apps/msproject/parser.py +1276 -0
  154. trueppm_api/apps/msproject/serializers.py +136 -0
  155. trueppm_api/apps/msproject/services.py +58 -0
  156. trueppm_api/apps/msproject/tasks.py +439 -0
  157. trueppm_api/apps/msproject/urls.py +39 -0
  158. trueppm_api/apps/msproject/views.py +531 -0
  159. trueppm_api/apps/notifications/__init__.py +7 -0
  160. trueppm_api/apps/notifications/apps.py +15 -0
  161. trueppm_api/apps/notifications/backfill.py +79 -0
  162. trueppm_api/apps/notifications/categories.py +112 -0
  163. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  164. trueppm_api/apps/notifications/digests.py +462 -0
  165. trueppm_api/apps/notifications/email_backend.py +350 -0
  166. trueppm_api/apps/notifications/email_health.py +128 -0
  167. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  168. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
  169. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  170. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  171. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  172. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  173. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  174. trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
  175. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  176. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  177. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  178. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  179. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  180. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  181. trueppm_api/apps/notifications/models.py +1148 -0
  182. trueppm_api/apps/notifications/receivers.py +155 -0
  183. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  184. trueppm_api/apps/notifications/serializers.py +1094 -0
  185. trueppm_api/apps/notifications/services.py +1489 -0
  186. trueppm_api/apps/notifications/tasks.py +762 -0
  187. trueppm_api/apps/notifications/throttles.py +98 -0
  188. trueppm_api/apps/notifications/urls.py +97 -0
  189. trueppm_api/apps/notifications/views.py +919 -0
  190. trueppm_api/apps/observability/__init__.py +0 -0
  191. trueppm_api/apps/observability/apps.py +31 -0
  192. trueppm_api/apps/observability/logging.py +213 -0
  193. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  194. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  195. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  196. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  197. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  198. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  199. trueppm_api/apps/observability/models.py +141 -0
  200. trueppm_api/apps/observability/otel/__init__.py +45 -0
  201. trueppm_api/apps/observability/otel/attributes.py +247 -0
  202. trueppm_api/apps/observability/otel/export_health.py +533 -0
  203. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  204. trueppm_api/apps/observability/otel/metrics.py +566 -0
  205. trueppm_api/apps/observability/otel/provider.py +511 -0
  206. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  207. trueppm_api/apps/observability/purge_registry.py +124 -0
  208. trueppm_api/apps/observability/retention.py +139 -0
  209. trueppm_api/apps/observability/selectors.py +993 -0
  210. trueppm_api/apps/observability/serializers.py +142 -0
  211. trueppm_api/apps/observability/services.py +407 -0
  212. trueppm_api/apps/observability/tasks.py +266 -0
  213. trueppm_api/apps/observability/urls.py +27 -0
  214. trueppm_api/apps/observability/views.py +537 -0
  215. trueppm_api/apps/profiles/__init__.py +0 -0
  216. trueppm_api/apps/profiles/apps.py +15 -0
  217. trueppm_api/apps/profiles/constants.py +54 -0
  218. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  219. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  220. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  221. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  222. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  223. trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
  224. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  225. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  226. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  227. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  228. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  229. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  230. trueppm_api/apps/profiles/models.py +276 -0
  231. trueppm_api/apps/profiles/serializers.py +199 -0
  232. trueppm_api/apps/profiles/services.py +541 -0
  233. trueppm_api/apps/profiles/views.py +124 -0
  234. trueppm_api/apps/projects/__init__.py +0 -0
  235. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  236. trueppm_api/apps/projects/amend.py +133 -0
  237. trueppm_api/apps/projects/apps.py +38 -0
  238. trueppm_api/apps/projects/asset_feed.py +459 -0
  239. trueppm_api/apps/projects/asset_views.py +317 -0
  240. trueppm_api/apps/projects/attachment_policy.py +278 -0
  241. trueppm_api/apps/projects/authentication.py +458 -0
  242. trueppm_api/apps/projects/backfill.py +459 -0
  243. trueppm_api/apps/projects/backlog_services.py +324 -0
  244. trueppm_api/apps/projects/backlog_views.py +302 -0
  245. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  246. trueppm_api/apps/projects/blocker_services.py +510 -0
  247. trueppm_api/apps/projects/board_activity.py +462 -0
  248. trueppm_api/apps/projects/board_activity_views.py +186 -0
  249. trueppm_api/apps/projects/board_lanes.py +134 -0
  250. trueppm_api/apps/projects/bulk_settings.py +194 -0
  251. trueppm_api/apps/projects/bundled_templates.py +215 -0
  252. trueppm_api/apps/projects/calendar_settings.py +180 -0
  253. trueppm_api/apps/projects/ceremony_views.py +244 -0
  254. trueppm_api/apps/projects/commit_moment.py +212 -0
  255. trueppm_api/apps/projects/config_notice.py +1330 -0
  256. trueppm_api/apps/projects/custom_field_values.py +266 -0
  257. trueppm_api/apps/projects/decisions_services.py +132 -0
  258. trueppm_api/apps/projects/decisions_views.py +97 -0
  259. trueppm_api/apps/projects/estimation_scale.py +126 -0
  260. trueppm_api/apps/projects/export_bundle.py +423 -0
  261. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
  262. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
  263. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
  264. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
  265. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
  266. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  267. trueppm_api/apps/projects/inbound_sync.py +555 -0
  268. trueppm_api/apps/projects/iteration_label.py +85 -0
  269. trueppm_api/apps/projects/key_views.py +468 -0
  270. trueppm_api/apps/projects/keys.py +205 -0
  271. trueppm_api/apps/projects/lifecycle.py +191 -0
  272. trueppm_api/apps/projects/management/__init__.py +0 -0
  273. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  274. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  275. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  276. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  277. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  278. trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
  279. trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
  280. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
  281. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  282. trueppm_api/apps/projects/mcp_settings.py +305 -0
  283. trueppm_api/apps/projects/methodology.py +178 -0
  284. trueppm_api/apps/projects/migration_helpers.py +75 -0
  285. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  286. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
  287. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  288. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  289. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  290. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  291. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  292. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  293. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  294. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  295. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  296. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  297. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  298. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  299. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  300. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  301. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  302. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  303. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  304. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  305. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  306. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  307. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  308. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  309. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  310. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  311. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  312. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  313. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  314. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  315. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  316. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  317. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  318. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  319. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  320. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  321. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  322. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  323. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  324. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  325. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  326. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  327. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  328. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  329. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  330. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  331. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  332. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  333. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  334. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  335. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  336. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  337. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  338. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  339. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  340. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  341. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  342. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  343. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  344. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  345. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  346. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  347. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  348. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  349. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  350. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  351. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  352. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  353. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  354. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  355. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  356. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  357. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  358. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  359. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  360. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  361. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  362. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  363. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  364. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  365. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  366. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  367. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  368. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  369. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  370. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  371. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  372. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  373. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  374. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  375. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  376. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  377. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  378. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  379. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
  380. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  381. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  382. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  383. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  384. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  385. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  386. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  387. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  388. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  389. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  390. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
  391. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  392. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  393. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  394. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  395. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  396. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
  397. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  398. trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
  399. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  400. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  401. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  402. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  403. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  404. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  405. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  406. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  407. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  408. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  409. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  410. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  411. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  412. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  413. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  414. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  415. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  416. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  417. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  418. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  419. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  420. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  421. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  422. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  423. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  424. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  425. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  426. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  427. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  428. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  429. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  430. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  431. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  432. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  433. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  434. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  435. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  436. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  437. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  438. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  439. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  440. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  441. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  442. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  443. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  444. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  445. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  446. trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
  447. trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
  448. trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
  449. trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
  450. trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
  451. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  452. trueppm_api/apps/projects/models.py +9209 -0
  453. trueppm_api/apps/projects/poker_services.py +154 -0
  454. trueppm_api/apps/projects/poker_views.py +398 -0
  455. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  456. trueppm_api/apps/projects/program_label_services.py +94 -0
  457. trueppm_api/apps/projects/program_label_views.py +189 -0
  458. trueppm_api/apps/projects/program_rollup.py +903 -0
  459. trueppm_api/apps/projects/program_schedule.py +767 -0
  460. trueppm_api/apps/projects/program_views.py +3445 -0
  461. trueppm_api/apps/projects/project_templates.py +422 -0
  462. trueppm_api/apps/projects/receivers.py +234 -0
  463. trueppm_api/apps/projects/refusal_codes.py +140 -0
  464. trueppm_api/apps/projects/reorder_services.py +145 -0
  465. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  466. trueppm_api/apps/projects/retro_board_services.py +368 -0
  467. trueppm_api/apps/projects/retro_services.py +285 -0
  468. trueppm_api/apps/projects/risk_import.py +396 -0
  469. trueppm_api/apps/projects/row_identity.py +113 -0
  470. trueppm_api/apps/projects/schema_migrations.py +228 -0
  471. trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
  472. trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
  473. trueppm_api/apps/projects/seed/__init__.py +61 -0
  474. trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
  475. trueppm_api/apps/projects/seed/exporter.py +1717 -0
  476. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  477. trueppm_api/apps/projects/seed/importer.py +2764 -0
  478. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  479. trueppm_api/apps/projects/seed/reldates.py +126 -0
  480. trueppm_api/apps/projects/seed/replace.py +128 -0
  481. trueppm_api/apps/projects/seed/replay.py +1695 -0
  482. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  483. trueppm_api/apps/projects/seed/samples.py +427 -0
  484. trueppm_api/apps/projects/seed/validation.py +1169 -0
  485. trueppm_api/apps/projects/serializers.py +12904 -0
  486. trueppm_api/apps/projects/services.py +7525 -0
  487. trueppm_api/apps/projects/share_serializers.py +142 -0
  488. trueppm_api/apps/projects/share_services.py +373 -0
  489. trueppm_api/apps/projects/share_views.py +395 -0
  490. trueppm_api/apps/projects/sharing_settings.py +187 -0
  491. trueppm_api/apps/projects/signal_privacy_services.py +910 -0
  492. trueppm_api/apps/projects/signal_privacy_views.py +485 -0
  493. trueppm_api/apps/projects/signals.py +132 -0
  494. trueppm_api/apps/projects/slip_conflict.py +288 -0
  495. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  496. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  497. trueppm_api/apps/projects/standup.py +304 -0
  498. trueppm_api/apps/projects/standup_views.py +99 -0
  499. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  500. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  501. trueppm_api/apps/projects/surface_visibility.py +126 -0
  502. trueppm_api/apps/projects/task_batch_services.py +379 -0
  503. trueppm_api/apps/projects/task_bulk.py +1244 -0
  504. trueppm_api/apps/projects/task_classification.py +561 -0
  505. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  506. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  507. trueppm_api/apps/projects/task_grouping.py +754 -0
  508. trueppm_api/apps/projects/tasks.py +1979 -0
  509. trueppm_api/apps/projects/template_divergence.py +168 -0
  510. trueppm_api/apps/projects/template_services.py +160 -0
  511. trueppm_api/apps/projects/template_tasks.py +261 -0
  512. trueppm_api/apps/projects/template_views.py +829 -0
  513. trueppm_api/apps/projects/throttles.py +311 -0
  514. trueppm_api/apps/projects/urls.py +1016 -0
  515. trueppm_api/apps/projects/utilization.py +909 -0
  516. trueppm_api/apps/projects/views.py +21225 -0
  517. trueppm_api/apps/projects/wbs_paths.py +119 -0
  518. trueppm_api/apps/resources/__init__.py +0 -0
  519. trueppm_api/apps/resources/apps.py +8 -0
  520. trueppm_api/apps/resources/capacity.py +121 -0
  521. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  522. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  523. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  524. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  525. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  526. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  527. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  528. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  529. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  530. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  531. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  532. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  533. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  534. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  535. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  536. trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
  537. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  538. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  539. trueppm_api/apps/resources/models.py +298 -0
  540. trueppm_api/apps/resources/serializers.py +426 -0
  541. trueppm_api/apps/resources/services.py +465 -0
  542. trueppm_api/apps/resources/urls.py +28 -0
  543. trueppm_api/apps/resources/views.py +1752 -0
  544. trueppm_api/apps/scheduling/__init__.py +0 -0
  545. trueppm_api/apps/scheduling/apps.py +153 -0
  546. trueppm_api/apps/scheduling/calendars.py +144 -0
  547. trueppm_api/apps/scheduling/deadletter.py +91 -0
  548. trueppm_api/apps/scheduling/finish_reading.py +198 -0
  549. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  550. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  551. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  552. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  553. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  554. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  555. trueppm_api/apps/scheduling/mc_admission.py +181 -0
  556. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  557. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  558. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  559. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  560. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  561. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  562. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  563. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  564. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  565. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  566. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  567. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  568. trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
  569. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  570. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  571. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  572. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  573. trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
  574. trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
  575. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  576. trueppm_api/apps/scheduling/models.py +542 -0
  577. trueppm_api/apps/scheduling/receivers.py +160 -0
  578. trueppm_api/apps/scheduling/risk_premium.py +334 -0
  579. trueppm_api/apps/scheduling/serializers.py +723 -0
  580. trueppm_api/apps/scheduling/services.py +1251 -0
  581. trueppm_api/apps/scheduling/signals.py +56 -0
  582. trueppm_api/apps/scheduling/tasks.py +2248 -0
  583. trueppm_api/apps/scheduling/telemetry.py +80 -0
  584. trueppm_api/apps/scheduling/units.py +35 -0
  585. trueppm_api/apps/scheduling/urls.py +57 -0
  586. trueppm_api/apps/scheduling/views.py +2908 -0
  587. trueppm_api/apps/sso/__init__.py +0 -0
  588. trueppm_api/apps/sso/apps.py +19 -0
  589. trueppm_api/apps/sso/extensions.py +130 -0
  590. trueppm_api/apps/sso/management/__init__.py +0 -0
  591. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  592. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  593. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  594. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  595. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  596. trueppm_api/apps/sso/models.py +148 -0
  597. trueppm_api/apps/sso/serializers.py +446 -0
  598. trueppm_api/apps/sso/services.py +1473 -0
  599. trueppm_api/apps/sso/urls.py +47 -0
  600. trueppm_api/apps/sso/views.py +935 -0
  601. trueppm_api/apps/sync/__init__.py +0 -0
  602. trueppm_api/apps/sync/apps.py +15 -0
  603. trueppm_api/apps/sync/backfill.py +208 -0
  604. trueppm_api/apps/sync/broadcast.py +369 -0
  605. trueppm_api/apps/sync/conflict.py +262 -0
  606. trueppm_api/apps/sync/consumers.py +548 -0
  607. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  608. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  609. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  610. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  611. trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
  612. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  613. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  614. trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
  615. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  616. trueppm_api/apps/sync/models.py +272 -0
  617. trueppm_api/apps/sync/pagination.py +231 -0
  618. trueppm_api/apps/sync/sequence.py +488 -0
  619. trueppm_api/apps/sync/serializers.py +688 -0
  620. trueppm_api/apps/sync/tasks.py +260 -0
  621. trueppm_api/apps/sync/throttles.py +175 -0
  622. trueppm_api/apps/sync/upload.py +556 -0
  623. trueppm_api/apps/sync/urls.py +17 -0
  624. trueppm_api/apps/sync/views.py +957 -0
  625. trueppm_api/apps/sync/ws_auth.py +193 -0
  626. trueppm_api/apps/taskruns/__init__.py +0 -0
  627. trueppm_api/apps/taskruns/apps.py +11 -0
  628. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  629. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  630. trueppm_api/apps/taskruns/models.py +68 -0
  631. trueppm_api/apps/taskruns/serializers.py +92 -0
  632. trueppm_api/apps/taskruns/tasks.py +60 -0
  633. trueppm_api/apps/taskruns/tracker.py +286 -0
  634. trueppm_api/apps/taskruns/urls.py +46 -0
  635. trueppm_api/apps/taskruns/views.py +195 -0
  636. trueppm_api/apps/teams/__init__.py +8 -0
  637. trueppm_api/apps/teams/apps.py +16 -0
  638. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  639. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
  640. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  641. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  642. trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
  643. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  644. trueppm_api/apps/teams/models.py +145 -0
  645. trueppm_api/apps/teams/permissions.py +149 -0
  646. trueppm_api/apps/teams/serializers.py +95 -0
  647. trueppm_api/apps/teams/services.py +440 -0
  648. trueppm_api/apps/teams/signals.py +61 -0
  649. trueppm_api/apps/teams/urls.py +34 -0
  650. trueppm_api/apps/teams/views.py +201 -0
  651. trueppm_api/apps/timetracking/__init__.py +0 -0
  652. trueppm_api/apps/timetracking/apps.py +9 -0
  653. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  654. trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
  655. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  656. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  657. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  658. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  659. trueppm_api/apps/timetracking/models.py +178 -0
  660. trueppm_api/apps/timetracking/serializers.py +175 -0
  661. trueppm_api/apps/timetracking/services.py +211 -0
  662. trueppm_api/apps/timetracking/urls.py +47 -0
  663. trueppm_api/apps/timetracking/views.py +597 -0
  664. trueppm_api/apps/webhooks/__init__.py +0 -0
  665. trueppm_api/apps/webhooks/apps.py +8 -0
  666. trueppm_api/apps/webhooks/backfill.py +90 -0
  667. trueppm_api/apps/webhooks/dispatch.py +279 -0
  668. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  669. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
  670. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  671. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  672. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  673. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  674. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  675. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  676. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  677. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
  678. trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
  679. trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
  680. trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
  681. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  682. trueppm_api/apps/webhooks/models.py +437 -0
  683. trueppm_api/apps/webhooks/serializers.py +326 -0
  684. trueppm_api/apps/webhooks/tasks.py +680 -0
  685. trueppm_api/apps/webhooks/urls.py +22 -0
  686. trueppm_api/apps/webhooks/views.py +533 -0
  687. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  688. trueppm_api/apps/workflow_engine/apps.py +23 -0
  689. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  690. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  691. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  692. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  693. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  694. trueppm_api/apps/workflow_engine/models.py +265 -0
  695. trueppm_api/apps/workflow_engine/tasks.py +371 -0
  696. trueppm_api/apps/workspace/__init__.py +0 -0
  697. trueppm_api/apps/workspace/apps.py +10 -0
  698. trueppm_api/apps/workspace/backfill.py +111 -0
  699. trueppm_api/apps/workspace/export.py +254 -0
  700. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  701. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  702. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  703. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  704. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  705. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  706. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  707. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  708. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  709. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  710. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  711. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  712. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  713. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  714. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  715. trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
  716. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  717. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  718. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  719. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  720. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  721. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  722. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  723. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  724. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  725. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  726. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  727. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  728. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  729. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  730. trueppm_api/apps/workspace/models.py +857 -0
  731. trueppm_api/apps/workspace/permissions.py +187 -0
  732. trueppm_api/apps/workspace/serializers.py +590 -0
  733. trueppm_api/apps/workspace/services.py +967 -0
  734. trueppm_api/apps/workspace/signals.py +37 -0
  735. trueppm_api/apps/workspace/tasks.py +616 -0
  736. trueppm_api/apps/workspace/urls.py +113 -0
  737. trueppm_api/apps/workspace/views.py +1581 -0
  738. trueppm_api/asgi.py +49 -0
  739. trueppm_api/celery.py +40 -0
  740. trueppm_api/core/__init__.py +0 -0
  741. trueppm_api/core/admin_site.py +206 -0
  742. trueppm_api/core/auth_views.py +934 -0
  743. trueppm_api/core/constant_time.py +51 -0
  744. trueppm_api/core/csp.py +54 -0
  745. trueppm_api/core/db.py +87 -0
  746. trueppm_api/core/db_session.py +37 -0
  747. trueppm_api/core/demo_read_only.py +314 -0
  748. trueppm_api/core/exception_handlers.py +157 -0
  749. trueppm_api/core/export_downloads.py +56 -0
  750. trueppm_api/core/extension_providers.py +56 -0
  751. trueppm_api/core/extension_signals.py +147 -0
  752. trueppm_api/core/idempotent.py +260 -0
  753. trueppm_api/core/metadata.py +42 -0
  754. trueppm_api/core/middleware.py +51 -0
  755. trueppm_api/core/openapi.py +1221 -0
  756. trueppm_api/core/password_reset.py +513 -0
  757. trueppm_api/core/protect_conflict.py +100 -0
  758. trueppm_api/core/ratelimit.py +148 -0
  759. trueppm_api/core/redis_throttle.py +82 -0
  760. trueppm_api/core/refresh_cookie_policy.py +156 -0
  761. trueppm_api/core/request_body.py +86 -0
  762. trueppm_api/core/security_checks.py +1034 -0
  763. trueppm_api/core/storage_config.py +105 -0
  764. trueppm_api/core/text_decode.py +168 -0
  765. trueppm_api/core/throttling.py +514 -0
  766. trueppm_api/core/valkey.py +214 -0
  767. trueppm_api/core/valkey_cache.py +66 -0
  768. trueppm_api/core/valkey_checks.py +104 -0
  769. trueppm_api/core/valkey_config.py +39 -0
  770. trueppm_api/core/worker_broker_wait.py +149 -0
  771. trueppm_api/core/worker_heartbeat.py +134 -0
  772. trueppm_api/core/worker_selfheal.py +205 -0
  773. trueppm_api/fields.py +36 -0
  774. trueppm_api/permissions.py +30 -0
  775. trueppm_api/py.typed +0 -0
  776. trueppm_api/routing.py +11 -0
  777. trueppm_api/settings/__init__.py +0 -0
  778. trueppm_api/settings/base.py +2605 -0
  779. trueppm_api/settings/dev.py +296 -0
  780. trueppm_api/settings/prod.py +265 -0
  781. trueppm_api/urls.py +217 -0
  782. trueppm_api/workflows/__init__.py +38 -0
  783. trueppm_api/workflows/backends/__init__.py +6 -0
  784. trueppm_api/workflows/backends/default.py +297 -0
  785. trueppm_api/workflows/consumers/__init__.py +11 -0
  786. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  787. trueppm_api/workflows/interface.py +148 -0
  788. trueppm_api/workflows/registry.py +106 -0
  789. trueppm_api/workflows/services.py +90 -0
  790. trueppm_api/wsgi.py +11 -0
  791. trueppm_api-0.4.0.dist-info/METADATA +273 -0
  792. trueppm_api-0.4.0.dist-info/RECORD +799 -0
  793. trueppm_api-0.4.0.dist-info/WHEEL +4 -0
  794. trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
  795. trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
  796. trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
  797. trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
  798. trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
  799. trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
@@ -0,0 +1,957 @@
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 (
14
+ OpenApiParameter,
15
+ OpenApiResponse,
16
+ extend_schema,
17
+ inline_serializer,
18
+ )
19
+ from rest_framework import serializers, status
20
+ from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
21
+ from rest_framework.permissions import IsAuthenticated
22
+ from rest_framework.request import Request
23
+ from rest_framework.response import Response
24
+ from rest_framework.throttling import BaseThrottle, ScopedRateThrottle
25
+ from rest_framework.views import APIView
26
+
27
+ from trueppm_api.apps.access.models import ProgramMembership, ProjectMembership, Role
28
+ from trueppm_api.apps.access.permissions import IsNotTokenAuthenticated, _membership_role
29
+ from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
30
+ from trueppm_api.apps.integrations.models import TaskLink
31
+ from trueppm_api.apps.projects.models import (
32
+ Calendar,
33
+ Dependency,
34
+ Label,
35
+ Program,
36
+ Project,
37
+ RetroActionItem,
38
+ RetroVisibility,
39
+ Risk,
40
+ Sprint,
41
+ SprintRetro,
42
+ Task,
43
+ TaskCustomFieldValue,
44
+ TaskRecurrenceRule,
45
+ TaskRelation,
46
+ TaskSuggestedAssignee,
47
+ )
48
+ from trueppm_api.apps.sync.pagination import SyncCursor, SyncSource, paginate_changes
49
+ from trueppm_api.apps.sync.serializers import (
50
+ SyncCalendarSerializer,
51
+ SyncDependencySerializer,
52
+ SyncLabelSerializer,
53
+ SyncMembershipSerializer,
54
+ SyncProgramMembershipSerializer,
55
+ SyncProgramSerializer,
56
+ SyncProjectSerializer,
57
+ SyncPullResponseSerializer,
58
+ SyncRetroActionItemSerializer,
59
+ SyncRiskSerializer,
60
+ SyncSprintRetroSerializer,
61
+ SyncSprintSerializer,
62
+ SyncTaskLinkSerializer,
63
+ SyncTaskRecurrenceRuleSerializer,
64
+ SyncTaskRelationSerializer,
65
+ SyncTaskSerializer,
66
+ SyncTaskSuggestedAssigneeSerializer,
67
+ SyncTimeEntrySerializer,
68
+ SyncUploadRequestSerializer,
69
+ SyncUploadResponseSerializer,
70
+ )
71
+ from trueppm_api.apps.sync.ws_auth import TICKET_TTL_SECONDS, issue_ticket
72
+ from trueppm_api.apps.timetracking.models import TimeEntry
73
+
74
+ if TYPE_CHECKING:
75
+ from django.contrib.auth.models import User as _User
76
+
77
+
78
+ class WebSocketTicketView(IdempotencyMixin, APIView):
79
+ """Mint a short-lived, single-use ticket for the WebSocket handshake (ADR-0141).
80
+
81
+ Browsers cannot set an ``Authorization`` header on a WebSocket upgrade, so the
82
+ credential must ride in the URL. A raw JWT there leaks into every access log
83
+ (#818); a ticket that is single-use and expires in
84
+ :data:`~trueppm_api.apps.sync.ws_auth.TICKET_TTL_SECONDS` seconds is useless
85
+ once logged. The client POSTs here, then opens the socket with ``?ticket=``.
86
+
87
+ Authentication only — authorization (project membership/role) is enforced by
88
+ the consumer on connect. No project scope is required to mint a ticket.
89
+
90
+ **A personal access token may not mint a ticket (#4462).** Every real client
91
+ that opens a project socket — the web app and the mobile offline layer — mints
92
+ its ticket through the browser/app session's JWT; nothing in this codebase
93
+ mints one with a bearer PAT. A leaked PAT minting a ticket would open a live
94
+ WebSocket with no termination path of its own: revoking the token afterward
95
+ does not reach a socket that is already open, because the consumer resolves
96
+ its user once at connect and never re-checks the credential that opened it.
97
+ ``IsNotTokenAuthenticated`` closes that off at the one point it is cheap to
98
+ close — before the ticket exists.
99
+ """
100
+
101
+ permission_classes = [IsAuthenticated, IsNotTokenAuthenticated]
102
+ throttle_classes = [ScopedRateThrottle]
103
+ throttle_scope = "ws_ticket"
104
+ # Stateless mint — the generic Idempotency-Key path adds nothing.
105
+ idempotency_exempt = True
106
+
107
+ @extend_schema(
108
+ summary="Issue a WebSocket connection ticket",
109
+ responses={
110
+ 200: inline_serializer(
111
+ name="WebSocketTicketResponse",
112
+ fields={
113
+ "ticket": serializers.CharField(
114
+ help_text="Single-use opaque ticket to pass as the "
115
+ "`?ticket=` query parameter when opening the socket.",
116
+ ),
117
+ "expires_in": serializers.IntegerField(
118
+ help_text="Seconds until the ticket expires.",
119
+ ),
120
+ },
121
+ ),
122
+ },
123
+ description="Issue a single-use WebSocket connection ticket (ADR-0141).",
124
+ )
125
+ def post(self, request: Request) -> Response:
126
+ ticket = issue_ticket(str(request.user.pk))
127
+ return Response({"ticket": ticket, "expires_in": TICKET_TTL_SECONDS})
128
+
129
+
130
+ class ProjectSyncView(IdempotencyMixin, APIView):
131
+ """Pull-only delta sync endpoint for the mobile offline store.
132
+
133
+ Returns all rows (live and soft-deleted) whose ``sync_seq`` is strictly
134
+ greater than `since` for the given project, formatted for WatermelonDB's
135
+ synchronize() helper.
136
+
137
+ ``sync_seq`` — not ``server_version`` — is the cursor (ADR-0686, #2491). It is
138
+ drawn from the project's own monotonic sequence, so every synced row in the
139
+ project is totally ordered against every other one and the project-wide
140
+ maximum is a meaningful checkpoint. ``server_version`` counts a single row's
141
+ saves; comparing it against that maximum let a frequently-edited row raise the
142
+ checkpoint above every other row's counter and drop those rows from the delta
143
+ permanently.
144
+
145
+ The response ``timestamp`` is read *before* the delta queries, so a write that
146
+ lands during the pull always draws a cursor above it. Such a row is delivered by
147
+ this response or the next one — never dropped — at the cost of an occasional
148
+ re-delivery, which WatermelonDB's upsert absorbs. Clients therefore need no
149
+ retry or reconciliation logic for a mid-pull write; adopt ``timestamp`` as the
150
+ next ``since`` and the delta is complete. It is read once per *paging session*,
151
+ not per request: continuation pages echo the value pinned in the cursor (#2568).
152
+ See the comment at the watermark read for why the ordering, and not a transaction
153
+ isolation level, is what guarantees this.
154
+
155
+ Usage:
156
+ GET /api/v1/projects/{pk}/sync/?since=0
157
+ """
158
+
159
+ # Exempt from the generic Idempotency-Key path (ADR-0170): the upload POST is already
160
+ # idempotent by client_batch_id (SyncBatch dedup, ADR-0082); the generic header would
161
+ # be redundant and the sync protocol uses its own batch id.
162
+ idempotency_exempt = True
163
+ permission_classes = [IsAuthenticated]
164
+
165
+ @extend_schema(
166
+ summary="Pull the offline sync delta for a project",
167
+ parameters=[
168
+ OpenApiParameter(
169
+ name="since",
170
+ type=OpenApiTypes.INT,
171
+ location=OpenApiParameter.QUERY,
172
+ required=False,
173
+ default=0,
174
+ description=(
175
+ "WatermelonDB high-water mark — an opaque cursor, not a row "
176
+ "version. Returns only rows changed strictly after this point. "
177
+ "Omit (or pass `0`) for a full initial sync; passing the "
178
+ "previous response's `timestamp` fetches just the delta since "
179
+ "then. Omitting it silently triggers a full re-sync."
180
+ ),
181
+ ),
182
+ OpenApiParameter(
183
+ name="cursor",
184
+ type=OpenApiTypes.STR,
185
+ location=OpenApiParameter.QUERY,
186
+ required=False,
187
+ description=(
188
+ "Opaque pagination continuation token (#1013). Omit on the "
189
+ "first request; on each subsequent request pass the previous "
190
+ "response's `next_cursor`. Keep `since` constant for the whole "
191
+ "paging session. Loop until `next_cursor` is null / `has_more` "
192
+ "is false, then adopt `timestamp` as the next `since`."
193
+ ),
194
+ ),
195
+ OpenApiParameter(
196
+ name="page_size",
197
+ type=OpenApiTypes.INT,
198
+ location=OpenApiParameter.QUERY,
199
+ required=False,
200
+ description=(
201
+ "Maximum rows returned across all collections in one page. "
202
+ "Defaults to `TRUEPPM_SYNC_PULL_PAGE_SIZE`; clamped to "
203
+ "`TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE`."
204
+ ),
205
+ ),
206
+ ],
207
+ responses={200: SyncPullResponseSerializer},
208
+ )
209
+ def get(self, request: Request, pk: str) -> Response:
210
+ # Validate `since` parameter.
211
+ since_raw = request.query_params.get("since", "0")
212
+ try:
213
+ since = int(since_raw)
214
+ if since < 0:
215
+ raise ValueError
216
+ except (ValueError, TypeError) as err:
217
+ raise ValidationError({"since": "Must be a non-negative integer."}) from err
218
+
219
+ # Validate the optional continuation cursor and page size (#1013).
220
+ cursor_raw = request.query_params.get("cursor")
221
+ cursor = SyncCursor.decode(cursor_raw) if cursor_raw else None
222
+ page_size = self._resolve_page_size(request)
223
+
224
+ # Resolve project — 404 if missing or soft-deleted.
225
+ try:
226
+ project = Project.objects.get(pk=pk, is_deleted=False)
227
+ except Project.DoesNotExist as err:
228
+ raise NotFound("Project not found.") from err
229
+
230
+ # RBAC — any project member (Viewer+) may pull.
231
+ caller_role = _membership_role(request, project.pk)
232
+ if caller_role is None:
233
+ from rest_framework.exceptions import PermissionDenied
234
+
235
+ raise PermissionDenied("You must be a member of this project.")
236
+
237
+ # Read the high-water mark BEFORE the delta queries. This ORDERING is the
238
+ # whole guarantee — not an isolation level. ATOMIC_REQUESTS does wrap this
239
+ # request in a transaction, but at PostgreSQL's default READ COMMITTED, and
240
+ # nothing here or in settings raises that: every statement below still takes
241
+ # its own fresh snapshot, so being in a transaction pins nothing. REPEATABLE
242
+ # READ would add nothing either, and an earlier version of this comment
243
+ # claimed a wrapper that never existed at all (#2613).
244
+ #
245
+ # Two facts close the TOCTOU window:
246
+ #
247
+ # 1. A write landing after this read draws sync_seq > timestamp, so it is
248
+ # either carried by this response anyway (harmless — upsert) or caught
249
+ # by the next pull, whose `since` is this timestamp. Reading the
250
+ # watermark first errs toward re-delivery; reading it last is lossy.
251
+ # 2. A write that already committed is fully visible. Allocation UPDATEs
252
+ # the project row and holds its write lock to commit (sync/sequence.py),
253
+ # so allocations are totally ordered by commit: observing
254
+ # last_sync_version == W means every allocation <= W has committed —
255
+ # each one together with the row it stamped.
256
+ #
257
+ # Fact 2 rests on a WRITER-side precondition: the allocator bump and the row
258
+ # write must land in one transaction. ATOMIC_REQUESTS supplies it for every
259
+ # HTTP write and the batch writers open an explicit atomic(); a synced write
260
+ # in autocommit would violate it, and no reader isolation level can
261
+ # compensate — a snapshot cannot reveal an uncommitted row whose already-
262
+ # committed allocator bump the reader can see. Tracked in #2617.
263
+ #
264
+ # The value is taken off the Project row fetched above rather than re-read,
265
+ # so the snapshot instant is the RBAC fetch — earlier still, which only
266
+ # widens the margin. Do NOT add a refresh_from_db() here: re-reading the
267
+ # allocator after the delta queries is exactly the lossy order, and would
268
+ # publish a checkpoint over rows this response could not carry.
269
+ #
270
+ # One watermark per *session*, not per request (#2568) — which preserves the
271
+ # ordering above rather than weakening it: a continuation page's checkpoint was
272
+ # read before page 1's delta queries, so it precedes every query in the drain.
273
+ # On a continuation page the checkpoint is read back off the cursor: recomputing
274
+ # it would
275
+ # publish a value above rows written mid-drain into a collection the pager
276
+ # had already passed, and the client adopting the last page's `timestamp`
277
+ # would filter those rows out of every future pull. Pinning page 1's value
278
+ # errs the safe way — such rows simply arrive on the next pull, and rows
279
+ # this drain did deliver above the pin are re-delivered under upsert.
280
+ #
281
+ # The cursor is unsigned, so `w` is also ceiling-checked against the live
282
+ # sequence. `i` and `v` are stream *positions* the pager re-derives and
283
+ # bounds naturally, but `w` is a checkpoint the client is told to persist
284
+ # and echo back as `since` indefinitely — a junk value is permanent for
285
+ # that device rather than transient. The check is free here (the watermark
286
+ # is an attribute of the project row already fetched above) and rejects
287
+ # nothing legitimate, because the sequence only ever increases. It is
288
+ # deliberately NOT mirrored in UserProgramSyncView, whose watermark costs
289
+ # two aggregates over the caller's whole accessible set: paying them per
290
+ # page would reinstate the O(pages x rows) cost this fix removed, to guard
291
+ # a value that can only ever corrupt the tampering client's own cursor.
292
+ if cursor is not None:
293
+ timestamp = cursor.watermark
294
+ if timestamp > self._watermark(project):
295
+ raise ValidationError({"cursor": "Malformed pagination cursor."})
296
+ else:
297
+ timestamp = self._watermark(project)
298
+
299
+ # Retros are visibility-gated per ADR-0071 §3. A VIEWER on a TEAM_ONLY
300
+ # retro does not receive the retro's raw notes — the sync filters them
301
+ # out at the queryset level. The sync protocol delivers tombstones for
302
+ # visibility-removed rows so WatermelonDB can drop them client-side.
303
+ retro_qs = SprintRetro.objects.filter(sprint__project=project, sync_seq__gt=since)
304
+ if caller_role < Role.MEMBER:
305
+ retro_qs = retro_qs.exclude(team_visibility=RetroVisibility.TEAM_ONLY)
306
+
307
+ retro_action_item_qs = RetroActionItem.objects.filter(
308
+ retro__sprint__project=project, sync_seq__gt=since
309
+ ).select_related("retro")
310
+ if caller_role < Role.MEMBER:
311
+ retro_action_item_qs = retro_action_item_qs.exclude(
312
+ retro__team_visibility=RetroVisibility.TEAM_ONLY
313
+ )
314
+
315
+ # Ordered sync sources: (collection, base queryset, serializer). Each base
316
+ # queryset carries the project scope, the `sync_seq__gt=since` floor,
317
+ # and any RBAC/visibility filters. The pager (#1013) drains them in this
318
+ # fixed order, keyset-seeking on (sync_seq, id) within each, so the
319
+ # response is bounded by page_size without skipping or duplicating rows.
320
+ # The order is stable across pages — do not reorder without a protocol bump.
321
+ sources: list[SyncSource] = [
322
+ (
323
+ "projects",
324
+ Project.objects.filter(pk=project.pk, sync_seq__gt=since),
325
+ SyncProjectSerializer,
326
+ ),
327
+ (
328
+ "tasks",
329
+ # is_phase annotation (ADR-0293): EXISTS a direct non-subtask child.
330
+ # Mirrors the projects app's annotate_tasks_queryset is_phase shape so
331
+ # offline clients receive the same computed rollup flag. Static SQL
332
+ # literal, empty params — no user input interpolated; the ltree
333
+ # operator can't be expressed in the ORM.
334
+ Task.objects.filter(project=project, sync_seq__gt=since)
335
+ .annotate(
336
+ # nosemgrep: avoid-raw-sql
337
+ is_phase=RawSQL(
338
+ "EXISTS("
339
+ " SELECT 1 FROM projects_task c"
340
+ " WHERE c.project_id = projects_task.project_id"
341
+ " AND c.is_deleted = false"
342
+ " AND c.is_subtask = false"
343
+ " AND c.id != projects_task.id"
344
+ " AND c.wbs_path IS NOT NULL"
345
+ " AND projects_task.wbs_path IS NOT NULL"
346
+ " AND c.wbs_path ~ (projects_task.wbs_path::text || '.*{1}')::lquery"
347
+ ")",
348
+ [],
349
+ output_field=BooleanField(),
350
+ )
351
+ )
352
+ .prefetch_related(
353
+ # label_ids array (ADR-0400) — filter tombstoned labels so a
354
+ # deleted label never rides a task's sync payload; O(1) per page.
355
+ Prefetch("labels", queryset=Label.objects.filter(is_deleted=False)),
356
+ # custom_fields map (#2143, ADR-0528) rides the task like label_ids;
357
+ # select_related feeds the field-type resolver + person avatar O(1).
358
+ Prefetch(
359
+ "custom_field_values",
360
+ queryset=TaskCustomFieldValue.objects.select_related("field", "value_user"),
361
+ ),
362
+ ),
363
+ SyncTaskSerializer,
364
+ ),
365
+ (
366
+ "dependencies",
367
+ # Same-project edges only. Cross-project edges (ADR-0120) are
368
+ # deliberately excluded from the single-project offline delta:
369
+ # their successor task lives in a project this client may not have
370
+ # synced, and the offline WASM recompute is single-project. Offline
371
+ # cross-project scheduling arrives with the program-sync slice
372
+ # (D3/D4); until then the sync payload stays byte-identical to the
373
+ # pre-cross-project behaviour.
374
+ Dependency.objects.filter(
375
+ predecessor__project=project,
376
+ successor__project=project,
377
+ sync_seq__gt=since,
378
+ ).select_related("predecessor"),
379
+ SyncDependencySerializer,
380
+ ),
381
+ (
382
+ "calendars",
383
+ # prefetch_related("exceptions") feeds the nested read-only
384
+ # exceptions array on SyncCalendarSerializer (ADR-0193) without a
385
+ # per-calendar lazy load — parity with CalendarViewSet and
386
+ # N+1-safe for the future program-sync multi-calendar slice.
387
+ Calendar.objects.filter(
388
+ pk=project.calendar_id, sync_seq__gt=since
389
+ ).prefetch_related("exceptions")
390
+ if project.calendar_id
391
+ else Calendar.objects.none(),
392
+ SyncCalendarSerializer,
393
+ ),
394
+ (
395
+ "memberships",
396
+ ProjectMembership.objects.filter(project=project, sync_seq__gt=since),
397
+ SyncMembershipSerializer,
398
+ ),
399
+ (
400
+ "risks",
401
+ Risk.objects.filter(project=project, sync_seq__gt=since).prefetch_related("tasks"),
402
+ SyncRiskSerializer,
403
+ ),
404
+ (
405
+ "sprints",
406
+ Sprint.objects.filter(project=project, sync_seq__gt=since),
407
+ SyncSprintSerializer,
408
+ ),
409
+ ("sprint_retros", retro_qs, SyncSprintRetroSerializer),
410
+ ("retro_action_items", retro_action_item_qs, SyncRetroActionItemSerializer),
411
+ (
412
+ "task_suggested_assignees",
413
+ TaskSuggestedAssignee.objects.filter(task__project=project, sync_seq__gt=since),
414
+ SyncTaskSuggestedAssigneeSerializer,
415
+ ),
416
+ (
417
+ "task_links",
418
+ TaskLink.objects.filter(task__project=project, sync_seq__gt=since),
419
+ SyncTaskLinkSerializer,
420
+ ),
421
+ (
422
+ "task_recurrence_rules",
423
+ TaskRecurrenceRule.objects.filter(
424
+ task__project=project, sync_seq__gt=since
425
+ ).select_related("task"),
426
+ SyncTaskRecurrenceRuleSerializer,
427
+ ),
428
+ (
429
+ # Time entries are scoped to the CALLER's own rows (ADR-0185 §6): the
430
+ # per-project delta never leaks a colleague's entries, mirroring the
431
+ # /me/ REST surface's non-surveillance discipline. A soft-deleted entry
432
+ # yields a tombstone through the standard _collect path.
433
+ "time_entries",
434
+ TimeEntry.objects.filter(
435
+ task__project=project,
436
+ user=cast("_User", request.user),
437
+ sync_seq__gt=since,
438
+ ).select_related("task"),
439
+ SyncTimeEntrySerializer,
440
+ ),
441
+ (
442
+ # Label catalog (ADR-0400) — appended to preserve the stable protocol
443
+ # order. Assignment is NOT a source here; it rides Task.label_ids.
444
+ "labels",
445
+ Label.objects.filter(project=project, sync_seq__gt=since),
446
+ SyncLabelSerializer,
447
+ ),
448
+ (
449
+ # Informational task relations (ADR-0455) — appended LAST to preserve
450
+ # the stable protocol order. Same-project relations only: a
451
+ # cross-project relation's counterpart task may live in a project
452
+ # this client has not synced (mirrors the Dependency same-project
453
+ # scoping above). Pull-only, like Dependency — never in the upload
454
+ # WRITABLE_COLLECTIONS. The delta joins via ``source`` since
455
+ # TaskRelation carries no direct project FK.
456
+ "task_relations",
457
+ TaskRelation.objects.filter(
458
+ source__project=project,
459
+ target__project=project,
460
+ sync_seq__gt=since,
461
+ ).select_related("source"),
462
+ SyncTaskRelationSerializer,
463
+ ),
464
+ ]
465
+
466
+ changes, next_cursor, has_more = paginate_changes(
467
+ sources,
468
+ cursor=cursor,
469
+ page_size=page_size,
470
+ collect=self._collect,
471
+ watermark=timestamp,
472
+ version_field="sync_seq",
473
+ )
474
+
475
+ return Response(
476
+ {
477
+ "changes": changes,
478
+ "timestamp": timestamp,
479
+ "next_cursor": next_cursor.encode() if next_cursor is not None else None,
480
+ "has_more": has_more,
481
+ }
482
+ )
483
+
484
+ @staticmethod
485
+ def _resolve_page_size(request: Request) -> int:
486
+ """Clamp the requested ``page_size`` to ``[1, MAX]`` with a sane default (#1013).
487
+
488
+ A missing or non-numeric value falls back to the default rather than 400ing:
489
+ the pager is a transport optimization, not a contract the client must satisfy.
490
+ """
491
+ default = getattr(settings, "TRUEPPM_SYNC_PULL_PAGE_SIZE", 1000)
492
+ maximum = getattr(settings, "TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE", 5000)
493
+ raw = request.query_params.get("page_size")
494
+ if raw is None:
495
+ return default
496
+ try:
497
+ requested = int(raw)
498
+ except (ValueError, TypeError):
499
+ return default
500
+ return max(1, min(requested, maximum))
501
+
502
+ def get_throttles(self) -> list[BaseThrottle]:
503
+ """POST gets the dedicated upload throttle; GET keeps the default `user`
504
+ scope (#2939 — same defect class #2878 fixed in four other views: a bare
505
+ list here *replaces* ``DEFAULT_THROTTLE_CLASSES`` instead of adding to it,
506
+ so this branch resolved to no throttle at all on the delta-export pull,
507
+ the largest read surface in the API). Mobile sync is high-frequency by
508
+ design, so it is bounded by the generous default (1000/min per account,
509
+ ``TRUEPPM_THROTTLE_USER_RATE``) rather than a dedicated tighter scope —
510
+ that is a real decision, not an oversight, and now it is enforced.
511
+ """
512
+ if self.request.method == "POST":
513
+ from trueppm_api.apps.sync.throttles import SyncUploadThrottle
514
+
515
+ return [SyncUploadThrottle()]
516
+ return super().get_throttles()
517
+
518
+ @extend_schema(
519
+ request=SyncUploadRequestSerializer,
520
+ responses={
521
+ 200: SyncUploadResponseSerializer,
522
+ 403: OpenApiResponse(
523
+ description=(
524
+ "Not a project member, below the Team Member role (Viewer), project "
525
+ "archived, or a row the caller may not write: per-row edit/delete "
526
+ "rights, a field-level policy such as PM-only estimates or backlog "
527
+ "fields, or creating a task from a role that cannot author the plan "
528
+ "(the Scheduler band and Enterprise 2xx-band roles). The whole batch "
529
+ "is rolled back."
530
+ )
531
+ ),
532
+ 409: OpenApiTypes.OBJECT,
533
+ },
534
+ description=(
535
+ "Upload a WatermelonDB delta batch. Carries a client_batch_id for "
536
+ "all-or-nothing, idempotent replay (ADR-0082)."
537
+ ),
538
+ )
539
+ def post(self, request: Request, pk: str) -> Response:
540
+ """Upload a WatermelonDB delta batch with transactional atomicity (ADR-0082).
541
+
542
+ The batch carries a client-generated ``client_batch_id``. The first
543
+ request to win the unique-constraint race applies the delta and records
544
+ the batch + its response in one atomic transaction (all-or-nothing); a
545
+ retry carrying the same id within the freshness window replays the stored
546
+ response without re-applying. See ADR-0082 §D for the full algorithm.
547
+ """
548
+ from trueppm_api.apps.access.models import Role
549
+ from trueppm_api.apps.sync.models import SyncBatch, SyncBatchStatus
550
+ from trueppm_api.apps.sync.throttles import sync_batch_concurrency_slot
551
+
552
+ envelope = SyncUploadRequestSerializer(data=request.data)
553
+ envelope.is_valid(raise_exception=True)
554
+ client_batch_id = envelope.validated_data["client_batch_id"]
555
+ # The high-water mark the client last pulled — the default per-row base for the
556
+ # field-level conflict guard (#1718). Advisory only: absent → last-writer-wins.
557
+ last_pulled_at = envelope.validated_data.get("last_pulled_at")
558
+ # Feed the RAW changes map (not envelope.validated_data["changes"]) to
559
+ # apply_task_changes so it stays the single authority for the writable-
560
+ # collection whitelist (ADR-0082 §B). The typed nested serializer (#786)
561
+ # documents the shape but ignores unknown collection keys; passing the raw
562
+ # map keeps the explicit "unsupported collection → 400" guard byte-identical
563
+ # for existing clients rather than silently dropping the foreign collection.
564
+ # The envelope serializer above already rejected a non-mapping body with a
565
+ # 400 ("Expected a dictionary, but got list"), so the raw body is provably a
566
+ # mapping by here — narrowed rather than re-guarded.
567
+ changes = cast("dict[str, Any]", request.data)["changes"]
568
+
569
+ try:
570
+ project = Project.objects.get(pk=pk, is_deleted=False)
571
+ except Project.DoesNotExist as err:
572
+ raise NotFound("Project not found.") from err
573
+
574
+ # RBAC: only project members with a write role may push. Per-row
575
+ # ownership (Member edits own tasks; Scheduler cannot edit content) is
576
+ # enforced inside apply_task_changes, mirroring IsProjectMemberWriteOrOwn.
577
+ # Creating a row also requires the plan-authoring rule (IsProjectPlanAuthor
578
+ # on REST create), so the Scheduler band cannot create either.
579
+ role = _membership_role(request, project.pk)
580
+ if role is None:
581
+ raise PermissionDenied("You must be a member of this project.")
582
+ if role < Role.MEMBER:
583
+ raise PermissionDenied("You need at least Team Member role to upload changes.")
584
+
585
+ # Archived projects are hard read-only (#530) — mirror IsProjectNotArchived,
586
+ # which gates every REST write. Without this, the upload path would be a
587
+ # back-channel to mutate an archived project.
588
+ if project.is_archived:
589
+ raise PermissionDenied(
590
+ "This project is archived and cannot be modified. Unarchive it first."
591
+ )
592
+
593
+ ttl = getattr(settings, "TRUEPPM_SYNC_BATCH_RETENTION_HOURS", 24)
594
+
595
+ # Fast path: a fresh, completed duplicate replays its stored response.
596
+ # Scoped to (project, actor) so a member can never replay another
597
+ # project's — or another *user's* — batch by reusing its client_batch_id.
598
+ # The stored response carries that actor's task ids, server_versions, and
599
+ # watermark; cross-actor replay would leak it (#894, #887 IDOR guard).
600
+ existing = SyncBatch.objects.filter( # type: ignore[misc]
601
+ project=project, actor_user=request.user, client_batch_id=client_batch_id
602
+ ).first()
603
+ if (
604
+ existing is not None
605
+ and existing.status == SyncBatchStatus.COMPLETED
606
+ and existing.is_fresh(ttl_hours=ttl)
607
+ ):
608
+ return Response(existing.response_body, status=existing.response_status)
609
+
610
+ # Bound simultaneously in-flight heavy batch applies for this user (#1756).
611
+ # Wraps the whole apply — including the IntegrityError re-run — so one
612
+ # logical upload holds exactly one slot. Placed AFTER the fast-path replay
613
+ # so a cheap idempotent retry never consumes a slot. Raises 429 (Throttled)
614
+ # when the per-user concurrency cap is exceeded; fails open on Redis error.
615
+ with sync_batch_concurrency_slot(request.user.pk):
616
+ try:
617
+ return self._apply_and_record(
618
+ request, project, role, client_batch_id, changes, last_pulled_at
619
+ )
620
+ except IntegrityError:
621
+ # A concurrent duplicate committed between our check and create, or a
622
+ # stale row exists. Re-fetch (project + actor-scoped) and resolve.
623
+ existing = SyncBatch.objects.filter( # type: ignore[misc]
624
+ project=project, actor_user=request.user, client_batch_id=client_batch_id
625
+ ).first()
626
+ if existing is None:
627
+ raise # genuinely unexpected — surface it
628
+ if existing.is_fresh(ttl_hours=ttl):
629
+ if existing.status == SyncBatchStatus.COMPLETED:
630
+ return Response(existing.response_body, status=existing.response_status)
631
+ # Fresh but still pending: a true concurrent race whose winner has
632
+ # not committed yet. Ask the client to retry — the next attempt
633
+ # hits the fast-path replay.
634
+ return Response(
635
+ {"detail": "This batch is already being processed; retry shortly."},
636
+ status=status.HTTP_409_CONFLICT,
637
+ )
638
+ # Expired row is blocking a re-run: drop it and apply once more. After
639
+ # the freshness window the same client_batch_id is allowed to re-run.
640
+ existing.delete()
641
+ return self._apply_and_record(
642
+ request, project, role, client_batch_id, changes, last_pulled_at
643
+ )
644
+
645
+ def _apply_and_record(
646
+ self,
647
+ request: Request,
648
+ project: Project,
649
+ role: int,
650
+ client_batch_id: Any,
651
+ changes: dict[str, Any],
652
+ last_pulled_at: int | None = None,
653
+ ) -> Response:
654
+ """Apply the delta and snapshot the response inside one atomic batch.
655
+
656
+ The SyncBatch row is the **first** write so the unique constraint
657
+ serializes concurrent duplicates. Any failure (RBAC, validation, DB)
658
+ rolls back the row writes *and* the batch row together — nothing commits,
659
+ and the client re-uploads the whole batch under the same id.
660
+ """
661
+ from trueppm_api.apps.scheduling.services import enqueue_recalculate
662
+ from trueppm_api.apps.sync.broadcast import broadcast_board_event
663
+ from trueppm_api.apps.sync.models import SyncBatch, SyncBatchStatus
664
+ from trueppm_api.apps.sync.sequence import coalesce_sync_seq
665
+ from trueppm_api.apps.sync.upload import apply_task_changes
666
+
667
+ with transaction.atomic():
668
+ batch = SyncBatch.objects.create(
669
+ client_batch_id=client_batch_id,
670
+ project=project,
671
+ actor_user=request.user, # type: ignore[misc]
672
+ status=SyncBatchStatus.PENDING,
673
+ )
674
+ # Draw one sync_seq per project for the whole batch rather than one
675
+ # per row (#1527, ADR-0686). The allocations run inside this atomic
676
+ # block, so a rollback discards them with the rows.
677
+ with coalesce_sync_seq():
678
+ applied = apply_task_changes(
679
+ project=project,
680
+ request=request,
681
+ role=role,
682
+ changes=changes,
683
+ last_pulled_at=last_pulled_at,
684
+ )
685
+ body = {
686
+ "client_batch_id": str(client_batch_id),
687
+ "applied": {
688
+ "tasks": {
689
+ "created": applied.created,
690
+ "updated": applied.updated,
691
+ "deleted": applied.deleted,
692
+ }
693
+ },
694
+ # Per-row lost-update conflicts (#1718): rows that were NOT applied
695
+ # because a concurrent writer changed an overlapping field. The client
696
+ # reloads these and reapplies; empty on the common no-conflict batch.
697
+ "conflicts": {"tasks": applied.conflicts},
698
+ "timestamp": applied.max_version,
699
+ }
700
+ batch.status = SyncBatchStatus.COMPLETED
701
+ batch.response_body = body
702
+ batch.response_status = status.HTTP_200_OK
703
+ batch.save(update_fields=["status", "response_body", "response_status"])
704
+
705
+ # Side effects on commit, mirroring single-row writes (inbound_sync):
706
+ # one coalesced CPM recalc + one coalesced board event, so web clients
707
+ # react and a rolled-back batch broadcasts nothing.
708
+ #
709
+ # A delta batch can carry up to TRUEPPM_SYNC_BATCH_MAX_ROWS (500) rows.
710
+ # Emitting one broadcast per applied row (the previous behavior) issued
711
+ # up to 500 async_to_sync(group_send) round-trips at commit on a single
712
+ # upload — under a reconnect storm this overflowed the channel-layer
713
+ # inbox and dropped presence/CPM events (#809). Coalesce into a single
714
+ # tasks_bulk_mutated event (same pattern as sprint-close carry-over,
715
+ # projects/tasks.py); the web handler invalidates the whole task query
716
+ # on it, so per-row granularity is not needed.
717
+ project_id = str(project.pk)
718
+ if applied.changed:
719
+ transaction.on_commit(partial(enqueue_recalculate, project_id))
720
+ mutated_ids = [task_id for _event_type, task_id in applied.events]
721
+ if mutated_ids:
722
+ # default-arg binding (not closure capture) so a future branch
723
+ # can't late-bind the ids — matches the carry-over pattern.
724
+ def _broadcast_bulk(pid: str = project_id, ids: list[str] = mutated_ids) -> None:
725
+ broadcast_board_event(pid, "tasks_bulk_mutated", {"task_ids": ids})
726
+
727
+ transaction.on_commit(_broadcast_bulk)
728
+
729
+ return Response(body, status=status.HTTP_200_OK)
730
+
731
+ @staticmethod
732
+ def _collect(qs: Any, serializer_class: Any) -> dict[str, Any]:
733
+ """Split a queryset into WatermelonDB 'updated' and 'deleted' buckets.
734
+
735
+ All changed rows are returned in 'updated' (upsert semantics).
736
+ Soft-deleted rows have their IDs returned in 'deleted' as well.
737
+ 'created' is always empty — WatermelonDB upserts handle insert/update.
738
+ """
739
+ rows = list(qs)
740
+ live = [r for r in rows if not r.is_deleted]
741
+ deleted_ids = [str(r.pk) for r in rows if r.is_deleted]
742
+ return {
743
+ "created": [],
744
+ "updated": serializer_class(live, many=True).data,
745
+ "deleted": deleted_ids,
746
+ }
747
+
748
+ @classmethod
749
+ def _watermark(cls, project: Project) -> int:
750
+ """Return the project's sync cursor high-water mark (ADR-0686, #2491).
751
+
752
+ ``Project.last_sync_version`` is the allocator every synced row in this
753
+ project drew its ``sync_seq`` from (``apps/sync/sequence.py``), so it is
754
+ by construction greater than or equal to every row's cursor — the safe
755
+ upper bound the client adopts as its next ``since``.
756
+
757
+ It used to cache ``MAX(server_version)`` over a 14-table ``UNION ALL``
758
+ (ADR-0142). The cache was faithful; the value was not a valid ordering,
759
+ because ``server_version`` counts one row's own saves. The union, its
760
+ ``SYNC_WATERMARK_USE_COLUMN`` fallback, and the receiver-vs-union
761
+ conformance test are gone with it: there is nothing left to derive the
762
+ column from, and nothing left to drift.
763
+ """
764
+ return int(project.last_sync_version)
765
+
766
+
767
+ class UserProgramSyncView(IdempotencyMixin, APIView):
768
+ """Pull-only delta sync for the caller's Program + ProgramMembership rows.
769
+
770
+ The project-scoped :class:`ProjectSyncView` cannot reach user-scoped Program
771
+ rows (ADR-0070 §Sync): the ``Project.program`` FK syncs offline so the program
772
+ badge renders on cached projects, but the program list itself does not — on a
773
+ job site the program layer vanishes and the user falls back to a flat project
774
+ list. This endpoint closes that gap, delivering Program and ProgramMembership
775
+ deltas for every program the caller is a member of. It is the 0.4 follow-up
776
+ ADR-0070 §Sync flagged; no new ADR (see the updated §Sync note).
777
+
778
+ Scope is derived entirely from the caller's own live memberships, so — unlike
779
+ the project endpoint — there is **no path parameter to tamper with**: no IDOR
780
+ surface. All co-members of the caller's programs are returned (parity with the
781
+ project endpoint, which returns every ``ProjectMembership`` for the project),
782
+ which lets the client enforce offline program RBAC and render the roster.
783
+
784
+ Tombstones: a program soft-deleted while the caller is still a member, and any
785
+ co-member membership soft-deleted, are delivered as ``deleted`` ids.
786
+
787
+ Known v1 limitation (mirrors ``ProjectSyncView``): if the caller's *own*
788
+ membership is soft-deleted (they are removed from a program), that row drops
789
+ out of the accessible set and its tombstone is never delivered — the online
790
+ path 403s on the next access and the client evicts the stale program on a full
791
+ re-sync. Full write-side offline for programs is out of scope for v1 (reads
792
+ first, per #561).
793
+
794
+ Usage:
795
+ GET /api/v1/sync/user/programs/?since=0
796
+ """
797
+
798
+ # Read-only pull; the generic Idempotency-Key path adds nothing (parity with
799
+ # ProjectSyncView.get, ADR-0170).
800
+ idempotency_exempt = True
801
+ permission_classes = [IsAuthenticated]
802
+
803
+ @extend_schema(
804
+ summary="Pull the offline sync delta for the caller's programs",
805
+ parameters=[
806
+ OpenApiParameter(
807
+ name="since",
808
+ type=OpenApiTypes.INT,
809
+ location=OpenApiParameter.QUERY,
810
+ required=False,
811
+ default=0,
812
+ description=(
813
+ "WatermelonDB high-water mark. Returns only Program / "
814
+ "ProgramMembership rows whose `server_version` is strictly "
815
+ "greater than this value. Omit (or pass `0`) for a full initial "
816
+ "sync; adopt the previous response's `timestamp` for the delta."
817
+ ),
818
+ ),
819
+ OpenApiParameter(
820
+ name="cursor",
821
+ type=OpenApiTypes.STR,
822
+ location=OpenApiParameter.QUERY,
823
+ required=False,
824
+ description=(
825
+ "Opaque pagination continuation token (#1013). Omit on the "
826
+ "first request; on each subsequent request pass the previous "
827
+ "response's `next_cursor`. Keep `since` constant for the whole "
828
+ "paging session. Loop until `has_more` is false, then adopt "
829
+ "`timestamp` as the next `since`."
830
+ ),
831
+ ),
832
+ OpenApiParameter(
833
+ name="page_size",
834
+ type=OpenApiTypes.INT,
835
+ location=OpenApiParameter.QUERY,
836
+ required=False,
837
+ description=(
838
+ "Maximum rows returned across both collections in one page. "
839
+ "Defaults to `TRUEPPM_SYNC_PULL_PAGE_SIZE`; clamped to "
840
+ "`TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE`."
841
+ ),
842
+ ),
843
+ ],
844
+ responses={200: SyncPullResponseSerializer},
845
+ )
846
+ def get(self, request: Request) -> Response:
847
+ # Validate `since` — identical contract to ProjectSyncView.
848
+ since_raw = request.query_params.get("since", "0")
849
+ try:
850
+ since = int(since_raw)
851
+ if since < 0:
852
+ raise ValueError
853
+ except (ValueError, TypeError) as err:
854
+ raise ValidationError({"since": "Must be a non-negative integer."}) from err
855
+
856
+ cursor_raw = request.query_params.get("cursor")
857
+ cursor = SyncCursor.decode(cursor_raw) if cursor_raw else None
858
+ page_size = ProjectSyncView._resolve_page_size(request)
859
+
860
+ # Accessible set: programs the caller has a live membership on, kept as a
861
+ # queryset so Postgres evaluates it as a single subquery in the delta reads
862
+ # rather than round-tripping every id into Python.
863
+ accessible_ids = ProgramMembership.objects.filter(
864
+ user=cast("_User", request.user), is_deleted=False
865
+ ).values("program_id")
866
+
867
+ # Snapshot the high-water mark BEFORE the delta queries. server_version is
868
+ # monotonic, so a write landing after this snapshot moves the row forward in
869
+ # (server_version, id) order — delivered this pull (re-applied under upsert)
870
+ # or the next (its version > timestamp = next since); never lost. This is
871
+ # the same concurrency argument pagination.py makes, and the same
872
+ # watermark-before-rows ordering ProjectSyncView relies on. Neither view
873
+ # opens a REPEATABLE READ block; both run at READ COMMITTED, and the
874
+ # ordering — not the isolation level — is what closes the window.
875
+ #
876
+ # This slice keys on sync_seq, drawn from the installation-wide program
877
+ # sequence (ADR-0747, #2498). It used to key on the per-row server_version
878
+ # and carried the ordering defect ADR-0686 fixed for projects: a hot program
879
+ # raised the watermark above a cold program's row counters and those rows
880
+ # dropped out of the delta permanently. The project sequence could not fix
881
+ # it — programs have no single owning row — so program-scoped writes order
882
+ # against one installation counter instead, which restores a scalar cursor's
883
+ # validity without changing the wire contract.
884
+ #
885
+ # As in ProjectSyncView, the watermark is session state: a continuation page
886
+ # echoes the cursor's pinned value instead of recomputing one that could sit
887
+ # above an already-drained collection's mid-drain write (#2568).
888
+ timestamp = cursor.watermark if cursor is not None else self._watermark(accessible_ids)
889
+
890
+ # Program rows include soft-deleted ones so a program deleted while the
891
+ # caller is still a member arrives as a tombstone. Memberships cover every
892
+ # co-member of the caller's programs (offline RBAC + roster), soft-deleted
893
+ # rows included. Order is fixed — do not reorder without a protocol bump.
894
+ sources: list[SyncSource] = [
895
+ (
896
+ "programs",
897
+ Program.objects.filter(id__in=accessible_ids, sync_seq__gt=since),
898
+ SyncProgramSerializer,
899
+ ),
900
+ (
901
+ "program_memberships",
902
+ ProgramMembership.objects.filter(program_id__in=accessible_ids, sync_seq__gt=since),
903
+ SyncProgramMembershipSerializer,
904
+ ),
905
+ ]
906
+
907
+ changes, next_cursor, has_more = paginate_changes(
908
+ sources,
909
+ cursor=cursor,
910
+ page_size=page_size,
911
+ collect=ProjectSyncView._collect,
912
+ watermark=timestamp,
913
+ # Must match the delta filter above: the pager orders on this column
914
+ # and the sources floor on it. Ordering on one while filtering the
915
+ # other pages a stream the floor does not bound (ADR-0747, #2498).
916
+ version_field="sync_seq",
917
+ )
918
+
919
+ return Response(
920
+ {
921
+ "changes": changes,
922
+ "timestamp": timestamp,
923
+ "next_cursor": next_cursor.encode() if next_cursor is not None else None,
924
+ "has_more": has_more,
925
+ }
926
+ )
927
+
928
+ @staticmethod
929
+ def _watermark(accessible_ids: Any) -> int:
930
+ """The installation-wide program sequence's high-water mark (ADR-0747, #2498).
931
+
932
+ ``ProgramSyncSequence.value`` is the allocator every synced ``Program`` and
933
+ ``ProgramMembership`` row drew its ``sync_seq`` from, so it is by
934
+ construction greater than or equal to every row's cursor — the safe upper
935
+ bound the client adopts as its next ``since``. One row read, no aggregate.
936
+
937
+ It used to be ``MAX(server_version)`` across the caller's programs and their
938
+ memberships. That value was faithful and was not a valid ordering:
939
+ ``server_version`` counts a single row's saves, so a frequently-edited
940
+ program raised the checkpoint above every other program's row counter and
941
+ dropped those rows from the delta permanently (#2498) — the same defect
942
+ ADR-0686 fixed for projects, one level up.
943
+
944
+ ``accessible_ids`` is no longer read: the checkpoint is a property of the
945
+ installation's write log, not of the caller's accessible set. Scoping it per
946
+ caller is what made it a maximum-over-many-rows in the first place. The
947
+ parameter is kept so the call site and ``ProjectSyncView._watermark`` keep
948
+ one shape.
949
+ """
950
+ from trueppm_api.apps.sync.models import ProgramSyncSequence
951
+
952
+ value = (
953
+ ProgramSyncSequence.objects.filter(pk=ProgramSyncSequence.SINGLETON_PK)
954
+ .values_list("value", flat=True)
955
+ .first()
956
+ )
957
+ return value or 0