trueppm-api 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +27 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +109 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +459 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +178 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +863 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +597 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +528 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +280 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +551 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +566 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +993 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +537 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +541 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +458 -0
- trueppm_api/apps/projects/backfill.py +459 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1244 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1979 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/mc_admission.py +181 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2908 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +446 -0
- trueppm_api/apps/sso/services.py +1473 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +440 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +371 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +254 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/demo_read_only.py +314 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +1221 -0
- trueppm_api/core/password_reset.py +513 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/core/worker_selfheal.py +205 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""WebSocket consumer for real-time project board events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
|
13
|
+
|
|
14
|
+
from trueppm_api.apps.access.permissions import role_can_subscribe_live
|
|
15
|
+
from trueppm_api.apps.observability.otel import metrics
|
|
16
|
+
from trueppm_api.apps.sync.ws_auth import authenticate_scope, warn_if_legacy
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
_PRESENCE_TTL = 60 # seconds — refreshed by the per-socket keepalive below
|
|
21
|
+
|
|
22
|
+
# How often each open socket re-arms the presence TTL (#2607).
|
|
23
|
+
#
|
|
24
|
+
# The TTL was originally refreshed only from ``receive_json`` — a heartbeat no
|
|
25
|
+
# client sends. The web client's project socket is receive-only, so in practice
|
|
26
|
+
# nothing ever refreshed it and the roster emptied about a minute
|
|
27
|
+
# into every session while everyone was still connected and editing.
|
|
28
|
+
#
|
|
29
|
+
# Refreshing on *traffic* would not fix it either: a quiet project generates no
|
|
30
|
+
# frames, and presence means "this person is here", not "this person is busy".
|
|
31
|
+
# So each socket re-arms its own TTL on a timer for as long as it is open, which
|
|
32
|
+
# needs no client change and therefore covers mobile and any future client for
|
|
33
|
+
# free. The TTL keeps doing the job it is actually good at — reaping a socket
|
|
34
|
+
# that died without ``disconnect`` running (worker kill, network partition).
|
|
35
|
+
#
|
|
36
|
+
# Half the TTL: one missed tick (a slow event loop, a brief Redis blip) still
|
|
37
|
+
# leaves a full interval of margin before anyone is wrongly reaped.
|
|
38
|
+
_PRESENCE_REFRESH_INTERVAL = _PRESENCE_TTL // 2
|
|
39
|
+
|
|
40
|
+
# Upper bound on events replayed on a single reconnect (ADR-0236). A client
|
|
41
|
+
# further behind than this is cheaper to fully refetch than to stream, so it gets
|
|
42
|
+
# a ``resync_required`` frame instead — matching the "since older than retention"
|
|
43
|
+
# fallback.
|
|
44
|
+
_REPLAY_CAP = 1000
|
|
45
|
+
|
|
46
|
+
# Close codes (#4375). Which side of the handshake a refusal happens on decides
|
|
47
|
+
# whether a browser can ever see the code:
|
|
48
|
+
#
|
|
49
|
+
# - Before ``accept``, the ASGI server (uvicorn in production) answers the HTTP
|
|
50
|
+
# upgrade with a 403 instead of switching protocols, and a browser reports close
|
|
51
|
+
# code **1006** with no application code at all. The client cannot
|
|
52
|
+
# tell "you may not" from "the network dropped", so it retries forever.
|
|
53
|
+
# - After ``accept``, the close frame carries the code and the browser's
|
|
54
|
+
# ``CloseEvent.code`` is the value sent here.
|
|
55
|
+
#
|
|
56
|
+
# So every *policy* refusal a legitimate client must act on is post-accept:
|
|
57
|
+
#
|
|
58
|
+
# - ``WS_CLOSE_UNAUTHENTICATED`` (4001): no valid ticket. Accepting first reveals
|
|
59
|
+
# nothing — the check runs before the project id is even read.
|
|
60
|
+
# - ``WS_CLOSE_FORBIDDEN`` (4003): not a member, a soft-deleted member, a role
|
|
61
|
+
# below the live-update floor, or a project that does not exist. All four send
|
|
62
|
+
# the same code from the same point, so the code is not an existence oracle;
|
|
63
|
+
# also sent by ``connection_evict`` on a live socket.
|
|
64
|
+
#
|
|
65
|
+
# The one pre-accept rejection is the router catch-all in ``asgi.py`` (4404, an
|
|
66
|
+
# unrouted path). No client the product ships connects to one, there is nothing
|
|
67
|
+
# for a browser to act on, and refusing at the upgrade keeps scanners off the
|
|
68
|
+
# accept path. Its code never reaches a browser, by design.
|
|
69
|
+
WS_CLOSE_UNAUTHENTICATED = 4001
|
|
70
|
+
WS_CLOSE_FORBIDDEN = 4003
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _presence_key(project_pk: str) -> str:
|
|
74
|
+
return f"project:{project_pk}:presence"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_since(query_string: bytes) -> int:
|
|
78
|
+
"""Parse a non-negative ``?since=<seq>`` from the raw ASGI query string.
|
|
79
|
+
|
|
80
|
+
Returns 0 when the param is absent or non-numeric — 0 means "no replay, I am
|
|
81
|
+
a fresh client" (the initial REST load already has current state). Mirrors the
|
|
82
|
+
naive byte-split parsing in ``ws_auth._parse_query``; ``since`` is plain digits
|
|
83
|
+
so no percent-decoding is needed.
|
|
84
|
+
"""
|
|
85
|
+
for pair in query_string.split(b"&"):
|
|
86
|
+
if pair.startswith(b"since="):
|
|
87
|
+
try:
|
|
88
|
+
# Clamp to [0, bigint max]: a value above Postgres' bigint range
|
|
89
|
+
# (sys.maxsize == 2**63-1 on 64-bit) would raise mid-query when
|
|
90
|
+
# bound as a bigint, closing the socket on a bogus input.
|
|
91
|
+
return max(min(int(pair[len(b"since=") :]), sys.maxsize), 0)
|
|
92
|
+
except ValueError:
|
|
93
|
+
return 0
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ProjectConsumer(AsyncJsonWebsocketConsumer): # type: ignore[misc]
|
|
98
|
+
"""Pushes project board events to subscribed clients.
|
|
99
|
+
|
|
100
|
+
Authentication: A single-use ticket supplied as `?ticket=<id>` (ADR-0141),
|
|
101
|
+
issued by `POST /api/v1/ws/ticket/` and consumed from Redis
|
|
102
|
+
on connect. The legacy `?token=<jwt>` query param is disabled
|
|
103
|
+
by default and opt-in only via
|
|
104
|
+
`TRUEPPM_WS_LEGACY_TOKEN_AUTH_ENABLED` (#1723). Connection is
|
|
105
|
+
refused (4001) if no valid credential is present.
|
|
106
|
+
|
|
107
|
+
Authorization: The user must clear ``role_can_subscribe_live`` (Member+) on
|
|
108
|
+
the requested project; anyone else is refused (4003). The
|
|
109
|
+
project serializer's ``can_subscribe_live`` emits the same
|
|
110
|
+
verdict so the web client does not dial a socket it will be
|
|
111
|
+
refused (#4375).
|
|
112
|
+
|
|
113
|
+
Refusals: Accepted first, then closed with the code, so a browser's
|
|
114
|
+
``CloseEvent`` carries it — see the module-level close-code
|
|
115
|
+
notes. A pre-accept close surfaces as 1006 in a browser.
|
|
116
|
+
|
|
117
|
+
Presence: On connect, the user is added to a Redis hash keyed by
|
|
118
|
+
`project:{pk}:presence`. A `presence_join` event is
|
|
119
|
+
broadcast to the group. On disconnect, the entry is
|
|
120
|
+
removed and a `presence_leave` event is broadcast.
|
|
121
|
+
The hash expires after _PRESENCE_TTL seconds and is re-armed
|
|
122
|
+
every _PRESENCE_REFRESH_INTERVAL by a per-socket keepalive
|
|
123
|
+
task, so the roster reflects who is connected rather than
|
|
124
|
+
who is generating traffic (#2607). The TTL remains the
|
|
125
|
+
backstop for a socket that dies without `disconnect`.
|
|
126
|
+
|
|
127
|
+
Receive: Client→server messages also reset the presence TTL, but no
|
|
128
|
+
client sends any — the keepalive above is what keeps the
|
|
129
|
+
roster alive. All content is silently discarded.
|
|
130
|
+
|
|
131
|
+
Not here: There are **no row-level or per-field locks**, and there will
|
|
132
|
+
not be — ADR-0914 (#2721) is the decision, not an omission.
|
|
133
|
+
Presence answers "who is here"; it does not answer "who is
|
|
134
|
+
busy", and it never answers "who may write".
|
|
135
|
+
|
|
136
|
+
The reason is not cost. A soft lock expires on inactivity and
|
|
137
|
+
on disconnect and never gates the write, so it cannot prevent
|
|
138
|
+
the conflict it appears to prevent, and every one of its
|
|
139
|
+
failure modes is silent in the reassuring direction: a ring
|
|
140
|
+
missing while someone is still typing implies a safety nobody
|
|
141
|
+
held, and a ring left on an idle row makes a planner wait for
|
|
142
|
+
a hold that does not exist. What actually protects the data is
|
|
143
|
+
``server_version`` optimistic locking returning 409.
|
|
144
|
+
|
|
145
|
+
Coming here: Row *edit-awareness* is accepted, and it is not a lock.
|
|
146
|
+
ADR-0914 adopts ADR-0046 §1's advisory
|
|
147
|
+
``state: editing | idle`` broadcast for the Schedule outline:
|
|
148
|
+
it renders who is typing where, it never gates a write, and
|
|
149
|
+
its copy never claims exclusivity. Staleness is therefore
|
|
150
|
+
cheap — it misleads about a fact rather than about permission,
|
|
151
|
+
which is the distinction the whole decision turns on.
|
|
152
|
+
|
|
153
|
+
It is not implemented yet, and implementing it makes this
|
|
154
|
+
consumer **bidirectional** — every open project socket becomes
|
|
155
|
+
a Redis write path for any Member. ADR-0914 §6 binds the
|
|
156
|
+
constraints that has to carry: the row id must be validated as
|
|
157
|
+
belonging to *this* project (otherwise the socket becomes the
|
|
158
|
+
cross-project existence oracle ADR-0772 removed), the frame
|
|
159
|
+
rate must be bounded, the payload fixed and size-bounded, and
|
|
160
|
+
nothing persisted. Do not add the frame handler without them.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
#: One async Redis client per socket, created lazily on the first presence
|
|
164
|
+
#: call and closed in ``disconnect`` (#1530). ``None`` until first use / after
|
|
165
|
+
#: close, so ``disconnect`` before ``connect`` is a safe no-op.
|
|
166
|
+
_redis: Any = None
|
|
167
|
+
|
|
168
|
+
#: True once this socket was counted into the active-connections gauge (after
|
|
169
|
+
#: accept). Guards the disconnect decrement so an early-rejected connect — which
|
|
170
|
+
#: returns before the increment — never underflows the gauge (#1900).
|
|
171
|
+
_ws_counted: bool = False
|
|
172
|
+
|
|
173
|
+
async def websocket_connect(self, message: dict[str, Any]) -> None:
|
|
174
|
+
"""Override to run sync DB queries before accepting the socket."""
|
|
175
|
+
# Authenticate the handshake before calling super() which sends ACCEPT.
|
|
176
|
+
# Prefers the single-use ?ticket= (ADR-0141); the ?token=<jwt> fallback is
|
|
177
|
+
# off by default, opt-in via TRUEPPM_WS_LEGACY_TOKEN_AUTH_ENABLED (#1723).
|
|
178
|
+
scope = self.scope
|
|
179
|
+
auth = await authenticate_scope(scope)
|
|
180
|
+
if auth.user is None:
|
|
181
|
+
await self._refuse(WS_CLOSE_UNAUTHENTICATED)
|
|
182
|
+
return
|
|
183
|
+
user = auth.user
|
|
184
|
+
|
|
185
|
+
# Resolve project PK from URL route kwargs.
|
|
186
|
+
project_pk = str(scope["url_route"]["kwargs"]["pk"])
|
|
187
|
+
warn_if_legacy(auth, consumer="ProjectConsumer", project_pk=project_pk)
|
|
188
|
+
|
|
189
|
+
# Live-update floor (Member+; Viewers and non-members are refused). The
|
|
190
|
+
# predicate is shared with ProjectSerializer.can_subscribe_live so the client
|
|
191
|
+
# gate and this one cannot drift (#4375).
|
|
192
|
+
role = await self._get_role(user, project_pk)
|
|
193
|
+
if not role_can_subscribe_live(role):
|
|
194
|
+
await self._refuse(WS_CLOSE_FORBIDDEN)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
self.project_pk = project_pk
|
|
198
|
+
self.group_name = f"project_{project_pk}"
|
|
199
|
+
self._user = user
|
|
200
|
+
self._display_name: str = user.get_full_name() or user.username
|
|
201
|
+
|
|
202
|
+
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
|
203
|
+
|
|
204
|
+
# Register presence in Redis and notify other clients.
|
|
205
|
+
await self._presence_join()
|
|
206
|
+
|
|
207
|
+
# Keep this socket's presence alive for as long as it is open (#2607).
|
|
208
|
+
# Started before accept so a socket that never sees a single frame is
|
|
209
|
+
# still represented in the roster.
|
|
210
|
+
self._presence_task: asyncio.Task[None] | None = asyncio.create_task(
|
|
211
|
+
self._presence_keepalive()
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
await super().websocket_connect(message)
|
|
215
|
+
|
|
216
|
+
# Count this socket into the active-connections gauge only after accept, so
|
|
217
|
+
# rejected handshakes (which returned above) are never counted. The paired
|
|
218
|
+
# decrement in disconnect() is guarded by _ws_counted so the gauge balances
|
|
219
|
+
# even if the connection is torn down at any later point (#1900).
|
|
220
|
+
metrics.record_ws_connection_opened()
|
|
221
|
+
self._ws_counted = True
|
|
222
|
+
|
|
223
|
+
# Replay events missed while this client was disconnected (ADR-0236).
|
|
224
|
+
# Runs AFTER accept (send_json needs an open socket) but still inside
|
|
225
|
+
# websocket_connect: AsyncJsonWebsocketConsumer's dispatch loop is blocked
|
|
226
|
+
# until this coroutine returns, so no live board_event is delivered during
|
|
227
|
+
# replay — replayed (older seq) frames always precede live (newer seq)
|
|
228
|
+
# ones. group_add ran first, so an event broadcast mid-replay is queued and
|
|
229
|
+
# delivered right after these frames, never lost. Only members reach here
|
|
230
|
+
# (Viewers were rejected above), and the query is scoped to this project,
|
|
231
|
+
# so replay honors the same authz as the live stream (no cross-project
|
|
232
|
+
# leak).
|
|
233
|
+
since = _parse_since(scope.get("query_string", b""))
|
|
234
|
+
if since > 0:
|
|
235
|
+
await self._replay_missed_events(since)
|
|
236
|
+
|
|
237
|
+
async def disconnect(self, code: int) -> None:
|
|
238
|
+
# Balance the active-connections gauge: decrement only if this socket was
|
|
239
|
+
# actually counted at accept (early-rejected connects never were) (#1900).
|
|
240
|
+
if self._ws_counted:
|
|
241
|
+
metrics.record_ws_connection_closed()
|
|
242
|
+
self._ws_counted = False
|
|
243
|
+
|
|
244
|
+
if hasattr(self, "group_name"):
|
|
245
|
+
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
|
246
|
+
|
|
247
|
+
# Stop the keepalive before releasing presence, so a tick in flight cannot
|
|
248
|
+
# re-arm the TTL on a key the leave below is about to clean up.
|
|
249
|
+
task = getattr(self, "_presence_task", None)
|
|
250
|
+
if task is not None:
|
|
251
|
+
task.cancel()
|
|
252
|
+
with contextlib.suppress(BaseException):
|
|
253
|
+
await task
|
|
254
|
+
self._presence_task = None
|
|
255
|
+
|
|
256
|
+
if hasattr(self, "_user"):
|
|
257
|
+
await self._presence_leave()
|
|
258
|
+
|
|
259
|
+
# Close the single Redis client opened for this socket's presence calls.
|
|
260
|
+
# Each open client owns a connection pool; leaving it open on disconnect
|
|
261
|
+
# leaks connections against Valkey/fd limits (#1530). Guarded because
|
|
262
|
+
# disconnect can fire before connect ever created a client (early close).
|
|
263
|
+
client = getattr(self, "_redis", None)
|
|
264
|
+
if client is not None:
|
|
265
|
+
await client.aclose()
|
|
266
|
+
self._redis = None
|
|
267
|
+
|
|
268
|
+
async def receive_json(self, content: Any, **kwargs: Any) -> None:
|
|
269
|
+
# Heartbeat: client messages refresh the presence TTL so the user is
|
|
270
|
+
# not evicted while the socket is open but idle.
|
|
271
|
+
if hasattr(self, "_user"):
|
|
272
|
+
await self._presence_heartbeat()
|
|
273
|
+
|
|
274
|
+
async def board_event(self, event: dict[str, Any]) -> None:
|
|
275
|
+
"""Handle channel layer group_send messages of type 'board.event'.
|
|
276
|
+
|
|
277
|
+
A board.event can be dispatched to this channel *after* its socket has
|
|
278
|
+
already closed: the client disconnects (or is evicted, code 4003) while
|
|
279
|
+
a group_send for the same project is still in flight. The channel is not
|
|
280
|
+
removed from the group until ``disconnect`` runs ``group_discard``, and
|
|
281
|
+
messages already queued are still delivered — and ``await_many_dispatch``
|
|
282
|
+
does not guarantee the ``websocket.disconnect`` is dispatched before an
|
|
283
|
+
already-queued ``board.event``, so a closed-state flag cannot fully
|
|
284
|
+
prevent the race. Sending on a closed socket raises ``RuntimeError`` from
|
|
285
|
+
the ASGI server, so guard the send and drop the stale event. Losing it is
|
|
286
|
+
safe: the broadcast is best-effort by design and clients reconcile via
|
|
287
|
+
the sync delta on reconnect (see ``broadcast.py``). (#1108)
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
from trueppm_api.apps.sync.broadcast import SEQ_SCOPE_PROJECT, WS_PROTOCOL_VERSION
|
|
291
|
+
|
|
292
|
+
# Forward a seq only when the sender marked it as the per-project
|
|
293
|
+
# sequence (ADR-1285, #4386). A pre-#4386 pod in a rolling update sends
|
|
294
|
+
# its global row id unmarked; adopting that as a cursor would put the
|
|
295
|
+
# client in the wrong number space, so it goes out as an unsequenced
|
|
296
|
+
# frame instead — applied, never gated.
|
|
297
|
+
seq = event.get("seq") if event.get("seq_scope") == SEQ_SCOPE_PROJECT else None
|
|
298
|
+
await self.send_json(
|
|
299
|
+
{
|
|
300
|
+
"protocol_version": event.get("protocol_version", WS_PROTOCOL_VERSION),
|
|
301
|
+
"event_type": event.get("event_type"),
|
|
302
|
+
"payload": event.get("payload"),
|
|
303
|
+
# Replay sequence for persisted events, or None for ephemeral
|
|
304
|
+
# ones (presence / task-run progress). The client advances its
|
|
305
|
+
# last-seen ``seq`` off this and requests ``?since=`` on
|
|
306
|
+
# reconnect (ADR-0236).
|
|
307
|
+
"seq": seq,
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
except RuntimeError:
|
|
311
|
+
logger.debug(
|
|
312
|
+
"Dropped board.event for closed socket on project %s",
|
|
313
|
+
getattr(self, "project_pk", "?"),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
async def _replay_missed_events(self, since: int) -> None:
|
|
317
|
+
"""Stream buffered BoardEvents with ``seq > since``, or signal a resync.
|
|
318
|
+
|
|
319
|
+
``seq`` is dense per project (ADR-1285, #4386), so completeness is a count,
|
|
320
|
+
not a guess: the project's counter value is the head, the client is owed
|
|
321
|
+
exactly ``head - since`` events, and a replay is sent only when exactly that
|
|
322
|
+
many rows survive. Anything else gets one ``resync_required`` frame and the
|
|
323
|
+
client refetches:
|
|
324
|
+
|
|
325
|
+
- ``since > head`` — a cursor the project's sequence never reached. One
|
|
326
|
+
example is a global id an old pod issued during the #4386 rolling update
|
|
327
|
+
for a project created in that window, whose counter starts at 0.
|
|
328
|
+
``latest_seq`` re-baselines it.
|
|
329
|
+
- A global-id cursor from before or during the #4386 upgrade, for a project
|
|
330
|
+
that existed at the upgrade, is below the project's seed
|
|
331
|
+
(``seed_board_event_sequences``, offset by
|
|
332
|
+
``BOARD_EVENT_SEQ_UPGRADE_OFFSET``). It is owed more events than carry a
|
|
333
|
+
``seq``, so it falls into the "fewer rows than owed" case below and
|
|
334
|
+
resyncs.
|
|
335
|
+
- ``head - since > _REPLAY_CAP`` — cheaper to refetch than to stream.
|
|
336
|
+
- fewer rows than owed — the purge reaped part of the gap.
|
|
337
|
+
|
|
338
|
+
``since == head`` replays nothing and needs no resync. The row query is
|
|
339
|
+
bounded above by the head it was counted against, so an event committed
|
|
340
|
+
between the two reads cannot inflate the count; it reaches the client live,
|
|
341
|
+
because the socket joined the group before this ran.
|
|
342
|
+
|
|
343
|
+
Every value read here is scoped to this project. The old global-id design
|
|
344
|
+
had to read a *global* minimum id to detect purge gaps; the density check
|
|
345
|
+
needs nothing outside the project, so no instance-wide figure reaches a
|
|
346
|
+
member.
|
|
347
|
+
"""
|
|
348
|
+
from channels.db import database_sync_to_async
|
|
349
|
+
|
|
350
|
+
from trueppm_api.apps.sync.broadcast import WS_PROTOCOL_VERSION
|
|
351
|
+
|
|
352
|
+
@database_sync_to_async # type: ignore[untyped-decorator]
|
|
353
|
+
def _fetch() -> tuple[list[dict[str, Any]], int]:
|
|
354
|
+
from trueppm_api.apps.sync.models import BoardEvent, BoardEventSequence
|
|
355
|
+
|
|
356
|
+
head = (
|
|
357
|
+
BoardEventSequence.objects.filter(project_id=self.project_pk)
|
|
358
|
+
.values_list("value", flat=True)
|
|
359
|
+
.first()
|
|
360
|
+
) or 0
|
|
361
|
+
owed = head - since
|
|
362
|
+
if owed <= 0 or owed > _REPLAY_CAP:
|
|
363
|
+
return [], head
|
|
364
|
+
rows = cast(
|
|
365
|
+
"list[dict[str, Any]]",
|
|
366
|
+
list(
|
|
367
|
+
BoardEvent.objects.filter(
|
|
368
|
+
project_id=self.project_pk, seq__gt=since, seq__lte=head
|
|
369
|
+
)
|
|
370
|
+
.order_by("seq")
|
|
371
|
+
.values("seq", "event_type", "payload")
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
return rows, head
|
|
375
|
+
|
|
376
|
+
rows, head = await _fetch()
|
|
377
|
+
owed = head - since
|
|
378
|
+
if owed == 0:
|
|
379
|
+
return
|
|
380
|
+
if owed < 0 or len(rows) != owed:
|
|
381
|
+
await self.send_json(
|
|
382
|
+
{
|
|
383
|
+
"protocol_version": WS_PROTOCOL_VERSION,
|
|
384
|
+
"event_type": "resync_required",
|
|
385
|
+
# latest_seq lets the client baseline its last-seen sequence to
|
|
386
|
+
# the buffer head after it refetches, so the next reconnect
|
|
387
|
+
# doesn't re-request the purged gap.
|
|
388
|
+
"payload": {"latest_seq": head},
|
|
389
|
+
"seq": None,
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
for row in rows:
|
|
395
|
+
await self.send_json(
|
|
396
|
+
{
|
|
397
|
+
"protocol_version": WS_PROTOCOL_VERSION,
|
|
398
|
+
"event_type": row["event_type"],
|
|
399
|
+
"payload": row["payload"],
|
|
400
|
+
"seq": row["seq"],
|
|
401
|
+
"replayed": True,
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
async def connection_evict(self, event: dict[str, Any]) -> None:
|
|
406
|
+
"""Close this socket if its user's project access was just revoked (#813).
|
|
407
|
+
|
|
408
|
+
Handles channel-layer messages of type 'connection.evict'. Membership is
|
|
409
|
+
only checked at connect; this evicts a live socket when the user's
|
|
410
|
+
ProjectMembership is soft-deleted or demoted below Member, so revocation
|
|
411
|
+
takes effect immediately instead of lasting until the client disconnects.
|
|
412
|
+
Sockets for other users in the group ignore the message.
|
|
413
|
+
|
|
414
|
+
``all=True`` (``evict_project_group``, #4577) closes every socket in the group:
|
|
415
|
+
the project itself was trashed, so no member keeps a role on it.
|
|
416
|
+
"""
|
|
417
|
+
if event.get("all") is True:
|
|
418
|
+
await self.close(code=WS_CLOSE_FORBIDDEN)
|
|
419
|
+
return
|
|
420
|
+
if hasattr(self, "_user") and event.get("user_id") == str(self._user.pk):
|
|
421
|
+
await self.close(code=WS_CLOSE_FORBIDDEN)
|
|
422
|
+
|
|
423
|
+
async def _refuse(self, code: int) -> None:
|
|
424
|
+
"""Refuse a connect so the browser actually receives ``code`` (#4375).
|
|
425
|
+
|
|
426
|
+
Accept, then close. Closing before accept makes the ASGI server (uvicorn)
|
|
427
|
+
answer the HTTP upgrade with a 403, which a browser reports as code 1006 — so the
|
|
428
|
+
client could never recognize 4001 or 4003 as terminal and kept reconnecting.
|
|
429
|
+
Nothing has been joined or counted at this point (no group, no presence, no
|
|
430
|
+
gauge increment), so ``disconnect`` is a no-op for a refused socket.
|
|
431
|
+
"""
|
|
432
|
+
await self.accept()
|
|
433
|
+
await self.close(code=code)
|
|
434
|
+
|
|
435
|
+
# ------------------------------------------------------------------
|
|
436
|
+
# Presence helpers
|
|
437
|
+
# ------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
async def _presence_join(self) -> None:
|
|
440
|
+
"""Add this user to the Redis presence hash and broadcast presence_join."""
|
|
441
|
+
r = await self._get_redis()
|
|
442
|
+
key = _presence_key(self.project_pk)
|
|
443
|
+
entry = json.dumps({"user_id": str(self._user.pk), "display_name": self._display_name})
|
|
444
|
+
await r.hset(key, str(self._user.pk), entry)
|
|
445
|
+
await r.expire(key, _PRESENCE_TTL)
|
|
446
|
+
|
|
447
|
+
# Async-native broadcast: we are on the consumer's event loop, so the
|
|
448
|
+
# sync broadcast_board_event (which wraps group_send in async_to_sync)
|
|
449
|
+
# would raise "cannot use AsyncToSync in the same thread" here (#958).
|
|
450
|
+
from trueppm_api.apps.sync.broadcast import abroadcast_board_event
|
|
451
|
+
|
|
452
|
+
await abroadcast_board_event(
|
|
453
|
+
project_id=self.project_pk,
|
|
454
|
+
event_type="presence_join",
|
|
455
|
+
payload={"user_id": str(self._user.pk), "display_name": self._display_name},
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
async def _presence_leave(self) -> None:
|
|
459
|
+
"""Remove this user from the Redis presence hash and broadcast presence_leave."""
|
|
460
|
+
r = await self._get_redis()
|
|
461
|
+
key = _presence_key(self.project_pk)
|
|
462
|
+
await r.hdel(key, str(self._user.pk))
|
|
463
|
+
|
|
464
|
+
# Async-native broadcast — see _presence_join for why the sync helper
|
|
465
|
+
# cannot be used from the consumer's event loop (#958).
|
|
466
|
+
from trueppm_api.apps.sync.broadcast import abroadcast_board_event
|
|
467
|
+
|
|
468
|
+
await abroadcast_board_event(
|
|
469
|
+
project_id=self.project_pk,
|
|
470
|
+
event_type="presence_leave",
|
|
471
|
+
payload={"user_id": str(self._user.pk), "display_name": self._display_name},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
async def _presence_heartbeat(self) -> None:
|
|
475
|
+
"""Refresh the presence TTL so an active user is not evicted."""
|
|
476
|
+
r = await self._get_redis()
|
|
477
|
+
await r.expire(_presence_key(self.project_pk), _PRESENCE_TTL)
|
|
478
|
+
|
|
479
|
+
async def _presence_keepalive(self) -> None:
|
|
480
|
+
"""Re-arm the presence TTL every interval for as long as this socket is open.
|
|
481
|
+
|
|
482
|
+
Started at connect and cancelled at disconnect, so presence tracks socket
|
|
483
|
+
lifetime rather than traffic (#2607) — an idle project's roster is as
|
|
484
|
+
correct as a busy one's.
|
|
485
|
+
|
|
486
|
+
Never allowed to kill the socket. A Redis blip here means one skipped
|
|
487
|
+
refresh, and the next tick re-arms a TTL that has not yet expired; letting
|
|
488
|
+
the exception escape would tear down a working connection over a
|
|
489
|
+
supporting affordance. ``CancelledError`` is re-raised so ``disconnect``'s
|
|
490
|
+
await returns promptly rather than swallowing its own cancellation.
|
|
491
|
+
"""
|
|
492
|
+
while True:
|
|
493
|
+
try:
|
|
494
|
+
await asyncio.sleep(_PRESENCE_REFRESH_INTERVAL)
|
|
495
|
+
await self._presence_heartbeat()
|
|
496
|
+
except asyncio.CancelledError:
|
|
497
|
+
raise
|
|
498
|
+
except Exception:
|
|
499
|
+
logger.warning(
|
|
500
|
+
"presence keepalive refresh failed for project %s",
|
|
501
|
+
getattr(self, "project_pk", "?"),
|
|
502
|
+
exc_info=True,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
async def _get_redis(self) -> Any:
|
|
506
|
+
"""Return this consumer's async Redis client, creating it once and caching it.
|
|
507
|
+
|
|
508
|
+
One client (and its connection pool) is created lazily on the socket's
|
|
509
|
+
first presence call — which happens during ``websocket_connect`` — and
|
|
510
|
+
reused for every subsequent presence call (join, leave, and each
|
|
511
|
+
heartbeat frame) for the socket's lifetime, then closed in
|
|
512
|
+
``disconnect``. The presence path previously built a fresh
|
|
513
|
+
``aioredis.from_url`` client on *every* call: because ``receive_json``
|
|
514
|
+
heartbeats fire on every inbound frame and nothing ever closed them, the
|
|
515
|
+
Channels worker steadily leaked a connection pool per message until
|
|
516
|
+
Valkey/fd limits were hit (#1530). Caching the client here bounds it to
|
|
517
|
+
one pool per socket, matching the explicit close pattern in ``ws_auth``.
|
|
518
|
+
"""
|
|
519
|
+
client = getattr(self, "_redis", None)
|
|
520
|
+
if client is None:
|
|
521
|
+
from trueppm_api.core import valkey
|
|
522
|
+
|
|
523
|
+
client = valkey.async_client(valkey.DB_CELERY, decode_responses=True)
|
|
524
|
+
self._redis = client
|
|
525
|
+
return client
|
|
526
|
+
|
|
527
|
+
# ------------------------------------------------------------------
|
|
528
|
+
# Private helpers (run in thread pool via sync_to_async internally)
|
|
529
|
+
# ------------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
async def _get_role(self, user: Any, project_pk: str) -> int | None:
|
|
532
|
+
"""Return the user's role ordinal on the project, or None if not a member."""
|
|
533
|
+
from channels.db import database_sync_to_async
|
|
534
|
+
|
|
535
|
+
@database_sync_to_async # type: ignore[untyped-decorator]
|
|
536
|
+
def _query(u: Any, pk: str) -> int | None:
|
|
537
|
+
from trueppm_api.apps.access.models import ProjectMembership
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
# project__is_deleted (#4577): a trashed project confers no role, so
|
|
541
|
+
# a socket cannot subscribe to one — same floor as `_membership_role`.
|
|
542
|
+
return ProjectMembership.objects.get(
|
|
543
|
+
project_id=pk, user=u, is_deleted=False, project__is_deleted=False
|
|
544
|
+
).role
|
|
545
|
+
except ProjectMembership.DoesNotExist:
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
return await _query(user, project_pk) # type: ignore[no-any-return]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Generated by Django 6.0.3 on 2026-05-24 12:15
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
("projects", "0046_merge_20260523_2248"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name="SyncBatch",
|
|
19
|
+
fields=[
|
|
20
|
+
(
|
|
21
|
+
"id",
|
|
22
|
+
models.UUIDField(
|
|
23
|
+
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
("client_batch_id", models.UUIDField()),
|
|
27
|
+
(
|
|
28
|
+
"status",
|
|
29
|
+
models.CharField(
|
|
30
|
+
choices=[("pending", "Pending"), ("completed", "Completed")],
|
|
31
|
+
db_index=True,
|
|
32
|
+
default="pending",
|
|
33
|
+
max_length=12,
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
("response_body", models.JSONField(blank=True, default=dict)),
|
|
37
|
+
("response_status", models.PositiveSmallIntegerField(default=200)),
|
|
38
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
39
|
+
(
|
|
40
|
+
"project",
|
|
41
|
+
models.ForeignKey(
|
|
42
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
43
|
+
related_name="sync_batches",
|
|
44
|
+
to="projects.project",
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
options={
|
|
49
|
+
"ordering": ["created_at"],
|
|
50
|
+
"indexes": [models.Index(fields=["created_at"], name="syncbatch_created_idx")],
|
|
51
|
+
"constraints": [
|
|
52
|
+
models.UniqueConstraint(
|
|
53
|
+
fields=("project", "client_batch_id"),
|
|
54
|
+
name="syncbatch_project_client_batch_uniq",
|
|
55
|
+
)
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Generated by Django 5.2.14 on 2026-06-23 21:38
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.db import migrations, models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
replaces = [
|
|
12
|
+
("sync", "0001_initial"),
|
|
13
|
+
("sync", "0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
initial = True
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
("projects", "0046_merge_20260523_2248"),
|
|
20
|
+
("projects", "0054_sprintscopechange_goal_impact_and_more"),
|
|
21
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
operations = [
|
|
25
|
+
migrations.CreateModel(
|
|
26
|
+
name="SyncBatch",
|
|
27
|
+
fields=[
|
|
28
|
+
(
|
|
29
|
+
"id",
|
|
30
|
+
models.UUIDField(
|
|
31
|
+
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
("client_batch_id", models.UUIDField()),
|
|
35
|
+
(
|
|
36
|
+
"status",
|
|
37
|
+
models.CharField(
|
|
38
|
+
choices=[("pending", "Pending"), ("completed", "Completed")],
|
|
39
|
+
db_index=True,
|
|
40
|
+
default="pending",
|
|
41
|
+
max_length=12,
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
("response_body", models.JSONField(blank=True, default=dict)),
|
|
45
|
+
("response_status", models.PositiveSmallIntegerField(default=200)),
|
|
46
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
47
|
+
(
|
|
48
|
+
"project",
|
|
49
|
+
models.ForeignKey(
|
|
50
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
51
|
+
related_name="sync_batches",
|
|
52
|
+
to="projects.project",
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"actor_user",
|
|
57
|
+
models.ForeignKey(
|
|
58
|
+
blank=True,
|
|
59
|
+
null=True,
|
|
60
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
61
|
+
related_name="sync_batches",
|
|
62
|
+
to=settings.AUTH_USER_MODEL,
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
options={
|
|
67
|
+
"ordering": ["created_at"],
|
|
68
|
+
"indexes": [models.Index(fields=["created_at"], name="syncbatch_created_idx")],
|
|
69
|
+
"constraints": [
|
|
70
|
+
models.UniqueConstraint(
|
|
71
|
+
fields=("project", "actor_user", "client_batch_id"),
|
|
72
|
+
name="syncbatch_project_actor_client_batch_uniq",
|
|
73
|
+
)
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
]
|