trueppm-api 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +27 -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 +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -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_squashed_0019_release_0_4.py +302 -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 +143 -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/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -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/0001_squashed_0003_release_0_4.py +344 -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 +109 -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/0001_squashed_0003_release_0_4.py +90 -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 +459 -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 +178 -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 +863 -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 +597 -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_squashed_0014_release_0_4.py +225 -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/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -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 +528 -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 +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -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 +280 -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 +551 -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 +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -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 +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -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 +299 -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_squashed_0011_release_0_4.py +204 -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 +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -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 +566 -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 +993 -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 +537 -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_squashed_0009_release_0_4.py +128 -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 +541 -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 +458 -0
- trueppm_api/apps/projects/backfill.py +459 -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 +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -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/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -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 +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -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/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -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 +59 -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 +75 -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 +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -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/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -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 +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -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 +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -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 +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -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 +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -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 +1244 -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 +1979 -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 +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -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/0014_squashed_0015_release_0_4.py +47 -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 +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -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/mc_admission.py +181 -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/0010_squashed_0016_release_0_4.py +106 -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/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -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 +2908 -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 +446 -0
- trueppm_api/apps/sso/services.py +1473 -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 +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -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/0003_squashed_0005_release_0_4.py +77 -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/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -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 +159 -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/0004_backfill_default_teams_on_upgrade.py +49 -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 +440 -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/0001_squashed_0004_release_0_4.py +167 -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 +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -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 +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -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 +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -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 +371 -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 +254 -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_squashed_0027_release_0_4.py +199 -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 +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -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/demo_read_only.py +314 -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 +1221 -0
- trueppm_api/core/password_reset.py +513 -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 +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -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/core/worker_selfheal.py +205 -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 +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -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.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"""Apply logic for the mobile sync upload (push) endpoint (ADR-0082, #667).
|
|
2
|
+
|
|
3
|
+
Kept in its own module to keep ``views.py`` focused on the SyncBatch
|
|
4
|
+
idempotency/atomicity orchestration. The single public entry point is
|
|
5
|
+
:func:`apply_task_changes`, called from ``ProjectSyncView.post`` inside the
|
|
6
|
+
batch's ``transaction.atomic`` block.
|
|
7
|
+
|
|
8
|
+
Conflict resolution mirrors the REST ``perform_update`` field-level guard
|
|
9
|
+
(ADR-0217, #1718): a row whose ``base_version`` (its own ``base_version`` key, or
|
|
10
|
+
the batch's ``last_pulled_at`` watermark) is stale is checked against the fields a
|
|
11
|
+
concurrent writer changed. A disjoint edit still applies (field-level merge); an
|
|
12
|
+
overlapping one is reported as a per-row conflict entry and **skipped** — it does
|
|
13
|
+
not bump ``server_version`` and does not broadcast. This closes the lost-update
|
|
14
|
+
hole where routing a stale edit through ``/sync/`` bypassed the REST 409 that a
|
|
15
|
+
stale ``PATCH`` would have hit. A row that omits any base version keeps plain
|
|
16
|
+
last-writer-wins, so existing clients are unaffected.
|
|
17
|
+
|
|
18
|
+
Apply reuses ``TaskSerializer`` — the same serializer the REST PATCH path uses —
|
|
19
|
+
so mobile writes inherit identical validation and business rules (progress-anchor
|
|
20
|
+
gate, milestone invariant, sprint cross-project IDOR check, auto-promote on
|
|
21
|
+
past planned_start). This is deliberate: the upload path must never be able to
|
|
22
|
+
do something a member could not do via ``PATCH /tasks/{id}/``.
|
|
23
|
+
|
|
24
|
+
Per-row edit/delete permission likewise calls ``can_user_edit_task`` (the ADR-0133
|
|
25
|
+
source of truth) directly rather than re-implementing the role matrix, so the
|
|
26
|
+
upload path can never drift from REST — it previously omitted the Product Owner
|
|
27
|
+
facet, silently rolling back a PO's whole offline batch the moment one groomed
|
|
28
|
+
EPIC/STORY row was touched (#1771). Edits pass ``method="PATCH"`` and deletes
|
|
29
|
+
``method="DELETE"`` so the PO-may-groom-but-not-delete asymmetry is preserved.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import TYPE_CHECKING, Any
|
|
36
|
+
|
|
37
|
+
from django.conf import settings
|
|
38
|
+
from rest_framework import status
|
|
39
|
+
from rest_framework.exceptions import APIException, PermissionDenied, ValidationError
|
|
40
|
+
|
|
41
|
+
from trueppm_api.apps.access.permissions import can_user_author_plan, can_user_edit_task
|
|
42
|
+
from trueppm_api.apps.projects.models import (
|
|
43
|
+
Task,
|
|
44
|
+
resync_structural_parent_after_delete,
|
|
45
|
+
structural_parent,
|
|
46
|
+
)
|
|
47
|
+
from trueppm_api.apps.projects.services import soft_delete_task_with_cross_fanout
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from rest_framework.request import Request
|
|
51
|
+
|
|
52
|
+
from trueppm_api.apps.projects.models import Project
|
|
53
|
+
|
|
54
|
+
# v1 writable surface (ADR-0082 §B): mobile may push only the ``tasks``
|
|
55
|
+
# collection. Any other key is rejected with 400 rather than silently dropped.
|
|
56
|
+
WRITABLE_COLLECTIONS = frozenset({"tasks"})
|
|
57
|
+
|
|
58
|
+
# Fields the apply layer controls itself — stripped from every incoming row so a
|
|
59
|
+
# client cannot reparent a task to another project or forge identity/version.
|
|
60
|
+
# (CPM-output and sync-bookkeeping fields are already read-only on TaskSerializer
|
|
61
|
+
# and dropped on input automatically.)
|
|
62
|
+
#
|
|
63
|
+
# ``wbs_path`` is now **also** read-only on TaskSerializer (ADR-0743, #2585), so
|
|
64
|
+
# apply — which reuses that serializer — would drop it regardless. It is retained
|
|
65
|
+
# here as defense-in-depth, the additive pattern ADR-0184 established: the
|
|
66
|
+
# declaration is authoritative, this strip is a second expression of it.
|
|
67
|
+
#
|
|
68
|
+
# Do NOT extend this set with ``is_subtask`` or ``parent_governance_inherited``.
|
|
69
|
+
# They are read-only on TaskSerializer too, which covers the REST path and this one
|
|
70
|
+
# from a single declaration. Re-stating the rule here is exactly how enforcement
|
|
71
|
+
# drifted apart before: the strip set had ``wbs_path`` while the serializer did not,
|
|
72
|
+
# so sync was guarded and PATCH was not.
|
|
73
|
+
_STRIPPED_ROW_KEYS = frozenset({"id", "project", "wbs_path"})
|
|
74
|
+
|
|
75
|
+
# Default cap on total rows (created + updated + deleted) in one upload batch.
|
|
76
|
+
# The whole batch applies inside a single transaction; an unbounded batch would
|
|
77
|
+
# hold that transaction (and per-task row locks) open arbitrarily long. The
|
|
78
|
+
# per-user request throttle limits frequency, not payload size — this limits
|
|
79
|
+
# size. Overridable via settings.TRUEPPM_SYNC_BATCH_MAX_ROWS.
|
|
80
|
+
DEFAULT_MAX_BATCH_ROWS = 500
|
|
81
|
+
|
|
82
|
+
# Default cap on inline ``owners`` entries summed across an ENTIRE upload batch
|
|
83
|
+
# (#3643). Same shape as ``TaskBulkView``: ``MAX_TASK_OWNERS_PER_WRITE``
|
|
84
|
+
# (serializers.py, #3596) bounds a single row's ``owners`` list at 100, but this
|
|
85
|
+
# module reuses ``TaskSerializer`` per row across up to ``DEFAULT_MAX_BATCH_ROWS``
|
|
86
|
+
# created/updated rows, so the per-row cap alone still lets one push compose
|
|
87
|
+
# 500 x 100 = 50,000 owner entries, each minting a ``TaskActivityEvent`` and (this
|
|
88
|
+
# path does not suppress it — see ``TaskSerializer._owners_broadcast_enabled``) a
|
|
89
|
+
# post-commit ``broadcast_board_event``. Bounded batch-wide, checked up front
|
|
90
|
+
# alongside the row cap, for the same reason: cheaper to reject before the write
|
|
91
|
+
# transaction opens than mid-batch. Overridable via
|
|
92
|
+
# settings.TRUEPPM_SYNC_BATCH_MAX_OWNERS.
|
|
93
|
+
DEFAULT_MAX_BATCH_OWNERS = 500
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SyncIdCollision(APIException):
|
|
97
|
+
"""A client-generated id in the ``created`` bucket already exists in another project.
|
|
98
|
+
|
|
99
|
+
Raised when the upserted row's id resolves to a task the caller's URL-scoped
|
|
100
|
+
project does not own. This is an IDOR guard (#887): without it the upsert
|
|
101
|
+
would treat the foreign row as an idempotent re-create and apply the caller's
|
|
102
|
+
content using their role on the *URL* project, not the project that actually
|
|
103
|
+
owns the row. A 409 forces the offline client to regenerate the id and retry,
|
|
104
|
+
rather than silently mutating a task in a project it cannot see.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
status_code = status.HTTP_409_CONFLICT
|
|
108
|
+
default_detail = (
|
|
109
|
+
"A task with this client-generated id already exists in another project. "
|
|
110
|
+
"Regenerate the id and re-upload."
|
|
111
|
+
)
|
|
112
|
+
default_code = "sync_id_collision"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class BatchApplyResult:
|
|
117
|
+
"""Outcome of applying one upload batch — drives the response + broadcasts."""
|
|
118
|
+
|
|
119
|
+
created: list[dict[str, Any]] = field(default_factory=list)
|
|
120
|
+
updated: list[dict[str, Any]] = field(default_factory=list)
|
|
121
|
+
deleted: list[dict[str, Any]] = field(default_factory=list)
|
|
122
|
+
# Rows rejected by the field-level conflict guard (#1718): each entry carries the
|
|
123
|
+
# REST-parity 409 body (conflict_fields / server_value / client_value /
|
|
124
|
+
# server_version) plus the row ``id``. A conflicting row is NOT applied — it is
|
|
125
|
+
# absent from created/updated and never bumps server_version or broadcasts.
|
|
126
|
+
conflicts: list[dict[str, Any]] = field(default_factory=list)
|
|
127
|
+
# (event_type, task_id) pairs to broadcast on_commit, mirroring single-row writes.
|
|
128
|
+
events: list[tuple[str, str]] = field(default_factory=list)
|
|
129
|
+
# Highest delta cursor (``sync_seq``) touched — the checkpoint the client
|
|
130
|
+
# adopts as its next ``since``, so it must be on the *cursor* scale, not the
|
|
131
|
+
# per-row ``server_version`` scale (ADR-0686). Handing back a row's save count
|
|
132
|
+
# here would give the client a `since` from an unrelated number line.
|
|
133
|
+
max_version: int = 0
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def changed(self) -> bool:
|
|
137
|
+
return bool(self.created or self.updated or self.deleted)
|
|
138
|
+
|
|
139
|
+
def bump(self, version: int) -> None:
|
|
140
|
+
"""Advance the batch's high-water delta cursor."""
|
|
141
|
+
self.max_version = max(self.max_version, version)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _content(row: dict[str, Any]) -> dict[str, Any]:
|
|
145
|
+
"""Strip server-controlled keys from an incoming row."""
|
|
146
|
+
return {k: v for k, v in row.items() if k not in _STRIPPED_ROW_KEYS}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _row_base_version(row: dict[str, Any], batch_base: int | None) -> int | None:
|
|
150
|
+
"""The ``server_version`` the client last saw for this row (#1718 conflict guard).
|
|
151
|
+
|
|
152
|
+
Prefer a row-level ``base_version`` key (mirroring the REST ``base_version`` body
|
|
153
|
+
key that :func:`~trueppm_api.apps.sync.conflict.parse_base_version` reads) for
|
|
154
|
+
per-row precision; fall back to the batch's ``last_pulled_at`` watermark when the
|
|
155
|
+
row omits it. ``None`` (neither present, or unparseable) preserves the pre-#1718
|
|
156
|
+
last-writer-wins behavior, so existing clients are fully backward compatible.
|
|
157
|
+
|
|
158
|
+
Note the row's own ``server_version`` field is deliberately NOT used as the base:
|
|
159
|
+
it is a read-only, server-owned field (stripped on input) and clients have long
|
|
160
|
+
sent arbitrary values there as an opaque marker — repurposing it as a conflict
|
|
161
|
+
base would silently change their semantics.
|
|
162
|
+
"""
|
|
163
|
+
raw = row.get("base_version")
|
|
164
|
+
if raw is None or isinstance(raw, bool):
|
|
165
|
+
return batch_base
|
|
166
|
+
try:
|
|
167
|
+
value = int(raw)
|
|
168
|
+
except (TypeError, ValueError):
|
|
169
|
+
return batch_base
|
|
170
|
+
return value if value >= 0 else batch_base
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class _ApplyContext:
|
|
175
|
+
"""Read-only inputs shared by the per-bucket apply helpers.
|
|
176
|
+
|
|
177
|
+
Bundling them keeps each helper's signature small — the alternative is
|
|
178
|
+
threading the request, project, serializer context, the batched existing-row
|
|
179
|
+
index, and the batch conflict base through three parallel loops.
|
|
180
|
+
|
|
181
|
+
``serializer_context`` is the DRF context (``request`` / ``caller_role`` /
|
|
182
|
+
``project``) every ``TaskSerializer`` in the batch shares. ``existing_by_id``
|
|
183
|
+
is the project-scoped bulk fetch keyed by ``str(pk)``. ``last_pulled_at`` is
|
|
184
|
+
the batch's default per-row conflict base.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
request: Request
|
|
188
|
+
project: Project
|
|
189
|
+
serializer_context: dict[str, Any]
|
|
190
|
+
existing_by_id: dict[str, Task]
|
|
191
|
+
last_pulled_at: int | None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _validate_and_extract_tasks(changes: dict[str, Any]) -> dict[str, Any]:
|
|
195
|
+
"""Validate the upload envelope and return its ``tasks`` delta.
|
|
196
|
+
|
|
197
|
+
Enforces the v1 writable surface (only ``tasks`` — ADR-0082 §B) and the
|
|
198
|
+
per-batch row cap up front, so a malformed or oversized envelope is rejected
|
|
199
|
+
before the write transaction touches any row.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValidationError: malformed envelope, an unsupported collection key, or a
|
|
203
|
+
batch exceeding the row cap.
|
|
204
|
+
"""
|
|
205
|
+
if not isinstance(changes, dict):
|
|
206
|
+
raise ValidationError({"changes": "Must be an object."})
|
|
207
|
+
unsupported = set(changes) - WRITABLE_COLLECTIONS
|
|
208
|
+
if unsupported:
|
|
209
|
+
raise ValidationError(
|
|
210
|
+
{
|
|
211
|
+
"changes": f"Unsupported collection(s): {', '.join(sorted(unsupported))}. "
|
|
212
|
+
f"Only {', '.join(sorted(WRITABLE_COLLECTIONS))} may be uploaded."
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
tasks = changes.get("tasks", {})
|
|
217
|
+
if not isinstance(tasks, dict):
|
|
218
|
+
raise ValidationError({"changes.tasks": "Must be an object."})
|
|
219
|
+
|
|
220
|
+
# Bound the batch so one request cannot hold a long transaction open.
|
|
221
|
+
max_rows = getattr(settings, "TRUEPPM_SYNC_BATCH_MAX_ROWS", DEFAULT_MAX_BATCH_ROWS)
|
|
222
|
+
row_count = sum(
|
|
223
|
+
len(tasks.get(bucket, []) or []) for bucket in ("created", "updated", "deleted")
|
|
224
|
+
)
|
|
225
|
+
if row_count > max_rows:
|
|
226
|
+
raise ValidationError(
|
|
227
|
+
{"changes": f"Batch too large: {row_count} rows exceeds the limit of {max_rows}."}
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Batch-wide ``owners`` budget (#3643) — see ``DEFAULT_MAX_BATCH_OWNERS``. Only
|
|
231
|
+
# ``created``/``updated`` rows can carry inline ``owners``; ``deleted`` is a bare
|
|
232
|
+
# id list. A non-list ``owners`` counts as zero here and is left for
|
|
233
|
+
# ``TaskSerializer``'s own field validation to reject with the proper message,
|
|
234
|
+
# same as the row itself failing to be a dict.
|
|
235
|
+
max_owners = getattr(settings, "TRUEPPM_SYNC_BATCH_MAX_OWNERS", DEFAULT_MAX_BATCH_OWNERS)
|
|
236
|
+
total_owners = sum(
|
|
237
|
+
len(owners)
|
|
238
|
+
for bucket in ("created", "updated")
|
|
239
|
+
for row in (tasks.get(bucket, []) or [])
|
|
240
|
+
if isinstance(row, dict) and isinstance(owners := row.get("owners"), list)
|
|
241
|
+
)
|
|
242
|
+
if total_owners > max_owners:
|
|
243
|
+
raise ValidationError(
|
|
244
|
+
{
|
|
245
|
+
"changes": (
|
|
246
|
+
f"Batch names {total_owners} owner entries across its rows, more "
|
|
247
|
+
f"than the limit of {max_owners}. Split the push into smaller "
|
|
248
|
+
"batches, or send fewer owners per row."
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
return tasks
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _fetch_existing_tasks(tasks: dict[str, Any], project: Project) -> dict[str, Task]:
|
|
256
|
+
"""Bulk-fetch every project-scoped row the batch references in one query.
|
|
257
|
+
|
|
258
|
+
So each bucket loop is a dict lookup, not a per-row SELECT (#809): a 500-row
|
|
259
|
+
batch previously issued up to 500 ``Task.objects.filter(...).first()``
|
|
260
|
+
round-trips. Keyed by ``str(pk)`` because ``in_bulk()``'s UUID keys won't
|
|
261
|
+
match the string ids in the JSON payload.
|
|
262
|
+
|
|
263
|
+
SECURITY (#887): this lookup is scoped to ``project``. The created-bucket
|
|
264
|
+
upsert treats a hit as an idempotent re-create and applies content after a
|
|
265
|
+
role check on the *URL-scoped* project; if the lookup were unscoped a client
|
|
266
|
+
could land a created row whose id collides with a task in a different project
|
|
267
|
+
and mutate it using their role here, not their (possibly absent) role there —
|
|
268
|
+
a cross-project IDOR. Scoping the fetch means a foreign id simply isn't in the
|
|
269
|
+
returned map; the created loop then probes project-unscoped *only* to
|
|
270
|
+
distinguish a genuine cross-project collision (→ 409, regenerate) from a fresh
|
|
271
|
+
id (→ create). The updated and deleted loops re-apply their project +
|
|
272
|
+
is_deleted predicate in Python as a backstop.
|
|
273
|
+
"""
|
|
274
|
+
from trueppm_api.apps.projects.row_identity import fetch_project_scoped_tasks
|
|
275
|
+
|
|
276
|
+
batch_ids = {
|
|
277
|
+
str(rid)
|
|
278
|
+
for bucket in ("created", "updated")
|
|
279
|
+
for row in (tasks.get(bucket, []) or [])
|
|
280
|
+
if (rid := row.get("id"))
|
|
281
|
+
} | {str(del_id) for del_id in (tasks.get("deleted", []) or []) if del_id}
|
|
282
|
+
# Shared with TaskBulkView so the two client-minted-id write paths cannot drift
|
|
283
|
+
# (ADR-0772 §Security; ADR-0743 §4 is the standing rule).
|
|
284
|
+
return fetch_project_scoped_tasks(batch_ids, project)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _cross_project_created_ids(
|
|
288
|
+
tasks: dict[str, Any], existing_by_id: dict[str, Task], project: Project
|
|
289
|
+
) -> set[str]:
|
|
290
|
+
"""Created ids that resolve to a task in *another* project (#887 IDOR guard).
|
|
291
|
+
|
|
292
|
+
Such an id must be a 409 rather than a silent foreign-row mutation. Only ids
|
|
293
|
+
absent from the project-scoped fetch can be foreign, so probe just those — the
|
|
294
|
+
common (all-local) batch issues no extra query.
|
|
295
|
+
"""
|
|
296
|
+
from trueppm_api.apps.projects.row_identity import foreign_task_ids
|
|
297
|
+
|
|
298
|
+
created_ids = {str(rid) for row in (tasks.get("created", []) or []) if (rid := row.get("id"))}
|
|
299
|
+
# Shared with TaskBulkView — see _fetch_existing_tasks. The two paths differ only
|
|
300
|
+
# in how they REPORT a hit: sync aborts the request with a 409, the batch endpoint
|
|
301
|
+
# rejects the single row with a non-asserting ``id_unavailable``.
|
|
302
|
+
return foreign_task_ids(created_ids, existing_by_id, project)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _apply_created_row(
|
|
306
|
+
row: dict[str, Any],
|
|
307
|
+
ctx: _ApplyContext,
|
|
308
|
+
cross_project_ids: set[str],
|
|
309
|
+
result: BatchApplyResult,
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Upsert one ``created``-bucket row by its client-generated id.
|
|
312
|
+
|
|
313
|
+
A fresh id creates; an id already present in the project is an idempotent
|
|
314
|
+
re-create applied as an in-place edit (with the stricter edit permission and
|
|
315
|
+
the same field-level conflict guard as the updated bucket).
|
|
316
|
+
"""
|
|
317
|
+
from trueppm_api.apps.projects.serializers import TaskSerializer
|
|
318
|
+
from trueppm_api.apps.projects.services import maybe_record_scope_injection
|
|
319
|
+
from trueppm_api.apps.sync.conflict import check_row_conflict
|
|
320
|
+
|
|
321
|
+
row_id = row.get("id")
|
|
322
|
+
if not row_id:
|
|
323
|
+
raise ValidationError({"tasks.created": "Each created row requires an 'id'."})
|
|
324
|
+
if str(row_id) in cross_project_ids:
|
|
325
|
+
raise SyncIdCollision()
|
|
326
|
+
existing = ctx.existing_by_id.get(str(row_id))
|
|
327
|
+
if existing is not None and existing.is_deleted:
|
|
328
|
+
# A client re-pushing a created row whose id matches a tombstone —
|
|
329
|
+
# skip, mirroring the updated/deleted loops (#1730). ``is_deleted`` is
|
|
330
|
+
# non-writable so this could never resurrect the row anyway; without
|
|
331
|
+
# the guard the re-create branch below would run a full serializer save
|
|
332
|
+
# on the dead row, bump server_version, and emit a spurious
|
|
333
|
+
# task_updated. The next pull reconciles the client via the tombstone.
|
|
334
|
+
return
|
|
335
|
+
if existing is not None:
|
|
336
|
+
# Idempotent re-create (the row already landed in a prior batch) —
|
|
337
|
+
# apply as an update, but enforce the stricter edit permission.
|
|
338
|
+
if not can_user_edit_task(ctx.request, existing, method="PATCH"):
|
|
339
|
+
raise PermissionDenied("You may not edit this task.")
|
|
340
|
+
old_sprint_id = str(existing.sprint_id) if existing.sprint_id else None
|
|
341
|
+
# We already know this row exists (it came from the batched
|
|
342
|
+
# existing_by_id fetch), so tell VersionedModel.save() to skip its
|
|
343
|
+
# per-row exists() probe. DRF's serializer.save() can't forward a
|
|
344
|
+
# save() kwarg, so the known state is carried on the instance (#1527).
|
|
345
|
+
existing._sync_known_exists = True # type: ignore[attr-defined]
|
|
346
|
+
ser = TaskSerializer(
|
|
347
|
+
existing, data=_content(row), partial=True, context=ctx.serializer_context
|
|
348
|
+
)
|
|
349
|
+
ser.is_valid(raise_exception=True)
|
|
350
|
+
# An idempotent re-create is an in-place edit, so it can lose a concurrent
|
|
351
|
+
# update just like the updated bucket — guard it identically (#1718).
|
|
352
|
+
conflict = check_row_conflict(ser, _row_base_version(row, ctx.last_pulled_at))
|
|
353
|
+
if conflict is not None:
|
|
354
|
+
result.conflicts.append({"id": str(existing.pk), **conflict})
|
|
355
|
+
return
|
|
356
|
+
task = ser.save()
|
|
357
|
+
maybe_record_scope_injection(task, old_sprint_id, ctx.request.user)
|
|
358
|
+
result.events.append(("task_updated", str(task.pk)))
|
|
359
|
+
else:
|
|
360
|
+
# New row. The endpoint's role >= MEMBER floor is NOT the create bar: REST
|
|
361
|
+
# create carries IsProjectPlanAuthor alongside IsProjectMemberWrite (#3034,
|
|
362
|
+
# ADR-0773), which refuses the Scheduler band (and any Enterprise role in the
|
|
363
|
+
# 2xx band). Admitting it here would commit a row the caller can never edit or
|
|
364
|
+
# delete (the can_user_edit_task refusal on the other two buckets). The same
|
|
365
|
+
# predicate REST uses is called so custom roles get an identical answer, and
|
|
366
|
+
# the PermissionDenied rolls the whole batch back (all-or-nothing). project is
|
|
367
|
+
# injected from the URL (not the client row, which is stripped) so a push can
|
|
368
|
+
# never create into a different project.
|
|
369
|
+
if not can_user_author_plan(ctx.request, ctx.project):
|
|
370
|
+
raise PermissionDenied("Your role on this project cannot author the plan.")
|
|
371
|
+
data = _content(row)
|
|
372
|
+
data["project"] = ctx.project.pk
|
|
373
|
+
ser = TaskSerializer(data=data, context=ctx.serializer_context)
|
|
374
|
+
ser.is_valid(raise_exception=True)
|
|
375
|
+
task = ser.save(id=row_id)
|
|
376
|
+
# A task created directly into an ACTIVE sprint via sync (old link =
|
|
377
|
+
# None) still enters pending-acceptance, not the commitment.
|
|
378
|
+
maybe_record_scope_injection(task, None, ctx.request.user)
|
|
379
|
+
result.events.append(("task_created", str(task.pk)))
|
|
380
|
+
result.created.append({"id": str(task.pk), "server_version": task.server_version})
|
|
381
|
+
result.bump(task.sync_seq)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _apply_updated_row(row: dict[str, Any], ctx: _ApplyContext, result: BatchApplyResult) -> None:
|
|
385
|
+
"""Apply one ``updated``-bucket row, honoring the field-level conflict guard."""
|
|
386
|
+
from trueppm_api.apps.projects.serializers import TaskSerializer
|
|
387
|
+
from trueppm_api.apps.projects.services import maybe_record_scope_injection
|
|
388
|
+
from trueppm_api.apps.sync.conflict import check_row_conflict
|
|
389
|
+
|
|
390
|
+
row_id = row.get("id")
|
|
391
|
+
if not row_id:
|
|
392
|
+
raise ValidationError({"tasks.updated": "Each updated row requires an 'id'."})
|
|
393
|
+
target = ctx.existing_by_id.get(str(row_id))
|
|
394
|
+
if target is None or target.project_id != ctx.project.pk or target.is_deleted:
|
|
395
|
+
# Unknown, cross-project, or already-tombstoned row — skip; the next
|
|
396
|
+
# pull reconciles it via the tombstone. Not an error: a benign
|
|
397
|
+
# offline/online race. (Predicate mirrors the original
|
|
398
|
+
# filter(pk=row_id, project=project, is_deleted=False).)
|
|
399
|
+
return
|
|
400
|
+
if not can_user_edit_task(ctx.request, target, method="PATCH"):
|
|
401
|
+
raise PermissionDenied("You may not edit this task.")
|
|
402
|
+
# ADR-0102 §4: capture the prior sprint link so a task linked to an ACTIVE
|
|
403
|
+
# sprint via sync enters pending-acceptance, same as the REST PATCH path —
|
|
404
|
+
# otherwise an offline edit could land work straight into the commitment.
|
|
405
|
+
old_sprint_id = str(target.sprint_id) if target.sprint_id else None
|
|
406
|
+
# ``target`` came from the batched existing_by_id fetch — the row exists, so
|
|
407
|
+
# skip VersionedModel.save()'s per-row exists() probe (#1527). Carried on the
|
|
408
|
+
# instance because DRF's serializer.save() can't forward a save() kwarg.
|
|
409
|
+
target._sync_known_exists = True # type: ignore[attr-defined]
|
|
410
|
+
ser = TaskSerializer(target, data=_content(row), partial=True, context=ctx.serializer_context)
|
|
411
|
+
ser.is_valid(raise_exception=True)
|
|
412
|
+
# Field-level lost-update guard (#1718): if this stale edit overlaps a field a
|
|
413
|
+
# concurrent writer changed since the client's base version, skip it and report
|
|
414
|
+
# a per-row conflict rather than silently clobbering — mirrors the REST 409.
|
|
415
|
+
# Skipping before save() means no server_version bump and no broadcast for it.
|
|
416
|
+
# No batch N+1: check_row_conflict short-circuits (no query) unless the row's
|
|
417
|
+
# server_version has actually advanced past the client's base, so the bounded
|
|
418
|
+
# history read runs only for the typically-tiny contended subset, not per row.
|
|
419
|
+
conflict = check_row_conflict(ser, _row_base_version(row, ctx.last_pulled_at))
|
|
420
|
+
if conflict is not None:
|
|
421
|
+
result.conflicts.append({"id": str(target.pk), **conflict})
|
|
422
|
+
return
|
|
423
|
+
saved = ser.save()
|
|
424
|
+
maybe_record_scope_injection(saved, old_sprint_id, ctx.request.user)
|
|
425
|
+
result.updated.append({"id": str(saved.pk), "server_version": saved.server_version})
|
|
426
|
+
result.events.append(("task_updated", str(saved.pk)))
|
|
427
|
+
result.bump(saved.sync_seq)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _apply_deleted_row(del_id: Any, ctx: _ApplyContext, result: BatchApplyResult) -> None:
|
|
431
|
+
"""Soft-delete one ``deleted``-bucket row; unknown/cross-project ids are idempotent."""
|
|
432
|
+
target = ctx.existing_by_id.get(str(del_id))
|
|
433
|
+
if target is None or target.project_id != ctx.project.pk or target.is_deleted:
|
|
434
|
+
return # unknown, cross-project, or already gone — idempotent
|
|
435
|
+
if not can_user_edit_task(ctx.request, target, method="DELETE"):
|
|
436
|
+
raise PermissionDenied("You may not delete this task.")
|
|
437
|
+
parent = structural_parent(target)
|
|
438
|
+
# bumps server_version, sets deleted_version, cascades; also recalcs/notifies the far
|
|
439
|
+
# side of any cascaded cross-project edge (#4592)
|
|
440
|
+
soft_delete_task_with_cross_fanout(target)
|
|
441
|
+
result.deleted.append({"id": str(target.pk), "server_version": target.server_version})
|
|
442
|
+
result.events.append(("task_deleted", str(target.pk)))
|
|
443
|
+
result.bump(target.sync_seq)
|
|
444
|
+
# Mirror the REST DELETE path's post-delete repair (#2950) so this path does
|
|
445
|
+
# not reach the #4371 lost-update class: without it, an accidental
|
|
446
|
+
# container that just lost its last child keeps its rolled-up duration and
|
|
447
|
+
# the next CPM write-back finds what looks like a still-has-children row
|
|
448
|
+
# and never fixes it (#4463). Resyncing per deleted row — rather than once
|
|
449
|
+
# per unique parent after the whole bucket — is still correct when a batch
|
|
450
|
+
# deletes several siblings under the same container: each call re-derives
|
|
451
|
+
# the parent's container-ness from the current DB state, so an
|
|
452
|
+
# intermediate no-op call (parent still has a live sibling) costs nothing
|
|
453
|
+
# and the call that removes the last live child is the one that fires.
|
|
454
|
+
resynced_parent = resync_structural_parent_after_delete(parent)
|
|
455
|
+
if resynced_parent is not None:
|
|
456
|
+
# Replace the batch's pre-fetch snapshot of this row with the resynced
|
|
457
|
+
# instance. Without this, a parent ALSO named in the same batch's own
|
|
458
|
+
# `deleted` bucket (deleting a container and its last live child
|
|
459
|
+
# together) would be soft-deleted off the STALE snapshot taken before
|
|
460
|
+
# this resync ran — `soft_delete()`'s `save()` carries no explicit
|
|
461
|
+
# `update_fields`, so `VersionedModel.save()` writes every column off
|
|
462
|
+
# that stale instance and silently re-clobbers the correction this very
|
|
463
|
+
# request just made. The row is then tombstoned with the wrong duration
|
|
464
|
+
# forever, invisible until someone restores it (#1113).
|
|
465
|
+
ctx.existing_by_id[str(resynced_parent.pk)] = resynced_parent
|
|
466
|
+
# Report it the same way a created/updated row is reported: fold the
|
|
467
|
+
# parent into the coalesced WS broadcast (connected web peers get a
|
|
468
|
+
# prompt refetch rather than waiting on the deferred CPM recalc's own
|
|
469
|
+
# broadcast) and extend the batch's delta cursor to cover it — a no-op
|
|
470
|
+
# today since `coalesce_sync_seq()` stamps every row touched in this
|
|
471
|
+
# apply with the SAME project-wide value the deleted row already bumped
|
|
472
|
+
# to (sequence.py: "a batch is either wholly before or wholly after any
|
|
473
|
+
# client checkpoint"), but `bump()` is a max() so this stays correct if
|
|
474
|
+
# that ever changes. The row's actual *content* still reaches an
|
|
475
|
+
# offline client only through the next regular delta pull — like any
|
|
476
|
+
# other row, sync_seq is what makes it match `sync_seq__gt=since` there.
|
|
477
|
+
result.events.append(("task_updated", str(resynced_parent.pk)))
|
|
478
|
+
result.bump(resynced_parent.sync_seq)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def apply_task_changes(
|
|
482
|
+
*,
|
|
483
|
+
project: Project,
|
|
484
|
+
request: Request,
|
|
485
|
+
role: int,
|
|
486
|
+
changes: dict[str, Any],
|
|
487
|
+
last_pulled_at: int | None = None,
|
|
488
|
+
) -> BatchApplyResult:
|
|
489
|
+
"""Apply a WatermelonDB-shaped delta for the ``tasks`` collection.
|
|
490
|
+
|
|
491
|
+
Processing order is created → updated → deleted so an update or delete that
|
|
492
|
+
references a row created earlier in the same batch resolves. Any row that
|
|
493
|
+
fails its RBAC check raises ``PermissionDenied``, which rolls the whole batch
|
|
494
|
+
back (all-or-nothing) — a batch carrying a forbidden op is rejected entirely.
|
|
495
|
+
|
|
496
|
+
A stale edit whose fields overlap a concurrent writer's is NOT an error: it is
|
|
497
|
+
collected in ``result.conflicts`` and skipped (mirroring the REST 409 rather
|
|
498
|
+
than aborting the batch), so the client reconciles just that row while the rest
|
|
499
|
+
commit. ``last_pulled_at`` is the batch's high-water mark from its last pull and
|
|
500
|
+
is the default per-row conflict base (a row may override it with ``base_version``).
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
ValidationError: malformed envelope or an unsupported collection key.
|
|
504
|
+
PermissionDenied: a row the caller may not write.
|
|
505
|
+
TaskWriteRefusal: a row ``TaskSerializer`` refuses with a structured body
|
|
506
|
+
(progress anchor, milestone rollup lock, zero-duration non-milestone,
|
|
507
|
+
BLOCK guardrail). An ``APIException`` (400), so the exception handler
|
|
508
|
+
renders the same body REST returns; like every refusal above it rolls
|
|
509
|
+
the whole batch back and is not recorded against the batch id.
|
|
510
|
+
"""
|
|
511
|
+
tasks = _validate_and_extract_tasks(changes)
|
|
512
|
+
|
|
513
|
+
ctx = _ApplyContext(
|
|
514
|
+
request=request,
|
|
515
|
+
project=project,
|
|
516
|
+
# suppress_owner_broadcast (#3643): ``ProjectSyncView._apply_and_record``
|
|
517
|
+
# (sync/views.py) coalesces every row this batch touches into ONE
|
|
518
|
+
# ``tasks_bulk_mutated`` broadcast per upload — added for #809, where
|
|
519
|
+
# emitting one ``async_to_sync(group_send)`` per row (up to
|
|
520
|
+
# ``TRUEPPM_SYNC_BATCH_MAX_ROWS``) overflowed the channel-layer inbox
|
|
521
|
+
# under a reconnect storm. That coarse event is built from
|
|
522
|
+
# ``applied.events``, which already carries every row an inline
|
|
523
|
+
# ``owners`` write applies to (``_apply_created_row``/
|
|
524
|
+
# ``_apply_updated_row`` append to ``result.events`` on the same
|
|
525
|
+
# successful save that also applies ``owners``) — so a per-entry
|
|
526
|
+
# ``assignment_*`` broadcast on top of it would be exactly the
|
|
527
|
+
# duplicate load ``TaskBulkView`` suppresses this for. Safe under the
|
|
528
|
+
# all-or-nothing shape too: this function runs inside one
|
|
529
|
+
# ``transaction.atomic()`` with no per-row savepoints, so any row
|
|
530
|
+
# failure rolls back the whole batch before the coarse broadcast is
|
|
531
|
+
# ever registered — there is no partial-commit case to under-cover.
|
|
532
|
+
serializer_context={
|
|
533
|
+
"request": request,
|
|
534
|
+
"caller_role": role,
|
|
535
|
+
"project": project,
|
|
536
|
+
"suppress_owner_broadcast": True,
|
|
537
|
+
},
|
|
538
|
+
existing_by_id=_fetch_existing_tasks(tasks, project),
|
|
539
|
+
last_pulled_at=last_pulled_at,
|
|
540
|
+
)
|
|
541
|
+
result = BatchApplyResult()
|
|
542
|
+
|
|
543
|
+
# --- created (upsert by client-generated id) ------------------------------
|
|
544
|
+
cross_project_ids = _cross_project_created_ids(tasks, ctx.existing_by_id, project)
|
|
545
|
+
for row in tasks.get("created", []) or []:
|
|
546
|
+
_apply_created_row(row, ctx, cross_project_ids, result)
|
|
547
|
+
|
|
548
|
+
# --- updated --------------------------------------------------------------
|
|
549
|
+
for row in tasks.get("updated", []) or []:
|
|
550
|
+
_apply_updated_row(row, ctx, result)
|
|
551
|
+
|
|
552
|
+
# --- deleted --------------------------------------------------------------
|
|
553
|
+
for del_id in tasks.get("deleted", []) or []:
|
|
554
|
+
_apply_deleted_row(del_id, ctx, result)
|
|
555
|
+
|
|
556
|
+
return result
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""URL patterns for the sync app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.urls import path
|
|
6
|
+
|
|
7
|
+
from trueppm_api.apps.sync.views import (
|
|
8
|
+
ProjectSyncView,
|
|
9
|
+
UserProgramSyncView,
|
|
10
|
+
WebSocketTicketView,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
urlpatterns = [
|
|
14
|
+
path("projects/<uuid:pk>/sync/", ProjectSyncView.as_view(), name="project-sync"),
|
|
15
|
+
path("sync/user/programs/", UserProgramSyncView.as_view(), name="user-program-sync"),
|
|
16
|
+
path("ws/ticket/", WebSocketTicketView.as_view(), name="ws-ticket"),
|
|
17
|
+
]
|