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.
- trueppm_api/__init__.py +14 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +164 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +2837 -0
- trueppm_api/apps/access/serializers.py +946 -0
- trueppm_api/apps/access/services.py +667 -0
- trueppm_api/apps/access/signals.py +103 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +95 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1931 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +108 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +418 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +177 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +837 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +403 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +622 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +519 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +731 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1106 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +277 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +500 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1231 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +436 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +447 -0
- trueppm_api/apps/notifications/email_backend.py +330 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1197 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +961 -0
- trueppm_api/apps/notifications/services.py +1451 -0
- trueppm_api/apps/notifications/tasks.py +711 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +904 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +551 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +969 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +523 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +466 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +449 -0
- trueppm_api/apps/projects/backfill.py +287 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +204 -0
- trueppm_api/apps/projects/config_notice.py +1325 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +93 -0
- trueppm_api/apps/projects/decisions_views.py +94 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +353 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +8946 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +575 -0
- trueppm_api/apps/projects/program_schedule.py +745 -0
- trueppm_api/apps/projects/program_views.py +3220 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/exporter.py +1644 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2390 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +126 -0
- trueppm_api/apps/projects/seed/replay.py +1691 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1149 -0
- trueppm_api/apps/projects/serializers.py +12310 -0
- trueppm_api/apps/projects/services.py +6966 -0
- trueppm_api/apps/projects/share_serializers.py +132 -0
- trueppm_api/apps/projects/share_services.py +325 -0
- trueppm_api/apps/projects/share_views.py +374 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +909 -0
- trueppm_api/apps/projects/signal_privacy_views.py +476 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +266 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1152 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1917 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +258 -0
- trueppm_api/apps/projects/template_views.py +799 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1011 -0
- trueppm_api/apps/projects/utilization.py +885 -0
- trueppm_api/apps/projects/views.py +20572 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1749 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +119 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +516 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +283 -0
- trueppm_api/apps/scheduling/serializers.py +564 -0
- trueppm_api/apps/scheduling/services.py +1182 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +1984 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2564 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +437 -0
- trueppm_api/apps/sso/services.py +1385 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +320 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +471 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +225 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +371 -0
- trueppm_api/apps/sync/serializers.py +687 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +501 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +930 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +434 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +117 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +526 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +144 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +372 -0
- trueppm_api/apps/webhooks/serializers.py +318 -0
- trueppm_api/apps/webhooks/tasks.py +663 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +383 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +368 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +242 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +919 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +555 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1502 -0
- trueppm_api/asgi.py +42 -0
- trueppm_api/celery.py +31 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +916 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +949 -0
- trueppm_api/core/password_reset.py +500 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +884 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +344 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2455 -0
- trueppm_api/settings/dev.py +273 -0
- trueppm_api/settings/prod.py +228 -0
- trueppm_api/urls.py +153 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
- trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
- trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
"""REST API views for CSV / Excel import (#743, ADR-0632)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.db import transaction
|
|
13
|
+
from django.http import HttpResponse
|
|
14
|
+
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
|
15
|
+
from rest_framework import serializers, status
|
|
16
|
+
from rest_framework.exceptions import PermissionDenied, ValidationError
|
|
17
|
+
from rest_framework.parsers import MultiPartParser
|
|
18
|
+
from rest_framework.permissions import IsAuthenticated
|
|
19
|
+
from rest_framework.request import Request
|
|
20
|
+
from rest_framework.response import Response
|
|
21
|
+
from rest_framework.views import APIView
|
|
22
|
+
|
|
23
|
+
from trueppm_api.apps.access.models import ProjectMembership, Role
|
|
24
|
+
from trueppm_api.apps.access.permissions import (
|
|
25
|
+
IsProjectNotArchived,
|
|
26
|
+
IsProjectScheduler,
|
|
27
|
+
can_user_undo_batch_operation,
|
|
28
|
+
role_can_undo_batch_operation,
|
|
29
|
+
)
|
|
30
|
+
from trueppm_api.apps.csvimport.mapping import field_choices
|
|
31
|
+
from trueppm_api.apps.csvimport.parser import (
|
|
32
|
+
DATE_ORDERS,
|
|
33
|
+
REVIEW_BRANCH_NAME,
|
|
34
|
+
SUPPORTED_EXTENSIONS,
|
|
35
|
+
CsvImportError,
|
|
36
|
+
parse_spreadsheet,
|
|
37
|
+
)
|
|
38
|
+
from trueppm_api.apps.csvimport.template import CSV_TEMPLATE, TEMPLATE_FILENAME
|
|
39
|
+
from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# Allow-list (mirrors the hardened MS Project sanitizer, #816): substitute
|
|
44
|
+
# anything outside a conservative safe set so an attacker-controlled
|
|
45
|
+
# UploadedFile.name can never carry an HTML/Content-Disposition-injection
|
|
46
|
+
# payload into the provenance surface. os.path.basename blocks path traversal.
|
|
47
|
+
_FILENAME_ALLOWED = re.compile(r"[^A-Za-z0-9._\- ()]")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _sanitize_filename(raw: str) -> str:
|
|
51
|
+
name = os.path.basename(raw or "")
|
|
52
|
+
name = _FILENAME_ALLOWED.sub("_", name)
|
|
53
|
+
name = re.sub(r"\s+", " ", name).strip()
|
|
54
|
+
return name[:255] or "upload.csv"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _require_project_scheduler(user: object, project_pk: str) -> None:
|
|
58
|
+
"""Authoritative in-body role check (defense-in-depth behind IsProjectScheduler).
|
|
59
|
+
|
|
60
|
+
Import creates tasks, dependencies and resources wholesale, so it sits at the
|
|
61
|
+
Scheduler line rather than Member: Owner / Admin / Scheduler may import,
|
|
62
|
+
Member and Viewer may not.
|
|
63
|
+
"""
|
|
64
|
+
role = (
|
|
65
|
+
ProjectMembership.objects.filter(
|
|
66
|
+
project_id=project_pk,
|
|
67
|
+
user=user, # type: ignore[misc]
|
|
68
|
+
is_deleted=False,
|
|
69
|
+
)
|
|
70
|
+
.values_list("role", flat=True)
|
|
71
|
+
.first()
|
|
72
|
+
)
|
|
73
|
+
if role is None or role < Role.SCHEDULER:
|
|
74
|
+
# "Resource Manager" is the label for ``Role.SCHEDULER``; "Scheduler" is the
|
|
75
|
+
# code name and appears on no surface a user can reach (#3503).
|
|
76
|
+
raise PermissionDenied(
|
|
77
|
+
"You need at least the Resource Manager role to import a spreadsheet."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _require_project_admin(user: object, project_pk: str) -> None:
|
|
82
|
+
"""Authoritative in-body role check for undoing an import (ADR-0810, #2756).
|
|
83
|
+
|
|
84
|
+
Admin+, not Scheduler — the same bar as undoing a template apply or a
|
|
85
|
+
cascade (``batch_operation_views._require_admin``). A Scheduler may have run
|
|
86
|
+
the import and still not clear this floor.
|
|
87
|
+
|
|
88
|
+
**Open at #3355, and not for the reason once given here.** This docstring used
|
|
89
|
+
to say the undo "removes rows other collaborators may already be building on
|
|
90
|
+
top of". ``undo_import_fix_operation`` partitions on ``server_version`` via
|
|
91
|
+
``task_batch_services._partition_touched`` and reverts only untouched rows,
|
|
92
|
+
counting the rest as ``kept`` — so it removes nothing anyone else has changed.
|
|
93
|
+
|
|
94
|
+
Defers the comparison to :func:`role_can_undo_batch_operation` rather than
|
|
95
|
+
testing the ordinal here (#3353). ``CsvImportStatusView`` reports the same rule
|
|
96
|
+
to the wizard as ``can_undo`` so it can withhold an Undo this endpoint would
|
|
97
|
+
refuse, and a second copy of the comparison is exactly how the two drift —
|
|
98
|
+
especially while the floor itself is open at #3355.
|
|
99
|
+
"""
|
|
100
|
+
role = (
|
|
101
|
+
ProjectMembership.objects.filter(
|
|
102
|
+
project_id=project_pk,
|
|
103
|
+
user=user, # type: ignore[misc]
|
|
104
|
+
is_deleted=False,
|
|
105
|
+
)
|
|
106
|
+
.values_list("role", flat=True)
|
|
107
|
+
.first()
|
|
108
|
+
)
|
|
109
|
+
if not role_can_undo_batch_operation(role):
|
|
110
|
+
raise PermissionDenied("You need at least Project Manager role to undo an import.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _read_validated_upload(request: Request) -> tuple[str, bytes] | Response:
|
|
114
|
+
"""Read the multipart ``file`` field; validate extension + size.
|
|
115
|
+
|
|
116
|
+
Returns ``(filename, raw_bytes)`` or a 400 ``Response``.
|
|
117
|
+
"""
|
|
118
|
+
uploaded_file = request.FILES.get("file")
|
|
119
|
+
if not uploaded_file:
|
|
120
|
+
return Response(
|
|
121
|
+
{"detail": "No file provided. Send a multipart form with a 'file' field."},
|
|
122
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
filename = _sanitize_filename(uploaded_file.name or "")
|
|
126
|
+
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
|
|
127
|
+
if ext not in SUPPORTED_EXTENSIONS:
|
|
128
|
+
return Response(
|
|
129
|
+
{"detail": f"Unsupported file type: .{ext}. Upload a .csv or .xlsx file."},
|
|
130
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
max_bytes = settings.CSV_IMPORT_MAX_UPLOAD_MB * 1024 * 1024
|
|
134
|
+
if uploaded_file.size and uploaded_file.size > max_bytes:
|
|
135
|
+
return Response(
|
|
136
|
+
{
|
|
137
|
+
"detail": (
|
|
138
|
+
f"File too large ({uploaded_file.size} bytes). "
|
|
139
|
+
f"Maximum: {settings.CSV_IMPORT_MAX_UPLOAD_MB} MB."
|
|
140
|
+
)
|
|
141
|
+
},
|
|
142
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return filename, uploaded_file.read()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _form_body(request: Request) -> dict[str, Any]:
|
|
149
|
+
"""Return ``request.data`` narrowed to a mapping.
|
|
150
|
+
|
|
151
|
+
Every caller sets ``parser_classes = [MultiPartParser]``, so the body is always
|
|
152
|
+
a QueryDict — a JSON list is rejected as 415 before this runs. Narrowed
|
|
153
|
+
rather than guarded: a branch that cannot be reached cannot be tested.
|
|
154
|
+
"""
|
|
155
|
+
return cast("dict[str, Any]", request.data)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _parse_column_map(request: Request) -> dict[str, str]:
|
|
159
|
+
"""Read the optional ``column_map`` multipart field as ``{header: field}``.
|
|
160
|
+
|
|
161
|
+
Accepts a JSON object, either as a real dict (test client) or a JSON string
|
|
162
|
+
(what a browser's FormData sends). A malformed value is ignored rather than
|
|
163
|
+
rejected: auto-detection is a safe fallback, and failing the whole upload
|
|
164
|
+
over a mapping hint would be a worse outcome than importing the detected
|
|
165
|
+
columns and letting the operator see the result.
|
|
166
|
+
"""
|
|
167
|
+
# Both callers set `parser_classes = [MultiPartParser]`, so the body is always
|
|
168
|
+
# a QueryDict — a JSON list is rejected as 415 before this runs. Narrowed
|
|
169
|
+
# rather than guarded: a branch that cannot be reached cannot be tested.
|
|
170
|
+
raw = _form_body(request).get("column_map")
|
|
171
|
+
if not raw:
|
|
172
|
+
return {}
|
|
173
|
+
if isinstance(raw, dict):
|
|
174
|
+
parsed: Any = raw
|
|
175
|
+
else:
|
|
176
|
+
import json
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
parsed = json.loads(raw)
|
|
180
|
+
except (TypeError, ValueError):
|
|
181
|
+
logger.info("csv import: ignoring unparseable column_map")
|
|
182
|
+
return {}
|
|
183
|
+
if not isinstance(parsed, dict):
|
|
184
|
+
return {}
|
|
185
|
+
return {str(k): str(v) for k, v in parsed.items() if isinstance(k, str)}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_date_order(request: Request) -> str:
|
|
189
|
+
"""Read the optional ``date_order`` multipart field (#2926).
|
|
190
|
+
|
|
191
|
+
Rejected rather than ignored, unlike ``column_map``: a bad mapping hint
|
|
192
|
+
degrades to auto-detection and the operator sees the result, but a
|
|
193
|
+
misspelled date order that silently fell back to ``auto`` would import
|
|
194
|
+
03/04 as March 4 while the caller believed they had asserted day-first —
|
|
195
|
+
which is precisely the silent-corruption failure this endpoint exists to
|
|
196
|
+
close. A scripted caller must hear that its parameter did nothing.
|
|
197
|
+
|
|
198
|
+
Non-string input is caught here too: ``request.data`` is attacker-shaped,
|
|
199
|
+
and a JSON list reaching a membership test against strings is the #2795
|
|
200
|
+
container-type-confusion class.
|
|
201
|
+
"""
|
|
202
|
+
# Both callers set `parser_classes = [MultiPartParser]`, so the body is always
|
|
203
|
+
# a QueryDict — a JSON list is rejected as 415 before this runs. Narrowed
|
|
204
|
+
# rather than guarded: a branch that cannot be reached cannot be tested.
|
|
205
|
+
raw = _form_body(request).get("date_order")
|
|
206
|
+
if raw is None or raw == "":
|
|
207
|
+
return "auto"
|
|
208
|
+
if not isinstance(raw, str) or raw not in DATE_ORDERS:
|
|
209
|
+
raise ValidationError({"date_order": f"Must be one of {', '.join(DATE_ORDERS)}."})
|
|
210
|
+
return raw
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
#: Multipart request schema shared by preview and commit.
|
|
214
|
+
#:
|
|
215
|
+
#: Declared rather than left as bare ``BINARY`` so ``date_order`` is visible to
|
|
216
|
+
#: SDK codegen — a parameter a caller cannot discover from the schema is a
|
|
217
|
+
#: parameter that does not exist for them (the #2942 class, one endpoint over).
|
|
218
|
+
_IMPORT_REQUEST_SCHEMA = {
|
|
219
|
+
"multipart/form-data": {
|
|
220
|
+
"type": "object",
|
|
221
|
+
"properties": {
|
|
222
|
+
"file": {"type": "string", "format": "binary"},
|
|
223
|
+
"column_map": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"description": "JSON object of {header: field} overrides from the wizard.",
|
|
226
|
+
},
|
|
227
|
+
"date_order": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"enum": list(DATE_ORDERS),
|
|
230
|
+
"default": "auto",
|
|
231
|
+
"description": (
|
|
232
|
+
"How to read slash dates. `auto` scans the file and settles the order "
|
|
233
|
+
"from the first self-identifying value; the other three assert a "
|
|
234
|
+
"convention. An unknown value is a 400, never a silent fallback."
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
"required": ["file"],
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class CsvImportStatusResponseSerializer(serializers.Serializer[Any]):
|
|
244
|
+
"""The 200 body of ``GET …/import/csv/{id}/`` — declared, not prose (#3353).
|
|
245
|
+
|
|
246
|
+
This payload used to be typed ``OpenApiTypes.OBJECT`` with the key names listed
|
|
247
|
+
in a sentence, which is not a contract anyone can generate an SDK against. It
|
|
248
|
+
matters most for ``can_undo``: a field whose entire purpose is to be read by a
|
|
249
|
+
client, reaching ``docs/api/openapi.json`` as an undiscoverable member of a free
|
|
250
|
+
object, is a field that does not exist for a non-browser caller (the #2942 class).
|
|
251
|
+
|
|
252
|
+
Response-only — nothing deserializes it; the view builds the dict itself.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
id = serializers.UUIDField()
|
|
256
|
+
status = serializers.CharField(
|
|
257
|
+
help_text="Outbox lifecycle: `pending` → `dispatched` → `done` | `dead`."
|
|
258
|
+
)
|
|
259
|
+
filename = serializers.CharField()
|
|
260
|
+
summary = serializers.DictField(
|
|
261
|
+
help_text=(
|
|
262
|
+
"The importer's own result summary — counts, parked rows, row errors. "
|
|
263
|
+
"**Empty (`{}`), never null**, until the job has run: `result_summary` is a "
|
|
264
|
+
"`JSONField(default=dict)` with `null=False` and nothing writes `None` to it. "
|
|
265
|
+
"Branch on `status`, not on this being absent."
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
requested_at = serializers.DateTimeField()
|
|
269
|
+
date_order = serializers.CharField(
|
|
270
|
+
help_text=(
|
|
271
|
+
"The slash-date convention this import actually ran under, read back so "
|
|
272
|
+
'"why does this task say 62 days" stays answerable afterwards (#2926).'
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
date_order_confirmed = serializers.BooleanField(
|
|
276
|
+
help_text="Whether a human accepted that convention rather than letting `auto` decide."
|
|
277
|
+
)
|
|
278
|
+
can_undo = serializers.BooleanField(
|
|
279
|
+
help_text=(
|
|
280
|
+
"May THIS caller undo the import? (#3353) Committing is the Scheduler band "
|
|
281
|
+
"and `POST …/import/csv/{id}/undo/` is Admin+, so reaching this endpoint at "
|
|
282
|
+
"all says nothing about the undo's gate. **Authority only** — a `true` is "
|
|
283
|
+
"not a promise the undo will succeed: it additionally requires the project "
|
|
284
|
+
"to be unarchived, and the import to still be in `done` state. Orthogonal "
|
|
285
|
+
"to `status`, which answers whether there is anything to undo."
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class CsvImportColumnMappingSerializer(serializers.Serializer[Any]):
|
|
291
|
+
"""One detected column, as returned in preview's ``columns[]`` (#3682)."""
|
|
292
|
+
|
|
293
|
+
index = serializers.IntegerField()
|
|
294
|
+
header = serializers.CharField()
|
|
295
|
+
field = serializers.CharField(
|
|
296
|
+
allow_null=True, help_text="Target field this column resolved to, or null when unmapped."
|
|
297
|
+
)
|
|
298
|
+
confidence = serializers.CharField(
|
|
299
|
+
help_text="One of `exact` / `fuzzy` / `none` / `duplicate` / `override`."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class CsvImportRowErrorSerializer(serializers.Serializer[Any]):
|
|
304
|
+
"""One row-scoped diagnostic, as returned in preview's ``row_errors[]`` (#3682)."""
|
|
305
|
+
|
|
306
|
+
row = serializers.IntegerField()
|
|
307
|
+
column = serializers.CharField(allow_null=True)
|
|
308
|
+
code = serializers.CharField()
|
|
309
|
+
message = serializers.CharField()
|
|
310
|
+
severity = serializers.CharField(
|
|
311
|
+
help_text="`error` (row did not import) or `warning` (row imported, field dropped)."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class CsvImportDateEvidenceSerializer(serializers.Serializer[Any]):
|
|
316
|
+
"""The single cell that self-identified the date order, when one did (#2926)."""
|
|
317
|
+
|
|
318
|
+
row = serializers.IntegerField()
|
|
319
|
+
column = serializers.CharField()
|
|
320
|
+
value = serializers.CharField()
|
|
321
|
+
reason = serializers.CharField(
|
|
322
|
+
help_text="`no_thirteenth_month` or `second_part_exceeds_twelve`."
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class CsvImportDateReadingSerializer(serializers.Serializer[Any]):
|
|
327
|
+
"""One convention's reading of a sample row — populated only for an ambiguous file."""
|
|
328
|
+
|
|
329
|
+
order = serializers.CharField()
|
|
330
|
+
sample_row = serializers.IntegerField(allow_null=True)
|
|
331
|
+
sample_name = serializers.CharField(allow_blank=True)
|
|
332
|
+
sample_raw_start = serializers.CharField(allow_blank=True)
|
|
333
|
+
start = serializers.CharField(allow_null=True)
|
|
334
|
+
finish = serializers.CharField(allow_null=True)
|
|
335
|
+
duration_days = serializers.IntegerField(allow_null=True)
|
|
336
|
+
values_matched = serializers.IntegerField()
|
|
337
|
+
values_failed = serializers.IntegerField()
|
|
338
|
+
rows_unparseable = serializers.IntegerField()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class CsvImportDatePreviewRowSerializer(serializers.Serializer[Any]):
|
|
342
|
+
"""One rendered row for the wizard's dates preview table (#2926)."""
|
|
343
|
+
|
|
344
|
+
row = serializers.IntegerField()
|
|
345
|
+
name = serializers.CharField(allow_blank=True)
|
|
346
|
+
raw_start = serializers.CharField(allow_blank=True)
|
|
347
|
+
raw_finish = serializers.CharField(allow_blank=True)
|
|
348
|
+
start = serializers.CharField(allow_null=True)
|
|
349
|
+
finish = serializers.CharField(allow_null=True)
|
|
350
|
+
duration_days = serializers.IntegerField(allow_null=True)
|
|
351
|
+
unreadable = serializers.BooleanField()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class CsvImportFieldChoiceSerializer(serializers.Serializer[Any]):
|
|
355
|
+
"""One entry in the wizard's per-column field dropdown catalog."""
|
|
356
|
+
|
|
357
|
+
field = serializers.CharField()
|
|
358
|
+
label = serializers.CharField() # type: ignore[assignment]
|
|
359
|
+
required = serializers.BooleanField() # type: ignore[assignment]
|
|
360
|
+
multi = serializers.BooleanField()
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class CsvImportPreviewResponseSerializer(serializers.Serializer[Any]):
|
|
364
|
+
"""The 200 body of ``POST …/import/csv/preview/`` — declared, not prose (#3682).
|
|
365
|
+
|
|
366
|
+
Nothing is persisted by this endpoint; the view builds this dict directly
|
|
367
|
+
from a :class:`~trueppm_api.apps.csvimport.parser.ParseResult`.
|
|
368
|
+
Response-only — nothing deserializes it.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
filename = serializers.CharField()
|
|
372
|
+
headers = serializers.ListField(child=serializers.CharField())
|
|
373
|
+
columns = CsvImportColumnMappingSerializer(many=True)
|
|
374
|
+
sample_rows = serializers.ListField(
|
|
375
|
+
child=serializers.ListField(child=serializers.CharField()),
|
|
376
|
+
help_text="First 10 parsed rows, raw cell text.",
|
|
377
|
+
)
|
|
378
|
+
row_count = serializers.IntegerField()
|
|
379
|
+
truncated_rows = serializers.IntegerField()
|
|
380
|
+
task_count = serializers.IntegerField(
|
|
381
|
+
help_text="The plan-task count, excluding the Import review branch."
|
|
382
|
+
)
|
|
383
|
+
existing_task_count = serializers.IntegerField()
|
|
384
|
+
projected_task_count = serializers.IntegerField()
|
|
385
|
+
recommended_task_ceiling = serializers.IntegerField()
|
|
386
|
+
exceeds_recommended_ceiling = serializers.BooleanField()
|
|
387
|
+
resource_count = serializers.IntegerField()
|
|
388
|
+
parked_row_count = serializers.IntegerField(
|
|
389
|
+
help_text="Rows that cannot become plan tasks and will be parked in the review branch."
|
|
390
|
+
)
|
|
391
|
+
review_branch_name = serializers.CharField()
|
|
392
|
+
row_errors = CsvImportRowErrorSerializer(many=True)
|
|
393
|
+
error_count = serializers.IntegerField()
|
|
394
|
+
warning_count = serializers.IntegerField()
|
|
395
|
+
warnings = serializers.ListField(child=serializers.CharField())
|
|
396
|
+
date_order = serializers.CharField(help_text="What the caller requested: one of `DATE_ORDERS`.")
|
|
397
|
+
date_order_resolved = serializers.CharField(help_text="What will actually be used.")
|
|
398
|
+
date_order_auto = serializers.CharField(help_text="What `auto` would have chosen.")
|
|
399
|
+
date_order_ambiguous = serializers.BooleanField()
|
|
400
|
+
date_order_evidence = CsvImportDateEvidenceSerializer(allow_null=True)
|
|
401
|
+
date_order_has_columns = serializers.BooleanField()
|
|
402
|
+
date_order_readings = CsvImportDateReadingSerializer(
|
|
403
|
+
many=True, help_text="Both conventions' readings, populated only when ambiguous."
|
|
404
|
+
)
|
|
405
|
+
values_matched = serializers.IntegerField()
|
|
406
|
+
values_failed = serializers.IntegerField()
|
|
407
|
+
date_preview = CsvImportDatePreviewRowSerializer(many=True)
|
|
408
|
+
available_fields = CsvImportFieldChoiceSerializer(many=True)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class CsvImportUndoResultSerializer(serializers.Serializer[Any]):
|
|
412
|
+
"""The ``undo`` sub-object of the undo response — split counts (#3682)."""
|
|
413
|
+
|
|
414
|
+
deleted = serializers.IntegerField(
|
|
415
|
+
help_text="Rows soft-deleted because no one had touched them."
|
|
416
|
+
)
|
|
417
|
+
kept = serializers.IntegerField(
|
|
418
|
+
help_text="Rows left in place because a collaborator has since modified them."
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class CsvImportUndoResponseSerializer(serializers.Serializer[Any]):
|
|
423
|
+
"""The 200 body of ``POST …/import/csv/{id}/undo/`` — declared, not prose (#3682)."""
|
|
424
|
+
|
|
425
|
+
id = serializers.UUIDField()
|
|
426
|
+
status = serializers.CharField()
|
|
427
|
+
undo = CsvImportUndoResultSerializer()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class CsvImportPreviewView(IdempotencyMixin, APIView):
|
|
431
|
+
"""Parse a spreadsheet and return the detected mapping — persisting nothing.
|
|
432
|
+
|
|
433
|
+
Synchronous and stateless by design (ADR-0632 decision 5): the wizard holds
|
|
434
|
+
the mapping in client state, so a retry after a failed commit replays it
|
|
435
|
+
without any server-side draft to resume or expire.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
# Nothing is persisted, so a replay has no resource to dedup.
|
|
439
|
+
idempotency_exempt = True
|
|
440
|
+
# Gated identically to the commit path: preview parses attacker-supplied
|
|
441
|
+
# files, so it is not a lighter-privilege surface just because it does not
|
|
442
|
+
# persist.
|
|
443
|
+
permission_classes = [IsAuthenticated, IsProjectScheduler, IsProjectNotArchived]
|
|
444
|
+
parser_classes = [MultiPartParser]
|
|
445
|
+
|
|
446
|
+
@extend_schema(
|
|
447
|
+
summary="Preview a CSV/Excel import: detected column mapping and sample rows",
|
|
448
|
+
request=_IMPORT_REQUEST_SCHEMA,
|
|
449
|
+
responses={
|
|
450
|
+
200: OpenApiResponse(
|
|
451
|
+
response=CsvImportPreviewResponseSerializer,
|
|
452
|
+
description=(
|
|
453
|
+
"Detected mapping, first 10 parsed rows, row-level warnings and the "
|
|
454
|
+
"field catalog for the wizard's dropdown. Nothing is persisted. "
|
|
455
|
+
"Also carries how the date order was settled and the evidence for "
|
|
456
|
+
"it: `date_order_resolved`, `date_order_auto`, "
|
|
457
|
+
"`date_order_ambiguous`, `date_order_evidence` "
|
|
458
|
+
"({row, column, value, reason}), `date_order_has_columns`, "
|
|
459
|
+
"`values_matched`/`values_failed`, `date_preview` (per-row raw cell, "
|
|
460
|
+
"reading and duration), and — only when the file identifies no "
|
|
461
|
+
"convention — `date_order_readings`, both conventions with the "
|
|
462
|
+
"duration each produces for one sample row. `exceeds_recommended_ceiling` "
|
|
463
|
+
"and its sibling fields warn (never block) when this project would land "
|
|
464
|
+
"past the tested-comfortable Schedule size — see "
|
|
465
|
+
"`administration/sizing.md`."
|
|
466
|
+
),
|
|
467
|
+
),
|
|
468
|
+
400: OpenApiResponse(
|
|
469
|
+
description="Missing, oversized, or unreadable upload, or an unknown `date_order`."
|
|
470
|
+
),
|
|
471
|
+
403: OpenApiResponse(
|
|
472
|
+
description="Caller lacks the Resource Manager role on the project."
|
|
473
|
+
),
|
|
474
|
+
},
|
|
475
|
+
)
|
|
476
|
+
def post(self, request: Request, project_pk: str) -> Response:
|
|
477
|
+
_require_project_scheduler(request.user, project_pk)
|
|
478
|
+
|
|
479
|
+
date_order = _parse_date_order(request)
|
|
480
|
+
|
|
481
|
+
validated = _read_validated_upload(request)
|
|
482
|
+
if isinstance(validated, Response):
|
|
483
|
+
return validated
|
|
484
|
+
filename, content = validated
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
parsed = parse_spreadsheet(
|
|
488
|
+
content,
|
|
489
|
+
filename,
|
|
490
|
+
column_map=_parse_column_map(request) or None,
|
|
491
|
+
date_order=date_order,
|
|
492
|
+
max_rows=settings.CSV_IMPORT_MAX_ROWS,
|
|
493
|
+
max_uncompressed_bytes=settings.CSV_IMPORT_MAX_UNCOMPRESSED_MB * 1024 * 1024,
|
|
494
|
+
)
|
|
495
|
+
except CsvImportError as exc:
|
|
496
|
+
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
|
497
|
+
|
|
498
|
+
# #3388: warn — never block — when this import would land the project past
|
|
499
|
+
# the tested-comfortable Schedule size (administration/sizing.md "Tested
|
|
500
|
+
# envelope": 1.85s at 1,000 tasks, 60s at 2,000 — a cliff, not a slope).
|
|
501
|
+
# Local import: projects -> csvimport already exists (task_batch_services,
|
|
502
|
+
# tasks.py), so this direction is symmetric with the established pattern
|
|
503
|
+
# rather than a new circular-import risk.
|
|
504
|
+
from trueppm_api.apps.projects.models import Task
|
|
505
|
+
|
|
506
|
+
existing_task_count = Task.objects.filter(project_id=project_pk, is_deleted=False).count()
|
|
507
|
+
projected_task_count = existing_task_count + parsed.plan_task_count
|
|
508
|
+
|
|
509
|
+
return Response(
|
|
510
|
+
{
|
|
511
|
+
"filename": filename,
|
|
512
|
+
"headers": parsed.headers,
|
|
513
|
+
"columns": [
|
|
514
|
+
{
|
|
515
|
+
"index": m.index,
|
|
516
|
+
"header": m.header,
|
|
517
|
+
"field": m.field,
|
|
518
|
+
"confidence": m.confidence,
|
|
519
|
+
}
|
|
520
|
+
for m in parsed.mapping
|
|
521
|
+
],
|
|
522
|
+
"sample_rows": parsed.sample_rows,
|
|
523
|
+
"row_count": parsed.total_rows,
|
|
524
|
+
"truncated_rows": parsed.truncated_rows,
|
|
525
|
+
# The plan, not the plan plus its Import review branch (#2732):
|
|
526
|
+
# this number answers "how much of my file arrives as schedule",
|
|
527
|
+
# and the parked rows are counted on their own line below.
|
|
528
|
+
"task_count": parsed.plan_task_count,
|
|
529
|
+
# The tested-comfortable Schedule ceiling (#3388), and whether this
|
|
530
|
+
# project would land past it once this import lands — existing plan
|
|
531
|
+
# tasks plus this file's, not just the file's row count, because the
|
|
532
|
+
# failure the operator would hit is opening the *project*, not the
|
|
533
|
+
# import. Advisory only: `settings.SCHEDULE_TASK_CEILING` is a
|
|
534
|
+
# measured comfort line, not a correctness limit, so this never
|
|
535
|
+
# blocks the commit step.
|
|
536
|
+
"existing_task_count": existing_task_count,
|
|
537
|
+
"projected_task_count": projected_task_count,
|
|
538
|
+
"recommended_task_ceiling": settings.SCHEDULE_TASK_CEILING,
|
|
539
|
+
"exceeds_recommended_ceiling": projected_task_count
|
|
540
|
+
> settings.SCHEDULE_TASK_CEILING,
|
|
541
|
+
"resource_count": len(parsed.project_data.resources),
|
|
542
|
+
# Rows that cannot become plan tasks and will be parked in the
|
|
543
|
+
# Import review branch instead of dropped.
|
|
544
|
+
"parked_row_count": len(parsed.unresolved_rows),
|
|
545
|
+
"review_branch_name": REVIEW_BRANCH_NAME,
|
|
546
|
+
"row_errors": [e.as_dict() for e in parsed.row_errors],
|
|
547
|
+
# Split counts, not just a total: an operator deciding whether to
|
|
548
|
+
# commit needs to know how many rows would be *lost* separately
|
|
549
|
+
# from how many would land with a field defaulted.
|
|
550
|
+
"error_count": parsed.error_count,
|
|
551
|
+
"warning_count": parsed.warning_count,
|
|
552
|
+
"warnings": parsed.warnings,
|
|
553
|
+
# How the date order was settled, and the evidence for it
|
|
554
|
+
# (#2926). Flattened onto the response rather than nested so the
|
|
555
|
+
# wizard's Dates block reads one shape whether it is confirming
|
|
556
|
+
# an inference or reporting an override. `readings` is populated
|
|
557
|
+
# only for the ambiguous file, which is the state that needs both
|
|
558
|
+
# conventions' durations side by side to be decidable at all.
|
|
559
|
+
"date_order": parsed.date_order.requested,
|
|
560
|
+
"date_order_resolved": parsed.date_order.resolved,
|
|
561
|
+
"date_order_auto": parsed.date_order.auto_resolved,
|
|
562
|
+
"date_order_ambiguous": parsed.date_order.ambiguous,
|
|
563
|
+
"date_order_evidence": parsed.date_order.evidence,
|
|
564
|
+
"date_order_has_columns": parsed.date_order.has_date_columns,
|
|
565
|
+
"date_order_readings": [r.as_dict() for r in parsed.date_order.readings],
|
|
566
|
+
"values_matched": parsed.date_order.values_matched,
|
|
567
|
+
"values_failed": parsed.date_order.values_failed,
|
|
568
|
+
"date_preview": parsed.date_preview,
|
|
569
|
+
"available_fields": field_choices(),
|
|
570
|
+
},
|
|
571
|
+
status=status.HTTP_200_OK,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class CsvImportView(IdempotencyMixin, APIView):
|
|
576
|
+
"""Upload and import a CSV/Excel spreadsheet into an existing project.
|
|
577
|
+
|
|
578
|
+
Requires the Resource Manager role. The import runs asynchronously through the
|
|
579
|
+
transactional outbox; the 202 response carries ``import_request_id`` — not a
|
|
580
|
+
Celery task id, which does not exist until the on_commit dispatch fires
|
|
581
|
+
(ADR-0632 decision 6).
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
# Multipart upload, deduped at the table level via CsvImportRequest.
|
|
585
|
+
idempotency_exempt = True
|
|
586
|
+
permission_classes = [IsAuthenticated, IsProjectScheduler, IsProjectNotArchived]
|
|
587
|
+
parser_classes = [MultiPartParser]
|
|
588
|
+
|
|
589
|
+
@extend_schema(
|
|
590
|
+
summary="Import a CSV/Excel spreadsheet into an existing project",
|
|
591
|
+
request=_IMPORT_REQUEST_SCHEMA,
|
|
592
|
+
responses={
|
|
593
|
+
202: inline_serializer(
|
|
594
|
+
name="CsvImportQueuedResponse",
|
|
595
|
+
fields={
|
|
596
|
+
"detail": serializers.CharField(),
|
|
597
|
+
"queued": serializers.BooleanField(),
|
|
598
|
+
"import_request_id": serializers.UUIDField(),
|
|
599
|
+
},
|
|
600
|
+
),
|
|
601
|
+
400: OpenApiResponse(
|
|
602
|
+
description=(
|
|
603
|
+
"Missing or invalid upload (wrong type/too large), or an unknown "
|
|
604
|
+
"`date_order`. The order is persisted on the import request so a "
|
|
605
|
+
"drain re-dispatch replays the convention the operator confirmed."
|
|
606
|
+
)
|
|
607
|
+
),
|
|
608
|
+
403: OpenApiResponse(
|
|
609
|
+
description="Caller lacks the Resource Manager role on the project."
|
|
610
|
+
),
|
|
611
|
+
},
|
|
612
|
+
)
|
|
613
|
+
def post(self, request: Request, project_pk: str) -> Response:
|
|
614
|
+
_require_project_scheduler(request.user, project_pk)
|
|
615
|
+
date_order = _parse_date_order(request)
|
|
616
|
+
# True when the operator explicitly accepted a convention rather than
|
|
617
|
+
# letting auto decide. Kept for support archaeology only — nothing
|
|
618
|
+
# branches on it (#2926).
|
|
619
|
+
# `parser_classes = [MultiPartParser]` — see `_parse_column_map`.
|
|
620
|
+
date_order_confirmed = str(_form_body(request).get("date_order_confirmed", "")).lower() in {
|
|
621
|
+
"true",
|
|
622
|
+
"1",
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
validated = _read_validated_upload(request)
|
|
626
|
+
if isinstance(validated, Response):
|
|
627
|
+
return validated
|
|
628
|
+
filename, content = validated
|
|
629
|
+
|
|
630
|
+
from trueppm_api.apps.csvimport.models import CsvImportRequest
|
|
631
|
+
from trueppm_api.apps.csvimport.services import enqueue_csv_import
|
|
632
|
+
|
|
633
|
+
# Commit the outbox row before any dispatch attempt so a broker outage
|
|
634
|
+
# cannot lose the import — the row stays PENDING and the drain retries.
|
|
635
|
+
with transaction.atomic():
|
|
636
|
+
req = CsvImportRequest.objects.create(
|
|
637
|
+
project_id=project_pk,
|
|
638
|
+
filename=filename,
|
|
639
|
+
file_content_b64=base64.b64encode(content).decode("ascii"),
|
|
640
|
+
column_map=_parse_column_map(request),
|
|
641
|
+
date_order=date_order,
|
|
642
|
+
date_order_confirmed=date_order_confirmed,
|
|
643
|
+
initiated_by_id=request.user.pk,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
req_id = str(req.pk)
|
|
647
|
+
transaction.on_commit(lambda: enqueue_csv_import(req_id))
|
|
648
|
+
|
|
649
|
+
return Response(
|
|
650
|
+
{"detail": "Import queued.", "queued": True, "import_request_id": req_id},
|
|
651
|
+
status=status.HTTP_202_ACCEPTED,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class CsvImportStatusView(APIView):
|
|
656
|
+
"""Poll one import's outbox row for its terminal state and summary.
|
|
657
|
+
|
|
658
|
+
This is what makes an async failure visible on the Schedule that launched it
|
|
659
|
+
(#2151): the wizard polls here after its 202 rather than for a Celery task
|
|
660
|
+
id that the outbox never produces synchronously.
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
permission_classes = [IsAuthenticated, IsProjectScheduler, IsProjectNotArchived]
|
|
664
|
+
|
|
665
|
+
@extend_schema(
|
|
666
|
+
summary="Status and result summary of one CSV/Excel import",
|
|
667
|
+
responses={
|
|
668
|
+
200: OpenApiResponse(
|
|
669
|
+
response=CsvImportStatusResponseSerializer,
|
|
670
|
+
description=(
|
|
671
|
+
"The import's outbox row: lifecycle state, result summary, the date "
|
|
672
|
+
"convention it ran under, and the caller's authority over the undo."
|
|
673
|
+
),
|
|
674
|
+
),
|
|
675
|
+
403: OpenApiResponse(
|
|
676
|
+
description="Caller lacks the Resource Manager role on the project."
|
|
677
|
+
),
|
|
678
|
+
404: OpenApiResponse(description="No such import for this project."),
|
|
679
|
+
},
|
|
680
|
+
)
|
|
681
|
+
def get(self, request: Request, project_pk: str, pk: str) -> Response:
|
|
682
|
+
_require_project_scheduler(request.user, project_pk)
|
|
683
|
+
|
|
684
|
+
from trueppm_api.apps.csvimport.models import CsvImportRequest
|
|
685
|
+
|
|
686
|
+
# Scope by project as well as pk so an id leaked from another project
|
|
687
|
+
# cannot be read here (IDOR).
|
|
688
|
+
req = CsvImportRequest.objects.filter(id=pk, project_id=project_pk).first()
|
|
689
|
+
if req is None:
|
|
690
|
+
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
691
|
+
|
|
692
|
+
return Response(
|
|
693
|
+
{
|
|
694
|
+
"id": str(req.id),
|
|
695
|
+
"status": req.status,
|
|
696
|
+
"filename": req.filename,
|
|
697
|
+
"summary": req.result_summary,
|
|
698
|
+
"requested_at": req.requested_at,
|
|
699
|
+
# The convention this import actually ran under, and whether a
|
|
700
|
+
# human accepted it (#2926). Read back here rather than only
|
|
701
|
+
# persisted: "why does this task say 62 days" is answerable only
|
|
702
|
+
# if the convention is legible after the fact, and a field
|
|
703
|
+
# nothing can read is a field that does not exist for a headless
|
|
704
|
+
# client. `date_order_confirmed` had no reader at all until this.
|
|
705
|
+
"date_order": req.date_order,
|
|
706
|
+
"date_order_confirmed": req.date_order_confirmed,
|
|
707
|
+
# #3353: committing an import is `IsProjectScheduler` (200) and
|
|
708
|
+
# `CsvImportUndoView` is Admin+ (300), so a Scheduler runs an import
|
|
709
|
+
# and is refused the Undo the wizard used to offer regardless. The
|
|
710
|
+
# status payload is where the wizard learns which of the two floors
|
|
711
|
+
# the caller cleared — a 202 on the commit says nothing about the
|
|
712
|
+
# undo's gate (web rule 373(a)).
|
|
713
|
+
#
|
|
714
|
+
# Pure authority, deliberately orthogonal to `status`: `status ==
|
|
715
|
+
# "done"` answers "is there anything to undo", this answers "may
|
|
716
|
+
# you". Folding them into one boolean would make a `false` mean
|
|
717
|
+
# either, with no way for a client to tell them apart.
|
|
718
|
+
"can_undo": can_user_undo_batch_operation(request, project_pk),
|
|
719
|
+
},
|
|
720
|
+
status=status.HTTP_200_OK,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class CsvImportUndoView(IdempotencyMixin, APIView):
|
|
725
|
+
"""⌘Z one completed import (ADR-0810, #2756, #2732).
|
|
726
|
+
|
|
727
|
+
Project-nested like ``CsvImportStatusView`` rather than a router-registered
|
|
728
|
+
``import-fix-operations`` collection: this app has no router, and every
|
|
729
|
+
other endpoint here is a plain ``path()`` under ``projects/<project_pk>/
|
|
730
|
+
import/csv/...`` — matching that convention beats forcing in a different
|
|
731
|
+
app's routing style for one endpoint.
|
|
732
|
+
|
|
733
|
+
No separate ``ImportFixOperation`` model backs this — ``CsvImportRequest``
|
|
734
|
+
already is the outbox row for the import (see ``task_batch_services.py``'s
|
|
735
|
+
import-fix section for why a parallel table would have duplicated it).
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
permission_classes = [IsAuthenticated, IsProjectScheduler, IsProjectNotArchived]
|
|
739
|
+
|
|
740
|
+
@extend_schema(
|
|
741
|
+
summary="Undo one completed CSV/Excel import",
|
|
742
|
+
request=None,
|
|
743
|
+
responses={
|
|
744
|
+
200: OpenApiResponse(
|
|
745
|
+
response=CsvImportUndoResponseSerializer,
|
|
746
|
+
description='{"status", "undo": {"deleted", "kept"}}.',
|
|
747
|
+
),
|
|
748
|
+
400: OpenApiResponse(description="Import is not in a completed (done) state."),
|
|
749
|
+
403: OpenApiResponse(description="Caller lacks the Admin role on the project."),
|
|
750
|
+
404: OpenApiResponse(description="No such import for this project."),
|
|
751
|
+
},
|
|
752
|
+
)
|
|
753
|
+
def post(self, request: Request, project_pk: str, pk: str) -> Response:
|
|
754
|
+
_require_project_admin(request.user, project_pk)
|
|
755
|
+
|
|
756
|
+
from trueppm_api.apps.csvimport.models import CsvImportRequest, CsvImportStatus
|
|
757
|
+
from trueppm_api.apps.projects.task_batch_services import undo_import_fix_operation
|
|
758
|
+
|
|
759
|
+
req = CsvImportRequest.objects.filter(id=pk, project_id=project_pk).first()
|
|
760
|
+
if req is None:
|
|
761
|
+
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
762
|
+
if req.status != CsvImportStatus.DONE:
|
|
763
|
+
return Response(
|
|
764
|
+
{"detail": f"Only a completed import can be undone (this one is {req.status})."},
|
|
765
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
summary = undo_import_fix_operation(req)
|
|
769
|
+
req.refresh_from_db()
|
|
770
|
+
return Response(
|
|
771
|
+
{"id": str(req.id), "status": req.status, "undo": summary},
|
|
772
|
+
status=status.HTTP_200_OK,
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
class CsvImportTemplateView(APIView):
|
|
777
|
+
"""Download the known-good CSV template.
|
|
778
|
+
|
|
779
|
+
Authenticated but not project-scoped: the template is static content with no
|
|
780
|
+
customer data in it, and the wizard links it from step 1 before a project
|
|
781
|
+
context necessarily exists.
|
|
782
|
+
"""
|
|
783
|
+
|
|
784
|
+
permission_classes = [IsAuthenticated]
|
|
785
|
+
|
|
786
|
+
@extend_schema(
|
|
787
|
+
summary="Download a known-good CSV import template",
|
|
788
|
+
responses={200: OpenApiResponse(description="text/csv template file.")},
|
|
789
|
+
)
|
|
790
|
+
def get(self, request: Request) -> HttpResponse:
|
|
791
|
+
response = HttpResponse(CSV_TEMPLATE, content_type="text/csv; charset=utf-8")
|
|
792
|
+
response["Content-Disposition"] = f'attachment; filename="{TEMPLATE_FILENAME}"'
|
|
793
|
+
return response
|