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,793 @@
1
+ """REST API views for CSV / Excel import (#743, ADR-0632)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import logging
7
+ import os
8
+ import re
9
+ from typing import Any, cast
10
+
11
+ from django.conf import settings
12
+ from django.db import transaction
13
+ from django.http import HttpResponse
14
+ from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
15
+ from rest_framework import serializers, status
16
+ from rest_framework.exceptions import PermissionDenied, ValidationError
17
+ from rest_framework.parsers import MultiPartParser
18
+ from rest_framework.permissions import IsAuthenticated
19
+ from rest_framework.request import Request
20
+ from rest_framework.response import Response
21
+ from rest_framework.views import APIView
22
+
23
+ from trueppm_api.apps.access.models import ProjectMembership, Role
24
+ from trueppm_api.apps.access.permissions import (
25
+ IsProjectNotArchived,
26
+ IsProjectScheduler,
27
+ can_user_undo_batch_operation,
28
+ role_can_undo_batch_operation,
29
+ )
30
+ from trueppm_api.apps.csvimport.mapping import field_choices
31
+ from trueppm_api.apps.csvimport.parser import (
32
+ DATE_ORDERS,
33
+ REVIEW_BRANCH_NAME,
34
+ SUPPORTED_EXTENSIONS,
35
+ CsvImportError,
36
+ parse_spreadsheet,
37
+ )
38
+ from trueppm_api.apps.csvimport.template import CSV_TEMPLATE, TEMPLATE_FILENAME
39
+ from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Allow-list (mirrors the hardened MS Project sanitizer, #816): substitute
44
+ # anything outside a conservative safe set so an attacker-controlled
45
+ # UploadedFile.name can never carry an HTML/Content-Disposition-injection
46
+ # payload into the provenance surface. os.path.basename blocks path traversal.
47
+ _FILENAME_ALLOWED = re.compile(r"[^A-Za-z0-9._\- ()]")
48
+
49
+
50
+ def _sanitize_filename(raw: str) -> str:
51
+ name = os.path.basename(raw or "")
52
+ name = _FILENAME_ALLOWED.sub("_", name)
53
+ name = re.sub(r"\s+", " ", name).strip()
54
+ return name[:255] or "upload.csv"
55
+
56
+
57
+ def _require_project_scheduler(user: object, project_pk: str) -> None:
58
+ """Authoritative in-body role check (defense-in-depth behind IsProjectScheduler).
59
+
60
+ Import creates tasks, dependencies and resources wholesale, so it sits at the
61
+ Scheduler line rather than Member: Owner / Admin / Scheduler may import,
62
+ Member and Viewer may not.
63
+ """
64
+ role = (
65
+ ProjectMembership.objects.filter(
66
+ project_id=project_pk,
67
+ user=user, # type: ignore[misc]
68
+ is_deleted=False,
69
+ )
70
+ .values_list("role", flat=True)
71
+ .first()
72
+ )
73
+ if role is None or role < Role.SCHEDULER:
74
+ # "Resource Manager" is the label for ``Role.SCHEDULER``; "Scheduler" is the
75
+ # code name and appears on no surface a user can reach (#3503).
76
+ raise PermissionDenied(
77
+ "You need at least the Resource Manager role to import a spreadsheet."
78
+ )
79
+
80
+
81
+ def _require_project_admin(user: object, project_pk: str) -> None:
82
+ """Authoritative in-body role check for undoing an import (ADR-0810, #2756).
83
+
84
+ Admin+, not Scheduler — the same bar as undoing a template apply or a
85
+ cascade (``batch_operation_views._require_admin``). A Scheduler may have run
86
+ the import and still not clear this floor.
87
+
88
+ **Open at #3355, and not for the reason once given here.** This docstring used
89
+ to say the undo "removes rows other collaborators may already be building on
90
+ top of". ``undo_import_fix_operation`` partitions on ``server_version`` via
91
+ ``task_batch_services._partition_touched`` and reverts only untouched rows,
92
+ counting the rest as ``kept`` — so it removes nothing anyone else has changed.
93
+
94
+ Defers the comparison to :func:`role_can_undo_batch_operation` rather than
95
+ testing the ordinal here (#3353). ``CsvImportStatusView`` reports the same rule
96
+ to the wizard as ``can_undo`` so it can withhold an Undo this endpoint would
97
+ refuse, and a second copy of the comparison is exactly how the two drift —
98
+ especially while the floor itself is open at #3355.
99
+ """
100
+ role = (
101
+ ProjectMembership.objects.filter(
102
+ project_id=project_pk,
103
+ user=user, # type: ignore[misc]
104
+ is_deleted=False,
105
+ )
106
+ .values_list("role", flat=True)
107
+ .first()
108
+ )
109
+ if not role_can_undo_batch_operation(role):
110
+ raise PermissionDenied("You need at least Project Manager role to undo an import.")
111
+
112
+
113
+ def _read_validated_upload(request: Request) -> tuple[str, bytes] | Response:
114
+ """Read the multipart ``file`` field; validate extension + size.
115
+
116
+ Returns ``(filename, raw_bytes)`` or a 400 ``Response``.
117
+ """
118
+ uploaded_file = request.FILES.get("file")
119
+ if not uploaded_file:
120
+ return Response(
121
+ {"detail": "No file provided. Send a multipart form with a 'file' field."},
122
+ status=status.HTTP_400_BAD_REQUEST,
123
+ )
124
+
125
+ filename = _sanitize_filename(uploaded_file.name or "")
126
+ ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
127
+ if ext not in SUPPORTED_EXTENSIONS:
128
+ return Response(
129
+ {"detail": f"Unsupported file type: .{ext}. Upload a .csv or .xlsx file."},
130
+ status=status.HTTP_400_BAD_REQUEST,
131
+ )
132
+
133
+ max_bytes = settings.CSV_IMPORT_MAX_UPLOAD_MB * 1024 * 1024
134
+ if uploaded_file.size and uploaded_file.size > max_bytes:
135
+ return Response(
136
+ {
137
+ "detail": (
138
+ f"File too large ({uploaded_file.size} bytes). "
139
+ f"Maximum: {settings.CSV_IMPORT_MAX_UPLOAD_MB} MB."
140
+ )
141
+ },
142
+ status=status.HTTP_400_BAD_REQUEST,
143
+ )
144
+
145
+ return filename, uploaded_file.read()
146
+
147
+
148
+ def _form_body(request: Request) -> dict[str, Any]:
149
+ """Return ``request.data`` narrowed to a mapping.
150
+
151
+ Every caller sets ``parser_classes = [MultiPartParser]``, so the body is always
152
+ a QueryDict — a JSON list is rejected as 415 before this runs. Narrowed
153
+ rather than guarded: a branch that cannot be reached cannot be tested.
154
+ """
155
+ return cast("dict[str, Any]", request.data)
156
+
157
+
158
+ def _parse_column_map(request: Request) -> dict[str, str]:
159
+ """Read the optional ``column_map`` multipart field as ``{header: field}``.
160
+
161
+ Accepts a JSON object, either as a real dict (test client) or a JSON string
162
+ (what a browser's FormData sends). A malformed value is ignored rather than
163
+ rejected: auto-detection is a safe fallback, and failing the whole upload
164
+ over a mapping hint would be a worse outcome than importing the detected
165
+ columns and letting the operator see the result.
166
+ """
167
+ # Both callers set `parser_classes = [MultiPartParser]`, so the body is always
168
+ # a QueryDict — a JSON list is rejected as 415 before this runs. Narrowed
169
+ # rather than guarded: a branch that cannot be reached cannot be tested.
170
+ raw = _form_body(request).get("column_map")
171
+ if not raw:
172
+ return {}
173
+ if isinstance(raw, dict):
174
+ parsed: Any = raw
175
+ else:
176
+ import json
177
+
178
+ try:
179
+ parsed = json.loads(raw)
180
+ except (TypeError, ValueError):
181
+ logger.info("csv import: ignoring unparseable column_map")
182
+ return {}
183
+ if not isinstance(parsed, dict):
184
+ return {}
185
+ return {str(k): str(v) for k, v in parsed.items() if isinstance(k, str)}
186
+
187
+
188
+ def _parse_date_order(request: Request) -> str:
189
+ """Read the optional ``date_order`` multipart field (#2926).
190
+
191
+ Rejected rather than ignored, unlike ``column_map``: a bad mapping hint
192
+ degrades to auto-detection and the operator sees the result, but a
193
+ misspelled date order that silently fell back to ``auto`` would import
194
+ 03/04 as March 4 while the caller believed they had asserted day-first —
195
+ which is precisely the silent-corruption failure this endpoint exists to
196
+ close. A scripted caller must hear that its parameter did nothing.
197
+
198
+ Non-string input is caught here too: ``request.data`` is attacker-shaped,
199
+ and a JSON list reaching a membership test against strings is the #2795
200
+ container-type-confusion class.
201
+ """
202
+ # Both callers set `parser_classes = [MultiPartParser]`, so the body is always
203
+ # a QueryDict — a JSON list is rejected as 415 before this runs. Narrowed
204
+ # rather than guarded: a branch that cannot be reached cannot be tested.
205
+ raw = _form_body(request).get("date_order")
206
+ if raw is None or raw == "":
207
+ return "auto"
208
+ if not isinstance(raw, str) or raw not in DATE_ORDERS:
209
+ raise ValidationError({"date_order": f"Must be one of {', '.join(DATE_ORDERS)}."})
210
+ return raw
211
+
212
+
213
+ #: Multipart request schema shared by preview and commit.
214
+ #:
215
+ #: Declared rather than left as bare ``BINARY`` so ``date_order`` is visible to
216
+ #: SDK codegen — a parameter a caller cannot discover from the schema is a
217
+ #: parameter that does not exist for them (the #2942 class, one endpoint over).
218
+ _IMPORT_REQUEST_SCHEMA = {
219
+ "multipart/form-data": {
220
+ "type": "object",
221
+ "properties": {
222
+ "file": {"type": "string", "format": "binary"},
223
+ "column_map": {
224
+ "type": "string",
225
+ "description": "JSON object of {header: field} overrides from the wizard.",
226
+ },
227
+ "date_order": {
228
+ "type": "string",
229
+ "enum": list(DATE_ORDERS),
230
+ "default": "auto",
231
+ "description": (
232
+ "How to read slash dates. `auto` scans the file and settles the order "
233
+ "from the first self-identifying value; the other three assert a "
234
+ "convention. An unknown value is a 400, never a silent fallback."
235
+ ),
236
+ },
237
+ },
238
+ "required": ["file"],
239
+ }
240
+ }
241
+
242
+
243
+ class CsvImportStatusResponseSerializer(serializers.Serializer[Any]):
244
+ """The 200 body of ``GET …/import/csv/{id}/`` — declared, not prose (#3353).
245
+
246
+ This payload used to be typed ``OpenApiTypes.OBJECT`` with the key names listed
247
+ in a sentence, which is not a contract anyone can generate an SDK against. It
248
+ matters most for ``can_undo``: a field whose entire purpose is to be read by a
249
+ client, reaching ``docs/api/openapi.json`` as an undiscoverable member of a free
250
+ object, is a field that does not exist for a non-browser caller (the #2942 class).
251
+
252
+ Response-only — nothing deserializes it; the view builds the dict itself.
253
+ """
254
+
255
+ id = serializers.UUIDField()
256
+ status = serializers.CharField(
257
+ help_text="Outbox lifecycle: `pending` → `dispatched` → `done` | `dead`."
258
+ )
259
+ filename = serializers.CharField()
260
+ summary = serializers.DictField(
261
+ help_text=(
262
+ "The importer's own result summary — counts, parked rows, row errors. "
263
+ "**Empty (`{}`), never null**, until the job has run: `result_summary` is a "
264
+ "`JSONField(default=dict)` with `null=False` and nothing writes `None` to it. "
265
+ "Branch on `status`, not on this being absent."
266
+ )
267
+ )
268
+ requested_at = serializers.DateTimeField()
269
+ date_order = serializers.CharField(
270
+ help_text=(
271
+ "The slash-date convention this import actually ran under, read back so "
272
+ '"why does this task say 62 days" stays answerable afterwards (#2926).'
273
+ )
274
+ )
275
+ date_order_confirmed = serializers.BooleanField(
276
+ help_text="Whether a human accepted that convention rather than letting `auto` decide."
277
+ )
278
+ can_undo = serializers.BooleanField(
279
+ help_text=(
280
+ "May THIS caller undo the import? (#3353) Committing is the Scheduler band "
281
+ "and `POST …/import/csv/{id}/undo/` is Admin+, so reaching this endpoint at "
282
+ "all says nothing about the undo's gate. **Authority only** — a `true` is "
283
+ "not a promise the undo will succeed: it additionally requires the project "
284
+ "to be unarchived, and the import to still be in `done` state. Orthogonal "
285
+ "to `status`, which answers whether there is anything to undo."
286
+ )
287
+ )
288
+
289
+
290
+ class CsvImportColumnMappingSerializer(serializers.Serializer[Any]):
291
+ """One detected column, as returned in preview's ``columns[]`` (#3682)."""
292
+
293
+ index = serializers.IntegerField()
294
+ header = serializers.CharField()
295
+ field = serializers.CharField(
296
+ allow_null=True, help_text="Target field this column resolved to, or null when unmapped."
297
+ )
298
+ confidence = serializers.CharField(
299
+ help_text="One of `exact` / `fuzzy` / `none` / `duplicate` / `override`."
300
+ )
301
+
302
+
303
+ class CsvImportRowErrorSerializer(serializers.Serializer[Any]):
304
+ """One row-scoped diagnostic, as returned in preview's ``row_errors[]`` (#3682)."""
305
+
306
+ row = serializers.IntegerField()
307
+ column = serializers.CharField(allow_null=True)
308
+ code = serializers.CharField()
309
+ message = serializers.CharField()
310
+ severity = serializers.CharField(
311
+ help_text="`error` (row did not import) or `warning` (row imported, field dropped)."
312
+ )
313
+
314
+
315
+ class CsvImportDateEvidenceSerializer(serializers.Serializer[Any]):
316
+ """The single cell that self-identified the date order, when one did (#2926)."""
317
+
318
+ row = serializers.IntegerField()
319
+ column = serializers.CharField()
320
+ value = serializers.CharField()
321
+ reason = serializers.CharField(
322
+ help_text="`no_thirteenth_month` or `second_part_exceeds_twelve`."
323
+ )
324
+
325
+
326
+ class CsvImportDateReadingSerializer(serializers.Serializer[Any]):
327
+ """One convention's reading of a sample row — populated only for an ambiguous file."""
328
+
329
+ order = serializers.CharField()
330
+ sample_row = serializers.IntegerField(allow_null=True)
331
+ sample_name = serializers.CharField(allow_blank=True)
332
+ sample_raw_start = serializers.CharField(allow_blank=True)
333
+ start = serializers.CharField(allow_null=True)
334
+ finish = serializers.CharField(allow_null=True)
335
+ duration_days = serializers.IntegerField(allow_null=True)
336
+ values_matched = serializers.IntegerField()
337
+ values_failed = serializers.IntegerField()
338
+ rows_unparseable = serializers.IntegerField()
339
+
340
+
341
+ class CsvImportDatePreviewRowSerializer(serializers.Serializer[Any]):
342
+ """One rendered row for the wizard's dates preview table (#2926)."""
343
+
344
+ row = serializers.IntegerField()
345
+ name = serializers.CharField(allow_blank=True)
346
+ raw_start = serializers.CharField(allow_blank=True)
347
+ raw_finish = serializers.CharField(allow_blank=True)
348
+ start = serializers.CharField(allow_null=True)
349
+ finish = serializers.CharField(allow_null=True)
350
+ duration_days = serializers.IntegerField(allow_null=True)
351
+ unreadable = serializers.BooleanField()
352
+
353
+
354
+ class CsvImportFieldChoiceSerializer(serializers.Serializer[Any]):
355
+ """One entry in the wizard's per-column field dropdown catalog."""
356
+
357
+ field = serializers.CharField()
358
+ label = serializers.CharField() # type: ignore[assignment]
359
+ required = serializers.BooleanField() # type: ignore[assignment]
360
+ multi = serializers.BooleanField()
361
+
362
+
363
+ class CsvImportPreviewResponseSerializer(serializers.Serializer[Any]):
364
+ """The 200 body of ``POST …/import/csv/preview/`` — declared, not prose (#3682).
365
+
366
+ Nothing is persisted by this endpoint; the view builds this dict directly
367
+ from a :class:`~trueppm_api.apps.csvimport.parser.ParseResult`.
368
+ Response-only — nothing deserializes it.
369
+ """
370
+
371
+ filename = serializers.CharField()
372
+ headers = serializers.ListField(child=serializers.CharField())
373
+ columns = CsvImportColumnMappingSerializer(many=True)
374
+ sample_rows = serializers.ListField(
375
+ child=serializers.ListField(child=serializers.CharField()),
376
+ help_text="First 10 parsed rows, raw cell text.",
377
+ )
378
+ row_count = serializers.IntegerField()
379
+ truncated_rows = serializers.IntegerField()
380
+ task_count = serializers.IntegerField(
381
+ help_text="The plan-task count, excluding the Import review branch."
382
+ )
383
+ existing_task_count = serializers.IntegerField()
384
+ projected_task_count = serializers.IntegerField()
385
+ recommended_task_ceiling = serializers.IntegerField()
386
+ exceeds_recommended_ceiling = serializers.BooleanField()
387
+ resource_count = serializers.IntegerField()
388
+ parked_row_count = serializers.IntegerField(
389
+ help_text="Rows that cannot become plan tasks and will be parked in the review branch."
390
+ )
391
+ review_branch_name = serializers.CharField()
392
+ row_errors = CsvImportRowErrorSerializer(many=True)
393
+ error_count = serializers.IntegerField()
394
+ warning_count = serializers.IntegerField()
395
+ warnings = serializers.ListField(child=serializers.CharField())
396
+ date_order = serializers.CharField(help_text="What the caller requested: one of `DATE_ORDERS`.")
397
+ date_order_resolved = serializers.CharField(help_text="What will actually be used.")
398
+ date_order_auto = serializers.CharField(help_text="What `auto` would have chosen.")
399
+ date_order_ambiguous = serializers.BooleanField()
400
+ date_order_evidence = CsvImportDateEvidenceSerializer(allow_null=True)
401
+ date_order_has_columns = serializers.BooleanField()
402
+ date_order_readings = CsvImportDateReadingSerializer(
403
+ many=True, help_text="Both conventions' readings, populated only when ambiguous."
404
+ )
405
+ values_matched = serializers.IntegerField()
406
+ values_failed = serializers.IntegerField()
407
+ date_preview = CsvImportDatePreviewRowSerializer(many=True)
408
+ available_fields = CsvImportFieldChoiceSerializer(many=True)
409
+
410
+
411
+ class CsvImportUndoResultSerializer(serializers.Serializer[Any]):
412
+ """The ``undo`` sub-object of the undo response — split counts (#3682)."""
413
+
414
+ deleted = serializers.IntegerField(
415
+ help_text="Rows soft-deleted because no one had touched them."
416
+ )
417
+ kept = serializers.IntegerField(
418
+ help_text="Rows left in place because a collaborator has since modified them."
419
+ )
420
+
421
+
422
+ class CsvImportUndoResponseSerializer(serializers.Serializer[Any]):
423
+ """The 200 body of ``POST …/import/csv/{id}/undo/`` — declared, not prose (#3682)."""
424
+
425
+ id = serializers.UUIDField()
426
+ status = serializers.CharField()
427
+ undo = CsvImportUndoResultSerializer()
428
+
429
+
430
+ class CsvImportPreviewView(IdempotencyMixin, APIView):
431
+ """Parse a spreadsheet and return the detected mapping — persisting nothing.
432
+
433
+ Synchronous and stateless by design (ADR-0632 decision 5): the wizard holds
434
+ the mapping in client state, so a retry after a failed commit replays it
435
+ without any server-side draft to resume or expire.
436
+ """
437
+
438
+ # Nothing is persisted, so a replay has no resource to dedup.
439
+ idempotency_exempt = True
440
+ # Gated identically to the commit path: preview parses attacker-supplied
441
+ # files, so it is not a lighter-privilege surface just because it does not
442
+ # persist.
443
+ permission_classes = [IsAuthenticated, IsProjectScheduler, IsProjectNotArchived]
444
+ parser_classes = [MultiPartParser]
445
+
446
+ @extend_schema(
447
+ summary="Preview a CSV/Excel import: detected column mapping and sample rows",
448
+ request=_IMPORT_REQUEST_SCHEMA,
449
+ responses={
450
+ 200: OpenApiResponse(
451
+ response=CsvImportPreviewResponseSerializer,
452
+ description=(
453
+ "Detected mapping, first 10 parsed rows, row-level warnings and the "
454
+ "field catalog for the wizard's dropdown. Nothing is persisted. "
455
+ "Also carries how the date order was settled and the evidence for "
456
+ "it: `date_order_resolved`, `date_order_auto`, "
457
+ "`date_order_ambiguous`, `date_order_evidence` "
458
+ "({row, column, value, reason}), `date_order_has_columns`, "
459
+ "`values_matched`/`values_failed`, `date_preview` (per-row raw cell, "
460
+ "reading and duration), and — only when the file identifies no "
461
+ "convention — `date_order_readings`, both conventions with the "
462
+ "duration each produces for one sample row. `exceeds_recommended_ceiling` "
463
+ "and its sibling fields warn (never block) when this project would land "
464
+ "past the tested-comfortable Schedule size — see "
465
+ "`administration/sizing.md`."
466
+ ),
467
+ ),
468
+ 400: OpenApiResponse(
469
+ description="Missing, oversized, or unreadable upload, or an unknown `date_order`."
470
+ ),
471
+ 403: OpenApiResponse(
472
+ description="Caller lacks the Resource Manager role on the project."
473
+ ),
474
+ },
475
+ )
476
+ def post(self, request: Request, project_pk: str) -> Response:
477
+ _require_project_scheduler(request.user, project_pk)
478
+
479
+ date_order = _parse_date_order(request)
480
+
481
+ validated = _read_validated_upload(request)
482
+ if isinstance(validated, Response):
483
+ return validated
484
+ filename, content = validated
485
+
486
+ try:
487
+ parsed = parse_spreadsheet(
488
+ content,
489
+ filename,
490
+ column_map=_parse_column_map(request) or None,
491
+ date_order=date_order,
492
+ max_rows=settings.CSV_IMPORT_MAX_ROWS,
493
+ max_uncompressed_bytes=settings.CSV_IMPORT_MAX_UNCOMPRESSED_MB * 1024 * 1024,
494
+ )
495
+ except CsvImportError as exc:
496
+ return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
497
+
498
+ # #3388: warn — never block — when this import would land the project past
499
+ # the tested-comfortable Schedule size (administration/sizing.md "Tested
500
+ # envelope": 1.85s at 1,000 tasks, 60s at 2,000 — a cliff, not a slope).
501
+ # Local import: projects -> csvimport already exists (task_batch_services,
502
+ # tasks.py), so this direction is symmetric with the established pattern
503
+ # rather than a new circular-import risk.
504
+ from trueppm_api.apps.projects.models import Task
505
+
506
+ existing_task_count = Task.objects.filter(project_id=project_pk, is_deleted=False).count()
507
+ projected_task_count = existing_task_count + parsed.plan_task_count
508
+
509
+ return Response(
510
+ {
511
+ "filename": filename,
512
+ "headers": parsed.headers,
513
+ "columns": [
514
+ {
515
+ "index": m.index,
516
+ "header": m.header,
517
+ "field": m.field,
518
+ "confidence": m.confidence,
519
+ }
520
+ for m in parsed.mapping
521
+ ],
522
+ "sample_rows": parsed.sample_rows,
523
+ "row_count": parsed.total_rows,
524
+ "truncated_rows": parsed.truncated_rows,
525
+ # The plan, not the plan plus its Import review branch (#2732):
526
+ # this number answers "how much of my file arrives as schedule",
527
+ # and the parked rows are counted on their own line below.
528
+ "task_count": parsed.plan_task_count,
529
+ # The tested-comfortable Schedule ceiling (#3388), and whether this
530
+ # project would land past it once this import lands — existing plan
531
+ # tasks plus this file's, not just the file's row count, because the
532
+ # failure the operator would hit is opening the *project*, not the
533
+ # import. Advisory only: `settings.SCHEDULE_TASK_CEILING` is a
534
+ # measured comfort line, not a correctness limit, so this never
535
+ # blocks the commit step.
536
+ "existing_task_count": existing_task_count,
537
+ "projected_task_count": projected_task_count,
538
+ "recommended_task_ceiling": settings.SCHEDULE_TASK_CEILING,
539
+ "exceeds_recommended_ceiling": projected_task_count
540
+ > settings.SCHEDULE_TASK_CEILING,
541
+ "resource_count": len(parsed.project_data.resources),
542
+ # Rows that cannot become plan tasks and will be parked in the
543
+ # Import review branch instead of dropped.
544
+ "parked_row_count": len(parsed.unresolved_rows),
545
+ "review_branch_name": REVIEW_BRANCH_NAME,
546
+ "row_errors": [e.as_dict() for e in parsed.row_errors],
547
+ # Split counts, not just a total: an operator deciding whether to
548
+ # commit needs to know how many rows would be *lost* separately
549
+ # from how many would land with a field defaulted.
550
+ "error_count": parsed.error_count,
551
+ "warning_count": parsed.warning_count,
552
+ "warnings": parsed.warnings,
553
+ # How the date order was settled, and the evidence for it
554
+ # (#2926). Flattened onto the response rather than nested so the
555
+ # wizard's Dates block reads one shape whether it is confirming
556
+ # an inference or reporting an override. `readings` is populated
557
+ # only for the ambiguous file, which is the state that needs both
558
+ # conventions' durations side by side to be decidable at all.
559
+ "date_order": parsed.date_order.requested,
560
+ "date_order_resolved": parsed.date_order.resolved,
561
+ "date_order_auto": parsed.date_order.auto_resolved,
562
+ "date_order_ambiguous": parsed.date_order.ambiguous,
563
+ "date_order_evidence": parsed.date_order.evidence,
564
+ "date_order_has_columns": parsed.date_order.has_date_columns,
565
+ "date_order_readings": [r.as_dict() for r in parsed.date_order.readings],
566
+ "values_matched": parsed.date_order.values_matched,
567
+ "values_failed": parsed.date_order.values_failed,
568
+ "date_preview": parsed.date_preview,
569
+ "available_fields": field_choices(),
570
+ },
571
+ status=status.HTTP_200_OK,
572
+ )
573
+
574
+
575
+ class CsvImportView(IdempotencyMixin, APIView):
576
+ """Upload and import a CSV/Excel spreadsheet into an existing project.
577
+
578
+ Requires the Resource Manager role. The import runs asynchronously through the
579
+ transactional outbox; the 202 response carries ``import_request_id`` — not a
580
+ Celery task id, which does not exist until the on_commit dispatch fires
581
+ (ADR-0632 decision 6).
582
+ """
583
+
584
+ # Multipart upload, deduped at the table level via CsvImportRequest.
585
+ idempotency_exempt = True
586
+ permission_classes = [IsAuthenticated, IsProjectScheduler, IsProjectNotArchived]
587
+ parser_classes = [MultiPartParser]
588
+
589
+ @extend_schema(
590
+ summary="Import a CSV/Excel spreadsheet into an existing project",
591
+ request=_IMPORT_REQUEST_SCHEMA,
592
+ responses={
593
+ 202: inline_serializer(
594
+ name="CsvImportQueuedResponse",
595
+ fields={
596
+ "detail": serializers.CharField(),
597
+ "queued": serializers.BooleanField(),
598
+ "import_request_id": serializers.UUIDField(),
599
+ },
600
+ ),
601
+ 400: OpenApiResponse(
602
+ description=(
603
+ "Missing or invalid upload (wrong type/too large), or an unknown "
604
+ "`date_order`. The order is persisted on the import request so a "
605
+ "drain re-dispatch replays the convention the operator confirmed."
606
+ )
607
+ ),
608
+ 403: OpenApiResponse(
609
+ description="Caller lacks the Resource Manager role on the project."
610
+ ),
611
+ },
612
+ )
613
+ def post(self, request: Request, project_pk: str) -> Response:
614
+ _require_project_scheduler(request.user, project_pk)
615
+ date_order = _parse_date_order(request)
616
+ # True when the operator explicitly accepted a convention rather than
617
+ # letting auto decide. Kept for support archaeology only — nothing
618
+ # branches on it (#2926).
619
+ # `parser_classes = [MultiPartParser]` — see `_parse_column_map`.
620
+ date_order_confirmed = str(_form_body(request).get("date_order_confirmed", "")).lower() in {
621
+ "true",
622
+ "1",
623
+ }
624
+
625
+ validated = _read_validated_upload(request)
626
+ if isinstance(validated, Response):
627
+ return validated
628
+ filename, content = validated
629
+
630
+ from trueppm_api.apps.csvimport.models import CsvImportRequest
631
+ from trueppm_api.apps.csvimport.services import enqueue_csv_import
632
+
633
+ # Commit the outbox row before any dispatch attempt so a broker outage
634
+ # cannot lose the import — the row stays PENDING and the drain retries.
635
+ with transaction.atomic():
636
+ req = CsvImportRequest.objects.create(
637
+ project_id=project_pk,
638
+ filename=filename,
639
+ file_content_b64=base64.b64encode(content).decode("ascii"),
640
+ column_map=_parse_column_map(request),
641
+ date_order=date_order,
642
+ date_order_confirmed=date_order_confirmed,
643
+ initiated_by_id=request.user.pk,
644
+ )
645
+
646
+ req_id = str(req.pk)
647
+ transaction.on_commit(lambda: enqueue_csv_import(req_id))
648
+
649
+ return Response(
650
+ {"detail": "Import queued.", "queued": True, "import_request_id": req_id},
651
+ status=status.HTTP_202_ACCEPTED,
652
+ )
653
+
654
+
655
+ class CsvImportStatusView(APIView):
656
+ """Poll one import's outbox row for its terminal state and summary.
657
+
658
+ This is what makes an async failure visible on the Schedule that launched it
659
+ (#2151): the wizard polls here after its 202 rather than for a Celery task
660
+ id that the outbox never produces synchronously.
661
+ """
662
+
663
+ permission_classes = [IsAuthenticated, IsProjectScheduler, IsProjectNotArchived]
664
+
665
+ @extend_schema(
666
+ summary="Status and result summary of one CSV/Excel import",
667
+ responses={
668
+ 200: OpenApiResponse(
669
+ response=CsvImportStatusResponseSerializer,
670
+ description=(
671
+ "The import's outbox row: lifecycle state, result summary, the date "
672
+ "convention it ran under, and the caller's authority over the undo."
673
+ ),
674
+ ),
675
+ 403: OpenApiResponse(
676
+ description="Caller lacks the Resource Manager role on the project."
677
+ ),
678
+ 404: OpenApiResponse(description="No such import for this project."),
679
+ },
680
+ )
681
+ def get(self, request: Request, project_pk: str, pk: str) -> Response:
682
+ _require_project_scheduler(request.user, project_pk)
683
+
684
+ from trueppm_api.apps.csvimport.models import CsvImportRequest
685
+
686
+ # Scope by project as well as pk so an id leaked from another project
687
+ # cannot be read here (IDOR).
688
+ req = CsvImportRequest.objects.filter(id=pk, project_id=project_pk).first()
689
+ if req is None:
690
+ return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
691
+
692
+ return Response(
693
+ {
694
+ "id": str(req.id),
695
+ "status": req.status,
696
+ "filename": req.filename,
697
+ "summary": req.result_summary,
698
+ "requested_at": req.requested_at,
699
+ # The convention this import actually ran under, and whether a
700
+ # human accepted it (#2926). Read back here rather than only
701
+ # persisted: "why does this task say 62 days" is answerable only
702
+ # if the convention is legible after the fact, and a field
703
+ # nothing can read is a field that does not exist for a headless
704
+ # client. `date_order_confirmed` had no reader at all until this.
705
+ "date_order": req.date_order,
706
+ "date_order_confirmed": req.date_order_confirmed,
707
+ # #3353: committing an import is `IsProjectScheduler` (200) and
708
+ # `CsvImportUndoView` is Admin+ (300), so a Scheduler runs an import
709
+ # and is refused the Undo the wizard used to offer regardless. The
710
+ # status payload is where the wizard learns which of the two floors
711
+ # the caller cleared — a 202 on the commit says nothing about the
712
+ # undo's gate (web rule 373(a)).
713
+ #
714
+ # Pure authority, deliberately orthogonal to `status`: `status ==
715
+ # "done"` answers "is there anything to undo", this answers "may
716
+ # you". Folding them into one boolean would make a `false` mean
717
+ # either, with no way for a client to tell them apart.
718
+ "can_undo": can_user_undo_batch_operation(request, project_pk),
719
+ },
720
+ status=status.HTTP_200_OK,
721
+ )
722
+
723
+
724
+ class CsvImportUndoView(IdempotencyMixin, APIView):
725
+ """⌘Z one completed import (ADR-0810, #2756, #2732).
726
+
727
+ Project-nested like ``CsvImportStatusView`` rather than a router-registered
728
+ ``import-fix-operations`` collection: this app has no router, and every
729
+ other endpoint here is a plain ``path()`` under ``projects/<project_pk>/
730
+ import/csv/...`` — matching that convention beats forcing in a different
731
+ app's routing style for one endpoint.
732
+
733
+ No separate ``ImportFixOperation`` model backs this — ``CsvImportRequest``
734
+ already is the outbox row for the import (see ``task_batch_services.py``'s
735
+ import-fix section for why a parallel table would have duplicated it).
736
+ """
737
+
738
+ permission_classes = [IsAuthenticated, IsProjectScheduler, IsProjectNotArchived]
739
+
740
+ @extend_schema(
741
+ summary="Undo one completed CSV/Excel import",
742
+ request=None,
743
+ responses={
744
+ 200: OpenApiResponse(
745
+ response=CsvImportUndoResponseSerializer,
746
+ description='{"status", "undo": {"deleted", "kept"}}.',
747
+ ),
748
+ 400: OpenApiResponse(description="Import is not in a completed (done) state."),
749
+ 403: OpenApiResponse(description="Caller lacks the Admin role on the project."),
750
+ 404: OpenApiResponse(description="No such import for this project."),
751
+ },
752
+ )
753
+ def post(self, request: Request, project_pk: str, pk: str) -> Response:
754
+ _require_project_admin(request.user, project_pk)
755
+
756
+ from trueppm_api.apps.csvimport.models import CsvImportRequest, CsvImportStatus
757
+ from trueppm_api.apps.projects.task_batch_services import undo_import_fix_operation
758
+
759
+ req = CsvImportRequest.objects.filter(id=pk, project_id=project_pk).first()
760
+ if req is None:
761
+ return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
762
+ if req.status != CsvImportStatus.DONE:
763
+ return Response(
764
+ {"detail": f"Only a completed import can be undone (this one is {req.status})."},
765
+ status=status.HTTP_400_BAD_REQUEST,
766
+ )
767
+
768
+ summary = undo_import_fix_operation(req)
769
+ req.refresh_from_db()
770
+ return Response(
771
+ {"id": str(req.id), "status": req.status, "undo": summary},
772
+ status=status.HTTP_200_OK,
773
+ )
774
+
775
+
776
+ class CsvImportTemplateView(APIView):
777
+ """Download the known-good CSV template.
778
+
779
+ Authenticated but not project-scoped: the template is static content with no
780
+ customer data in it, and the wizard links it from step 1 before a project
781
+ context necessarily exists.
782
+ """
783
+
784
+ permission_classes = [IsAuthenticated]
785
+
786
+ @extend_schema(
787
+ summary="Download a known-good CSV import template",
788
+ responses={200: OpenApiResponse(description="text/csv template file.")},
789
+ )
790
+ def get(self, request: Request) -> HttpResponse:
791
+ response = HttpResponse(CSV_TEMPLATE, content_type="text/csv; charset=utf-8")
792
+ response["Content-Disposition"] = f'attachment; filename="{TEMPLATE_FILENAME}"'
793
+ return response