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,287 @@
|
|
|
1
|
+
"""Data-backfill helpers for the projects app migrations.
|
|
2
|
+
|
|
3
|
+
These functions are extracted from migration files so tests can import them
|
|
4
|
+
without coupling to migration file names, which break on squash (CLAUDE.md rule 3).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, NamedTuple
|
|
11
|
+
|
|
12
|
+
from django.db.models import Count, F
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def backfill_risk_short_ids(apps: Any, schema_editor: Any) -> None:
|
|
18
|
+
"""Renumber existing risks to a contiguous decimal sequence per project (#929).
|
|
19
|
+
|
|
20
|
+
Existing risks carry 8-char hex short_ids allocated from the shared
|
|
21
|
+
``object_sequence`` — the bug behind every register row collapsing to
|
|
22
|
+
``R-0000``. For each project, order *all* risks (including soft-deleted ones)
|
|
23
|
+
by ``created_at`` and assign ``1, 2, 3 …`` as the new decimal ``short_id``,
|
|
24
|
+
then set ``project.risk_sequence`` to the high-water mark so new risks never
|
|
25
|
+
reuse a number.
|
|
26
|
+
|
|
27
|
+
Soft-deleted risks are included in the ordering on purpose: the VoC panel
|
|
28
|
+
required immutability with *no reuse after deletion* (deletion leaves a gap —
|
|
29
|
+
"where is R-8?" is an expected audit finding, not a bug). Numbering only the
|
|
30
|
+
live rows would silently re-pack the sequence and reuse a dead risk's number.
|
|
31
|
+
|
|
32
|
+
``server_version`` is bumped on every renumbered risk so offline/mobile
|
|
33
|
+
clients re-pull the corrected identifier on their next sync delta. The writes
|
|
34
|
+
use bulk ``.update()`` / ``bulk_update()`` — a one-time data correction, not
|
|
35
|
+
an audited user edit, so they intentionally leave no HistoricalRisk rows
|
|
36
|
+
(matching the 0071 backfill precedent).
|
|
37
|
+
"""
|
|
38
|
+
Project = apps.get_model("projects", "Project")
|
|
39
|
+
Risk = apps.get_model("projects", "Risk")
|
|
40
|
+
|
|
41
|
+
for project_id in Project.objects.values_list("pk", flat=True):
|
|
42
|
+
risks = list(Risk.objects.filter(project_id=project_id).order_by("created_at", "pk"))
|
|
43
|
+
if not risks:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# Two-pass renumber. ``unique_risk_short_id_per_project`` is a plain
|
|
47
|
+
# (non-deferrable) UniqueConstraint, so Postgres checks it per-row inside
|
|
48
|
+
# the bulk UPDATE. Writing the final decimals in one pass is only safe
|
|
49
|
+
# because legacy hex short_ids can't equal a bare decimal — but a
|
|
50
|
+
# mixed-version deploy window (new app code minting decimal short_ids
|
|
51
|
+
# before this migration runs) could break that assumption and abort the
|
|
52
|
+
# migration with an IntegrityError. Routing through a sentinel namespace
|
|
53
|
+
# that is disjoint from BOTH legacy hex (uppercase ``[0-9A-F]``) and the
|
|
54
|
+
# final decimals (the lowercase ``x`` prefix guarantees this) makes the
|
|
55
|
+
# renumber collision-proof regardless of the starting values.
|
|
56
|
+
for seq, risk in enumerate(risks, start=1):
|
|
57
|
+
risk.short_id = f"x{seq:07d}"
|
|
58
|
+
Risk.objects.bulk_update(risks, ["short_id"], batch_size=2000)
|
|
59
|
+
for seq, risk in enumerate(risks, start=1):
|
|
60
|
+
risk.short_id = str(seq)
|
|
61
|
+
Risk.objects.bulk_update(risks, ["short_id"], batch_size=2000)
|
|
62
|
+
|
|
63
|
+
# Bump server_version so sync clients see the changed short_id. Done as a
|
|
64
|
+
# single bulk UPDATE per project rather than per-row save().
|
|
65
|
+
Risk.objects.filter(project_id=project_id).update(server_version=F("server_version") + 1)
|
|
66
|
+
Project.objects.filter(pk=project_id).update(risk_sequence=len(risks))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _backfill_wbs_paths(apps: Any, schema_editor: object) -> None:
|
|
70
|
+
"""Assign sequential root-level wbs_path values to tasks that have none (#138).
|
|
71
|
+
|
|
72
|
+
Tasks with null wbs_path have no hierarchy information (parent_id is derived
|
|
73
|
+
from wbs_path, not stored separately), so they are assigned sequential
|
|
74
|
+
root-level paths within their project, ordered by short_id (project-scoped
|
|
75
|
+
insertion order).
|
|
76
|
+
"""
|
|
77
|
+
Task = apps.get_model("projects", "Task")
|
|
78
|
+
|
|
79
|
+
# Order by short_id, not pk: Task.id is a UUID (random), so pk ordering is
|
|
80
|
+
# non-deterministic. short_id is allocated from Project.object_sequence on
|
|
81
|
+
# INSERT and zero-padded to 8 hex digits, so its lexicographic order matches
|
|
82
|
+
# creation order within a project. Backfill from migration 0015 ensures
|
|
83
|
+
# every pre-existing Task has a short_id assigned.
|
|
84
|
+
null_tasks = (
|
|
85
|
+
Task.objects.filter(wbs_path__isnull=True, is_deleted=False)
|
|
86
|
+
.order_by("project_id", "short_id")
|
|
87
|
+
.values_list("id", "project_id")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
project_root_counts: dict[Any, int] = {}
|
|
91
|
+
updates = []
|
|
92
|
+
for task_id, project_id in null_tasks:
|
|
93
|
+
# Count existing root-level tasks (wbs_path matches ^\d+$) for this project.
|
|
94
|
+
# Computed once per project and cached; the update list preserves order so
|
|
95
|
+
# each new task sees the correct next position.
|
|
96
|
+
if project_id not in project_root_counts:
|
|
97
|
+
project_root_counts[project_id] = Task.objects.filter(
|
|
98
|
+
project_id=project_id,
|
|
99
|
+
is_deleted=False,
|
|
100
|
+
wbs_path__isnull=False,
|
|
101
|
+
wbs_path__regex=r"^\d+$",
|
|
102
|
+
).count()
|
|
103
|
+
project_root_counts[project_id] += 1
|
|
104
|
+
updates.append((task_id, str(project_root_counts[project_id])))
|
|
105
|
+
|
|
106
|
+
for task_id, new_path in updates:
|
|
107
|
+
Task.objects.filter(id=task_id).update(wbs_path=new_path)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class RepairedWbsPath(NamedTuple):
|
|
111
|
+
"""One row moved off a duplicated ``wbs_path`` by :func:`repair_duplicate_wbs_paths`."""
|
|
112
|
+
|
|
113
|
+
project_id: str
|
|
114
|
+
task_id: str
|
|
115
|
+
old_path: str
|
|
116
|
+
new_path: str
|
|
117
|
+
kept_task_id: str
|
|
118
|
+
#: Live rows under ``old_path`` that stayed with the kept task. See the
|
|
119
|
+
#: function docstring — they are ambiguous by construction, not merely unmoved.
|
|
120
|
+
stranded_descendants: int
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _free_sibling_path(taken: set[str], prefix: str, start: int) -> tuple[str, int]:
|
|
124
|
+
"""The next unused path under ``prefix``, and the ordinal to resume searching from.
|
|
125
|
+
|
|
126
|
+
``taken`` is every live path already claimed in the project, including the ones
|
|
127
|
+
this repair has just handed out — so two duplicates in the same group cannot
|
|
128
|
+
collide with each other.
|
|
129
|
+
"""
|
|
130
|
+
ordinal = start
|
|
131
|
+
while True:
|
|
132
|
+
candidate = f"{prefix}{ordinal}" if prefix else str(ordinal)
|
|
133
|
+
if candidate not in taken:
|
|
134
|
+
return candidate, ordinal + 1
|
|
135
|
+
ordinal += 1
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def repair_duplicate_wbs_paths(task_model: Any) -> list[RepairedWbsPath]:
|
|
139
|
+
"""Re-path duplicate live ``(project, wbs_path)`` rows and report every move (#3068).
|
|
140
|
+
|
|
141
|
+
Migration ``0148`` adds ``unique_task_wbs_path_per_project_live`` as an
|
|
142
|
+
``ExclusionConstraint``. ``AddConstraint`` builds and **validates** the GiST index
|
|
143
|
+
against every existing row, so on a database that already holds two live tasks
|
|
144
|
+
sharing a path, ``migrate`` aborts. Migrations run on container start, which makes
|
|
145
|
+
that an upgrade crash-loop rather than an error an operator can step around — and
|
|
146
|
+
the state is not hypothetical. It is exactly the corruption #3048 was filed to stop,
|
|
147
|
+
which any database running the pre-#3048 code could have accumulated (#3061 and the
|
|
148
|
+
three importer paths in #3069 are the known producers).
|
|
149
|
+
|
|
150
|
+
Repairing rather than refusing is deliberate: refusing leaves the operator with a
|
|
151
|
+
crash loop and no tooling to get out of it. But rows are being moved out from under
|
|
152
|
+
them, so **every move is logged at WARNING here**, not by the caller. A silent
|
|
153
|
+
reshuffle of someone's WBS would be worse than failing loudly, and the migration
|
|
154
|
+
output is the only record the operator will have — so the log is part of this
|
|
155
|
+
function's contract and is testable with it, rather than living in the migration
|
|
156
|
+
wrapper where CLAUDE.md rule 3 forbids a test from reaching it.
|
|
157
|
+
|
|
158
|
+
**Which row keeps the path.** #3068 proposed "oldest ``created_at``, tie-break on
|
|
159
|
+
``pk``". ``Task`` has no ``created_at`` — it carries ``blocked_since``,
|
|
160
|
+
``status_changed_at``, ``deleted_at``, ``seeded_at`` and ``edited_at``, none of
|
|
161
|
+
which is a creation stamp and each of which is null on most rows. The rule used is
|
|
162
|
+
**lowest ``sync_seq``, then lowest ``id``**: ``sync_seq`` is the owning project's
|
|
163
|
+
monotonic write cursor (ADR-0686), so the row that reached the project's sequence
|
|
164
|
+
first keeps the path — the closest thing to "created first" this table records.
|
|
165
|
+
Both terms are stable, so repairing the same database twice, or two replicas of it,
|
|
166
|
+
produces identical output.
|
|
167
|
+
|
|
168
|
+
**Descendant count deliberately does not enter the rule, and that is the crux.** The
|
|
169
|
+
instinct is "keep whichever row owns the subtree" — but descendants are addressed BY
|
|
170
|
+
PATH, and every row in a duplicate group shares the path. A row at ``4.2.1`` is a
|
|
171
|
+
child of "the ``4.2`` in this project"; when there are two of those, nothing in the
|
|
172
|
+
data says which. The count is identical for every candidate and cannot rank them.
|
|
173
|
+
Using it would look principled while sorting on a constant.
|
|
174
|
+
|
|
175
|
+
So the subtree stays with the kept row, and the moved row becomes childless at its
|
|
176
|
+
new path. That is a real consequence for the operator, which is why the count rides
|
|
177
|
+
back on :attr:`RepairedWbsPath.stranded_descendants` instead of being dropped.
|
|
178
|
+
|
|
179
|
+
A moved row is re-pathed among its **own siblings** — ``4.2`` becomes ``4.9``, not a
|
|
180
|
+
new root — so it stays inside the phase a planner put it in. A root-level duplicate
|
|
181
|
+
shifts to a free root ordinal past the project's current max, the same rule
|
|
182
|
+
``root_ordinal_offset()`` applies to template adoption, so repaired and seeded rows
|
|
183
|
+
agree about what "the next free root" means.
|
|
184
|
+
|
|
185
|
+
Takes the model class rather than reaching for ``Task`` directly so the migration can
|
|
186
|
+
pass its historical model and a test can pass the real one (CLAUDE.md rule 3: never
|
|
187
|
+
import a migration module in a test).
|
|
188
|
+
|
|
189
|
+
On the overwhelmingly common clean database this is one aggregate query and a return.
|
|
190
|
+
"""
|
|
191
|
+
dupes = (
|
|
192
|
+
task_model.objects.filter(is_deleted=False, wbs_path__isnull=False)
|
|
193
|
+
.values("project_id", "wbs_path")
|
|
194
|
+
.annotate(n=Count("id"))
|
|
195
|
+
.filter(n__gt=1)
|
|
196
|
+
)
|
|
197
|
+
affected = {(g["project_id"], str(g["wbs_path"])) for g in dupes}
|
|
198
|
+
if not affected:
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
repaired: list[RepairedWbsPath] = []
|
|
202
|
+
|
|
203
|
+
for project_id in sorted({pid for pid, _ in affected}, key=str):
|
|
204
|
+
rows = list(
|
|
205
|
+
task_model.objects.filter(
|
|
206
|
+
project_id=project_id, is_deleted=False, wbs_path__isnull=False
|
|
207
|
+
).values("id", "wbs_path", "sync_seq")
|
|
208
|
+
)
|
|
209
|
+
taken = {str(r["wbs_path"]) for r in rows}
|
|
210
|
+
|
|
211
|
+
by_path: dict[str, list[dict[str, Any]]] = {}
|
|
212
|
+
for row in rows:
|
|
213
|
+
by_path.setdefault(str(row["wbs_path"]), []).append(row)
|
|
214
|
+
|
|
215
|
+
# Live descendants per path — for the operator-facing report ONLY, never for
|
|
216
|
+
# the keep rule (see the docstring). A path's descendants are the rows whose
|
|
217
|
+
# path starts with it plus a separator; the string test is exact because an
|
|
218
|
+
# ltree label cannot contain a dot.
|
|
219
|
+
descendants = {
|
|
220
|
+
path: sum(len(v) for p, v in by_path.items() if p.startswith(path + "."))
|
|
221
|
+
for path in by_path
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Resume the ordinal search per prefix so a group of three duplicates does not
|
|
225
|
+
# rescan from 1 for each move.
|
|
226
|
+
next_ordinal: dict[str, int] = {}
|
|
227
|
+
|
|
228
|
+
for path in sorted(p for pid, p in affected if pid == project_id):
|
|
229
|
+
group = by_path.get(path, [])
|
|
230
|
+
if len(group) < 2:
|
|
231
|
+
continue
|
|
232
|
+
# Earliest position in the project's write sequence keeps the path; the pk
|
|
233
|
+
# breaks a tie. Both are stable, so this is reproducible.
|
|
234
|
+
group.sort(key=lambda r: (r["sync_seq"], str(r["id"])))
|
|
235
|
+
keeper, movers = group[0], group[1:]
|
|
236
|
+
|
|
237
|
+
head, _, _ = path.rpartition(".")
|
|
238
|
+
prefix = f"{head}." if head else ""
|
|
239
|
+
start = next_ordinal.get(prefix, 1)
|
|
240
|
+
|
|
241
|
+
for mover in movers:
|
|
242
|
+
new_path, start = _free_sibling_path(taken, prefix, start)
|
|
243
|
+
taken.add(new_path)
|
|
244
|
+
task_model.objects.filter(pk=mover["id"]).update(wbs_path=new_path)
|
|
245
|
+
repaired.append(
|
|
246
|
+
RepairedWbsPath(
|
|
247
|
+
project_id=str(project_id),
|
|
248
|
+
task_id=str(mover["id"]),
|
|
249
|
+
old_path=path,
|
|
250
|
+
new_path=new_path,
|
|
251
|
+
kept_task_id=str(keeper["id"]),
|
|
252
|
+
stranded_descendants=descendants.get(path, 0),
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
next_ordinal[prefix] = start
|
|
256
|
+
|
|
257
|
+
_log_repairs(repaired)
|
|
258
|
+
return repaired
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _log_repairs(repaired: list[RepairedWbsPath]) -> None:
|
|
262
|
+
"""Report every moved row at WARNING. See :func:`repair_duplicate_wbs_paths`."""
|
|
263
|
+
if not repaired:
|
|
264
|
+
return
|
|
265
|
+
logger.warning(
|
|
266
|
+
"wbs_path repair (#3068): %d row(s) across %d project(s) shared a live WBS path "
|
|
267
|
+
"with another row and have been moved so the uniqueness constraint can be "
|
|
268
|
+
"applied. Review the moves below.",
|
|
269
|
+
len(repaired),
|
|
270
|
+
len({r.project_id for r in repaired}),
|
|
271
|
+
)
|
|
272
|
+
for row in repaired:
|
|
273
|
+
logger.warning(
|
|
274
|
+
"wbs_path repair (#3068): project=%s task=%s moved %s -> %s (kept task=%s)%s",
|
|
275
|
+
row.project_id,
|
|
276
|
+
row.task_id,
|
|
277
|
+
row.old_path,
|
|
278
|
+
row.new_path,
|
|
279
|
+
row.kept_task_id,
|
|
280
|
+
(
|
|
281
|
+
f"; {row.stranded_descendants} live row(s) under {row.old_path} stayed "
|
|
282
|
+
f"with task {row.kept_task_id} - a duplicated path makes their parent "
|
|
283
|
+
"ambiguous and nothing in the data distinguishes them"
|
|
284
|
+
if row.stranded_descendants
|
|
285
|
+
else ""
|
|
286
|
+
),
|
|
287
|
+
)
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Service layer for the program backlog pull-down (ADR-0069 Erratum, #737).
|
|
2
|
+
|
|
3
|
+
The pull action is the one place where a program-level ``BacklogItem`` crosses
|
|
4
|
+
into a project: it converts a PROPOSED item into a project-backlog ``Task``
|
|
5
|
+
(``status=BACKLOG``, ``sprint=NULL``) and records the conversion on the item.
|
|
6
|
+
Sprint sovereignty is preserved by design — pull never assigns a sprint
|
|
7
|
+
(Morgan's VoC blocker, resolved in ADR-0069).
|
|
8
|
+
|
|
9
|
+
Kept as a service (not inline in the view) so the atomic transition is testable
|
|
10
|
+
in isolation and reusable by a future batch-pull from Sprint Planning.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import uuid
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from django.db import IntegrityError, transaction
|
|
19
|
+
from django.db.models import Max
|
|
20
|
+
from django.utils import timezone
|
|
21
|
+
|
|
22
|
+
from trueppm_api.apps.projects.models import (
|
|
23
|
+
BacklogItem,
|
|
24
|
+
BacklogItemStatus,
|
|
25
|
+
BacklogItemType,
|
|
26
|
+
Label,
|
|
27
|
+
LabelColor,
|
|
28
|
+
Project,
|
|
29
|
+
Task,
|
|
30
|
+
TaskLabel,
|
|
31
|
+
TaskStatus,
|
|
32
|
+
TaskType,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Map the program intake taxonomy (ADR-0069) onto the project work-item taxonomy
|
|
36
|
+
# (ADR-0105). The two enums are reconciled (#1995): every intake type maps to a
|
|
37
|
+
# distinct Task type so a pulled bug/spike/chore keeps its kind instead of
|
|
38
|
+
# silently collapsing to a plain task. ``chore`` is the intake name for
|
|
39
|
+
# ``TaskType.TECH_DEBT``; ``feature`` has no Task analogue and maps to ``TASK``.
|
|
40
|
+
_ITEM_TYPE_TO_TASK_TYPE: dict[str, TaskType] = {
|
|
41
|
+
BacklogItemType.EPIC: TaskType.EPIC,
|
|
42
|
+
BacklogItemType.STORY: TaskType.STORY,
|
|
43
|
+
BacklogItemType.TASK: TaskType.TASK,
|
|
44
|
+
BacklogItemType.BUG: TaskType.BUG,
|
|
45
|
+
BacklogItemType.SPIKE: TaskType.SPIKE,
|
|
46
|
+
BacklogItemType.CHORE: TaskType.TECH_DEBT,
|
|
47
|
+
BacklogItemType.FEATURE: TaskType.TASK,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BacklogItemNotPullable(Exception):
|
|
52
|
+
"""Raised when a pull is attempted on an item that is not PROPOSED.
|
|
53
|
+
|
|
54
|
+
The view maps this to ``409 Conflict`` — the item was already pulled or
|
|
55
|
+
archived (or a concurrent pull won the row lock first).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CrossProgramPullError(Exception):
|
|
60
|
+
"""Raised when the pull target project does not belong to the item's program.
|
|
61
|
+
|
|
62
|
+
The view maps this to ``400 Bad Request``. Single-program scoping is the OSS
|
|
63
|
+
boundary (ADR-0069): a program backlog item can only be pulled into one of
|
|
64
|
+
its own program's projects, never into another program's project.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pull_to_project_backlog(
|
|
69
|
+
item_id: str,
|
|
70
|
+
project: Project,
|
|
71
|
+
actor: Any,
|
|
72
|
+
) -> Task:
|
|
73
|
+
"""Atomically convert a PROPOSED ``BacklogItem`` into a project-backlog Task.
|
|
74
|
+
|
|
75
|
+
Locks the item ``FOR UPDATE`` and asserts it is still PROPOSED before doing
|
|
76
|
+
any work, so two concurrent pulls cannot both succeed — the loser raises
|
|
77
|
+
:class:`BacklogItemNotPullable` (→ 409). The new Task lands in the project
|
|
78
|
+
backlog (``status=BACKLOG``, ``sprint=None``); the item is flipped to PULLED
|
|
79
|
+
with ``pulled_task``/``pulled_at``/``pulled_by`` recorded. Both rows bump
|
|
80
|
+
``server_version`` via their ``save()``.
|
|
81
|
+
|
|
82
|
+
On commit it enqueues a CPM recalculation (existing scheduling outbox — no
|
|
83
|
+
new outbox category), broadcasts a ``task_created`` board event, and fires the
|
|
84
|
+
``task.created`` outbound webhook (#752) — matching every other Task-create
|
|
85
|
+
path so external subscribers see backlog pulls like any other create. All
|
|
86
|
+
three are deferred with ``transaction.on_commit`` so a rolled-back pull never
|
|
87
|
+
leaks a recalc request, a phantom WS event, or a webhook delivery.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
item_id: PK of the ``BacklogItem`` to pull.
|
|
91
|
+
project: The resolved target ``Project`` (must belong to the item's program).
|
|
92
|
+
actor: The user performing the pull (recorded as ``pulled_by``).
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The created project-backlog ``Task``.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
BacklogItem.DoesNotExist: if no such item exists.
|
|
99
|
+
CrossProgramPullError: if ``project`` is not in the item's program.
|
|
100
|
+
BacklogItemNotPullable: if the item is not in PROPOSED status.
|
|
101
|
+
"""
|
|
102
|
+
# Call-time import (not module-level) to avoid a views→services→views cycle.
|
|
103
|
+
# By the time this service runs, the app is fully loaded, so reusing the
|
|
104
|
+
# canonical task payload builder keeps the pull's task.created shape identical
|
|
105
|
+
# to the REST create path with zero drift risk.
|
|
106
|
+
from trueppm_api.apps.access.permissions import assert_project_not_archived
|
|
107
|
+
from trueppm_api.apps.projects.views import _task_webhook_payload
|
|
108
|
+
from trueppm_api.apps.scheduling.services import enqueue_recalculate
|
|
109
|
+
from trueppm_api.apps.sync.broadcast import broadcast_board_event
|
|
110
|
+
from trueppm_api.apps.webhooks.dispatch import dispatch_webhooks
|
|
111
|
+
|
|
112
|
+
# A pull creates a Task, and archived plans take no writes (#3354). The view
|
|
113
|
+
# checks this too, but archived is lifecycle state — a property of the target
|
|
114
|
+
# plan, not of the caller — so it holds for every caller rather than only for
|
|
115
|
+
# one that arrives through DRF. Every caller is a view today; the point is that
|
|
116
|
+
# nothing makes that stay true. Placed before the transaction so nothing is
|
|
117
|
+
# opened on a write that must not happen.
|
|
118
|
+
assert_project_not_archived(project.pk)
|
|
119
|
+
|
|
120
|
+
new_label_ids: list[str] = []
|
|
121
|
+
with transaction.atomic():
|
|
122
|
+
item = BacklogItem.objects.select_for_update().get(pk=item_id, is_deleted=False)
|
|
123
|
+
|
|
124
|
+
if project.program_id != item.program_id:
|
|
125
|
+
raise CrossProgramPullError("The target project is not part of this program.")
|
|
126
|
+
|
|
127
|
+
if item.status != BacklogItemStatus.PROPOSED:
|
|
128
|
+
raise BacklogItemNotPullable(
|
|
129
|
+
f"This item is '{item.status}' and can no longer be pulled."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
task = Task.objects.create(
|
|
133
|
+
project=project,
|
|
134
|
+
name=item.title,
|
|
135
|
+
# BacklogItem.description maps to Task.notes — Task has no separate
|
|
136
|
+
# description field; the Board/drawer render notes as the body.
|
|
137
|
+
notes=item.description,
|
|
138
|
+
story_points=item.story_points,
|
|
139
|
+
# The PO's intake ordering is the whole point of ranking a program
|
|
140
|
+
# backlog, and it is not reconstructable after the crossing — there is
|
|
141
|
+
# no un-pull endpoint, so a lost rank cannot be recovered by undoing.
|
|
142
|
+
# Both fields are nullable PositiveIntegerFields meaning "lower = higher
|
|
143
|
+
# priority", so the raw value carries: it preserves relative order among
|
|
144
|
+
# items pulled from the same pool. Ranks are not unique and ties already
|
|
145
|
+
# break on a secondary sort, so colliding with an existing project task's
|
|
146
|
+
# rank is harmless — whereas leaving it null sorts the pulled item last
|
|
147
|
+
# (the 9999 client sentinel), which is the defect (#2906).
|
|
148
|
+
priority_rank=item.priority_rank,
|
|
149
|
+
status=TaskStatus.BACKLOG,
|
|
150
|
+
sprint=None,
|
|
151
|
+
# ADR-0105: carry the intake item's type into the project work-item
|
|
152
|
+
# taxonomy so a pulled story/bug/spike keeps its kind. The taxonomies
|
|
153
|
+
# are reconciled (#1995), so ``.get`` always hits; the TASK fallback is
|
|
154
|
+
# defensive only (an unknown value can no longer be created).
|
|
155
|
+
type=_ITEM_TYPE_TO_TASK_TYPE.get(item.item_type, TaskType.TASK),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Carry the intake item's free-text tags onto the task as project labels
|
|
159
|
+
# (#1992) so a PO's thematic grouping survives the crossing. Runs inside
|
|
160
|
+
# the atomic block so a rolled-back pull leaves no orphan labels.
|
|
161
|
+
new_label_ids = _apply_tags_as_labels(task, project, item.tags, actor)
|
|
162
|
+
|
|
163
|
+
item.status = BacklogItemStatus.PULLED
|
|
164
|
+
item.pulled_task = task
|
|
165
|
+
item.pulled_at = timezone.now()
|
|
166
|
+
item.pulled_by = actor if getattr(actor, "is_authenticated", False) else None
|
|
167
|
+
# server_version is bumped automatically by VersionedModel.save() (it is
|
|
168
|
+
# stripped from update_fields and incremented via F()), so it is omitted here.
|
|
169
|
+
item.save(update_fields=["status", "pulled_task", "pulled_at", "pulled_by"])
|
|
170
|
+
|
|
171
|
+
project_id = str(project.pk)
|
|
172
|
+
task_id = str(task.pk)
|
|
173
|
+
# source="backlog_pull" tells external consumers which surface originated the
|
|
174
|
+
# create (ADR-0065 X-Source semantics) so a pull is distinguishable from a
|
|
175
|
+
# board/schedule/sync create on the same task.created event.
|
|
176
|
+
webhook_payload = _task_webhook_payload(task, source="backlog_pull")
|
|
177
|
+
# CPM recalc via the existing scheduling outbox (ADR-0027). A BACKLOG task is
|
|
178
|
+
# excluded from the CPM graph, so this coalesces to a no-op run today, but we
|
|
179
|
+
# keep it on the standard Task-create path for consistency and forward-safety.
|
|
180
|
+
transaction.on_commit(lambda: enqueue_recalculate(project_id))
|
|
181
|
+
transaction.on_commit(
|
|
182
|
+
lambda: broadcast_board_event(project_id, "task_created", {"id": task_id})
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Any labels coined from the item's tags are new project vocabulary — tell open
|
|
186
|
+
# boards/label managers so they can render the pulled task's pills (#1992). Bind
|
|
187
|
+
# the id through a function arg so closure late-binding can't collapse the loop's
|
|
188
|
+
# events to the last id.
|
|
189
|
+
def _broadcast_label(lid: str) -> None:
|
|
190
|
+
transaction.on_commit(
|
|
191
|
+
lambda: broadcast_board_event(project_id, "label_created", {"id": lid})
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
for _label_id in new_label_ids:
|
|
195
|
+
_broadcast_label(_label_id)
|
|
196
|
+
|
|
197
|
+
# Bind the payload via a default arg so closure late-binding can never swap it
|
|
198
|
+
# if this function grows more branches later (matches inbound_sync.py).
|
|
199
|
+
def _dispatch(pid: str = project_id, wp: dict[str, Any] = webhook_payload) -> None:
|
|
200
|
+
dispatch_webhooks(pid, "task.created", wp)
|
|
201
|
+
|
|
202
|
+
transaction.on_commit(_dispatch)
|
|
203
|
+
return task
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _apply_tags_as_labels(
|
|
207
|
+
task: Task,
|
|
208
|
+
project: Project,
|
|
209
|
+
tags: list[str],
|
|
210
|
+
actor: Any,
|
|
211
|
+
) -> list[str]:
|
|
212
|
+
"""Carry a pulled item's free-text tags onto the task as project labels (#1992).
|
|
213
|
+
|
|
214
|
+
Each tag becomes — or, case-insensitively, reuses — a project-scoped ``Label``
|
|
215
|
+
and is attached to ``task`` via ``TaskLabel``. Reuse keeps the catalog from
|
|
216
|
+
sprouting a near-duplicate on every pull; new labels take the neutral default
|
|
217
|
+
slot (``SLATE``) appended to the end of the palette order, which a project admin
|
|
218
|
+
can recolor later. Idempotent per tag via ``TaskLabel.get_or_create``.
|
|
219
|
+
|
|
220
|
+
Must be called inside the pull's ``transaction.atomic`` block so a rolled-back
|
|
221
|
+
pull leaves no orphan labels. The label soft cap is honored: once the project is
|
|
222
|
+
at the ceiling, further *new* tags are skipped (existing matches still attach) so
|
|
223
|
+
a pull can never blow past the curation floor the ``LabelViewSet`` enforces.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
task: The freshly created project-backlog Task to label.
|
|
227
|
+
project: The task's project (labels are project-scoped).
|
|
228
|
+
tags: The item's tags — already normalized (trimmed, de-duped) by the
|
|
229
|
+
serializer's ``validate_tags``.
|
|
230
|
+
actor: The pulling user, recorded as ``created_by`` on any new label.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
The ids of labels *created* here (not reused), so the caller can broadcast
|
|
234
|
+
``label_created`` once the transaction commits.
|
|
235
|
+
"""
|
|
236
|
+
if not tags:
|
|
237
|
+
return []
|
|
238
|
+
# Call-time import mirrors the pull's views import — avoids a services→views cycle.
|
|
239
|
+
from trueppm_api.apps.projects.views import _label_soft_cap
|
|
240
|
+
|
|
241
|
+
cap = _label_soft_cap()
|
|
242
|
+
live = Label.objects.filter(project=project, is_deleted=False)
|
|
243
|
+
label_count = live.count()
|
|
244
|
+
next_position = (live.aggregate(m=Max("position"))["m"] or 0) + 1
|
|
245
|
+
creator = actor if getattr(actor, "is_authenticated", False) else None
|
|
246
|
+
|
|
247
|
+
created_ids: list[str] = []
|
|
248
|
+
for tag in tags:
|
|
249
|
+
# Label.name is capped at 50 chars; BacklogItem tags are unbounded, so clamp
|
|
250
|
+
# to keep an over-long tag from raising instead of degrading gracefully.
|
|
251
|
+
name = tag[:50]
|
|
252
|
+
existing = Label.objects.filter(
|
|
253
|
+
project=project, name__iexact=name, is_deleted=False
|
|
254
|
+
).first()
|
|
255
|
+
if existing is not None:
|
|
256
|
+
TaskLabel.objects.get_or_create(task=task, label=existing)
|
|
257
|
+
continue
|
|
258
|
+
if label_count >= cap:
|
|
259
|
+
# At the curation ceiling — skip coining a new label but keep going;
|
|
260
|
+
# a pull must not fail because the project's label catalog is full.
|
|
261
|
+
continue
|
|
262
|
+
label, created = _coin_label(project, name, next_position, creator)
|
|
263
|
+
if label is None:
|
|
264
|
+
continue
|
|
265
|
+
if created:
|
|
266
|
+
next_position += 1
|
|
267
|
+
label_count += 1
|
|
268
|
+
created_ids.append(str(label.pk))
|
|
269
|
+
TaskLabel.objects.get_or_create(task=task, label=label)
|
|
270
|
+
return created_ids
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _coin_label(
|
|
274
|
+
project: Project,
|
|
275
|
+
name: str,
|
|
276
|
+
position: int,
|
|
277
|
+
creator: Any,
|
|
278
|
+
) -> tuple[Any, bool]:
|
|
279
|
+
"""Create a new project label at ``position``, or reuse the race winner.
|
|
280
|
+
|
|
281
|
+
Returns ``(label, created)`` where ``created`` is True only when this call
|
|
282
|
+
coined the label (so the caller can bump the position/count and broadcast).
|
|
283
|
+
|
|
284
|
+
Savepoint the create so a concurrent pull that coined the same tag first raises
|
|
285
|
+
only a local IntegrityError (on uniq_label_name_per_project) instead of poisoning
|
|
286
|
+
the whole pull transaction. Without the nested atomic the outer pull would roll
|
|
287
|
+
back to a transient 500. On a lost race the other transaction's label is now
|
|
288
|
+
committed and visible; reuse it (exact-name match, since the unique constraint is
|
|
289
|
+
on exact (project, name)).
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
with transaction.atomic():
|
|
293
|
+
label = Label.objects.create(
|
|
294
|
+
project=project,
|
|
295
|
+
name=name,
|
|
296
|
+
color=LabelColor.SLATE,
|
|
297
|
+
position=position,
|
|
298
|
+
created_by=creator,
|
|
299
|
+
)
|
|
300
|
+
except IntegrityError:
|
|
301
|
+
return Label.objects.filter(project=project, name=name, is_deleted=False).first(), False
|
|
302
|
+
return label, True
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def reset_pulled_item_on_task_delete(task_id: str) -> None:
|
|
306
|
+
"""Reset a BacklogItem to PROPOSED when its pulled Task is (soft-)deleted.
|
|
307
|
+
|
|
308
|
+
Rollback path (ADR-0069): if the Task created by a pull is deleted, the
|
|
309
|
+
originating item must become re-pullable. ``pulled_task`` is ``SET_NULL`` so
|
|
310
|
+
deletion never cascades to the item; this restores the lifecycle by clearing
|
|
311
|
+
the pull bookkeeping and flipping the item back to PROPOSED.
|
|
312
|
+
|
|
313
|
+
Idempotent and a no-op when no item points at the task. Called from the Task
|
|
314
|
+
post_save soft-delete receiver (TruePPM Tasks soft-delete, not hard-delete).
|
|
315
|
+
"""
|
|
316
|
+
item = BacklogItem.objects.filter(pulled_task_id=uuid.UUID(task_id), is_deleted=False).first()
|
|
317
|
+
if item is None or item.status != BacklogItemStatus.PULLED:
|
|
318
|
+
return
|
|
319
|
+
item.status = BacklogItemStatus.PROPOSED
|
|
320
|
+
item.pulled_task = None
|
|
321
|
+
item.pulled_at = None
|
|
322
|
+
item.pulled_by = None
|
|
323
|
+
# server_version auto-bumps in VersionedModel.save() — omitted from update_fields.
|
|
324
|
+
item.save(update_fields=["status", "pulled_task", "pulled_at", "pulled_by"])
|