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,46 @@
|
|
|
1
|
+
# Generated by Django 5.2.15 on 2026-07-05 23:56
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("projects", "0109_projectexportjob"),
|
|
10
|
+
(
|
|
11
|
+
"sync",
|
|
12
|
+
"0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more",
|
|
13
|
+
),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name="BoardEvent",
|
|
19
|
+
fields=[
|
|
20
|
+
(
|
|
21
|
+
"id",
|
|
22
|
+
models.BigAutoField(
|
|
23
|
+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
("event_type", models.CharField(max_length=64)),
|
|
27
|
+
("payload", models.JSONField(blank=True, default=dict)),
|
|
28
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
29
|
+
(
|
|
30
|
+
"project",
|
|
31
|
+
models.ForeignKey(
|
|
32
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
33
|
+
related_name="board_events",
|
|
34
|
+
to="projects.project",
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
],
|
|
38
|
+
options={
|
|
39
|
+
"ordering": ["id"],
|
|
40
|
+
"indexes": [
|
|
41
|
+
models.Index(fields=["project", "id"], name="boardevent_project_seq_idx"),
|
|
42
|
+
models.Index(fields=["created_at"], name="boardevent_created_idx"),
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Seed the per-project sync delta cursor for existing data (ADR-0686, #2491).
|
|
2
|
+
|
|
3
|
+
Runs after every app that adds the ``sync_seq`` column, because the renumber
|
|
4
|
+
touches all of them in one pass. The logic lives in ``apps/sync/backfill.py`` so
|
|
5
|
+
tests can import it without referencing this file's name (CLAUDE.md rule 3).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from django.db import migrations
|
|
9
|
+
|
|
10
|
+
from trueppm_api.apps.sync.backfill import seed_sync_seq, unseed_sync_seq
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Migration(migrations.Migration):
|
|
14
|
+
dependencies = [
|
|
15
|
+
("sync", "0003_boardevent"),
|
|
16
|
+
("projects", "0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more"),
|
|
17
|
+
("access", "0018_programmembership_sync_seq_and_more"),
|
|
18
|
+
("integrations", "0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx"),
|
|
19
|
+
("timetracking", "0004_timeentry_sync_seq_and_more"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
operations = [
|
|
23
|
+
migrations.RunPython(seed_sync_seq, unseed_sync_seq, elidable=True),
|
|
24
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Installation-wide allocator for the program sync cursor (ADR-0747, #2498).
|
|
2
|
+
|
|
3
|
+
Creates the singleton counter, then seeds it and both program tables' ``sync_seq``
|
|
4
|
+
in one ordered pass. The backfill logic lives in ``apps/sync/backfill.py`` so tests
|
|
5
|
+
can import it without referencing this file's name (CLAUDE.md migration rule 3).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from django.db import migrations, models
|
|
9
|
+
|
|
10
|
+
from trueppm_api.apps.sync.backfill import seed_program_sync_seq, unseed_program_sync_seq
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Migration(migrations.Migration):
|
|
14
|
+
dependencies = [
|
|
15
|
+
("sync", "0004_seed_sync_seq"),
|
|
16
|
+
# The backfill reads both tables' server_version and writes their sync_seq,
|
|
17
|
+
# so both columns must already exist.
|
|
18
|
+
("projects", "0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more"),
|
|
19
|
+
("access", "0018_programmembership_sync_seq_and_more"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
operations = [
|
|
23
|
+
migrations.CreateModel(
|
|
24
|
+
name="ProgramSyncSequence",
|
|
25
|
+
fields=[
|
|
26
|
+
("id", models.BigAutoField(primary_key=True, serialize=False)),
|
|
27
|
+
("value", models.BigIntegerField(default=0, editable=False)),
|
|
28
|
+
],
|
|
29
|
+
options={
|
|
30
|
+
"verbose_name": "program sync sequence",
|
|
31
|
+
"verbose_name_plural": "program sync sequence",
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
migrations.RunPython(seed_program_sync_seq, unseed_program_sync_seq, elidable=True),
|
|
35
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Models for the sync app.
|
|
2
|
+
|
|
3
|
+
The only model here is :class:`SyncBatch`, the idempotency + atomicity
|
|
4
|
+
envelope for a mobile offline **upload** batch (ADR-0082, issue #667). It is
|
|
5
|
+
server infrastructure — the mobile client never pulls it as a domain row — so
|
|
6
|
+
it is a plain ``models.Model`` (not ``VersionedModel``), mirroring the
|
|
7
|
+
transactional-outbox style of ``projects.SprintCloseRequest``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import timedelta
|
|
14
|
+
|
|
15
|
+
from django.conf import settings
|
|
16
|
+
from django.db import models
|
|
17
|
+
from django.utils import timezone
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SyncBatchStatus(models.TextChoices):
|
|
21
|
+
"""Lifecycle of an upload batch envelope (ADR-0082 §D)."""
|
|
22
|
+
|
|
23
|
+
PENDING = "pending", "Pending"
|
|
24
|
+
COMPLETED = "completed", "Completed"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SyncBatch(models.Model):
|
|
28
|
+
"""Records a mobile upload batch so a retry is a no-op, not a double-apply.
|
|
29
|
+
|
|
30
|
+
A mobile client generates a stable ``client_batch_id`` (UUID) per delta and
|
|
31
|
+
re-uploads the *same* id until it sees an ACK. The first request that wins
|
|
32
|
+
the unique-constraint race applies the delta and snapshots its HTTP response
|
|
33
|
+
here, inside the same transaction as the row writes (all-or-nothing). A
|
|
34
|
+
later request carrying the same id — the lost-ACK retry — returns the stored
|
|
35
|
+
response without re-applying anything.
|
|
36
|
+
|
|
37
|
+
Concurrency is serialized by the ``client_batch_id`` unique constraint:
|
|
38
|
+
Postgres blocks a concurrent duplicate INSERT until the first transaction
|
|
39
|
+
commits or aborts, after which the duplicate either reads the committed row
|
|
40
|
+
(replay) or proceeds (the first attempt rolled back). See
|
|
41
|
+
``sync.views.ProjectSyncView.post`` for the full algorithm.
|
|
42
|
+
|
|
43
|
+
Rows are dedup-relevant only within the freshness window
|
|
44
|
+
(``TRUEPPM_SYNC_BATCH_RETENTION_HOURS``, default 24h); the
|
|
45
|
+
``sync.purge_sync_batches`` Beat task reaps older rows so the table stays
|
|
46
|
+
bounded (ADR-0081 purge convention).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
50
|
+
# Client-generated dedup key. Uniqueness (and every lookup) is scoped to
|
|
51
|
+
# (project, actor_user): a retry always targets the same project endpoint as
|
|
52
|
+
# the same authenticated user, so that composite unique index is a sufficient
|
|
53
|
+
# concurrency backstop, while scoping prevents one project — or one user —
|
|
54
|
+
# replaying another's stored response (the dedup lookup must never cross the
|
|
55
|
+
# project or actor boundary — see views.post, #887/#894).
|
|
56
|
+
client_batch_id = models.UUIDField()
|
|
57
|
+
project = models.ForeignKey(
|
|
58
|
+
"projects.Project",
|
|
59
|
+
on_delete=models.CASCADE,
|
|
60
|
+
related_name="sync_batches",
|
|
61
|
+
)
|
|
62
|
+
# Actor scoping (#894): the stored response body carries task ids,
|
|
63
|
+
# server_versions, and the sync watermark for the rows *this user* pushed.
|
|
64
|
+
# Keying dedup on (project, client_batch_id) alone let any project member
|
|
65
|
+
# replay another member's batch by reusing its client_batch_id and read back
|
|
66
|
+
# that user's response — an information leak across actors. Scoping
|
|
67
|
+
# uniqueness and the replay lookup to the actor closes it: a reused id from a
|
|
68
|
+
# different user is a distinct batch, never a replay of someone else's.
|
|
69
|
+
# Nullable so the migration is backfill-safe for rows written before this
|
|
70
|
+
# field existed (those legacy rows simply never match a new actor-scoped
|
|
71
|
+
# lookup and age out of the freshness window).
|
|
72
|
+
actor_user = models.ForeignKey(
|
|
73
|
+
settings.AUTH_USER_MODEL,
|
|
74
|
+
on_delete=models.CASCADE,
|
|
75
|
+
related_name="sync_batches",
|
|
76
|
+
null=True,
|
|
77
|
+
blank=True,
|
|
78
|
+
)
|
|
79
|
+
status = models.CharField(
|
|
80
|
+
max_length=12,
|
|
81
|
+
choices=SyncBatchStatus.choices,
|
|
82
|
+
default=SyncBatchStatus.PENDING,
|
|
83
|
+
db_index=True,
|
|
84
|
+
)
|
|
85
|
+
# Verbatim success response so a retry replays byte-identically.
|
|
86
|
+
response_body = models.JSONField(default=dict, blank=True)
|
|
87
|
+
response_status = models.PositiveSmallIntegerField(default=200)
|
|
88
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
89
|
+
|
|
90
|
+
objects: models.Manager[SyncBatch] = models.Manager()
|
|
91
|
+
|
|
92
|
+
class Meta:
|
|
93
|
+
ordering = ["created_at"]
|
|
94
|
+
constraints = [
|
|
95
|
+
# Actor-scoped uniqueness (#894): two different users may legitimately
|
|
96
|
+
# reuse the same client_batch_id; only a same-(project, actor) duplicate
|
|
97
|
+
# is a replay. Scoping the constraint to the actor prevents one user's
|
|
98
|
+
# batch from blocking — or being replayed by — another's.
|
|
99
|
+
models.UniqueConstraint(
|
|
100
|
+
fields=["project", "actor_user", "client_batch_id"],
|
|
101
|
+
name="syncbatch_project_actor_client_batch_uniq",
|
|
102
|
+
),
|
|
103
|
+
]
|
|
104
|
+
indexes = [
|
|
105
|
+
# Reaper scans by age; the index keeps the nightly purge cheap.
|
|
106
|
+
models.Index(fields=["created_at"], name="syncbatch_created_idx"),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
def __str__(self) -> str:
|
|
110
|
+
return f"SyncBatch({self.client_batch_id}, {self.status})"
|
|
111
|
+
|
|
112
|
+
def is_fresh(self, *, ttl_hours: int = 24) -> bool:
|
|
113
|
+
"""Return True if this batch is still within the dedup window.
|
|
114
|
+
|
|
115
|
+
A duplicate of a *fresh* completed batch replays the stored response; a
|
|
116
|
+
duplicate of an *expired* batch re-runs the apply (the stale row is
|
|
117
|
+
deleted first). ``ttl_hours`` is supplied by the caller from
|
|
118
|
+
``settings.TRUEPPM_SYNC_BATCH_RETENTION_HOURS``.
|
|
119
|
+
"""
|
|
120
|
+
return self.created_at > timezone.now() - timedelta(hours=ttl_hours)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class BoardEvent(models.Model):
|
|
124
|
+
"""Bounded per-project replay buffer for WebSocket events (ADR-0236, #321).
|
|
125
|
+
|
|
126
|
+
Real-time broadcasts are best-effort: a client that disconnects loses every
|
|
127
|
+
event sent during the gap. Each replayable broadcast persists one ``BoardEvent``
|
|
128
|
+
row here so a reconnecting client can request ``?since=<seq>`` and replay only
|
|
129
|
+
the events it missed — instead of a broad refetch storm — and recover the
|
|
130
|
+
incremental ``task_dates_updated`` CPM splices a refetch-on-focus would drop.
|
|
131
|
+
|
|
132
|
+
Like :class:`SyncBatch`, this is server infrastructure, never pulled as a synced
|
|
133
|
+
domain row, so it is a plain ``models.Model`` (no ``server_version``).
|
|
134
|
+
|
|
135
|
+
**The primary key is the sequence.** The ``BigAutoField`` ``id`` is assigned
|
|
136
|
+
atomically by Postgres at INSERT, so it is globally monotonic with no lost or
|
|
137
|
+
duplicate values under concurrent commits. Clients only need a strictly
|
|
138
|
+
increasing-per-project value for a ``seq > since`` comparison and tolerate gaps
|
|
139
|
+
(other projects' rows interleave the global sequence), so the PK *is* the
|
|
140
|
+
per-project sequence — no per-project counter row or dedicated sequence is
|
|
141
|
+
needed. The wire exposes it as ``seq``.
|
|
142
|
+
|
|
143
|
+
Rows are reaped past ``TRUEPPM_BOARD_EVENT_RETENTION_HOURS`` (default 24h) by
|
|
144
|
+
the ``sync.purge_board_events`` Beat task, keeping the buffer bounded (the
|
|
145
|
+
ADR-0081 purge convention). Because ``created_at`` is monotonic with ``id`` at
|
|
146
|
+
that granularity, the purge removes a contiguous low-``id`` prefix — which is
|
|
147
|
+
what lets the consumer use "oldest surviving id" as the gap-detection
|
|
148
|
+
watermark (see ``consumers.py``).
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# id: implicit BigAutoField PK (DEFAULT_AUTO_FIELD) — doubles as the sequence.
|
|
152
|
+
project = models.ForeignKey(
|
|
153
|
+
"projects.Project",
|
|
154
|
+
on_delete=models.CASCADE,
|
|
155
|
+
related_name="board_events",
|
|
156
|
+
)
|
|
157
|
+
event_type = models.CharField(max_length=64)
|
|
158
|
+
payload = models.JSONField(default=dict, blank=True)
|
|
159
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
160
|
+
|
|
161
|
+
objects: models.Manager[BoardEvent] = models.Manager()
|
|
162
|
+
|
|
163
|
+
class Meta:
|
|
164
|
+
# Replay streams strictly by ascending sequence (the PK).
|
|
165
|
+
ordering = ["id"]
|
|
166
|
+
indexes = [
|
|
167
|
+
# The replay query: WHERE project_id=? AND id > ? ORDER BY id.
|
|
168
|
+
models.Index(fields=["project", "id"], name="boardevent_project_seq_idx"),
|
|
169
|
+
# The reaper scans by age; keeps the nightly purge cheap.
|
|
170
|
+
models.Index(fields=["created_at"], name="boardevent_created_idx"),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
def __str__(self) -> str:
|
|
174
|
+
return f"BoardEvent(seq={self.pk}, {self.event_type}, project={self.project_id})"
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def seq(self) -> int:
|
|
178
|
+
"""The wire sequence — an alias for the monotonic PK (ADR-0236)."""
|
|
179
|
+
return self.pk
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ProgramSyncSequence(models.Model):
|
|
183
|
+
"""Installation-wide monotonic allocator for the program sync cursor (ADR-0747).
|
|
184
|
+
|
|
185
|
+
Exactly one row, pk=1. Every synced write to ``Program`` or
|
|
186
|
+
``ProgramMembership`` draws the next value from it and stamps the row's
|
|
187
|
+
``sync_seq``, so those values are totally ordered installation-wide, ``MAX`` is
|
|
188
|
+
a meaningful checkpoint, and ``sync_seq__gt=since`` is a correct delta filter.
|
|
189
|
+
|
|
190
|
+
**Why a table row and not a PostgreSQL SEQUENCE.** ``nextval()`` would avoid
|
|
191
|
+
this row's write lock, and would be wrong. Sequences are non-transactional, so
|
|
192
|
+
they grant no relationship between allocation order and commit order: one
|
|
193
|
+
transaction could allocate 100, a later one allocate 101 and commit first, and a
|
|
194
|
+
pull between the two commits would report 101 and permanently skip 100 — the
|
|
195
|
+
exact defect this replaces, reintroduced as a race. Allocating by ``UPDATE``
|
|
196
|
+
holds this row's write lock until commit, so commit order equals allocation
|
|
197
|
+
order. The lock is the correctness mechanism, not overhead.
|
|
198
|
+
|
|
199
|
+
**Why installation-wide and not per-program.** ``Program`` rows have no single
|
|
200
|
+
owning entity to sequence from the way a ``Task`` has its project (ADR-0686).
|
|
201
|
+
The accessible set is per-user — the union of the caller's programs — so a
|
|
202
|
+
scalar cursor over per-program sequences would reintroduce the same
|
|
203
|
+
hot-row-outruns-cold-row failure one level up. Ordering every program-scoped
|
|
204
|
+
write against one counter is what makes a scalar cursor sound.
|
|
205
|
+
|
|
206
|
+
The cost is that program writes serialize installation-wide. That is acceptable
|
|
207
|
+
because program writes are rare and administrative — create, rename, add or
|
|
208
|
+
remove a member — never a hot path or a bulk import. See ADR-0747 for the
|
|
209
|
+
escape hatch if that ever changes.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
#: The single row's primary key. Named so call sites read as intent.
|
|
213
|
+
SINGLETON_PK = 1
|
|
214
|
+
|
|
215
|
+
id = models.BigAutoField(primary_key=True)
|
|
216
|
+
value = models.BigIntegerField(default=0, editable=False)
|
|
217
|
+
|
|
218
|
+
objects: models.Manager[ProgramSyncSequence] = models.Manager()
|
|
219
|
+
|
|
220
|
+
class Meta:
|
|
221
|
+
verbose_name = "program sync sequence"
|
|
222
|
+
verbose_name_plural = "program sync sequence"
|
|
223
|
+
|
|
224
|
+
def __str__(self) -> str:
|
|
225
|
+
return f"ProgramSyncSequence(value={self.value})"
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Cursor pagination for the offline delta pull (#1013).
|
|
2
|
+
|
|
3
|
+
The delta pull (:class:`~trueppm_api.apps.sync.views.ProjectSyncView`) must not
|
|
4
|
+
materialize an entire project into one unbounded JSON response on a cold start
|
|
5
|
+
(``since=0``): a large project blows past the "500-task delta < 3s" mobile
|
|
6
|
+
target. This module pages the delta so each response carries at most
|
|
7
|
+
``page_size`` rows across all synced collections, and the client loops until the
|
|
8
|
+
cursor is exhausted.
|
|
9
|
+
|
|
10
|
+
Why a compound ``(table_index, version, id)`` keyset and not a scalar ceiling
|
|
11
|
+
--------------------------------------------------------------------------------
|
|
12
|
+
Rows share version values. Under the project sync cursor (``sync_seq``,
|
|
13
|
+
ADR-0686) every row written in one batch draws the same value, and the program
|
|
14
|
+
slice (ADR-0747) allocates from one installation-wide counter, so ties are rarer
|
|
15
|
+
there but still possible across a single transaction's writes. A
|
|
16
|
+
scalar "return ``since < version <= ceiling``, then ``next_since = ceiling``"
|
|
17
|
+
cursor cannot bound page size in either case: to avoid splitting a version (which
|
|
18
|
+
would silently drop rows) it must return *all* rows at the boundary version, and
|
|
19
|
+
on a cold start that is the whole project. It is either unbounded or lossy.
|
|
20
|
+
|
|
21
|
+
The fix keys the cursor on the pair ``(version, id)`` within each collection.
|
|
22
|
+
``id`` is a globally unique UUID, so ``(version, id)`` is a **total order** even
|
|
23
|
+
when every row shares a version — a page boundary can fall between two rows of
|
|
24
|
+
the same version without ambiguity. Collections are drained in a fixed order, so
|
|
25
|
+
each collection is a contiguous, non-overlapping segment of the global stream.
|
|
26
|
+
The result: **no row is skipped and no row is duplicated** across pages, and
|
|
27
|
+
every page is bounded by ``page_size``.
|
|
28
|
+
|
|
29
|
+
Concurrency during a multi-page pull is safe because the version field only ever
|
|
30
|
+
increases: a row edited mid-pull moves *forward* in ``(version, id)`` order, so a
|
|
31
|
+
not-yet-reached row is delivered later and an already-delivered row is
|
|
32
|
+
re-delivered under WatermelonDB upsert semantics — never lost.
|
|
33
|
+
|
|
34
|
+
That argument only holds for rows the drain can still *reach*. A row written into
|
|
35
|
+
a collection the cursor has already passed moves forward in a stream nobody is
|
|
36
|
+
reading any more, so the drain cannot deliver it — the client's next ``since``
|
|
37
|
+
has to stay low enough to catch it on the following pull. The watermark is
|
|
38
|
+
therefore a property of the **pull session**, not of the request: it is computed
|
|
39
|
+
once on the first (cursor-less) request, carried in the cursor as ``w``, and
|
|
40
|
+
echoed unchanged on every continuation page (#2568). Recomputing it per request
|
|
41
|
+
published a checkpoint above rows the session never returned, and the client
|
|
42
|
+
adopting the last page's value skipped them permanently.
|
|
43
|
+
|
|
44
|
+
The field is caller-supplied (``version_field``) rather than hard-coded, but both
|
|
45
|
+
sync endpoints now pass ``sync_seq``: the project pull draws it from its project's
|
|
46
|
+
sequence (ADR-0686) and the program pull from the installation-wide program
|
|
47
|
+
sequence (ADR-0747). The parameter stays because the ordering column and the
|
|
48
|
+
sources' ``__gt=since`` floor must agree, and making that agreement explicit at
|
|
49
|
+
each call site is what stops the two drifting apart — the program pull used to
|
|
50
|
+
default to ``server_version`` and carried the ordering defect #2498 fixed.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import base64
|
|
56
|
+
import binascii
|
|
57
|
+
import json
|
|
58
|
+
from collections.abc import Callable
|
|
59
|
+
from dataclasses import dataclass
|
|
60
|
+
from typing import TYPE_CHECKING, Any
|
|
61
|
+
|
|
62
|
+
from django.db.models import Q
|
|
63
|
+
from rest_framework.exceptions import ValidationError
|
|
64
|
+
|
|
65
|
+
if TYPE_CHECKING:
|
|
66
|
+
from django.db.models import QuerySet
|
|
67
|
+
|
|
68
|
+
# A single sync source: (collection_name, base_queryset, serializer_class). The
|
|
69
|
+
# base queryset already carries the project scope, the ``<version_field>__gt=since``
|
|
70
|
+
# floor, and any RBAC/visibility filters (retro visibility, per-user time entries).
|
|
71
|
+
SyncSource = tuple[str, "QuerySet[Any]", Any]
|
|
72
|
+
|
|
73
|
+
# Callable that splits a materialized row list into WatermelonDB
|
|
74
|
+
# created/updated/deleted buckets (``ProjectSyncView._collect``).
|
|
75
|
+
CollectFn = Callable[[list[Any], Any], dict[str, Any]]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class SyncCursor:
|
|
80
|
+
"""Position in the global ``(table_index, version, id)`` delta stream.
|
|
81
|
+
|
|
82
|
+
``row_id`` is ``None`` for a *fresh-table* cursor (resume at the top of
|
|
83
|
+
``sources[index]`` with only the ``since`` floor applied) and a UUID string
|
|
84
|
+
for an *intra-table* cursor (resume strictly after ``(version, row_id)``
|
|
85
|
+
within ``sources[index]``). Keeping the two cases distinct avoids emitting a
|
|
86
|
+
``id > ''`` predicate that Postgres would reject when casting to ``uuid``.
|
|
87
|
+
|
|
88
|
+
``watermark`` pins the checkpoint the whole drain must report as
|
|
89
|
+
``timestamp`` (#2568). It is the session's state, not the page's: the view
|
|
90
|
+
computes it once on the cursor-less first request and thereafter reads it
|
|
91
|
+
back off the cursor instead of re-querying, so the client can never adopt a
|
|
92
|
+
checkpoint above a row the session had already paged past.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
index: int
|
|
96
|
+
version: int
|
|
97
|
+
row_id: str | None
|
|
98
|
+
watermark: int
|
|
99
|
+
|
|
100
|
+
def encode(self) -> str:
|
|
101
|
+
"""Serialize to an opaque, URL-safe token for the response envelope."""
|
|
102
|
+
raw = json.dumps(
|
|
103
|
+
{"i": self.index, "v": self.version, "id": self.row_id, "w": self.watermark},
|
|
104
|
+
separators=(",", ":"),
|
|
105
|
+
)
|
|
106
|
+
return base64.urlsafe_b64encode(raw.encode()).decode()
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def decode(cls, token: str) -> SyncCursor:
|
|
110
|
+
"""Parse a client-supplied cursor token, rejecting anything malformed.
|
|
111
|
+
|
|
112
|
+
The token is client-controlled, so every field is validated: a tampered
|
|
113
|
+
or truncated token yields a 400 rather than an unhandled 500. ``w`` is
|
|
114
|
+
validated exactly like ``i`` and ``v`` — required, integral, and
|
|
115
|
+
non-negative — because the view returns it verbatim as the checkpoint the
|
|
116
|
+
client adopts, which makes a junk value a data-loss vector rather than a
|
|
117
|
+
cosmetic one.
|
|
118
|
+
|
|
119
|
+
``OverflowError`` is caught alongside the rest because Python's ``json``
|
|
120
|
+
accepts the non-standard literals ``Infinity``, ``-Infinity`` and
|
|
121
|
+
overflowing floats such as ``1e400``, and ``int()`` rejects them with an
|
|
122
|
+
``ArithmeticError`` rather than the ``ValueError`` the other bad inputs
|
|
123
|
+
raise. Without it a one-token request reached an unhandled 500 — and this
|
|
124
|
+
endpoint is deliberately unthrottled (``get_throttles`` returns ``[]``
|
|
125
|
+
for GET), so that was a free error-log amplifier for any member.
|
|
126
|
+
|
|
127
|
+
A token minted before ``w`` existed carries no such key and is rejected
|
|
128
|
+
as malformed. That costs a drain straddling the deploy one 400; the
|
|
129
|
+
client restarts it at the same unchanged ``since`` and loses nothing.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
raw = base64.urlsafe_b64decode(token.encode())
|
|
133
|
+
data = json.loads(raw)
|
|
134
|
+
index = int(data["i"])
|
|
135
|
+
version = int(data["v"])
|
|
136
|
+
row_id = data["id"]
|
|
137
|
+
watermark = int(data["w"])
|
|
138
|
+
except (
|
|
139
|
+
binascii.Error,
|
|
140
|
+
ValueError,
|
|
141
|
+
KeyError,
|
|
142
|
+
TypeError,
|
|
143
|
+
OverflowError,
|
|
144
|
+
UnicodeDecodeError,
|
|
145
|
+
) as err:
|
|
146
|
+
raise ValidationError({"cursor": "Malformed pagination cursor."}) from err
|
|
147
|
+
if (
|
|
148
|
+
index < 0
|
|
149
|
+
or version < 0
|
|
150
|
+
or watermark < 0
|
|
151
|
+
or (row_id is not None and not isinstance(row_id, str))
|
|
152
|
+
):
|
|
153
|
+
raise ValidationError({"cursor": "Malformed pagination cursor."})
|
|
154
|
+
return cls(index=index, version=version, row_id=row_id, watermark=watermark)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def paginate_changes(
|
|
158
|
+
sources: list[SyncSource],
|
|
159
|
+
*,
|
|
160
|
+
cursor: SyncCursor | None,
|
|
161
|
+
page_size: int,
|
|
162
|
+
collect: CollectFn,
|
|
163
|
+
watermark: int,
|
|
164
|
+
version_field: str = "server_version",
|
|
165
|
+
) -> tuple[dict[str, Any], SyncCursor | None, bool]:
|
|
166
|
+
"""Return one page of the delta: ``(changes, next_cursor, has_more)``.
|
|
167
|
+
|
|
168
|
+
``changes`` always contains every collection key (empty buckets for
|
|
169
|
+
collections this page does not touch) so the response shape is stable across
|
|
170
|
+
pages. ``next_cursor`` is ``None`` exactly when the delta is fully drained
|
|
171
|
+
(``has_more`` is then ``False``); the client loops until then.
|
|
172
|
+
|
|
173
|
+
Each source is fetched ``ORDER BY <version_field>, id`` with ``LIMIT
|
|
174
|
+
page_size + 1`` (the extra row detects whether the collection still has more
|
|
175
|
+
beyond this page). Collections are drained in list order; a collection is
|
|
176
|
+
fully drained before the next one is touched, so the pages partition the
|
|
177
|
+
global stream without gaps or overlap.
|
|
178
|
+
|
|
179
|
+
``version_field`` must be the same column the caller used for its
|
|
180
|
+
``__gt=since`` floor — mixing the two (ordering on ``sync_seq`` while
|
|
181
|
+
filtering ``server_version``) would page a stream the floor does not bound.
|
|
182
|
+
|
|
183
|
+
``watermark`` is stamped unchanged into every emitted cursor so the caller can
|
|
184
|
+
read the session checkpoint back off a continuation cursor rather than
|
|
185
|
+
recomputing it per page (#2568). It is threaded through here, not derived
|
|
186
|
+
here, because only the caller knows which sequence its ``since`` is drawn
|
|
187
|
+
from.
|
|
188
|
+
"""
|
|
189
|
+
changes: dict[str, Any] = {
|
|
190
|
+
name: {"created": [], "updated": [], "deleted": []} for name, _, _ in sources
|
|
191
|
+
}
|
|
192
|
+
remaining = page_size
|
|
193
|
+
start = cursor.index if cursor is not None else 0
|
|
194
|
+
|
|
195
|
+
for i in range(start, len(sources)):
|
|
196
|
+
name, qs, serializer_class = sources[i]
|
|
197
|
+
|
|
198
|
+
# Intra-table resume: seek strictly past (version, row_id). A fresh-table
|
|
199
|
+
# cursor (row_id is None) or the very first page applies only the
|
|
200
|
+
# ``<version_field>__gt=since`` floor already baked into ``qs``.
|
|
201
|
+
if cursor is not None and i == cursor.index and cursor.row_id is not None:
|
|
202
|
+
qs = qs.filter(
|
|
203
|
+
Q(**{f"{version_field}__gt": cursor.version})
|
|
204
|
+
| Q(**{version_field: cursor.version, "id__gt": cursor.row_id})
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
rows = list(qs.order_by(version_field, "id")[: remaining + 1])
|
|
208
|
+
has_more_in_source = len(rows) > remaining
|
|
209
|
+
rows = rows[:remaining]
|
|
210
|
+
if rows:
|
|
211
|
+
changes[name] = collect(rows, serializer_class)
|
|
212
|
+
remaining -= len(rows)
|
|
213
|
+
|
|
214
|
+
if has_more_in_source:
|
|
215
|
+
# This collection still has rows; stop with an intra-table cursor.
|
|
216
|
+
last = rows[-1]
|
|
217
|
+
return (
|
|
218
|
+
changes,
|
|
219
|
+
SyncCursor(i, getattr(last, version_field), str(last.pk), watermark),
|
|
220
|
+
True,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if remaining == 0:
|
|
224
|
+
# Page is exactly full and this collection is drained. Resume at the
|
|
225
|
+
# top of the next collection, or finish if this was the last one.
|
|
226
|
+
if i + 1 < len(sources):
|
|
227
|
+
return changes, SyncCursor(i + 1, 0, None, watermark), True
|
|
228
|
+
return changes, None, False
|
|
229
|
+
|
|
230
|
+
# Every remaining collection drained within the budget — delta exhausted.
|
|
231
|
+
return changes, None, False
|