trueppm-api 0.4.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (759) hide show
  1. trueppm_api/__init__.py +14 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +164 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  24. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  25. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  26. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  27. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
  28. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  29. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  30. trueppm_api/apps/access/migrations/__init__.py +0 -0
  31. trueppm_api/apps/access/models.py +588 -0
  32. trueppm_api/apps/access/permissions.py +2837 -0
  33. trueppm_api/apps/access/serializers.py +946 -0
  34. trueppm_api/apps/access/services.py +667 -0
  35. trueppm_api/apps/access/signals.py +103 -0
  36. trueppm_api/apps/access/tasks.py +50 -0
  37. trueppm_api/apps/access/throttles.py +95 -0
  38. trueppm_api/apps/access/urls.py +191 -0
  39. trueppm_api/apps/access/views.py +1931 -0
  40. trueppm_api/apps/agents/__init__.py +6 -0
  41. trueppm_api/apps/agents/apps.py +11 -0
  42. trueppm_api/apps/agents/canonical.py +73 -0
  43. trueppm_api/apps/agents/deferred.py +108 -0
  44. trueppm_api/apps/agents/management/__init__.py +0 -0
  45. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  46. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  47. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  48. trueppm_api/apps/agents/middleware.py +44 -0
  49. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  50. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  51. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  52. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  53. trueppm_api/apps/agents/models.py +371 -0
  54. trueppm_api/apps/agents/refusal.py +103 -0
  55. trueppm_api/apps/agents/serializers.py +85 -0
  56. trueppm_api/apps/agents/services.py +397 -0
  57. trueppm_api/apps/agents/signals.py +30 -0
  58. trueppm_api/apps/agents/urls.py +15 -0
  59. trueppm_api/apps/agents/views.py +108 -0
  60. trueppm_api/apps/csvimport/__init__.py +12 -0
  61. trueppm_api/apps/csvimport/apps.py +11 -0
  62. trueppm_api/apps/csvimport/mapping.py +363 -0
  63. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  64. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  65. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  66. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  67. trueppm_api/apps/csvimport/models.py +111 -0
  68. trueppm_api/apps/csvimport/parser.py +1825 -0
  69. trueppm_api/apps/csvimport/services.py +48 -0
  70. trueppm_api/apps/csvimport/tasks.py +418 -0
  71. trueppm_api/apps/csvimport/template.py +37 -0
  72. trueppm_api/apps/csvimport/urls.py +50 -0
  73. trueppm_api/apps/csvimport/views.py +793 -0
  74. trueppm_api/apps/history/__init__.py +0 -0
  75. trueppm_api/apps/history/apps.py +12 -0
  76. trueppm_api/apps/history/changelog.py +359 -0
  77. trueppm_api/apps/history/diff_policy.py +177 -0
  78. trueppm_api/apps/history/serializers.py +104 -0
  79. trueppm_api/apps/history/signals.py +82 -0
  80. trueppm_api/apps/history/tasks.py +105 -0
  81. trueppm_api/apps/history/urls.py +38 -0
  82. trueppm_api/apps/history/views.py +443 -0
  83. trueppm_api/apps/idempotency/__init__.py +0 -0
  84. trueppm_api/apps/idempotency/apps.py +11 -0
  85. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  86. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  87. trueppm_api/apps/idempotency/mixins.py +226 -0
  88. trueppm_api/apps/idempotency/models.py +75 -0
  89. trueppm_api/apps/idempotency/tasks.py +61 -0
  90. trueppm_api/apps/integrations/__init__.py +13 -0
  91. trueppm_api/apps/integrations/apps.py +59 -0
  92. trueppm_api/apps/integrations/connections.py +806 -0
  93. trueppm_api/apps/integrations/encryption.py +92 -0
  94. trueppm_api/apps/integrations/external_sources.py +837 -0
  95. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  96. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  97. trueppm_api/apps/integrations/http.py +403 -0
  98. trueppm_api/apps/integrations/me_work.py +218 -0
  99. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  100. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  101. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  102. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  103. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  104. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  105. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  106. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  107. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  108. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  109. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  110. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  111. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  112. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  113. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  114. trueppm_api/apps/integrations/models.py +622 -0
  115. trueppm_api/apps/integrations/notification_channels.py +57 -0
  116. trueppm_api/apps/integrations/opengraph.py +229 -0
  117. trueppm_api/apps/integrations/outgoing.py +201 -0
  118. trueppm_api/apps/integrations/providers.py +519 -0
  119. trueppm_api/apps/integrations/registry.py +345 -0
  120. trueppm_api/apps/integrations/serializers.py +453 -0
  121. trueppm_api/apps/integrations/services.py +171 -0
  122. trueppm_api/apps/integrations/tasks.py +731 -0
  123. trueppm_api/apps/integrations/throttles.py +165 -0
  124. trueppm_api/apps/integrations/urls.py +77 -0
  125. trueppm_api/apps/integrations/views.py +1106 -0
  126. trueppm_api/apps/jiraimport/__init__.py +8 -0
  127. trueppm_api/apps/jiraimport/apps.py +11 -0
  128. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  129. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  130. trueppm_api/apps/jiraimport/models.py +70 -0
  131. trueppm_api/apps/jiraimport/parser.py +350 -0
  132. trueppm_api/apps/jiraimport/services.py +46 -0
  133. trueppm_api/apps/jiraimport/tasks.py +277 -0
  134. trueppm_api/apps/jiraimport/urls.py +15 -0
  135. trueppm_api/apps/jiraimport/views.py +153 -0
  136. trueppm_api/apps/msproject/__init__.py +0 -0
  137. trueppm_api/apps/msproject/apps.py +11 -0
  138. trueppm_api/apps/msproject/dataclasses.py +153 -0
  139. trueppm_api/apps/msproject/exporter.py +500 -0
  140. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  141. trueppm_api/apps/msproject/importer.py +1095 -0
  142. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  143. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  144. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  145. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  146. trueppm_api/apps/msproject/models.py +81 -0
  147. trueppm_api/apps/msproject/parser.py +1231 -0
  148. trueppm_api/apps/msproject/serializers.py +136 -0
  149. trueppm_api/apps/msproject/services.py +58 -0
  150. trueppm_api/apps/msproject/tasks.py +436 -0
  151. trueppm_api/apps/msproject/urls.py +39 -0
  152. trueppm_api/apps/msproject/views.py +531 -0
  153. trueppm_api/apps/notifications/__init__.py +7 -0
  154. trueppm_api/apps/notifications/apps.py +15 -0
  155. trueppm_api/apps/notifications/backfill.py +79 -0
  156. trueppm_api/apps/notifications/categories.py +112 -0
  157. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  158. trueppm_api/apps/notifications/digests.py +447 -0
  159. trueppm_api/apps/notifications/email_backend.py +330 -0
  160. trueppm_api/apps/notifications/email_health.py +128 -0
  161. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  162. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
  163. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  164. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  165. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  166. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  167. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  168. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  169. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  170. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  171. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  172. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  173. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  174. trueppm_api/apps/notifications/models.py +1197 -0
  175. trueppm_api/apps/notifications/receivers.py +155 -0
  176. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  177. trueppm_api/apps/notifications/serializers.py +961 -0
  178. trueppm_api/apps/notifications/services.py +1451 -0
  179. trueppm_api/apps/notifications/tasks.py +711 -0
  180. trueppm_api/apps/notifications/throttles.py +98 -0
  181. trueppm_api/apps/notifications/urls.py +97 -0
  182. trueppm_api/apps/notifications/views.py +904 -0
  183. trueppm_api/apps/observability/__init__.py +0 -0
  184. trueppm_api/apps/observability/apps.py +31 -0
  185. trueppm_api/apps/observability/logging.py +213 -0
  186. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  187. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  188. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  189. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  190. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  191. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  192. trueppm_api/apps/observability/models.py +141 -0
  193. trueppm_api/apps/observability/otel/__init__.py +45 -0
  194. trueppm_api/apps/observability/otel/attributes.py +247 -0
  195. trueppm_api/apps/observability/otel/export_health.py +533 -0
  196. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  197. trueppm_api/apps/observability/otel/metrics.py +551 -0
  198. trueppm_api/apps/observability/otel/provider.py +511 -0
  199. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  200. trueppm_api/apps/observability/purge_registry.py +124 -0
  201. trueppm_api/apps/observability/retention.py +139 -0
  202. trueppm_api/apps/observability/selectors.py +969 -0
  203. trueppm_api/apps/observability/serializers.py +142 -0
  204. trueppm_api/apps/observability/services.py +407 -0
  205. trueppm_api/apps/observability/tasks.py +266 -0
  206. trueppm_api/apps/observability/urls.py +27 -0
  207. trueppm_api/apps/observability/views.py +523 -0
  208. trueppm_api/apps/profiles/__init__.py +0 -0
  209. trueppm_api/apps/profiles/apps.py +15 -0
  210. trueppm_api/apps/profiles/constants.py +54 -0
  211. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  212. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  213. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  214. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  215. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  216. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  217. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  218. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  219. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  220. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  221. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  222. trueppm_api/apps/profiles/models.py +276 -0
  223. trueppm_api/apps/profiles/serializers.py +199 -0
  224. trueppm_api/apps/profiles/services.py +466 -0
  225. trueppm_api/apps/profiles/views.py +124 -0
  226. trueppm_api/apps/projects/__init__.py +0 -0
  227. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  228. trueppm_api/apps/projects/amend.py +133 -0
  229. trueppm_api/apps/projects/apps.py +38 -0
  230. trueppm_api/apps/projects/asset_feed.py +459 -0
  231. trueppm_api/apps/projects/asset_views.py +317 -0
  232. trueppm_api/apps/projects/attachment_policy.py +278 -0
  233. trueppm_api/apps/projects/authentication.py +449 -0
  234. trueppm_api/apps/projects/backfill.py +287 -0
  235. trueppm_api/apps/projects/backlog_services.py +324 -0
  236. trueppm_api/apps/projects/backlog_views.py +302 -0
  237. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  238. trueppm_api/apps/projects/blocker_services.py +510 -0
  239. trueppm_api/apps/projects/board_activity.py +462 -0
  240. trueppm_api/apps/projects/board_activity_views.py +186 -0
  241. trueppm_api/apps/projects/board_lanes.py +134 -0
  242. trueppm_api/apps/projects/bulk_settings.py +194 -0
  243. trueppm_api/apps/projects/bundled_templates.py +215 -0
  244. trueppm_api/apps/projects/calendar_settings.py +180 -0
  245. trueppm_api/apps/projects/ceremony_views.py +244 -0
  246. trueppm_api/apps/projects/commit_moment.py +204 -0
  247. trueppm_api/apps/projects/config_notice.py +1325 -0
  248. trueppm_api/apps/projects/custom_field_values.py +266 -0
  249. trueppm_api/apps/projects/decisions_services.py +93 -0
  250. trueppm_api/apps/projects/decisions_views.py +94 -0
  251. trueppm_api/apps/projects/estimation_scale.py +126 -0
  252. trueppm_api/apps/projects/export_bundle.py +353 -0
  253. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
  254. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
  255. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
  256. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
  257. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
  258. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  259. trueppm_api/apps/projects/inbound_sync.py +555 -0
  260. trueppm_api/apps/projects/iteration_label.py +85 -0
  261. trueppm_api/apps/projects/lifecycle.py +191 -0
  262. trueppm_api/apps/projects/management/__init__.py +0 -0
  263. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  264. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  265. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  266. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  267. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  268. trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
  269. trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
  270. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
  271. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  272. trueppm_api/apps/projects/mcp_settings.py +305 -0
  273. trueppm_api/apps/projects/methodology.py +178 -0
  274. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  275. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
  276. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  277. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  278. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  279. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  280. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  281. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  282. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  283. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  284. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  285. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  286. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  287. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  288. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  289. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  290. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  291. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  292. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  293. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  294. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  295. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  296. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  297. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  298. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  299. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  300. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  301. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  302. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  303. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  304. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  305. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  306. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  307. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  308. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  309. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  310. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  311. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  312. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  313. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  314. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  315. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  316. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  317. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  318. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  319. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  320. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  321. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  322. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  323. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  324. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  325. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  326. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  327. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  328. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  329. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  330. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  331. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  332. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  333. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  334. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  335. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  336. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  337. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  338. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  339. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  340. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  341. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  342. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  343. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  344. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  345. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  346. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  347. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  348. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  349. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  350. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  351. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  352. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  353. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  354. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  355. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  356. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  357. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  358. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  359. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  360. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  361. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  362. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  363. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  364. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  365. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  366. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  367. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  368. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
  369. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  370. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  371. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  372. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  373. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  374. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  375. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  376. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  377. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  378. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  379. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
  380. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  381. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  382. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  383. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  384. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  385. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
  386. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  387. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  388. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  389. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  390. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  391. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  392. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  393. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  394. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  395. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  396. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  397. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  398. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  399. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  400. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  401. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  402. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  403. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  404. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  405. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  406. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  407. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  408. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  409. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  410. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  411. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  412. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  413. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  414. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  415. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  416. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  417. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  418. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  419. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  420. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  421. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  422. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  423. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  424. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  425. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  426. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  427. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  428. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  429. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  430. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  431. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  432. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  433. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  434. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  435. trueppm_api/apps/projects/models.py +8946 -0
  436. trueppm_api/apps/projects/poker_services.py +154 -0
  437. trueppm_api/apps/projects/poker_views.py +398 -0
  438. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  439. trueppm_api/apps/projects/program_label_services.py +94 -0
  440. trueppm_api/apps/projects/program_label_views.py +189 -0
  441. trueppm_api/apps/projects/program_rollup.py +575 -0
  442. trueppm_api/apps/projects/program_schedule.py +745 -0
  443. trueppm_api/apps/projects/program_views.py +3220 -0
  444. trueppm_api/apps/projects/project_templates.py +422 -0
  445. trueppm_api/apps/projects/receivers.py +234 -0
  446. trueppm_api/apps/projects/refusal_codes.py +140 -0
  447. trueppm_api/apps/projects/reorder_services.py +145 -0
  448. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  449. trueppm_api/apps/projects/retro_board_services.py +368 -0
  450. trueppm_api/apps/projects/retro_services.py +285 -0
  451. trueppm_api/apps/projects/risk_import.py +396 -0
  452. trueppm_api/apps/projects/row_identity.py +113 -0
  453. trueppm_api/apps/projects/schema_migrations.py +228 -0
  454. trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
  455. trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
  456. trueppm_api/apps/projects/seed/__init__.py +61 -0
  457. trueppm_api/apps/projects/seed/exporter.py +1644 -0
  458. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  459. trueppm_api/apps/projects/seed/importer.py +2390 -0
  460. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  461. trueppm_api/apps/projects/seed/reldates.py +126 -0
  462. trueppm_api/apps/projects/seed/replace.py +126 -0
  463. trueppm_api/apps/projects/seed/replay.py +1691 -0
  464. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  465. trueppm_api/apps/projects/seed/samples.py +427 -0
  466. trueppm_api/apps/projects/seed/validation.py +1149 -0
  467. trueppm_api/apps/projects/serializers.py +12310 -0
  468. trueppm_api/apps/projects/services.py +6966 -0
  469. trueppm_api/apps/projects/share_serializers.py +132 -0
  470. trueppm_api/apps/projects/share_services.py +325 -0
  471. trueppm_api/apps/projects/share_views.py +374 -0
  472. trueppm_api/apps/projects/sharing_settings.py +187 -0
  473. trueppm_api/apps/projects/signal_privacy_services.py +909 -0
  474. trueppm_api/apps/projects/signal_privacy_views.py +476 -0
  475. trueppm_api/apps/projects/signals.py +132 -0
  476. trueppm_api/apps/projects/slip_conflict.py +266 -0
  477. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  478. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  479. trueppm_api/apps/projects/standup.py +304 -0
  480. trueppm_api/apps/projects/standup_views.py +99 -0
  481. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  482. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  483. trueppm_api/apps/projects/surface_visibility.py +126 -0
  484. trueppm_api/apps/projects/task_batch_services.py +379 -0
  485. trueppm_api/apps/projects/task_bulk.py +1152 -0
  486. trueppm_api/apps/projects/task_classification.py +561 -0
  487. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  488. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  489. trueppm_api/apps/projects/task_grouping.py +754 -0
  490. trueppm_api/apps/projects/tasks.py +1917 -0
  491. trueppm_api/apps/projects/template_divergence.py +168 -0
  492. trueppm_api/apps/projects/template_services.py +160 -0
  493. trueppm_api/apps/projects/template_tasks.py +258 -0
  494. trueppm_api/apps/projects/template_views.py +799 -0
  495. trueppm_api/apps/projects/throttles.py +311 -0
  496. trueppm_api/apps/projects/urls.py +1011 -0
  497. trueppm_api/apps/projects/utilization.py +885 -0
  498. trueppm_api/apps/projects/views.py +20572 -0
  499. trueppm_api/apps/projects/wbs_paths.py +119 -0
  500. trueppm_api/apps/resources/__init__.py +0 -0
  501. trueppm_api/apps/resources/apps.py +8 -0
  502. trueppm_api/apps/resources/capacity.py +121 -0
  503. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  504. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  505. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  506. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  507. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  508. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  509. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  510. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  511. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  512. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  513. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  514. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  515. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  516. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  517. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  518. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  519. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  520. trueppm_api/apps/resources/models.py +298 -0
  521. trueppm_api/apps/resources/serializers.py +426 -0
  522. trueppm_api/apps/resources/services.py +465 -0
  523. trueppm_api/apps/resources/urls.py +28 -0
  524. trueppm_api/apps/resources/views.py +1749 -0
  525. trueppm_api/apps/scheduling/__init__.py +0 -0
  526. trueppm_api/apps/scheduling/apps.py +153 -0
  527. trueppm_api/apps/scheduling/calendars.py +119 -0
  528. trueppm_api/apps/scheduling/deadletter.py +91 -0
  529. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  530. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  531. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  532. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  533. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  534. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  535. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  536. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  537. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  538. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  539. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  540. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  541. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  542. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  543. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  544. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  545. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  546. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  547. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  548. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  549. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  550. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  551. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  552. trueppm_api/apps/scheduling/models.py +516 -0
  553. trueppm_api/apps/scheduling/receivers.py +160 -0
  554. trueppm_api/apps/scheduling/risk_premium.py +283 -0
  555. trueppm_api/apps/scheduling/serializers.py +564 -0
  556. trueppm_api/apps/scheduling/services.py +1182 -0
  557. trueppm_api/apps/scheduling/signals.py +56 -0
  558. trueppm_api/apps/scheduling/tasks.py +1984 -0
  559. trueppm_api/apps/scheduling/telemetry.py +80 -0
  560. trueppm_api/apps/scheduling/units.py +35 -0
  561. trueppm_api/apps/scheduling/urls.py +57 -0
  562. trueppm_api/apps/scheduling/views.py +2564 -0
  563. trueppm_api/apps/sso/__init__.py +0 -0
  564. trueppm_api/apps/sso/apps.py +19 -0
  565. trueppm_api/apps/sso/extensions.py +130 -0
  566. trueppm_api/apps/sso/management/__init__.py +0 -0
  567. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  568. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  569. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  570. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  571. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  572. trueppm_api/apps/sso/models.py +148 -0
  573. trueppm_api/apps/sso/serializers.py +437 -0
  574. trueppm_api/apps/sso/services.py +1385 -0
  575. trueppm_api/apps/sso/urls.py +47 -0
  576. trueppm_api/apps/sso/views.py +935 -0
  577. trueppm_api/apps/sync/__init__.py +0 -0
  578. trueppm_api/apps/sync/apps.py +15 -0
  579. trueppm_api/apps/sync/backfill.py +208 -0
  580. trueppm_api/apps/sync/broadcast.py +320 -0
  581. trueppm_api/apps/sync/conflict.py +262 -0
  582. trueppm_api/apps/sync/consumers.py +471 -0
  583. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  584. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  585. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  586. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  587. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  588. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  589. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  590. trueppm_api/apps/sync/models.py +225 -0
  591. trueppm_api/apps/sync/pagination.py +231 -0
  592. trueppm_api/apps/sync/sequence.py +371 -0
  593. trueppm_api/apps/sync/serializers.py +687 -0
  594. trueppm_api/apps/sync/tasks.py +260 -0
  595. trueppm_api/apps/sync/throttles.py +175 -0
  596. trueppm_api/apps/sync/upload.py +501 -0
  597. trueppm_api/apps/sync/urls.py +17 -0
  598. trueppm_api/apps/sync/views.py +930 -0
  599. trueppm_api/apps/sync/ws_auth.py +193 -0
  600. trueppm_api/apps/taskruns/__init__.py +0 -0
  601. trueppm_api/apps/taskruns/apps.py +11 -0
  602. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  603. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  604. trueppm_api/apps/taskruns/models.py +68 -0
  605. trueppm_api/apps/taskruns/serializers.py +92 -0
  606. trueppm_api/apps/taskruns/tasks.py +60 -0
  607. trueppm_api/apps/taskruns/tracker.py +286 -0
  608. trueppm_api/apps/taskruns/urls.py +46 -0
  609. trueppm_api/apps/taskruns/views.py +195 -0
  610. trueppm_api/apps/teams/__init__.py +8 -0
  611. trueppm_api/apps/teams/apps.py +16 -0
  612. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  613. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
  614. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  615. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  616. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  617. trueppm_api/apps/teams/models.py +145 -0
  618. trueppm_api/apps/teams/permissions.py +149 -0
  619. trueppm_api/apps/teams/serializers.py +95 -0
  620. trueppm_api/apps/teams/services.py +434 -0
  621. trueppm_api/apps/teams/signals.py +61 -0
  622. trueppm_api/apps/teams/urls.py +34 -0
  623. trueppm_api/apps/teams/views.py +201 -0
  624. trueppm_api/apps/timetracking/__init__.py +0 -0
  625. trueppm_api/apps/timetracking/apps.py +9 -0
  626. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  627. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  628. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  629. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  630. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  631. trueppm_api/apps/timetracking/models.py +178 -0
  632. trueppm_api/apps/timetracking/serializers.py +175 -0
  633. trueppm_api/apps/timetracking/services.py +117 -0
  634. trueppm_api/apps/timetracking/urls.py +47 -0
  635. trueppm_api/apps/timetracking/views.py +526 -0
  636. trueppm_api/apps/webhooks/__init__.py +0 -0
  637. trueppm_api/apps/webhooks/apps.py +8 -0
  638. trueppm_api/apps/webhooks/backfill.py +90 -0
  639. trueppm_api/apps/webhooks/dispatch.py +144 -0
  640. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  641. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
  642. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  643. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  644. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  645. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  646. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  647. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  648. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  649. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
  650. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  651. trueppm_api/apps/webhooks/models.py +372 -0
  652. trueppm_api/apps/webhooks/serializers.py +318 -0
  653. trueppm_api/apps/webhooks/tasks.py +663 -0
  654. trueppm_api/apps/webhooks/urls.py +22 -0
  655. trueppm_api/apps/webhooks/views.py +383 -0
  656. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  657. trueppm_api/apps/workflow_engine/apps.py +23 -0
  658. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  659. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  660. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  661. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  662. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  663. trueppm_api/apps/workflow_engine/models.py +265 -0
  664. trueppm_api/apps/workflow_engine/tasks.py +368 -0
  665. trueppm_api/apps/workspace/__init__.py +0 -0
  666. trueppm_api/apps/workspace/apps.py +10 -0
  667. trueppm_api/apps/workspace/backfill.py +111 -0
  668. trueppm_api/apps/workspace/export.py +242 -0
  669. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  670. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  671. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  672. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  673. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  674. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  675. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  676. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  677. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  678. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  679. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  680. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  681. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  682. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  683. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  684. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  685. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  686. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  687. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  688. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  689. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  690. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  691. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  692. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  693. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  694. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  695. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  696. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  697. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  698. trueppm_api/apps/workspace/models.py +857 -0
  699. trueppm_api/apps/workspace/permissions.py +187 -0
  700. trueppm_api/apps/workspace/serializers.py +590 -0
  701. trueppm_api/apps/workspace/services.py +919 -0
  702. trueppm_api/apps/workspace/signals.py +37 -0
  703. trueppm_api/apps/workspace/tasks.py +555 -0
  704. trueppm_api/apps/workspace/urls.py +113 -0
  705. trueppm_api/apps/workspace/views.py +1502 -0
  706. trueppm_api/asgi.py +42 -0
  707. trueppm_api/celery.py +31 -0
  708. trueppm_api/core/__init__.py +0 -0
  709. trueppm_api/core/admin_site.py +206 -0
  710. trueppm_api/core/auth_views.py +916 -0
  711. trueppm_api/core/constant_time.py +51 -0
  712. trueppm_api/core/csp.py +54 -0
  713. trueppm_api/core/db.py +87 -0
  714. trueppm_api/core/db_session.py +37 -0
  715. trueppm_api/core/exception_handlers.py +157 -0
  716. trueppm_api/core/export_downloads.py +56 -0
  717. trueppm_api/core/extension_providers.py +56 -0
  718. trueppm_api/core/extension_signals.py +147 -0
  719. trueppm_api/core/idempotent.py +260 -0
  720. trueppm_api/core/metadata.py +42 -0
  721. trueppm_api/core/middleware.py +51 -0
  722. trueppm_api/core/openapi.py +949 -0
  723. trueppm_api/core/password_reset.py +500 -0
  724. trueppm_api/core/protect_conflict.py +100 -0
  725. trueppm_api/core/ratelimit.py +148 -0
  726. trueppm_api/core/redis_throttle.py +82 -0
  727. trueppm_api/core/refresh_cookie_policy.py +156 -0
  728. trueppm_api/core/request_body.py +86 -0
  729. trueppm_api/core/security_checks.py +884 -0
  730. trueppm_api/core/storage_config.py +105 -0
  731. trueppm_api/core/text_decode.py +168 -0
  732. trueppm_api/core/throttling.py +344 -0
  733. trueppm_api/core/valkey.py +214 -0
  734. trueppm_api/core/valkey_cache.py +66 -0
  735. trueppm_api/core/valkey_checks.py +104 -0
  736. trueppm_api/core/valkey_config.py +39 -0
  737. trueppm_api/core/worker_broker_wait.py +149 -0
  738. trueppm_api/core/worker_heartbeat.py +134 -0
  739. trueppm_api/fields.py +36 -0
  740. trueppm_api/permissions.py +30 -0
  741. trueppm_api/py.typed +0 -0
  742. trueppm_api/routing.py +11 -0
  743. trueppm_api/settings/__init__.py +0 -0
  744. trueppm_api/settings/base.py +2455 -0
  745. trueppm_api/settings/dev.py +273 -0
  746. trueppm_api/settings/prod.py +228 -0
  747. trueppm_api/urls.py +153 -0
  748. trueppm_api/workflows/__init__.py +38 -0
  749. trueppm_api/workflows/backends/__init__.py +6 -0
  750. trueppm_api/workflows/backends/default.py +297 -0
  751. trueppm_api/workflows/consumers/__init__.py +11 -0
  752. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  753. trueppm_api/workflows/interface.py +148 -0
  754. trueppm_api/workflows/registry.py +106 -0
  755. trueppm_api/workflows/services.py +90 -0
  756. trueppm_api/wsgi.py +11 -0
  757. trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
  758. trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
  759. trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
