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,930 @@
|
|
|
1
|
+
"""REST views for the sync app — offline delta pull endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.db import IntegrityError, transaction
|
|
10
|
+
from django.db.models import BooleanField, Prefetch
|
|
11
|
+
from django.db.models.expressions import RawSQL
|
|
12
|
+
from drf_spectacular.types import OpenApiTypes
|
|
13
|
+
from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
|
|
14
|
+
from rest_framework import serializers, status
|
|
15
|
+
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
|
|
16
|
+
from rest_framework.permissions import IsAuthenticated
|
|
17
|
+
from rest_framework.request import Request
|
|
18
|
+
from rest_framework.response import Response
|
|
19
|
+
from rest_framework.throttling import BaseThrottle, ScopedRateThrottle
|
|
20
|
+
from rest_framework.views import APIView
|
|
21
|
+
|
|
22
|
+
from trueppm_api.apps.access.models import ProgramMembership, ProjectMembership, Role
|
|
23
|
+
from trueppm_api.apps.access.permissions import _membership_role
|
|
24
|
+
from trueppm_api.apps.idempotency.mixins import IdempotencyMixin
|
|
25
|
+
from trueppm_api.apps.integrations.models import TaskLink
|
|
26
|
+
from trueppm_api.apps.projects.models import (
|
|
27
|
+
Calendar,
|
|
28
|
+
Dependency,
|
|
29
|
+
Label,
|
|
30
|
+
Program,
|
|
31
|
+
Project,
|
|
32
|
+
RetroActionItem,
|
|
33
|
+
RetroVisibility,
|
|
34
|
+
Risk,
|
|
35
|
+
Sprint,
|
|
36
|
+
SprintRetro,
|
|
37
|
+
Task,
|
|
38
|
+
TaskCustomFieldValue,
|
|
39
|
+
TaskRecurrenceRule,
|
|
40
|
+
TaskRelation,
|
|
41
|
+
TaskSuggestedAssignee,
|
|
42
|
+
)
|
|
43
|
+
from trueppm_api.apps.sync.pagination import SyncCursor, SyncSource, paginate_changes
|
|
44
|
+
from trueppm_api.apps.sync.serializers import (
|
|
45
|
+
SyncCalendarSerializer,
|
|
46
|
+
SyncDependencySerializer,
|
|
47
|
+
SyncLabelSerializer,
|
|
48
|
+
SyncMembershipSerializer,
|
|
49
|
+
SyncProgramMembershipSerializer,
|
|
50
|
+
SyncProgramSerializer,
|
|
51
|
+
SyncProjectSerializer,
|
|
52
|
+
SyncPullResponseSerializer,
|
|
53
|
+
SyncRetroActionItemSerializer,
|
|
54
|
+
SyncRiskSerializer,
|
|
55
|
+
SyncSprintRetroSerializer,
|
|
56
|
+
SyncSprintSerializer,
|
|
57
|
+
SyncTaskLinkSerializer,
|
|
58
|
+
SyncTaskRecurrenceRuleSerializer,
|
|
59
|
+
SyncTaskRelationSerializer,
|
|
60
|
+
SyncTaskSerializer,
|
|
61
|
+
SyncTaskSuggestedAssigneeSerializer,
|
|
62
|
+
SyncTimeEntrySerializer,
|
|
63
|
+
SyncUploadRequestSerializer,
|
|
64
|
+
SyncUploadResponseSerializer,
|
|
65
|
+
)
|
|
66
|
+
from trueppm_api.apps.sync.ws_auth import TICKET_TTL_SECONDS, issue_ticket
|
|
67
|
+
from trueppm_api.apps.timetracking.models import TimeEntry
|
|
68
|
+
|
|
69
|
+
if TYPE_CHECKING:
|
|
70
|
+
from django.contrib.auth.models import User as _User
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class WebSocketTicketView(IdempotencyMixin, APIView):
|
|
74
|
+
"""Mint a short-lived, single-use ticket for the WebSocket handshake (ADR-0141).
|
|
75
|
+
|
|
76
|
+
Browsers cannot set an ``Authorization`` header on a WebSocket upgrade, so the
|
|
77
|
+
credential must ride in the URL. A raw JWT there leaks into every access log
|
|
78
|
+
(#818); a ticket that is single-use and expires in
|
|
79
|
+
:data:`~trueppm_api.apps.sync.ws_auth.TICKET_TTL_SECONDS` seconds is useless
|
|
80
|
+
once logged. The client POSTs here, then opens the socket with ``?ticket=``.
|
|
81
|
+
|
|
82
|
+
Authentication only — authorization (project membership/role) is enforced by
|
|
83
|
+
the consumer on connect. No project scope is required to mint a ticket.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
permission_classes = [IsAuthenticated]
|
|
87
|
+
throttle_classes = [ScopedRateThrottle]
|
|
88
|
+
throttle_scope = "ws_ticket"
|
|
89
|
+
# Stateless mint — the generic Idempotency-Key path adds nothing.
|
|
90
|
+
idempotency_exempt = True
|
|
91
|
+
|
|
92
|
+
@extend_schema(
|
|
93
|
+
summary="Issue a WebSocket connection ticket",
|
|
94
|
+
responses={
|
|
95
|
+
200: inline_serializer(
|
|
96
|
+
name="WebSocketTicketResponse",
|
|
97
|
+
fields={
|
|
98
|
+
"ticket": serializers.CharField(
|
|
99
|
+
help_text="Single-use opaque ticket to pass as the "
|
|
100
|
+
"`?ticket=` query parameter when opening the socket.",
|
|
101
|
+
),
|
|
102
|
+
"expires_in": serializers.IntegerField(
|
|
103
|
+
help_text="Seconds until the ticket expires.",
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
description="Issue a single-use WebSocket connection ticket (ADR-0141).",
|
|
109
|
+
)
|
|
110
|
+
def post(self, request: Request) -> Response:
|
|
111
|
+
ticket = issue_ticket(str(request.user.pk))
|
|
112
|
+
return Response({"ticket": ticket, "expires_in": TICKET_TTL_SECONDS})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ProjectSyncView(IdempotencyMixin, APIView):
|
|
116
|
+
"""Pull-only delta sync endpoint for the mobile offline store.
|
|
117
|
+
|
|
118
|
+
Returns all rows (live and soft-deleted) whose ``sync_seq`` is strictly
|
|
119
|
+
greater than `since` for the given project, formatted for WatermelonDB's
|
|
120
|
+
synchronize() helper.
|
|
121
|
+
|
|
122
|
+
``sync_seq`` — not ``server_version`` — is the cursor (ADR-0686, #2491). It is
|
|
123
|
+
drawn from the project's own monotonic sequence, so every synced row in the
|
|
124
|
+
project is totally ordered against every other one and the project-wide
|
|
125
|
+
maximum is a meaningful checkpoint. ``server_version`` counts a single row's
|
|
126
|
+
saves; comparing it against that maximum let a frequently-edited row raise the
|
|
127
|
+
checkpoint above every other row's counter and drop those rows from the delta
|
|
128
|
+
permanently.
|
|
129
|
+
|
|
130
|
+
The response ``timestamp`` is read *before* the delta queries, so a write that
|
|
131
|
+
lands during the pull always draws a cursor above it. Such a row is delivered by
|
|
132
|
+
this response or the next one — never dropped — at the cost of an occasional
|
|
133
|
+
re-delivery, which WatermelonDB's upsert absorbs. Clients therefore need no
|
|
134
|
+
retry or reconciliation logic for a mid-pull write; adopt ``timestamp`` as the
|
|
135
|
+
next ``since`` and the delta is complete. It is read once per *paging session*,
|
|
136
|
+
not per request: continuation pages echo the value pinned in the cursor (#2568).
|
|
137
|
+
See the comment at the watermark read for why the ordering, and not a transaction
|
|
138
|
+
isolation level, is what guarantees this.
|
|
139
|
+
|
|
140
|
+
Usage:
|
|
141
|
+
GET /api/v1/projects/{pk}/sync/?since=0
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Exempt from the generic Idempotency-Key path (ADR-0170): the upload POST is already
|
|
145
|
+
# idempotent by client_batch_id (SyncBatch dedup, ADR-0082); the generic header would
|
|
146
|
+
# be redundant and the sync protocol uses its own batch id.
|
|
147
|
+
idempotency_exempt = True
|
|
148
|
+
permission_classes = [IsAuthenticated]
|
|
149
|
+
|
|
150
|
+
@extend_schema(
|
|
151
|
+
summary="Pull the offline sync delta for a project",
|
|
152
|
+
parameters=[
|
|
153
|
+
OpenApiParameter(
|
|
154
|
+
name="since",
|
|
155
|
+
type=OpenApiTypes.INT,
|
|
156
|
+
location=OpenApiParameter.QUERY,
|
|
157
|
+
required=False,
|
|
158
|
+
default=0,
|
|
159
|
+
description=(
|
|
160
|
+
"WatermelonDB high-water mark — an opaque cursor, not a row "
|
|
161
|
+
"version. Returns only rows changed strictly after this point. "
|
|
162
|
+
"Omit (or pass `0`) for a full initial sync; passing the "
|
|
163
|
+
"previous response's `timestamp` fetches just the delta since "
|
|
164
|
+
"then. Omitting it silently triggers a full re-sync."
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
OpenApiParameter(
|
|
168
|
+
name="cursor",
|
|
169
|
+
type=OpenApiTypes.STR,
|
|
170
|
+
location=OpenApiParameter.QUERY,
|
|
171
|
+
required=False,
|
|
172
|
+
description=(
|
|
173
|
+
"Opaque pagination continuation token (#1013). Omit on the "
|
|
174
|
+
"first request; on each subsequent request pass the previous "
|
|
175
|
+
"response's `next_cursor`. Keep `since` constant for the whole "
|
|
176
|
+
"paging session. Loop until `next_cursor` is null / `has_more` "
|
|
177
|
+
"is false, then adopt `timestamp` as the next `since`."
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
OpenApiParameter(
|
|
181
|
+
name="page_size",
|
|
182
|
+
type=OpenApiTypes.INT,
|
|
183
|
+
location=OpenApiParameter.QUERY,
|
|
184
|
+
required=False,
|
|
185
|
+
description=(
|
|
186
|
+
"Maximum rows returned across all collections in one page. "
|
|
187
|
+
"Defaults to `TRUEPPM_SYNC_PULL_PAGE_SIZE`; clamped to "
|
|
188
|
+
"`TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE`."
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
],
|
|
192
|
+
responses={200: SyncPullResponseSerializer},
|
|
193
|
+
)
|
|
194
|
+
def get(self, request: Request, pk: str) -> Response:
|
|
195
|
+
# Validate `since` parameter.
|
|
196
|
+
since_raw = request.query_params.get("since", "0")
|
|
197
|
+
try:
|
|
198
|
+
since = int(since_raw)
|
|
199
|
+
if since < 0:
|
|
200
|
+
raise ValueError
|
|
201
|
+
except (ValueError, TypeError) as err:
|
|
202
|
+
raise ValidationError({"since": "Must be a non-negative integer."}) from err
|
|
203
|
+
|
|
204
|
+
# Validate the optional continuation cursor and page size (#1013).
|
|
205
|
+
cursor_raw = request.query_params.get("cursor")
|
|
206
|
+
cursor = SyncCursor.decode(cursor_raw) if cursor_raw else None
|
|
207
|
+
page_size = self._resolve_page_size(request)
|
|
208
|
+
|
|
209
|
+
# Resolve project — 404 if missing or soft-deleted.
|
|
210
|
+
try:
|
|
211
|
+
project = Project.objects.get(pk=pk, is_deleted=False)
|
|
212
|
+
except Project.DoesNotExist as err:
|
|
213
|
+
raise NotFound("Project not found.") from err
|
|
214
|
+
|
|
215
|
+
# RBAC — any project member (Viewer+) may pull.
|
|
216
|
+
caller_role = _membership_role(request, project.pk)
|
|
217
|
+
if caller_role is None:
|
|
218
|
+
from rest_framework.exceptions import PermissionDenied
|
|
219
|
+
|
|
220
|
+
raise PermissionDenied("You must be a member of this project.")
|
|
221
|
+
|
|
222
|
+
# Read the high-water mark BEFORE the delta queries. This ORDERING is the
|
|
223
|
+
# whole guarantee — not an isolation level. ATOMIC_REQUESTS does wrap this
|
|
224
|
+
# request in a transaction, but at PostgreSQL's default READ COMMITTED, and
|
|
225
|
+
# nothing here or in settings raises that: every statement below still takes
|
|
226
|
+
# its own fresh snapshot, so being in a transaction pins nothing. REPEATABLE
|
|
227
|
+
# READ would add nothing either, and an earlier version of this comment
|
|
228
|
+
# claimed a wrapper that never existed at all (#2613).
|
|
229
|
+
#
|
|
230
|
+
# Two facts close the TOCTOU window:
|
|
231
|
+
#
|
|
232
|
+
# 1. A write landing after this read draws sync_seq > timestamp, so it is
|
|
233
|
+
# either carried by this response anyway (harmless — upsert) or caught
|
|
234
|
+
# by the next pull, whose `since` is this timestamp. Reading the
|
|
235
|
+
# watermark first errs toward re-delivery; reading it last is lossy.
|
|
236
|
+
# 2. A write that already committed is fully visible. Allocation UPDATEs
|
|
237
|
+
# the project row and holds its write lock to commit (sync/sequence.py),
|
|
238
|
+
# so allocations are totally ordered by commit: observing
|
|
239
|
+
# last_sync_version == W means every allocation <= W has committed —
|
|
240
|
+
# each one together with the row it stamped.
|
|
241
|
+
#
|
|
242
|
+
# Fact 2 rests on a WRITER-side precondition: the allocator bump and the row
|
|
243
|
+
# write must land in one transaction. ATOMIC_REQUESTS supplies it for every
|
|
244
|
+
# HTTP write and the batch writers open an explicit atomic(); a synced write
|
|
245
|
+
# in autocommit would violate it, and no reader isolation level can
|
|
246
|
+
# compensate — a snapshot cannot reveal an uncommitted row whose already-
|
|
247
|
+
# committed allocator bump the reader can see. Tracked in #2617.
|
|
248
|
+
#
|
|
249
|
+
# The value is taken off the Project row fetched above rather than re-read,
|
|
250
|
+
# so the snapshot instant is the RBAC fetch — earlier still, which only
|
|
251
|
+
# widens the margin. Do NOT add a refresh_from_db() here: re-reading the
|
|
252
|
+
# allocator after the delta queries is exactly the lossy order, and would
|
|
253
|
+
# publish a checkpoint over rows this response could not carry.
|
|
254
|
+
#
|
|
255
|
+
# One watermark per *session*, not per request (#2568) — which preserves the
|
|
256
|
+
# ordering above rather than weakening it: a continuation page's checkpoint was
|
|
257
|
+
# read before page 1's delta queries, so it precedes every query in the drain.
|
|
258
|
+
# On a continuation page the checkpoint is read back off the cursor: recomputing
|
|
259
|
+
# it would
|
|
260
|
+
# publish a value above rows written mid-drain into a collection the pager
|
|
261
|
+
# had already passed, and the client adopting the last page's `timestamp`
|
|
262
|
+
# would filter those rows out of every future pull. Pinning page 1's value
|
|
263
|
+
# errs the safe way — such rows simply arrive on the next pull, and rows
|
|
264
|
+
# this drain did deliver above the pin are re-delivered under upsert.
|
|
265
|
+
#
|
|
266
|
+
# The cursor is unsigned, so `w` is also ceiling-checked against the live
|
|
267
|
+
# sequence. `i` and `v` are stream *positions* the pager re-derives and
|
|
268
|
+
# bounds naturally, but `w` is a checkpoint the client is told to persist
|
|
269
|
+
# and echo back as `since` indefinitely — a junk value is permanent for
|
|
270
|
+
# that device rather than transient. The check is free here (the watermark
|
|
271
|
+
# is an attribute of the project row already fetched above) and rejects
|
|
272
|
+
# nothing legitimate, because the sequence only ever increases. It is
|
|
273
|
+
# deliberately NOT mirrored in UserProgramSyncView, whose watermark costs
|
|
274
|
+
# two aggregates over the caller's whole accessible set: paying them per
|
|
275
|
+
# page would reinstate the O(pages x rows) cost this fix removed, to guard
|
|
276
|
+
# a value that can only ever corrupt the tampering client's own cursor.
|
|
277
|
+
if cursor is not None:
|
|
278
|
+
timestamp = cursor.watermark
|
|
279
|
+
if timestamp > self._watermark(project):
|
|
280
|
+
raise ValidationError({"cursor": "Malformed pagination cursor."})
|
|
281
|
+
else:
|
|
282
|
+
timestamp = self._watermark(project)
|
|
283
|
+
|
|
284
|
+
# Retros are visibility-gated per ADR-0071 §3. A VIEWER on a TEAM_ONLY
|
|
285
|
+
# retro does not receive the retro's raw notes — the sync filters them
|
|
286
|
+
# out at the queryset level. The sync protocol delivers tombstones for
|
|
287
|
+
# visibility-removed rows so WatermelonDB can drop them client-side.
|
|
288
|
+
retro_qs = SprintRetro.objects.filter(sprint__project=project, sync_seq__gt=since)
|
|
289
|
+
if caller_role < Role.MEMBER:
|
|
290
|
+
retro_qs = retro_qs.exclude(team_visibility=RetroVisibility.TEAM_ONLY)
|
|
291
|
+
|
|
292
|
+
retro_action_item_qs = RetroActionItem.objects.filter(
|
|
293
|
+
retro__sprint__project=project, sync_seq__gt=since
|
|
294
|
+
).select_related("retro")
|
|
295
|
+
if caller_role < Role.MEMBER:
|
|
296
|
+
retro_action_item_qs = retro_action_item_qs.exclude(
|
|
297
|
+
retro__team_visibility=RetroVisibility.TEAM_ONLY
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Ordered sync sources: (collection, base queryset, serializer). Each base
|
|
301
|
+
# queryset carries the project scope, the `sync_seq__gt=since` floor,
|
|
302
|
+
# and any RBAC/visibility filters. The pager (#1013) drains them in this
|
|
303
|
+
# fixed order, keyset-seeking on (sync_seq, id) within each, so the
|
|
304
|
+
# response is bounded by page_size without skipping or duplicating rows.
|
|
305
|
+
# The order is stable across pages — do not reorder without a protocol bump.
|
|
306
|
+
sources: list[SyncSource] = [
|
|
307
|
+
(
|
|
308
|
+
"projects",
|
|
309
|
+
Project.objects.filter(pk=project.pk, sync_seq__gt=since),
|
|
310
|
+
SyncProjectSerializer,
|
|
311
|
+
),
|
|
312
|
+
(
|
|
313
|
+
"tasks",
|
|
314
|
+
# is_phase annotation (ADR-0293): EXISTS a direct non-subtask child.
|
|
315
|
+
# Mirrors the projects app's annotate_tasks_queryset is_phase shape so
|
|
316
|
+
# offline clients receive the same computed rollup flag. Static SQL
|
|
317
|
+
# literal, empty params — no user input interpolated; the ltree
|
|
318
|
+
# operator can't be expressed in the ORM.
|
|
319
|
+
Task.objects.filter(project=project, sync_seq__gt=since)
|
|
320
|
+
.annotate(
|
|
321
|
+
# nosemgrep: avoid-raw-sql
|
|
322
|
+
is_phase=RawSQL(
|
|
323
|
+
"EXISTS("
|
|
324
|
+
" SELECT 1 FROM projects_task c"
|
|
325
|
+
" WHERE c.project_id = projects_task.project_id"
|
|
326
|
+
" AND c.is_deleted = false"
|
|
327
|
+
" AND c.is_subtask = false"
|
|
328
|
+
" AND c.id != projects_task.id"
|
|
329
|
+
" AND c.wbs_path IS NOT NULL"
|
|
330
|
+
" AND projects_task.wbs_path IS NOT NULL"
|
|
331
|
+
" AND c.wbs_path ~ (projects_task.wbs_path::text || '.*{1}')::lquery"
|
|
332
|
+
")",
|
|
333
|
+
[],
|
|
334
|
+
output_field=BooleanField(),
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
.prefetch_related(
|
|
338
|
+
# label_ids array (ADR-0400) — filter tombstoned labels so a
|
|
339
|
+
# deleted label never rides a task's sync payload; O(1) per page.
|
|
340
|
+
Prefetch("labels", queryset=Label.objects.filter(is_deleted=False)),
|
|
341
|
+
# custom_fields map (#2143, ADR-0528) rides the task like label_ids;
|
|
342
|
+
# select_related feeds the field-type resolver + person avatar O(1).
|
|
343
|
+
Prefetch(
|
|
344
|
+
"custom_field_values",
|
|
345
|
+
queryset=TaskCustomFieldValue.objects.select_related("field", "value_user"),
|
|
346
|
+
),
|
|
347
|
+
),
|
|
348
|
+
SyncTaskSerializer,
|
|
349
|
+
),
|
|
350
|
+
(
|
|
351
|
+
"dependencies",
|
|
352
|
+
# Same-project edges only. Cross-project edges (ADR-0120) are
|
|
353
|
+
# deliberately excluded from the single-project offline delta:
|
|
354
|
+
# their successor task lives in a project this client may not have
|
|
355
|
+
# synced, and the offline WASM recompute is single-project. Offline
|
|
356
|
+
# cross-project scheduling arrives with the program-sync slice
|
|
357
|
+
# (D3/D4); until then the sync payload stays byte-identical to the
|
|
358
|
+
# pre-cross-project behaviour.
|
|
359
|
+
Dependency.objects.filter(
|
|
360
|
+
predecessor__project=project,
|
|
361
|
+
successor__project=project,
|
|
362
|
+
sync_seq__gt=since,
|
|
363
|
+
).select_related("predecessor"),
|
|
364
|
+
SyncDependencySerializer,
|
|
365
|
+
),
|
|
366
|
+
(
|
|
367
|
+
"calendars",
|
|
368
|
+
# prefetch_related("exceptions") feeds the nested read-only
|
|
369
|
+
# exceptions array on SyncCalendarSerializer (ADR-0193) without a
|
|
370
|
+
# per-calendar lazy load — parity with CalendarViewSet and
|
|
371
|
+
# N+1-safe for the future program-sync multi-calendar slice.
|
|
372
|
+
Calendar.objects.filter(
|
|
373
|
+
pk=project.calendar_id, sync_seq__gt=since
|
|
374
|
+
).prefetch_related("exceptions")
|
|
375
|
+
if project.calendar_id
|
|
376
|
+
else Calendar.objects.none(),
|
|
377
|
+
SyncCalendarSerializer,
|
|
378
|
+
),
|
|
379
|
+
(
|
|
380
|
+
"memberships",
|
|
381
|
+
ProjectMembership.objects.filter(project=project, sync_seq__gt=since),
|
|
382
|
+
SyncMembershipSerializer,
|
|
383
|
+
),
|
|
384
|
+
(
|
|
385
|
+
"risks",
|
|
386
|
+
Risk.objects.filter(project=project, sync_seq__gt=since).prefetch_related("tasks"),
|
|
387
|
+
SyncRiskSerializer,
|
|
388
|
+
),
|
|
389
|
+
(
|
|
390
|
+
"sprints",
|
|
391
|
+
Sprint.objects.filter(project=project, sync_seq__gt=since),
|
|
392
|
+
SyncSprintSerializer,
|
|
393
|
+
),
|
|
394
|
+
("sprint_retros", retro_qs, SyncSprintRetroSerializer),
|
|
395
|
+
("retro_action_items", retro_action_item_qs, SyncRetroActionItemSerializer),
|
|
396
|
+
(
|
|
397
|
+
"task_suggested_assignees",
|
|
398
|
+
TaskSuggestedAssignee.objects.filter(task__project=project, sync_seq__gt=since),
|
|
399
|
+
SyncTaskSuggestedAssigneeSerializer,
|
|
400
|
+
),
|
|
401
|
+
(
|
|
402
|
+
"task_links",
|
|
403
|
+
TaskLink.objects.filter(task__project=project, sync_seq__gt=since),
|
|
404
|
+
SyncTaskLinkSerializer,
|
|
405
|
+
),
|
|
406
|
+
(
|
|
407
|
+
"task_recurrence_rules",
|
|
408
|
+
TaskRecurrenceRule.objects.filter(
|
|
409
|
+
task__project=project, sync_seq__gt=since
|
|
410
|
+
).select_related("task"),
|
|
411
|
+
SyncTaskRecurrenceRuleSerializer,
|
|
412
|
+
),
|
|
413
|
+
(
|
|
414
|
+
# Time entries are scoped to the CALLER's own rows (ADR-0185 §6): the
|
|
415
|
+
# per-project delta never leaks a colleague's entries, mirroring the
|
|
416
|
+
# /me/ REST surface's non-surveillance discipline. A soft-deleted entry
|
|
417
|
+
# yields a tombstone through the standard _collect path.
|
|
418
|
+
"time_entries",
|
|
419
|
+
TimeEntry.objects.filter(
|
|
420
|
+
task__project=project,
|
|
421
|
+
user=cast("_User", request.user),
|
|
422
|
+
sync_seq__gt=since,
|
|
423
|
+
).select_related("task"),
|
|
424
|
+
SyncTimeEntrySerializer,
|
|
425
|
+
),
|
|
426
|
+
(
|
|
427
|
+
# Label catalog (ADR-0400) — appended to preserve the stable protocol
|
|
428
|
+
# order. Assignment is NOT a source here; it rides Task.label_ids.
|
|
429
|
+
"labels",
|
|
430
|
+
Label.objects.filter(project=project, sync_seq__gt=since),
|
|
431
|
+
SyncLabelSerializer,
|
|
432
|
+
),
|
|
433
|
+
(
|
|
434
|
+
# Informational task relations (ADR-0455) — appended LAST to preserve
|
|
435
|
+
# the stable protocol order. Same-project relations only: a
|
|
436
|
+
# cross-project relation's counterpart task may live in a project
|
|
437
|
+
# this client has not synced (mirrors the Dependency same-project
|
|
438
|
+
# scoping above). Pull-only, like Dependency — never in the upload
|
|
439
|
+
# WRITABLE_COLLECTIONS. The delta joins via ``source`` since
|
|
440
|
+
# TaskRelation carries no direct project FK.
|
|
441
|
+
"task_relations",
|
|
442
|
+
TaskRelation.objects.filter(
|
|
443
|
+
source__project=project,
|
|
444
|
+
target__project=project,
|
|
445
|
+
sync_seq__gt=since,
|
|
446
|
+
).select_related("source"),
|
|
447
|
+
SyncTaskRelationSerializer,
|
|
448
|
+
),
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
changes, next_cursor, has_more = paginate_changes(
|
|
452
|
+
sources,
|
|
453
|
+
cursor=cursor,
|
|
454
|
+
page_size=page_size,
|
|
455
|
+
collect=self._collect,
|
|
456
|
+
watermark=timestamp,
|
|
457
|
+
version_field="sync_seq",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return Response(
|
|
461
|
+
{
|
|
462
|
+
"changes": changes,
|
|
463
|
+
"timestamp": timestamp,
|
|
464
|
+
"next_cursor": next_cursor.encode() if next_cursor is not None else None,
|
|
465
|
+
"has_more": has_more,
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@staticmethod
|
|
470
|
+
def _resolve_page_size(request: Request) -> int:
|
|
471
|
+
"""Clamp the requested ``page_size`` to ``[1, MAX]`` with a sane default (#1013).
|
|
472
|
+
|
|
473
|
+
A missing or non-numeric value falls back to the default rather than 400ing:
|
|
474
|
+
the pager is a transport optimization, not a contract the client must satisfy.
|
|
475
|
+
"""
|
|
476
|
+
default = getattr(settings, "TRUEPPM_SYNC_PULL_PAGE_SIZE", 1000)
|
|
477
|
+
maximum = getattr(settings, "TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE", 5000)
|
|
478
|
+
raw = request.query_params.get("page_size")
|
|
479
|
+
if raw is None:
|
|
480
|
+
return default
|
|
481
|
+
try:
|
|
482
|
+
requested = int(raw)
|
|
483
|
+
except (ValueError, TypeError):
|
|
484
|
+
return default
|
|
485
|
+
return max(1, min(requested, maximum))
|
|
486
|
+
|
|
487
|
+
def get_throttles(self) -> list[BaseThrottle]:
|
|
488
|
+
"""POST gets the dedicated upload throttle; GET keeps the default `user`
|
|
489
|
+
scope (#2939 — same defect class #2878 fixed in four other views: a bare
|
|
490
|
+
list here *replaces* ``DEFAULT_THROTTLE_CLASSES`` instead of adding to it,
|
|
491
|
+
so this branch resolved to no throttle at all on the delta-export pull,
|
|
492
|
+
the largest read surface in the API). Mobile sync is high-frequency by
|
|
493
|
+
design, so it is bounded by the generous default (1000/min per account,
|
|
494
|
+
``TRUEPPM_THROTTLE_USER_RATE``) rather than a dedicated tighter scope —
|
|
495
|
+
that is a real decision, not an oversight, and now it is enforced.
|
|
496
|
+
"""
|
|
497
|
+
if self.request.method == "POST":
|
|
498
|
+
from trueppm_api.apps.sync.throttles import SyncUploadThrottle
|
|
499
|
+
|
|
500
|
+
return [SyncUploadThrottle()]
|
|
501
|
+
return super().get_throttles()
|
|
502
|
+
|
|
503
|
+
@extend_schema(
|
|
504
|
+
request=SyncUploadRequestSerializer,
|
|
505
|
+
responses={
|
|
506
|
+
200: SyncUploadResponseSerializer,
|
|
507
|
+
409: OpenApiTypes.OBJECT,
|
|
508
|
+
},
|
|
509
|
+
description=(
|
|
510
|
+
"Upload a WatermelonDB delta batch. Carries a client_batch_id for "
|
|
511
|
+
"all-or-nothing, idempotent replay (ADR-0082)."
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
def post(self, request: Request, pk: str) -> Response:
|
|
515
|
+
"""Upload a WatermelonDB delta batch with transactional atomicity (ADR-0082).
|
|
516
|
+
|
|
517
|
+
The batch carries a client-generated ``client_batch_id``. The first
|
|
518
|
+
request to win the unique-constraint race applies the delta and records
|
|
519
|
+
the batch + its response in one atomic transaction (all-or-nothing); a
|
|
520
|
+
retry carrying the same id within the freshness window replays the stored
|
|
521
|
+
response without re-applying. See ADR-0082 §D for the full algorithm.
|
|
522
|
+
"""
|
|
523
|
+
from trueppm_api.apps.access.models import Role
|
|
524
|
+
from trueppm_api.apps.sync.models import SyncBatch, SyncBatchStatus
|
|
525
|
+
from trueppm_api.apps.sync.throttles import sync_batch_concurrency_slot
|
|
526
|
+
|
|
527
|
+
envelope = SyncUploadRequestSerializer(data=request.data)
|
|
528
|
+
envelope.is_valid(raise_exception=True)
|
|
529
|
+
client_batch_id = envelope.validated_data["client_batch_id"]
|
|
530
|
+
# The high-water mark the client last pulled — the default per-row base for the
|
|
531
|
+
# field-level conflict guard (#1718). Advisory only: absent → last-writer-wins.
|
|
532
|
+
last_pulled_at = envelope.validated_data.get("last_pulled_at")
|
|
533
|
+
# Feed the RAW changes map (not envelope.validated_data["changes"]) to
|
|
534
|
+
# apply_task_changes so it stays the single authority for the writable-
|
|
535
|
+
# collection whitelist (ADR-0082 §B). The typed nested serializer (#786)
|
|
536
|
+
# documents the shape but ignores unknown collection keys; passing the raw
|
|
537
|
+
# map keeps the explicit "unsupported collection → 400" guard byte-identical
|
|
538
|
+
# for existing clients rather than silently dropping the foreign collection.
|
|
539
|
+
# The envelope serializer above already rejected a non-mapping body with a
|
|
540
|
+
# 400 ("Expected a dictionary, but got list"), so the raw body is provably a
|
|
541
|
+
# mapping by here — narrowed rather than re-guarded.
|
|
542
|
+
changes = cast("dict[str, Any]", request.data)["changes"]
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
project = Project.objects.get(pk=pk, is_deleted=False)
|
|
546
|
+
except Project.DoesNotExist as err:
|
|
547
|
+
raise NotFound("Project not found.") from err
|
|
548
|
+
|
|
549
|
+
# RBAC: only project members with a write role may push. Per-row
|
|
550
|
+
# ownership (Member edits own tasks; Scheduler cannot edit content) is
|
|
551
|
+
# enforced inside apply_task_changes, mirroring IsProjectMemberWriteOrOwn.
|
|
552
|
+
role = _membership_role(request, project.pk)
|
|
553
|
+
if role is None:
|
|
554
|
+
raise PermissionDenied("You must be a member of this project.")
|
|
555
|
+
if role < Role.MEMBER:
|
|
556
|
+
raise PermissionDenied("You need at least Team Member role to upload changes.")
|
|
557
|
+
|
|
558
|
+
# Archived projects are hard read-only (#530) — mirror IsProjectNotArchived,
|
|
559
|
+
# which gates every REST write. Without this, the upload path would be a
|
|
560
|
+
# back-channel to mutate an archived project.
|
|
561
|
+
if project.is_archived:
|
|
562
|
+
raise PermissionDenied(
|
|
563
|
+
"This project is archived and cannot be modified. Unarchive it first."
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
ttl = getattr(settings, "TRUEPPM_SYNC_BATCH_RETENTION_HOURS", 24)
|
|
567
|
+
|
|
568
|
+
# Fast path: a fresh, completed duplicate replays its stored response.
|
|
569
|
+
# Scoped to (project, actor) so a member can never replay another
|
|
570
|
+
# project's — or another *user's* — batch by reusing its client_batch_id.
|
|
571
|
+
# The stored response carries that actor's task ids, server_versions, and
|
|
572
|
+
# watermark; cross-actor replay would leak it (#894, #887 IDOR guard).
|
|
573
|
+
existing = SyncBatch.objects.filter( # type: ignore[misc]
|
|
574
|
+
project=project, actor_user=request.user, client_batch_id=client_batch_id
|
|
575
|
+
).first()
|
|
576
|
+
if (
|
|
577
|
+
existing is not None
|
|
578
|
+
and existing.status == SyncBatchStatus.COMPLETED
|
|
579
|
+
and existing.is_fresh(ttl_hours=ttl)
|
|
580
|
+
):
|
|
581
|
+
return Response(existing.response_body, status=existing.response_status)
|
|
582
|
+
|
|
583
|
+
# Bound simultaneously in-flight heavy batch applies for this user (#1756).
|
|
584
|
+
# Wraps the whole apply — including the IntegrityError re-run — so one
|
|
585
|
+
# logical upload holds exactly one slot. Placed AFTER the fast-path replay
|
|
586
|
+
# so a cheap idempotent retry never consumes a slot. Raises 429 (Throttled)
|
|
587
|
+
# when the per-user concurrency cap is exceeded; fails open on Redis error.
|
|
588
|
+
with sync_batch_concurrency_slot(request.user.pk):
|
|
589
|
+
try:
|
|
590
|
+
return self._apply_and_record(
|
|
591
|
+
request, project, role, client_batch_id, changes, last_pulled_at
|
|
592
|
+
)
|
|
593
|
+
except IntegrityError:
|
|
594
|
+
# A concurrent duplicate committed between our check and create, or a
|
|
595
|
+
# stale row exists. Re-fetch (project + actor-scoped) and resolve.
|
|
596
|
+
existing = SyncBatch.objects.filter( # type: ignore[misc]
|
|
597
|
+
project=project, actor_user=request.user, client_batch_id=client_batch_id
|
|
598
|
+
).first()
|
|
599
|
+
if existing is None:
|
|
600
|
+
raise # genuinely unexpected — surface it
|
|
601
|
+
if existing.is_fresh(ttl_hours=ttl):
|
|
602
|
+
if existing.status == SyncBatchStatus.COMPLETED:
|
|
603
|
+
return Response(existing.response_body, status=existing.response_status)
|
|
604
|
+
# Fresh but still pending: a true concurrent race whose winner has
|
|
605
|
+
# not committed yet. Ask the client to retry — the next attempt
|
|
606
|
+
# hits the fast-path replay.
|
|
607
|
+
return Response(
|
|
608
|
+
{"detail": "This batch is already being processed; retry shortly."},
|
|
609
|
+
status=status.HTTP_409_CONFLICT,
|
|
610
|
+
)
|
|
611
|
+
# Expired row is blocking a re-run: drop it and apply once more. After
|
|
612
|
+
# the freshness window the same client_batch_id is allowed to re-run.
|
|
613
|
+
existing.delete()
|
|
614
|
+
return self._apply_and_record(
|
|
615
|
+
request, project, role, client_batch_id, changes, last_pulled_at
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
def _apply_and_record(
|
|
619
|
+
self,
|
|
620
|
+
request: Request,
|
|
621
|
+
project: Project,
|
|
622
|
+
role: int,
|
|
623
|
+
client_batch_id: Any,
|
|
624
|
+
changes: dict[str, Any],
|
|
625
|
+
last_pulled_at: int | None = None,
|
|
626
|
+
) -> Response:
|
|
627
|
+
"""Apply the delta and snapshot the response inside one atomic batch.
|
|
628
|
+
|
|
629
|
+
The SyncBatch row is the **first** write so the unique constraint
|
|
630
|
+
serializes concurrent duplicates. Any failure (RBAC, validation, DB)
|
|
631
|
+
rolls back the row writes *and* the batch row together — nothing commits,
|
|
632
|
+
and the client re-uploads the whole batch under the same id.
|
|
633
|
+
"""
|
|
634
|
+
from trueppm_api.apps.scheduling.services import enqueue_recalculate
|
|
635
|
+
from trueppm_api.apps.sync.broadcast import broadcast_board_event
|
|
636
|
+
from trueppm_api.apps.sync.models import SyncBatch, SyncBatchStatus
|
|
637
|
+
from trueppm_api.apps.sync.sequence import coalesce_sync_seq
|
|
638
|
+
from trueppm_api.apps.sync.upload import apply_task_changes
|
|
639
|
+
|
|
640
|
+
with transaction.atomic():
|
|
641
|
+
batch = SyncBatch.objects.create(
|
|
642
|
+
client_batch_id=client_batch_id,
|
|
643
|
+
project=project,
|
|
644
|
+
actor_user=request.user, # type: ignore[misc]
|
|
645
|
+
status=SyncBatchStatus.PENDING,
|
|
646
|
+
)
|
|
647
|
+
# Draw one sync_seq per project for the whole batch rather than one
|
|
648
|
+
# per row (#1527, ADR-0686). The allocations run inside this atomic
|
|
649
|
+
# block, so a rollback discards them with the rows.
|
|
650
|
+
with coalesce_sync_seq():
|
|
651
|
+
applied = apply_task_changes(
|
|
652
|
+
project=project,
|
|
653
|
+
request=request,
|
|
654
|
+
role=role,
|
|
655
|
+
changes=changes,
|
|
656
|
+
last_pulled_at=last_pulled_at,
|
|
657
|
+
)
|
|
658
|
+
body = {
|
|
659
|
+
"client_batch_id": str(client_batch_id),
|
|
660
|
+
"applied": {
|
|
661
|
+
"tasks": {
|
|
662
|
+
"created": applied.created,
|
|
663
|
+
"updated": applied.updated,
|
|
664
|
+
"deleted": applied.deleted,
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
# Per-row lost-update conflicts (#1718): rows that were NOT applied
|
|
668
|
+
# because a concurrent writer changed an overlapping field. The client
|
|
669
|
+
# reloads these and reapplies; empty on the common no-conflict batch.
|
|
670
|
+
"conflicts": {"tasks": applied.conflicts},
|
|
671
|
+
"timestamp": applied.max_version,
|
|
672
|
+
}
|
|
673
|
+
batch.status = SyncBatchStatus.COMPLETED
|
|
674
|
+
batch.response_body = body
|
|
675
|
+
batch.response_status = status.HTTP_200_OK
|
|
676
|
+
batch.save(update_fields=["status", "response_body", "response_status"])
|
|
677
|
+
|
|
678
|
+
# Side effects on commit, mirroring single-row writes (inbound_sync):
|
|
679
|
+
# one coalesced CPM recalc + one coalesced board event, so web clients
|
|
680
|
+
# react and a rolled-back batch broadcasts nothing.
|
|
681
|
+
#
|
|
682
|
+
# A delta batch can carry up to TRUEPPM_SYNC_BATCH_MAX_ROWS (500) rows.
|
|
683
|
+
# Emitting one broadcast per applied row (the previous behavior) issued
|
|
684
|
+
# up to 500 async_to_sync(group_send) round-trips at commit on a single
|
|
685
|
+
# upload — under a reconnect storm this overflowed the channel-layer
|
|
686
|
+
# inbox and dropped presence/CPM events (#809). Coalesce into a single
|
|
687
|
+
# tasks_bulk_mutated event (same pattern as sprint-close carry-over,
|
|
688
|
+
# projects/tasks.py); the web handler invalidates the whole task query
|
|
689
|
+
# on it, so per-row granularity is not needed.
|
|
690
|
+
project_id = str(project.pk)
|
|
691
|
+
if applied.changed:
|
|
692
|
+
transaction.on_commit(partial(enqueue_recalculate, project_id))
|
|
693
|
+
mutated_ids = [task_id for _event_type, task_id in applied.events]
|
|
694
|
+
if mutated_ids:
|
|
695
|
+
# default-arg binding (not closure capture) so a future branch
|
|
696
|
+
# can't late-bind the ids — matches the carry-over pattern.
|
|
697
|
+
def _broadcast_bulk(pid: str = project_id, ids: list[str] = mutated_ids) -> None:
|
|
698
|
+
broadcast_board_event(pid, "tasks_bulk_mutated", {"task_ids": ids})
|
|
699
|
+
|
|
700
|
+
transaction.on_commit(_broadcast_bulk)
|
|
701
|
+
|
|
702
|
+
return Response(body, status=status.HTTP_200_OK)
|
|
703
|
+
|
|
704
|
+
@staticmethod
|
|
705
|
+
def _collect(qs: Any, serializer_class: Any) -> dict[str, Any]:
|
|
706
|
+
"""Split a queryset into WatermelonDB 'updated' and 'deleted' buckets.
|
|
707
|
+
|
|
708
|
+
All changed rows are returned in 'updated' (upsert semantics).
|
|
709
|
+
Soft-deleted rows have their IDs returned in 'deleted' as well.
|
|
710
|
+
'created' is always empty — WatermelonDB upserts handle insert/update.
|
|
711
|
+
"""
|
|
712
|
+
rows = list(qs)
|
|
713
|
+
live = [r for r in rows if not r.is_deleted]
|
|
714
|
+
deleted_ids = [str(r.pk) for r in rows if r.is_deleted]
|
|
715
|
+
return {
|
|
716
|
+
"created": [],
|
|
717
|
+
"updated": serializer_class(live, many=True).data,
|
|
718
|
+
"deleted": deleted_ids,
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
@classmethod
|
|
722
|
+
def _watermark(cls, project: Project) -> int:
|
|
723
|
+
"""Return the project's sync cursor high-water mark (ADR-0686, #2491).
|
|
724
|
+
|
|
725
|
+
``Project.last_sync_version`` is the allocator every synced row in this
|
|
726
|
+
project drew its ``sync_seq`` from (``apps/sync/sequence.py``), so it is
|
|
727
|
+
by construction greater than or equal to every row's cursor — the safe
|
|
728
|
+
upper bound the client adopts as its next ``since``.
|
|
729
|
+
|
|
730
|
+
It used to cache ``MAX(server_version)`` over a 14-table ``UNION ALL``
|
|
731
|
+
(ADR-0142). The cache was faithful; the value was not a valid ordering,
|
|
732
|
+
because ``server_version`` counts one row's own saves. The union, its
|
|
733
|
+
``SYNC_WATERMARK_USE_COLUMN`` fallback, and the receiver-vs-union
|
|
734
|
+
conformance test are gone with it: there is nothing left to derive the
|
|
735
|
+
column from, and nothing left to drift.
|
|
736
|
+
"""
|
|
737
|
+
return int(project.last_sync_version)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class UserProgramSyncView(IdempotencyMixin, APIView):
|
|
741
|
+
"""Pull-only delta sync for the caller's Program + ProgramMembership rows.
|
|
742
|
+
|
|
743
|
+
The project-scoped :class:`ProjectSyncView` cannot reach user-scoped Program
|
|
744
|
+
rows (ADR-0070 §Sync): the ``Project.program`` FK syncs offline so the program
|
|
745
|
+
badge renders on cached projects, but the program list itself does not — on a
|
|
746
|
+
job site the program layer vanishes and the user falls back to a flat project
|
|
747
|
+
list. This endpoint closes that gap, delivering Program and ProgramMembership
|
|
748
|
+
deltas for every program the caller is a member of. It is the 0.4 follow-up
|
|
749
|
+
ADR-0070 §Sync flagged; no new ADR (see the updated §Sync note).
|
|
750
|
+
|
|
751
|
+
Scope is derived entirely from the caller's own live memberships, so — unlike
|
|
752
|
+
the project endpoint — there is **no path parameter to tamper with**: no IDOR
|
|
753
|
+
surface. All co-members of the caller's programs are returned (parity with the
|
|
754
|
+
project endpoint, which returns every ``ProjectMembership`` for the project),
|
|
755
|
+
which lets the client enforce offline program RBAC and render the roster.
|
|
756
|
+
|
|
757
|
+
Tombstones: a program soft-deleted while the caller is still a member, and any
|
|
758
|
+
co-member membership soft-deleted, are delivered as ``deleted`` ids.
|
|
759
|
+
|
|
760
|
+
Known v1 limitation (mirrors ``ProjectSyncView``): if the caller's *own*
|
|
761
|
+
membership is soft-deleted (they are removed from a program), that row drops
|
|
762
|
+
out of the accessible set and its tombstone is never delivered — the online
|
|
763
|
+
path 403s on the next access and the client evicts the stale program on a full
|
|
764
|
+
re-sync. Full write-side offline for programs is out of scope for v1 (reads
|
|
765
|
+
first, per #561).
|
|
766
|
+
|
|
767
|
+
Usage:
|
|
768
|
+
GET /api/v1/sync/user/programs/?since=0
|
|
769
|
+
"""
|
|
770
|
+
|
|
771
|
+
# Read-only pull; the generic Idempotency-Key path adds nothing (parity with
|
|
772
|
+
# ProjectSyncView.get, ADR-0170).
|
|
773
|
+
idempotency_exempt = True
|
|
774
|
+
permission_classes = [IsAuthenticated]
|
|
775
|
+
|
|
776
|
+
@extend_schema(
|
|
777
|
+
summary="Pull the offline sync delta for the caller's programs",
|
|
778
|
+
parameters=[
|
|
779
|
+
OpenApiParameter(
|
|
780
|
+
name="since",
|
|
781
|
+
type=OpenApiTypes.INT,
|
|
782
|
+
location=OpenApiParameter.QUERY,
|
|
783
|
+
required=False,
|
|
784
|
+
default=0,
|
|
785
|
+
description=(
|
|
786
|
+
"WatermelonDB high-water mark. Returns only Program / "
|
|
787
|
+
"ProgramMembership rows whose `server_version` is strictly "
|
|
788
|
+
"greater than this value. Omit (or pass `0`) for a full initial "
|
|
789
|
+
"sync; adopt the previous response's `timestamp` for the delta."
|
|
790
|
+
),
|
|
791
|
+
),
|
|
792
|
+
OpenApiParameter(
|
|
793
|
+
name="cursor",
|
|
794
|
+
type=OpenApiTypes.STR,
|
|
795
|
+
location=OpenApiParameter.QUERY,
|
|
796
|
+
required=False,
|
|
797
|
+
description=(
|
|
798
|
+
"Opaque pagination continuation token (#1013). Omit on the "
|
|
799
|
+
"first request; on each subsequent request pass the previous "
|
|
800
|
+
"response's `next_cursor`. Keep `since` constant for the whole "
|
|
801
|
+
"paging session. Loop until `has_more` is false, then adopt "
|
|
802
|
+
"`timestamp` as the next `since`."
|
|
803
|
+
),
|
|
804
|
+
),
|
|
805
|
+
OpenApiParameter(
|
|
806
|
+
name="page_size",
|
|
807
|
+
type=OpenApiTypes.INT,
|
|
808
|
+
location=OpenApiParameter.QUERY,
|
|
809
|
+
required=False,
|
|
810
|
+
description=(
|
|
811
|
+
"Maximum rows returned across both collections in one page. "
|
|
812
|
+
"Defaults to `TRUEPPM_SYNC_PULL_PAGE_SIZE`; clamped to "
|
|
813
|
+
"`TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE`."
|
|
814
|
+
),
|
|
815
|
+
),
|
|
816
|
+
],
|
|
817
|
+
responses={200: SyncPullResponseSerializer},
|
|
818
|
+
)
|
|
819
|
+
def get(self, request: Request) -> Response:
|
|
820
|
+
# Validate `since` — identical contract to ProjectSyncView.
|
|
821
|
+
since_raw = request.query_params.get("since", "0")
|
|
822
|
+
try:
|
|
823
|
+
since = int(since_raw)
|
|
824
|
+
if since < 0:
|
|
825
|
+
raise ValueError
|
|
826
|
+
except (ValueError, TypeError) as err:
|
|
827
|
+
raise ValidationError({"since": "Must be a non-negative integer."}) from err
|
|
828
|
+
|
|
829
|
+
cursor_raw = request.query_params.get("cursor")
|
|
830
|
+
cursor = SyncCursor.decode(cursor_raw) if cursor_raw else None
|
|
831
|
+
page_size = ProjectSyncView._resolve_page_size(request)
|
|
832
|
+
|
|
833
|
+
# Accessible set: programs the caller has a live membership on, kept as a
|
|
834
|
+
# queryset so Postgres evaluates it as a single subquery in the delta reads
|
|
835
|
+
# rather than round-tripping every id into Python.
|
|
836
|
+
accessible_ids = ProgramMembership.objects.filter(
|
|
837
|
+
user=cast("_User", request.user), is_deleted=False
|
|
838
|
+
).values("program_id")
|
|
839
|
+
|
|
840
|
+
# Snapshot the high-water mark BEFORE the delta queries. server_version is
|
|
841
|
+
# monotonic, so a write landing after this snapshot moves the row forward in
|
|
842
|
+
# (server_version, id) order — delivered this pull (re-applied under upsert)
|
|
843
|
+
# or the next (its version > timestamp = next since); never lost. This is
|
|
844
|
+
# the same concurrency argument pagination.py makes, and the same
|
|
845
|
+
# watermark-before-rows ordering ProjectSyncView relies on. Neither view
|
|
846
|
+
# opens a REPEATABLE READ block; both run at READ COMMITTED, and the
|
|
847
|
+
# ordering — not the isolation level — is what closes the window.
|
|
848
|
+
#
|
|
849
|
+
# This slice keys on sync_seq, drawn from the installation-wide program
|
|
850
|
+
# sequence (ADR-0747, #2498). It used to key on the per-row server_version
|
|
851
|
+
# and carried the ordering defect ADR-0686 fixed for projects: a hot program
|
|
852
|
+
# raised the watermark above a cold program's row counters and those rows
|
|
853
|
+
# dropped out of the delta permanently. The project sequence could not fix
|
|
854
|
+
# it — programs have no single owning row — so program-scoped writes order
|
|
855
|
+
# against one installation counter instead, which restores a scalar cursor's
|
|
856
|
+
# validity without changing the wire contract.
|
|
857
|
+
#
|
|
858
|
+
# As in ProjectSyncView, the watermark is session state: a continuation page
|
|
859
|
+
# echoes the cursor's pinned value instead of recomputing one that could sit
|
|
860
|
+
# above an already-drained collection's mid-drain write (#2568).
|
|
861
|
+
timestamp = cursor.watermark if cursor is not None else self._watermark(accessible_ids)
|
|
862
|
+
|
|
863
|
+
# Program rows include soft-deleted ones so a program deleted while the
|
|
864
|
+
# caller is still a member arrives as a tombstone. Memberships cover every
|
|
865
|
+
# co-member of the caller's programs (offline RBAC + roster), soft-deleted
|
|
866
|
+
# rows included. Order is fixed — do not reorder without a protocol bump.
|
|
867
|
+
sources: list[SyncSource] = [
|
|
868
|
+
(
|
|
869
|
+
"programs",
|
|
870
|
+
Program.objects.filter(id__in=accessible_ids, sync_seq__gt=since),
|
|
871
|
+
SyncProgramSerializer,
|
|
872
|
+
),
|
|
873
|
+
(
|
|
874
|
+
"program_memberships",
|
|
875
|
+
ProgramMembership.objects.filter(program_id__in=accessible_ids, sync_seq__gt=since),
|
|
876
|
+
SyncProgramMembershipSerializer,
|
|
877
|
+
),
|
|
878
|
+
]
|
|
879
|
+
|
|
880
|
+
changes, next_cursor, has_more = paginate_changes(
|
|
881
|
+
sources,
|
|
882
|
+
cursor=cursor,
|
|
883
|
+
page_size=page_size,
|
|
884
|
+
collect=ProjectSyncView._collect,
|
|
885
|
+
watermark=timestamp,
|
|
886
|
+
# Must match the delta filter above: the pager orders on this column
|
|
887
|
+
# and the sources floor on it. Ordering on one while filtering the
|
|
888
|
+
# other pages a stream the floor does not bound (ADR-0747, #2498).
|
|
889
|
+
version_field="sync_seq",
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
return Response(
|
|
893
|
+
{
|
|
894
|
+
"changes": changes,
|
|
895
|
+
"timestamp": timestamp,
|
|
896
|
+
"next_cursor": next_cursor.encode() if next_cursor is not None else None,
|
|
897
|
+
"has_more": has_more,
|
|
898
|
+
}
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
@staticmethod
|
|
902
|
+
def _watermark(accessible_ids: Any) -> int:
|
|
903
|
+
"""The installation-wide program sequence's high-water mark (ADR-0747, #2498).
|
|
904
|
+
|
|
905
|
+
``ProgramSyncSequence.value`` is the allocator every synced ``Program`` and
|
|
906
|
+
``ProgramMembership`` row drew its ``sync_seq`` from, so it is by
|
|
907
|
+
construction greater than or equal to every row's cursor — the safe upper
|
|
908
|
+
bound the client adopts as its next ``since``. One row read, no aggregate.
|
|
909
|
+
|
|
910
|
+
It used to be ``MAX(server_version)`` across the caller's programs and their
|
|
911
|
+
memberships. That value was faithful and was not a valid ordering:
|
|
912
|
+
``server_version`` counts a single row's saves, so a frequently-edited
|
|
913
|
+
program raised the checkpoint above every other program's row counter and
|
|
914
|
+
dropped those rows from the delta permanently (#2498) — the same defect
|
|
915
|
+
ADR-0686 fixed for projects, one level up.
|
|
916
|
+
|
|
917
|
+
``accessible_ids`` is no longer read: the checkpoint is a property of the
|
|
918
|
+
installation's write log, not of the caller's accessible set. Scoping it per
|
|
919
|
+
caller is what made it a maximum-over-many-rows in the first place. The
|
|
920
|
+
parameter is kept so the call site and ``ProjectSyncView._watermark`` keep
|
|
921
|
+
one shape.
|
|
922
|
+
"""
|
|
923
|
+
from trueppm_api.apps.sync.models import ProgramSyncSequence
|
|
924
|
+
|
|
925
|
+
value = (
|
|
926
|
+
ProgramSyncSequence.objects.filter(pk=ProgramSyncSequence.SINGLETON_PK)
|
|
927
|
+
.values_list("value", flat=True)
|
|
928
|
+
.first()
|
|
929
|
+
)
|
|
930
|
+
return value or 0
|