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,1095 @@
1
+ """Import parsed MS Project data into TruePPM models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from trueppm_api.apps.msproject.dataclasses import (
9
+ CONSTRAINT_TYPES_APPLIED_AS_SNET,
10
+ CalendarData,
11
+ ProjectData,
12
+ TaskData,
13
+ )
14
+ from trueppm_api.apps.sync.sequence import allocate_for_projects
15
+
16
+ if TYPE_CHECKING:
17
+ from trueppm_api.apps.scheduling.graph_guard import InfeasibleGraphError
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def import_project(
23
+ project_id: str,
24
+ data: ProjectData,
25
+ tracker: Any = None,
26
+ wipe_existing: bool = False,
27
+ *,
28
+ source_kind: str | None = None,
29
+ source_id: Any = None,
30
+ ) -> dict[str, Any]:
31
+ """Import parsed MS Project data into a TruePPM project.
32
+
33
+ Creates tasks, dependencies, resources, and assignments using bulk operations
34
+ to bypass django-simple-history (per ADR-0011).
35
+
36
+ Args:
37
+ project_id: UUID string of the target Project.
38
+ data: Parsed ProjectData from the MS Project file.
39
+ tracker: Optional TaskRunTracker for progress reporting.
40
+ wipe_existing: When True (create-from-import, ADR-0092), delete the
41
+ project's existing tasks before bulk-create so an orphan-drain
42
+ re-dispatch converges instead of duplicating. The default (False)
43
+ keeps the import-into-existing-project path additive — and, since
44
+ #3069, also shifts the file's WBS numbering past the rows already in
45
+ the project instead of writing onto them.
46
+ source_kind: Seed provenance stamped on every created row (ADR-0786,
47
+ #2730). Defaults to ``TaskSource.MSPROJECT_IMPORT``; the CSV/Excel
48
+ wizard reuses this whole path (``csvimport/tasks.py``) and passes
49
+ ``TaskSource.CSV_IMPORT`` so the two are distinguishable afterwards.
50
+ source_id: Id of the import job that wrote these rows, for the divergence
51
+ digest. ``None`` for a path with no persisted job row.
52
+
53
+ Returns:
54
+ Summary dict for result_summary. ``task_count`` and ``project_start_date``
55
+ feed the post-import summary the UI shows on the project landing.
56
+ """
57
+ from django.conf import settings
58
+
59
+ from trueppm_api.apps.projects.models import Task
60
+
61
+ # Chunk every bulk_create (#1721) so a large import is not emitted as one
62
+ # unbounded multi-row INSERT (which pins the whole set in a single statement
63
+ # and can blow PostgreSQL's parameter limit). Paired with the parser row cap.
64
+ batch_size = getattr(settings, "IMPORT_BULK_BATCH_SIZE", 500)
65
+
66
+ def _update(pct: int, msg: str) -> None:
67
+ if tracker is not None:
68
+ tracker.update(pct, msg)
69
+
70
+ _update(5, "Preparing import...")
71
+ summary = _init_summary(data)
72
+
73
+ if wipe_existing:
74
+ # Safe because the project was created empty for this import (ADR-0092);
75
+ # only a partial prior attempt could leave rows. Dependency/TaskResource
76
+ # cascade off Task, so deleting tasks clears the prior attempt.
77
+ Task.objects.filter(project_id=project_id).delete()
78
+
79
+ # --- Step 0: Apply the file's calendar (#1769) ---
80
+ # Runs before the no-tasks early return: the calendar is header-level plan
81
+ # data (like name/start_date) and applies even to a file with no tasks.
82
+ _update(8, "Applying project calendar...")
83
+ _apply_calendar(project_id, data, summary, overwrite=wipe_existing, batch_size=batch_size)
84
+
85
+ if not data.tasks:
86
+ summary["warnings"].append("No tasks found in file")
87
+ return summary
88
+
89
+ # --- Step 1: Create or match resources ---
90
+ _update(10, "Matching resources...")
91
+ resource_uid_to_pk = _import_resources(data, summary, batch_size=batch_size)
92
+
93
+ # --- Step 2: Create tasks ---
94
+ _update(30, f"Creating {len(data.tasks)} tasks...")
95
+ task_objects = _create_tasks(
96
+ project_id,
97
+ data,
98
+ summary,
99
+ batch_size=batch_size,
100
+ source_kind=source_kind,
101
+ source_id=source_id,
102
+ wipe_existing=wipe_existing,
103
+ )
104
+ task_uid_to_pk = {td.uid: str(task_objects[i].pk) for i, td in enumerate(data.tasks)}
105
+ # ADR-0810 (#2756, CSV path only): every row this import wrote, so the caller
106
+ # can record the ⌘Z undo ledger. Harmless for the MS Project path, which has
107
+ # no undo affordance today and simply ignores the key.
108
+ summary["created_task_ids"] = [str(t.pk) for t in task_objects]
109
+
110
+ # --- Step 3: Create dependencies ---
111
+ _update(50, "Creating dependencies...")
112
+ _import_dependencies(
113
+ data, task_uid_to_pk, summary, project_id=project_id, batch_size=batch_size
114
+ )
115
+
116
+ # --- Step 4: Create resource assignments ---
117
+ _update(70, "Creating resource assignments...")
118
+ _import_assignments(
119
+ project_id, data, task_uid_to_pk, resource_uid_to_pk, summary, batch_size=batch_size
120
+ )
121
+
122
+ # --- Step 5: Attach labels ---
123
+ _update(80, "Attaching labels...")
124
+ _import_labels(project_id, data, task_uid_to_pk, summary, batch_size=batch_size)
125
+
126
+ _update(90, "Import complete, triggering schedule recalculation...")
127
+ return summary
128
+
129
+
130
+ def _init_summary(data: ProjectData) -> dict[str, Any]:
131
+ """Build the zeroed result-summary dict, seeded from the parsed file."""
132
+ summary: dict[str, Any] = {
133
+ "tasks_created": 0,
134
+ "task_count": 0,
135
+ "dependencies_created": 0,
136
+ "resources_matched": 0,
137
+ "resources_created": 0,
138
+ "assignments_created": 0,
139
+ # Three-point / PERT counts (#798, ADR-0093). The UI uses these to
140
+ # confirm e.g. "3-point estimates imported for 17 of 23 work tasks".
141
+ # ``tasks_with_three_point_estimates`` includes only tasks that
142
+ # actually received all three values; ``tasks_skipped_partial_three_point``
143
+ # counts tasks the parser dropped to None because the file only
144
+ # supplied a subset.
145
+ "tasks_with_three_point_estimates": 0,
146
+ "tasks_skipped_partial_three_point": 0,
147
+ # ADR-1153 actual-date rules (#3709): counts rows where the file's
148
+ # actual_start/actual_finish would have violated ordering, the future
149
+ # bound, the span cap, or the sign-off gate. The offending field is
150
+ # dropped rather than the whole row — see ``_sanitize_actual_dates``.
151
+ "tasks_with_invalid_actuals_dropped": 0,
152
+ # Calendar mapping (#1769). ``calendar_applied`` is True when the file's
153
+ # calendar was wired as Project.calendar (created or matched);
154
+ # ``calendar_created``/``calendar_matched`` say which path produced it.
155
+ "calendar_applied": False,
156
+ "calendar_name": None,
157
+ "calendar_created": False,
158
+ "calendar_matched": False,
159
+ "project_start_date": data.start_date,
160
+ # #873/#867: set True when an imported task predated the project start and
161
+ # the start was pulled back. The import task broadcasts project_updated so
162
+ # live collaborators on an existing project re-fetch the moved boundary.
163
+ "project_start_shifted": False,
164
+ # Label attach counts (#2406, ADR-0400). ``labels_matched`` are catalog
165
+ # entries the file reused; ``labels_created`` are ones it minted.
166
+ "labels_matched": 0,
167
+ "labels_created": 0,
168
+ "label_assignments_created": 0,
169
+ "warnings": list(data.warnings),
170
+ }
171
+ # Count tasks the parser warned about for partial PERT data; the warning
172
+ # text is the canonical marker (the parser dropped values to None before
173
+ # we got here, so we can't tell from the dataclass alone).
174
+ summary["tasks_skipped_partial_three_point"] = sum(
175
+ 1 for w in data.warnings if "partial three-point estimate" in w
176
+ )
177
+ return summary
178
+
179
+
180
+ def broadcast_import_project_record_change(project_id: str, summary: dict[str, Any]) -> None:
181
+ """Broadcast ``project_updated`` when an import moved project-record state.
182
+
183
+ Two things an import can change live outside the task tree: the project's
184
+ ``start_date`` (pulled back when an imported task predates it, #867/#873) and
185
+ ``Project.calendar`` (#1769). Neither is invalidated by the recalc's
186
+ ``cpm_complete`` or by ``tasks_restructured`` — both of those speak about
187
+ tasks — so without this a collaborator watching the Gantt sees the whole
188
+ schedule shift under them with no signal and no attribution.
189
+
190
+ Deferred with ``transaction.on_commit`` so a rolled-back import never tells
191
+ peers a boundary moved. Callers invoke this *after* the import's ``atomic()``
192
+ block has committed, at which point no ambient transaction remains and the
193
+ callback fires immediately.
194
+
195
+ Shared by all three import wrappers on purpose. MS Project, CSV and Jira all
196
+ run this same ``import_project``, but each had its own task module, and the
197
+ #873 broadcast was added to one of them and never reached the other two —
198
+ CSV and Jira shifted the start silently for three releases (#2610). A fork
199
+ with three call sites and one behavior is a drift waiting to happen; there is
200
+ now one site.
201
+ """
202
+ if not (summary.get("project_start_shifted") or summary.get("calendar_applied")):
203
+ return
204
+
205
+ from django.db import transaction
206
+
207
+ from trueppm_api.apps.sync.broadcast import broadcast_board_event
208
+
209
+ transaction.on_commit(
210
+ lambda: broadcast_board_event(project_id, "project_updated", {"id": project_id})
211
+ )
212
+
213
+
214
+ def _import_labels(
215
+ project_id: str,
216
+ data: ProjectData,
217
+ task_uid_to_pk: dict[int, str],
218
+ summary: dict[str, Any],
219
+ *,
220
+ batch_size: int,
221
+ ) -> None:
222
+ """Match-or-create the file's label names in the project catalog and attach them.
223
+
224
+ Matching is **case-insensitive against the project's existing catalog**
225
+ (ADR-0400 scopes ``Label`` to a project), so re-importing a sheet that says
226
+ ``Safety`` into a project that already has ``safety`` reuses the existing
227
+ entry instead of minting a near-duplicate the operator then has to merge.
228
+ The existing spelling wins on a match — the catalog is curated, the
229
+ spreadsheet is not.
230
+
231
+ Idempotent across re-import in both directions: labels resolve by name, and
232
+ the ``TaskLabel`` rows use ``ignore_conflicts`` against the
233
+ ``unique(task, label)`` constraint, so a second run of the same file creates
234
+ nothing new.
235
+ """
236
+ from trueppm_api.apps.projects.models import Label, LabelColor, TaskLabel
237
+
238
+ wanted: dict[str, str] = {}
239
+ for td in data.tasks:
240
+ for name in td.labels:
241
+ wanted.setdefault(name.lower(), name)
242
+ if not wanted:
243
+ return
244
+
245
+ existing = {
246
+ label.name.lower(): label
247
+ for label in Label.objects.filter(project_id=project_id, is_deleted=False)
248
+ }
249
+ summary["labels_matched"] = sum(1 for key in wanted if key in existing)
250
+
251
+ # Colors cycle the palette from the end of the current catalog, so an import
252
+ # of several labels produces a distinguishable set rather than a wall of the
253
+ # default. Position continues the catalog's order for the same reason.
254
+ palette = [choice[0] for choice in LabelColor.choices]
255
+ start = len(existing)
256
+ new_labels = [
257
+ Label(
258
+ project_id=project_id,
259
+ name=wanted[key],
260
+ color=palette[(start + offset) % len(palette)],
261
+ position=start + offset,
262
+ )
263
+ for offset, key in enumerate(k for k in wanted if k not in existing)
264
+ ]
265
+ if new_labels:
266
+ # bulk_create bypasses VersionedModel.save(), so these rows draw no delta
267
+ # cursor of their own and would sit at sync_seq=0 — below every checkpoint,
268
+ # and so never delivered to an offline client. `labels` is its own sync
269
+ # collection, so it needs the same one-value-per-import treatment the task
270
+ # rows get above (ADR-0686). The post-import recalculation cannot stand in
271
+ # for this: it writes through bulk_update and allocates nothing.
272
+ seq = allocate_for_projects([project_id])
273
+ for label in new_labels:
274
+ label.sync_seq = seq
275
+ Label.objects.bulk_create(new_labels, batch_size=batch_size)
276
+ summary["labels_created"] = len(new_labels)
277
+ existing.update({label.name.lower(): label for label in new_labels})
278
+
279
+ assignments = [
280
+ TaskLabel(task_id=task_uid_to_pk[td.uid], label=existing[name.lower()])
281
+ for td in data.tasks
282
+ if td.uid in task_uid_to_pk
283
+ for name in td.labels
284
+ if name.lower() in existing
285
+ ]
286
+ if assignments:
287
+ TaskLabel.objects.bulk_create(assignments, batch_size=batch_size, ignore_conflicts=True)
288
+ summary["label_assignments_created"] = len(assignments)
289
+
290
+
291
+ def _import_resources(
292
+ data: ProjectData, summary: dict[str, Any], *, batch_size: int
293
+ ) -> dict[int, str]:
294
+ """Match parsed resources to the shared library by name, creating the rest.
295
+
296
+ Returns a map from the file's resource UID to the TruePPM Resource PK.
297
+ """
298
+ from trueppm_api.apps.resources.models import Resource
299
+
300
+ resource_uid_to_pk: dict[int, str] = {}
301
+ if not data.resources:
302
+ return resource_uid_to_pk
303
+
304
+ existing = {r.name.lower(): r for r in Resource.objects.all()}
305
+ new_resources: list[Resource] = []
306
+
307
+ for rd in data.resources:
308
+ name_lower = rd.name.lower()
309
+ if name_lower in existing:
310
+ resource_uid_to_pk[rd.uid] = str(existing[name_lower].pk)
311
+ summary["resources_matched"] += 1
312
+ else:
313
+ new_resources.append(Resource(name=rd.name, max_units=rd.max_units))
314
+
315
+ if new_resources:
316
+ Resource.objects.bulk_create(new_resources, batch_size=batch_size)
317
+ summary["resources_created"] = len(new_resources)
318
+ _map_created_resource_pks(data, resource_uid_to_pk)
319
+ return resource_uid_to_pk
320
+
321
+
322
+ def _map_created_resource_pks(data: ProjectData, resource_uid_to_pk: dict[int, str]) -> None:
323
+ """Fill in PKs for the just-created resources by re-reading the library by name.
324
+
325
+ bulk_create does not populate PKs on the passed instances for every backend, so
326
+ the newly created rows are re-fetched and matched to their file UIDs by name.
327
+ """
328
+ from trueppm_api.apps.resources.models import Resource
329
+
330
+ all_resources = {r.name.lower(): r for r in Resource.objects.all()}
331
+ for rd in data.resources:
332
+ if rd.uid not in resource_uid_to_pk:
333
+ matched = all_resources.get(rd.name.lower())
334
+ if matched:
335
+ resource_uid_to_pk[rd.uid] = str(matched.pk)
336
+
337
+
338
+ def _create_tasks(
339
+ project_id: str,
340
+ data: ProjectData,
341
+ summary: dict[str, Any],
342
+ *,
343
+ batch_size: int,
344
+ source_kind: str | None = None,
345
+ source_id: Any = None,
346
+ wipe_existing: bool = False,
347
+ ) -> list[Any]:
348
+ """Bulk-create Task rows for the parsed tasks and return them in file order.
349
+
350
+ Allocates a contiguous block of short_ids, computes WBS paths and summary-task
351
+ membership once, builds each row, pulls the project start back if a task
352
+ predates it, then bulk-creates. Returns the built Task objects so the caller
353
+ can map file UIDs to persisted PKs.
354
+
355
+ ``wipe_existing`` decides whether the file's own numbering is authoritative.
356
+ On create-from-import the project was made empty for this file, so its paths
357
+ are written verbatim. On import-into-existing they are shifted past the rows
358
+ already there — see the offset block below.
359
+ """
360
+ from django.db.models import F
361
+ from django.utils import timezone
362
+
363
+ from trueppm_api.apps.projects.models import Project, Task, TaskSource, bulk_create_tasks
364
+ from trueppm_api.apps.projects.wbs_paths import offset_wbs_path, root_ordinal_offset
365
+
366
+ # Allocate a batch of short_ids: increment object_sequence by len(tasks)
367
+ # in one UPDATE, then assign sequential hex IDs.
368
+ task_count = len(data.tasks)
369
+ Project.objects.filter(pk=project_id).update(object_sequence=F("object_sequence") + task_count)
370
+ end_seq: int = Project.objects.values_list("object_sequence", flat=True).get(pk=project_id)
371
+ start_seq = end_seq - task_count + 1
372
+ # Fetched once for the ADR-1153 actual-date check every row runs below
373
+ # (#3709) — needs project.start_date and .status_date, neither of which the
374
+ # object_sequence query above selects.
375
+ project = Project.objects.only("start_date", "status_date").get(pk=project_id)
376
+
377
+ wbs_paths = _build_wbs_paths(data.tasks)
378
+ if not wipe_existing:
379
+ # The file numbers its tasks from 1, which is only right for a project that
380
+ # started empty. Importing a revised .mpp / CSV / Jira pull into a project
381
+ # that already holds rows would otherwise write straight onto their paths
382
+ # (#3069) — the same defect template adoption had in #3061, and the reason
383
+ # `unique_task_wbs_path_per_project_live` now rejects it at COMMIT rather
384
+ # than letting it silently collapse the next rewrite_level pass.
385
+ #
386
+ # Shifted before summary detection, not after, so both work off one set of
387
+ # paths. The result is identical either way — `offset_wbs_path` moves only
388
+ # the leading segment, so ancestor/descendant relationships are preserved —
389
+ # but reading the same list twice is what keeps that true if either side
390
+ # changes.
391
+ offset = root_ordinal_offset(project_id, document_paths=wbs_paths)
392
+ if offset:
393
+ wbs_paths = [offset_wbs_path(path, offset) or path for path in wbs_paths]
394
+ # Summary-task detection (ADR-0093 Q5): a task is a summary if any later
395
+ # task's wbs_path is strictly descended from this one's. Computed once in
396
+ # O(N) by sweeping the WBS paths and marking each strict ancestor present
397
+ # in the prefix set. Three-point fields are NOT written on summary rows
398
+ # because MS Project never populates them there; round-tripping them
399
+ # would drift on re-export.
400
+ summary_indices = _summary_indices(wbs_paths)
401
+
402
+ task_objects: list[Task] = [
403
+ _build_task_object(
404
+ td, wbs_paths[i], i in summary_indices, project_id, start_seq + i, summary, project
405
+ )
406
+ for i, td in enumerate(data.tasks)
407
+ ]
408
+
409
+ _maybe_shift_project_start(project_id, data, summary)
410
+
411
+ # bulk_create bypasses VersionedModel.save(), so nothing draws a delta cursor
412
+ # for these rows — they would sit at sync_seq=0, below every checkpoint, and
413
+ # never reach an offline client. One value for the whole import is right: the
414
+ # rows are created together, and the delta is `> since` (ADR-0686).
415
+ seq = allocate_for_projects([project_id])
416
+ # Seed provenance (ADR-0786, #2730). Stamped on the instances rather than
417
+ # after the fact because bulk_create never reaches Task.save() — which is also
418
+ # why edited_at stays null here: a machine wrote these rows and no person has
419
+ # touched them, which is precisely what makes them sweepable for seven days.
420
+ seeded_at = timezone.now()
421
+ kind = source_kind or TaskSource.MSPROJECT_IMPORT
422
+ for task in task_objects:
423
+ task.sync_seq = seq
424
+ task.source_kind = kind
425
+ task.source_id = source_id
426
+ task.seeded_at = seeded_at
427
+
428
+ # `bulk_create_tasks`, not `Task.objects.bulk_create`: a summary row here has
429
+ # children and must land DECLARED a container, or the Board renders every
430
+ # imported phase as a card (#3030). The summary indices computed above answer
431
+ # the same question for the three-point gate, but the declaration is derived
432
+ # inside the helper so this path and the restructure endpoints cannot drift.
433
+ bulk_create_tasks(task_objects, batch_size=batch_size)
434
+ summary["tasks_created"] = len(task_objects)
435
+ summary["task_count"] = len(task_objects)
436
+ return task_objects
437
+
438
+
439
+ def _build_task_object(
440
+ td: TaskData,
441
+ wbs_path: str,
442
+ is_summary: bool,
443
+ project_id: str,
444
+ short_id_seq: int,
445
+ summary: dict[str, Any],
446
+ project: Any,
447
+ ) -> Any:
448
+ """Construct a single Task row from parsed TaskData, applying the import-time
449
+ invariants (three-point gating, milestone normalization, progress-anchor
450
+ clamp, ADR-1153 actual-date rules) that bulk_create bypasses by skipping the
451
+ TaskSerializer.
452
+ """
453
+ from trueppm_api.apps.projects.models import DeliveryMode, Task, TaskStatus
454
+
455
+ # All-or-none gate (ADR-0093 Q3): the parser already enforces it, so
456
+ # if any one of the three fields is set we know the other two are too.
457
+ # Skip three-point write on summaries (Q5) and milestones (already
458
+ # nulled by the parser, but kept defensive here).
459
+ if is_summary or td.is_milestone:
460
+ opt = ml = pess = None
461
+ est_status = None
462
+ else:
463
+ opt = td.optimistic_duration_days
464
+ ml = td.most_likely_duration_days
465
+ pess = td.pessimistic_duration_days
466
+ # estimate_status="accepted" on import (ADR-0093 Q4): the uploader
467
+ # holds project-admin permission and the values are PM-authored
468
+ # migration data, not contributor suggestions. Setting "pending"
469
+ # would force re-approval per task under SUGGEST_APPROVE for no
470
+ # governance benefit (the PM chose to import them).
471
+ est_status = "accepted" if ml is not None else None
472
+ if ml is not None:
473
+ summary["tasks_with_three_point_estimates"] += 1
474
+ # #1768: carry the source status onto Task.status so completed / in-flight
475
+ # work does not re-import as NOT_STARTED (which the scheduler treats as
476
+ # unstarted future work, inflating every forecast). Both importers populate
477
+ # td.status upstream — the Jira parser from the issue status name, the MS
478
+ # Project parser from the raw PercentComplete gated on start — so this path
479
+ # is agnostic to the percent storage scale (#1759). Fall back to the model
480
+ # default when the source supplied no usable status.
481
+ task_status = td.status or TaskStatus.NOT_STARTED.value
482
+ # Clamp percent to 0 when no start date — preserves the progress-anchor
483
+ # invariant that bulk_create bypasses (ADR-0057 Q5). A .mpp file can encode
484
+ # PercentComplete > 0 on an *unstarted* task; importing that value would
485
+ # create a task with ghost progress and no schedule anchor. A task whose
486
+ # status is terminal (REVIEW/COMPLETE) is not "unstarted" — its progress is
487
+ # real completion, not ghost — so it keeps its percent even with no start
488
+ # (the Jira path, which carries no dates), otherwise a COMPLETE issue would
489
+ # persist at 0% and read as unstarted future work (#1768).
490
+ _terminal = task_status in (TaskStatus.REVIEW.value, TaskStatus.COMPLETE.value)
491
+ # "Has an anchor" is `_task_anchor(td)`, not `td.start`. Those were the same
492
+ # condition until #2891: `planned_start` came only from `<Start>`, so a truthy
493
+ # `td.start` was exactly "this task has a schedule anchor". Two new sources
494
+ # broke that equivalence — `planned_start` can now come from a
495
+ # `<ConstraintDate>`, and `actual_start` is imported — and an actual start is
496
+ # the *strongest* anchor there is. Reading `td.start` here would force a real
497
+ # in-progress task to 0% whenever the file pinned it by constraint instead of
498
+ # by `<Start>`, which is the inverse of the invariant this clamp exists for.
499
+ effective_percent = td.percent_complete if (_task_anchor(td) or _terminal) else 0
500
+ actual_start, actual_finish = _sanitize_actual_dates(td, task_status, project, summary)
501
+ return Task(
502
+ project_id=project_id,
503
+ name=td.name,
504
+ wbs_path=wbs_path if wbs_path else None,
505
+ # Milestone invariant (#1773): bulk_create bypasses the TaskSerializer
506
+ # coupling, so enforce the canonical milestone state here — a milestone
507
+ # is zero-duration and carries delivery_mode='milestone' so the phase
508
+ # rollup weights it at 0. A source .mpp can encode a non-zero duration
509
+ # on a <Milestone> task; importing it raw would violate the invariant
510
+ # and the next PATCH would silently re-zero the duration and shift dates.
511
+ duration=0 if td.is_milestone else td.duration_days,
512
+ is_milestone=td.is_milestone,
513
+ delivery_mode=DeliveryMode.MILESTONE if td.is_milestone else DeliveryMode.WATERFALL,
514
+ status=task_status,
515
+ percent_complete=effective_percent,
516
+ notes=td.notes,
517
+ planned_start=_resolve_planned_start(td),
518
+ actual_start=actual_start,
519
+ actual_finish=actual_finish,
520
+ optimistic_duration=opt,
521
+ most_likely_duration=ml,
522
+ pessimistic_duration=pess,
523
+ estimate_status=est_status,
524
+ short_id=f"{short_id_seq:08X}",
525
+ )
526
+
527
+
528
+ def _sanitize_actual_dates(
529
+ td: TaskData, task_status: str, project: Any, summary: dict[str, Any]
530
+ ) -> tuple[Any, Any]:
531
+ """Enforce ADR-1153 on an imported row instead of persisting whatever the file said (#3709).
532
+
533
+ This is untrusted input: an uploaded .mpp can encode an ``ActualFinish``
534
+ before its ``ActualStart``, or an actual date far outside any representable
535
+ span. ``bulk_create_tasks`` bypasses ``TaskSerializer`` entirely (per
536
+ ADR-0011, to skip django-simple-history on a bulk insert), so nothing else
537
+ on this path enforces the rule — the row would otherwise persist and detonate
538
+ the *project's* recompute at compute time (``models.py``'s
539
+ ``MAX_PROJECT_SPAN_DAYS`` comment: "then every recalculate_schedule throws"),
540
+ not just fail its own import.
541
+
542
+ Mirrors the importer's existing partial-three-point precedent
543
+ (``tasks_skipped_partial_three_point``): drop only the offending field and
544
+ keep the row, rather than rejecting the whole task. A row disappearing from
545
+ the imported plan because one date was unparseable would be worse than
546
+ importing it with that one field left unset — the same "half-populated is
547
+ valid by design" policy ADR-0136/ADR-1153 already apply to a PM's own PATCH.
548
+
549
+ At most two passes: each pass clears exactly one of the two fields, and
550
+ :func:`check_actual_dates` returns only the first violation it finds, so a
551
+ file with both fields independently bad (e.g. two out-of-span dates) needs a
552
+ second pass to catch the one the first pass didn't reach.
553
+
554
+ ``td.actual_start``/``td.actual_finish`` are already ``None`` or a
555
+ parser-validated ``YYYY-MM-DD`` string by the time they reach here
556
+ (``parser._iso_date_or_none`` filters MS Project's "NA"/malformed sentinels
557
+ before this function ever runs), so ``fromisoformat`` cannot raise on the
558
+ live MSP path today. The ``try/except`` is defense-in-depth against a future
559
+ ``TaskData`` producer that populates these fields without that guarantee —
560
+ the CSV and Jira adapters currently leave them ``None`` — so an unparseable
561
+ value is dropped exactly like any other invalid actual, not a crash that
562
+ takes down the whole import (security-review, #3709).
563
+ """
564
+ from datetime import date as _date
565
+
566
+ from trueppm_api.apps.projects.actual_date_rules import check_actual_dates
567
+
568
+ def _parse(raw: str | None) -> Any:
569
+ if not raw:
570
+ return None
571
+ try:
572
+ return _date.fromisoformat(raw)
573
+ except ValueError:
574
+ return None
575
+
576
+ start = _parse(td.actual_start)
577
+ finish = _parse(td.actual_finish)
578
+ for _ in range(2):
579
+ violation = check_actual_dates(
580
+ actual_start=start, actual_finish=finish, status=task_status, project=project
581
+ )
582
+ if violation is None:
583
+ break
584
+ summary["tasks_with_invalid_actuals_dropped"] += 1
585
+ summary["warnings"].append(
586
+ f"{td.name!r}: dropped {violation.field.replace('_', ' ')} — {violation.message}"
587
+ )
588
+ if violation.field == "actual_start":
589
+ start = None
590
+ else:
591
+ finish = None
592
+ return start, finish
593
+
594
+
595
+ def _resolve_planned_start(td: TaskData) -> str | None:
596
+ """Choose the SNET floor for an imported task: its constraint date, else Start.
597
+
598
+ ``Task.planned_start`` is documented as *start no earlier than*, applied by
599
+ the CPM forward pass — which is exactly what an MSPDI
600
+ ``<ConstraintType>4</ConstraintType>`` plus ``<ConstraintDate>`` means. So
601
+ when the file carries one of the codes TruePPM can express, the **constraint
602
+ date wins over ``<Start>``** (#2891).
603
+
604
+ That precedence is the point of the fix rather than a detail. ``<Start>`` is
605
+ MS Project's *computed* schedule date; the constraint date is the commitment
606
+ the PM made. Reading only ``<Start>`` meant a task whose commitment differed
607
+ from its current computed start silently lost the commitment, and TruePPM then
608
+ rescheduled from CPM alone — producing dates the PM could not defend at the
609
+ next governance review.
610
+
611
+ In the overwhelmingly common case the two agree (MS Project sets SNET
612
+ automatically when a start is typed in), so this changes nothing; it matters
613
+ precisely when they diverge, which is when the commitment is load-bearing.
614
+ """
615
+ if td.constraint_type in CONSTRAINT_TYPES_APPLIED_AS_SNET and td.constraint_date is not None:
616
+ return td.constraint_date
617
+ return td.start or None
618
+
619
+
620
+ def _task_anchor(td: TaskData) -> str | None:
621
+ """The earliest date that anchors this task in the schedule, or ``None``.
622
+
623
+ ``_resolve_planned_start`` is the CPM floor; an ``actual_start`` is a *harder*
624
+ anchor than any planned date, and it can predate the floor on a task that
625
+ started early. Every "does this task sit somewhere on the calendar?" question
626
+ goes through here so the answer cannot drift between call sites again — which
627
+ is exactly what happened when `planned_start` gained a second source and two
628
+ of the three `td.start` reads were left behind.
629
+ """
630
+ candidates = [d for d in (_resolve_planned_start(td), td.actual_start) if d]
631
+ return min(candidates) if candidates else None
632
+
633
+
634
+ def _maybe_shift_project_start(project_id: str, data: ProjectData, summary: dict[str, Any]) -> None:
635
+ """Pull the project start back to the earliest imported task anchor (#873/#867).
636
+
637
+ The importer bulk_creates tasks, bypassing the TaskSerializer auto-shift, so a
638
+ .mpp whose tasks predate the project start would otherwise persist sub-start
639
+ "ghost" planned_starts the CPM clamps. Mirrors the interactive path: the
640
+ project boundary is elastic earlier. Dates are ISO strings (which sort
641
+ chronologically), parsed to a date for the comparison/assignment.
642
+
643
+ Reads ``_task_anchor``, **not** ``td.start`` (#2891). Reading ``<Start>`` here
644
+ reopened the very bug this branch fixes: MS Project keeps a ``<ConstraintDate>``
645
+ that predates the project start while computing ``<Start>`` *at* the project
646
+ start, so `ConstraintDate < StartDate <= Start` is the canonical shape. Keyed
647
+ on ``<Start>``, no shift fires, the constraint-derived ``planned_start`` lands
648
+ below the project floor, and ``early_start = max(project_start, planned_start,
649
+ …)`` clamps it away — the commitment dropped silently a second time, one
650
+ function downstream of the fix. ``actual_start`` is included for the same
651
+ reason: work that demonstrably began before the project start still has to fit
652
+ inside the project's window.
653
+ """
654
+ from datetime import date as _date
655
+
656
+ from trueppm_api.apps.projects.models import Project
657
+ from trueppm_api.apps.projects.services import shift_project_start_if_needed
658
+
659
+ task_start_strs = [anchor for td in data.tasks if (anchor := _task_anchor(td))]
660
+ if not task_start_strs:
661
+ return
662
+ earliest_start = _date.fromisoformat(min(task_start_strs))
663
+ project = Project.objects.filter(pk=project_id).first()
664
+ if project is not None and shift_project_start_if_needed(project, earliest_start):
665
+ summary["project_start_date"] = project.start_date.isoformat()
666
+ summary["project_start_shifted"] = True
667
+
668
+
669
+ def collect_dependency_edges(data: ProjectData) -> list[tuple[str, str]]:
670
+ """Return every predecessor link in the file's own uid space.
671
+
672
+ Exists so a caller can run the ADR-0259 graph guard *before* any row is
673
+ written. Cycle and self-reference structure is invariant under relabeling,
674
+ so validating uids rather than the PKs they become is what lets a cyclic
675
+ file be rejected while the project is still empty — ``bulk_create`` bypasses
676
+ ``DependencySerializer._check_no_cycle``, so there is no later gate.
677
+
678
+ Links whose predecessor uid is absent from the file are included: a dangling
679
+ uid can only ever be a source node (successors are always file tasks), so it
680
+ cannot manufacture a cycle, and keeping them makes this edge set identical
681
+ in shape to the one the CSV importer validates.
682
+ """
683
+ return [
684
+ (str(link.predecessor_uid), str(td.uid))
685
+ for td in data.tasks
686
+ for link in td.predecessor_links
687
+ ]
688
+
689
+
690
+ def describe_bad_graph(data: ProjectData, exc: InfeasibleGraphError) -> str:
691
+ """Name the offending tasks of a rejected graph in the uploader's terms.
692
+
693
+ Built from ``exc.reason``/``exc.offending`` rather than ``str(exc)``, whose
694
+ message is a domain signal carrying an internal reason code and a list repr
695
+ in uid space ("Infeasible task graph (cyclic_dependency): ['2', '5', '2']").
696
+ The person who uploaded the file knows task names, not uids, and needs the
697
+ two reasons distinguished — a self-referencing task is a different edit from
698
+ a multi-task loop.
699
+ """
700
+ names = {str(td.uid): td.name for td in data.tasks}
701
+ labelled = [f"{names.get(uid, 'unknown task')} (uid {uid})" for uid in exc.offending]
702
+ if exc.reason == "self_reference":
703
+ return f"Task {labelled[0]} lists itself as its own predecessor."
704
+ return "Circular dependency: " + " -> ".join(labelled)
705
+
706
+
707
+ def _import_dependencies(
708
+ data: ProjectData,
709
+ task_uid_to_pk: dict[int, str],
710
+ summary: dict[str, Any],
711
+ *,
712
+ project_id: str,
713
+ batch_size: int,
714
+ ) -> None:
715
+ """Create Dependency rows from parsed predecessor links.
716
+
717
+ Warns (one entry per link) on any predecessor UID absent from the import.
718
+ """
719
+ from trueppm_api.apps.projects.models import Dependency
720
+
721
+ dep_objects: list[Dependency] = []
722
+ for td in data.tasks:
723
+ successor_pk = task_uid_to_pk.get(td.uid)
724
+ if not successor_pk:
725
+ continue
726
+ for pl in td.predecessor_links:
727
+ predecessor_pk = task_uid_to_pk.get(pl.predecessor_uid)
728
+ if not predecessor_pk:
729
+ summary["warnings"].append(
730
+ f"Task '{td.name}': predecessor UID {pl.predecessor_uid} "
731
+ f"not found, dependency skipped"
732
+ )
733
+ continue
734
+ dep_objects.append(
735
+ Dependency(
736
+ predecessor_id=predecessor_pk,
737
+ successor_id=successor_pk,
738
+ dep_type=pl.dep_type,
739
+ lag=pl.lag_days,
740
+ )
741
+ )
742
+
743
+ if dep_objects:
744
+ # `dependencies` is its own delta collection filtered on the row's own
745
+ # cursor, and bulk_create bypasses save() — so without this the imported
746
+ # graph would sit at sync_seq=0 and never reach an offline client, leaving
747
+ # it a task list with no edges to recompute from (ADR-0686).
748
+ seq = allocate_for_projects([project_id])
749
+ for dep in dep_objects:
750
+ dep.sync_seq = seq
751
+ Dependency.objects.bulk_create(dep_objects, ignore_conflicts=True, batch_size=batch_size)
752
+ summary["dependencies_created"] = len(dep_objects)
753
+
754
+
755
+ def _import_assignments(
756
+ project_id: str,
757
+ data: ProjectData,
758
+ task_uid_to_pk: dict[int, str],
759
+ resource_uid_to_pk: dict[int, str],
760
+ summary: dict[str, Any],
761
+ *,
762
+ batch_size: int,
763
+ ) -> None:
764
+ """Create TaskResource assignments, then auto-roster each assigned resource."""
765
+ from trueppm_api.apps.resources.models import TaskResource
766
+
767
+ assignment_objects: list[TaskResource] = []
768
+ for td in data.tasks:
769
+ task_pk = task_uid_to_pk.get(td.uid)
770
+ if not task_pk:
771
+ continue
772
+ for ad in td.resource_assignments:
773
+ resource_pk = resource_uid_to_pk.get(ad.resource_uid)
774
+ if not resource_pk:
775
+ continue
776
+ assignment_objects.append(
777
+ TaskResource(task_id=task_pk, resource_id=resource_pk, units=ad.units)
778
+ )
779
+
780
+ if not assignment_objects:
781
+ return
782
+
783
+ TaskResource.objects.bulk_create(
784
+ assignment_objects, ignore_conflicts=True, batch_size=batch_size
785
+ )
786
+ summary["assignments_created"] = len(assignment_objects)
787
+ _roster_assigned_resources(project_id, assignment_objects, batch_size=batch_size)
788
+
789
+
790
+ def _roster_assigned_resources(
791
+ project_id: str, assignment_objects: list[Any], *, batch_size: int
792
+ ) -> None:
793
+ """Roster every assigned resource onto the project so they appear in Team →
794
+ Roster / Heatmap / Allocation views after import (#241).
795
+
796
+ Bulk-builds the rows because bulk_create skipped the per-row signal path used
797
+ by the API ViewSet.
798
+
799
+ ``server_version=1`` is set explicitly because ``bulk_create`` does not call
800
+ ``Model.save()``, and ``ProjectResource`` is a ``VersionedModel`` whose INSERT path
801
+ is the only thing that sets that counter (#3575). A row left at the field default of
802
+ 0 publishes an optimistic-lock token no client can have echoed back.
803
+ """
804
+ from trueppm_api.apps.resources.models import ProjectResource
805
+
806
+ rostered_resource_pks = {a.resource_id for a in assignment_objects}
807
+ existing_pairs = set(
808
+ ProjectResource.objects.filter(
809
+ project_id=project_id,
810
+ resource_id__in=rostered_resource_pks,
811
+ ).values_list("resource_id", flat=True)
812
+ )
813
+ new_roster = [
814
+ ProjectResource(project_id=project_id, resource_id=pk, server_version=1)
815
+ for pk in rostered_resource_pks
816
+ if pk not in existing_pairs
817
+ ]
818
+ if new_roster:
819
+ ProjectResource.objects.bulk_create(
820
+ new_roster, ignore_conflicts=True, batch_size=batch_size
821
+ )
822
+
823
+
824
+ def _apply_calendar(
825
+ project_id: str,
826
+ data: ProjectData,
827
+ summary: dict[str, Any],
828
+ *,
829
+ overwrite: bool,
830
+ batch_size: int,
831
+ ) -> None:
832
+ """Wire the file's project calendar onto ``Project.calendar`` (#1769).
833
+
834
+ Resolution: the project-level ``<CalendarUID>`` picks the calendar; when the
835
+ header omits it and the file has exactly one base calendar, that calendar is
836
+ unambiguous and used. The row is reused from the shared calendar library
837
+ only on an exact semantic match (see ``_match_or_create_calendar``),
838
+ otherwise created together with its ``CalendarException`` rows.
839
+
840
+ Overwrite policy mirrors ``_apply_header_to_project``: create-from-import
841
+ (``overwrite=True``) treats the file as authoritative; import-into-existing
842
+ keeps a PM-configured project calendar and only warns when the file's
843
+ calendar differs semantically (silently "conflicting" on the ubiquitous
844
+ vanilla Standard calendar would be pure noise).
845
+
846
+ TruePPM has no per-task calendars, so task-level ``CalendarUID`` references
847
+ to any *other* calendar degrade to one aggregated warning per calendar.
848
+ """
849
+ from trueppm_api.apps.projects.models import Project
850
+
851
+ if not data.calendars:
852
+ return
853
+
854
+ by_uid = {c.uid: c for c in data.calendars}
855
+ cal_data = _resolve_project_calendar_data(data, by_uid, summary)
856
+
857
+ _warn_unhonored_task_calendars(data, cal_data, by_uid, summary)
858
+
859
+ if cal_data is None:
860
+ return
861
+
862
+ project = (
863
+ Project.objects.select_related("calendar")
864
+ .prefetch_related("calendar__exceptions")
865
+ .get(pk=project_id)
866
+ )
867
+ if project.calendar_id is not None and not overwrite:
868
+ if project.calendar is not None and not _calendar_matches(project.calendar, cal_data):
869
+ summary["warnings"].append(
870
+ f"File calendar '{cal_data.name}' differs from the project's "
871
+ f"configured calendar '{project.calendar.name}'; the project "
872
+ f"calendar was kept — imported dates may shift"
873
+ )
874
+ return
875
+
876
+ calendar = _match_or_create_calendar(cal_data, summary, batch_size=batch_size)
877
+ if project.calendar_id != calendar.pk:
878
+ project.calendar = calendar
879
+ # save() rather than .update() so server_version bumps and sync clients
880
+ # observe the calendar change — same pattern as
881
+ # shift_project_start_if_needed on the start-date pull-back.
882
+ project.save(update_fields=["calendar"])
883
+ summary["calendar_applied"] = True
884
+ summary["calendar_name"] = calendar.name
885
+
886
+
887
+ def _resolve_project_calendar_data(
888
+ data: ProjectData, by_uid: dict[int, CalendarData], summary: dict[str, Any]
889
+ ) -> CalendarData | None:
890
+ """Pick the file's project calendar: the explicit ``CalendarUID`` if present,
891
+ else the sole base calendar when unambiguous. Warns and returns None when the
892
+ referenced UID is missing, or when the header omits it and several calendars exist.
893
+ """
894
+ cal_data = by_uid.get(data.calendar_uid) if data.calendar_uid is not None else None
895
+ if cal_data is not None:
896
+ return cal_data
897
+ if data.calendar_uid is not None:
898
+ summary["warnings"].append(
899
+ f"Project calendar UID {data.calendar_uid} not found among the "
900
+ f"file's base calendars; project calendar left unchanged"
901
+ )
902
+ elif len(data.calendars) == 1:
903
+ cal_data = data.calendars[0]
904
+ else:
905
+ summary["warnings"].append(
906
+ f"File defines {len(data.calendars)} base calendars but no "
907
+ f"project CalendarUID; project calendar left unchanged"
908
+ )
909
+ return cal_data
910
+
911
+
912
+ def _warn_unhonored_task_calendars(
913
+ data: ProjectData,
914
+ cal_data: CalendarData | None,
915
+ by_uid: dict[int, CalendarData],
916
+ summary: dict[str, Any],
917
+ ) -> None:
918
+ """Emit one aggregated warning per distinct task-level calendar TruePPM cannot
919
+ honor (it has no per-task calendars) — never one warning per task, so a
920
+ 500-task plan on a shared task calendar produces a single line, not 500.
921
+ """
922
+ project_cal_uid = cal_data.uid if cal_data is not None else data.calendar_uid
923
+ task_cal_counts: dict[int, int] = {}
924
+ for td in data.tasks:
925
+ if td.calendar_uid is not None and td.calendar_uid != project_cal_uid:
926
+ task_cal_counts[td.calendar_uid] = task_cal_counts.get(td.calendar_uid, 0) + 1
927
+ for uid, count in sorted(task_cal_counts.items()):
928
+ ref = by_uid.get(uid)
929
+ label = f"'{ref.name}' (UID {uid})" if ref is not None else f"UID {uid}"
930
+ summary["warnings"].append(
931
+ f"{count} task(s) reference calendar {label}; TruePPM has no "
932
+ f"per-task calendars — they are scheduled on the project calendar"
933
+ )
934
+
935
+
936
+ def _match_or_create_calendar(
937
+ cal_data: CalendarData, summary: dict[str, Any], *, batch_size: int
938
+ ) -> Any:
939
+ """Return a library ``Calendar`` row for the parsed calendar, reusing on exact match.
940
+
941
+ Reuse requires the same (case-insensitive) name AND identical semantics —
942
+ mask, hours per day, and exception ranges. A name-only match (every MS
943
+ Project file ships a "Standard") could silently attach another team's
944
+ holidays to this project; a semantics-only match under a different name
945
+ would confuse the calendar library UI. Repeated imports of the same file
946
+ therefore converge on one row instead of accreting duplicates.
947
+ """
948
+ from datetime import date as _date
949
+
950
+ from trueppm_api.apps.projects.models import Calendar, CalendarException
951
+
952
+ candidates = Calendar.objects.filter(
953
+ name__iexact=cal_data.name, is_deleted=False
954
+ ).prefetch_related("exceptions")
955
+ for candidate in candidates:
956
+ if _calendar_matches(candidate, cal_data):
957
+ summary["calendar_matched"] = True
958
+ return candidate
959
+
960
+ calendar = Calendar.objects.create(
961
+ name=cal_data.name[:255],
962
+ working_days=cal_data.working_days,
963
+ hours_per_day=cal_data.hours_per_day,
964
+ )
965
+ if cal_data.exceptions:
966
+ CalendarException.objects.bulk_create(
967
+ [
968
+ CalendarException(
969
+ calendar=calendar,
970
+ exc_start=_date.fromisoformat(exc.start),
971
+ exc_end=_date.fromisoformat(exc.end),
972
+ description=exc.name[:255],
973
+ )
974
+ for exc in cal_data.exceptions
975
+ ],
976
+ batch_size=batch_size,
977
+ )
978
+ summary["calendar_created"] = True
979
+ return calendar
980
+
981
+
982
+ def _calendar_matches(calendar: Any, cal_data: CalendarData) -> bool:
983
+ """True when a ``Calendar`` row is semantically identical to parsed data.
984
+
985
+ Compares the CPM inputs only — mask, hours per day, and exception date
986
+ ranges. Exception descriptions and timezone are metadata that cannot shift
987
+ a schedule, so they don't block reuse.
988
+ """
989
+ if calendar.working_days != cal_data.working_days:
990
+ return False
991
+ if abs(calendar.hours_per_day - cal_data.hours_per_day) > 1e-6:
992
+ return False
993
+ existing = {
994
+ (exc.exc_start.isoformat(), exc.exc_end.isoformat()) for exc in calendar.exceptions.all()
995
+ }
996
+ parsed = {(exc.start, exc.end) for exc in cal_data.exceptions}
997
+ return existing == parsed
998
+
999
+
1000
+ def _build_wbs_paths(tasks: list[TaskData]) -> list[str]:
1001
+ """Compute an ltree ``wbs_path`` per task, preserving the WBS hierarchy.
1002
+
1003
+ MS Project encodes hierarchy two ways: the dotted ``OutlineNumber``
1004
+ ("1", "1.1", "1.2") and the integer ``OutlineLevel`` (indent depth).
1005
+ Genuine MS Project exports keep both consistent, but many third-party and
1006
+ generated MSPDI files write a *flat* ``OutlineNumber`` (1, 2, 3, …) and
1007
+ carry the hierarchy only in ``OutlineLevel`` (#794). Trusting the outline
1008
+ number alone flattens those files — every phase imports as a sibling of its
1009
+ own sub-tasks. So:
1010
+
1011
+ - if any task has a dotted ``OutlineNumber`` the file uses the
1012
+ hierarchical form: map each number straight to an ltree path (the
1013
+ original, well-formed-file behavior — unchanged);
1014
+ - otherwise, when ``OutlineLevel`` indicates nesting, reconstruct the
1015
+ hierarchy from the level sequence (tasks arrive in document order).
1016
+ """
1017
+ has_dotted = any("." in (t.outline_number or "") for t in tasks)
1018
+ if has_dotted:
1019
+ return [_outline_number_to_ltree(t.outline_number) for t in tasks]
1020
+
1021
+ levels = [t.outline_level for t in tasks]
1022
+ if len(set(levels)) <= 1:
1023
+ # Genuinely flat project (or no level info): keep the outline number,
1024
+ # which is itself flat (1, 2, 3, …) for these files.
1025
+ return [_outline_number_to_ltree(t.outline_number) for t in tasks]
1026
+
1027
+ return _wbs_paths_from_levels(levels)
1028
+
1029
+
1030
+ def _wbs_paths_from_levels(levels: list[int]) -> list[str]:
1031
+ """Reconstruct dotted ltree paths from a document-ordered OutlineLevel list.
1032
+
1033
+ Walks tasks in document order maintaining a sibling counter per depth. A
1034
+ deeper level opens a child ("1" -> "1.1"); the same or a shallower level
1035
+ advances the sibling counter at that depth ("1.1" -> "1.2", "1.4" -> "2").
1036
+ Levels are normalized so the shallowest observed level becomes depth 1, so
1037
+ files whose top-level tasks start at OutlineLevel 1 (or any value) behave
1038
+ identically.
1039
+ """
1040
+ base = min(levels)
1041
+ counters: list[int] = []
1042
+ paths: list[str] = []
1043
+ for level in levels:
1044
+ depth = max(level - base + 1, 1)
1045
+ if depth <= len(counters):
1046
+ # Same or shallower: drop deeper ancestors, bump this depth's sibling.
1047
+ counters = counters[:depth]
1048
+ counters[depth - 1] += 1
1049
+ else:
1050
+ # Deeper: open child levels (pads with 1s if a level is skipped).
1051
+ while len(counters) < depth:
1052
+ counters.append(1)
1053
+ paths.append(".".join(str(c) for c in counters))
1054
+ return paths
1055
+
1056
+
1057
+ def _summary_indices(wbs_paths: list[str]) -> set[int]:
1058
+ """Return the indices of summary tasks (have at least one descendant).
1059
+
1060
+ A task is a summary when some other task's ``wbs_path`` is strictly
1061
+ descended from this one's (e.g. ``"1"`` is a summary if any task has
1062
+ path ``"1.1"`` or deeper). Implemented in O(N) by collecting all strict
1063
+ ancestor paths in one pass, then marking every task whose own path is in
1064
+ that set. Used by the importer to skip three-point field writes on
1065
+ summary rows (ADR-0093 Q5).
1066
+ """
1067
+ ancestor_paths: set[str] = set()
1068
+ for path in wbs_paths:
1069
+ if not path:
1070
+ continue
1071
+ parts = path.split(".")
1072
+ # Each strict prefix of a leaf is an ancestor (excluding the path
1073
+ # itself, since a task with no descendants is a leaf).
1074
+ for end in range(1, len(parts)):
1075
+ ancestor_paths.add(".".join(parts[:end]))
1076
+ return {i for i, p in enumerate(wbs_paths) if p and p in ancestor_paths}
1077
+
1078
+
1079
+ def _outline_number_to_ltree(outline_number: str) -> str:
1080
+ """Convert MS Project OutlineNumber (e.g. '1.2.3') to ltree path.
1081
+
1082
+ MS Project OutlineNumber is already dot-separated and hierarchical,
1083
+ which maps directly to the ltree format used by wbs_path.
1084
+ """
1085
+ if not outline_number:
1086
+ return ""
1087
+ parts = outline_number.split(".")
1088
+ cleaned: list[str] = []
1089
+ for p in parts:
1090
+ p = p.strip()
1091
+ if p.isdigit():
1092
+ cleaned.append(p)
1093
+ else:
1094
+ return ""
1095
+ return ".".join(cleaned)