@@ -0,0 +1,806 @@
1
+ """User-scoped external-source connection endpoints (ADR-0097 §3).
2
+
3
+ ``GET/PUT/PATCH/DELETE /api/v1/me/connections/<source>/`` — the personal, self-scoped
4
+ surface for connecting a read-only external task source (e.g. the user's own
5
+ Jira Cloud account) so its assigned items appear in My Work.
6
+
7
+ Isolation & security posture (ADR-0097 §3 / §Threat Model → Resolution):
8
+ - **Strictly personal.** Every read/write filters ``user=request.user``; no
9
+ project member, Admin, or Owner can see or touch another user's connection.
10
+ - **Secret never serialized.** The Fernet-encrypted PAT (``secret_ciphertext``)
11
+ is never returned — the summary exposes only ``{source, exists, base_url,
12
+ account_email, status, last_synced_at, jql, project_keys, poll_enabled,
13
+ last_sync}``. The stored ``config`` is never echoed wholesale; every key it
14
+ contributes is projected explicitly (see :func:`last_sync_summary`).
15
+ - **SSRF collapsed at connect.** ``base_url`` is Jira-Cloud-allow-listed
16
+ (``providers.assert_base_url_allowed`` → ``*.atlassian.net``, https) *before*
17
+ the token is ever put on the wire in the verify ping (#902 ordering).
18
+
19
+ This surface reuses ``IntegrationCredential`` (ADR-0097 §2) with the row's
20
+ ``config`` carrying ``{account_email, jql, project_keys, poll_enabled, status}``
21
+ — both ``jql`` and ``project_keys`` are read by the source at pull time, the
22
+ latter ANDed onto the query so it can only narrow what leaves the provider
23
+ (#2888). It does
24
+ **not** call ``IntegrationCredential.upsert`` — that validates ``provider``
25
+ against ``TASK_LINK_PROVIDERS`` (where ``jira`` is reserved Enterprise), whereas
26
+ an external source validates against the distinct ``EXTERNAL_TASK_SOURCES``
27
+ registry. Persistence therefore goes through this module's own
28
+ ``update_or_create`` + ``encrypt_secret``.
29
+
30
+ Scope note (#1418): this ticket ships the data layer + connection management.
31
+ The actual pull worker (Celery + ``ExternalSyncRequest`` outbox), the
32
+ ``POST .../sync/`` trigger, and the My Work augmentation ride on top in #1419;
33
+ this module deliberately does not enqueue a sync.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from typing import TYPE_CHECKING, Any, cast
39
+ from urllib.parse import urlparse
40
+
41
+ from drf_spectacular.utils import OpenApiResponse, extend_schema
42
+ from rest_framework import generics, pagination, serializers, status
43
+ from rest_framework.permissions import BasePermission, IsAuthenticated
44
+ from rest_framework.response import Response
45
+ from rest_framework.throttling import BaseThrottle, ScopedRateThrottle
46
+ from rest_framework.views import APIView
47
+
48
+ from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
49
+ from trueppm_api.core.openapi import state_refusal_400
50
+
51
+ from . import providers
52
+ from .encryption import encrypt_secret
53
+ from .external_sources import (
54
+ DEPLOYMENT_CLOUD,
55
+ EXTERNAL_TASK_SOURCES,
56
+ JIRA_DEPLOYMENTS,
57
+ JIRA_PROJECT_KEY_RE,
58
+ ExternalTaskSource,
59
+ JqlNotWellFormed,
60
+ scan_jql,
61
+ )
62
+ from .models import ExternalWorkItem, IntegrationCredential
63
+ from .services import SyncCooldownActive, enqueue_external_sync
64
+
65
+ if TYPE_CHECKING:
66
+ from django.contrib.auth.models import User
67
+ from rest_framework.request import Request
68
+
69
+ # Connection lifecycle status surfaced to the owner (ADR-0097 §5). Written by the
70
+ # #1419 pull worker into ``config["status"]``; this module only reads it and
71
+ # defaults a freshly-connected row to ``connected``.
72
+ STATUS_CONNECTED = "connected"
73
+ STATUS_NOT_CONNECTED = "not_connected"
74
+ STATUS_AUTH_FAILED = "auth_failed"
75
+ # The credential is fine but its stored filter cannot be scoped safely, so the
76
+ # worker refuses to pull rather than pull wider than the owner asked for (#2888).
77
+ # A separate state from ``auth_failed`` because the remedy is the same wizard but
78
+ # the cause is not the token — telling the user their token expired would send
79
+ # them to re-issue a credential that was never the problem.
80
+ STATUS_INVALID_FILTER = "invalid_filter"
81
+
82
+ # ``config["last_sync"]["reason"]`` vocabulary (#2925), written by the pull worker
83
+ # (``tasks.py``) and validated on the way out by :func:`last_sync_summary`. A
84
+ # **fixed token set**, not a formatted message, and that is a security constraint
85
+ # rather than a style one: ``config`` is owner-readable over this module's ``GET``,
86
+ # and the exception text on those paths can carry the request URL (which embeds
87
+ # the owner's JQL, and on some deployments the credential) or provider-echoed PII.
88
+ # A closed vocabulary cannot leak either — the web maps the token to user-facing
89
+ # copy. Never interpolate an exception, URL, or response body into this field.
90
+ #
91
+ # Defined *here*, not beside the writer, so the read surface can fail closed
92
+ # against it without importing ``tasks`` (which imports this module). Writer
93
+ # discipline alone would leave the guarantee unenforced at the one boundary that
94
+ # serves it, and ``config`` is a schemaless column an Enterprise integration also
95
+ # writes to.
96
+ SYNC_REASON_AUTH_FAILED = "auth_failed"
97
+ SYNC_REASON_INVALID_FILTER = "invalid_filter"
98
+ SYNC_REASON_UNREACHABLE = "unreachable"
99
+ SYNC_REASON_RATE_LIMITED = "rate_limited"
100
+ SYNC_REASON_DECRYPT_FAILED = "credential_unreadable"
101
+ SYNC_FAILURE_REASONS: frozenset[str] = frozenset(
102
+ {
103
+ SYNC_REASON_AUTH_FAILED,
104
+ SYNC_REASON_INVALID_FILTER,
105
+ SYNC_REASON_UNREACHABLE,
106
+ SYNC_REASON_RATE_LIMITED,
107
+ SYNC_REASON_DECRYPT_FAILED,
108
+ }
109
+ )
110
+
111
+ # Human-readable 422 detail per ``VerifyResult.reason`` (mirrors the credentials
112
+ # viewset map, plus the Jira-specific ``missing_email``).
113
+ _VERIFY_FAILURE_DETAIL: dict[str | None, str] = {
114
+ "invalid_token": (
115
+ "The source rejected this credential. Check the API token is valid, "
116
+ "unexpired, and paired with the correct account email."
117
+ ),
118
+ "missing_email": "This source needs the account email that owns the API token.",
119
+ "provider_unreachable": "Could not reach the source to verify this credential. Try again.",
120
+ "provider_timeout": "Verifying this credential with the source timed out. Try again.",
121
+ "blocked_host": (
122
+ "The host URL could not be reached — check it is correct, reachable over "
123
+ "https, and not an internal address."
124
+ ),
125
+ }
126
+
127
+
128
+ class ExternalConnectionUpsertSerializer(serializers.Serializer[Any]):
129
+ """Payload for ``PUT /me/connections/{source}/`` (connect or update).
130
+
131
+ ``secret`` is the user's own API token / PAT (write-only, never echoed).
132
+ ``base_url`` is the source host — Jira Cloud ``https://<tenant>.atlassian.net``
133
+ or a self-hosted Jira Data Center / Server host — allow-listed downstream.
134
+ ``deployment`` (``cloud`` default | ``server``) selects the Cloud vs DC/Server
135
+ API + auth shape. ``account_email`` (Cloud Basic auth only) + ``jql`` +
136
+ ``project_keys`` are stored in the credential's ``config`` — the source reads
137
+ them at pull time. ``jql`` selects what to pull; ``project_keys`` narrows it
138
+ and is **ANDed onto** the query, whether that query is the default or a
139
+ custom ``jql``, so the project filter can only ever restrict the pull.
140
+
141
+ ``poll_enabled`` opts the connection into the background poll (ADR-0097 §4).
142
+ It is declared **without** a ``default`` on purpose: a PUT rebuilds ``config``
143
+ wholesale, so a defaulted field would silently switch polling off every time
144
+ the owner re-connected to rotate a token. Absent from the payload therefore
145
+ means "keep what this connection already had" — see
146
+ :meth:`ExternalConnectionView.put`.
147
+ """
148
+
149
+ secret = serializers.CharField(
150
+ write_only=True, min_length=1, max_length=4096, trim_whitespace=False
151
+ )
152
+ base_url = serializers.CharField(max_length=512)
153
+ # Which Jira deployment this connection targets. Cloud is the default so a
154
+ # payload from before the field existed (or any non-Jira source that ignores
155
+ # it) keeps working unchanged.
156
+ deployment = serializers.ChoiceField(
157
+ choices=JIRA_DEPLOYMENTS, required=False, default=DEPLOYMENT_CLOUD
158
+ )
159
+ account_email = serializers.EmailField(required=False, allow_blank=True, default="")
160
+ jql = serializers.CharField(
161
+ required=False,
162
+ allow_blank=True,
163
+ max_length=1024,
164
+ default="",
165
+ help_text=(
166
+ "Optional JQL selecting what to pull. Blank uses 'assigned to me and "
167
+ "not done'. Parentheses and quotes must balance — the project filter "
168
+ "is ANDed onto this query, which is only a narrowing on a "
169
+ "well-formed one."
170
+ ),
171
+ )
172
+ project_keys = serializers.ListField(
173
+ child=serializers.CharField(max_length=64),
174
+ required=False,
175
+ default=list,
176
+ max_length=50,
177
+ help_text=(
178
+ "Optional Jira project keys to restrict the pull to. Each must be a "
179
+ "Jira project key (a letter followed by letters, digits or "
180
+ "underscores); stored upper-cased and de-duplicated. Composed into "
181
+ "the query as 'AND project IN (...)' on top of jql, so it can only "
182
+ "narrow the pull. Empty means every project you can see."
183
+ ),
184
+ )
185
+ poll_enabled = serializers.BooleanField(
186
+ required=False,
187
+ help_text=(
188
+ "Opt this connection into the background poll (ADR-0097 §4). Omit to "
189
+ "leave the current setting untouched — a re-connect must not silently "
190
+ "switch polling off. Default off for a connection that never set it."
191
+ ),
192
+ )
193
+
194
+ def validate_secret(self, value: str) -> str:
195
+ if not value.strip():
196
+ raise serializers.ValidationError("Secret must not be blank.")
197
+ return value
198
+
199
+ def validate_jql(self, value: str) -> str:
200
+ """Reject a JQL whose parentheses or quotes do not balance.
201
+
202
+ Not style policing. ``project_keys`` narrows the pull by wrapping this
203
+ query in one pair of parentheses and ANDing a ``project IN (...)`` clause
204
+ onto it, and an unbalanced query turns that wrap into a no-op that pulls
205
+ from projects the owner did not select. Jira would reject a malformed query
206
+ outright anyway, so the only thing this forbids is input that is already
207
+ broken — and it surfaces as an inline message on the field instead of a
208
+ connection that verifies, stores, and then fails on its first pull.
209
+ """
210
+ text = value.strip()
211
+ if not text:
212
+ return value
213
+ try:
214
+ scan_jql(text)
215
+ except JqlNotWellFormed as exc:
216
+ raise serializers.ValidationError(
217
+ f"This filter is not valid JQL ({exc}). Check the parentheses and quotes."
218
+ ) from exc
219
+ return value
220
+
221
+ def validate_project_keys(self, value: list[str]) -> list[str]:
222
+ """Normalize + reject anything that is not a Jira project key (#2888).
223
+
224
+ These keys are composed into the JQL the pull worker sends, so a value
225
+ carrying a quote or a parenthesis could rewrite the filter it is meant to
226
+ narrow. Rejecting at mint time gives the user an inline message on the
227
+ connect wizard rather than a connection that fails on its first pull.
228
+ Upper-cased and de-duplicated so the stored value matches what the wizard
229
+ shows and what the query will carry.
230
+ """
231
+ cleaned: dict[str, None] = {}
232
+ for raw in value:
233
+ key = raw.strip()
234
+ if not key:
235
+ continue
236
+ if not JIRA_PROJECT_KEY_RE.match(key):
237
+ raise serializers.ValidationError(
238
+ f"{key!r} is not a valid project key. Use the short key Jira "
239
+ "shows on an issue (letters, digits and underscores, starting "
240
+ "with a letter) — for example RIV."
241
+ )
242
+ cleaned.setdefault(key.upper(), None)
243
+ return list(cleaned)
244
+
245
+ def validate_base_url(self, value: str) -> str:
246
+ if "://" not in value:
247
+ raise serializers.ValidationError(
248
+ "Host URL must include a scheme (https://tenant.atlassian.net)."
249
+ )
250
+ parsed = urlparse(value)
251
+ # https-only (ADR-0097 §Resolution #1): an external source ships a personal
252
+ # token to base_url, so the transport must be encrypted. ``_jira_origin``
253
+ # also forces https at request time; rejecting here keeps the stored value
254
+ # spec-compliant rather than silently upgrading a cosmetic ``http://``.
255
+ if parsed.scheme.lower() != "https":
256
+ raise serializers.ValidationError(
257
+ f"Host URL scheme {parsed.scheme.lower()!r} is not allowed. Use https."
258
+ )
259
+ if parsed.query or parsed.fragment:
260
+ raise serializers.ValidationError(
261
+ "Host URL must not contain a query string or fragment."
262
+ )
263
+ return value
264
+
265
+
266
+ class ExternalConnectionLastSyncSerializer(serializers.Serializer[Any]):
267
+ """What the last pull actually did (#2925) — the outcome, not just the clock.
268
+
269
+ Written by the pull worker into ``config["last_sync"]`` (``tasks.py``). Before
270
+ this, a connection reported ``status`` + ``last_synced_at`` and nothing else,
271
+ so "Connected, last synced 5 minutes ago" read identically whether the pull
272
+ returned 200 items, zero, or failed — and a contributor past the source's page
273
+ size got a silently partial My Work.
274
+
275
+ ``truncated`` is the load-bearing field: it says the source had more than this
276
+ pull stored, so the owner is looking at the first ``stored`` of their assigned
277
+ work. ``total_available`` is the provider's own count when it reported one and
278
+ ``null`` when it did not — ``null`` means *unknown*, never *zero*, and a client
279
+ must not render a denominator from it.
280
+ """
281
+
282
+ at = serializers.DateTimeField(allow_null=True)
283
+ ok = serializers.BooleanField()
284
+ # A fixed token from ``tasks.SYNC_FAILURE_REASONS`` (blank on success), never a
285
+ # formatted message — see that constant for why the vocabulary is closed.
286
+ reason = serializers.CharField(allow_blank=True)
287
+ fetched = serializers.IntegerField()
288
+ stored = serializers.IntegerField()
289
+ total_available = serializers.IntegerField(allow_null=True)
290
+ truncated = serializers.BooleanField()
291
+
292
+
293
+ class ExternalConnectionSummarySerializer(serializers.Serializer[Any]):
294
+ """Owner-facing summary of a connection — **never** the secret (ADR-0097 §3).
295
+
296
+ The ``source`` key is intentionally not echoed: the caller already has it from
297
+ the ``/me/connections/{source}/`` request path, and a serializer field literally
298
+ named ``source`` collides with DRF's internal ``Field.source`` attribute.
299
+ """
300
+
301
+ name = serializers.CharField()
302
+ exists = serializers.BooleanField()
303
+ base_url = serializers.CharField(allow_blank=True)
304
+ deployment = serializers.CharField()
305
+ account_email = serializers.CharField(allow_blank=True)
306
+ status = serializers.CharField()
307
+ last_synced_at = serializers.DateTimeField(allow_null=True)
308
+ jql = serializers.CharField(allow_blank=True)
309
+ project_keys = serializers.ListField(child=serializers.CharField())
310
+ # Whether the background poll picks this connection up (ADR-0097 §4). A
311
+ # reviewed, owner-visible key — it is the owner's own setting, carries no
312
+ # provider data, and the toggle that writes it has to be able to read its
313
+ # current state back. Never inferred client-side from anything else.
314
+ poll_enabled = serializers.BooleanField()
315
+ # ``null`` until the connection's first pull completes — a freshly-connected
316
+ # row has a status but no outcome yet, which is a different thing from a pull
317
+ # that returned nothing.
318
+ last_sync = ExternalConnectionLastSyncSerializer(allow_null=True)
319
+
320
+
321
+ class ExternalConnectionPollSerializer(serializers.Serializer[Any]):
322
+ """Payload for ``PATCH /me/connections/{source}/`` — the poll opt-in, alone.
323
+
324
+ A separate, one-field serializer rather than a partial
325
+ :class:`ExternalConnectionUpsertSerializer`: flipping the background-poll
326
+ switch must not require the owner to re-enter their API token (``secret`` is
327
+ required on the upsert and is never readable back), and a partial upsert would
328
+ also make every other connection field silently writable from a control whose
329
+ entire job is one boolean. Anything else in the body is ignored, so this
330
+ endpoint can never rewrite a credential, host, or filter.
331
+ """
332
+
333
+ poll_enabled = serializers.BooleanField()
334
+
335
+
336
+ class ExternalConnectionErrorSerializer(serializers.Serializer[Any]):
337
+ """422 body when credential verification against the source fails."""
338
+
339
+ detail = serializers.CharField()
340
+ code = serializers.CharField()
341
+ reason = serializers.CharField(allow_null=True)
342
+
343
+
344
+ def last_sync_summary(config: dict[str, Any] | None) -> dict[str, Any] | None:
345
+ """Project ``config["last_sync"]`` onto the owner-facing outcome shape (#2925).
346
+
347
+ Field-by-field with per-field type coercion rather than passing the stored
348
+ dict through. ``config`` is a schemaless ``JSONField`` that also holds the
349
+ connection's filter and (for other providers on the same model) whatever an
350
+ Enterprise integration put there — echoing it wholesale would turn any future
351
+ key into an unreviewed client-visible field, which is exactly the
352
+ over-exposure the summary's "never the secret" contract exists to prevent.
353
+ Anything absent or of the wrong type degrades to the neutral value, so a
354
+ hand-edited or pre-#2925 row renders as "no outcome recorded" instead of
355
+ raising.
356
+
357
+ Returns ``None`` when no pull has ever completed — distinct from a pull that
358
+ completed and stored nothing.
359
+ """
360
+ raw = (config or {}).get("last_sync")
361
+ if not isinstance(raw, dict):
362
+ return None
363
+ total = raw.get("total_available")
364
+ at = raw.get("at")
365
+ # Fail closed on ``reason``: only a token from the closed vocabulary leaves
366
+ # this boundary. Type-checking it would enforce nothing — the whole point of
367
+ # the vocabulary is that a *formatted* string must never reach a client, and
368
+ # a formatted string is a ``str``. Anything unrecognized degrades to blank,
369
+ # which the web already renders as its generic "didn't complete" sentence.
370
+ raw_reason = raw.get("reason")
371
+ reason = raw_reason if raw_reason in SYNC_FAILURE_REASONS else ""
372
+ # ``None`` is meaningful for the total ("the source did not say"), so it is
373
+ # not collapsed to 0 the way the two counts are.
374
+ total_ok = isinstance(total, int) and not isinstance(total, bool)
375
+ return {
376
+ "at": at if isinstance(at, str) else None,
377
+ "ok": bool(raw.get("ok")),
378
+ "reason": reason,
379
+ "fetched": _as_count(raw.get("fetched")),
380
+ "stored": _as_count(raw.get("stored")),
381
+ "total_available": total if total_ok else None,
382
+ "truncated": bool(raw.get("truncated")),
383
+ }
384
+
385
+
386
+ def _as_count(value: Any) -> int:
387
+ """Narrow an untrusted stored value to a non-negative count (0 if it is not one).
388
+
389
+ ``bool`` is excluded explicitly because it is an ``int`` subclass, so ``True``
390
+ would otherwise read back as a count of 1.
391
+ """
392
+ if isinstance(value, bool) or not isinstance(value, int):
393
+ return 0
394
+ return max(int(value), 0)
395
+
396
+
397
+ def _summary(label: str, row: IntegrationCredential | None) -> dict[str, Any]:
398
+ """Build the owner-facing summary dict for one source (secret excluded)."""
399
+ cfg = (row.config if row else None) or {}
400
+ return {
401
+ "name": label,
402
+ "exists": row is not None,
403
+ "base_url": row.base_url if row else "",
404
+ # Cloud is the default for a row stored before the discriminant existed.
405
+ "deployment": cfg.get("deployment", DEPLOYMENT_CLOUD),
406
+ "account_email": cfg.get("account_email", ""),
407
+ # Freshly-connected rows read ``connected``; the #1419 worker flips this to
408
+ # ``auth_failed`` on a 401/403 so My Work can show a "Reconnect" prompt.
409
+ "status": cfg.get("status") or (STATUS_CONNECTED if row else STATUS_NOT_CONNECTED),
410
+ # ``last_used_at`` is stamped by the pull worker when it last used the token.
411
+ "last_synced_at": row.last_used_at if row else None,
412
+ "jql": cfg.get("jql", ""),
413
+ "project_keys": cfg.get("project_keys", []),
414
+ # ``is True``, not ``bool()``: ``config`` is a schemaless column, and the
415
+ # poll selects on ``config__poll_enabled=True`` — strict JSON equality. A
416
+ # row carrying the string ``"false"`` is truthy in Python and *not* what
417
+ # the poll matches, so a ``bool()`` coercion would render the switch on for
418
+ # a connection that is never polled. Report what the poll acts on.
419
+ "poll_enabled": cfg.get("poll_enabled") is True,
420
+ # What the last pull did — counts, and whether a cap truncated it (#2925).
421
+ "last_sync": last_sync_summary(cfg),
422
+ }
423
+
424
+
425
+ @extend_schema(tags=["me"])
426
+ class ExternalConnectionView(IdempotencyMixin, APIView):
427
+ """Manage the authenticated user's connection to one external task source.
428
+
429
+ Routes (``<source>`` is an ``EXTERNAL_TASK_SOURCES`` key, e.g. ``jira``):
430
+ GET /api/v1/me/connections/{source}/ summary (exists / status / config)
431
+ PUT /api/v1/me/connections/{source}/ connect or update (verify then store)
432
+ PATCH /api/v1/me/connections/{source}/ flip the background-poll opt-in
433
+ DELETE /api/v1/me/connections/{source}/ disconnect (hard-remove ciphertext)
434
+
435
+ All actions require authentication and are self-scoped to ``request.user`` —
436
+ the ``(user, provider)`` filter is the single IDOR boundary, so another
437
+ user's connection is invisible by construction.
438
+ """
439
+
440
+ # Plain (non-ClassVar) annotations: DRF's APIView declares these as instance
441
+ # attributes, so overriding them with a ClassVar trips mypy; noqa the RUF012
442
+ # mutable-default lint that the ``views.py`` per-file ignore doesn't reach here.
443
+ permission_classes: list[type[BasePermission]] = [IsAuthenticated] # noqa: RUF012
444
+ # Share the credential-store rate bucket — connect verifies against the source
445
+ # (an outbound call) and all actions touch the encrypted credential store.
446
+ throttle_classes: list[type[BaseThrottle]] = [ScopedRateThrottle] # noqa: RUF012
447
+ throttle_scope = "credential_rotate"
448
+
449
+ def _resolve_source(self, source: str) -> type[ExternalTaskSource] | None:
450
+ return EXTERNAL_TASK_SOURCES.get(source)
451
+
452
+ def _user(self, request: Request) -> User:
453
+ """Narrow ``request.user`` to the authenticated user for the ORM filter.
454
+
455
+ ``IsAuthenticated`` guarantees a real user before any handler runs, so the
456
+ ``AnonymousUser`` arm of ``request.user`` is unreachable here — the cast
457
+ just tells the type-checker what the permission already enforced.
458
+ """
459
+ return cast("User", request.user)
460
+
461
+ def _row(self, request: Request, source: str) -> IntegrationCredential | None:
462
+ return IntegrationCredential.objects.filter(
463
+ user=self._user(request), provider=source
464
+ ).first()
465
+
466
+ @extend_schema(responses={200: ExternalConnectionSummarySerializer})
467
+ def get(self, request: Request, source: str) -> Response:
468
+ source_cls = self._resolve_source(source)
469
+ if source_cls is None:
470
+ return Response(
471
+ {"detail": f"Unknown external task source {source!r}."},
472
+ status=status.HTTP_400_BAD_REQUEST,
473
+ )
474
+ row = self._row(request, source)
475
+ payload = _summary(getattr(source_cls, "label", source), row)
476
+ return Response(ExternalConnectionSummarySerializer(payload).data)
477
+
478
+ @extend_schema(
479
+ request=ExternalConnectionUpsertSerializer,
480
+ responses={
481
+ 200: ExternalConnectionSummarySerializer,
482
+ # Two different 400 bodies reach a caller here, so this is declared as
483
+ # a description rather than a serializer: an unknown source key or a
484
+ # disallowed ``base_url`` returns ``{detail, code}``, while DRF field
485
+ # validation (``secret``, ``base_url``, ``jql``, ``project_keys``)
486
+ # returns its own field-keyed ``{field: [message]}`` shape. Declaring
487
+ # either serializer would document one and misdescribe the other.
488
+ 400: OpenApiResponse(
489
+ description=(
490
+ "Unknown source, disallowed host URL, or a field that failed "
491
+ "validation (an unparseable JQL filter, or a project key that "
492
+ "is not a Jira project key)."
493
+ )
494
+ ),
495
+ 422: ExternalConnectionErrorSerializer,
496
+ },
497
+ )
498
+ def put(self, request: Request, source: str) -> Response:
499
+ """Connect or update the connection: allow-list host, verify, then store.
500
+
501
+ Order matters (#902): the ``base_url`` host is allow-listed *before*
502
+ ``verify_credential`` ships the token to it, so an attacker-controlled
503
+ host is rejected before the PAT is on the wire. On a failed verify the
504
+ plaintext is never written — ``encrypt_secret`` is not reached.
505
+ """
506
+ source_cls = self._resolve_source(source)
507
+ if source_cls is None:
508
+ return Response(
509
+ {"detail": f"Unknown external task source {source!r}."},
510
+ status=status.HTTP_400_BAD_REQUEST,
511
+ )
512
+ serializer = ExternalConnectionUpsertSerializer(data=request.data)
513
+ serializer.is_valid(raise_exception=True)
514
+ data = serializer.validated_data
515
+ secret: str = data["secret"]
516
+ base_url: str = data["base_url"]
517
+
518
+ # Gate the host BEFORE verify: verify sends the token to base_url.
519
+ try:
520
+ providers.assert_base_url_allowed(source, base_url)
521
+ except providers.BaseUrlNotAllowed as exc:
522
+ # False positive: BaseUrlNotAllowed carries a curated, user-facing
523
+ # message (#902); the only dynamic part is the caller's own submitted
524
+ # host, and it is an allow-list decision, not a DNS-resolved address.
525
+ return Response(
526
+ {
527
+ "detail": str(exc), # codeql[py/stack-trace-exposure]
528
+ "code": "base_url_not_allowed",
529
+ },
530
+ status=status.HTTP_400_BAD_REQUEST,
531
+ )
532
+
533
+ # Rebuilt from the payload, so a re-connect deliberately drops any stored
534
+ # ``last_sync``: the outcome described a pull made with the *previous*
535
+ # token and filter, and carrying it onto a connection whose filter just
536
+ # changed would report a truncation (or a count) that no longer applies.
537
+ existing = self._row(request, source)
538
+ # ``poll_enabled`` is the one key a rebuild must *not* drop. The rest of
539
+ # ``config`` describes the credential and its filter, both of which this
540
+ # payload replaces; the poll opt-in is an independent standing preference
541
+ # the owner set elsewhere (the Connected Accounts toggle → PATCH below).
542
+ # Rotating a token would otherwise silently stop a connection polling,
543
+ # with nothing in the UI to say it had happened.
544
+ existing_cfg = (existing.config if existing else None) or {}
545
+ current_poll = bool(existing_cfg.get("poll_enabled", False))
546
+ config: dict[str, Any] = {
547
+ "deployment": data.get("deployment", DEPLOYMENT_CLOUD),
548
+ "account_email": data.get("account_email", ""),
549
+ "jql": data.get("jql", ""),
550
+ "project_keys": data.get("project_keys", []),
551
+ "poll_enabled": bool(data.get("poll_enabled", current_poll)),
552
+ "status": STATUS_CONNECTED,
553
+ }
554
+
555
+ result = source_cls().verify_credential(base_url=base_url, secret=secret, config=config)
556
+ if not result.ok:
557
+ return Response(
558
+ {
559
+ "detail": _VERIFY_FAILURE_DETAIL.get(
560
+ result.reason, f"Could not verify the credential with {source}."
561
+ ),
562
+ "code": "source_verification_failed",
563
+ "reason": result.reason,
564
+ },
565
+ status=status.HTTP_422_UNPROCESSABLE_ENTITY,
566
+ )
567
+
568
+ # Persist directly (not IntegrationCredential.upsert — that validates the
569
+ # provider against TASK_LINK_PROVIDERS, which reserves ``jira`` for
570
+ # Enterprise; external sources use the EXTERNAL_TASK_SOURCES namespace).
571
+ row, _ = IntegrationCredential.objects.update_or_create(
572
+ user=self._user(request),
573
+ provider=source,
574
+ defaults={
575
+ "secret_ciphertext": encrypt_secret(secret),
576
+ "base_url": base_url,
577
+ "config": config,
578
+ },
579
+ )
580
+ payload = _summary(getattr(source_cls, "label", source), row)
581
+ return Response(
582
+ ExternalConnectionSummarySerializer(payload).data, status=status.HTTP_200_OK
583
+ )
584
+
585
+ @extend_schema(
586
+ # A raw schema, not ``ExternalConnectionPollSerializer``: drf-spectacular
587
+ # rewrites any serializer on a PATCH into a ``Patched…`` component with
588
+ # every field optional, which would document ``poll_enabled`` as omittable
589
+ # when the handler answers 400 without it. The serializer still does the
590
+ # validating — this only stops the schema from describing a shape the
591
+ # endpoint rejects. (``COMPONENT_SPLIT_PATCH`` is the global lever and is
592
+ # deliberately left alone; this is one endpoint, not a project-wide policy.)
593
+ request={
594
+ "application/json": {
595
+ "type": "object",
596
+ "properties": {
597
+ "poll_enabled": {
598
+ "type": "boolean",
599
+ "description": (
600
+ "Whether the background poll (ADR-0097 §4) should pick this "
601
+ "connection up. Required — there is no partial form of this "
602
+ "request."
603
+ ),
604
+ }
605
+ },
606
+ "required": ["poll_enabled"],
607
+ }
608
+ },
609
+ responses={
610
+ 200: ExternalConnectionSummarySerializer,
611
+ 400: OpenApiResponse(
612
+ description="Unknown source, or poll_enabled missing / not a boolean."
613
+ ),
614
+ 404: OpenApiResponse(description="No connection to this source for this user."),
615
+ },
616
+ )
617
+ def patch(self, request: Request, source: str) -> Response:
618
+ """Turn the background poll on or off for this connection (ADR-0097 §4).
619
+
620
+ The only mutation on a *stored* connection that does not go through the
621
+ connect wizard, because it is the only one that needs no credential. The
622
+ IDOR boundary is the same single ``(user, provider)`` filter every other
623
+ action uses (``self._row``): a source another user connected simply does
624
+ not exist for this caller, so a 404 is a "you have no such connection",
625
+ never a leak that someone else does.
626
+
627
+ ``config`` is copied-then-reassigned rather than mutated in place — the
628
+ JSONField's value is a live dict, and mutating it leaves Django unable to
629
+ tell the field changed if anything later relies on the loaded state.
630
+ """
631
+ source_cls = self._resolve_source(source)
632
+ if source_cls is None:
633
+ return Response(
634
+ {"detail": f"Unknown external task source {source!r}."},
635
+ status=status.HTTP_400_BAD_REQUEST,
636
+ )
637
+ serializer = ExternalConnectionPollSerializer(data=request.data)
638
+ serializer.is_valid(raise_exception=True)
639
+ row = self._row(request, source)
640
+ if row is None:
641
+ return Response(
642
+ {"detail": f"No {source} connection to configure — connect it first."},
643
+ status=status.HTTP_404_NOT_FOUND,
644
+ )
645
+ config = dict(row.config or {})
646
+ config["poll_enabled"] = bool(serializer.validated_data["poll_enabled"])
647
+ row.config = config
648
+ # ``updated_at`` is ``auto_now``; naming it keeps the stamp truthful, which
649
+ # an ``update_fields`` list omitting it would silently skip.
650
+ row.save(update_fields=["config", "updated_at"])
651
+ payload = _summary(getattr(source_cls, "label", source), row)
652
+ return Response(ExternalConnectionSummarySerializer(payload).data)
653
+
654
+ @extend_schema(
655
+ responses={
656
+ 204: None,
657
+ 400: state_refusal_400(
658
+ "``source`` does not name a registered external task source. The "
659
+ "path segment is free text, so an unregistered value reaches the "
660
+ "handler and is refused here rather than 404ing (#3319)."
661
+ ),
662
+ }
663
+ )
664
+ def delete(self, request: Request, source: str) -> Response:
665
+ """Disconnect: hard-remove the ciphertext, config, and cached items.
666
+
667
+ Idempotent — a 204 whether or not a row existed (ADR-0097 §Resolution #2:
668
+ "owner-only delete hard-removes ciphertext"). ``ExternalWorkItem`` FKs to
669
+ ``user`` (not the credential), so deleting the credential does not cascade
670
+ the cache — the cached items are removed explicitly here so a disconnect
671
+ leaves no residual external data in My Work.
672
+ """
673
+ source_cls = self._resolve_source(source)
674
+ if source_cls is None:
675
+ return Response(
676
+ {"detail": f"Unknown external task source {source!r}."},
677
+ status=status.HTTP_400_BAD_REQUEST,
678
+ )
679
+ user = self._user(request)
680
+ IntegrationCredential.objects.filter(user=user, provider=source).delete()
681
+ ExternalWorkItem.objects.filter(user=user, source=source).delete()
682
+ return Response(status=status.HTTP_204_NO_CONTENT)
683
+
684
+
685
+ class ExternalSyncQueuedSerializer(serializers.Serializer[Any]):
686
+ """202 body for a queued pull — ``{"queued": true}`` (not a task id)."""
687
+
688
+ queued = serializers.BooleanField()
689
+
690
+
691
+ class ExternalSyncCooldownSerializer(serializers.Serializer[Any]):
692
+ """429 body when a manual refresh lands inside the per-connection cooldown."""
693
+
694
+ detail = serializers.CharField()
695
+ code = serializers.CharField()
696
+ retry_after = serializers.IntegerField()
697
+
698
+
699
+ @extend_schema(tags=["me"])
700
+ class ExternalConnectionSyncView(IdempotencyMixin, APIView):
701
+ """Trigger a read-only pull of one external-source connection (ADR-0097 §4).
702
+
703
+ ``POST /api/v1/me/connections/{source}/sync/`` → ``202 {"queued": true}``.
704
+ Self-scoped: only the owner can trigger their own pull. The pull runs through
705
+ the ``ExternalSyncRequest`` outbox (never a direct ``.delay()``), so a broker
706
+ outage degrades to the drain rather than dropping the request.
707
+ """
708
+
709
+ permission_classes: list[type[BasePermission]] = [IsAuthenticated] # noqa: RUF012
710
+ throttle_classes: list[type[BaseThrottle]] = [ScopedRateThrottle] # noqa: RUF012
711
+ throttle_scope = "external_sync"
712
+
713
+ @extend_schema(
714
+ request=None,
715
+ responses={
716
+ 202: ExternalSyncQueuedSerializer,
717
+ 400: state_refusal_400(
718
+ "``source`` does not name a registered external task source. The "
719
+ "path segment is free text, so an unregistered value reaches the "
720
+ "handler and is refused here rather than 404ing (#3319)."
721
+ ),
722
+ 429: ExternalSyncCooldownSerializer,
723
+ },
724
+ )
725
+ def post(self, request: Request, source: str) -> Response:
726
+ if EXTERNAL_TASK_SOURCES.get(source) is None:
727
+ return Response(
728
+ {"detail": f"Unknown external task source {source!r}."},
729
+ status=status.HTTP_400_BAD_REQUEST,
730
+ )
731
+ user = cast("User", request.user)
732
+ # A pull needs a stored connection to read the token + config from.
733
+ if not IntegrationCredential.objects.filter(user=user, provider=source).exists():
734
+ return Response(
735
+ {"detail": f"No {source} connection to sync — connect it first."},
736
+ status=status.HTTP_404_NOT_FOUND,
737
+ )
738
+ try:
739
+ enqueue_external_sync(user.pk, source)
740
+ except SyncCooldownActive as exc:
741
+ resp = Response(
742
+ {
743
+ "detail": (
744
+ "This connection was refreshed a moment ago — "
745
+ f"try again in {exc.retry_after}s."
746
+ ),
747
+ "code": "sync_cooldown",
748
+ "retry_after": exc.retry_after,
749
+ },
750
+ status=status.HTTP_429_TOO_MANY_REQUESTS,
751
+ )
752
+ # Standard header so a client can honor the cooldown without parsing
753
+ # the body (RFC 9110 §10.2.3).
754
+ resp["Retry-After"] = str(exc.retry_after)
755
+ return resp
756
+ return Response({"queued": True}, status=status.HTTP_202_ACCEPTED)
757
+
758
+
759
+ class ExternalWorkItemSerializer(serializers.Serializer[Any]):
760
+ """One read-only external work item row for My Work (ADR-0097 §4).
761
+
762
+ Deliberately CPM-free: an external item is a personal cache row, never a
763
+ ``Task``, so it carries no schedule/board fields — only what My Work needs to
764
+ render a labeled, read-only link that opens in the provider.
765
+ """
766
+
767
+ id = serializers.UUIDField(read_only=True)
768
+ # ``source_key`` (not ``source``): a serializer field literally named
769
+ # ``source`` collides with DRF's internal ``Field.source`` attribute (same
770
+ # reason the connection summary omits it). ``source="source"`` maps this
771
+ # differently-named field back onto the model's ``source`` column.
772
+ source_key = serializers.CharField(source="source", read_only=True)
773
+ external_id = serializers.CharField(read_only=True)
774
+ external_url = serializers.CharField(read_only=True, allow_blank=True)
775
+ title = serializers.CharField(read_only=True, allow_blank=True)
776
+ external_status = serializers.CharField(read_only=True, allow_blank=True)
777
+ display_bucket = serializers.CharField(read_only=True)
778
+ last_synced_at = serializers.DateTimeField(read_only=True, allow_null=True)
779
+
780
+
781
+ class ExternalWorkItemPagination(pagination.LimitOffsetPagination):
782
+ """Limit/offset pagination for the personal external-items list."""
783
+
784
+ default_limit = 100
785
+ max_limit = 200
786
+
787
+
788
+ @extend_schema(tags=["me"])
789
+ class ExternalWorkItemListView(generics.ListAPIView[ExternalWorkItem]):
790
+ """List the authenticated user's cached external work items (ADR-0097 §3).
791
+
792
+ ``GET /api/v1/me/external-items/`` — the read-only items pulled from the
793
+ user's connected sources, for the My Work external section. Strictly personal:
794
+ the queryset filters ``user=request.user`` and hides soft-removed
795
+ (``is_stale``) rows, so no other user (member, Admin, or Owner) can ever see
796
+ another user's external items. Ordering comes from the model ``Meta``
797
+ (``display_bucket``, ``external_id``) so items group by bucket in My Work.
798
+ """
799
+
800
+ permission_classes: list[type[BasePermission]] = [IsAuthenticated] # noqa: RUF012
801
+ serializer_class = ExternalWorkItemSerializer
802
+ pagination_class = ExternalWorkItemPagination
803
+
804
+ def get_queryset(self) -> Any:
805
+ user = cast("User", self.request.user)
806
+ return ExternalWorkItem.objects.filter(user=user, is_stale=False)