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
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""App config for history."""
|
|
2
|
+
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HistoryConfig(AppConfig):
|
|
7
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
8
|
+
name = "trueppm_api.apps.history"
|
|
9
|
+
|
|
10
|
+
def ready(self) -> None:
|
|
11
|
+
# Register the post_save receiver that fires history_record_created.
|
|
12
|
+
import trueppm_api.apps.history.signals # noqa: F401
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Unified project changelog aggregator (ADR-0201, #371).
|
|
2
|
+
|
|
3
|
+
A read-only aggregator that merges the project-scoped ``django-simple-history``
|
|
4
|
+
tables into one newest-first "what changed" stream. No new model, no data
|
|
5
|
+
migration — every event already has a durable home in a ``Historical*`` table.
|
|
6
|
+
|
|
7
|
+
Why a Python k-way merge and not a SQL ``UNION``
|
|
8
|
+
------------------------------------------------
|
|
9
|
+
The nine source tables have heterogeneous columns, so a single ``UNION`` would
|
|
10
|
+
force a lowest-common-denominator projection and is brittle against schema
|
|
11
|
+
drift. Instead each source is fetched ``ORDER BY history_date DESC, history_id
|
|
12
|
+
DESC`` with ``LIMIT page_size + 1`` (a bounded scan — never a query per row),
|
|
13
|
+
and the bounded lists are merge-sorted in Python on the global total-order key.
|
|
14
|
+
|
|
15
|
+
Why a ``(history_date, table_rank, history_id)`` keyset and not ``until``
|
|
16
|
+
------------------------------------------------------------------------
|
|
17
|
+
``history_date`` is a timestamp and can collide **across** tables; ``history_id``
|
|
18
|
+
is only unique **within** a table. A timestamp-only cursor (the ``until`` cursor
|
|
19
|
+
of the per-object history and board feeds) is therefore lossy across a cross-table
|
|
20
|
+
tie. The strict total order is the triple ``(history_date DESC, table_rank ASC,
|
|
21
|
+
history_id DESC)``: ``table_rank`` (a fixed integer per source) disambiguates two
|
|
22
|
+
rows in *different* tables sharing an exact ``history_date``, and ``history_id``
|
|
23
|
+
disambiguates two rows in the *same* table. This partitions the global stream
|
|
24
|
+
into pages with no gap and no duplicate — the same guarantee
|
|
25
|
+
``sync/pagination.py`` documents for ``(server_version, id)``.
|
|
26
|
+
|
|
27
|
+
Permission model (ADR-0201): every source's live GET is ``IsProjectMember``
|
|
28
|
+
(Viewer+), so membership is a sufficient row-inclusion gate — there is no
|
|
29
|
+
per-user sub-project ACL on any source (TimeEntry and the Retrospective models
|
|
30
|
+
carry per-user / team_visibility gates but have **no** ``HistoricalRecords``, so
|
|
31
|
+
they cannot appear here). Field redaction is the shared ``history.diff_policy``
|
|
32
|
+
(one vocabulary for every diff surface, #3435 — it drops ``blocked_reason`` per
|
|
33
|
+
ADR-0124 and the CPM outputs at compute time); ``history_user`` is gated to
|
|
34
|
+
Owner/Admin via the view's ``hide_user`` context, and the ``user=`` filter is
|
|
35
|
+
honored only for those callers.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import base64
|
|
41
|
+
import binascii
|
|
42
|
+
import json
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from datetime import datetime
|
|
45
|
+
from typing import TYPE_CHECKING, Any
|
|
46
|
+
|
|
47
|
+
from django.db.models import Q
|
|
48
|
+
from rest_framework.exceptions import ValidationError
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from django.db.models import QuerySet
|
|
52
|
+
|
|
53
|
+
# django-simple-history ``history_type`` → the changelog's change-type vocabulary.
|
|
54
|
+
_HISTORY_TYPE_TO_CHANGE: dict[str, str] = {"+": "created", "~": "updated", "-": "deleted"}
|
|
55
|
+
_CHANGE_TO_HISTORY_TYPE: dict[str, str] = {v: k for k, v in _HISTORY_TYPE_TO_CHANGE.items()}
|
|
56
|
+
|
|
57
|
+
CHANGE_TYPES: frozenset[str] = frozenset(_CHANGE_TO_HISTORY_TYPE)
|
|
58
|
+
|
|
59
|
+
DEFAULT_PAGE_SIZE = 50
|
|
60
|
+
MAX_PAGE_SIZE = 100
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class ChangelogSource:
|
|
65
|
+
"""One historical table wired into the unified changelog.
|
|
66
|
+
|
|
67
|
+
``rank`` is the source's fixed position in the total order — it is encoded in
|
|
68
|
+
the cursor and MUST stay stable (reordering the source list would invalidate
|
|
69
|
+
in-flight opaque cursors, which is acceptable for a short-lived pagination
|
|
70
|
+
token but must be a deliberate change).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
rank: int
|
|
74
|
+
object_type: str
|
|
75
|
+
label_field: str | None # attribute on the historical row to use as the display label
|
|
76
|
+
label_fallback: str # used when label_field is None or the value is empty
|
|
77
|
+
project_filter: str # ORM lookup that scopes the historical rows to a project
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _sources() -> list[ChangelogSource]:
|
|
81
|
+
"""The ordered project-scoped historical sources (ADR-0201).
|
|
82
|
+
|
|
83
|
+
Program and CeremonyTemplate are deliberately excluded (program-scoped, not
|
|
84
|
+
project-scoped); Workspace is a singleton. TimeEntry and the Retrospective
|
|
85
|
+
models are excluded because they have no ``HistoricalRecords`` table at all.
|
|
86
|
+
"""
|
|
87
|
+
return [
|
|
88
|
+
ChangelogSource(0, "task", "name", "Task", "project_id"),
|
|
89
|
+
ChangelogSource(1, "sprint", "name", "Sprint", "project_id"),
|
|
90
|
+
ChangelogSource(2, "risk", "title", "Risk", "project_id"),
|
|
91
|
+
ChangelogSource(3, "dependency", None, "Dependency", "predecessor__project_id"),
|
|
92
|
+
# HistoricalProject's ``pk`` is ``history_id``; the original project's PK
|
|
93
|
+
# lives in the ``id`` column, so the project's own history is scoped by id.
|
|
94
|
+
ChangelogSource(4, "project", "name", "Project", "id"),
|
|
95
|
+
ChangelogSource(5, "task_recurrence", None, "Recurrence rule", "task__project_id"),
|
|
96
|
+
ChangelogSource(6, "guardrail_policy", None, "Guardrail policy", "project_id"),
|
|
97
|
+
ChangelogSource(7, "signal_privacy_policy", None, "Signal privacy policy", "project_id"),
|
|
98
|
+
ChangelogSource(8, "decisions_policy", None, "Decisions policy", "project_id"),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def object_type_choices() -> list[str]:
|
|
103
|
+
return [s.object_type for s in _sources()]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _historical_model(object_type: str) -> Any:
|
|
107
|
+
"""Return the ``Historical*`` model class for a source's live model."""
|
|
108
|
+
from trueppm_api.apps.projects.models import (
|
|
109
|
+
Dependency,
|
|
110
|
+
Project,
|
|
111
|
+
ProjectDecisionsPolicy,
|
|
112
|
+
ProjectGuardrailPolicy,
|
|
113
|
+
ProjectSignalPrivacyPolicy,
|
|
114
|
+
Risk,
|
|
115
|
+
Sprint,
|
|
116
|
+
Task,
|
|
117
|
+
TaskRecurrenceRule,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
live = {
|
|
121
|
+
"task": Task,
|
|
122
|
+
"sprint": Sprint,
|
|
123
|
+
"risk": Risk,
|
|
124
|
+
"dependency": Dependency,
|
|
125
|
+
"project": Project,
|
|
126
|
+
"task_recurrence": TaskRecurrenceRule,
|
|
127
|
+
"guardrail_policy": ProjectGuardrailPolicy,
|
|
128
|
+
"signal_privacy_policy": ProjectSignalPrivacyPolicy,
|
|
129
|
+
"decisions_policy": ProjectDecisionsPolicy,
|
|
130
|
+
}[object_type]
|
|
131
|
+
# ``history`` is the django-simple-history manager, added dynamically to the
|
|
132
|
+
# model class — not visible to the type checker on the VersionedModel base.
|
|
133
|
+
return live.history.model # type: ignore[attr-defined]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class ChangelogCursor:
|
|
138
|
+
"""Position in the ``(history_date, table_rank, history_id)`` total order.
|
|
139
|
+
|
|
140
|
+
Encoded as an opaque, URL-safe base64 JSON token, mirroring
|
|
141
|
+
:class:`~trueppm_api.apps.sync.pagination.SyncCursor`. The token is
|
|
142
|
+
client-controlled, so every field is validated on decode — a tampered or
|
|
143
|
+
truncated token yields a 400 rather than an unhandled 500.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
history_date: datetime
|
|
147
|
+
rank: int
|
|
148
|
+
history_id: int
|
|
149
|
+
|
|
150
|
+
def encode(self) -> str:
|
|
151
|
+
raw = json.dumps(
|
|
152
|
+
{"d": self.history_date.isoformat(), "r": self.rank, "id": self.history_id},
|
|
153
|
+
separators=(",", ":"),
|
|
154
|
+
)
|
|
155
|
+
return base64.urlsafe_b64encode(raw.encode()).decode()
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def decode(cls, token: str) -> ChangelogCursor:
|
|
159
|
+
from django.utils.dateparse import parse_datetime
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
raw = base64.urlsafe_b64decode(token.encode())
|
|
163
|
+
data = json.loads(raw)
|
|
164
|
+
parsed = parse_datetime(str(data["d"]))
|
|
165
|
+
if parsed is None:
|
|
166
|
+
raise ValueError("bad date")
|
|
167
|
+
rank = int(data["r"])
|
|
168
|
+
history_id = int(data["id"])
|
|
169
|
+
except (binascii.Error, ValueError, KeyError, TypeError, UnicodeDecodeError) as err:
|
|
170
|
+
raise ValidationError({"cursor": "Malformed pagination cursor."}) from err
|
|
171
|
+
if rank < 0:
|
|
172
|
+
raise ValidationError({"cursor": "Malformed pagination cursor."})
|
|
173
|
+
return cls(history_date=parsed, rank=rank, history_id=history_id)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _cursor_filter(source: ChangelogSource, cursor: ChangelogCursor) -> Q:
|
|
177
|
+
"""The keyset predicate that resumes strictly *after* ``cursor`` for one source.
|
|
178
|
+
|
|
179
|
+
"After" = older in the newest-first total order. With the source's fixed
|
|
180
|
+
``rank`` known as a constant, the cross-table tie-break collapses to a simple
|
|
181
|
+
per-source predicate (see the module docstring for the derivation):
|
|
182
|
+
|
|
183
|
+
* rank > cursor.rank → every same-``history_date`` row is older → ``<= cd``
|
|
184
|
+
* rank == cursor.rank → tie-break within the table on ``history_id``
|
|
185
|
+
* rank < cursor.rank → same-``history_date`` rows here are newer → already sent
|
|
186
|
+
"""
|
|
187
|
+
cd = cursor.history_date
|
|
188
|
+
if source.rank > cursor.rank:
|
|
189
|
+
return Q(history_date__lte=cd)
|
|
190
|
+
if source.rank == cursor.rank:
|
|
191
|
+
return Q(history_date__lt=cd) | Q(history_date=cd, history_id__lt=cursor.history_id)
|
|
192
|
+
return Q(history_date__lt=cd)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _label_for(source: ChangelogSource, row: Any) -> str:
|
|
196
|
+
if source.label_field is not None:
|
|
197
|
+
value = getattr(row, source.label_field, None)
|
|
198
|
+
if value:
|
|
199
|
+
return str(value)
|
|
200
|
+
return source.label_fallback
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _object_id_for(source: ChangelogSource, row: Any) -> str:
|
|
204
|
+
"""The id used for click-through. Policies point at their project (settings);
|
|
205
|
+
every other source points at the object's own row."""
|
|
206
|
+
if source.object_type in {"guardrail_policy", "signal_privacy_policy", "decisions_policy"}:
|
|
207
|
+
return str(row.project_id)
|
|
208
|
+
return str(row.id)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _fetch_source_rows(
|
|
212
|
+
source: ChangelogSource,
|
|
213
|
+
project: Any,
|
|
214
|
+
*,
|
|
215
|
+
since: datetime | None,
|
|
216
|
+
history_types: set[str] | None,
|
|
217
|
+
user_id: Any,
|
|
218
|
+
cursor: ChangelogCursor | None,
|
|
219
|
+
page_size: int,
|
|
220
|
+
) -> list[Any]:
|
|
221
|
+
"""Fetch one source's bounded, newest-first row window (``LIMIT page_size + 1``).
|
|
222
|
+
|
|
223
|
+
Each of the ``since`` / ``history_types`` / ``user_id`` / ``cursor`` filters is
|
|
224
|
+
applied only when set, so an unconstrained call scans just the project-scoped
|
|
225
|
+
rows. ``select_related("history_user")`` keeps the per-row ``history_user``
|
|
226
|
+
render from fanning out into a query per row.
|
|
227
|
+
"""
|
|
228
|
+
model = _historical_model(source.object_type)
|
|
229
|
+
qs: QuerySet[Any] = model.objects.filter(**{source.project_filter: project.pk})
|
|
230
|
+
if since is not None:
|
|
231
|
+
qs = qs.filter(history_date__gte=since)
|
|
232
|
+
if history_types is not None:
|
|
233
|
+
qs = qs.filter(history_type__in=history_types)
|
|
234
|
+
if user_id is not None:
|
|
235
|
+
qs = qs.filter(history_user_id=user_id)
|
|
236
|
+
if cursor is not None:
|
|
237
|
+
qs = qs.filter(_cursor_filter(source, cursor))
|
|
238
|
+
return list(
|
|
239
|
+
qs.select_related("history_user").order_by("-history_date", "-history_id")[: page_size + 1]
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _source_entries(
|
|
244
|
+
source: ChangelogSource,
|
|
245
|
+
rows: list[Any],
|
|
246
|
+
diff_fn: Any,
|
|
247
|
+
) -> list[tuple[tuple[datetime, int, int], dict[str, Any]]]:
|
|
248
|
+
"""Pair one source's rows with their diffs and key each surviving entry.
|
|
249
|
+
|
|
250
|
+
Diffs are computed within this source's batch (same model), so the
|
|
251
|
+
``history_id``-keyed dict never collides across tables. Rows whose entire diff
|
|
252
|
+
is redacted (:func:`_row_to_entry` returns ``None``) are dropped.
|
|
253
|
+
"""
|
|
254
|
+
diffs = diff_fn(rows, all_records=rows)
|
|
255
|
+
collected: list[tuple[tuple[datetime, int, int], dict[str, Any]]] = []
|
|
256
|
+
for row in rows:
|
|
257
|
+
entry = _row_to_entry(source, row, diffs)
|
|
258
|
+
if entry is not None:
|
|
259
|
+
collected.append(((row.history_date, source.rank, row.history_id), entry))
|
|
260
|
+
return collected
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def build_project_changelog(
|
|
264
|
+
project: Any,
|
|
265
|
+
*,
|
|
266
|
+
diff_fn: Any,
|
|
267
|
+
cursor: ChangelogCursor | None = None,
|
|
268
|
+
since: datetime | None = None,
|
|
269
|
+
object_types: set[str] | None = None,
|
|
270
|
+
change_types: set[str] | None = None,
|
|
271
|
+
user_id: Any = None,
|
|
272
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
273
|
+
) -> tuple[list[dict[str, Any]], ChangelogCursor | None]:
|
|
274
|
+
"""Build one newest-first page of the unified changelog (ADR-0201).
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
project: the project whose history is aggregated.
|
|
278
|
+
diff_fn: ``history.views._compute_diffs`` bound with ``object_scoped=False``
|
|
279
|
+
— injected to avoid an import cycle. It applies the shared
|
|
280
|
+
``history.diff_policy`` (CPM outputs, sync internals, and
|
|
281
|
+
``blocked_reason`` per ADR-0124 are never compared; noise is hidden
|
|
282
|
+
unless it is a record's only change) and keys rows by field name.
|
|
283
|
+
cursor: keyset position; ``None`` starts at the newest row.
|
|
284
|
+
since: inclusive lower bound on ``history_date``.
|
|
285
|
+
object_types: restrict to these source ``object_type`` keys (default all).
|
|
286
|
+
change_types: restrict to these change types (``created``/``updated``/``deleted``).
|
|
287
|
+
user_id: restrict to a single ``history_user`` (honored by the caller only
|
|
288
|
+
for Owner/Admin — see the view).
|
|
289
|
+
page_size: rows per page (clamped to ``[1, MAX_PAGE_SIZE]``).
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
``(entries, next_cursor)``. ``next_cursor`` is ``None`` when the stream is
|
|
293
|
+
exhausted. Each entry is a plain dict — see :func:`_row_to_entry`.
|
|
294
|
+
"""
|
|
295
|
+
page_size = max(1, min(int(page_size), MAX_PAGE_SIZE))
|
|
296
|
+
wanted_types = {s.strip() for s in object_types} if object_types else None
|
|
297
|
+
history_types: set[str] | None = (
|
|
298
|
+
{_CHANGE_TO_HISTORY_TYPE[c] for c in change_types} if change_types else None
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
entries: list[tuple[tuple[datetime, int, int], dict[str, Any]]] = []
|
|
302
|
+
for source in _sources():
|
|
303
|
+
if wanted_types is not None and source.object_type not in wanted_types:
|
|
304
|
+
continue
|
|
305
|
+
rows = _fetch_source_rows(
|
|
306
|
+
source,
|
|
307
|
+
project,
|
|
308
|
+
since=since,
|
|
309
|
+
history_types=history_types,
|
|
310
|
+
user_id=user_id,
|
|
311
|
+
cursor=cursor,
|
|
312
|
+
page_size=page_size,
|
|
313
|
+
)
|
|
314
|
+
if rows:
|
|
315
|
+
entries.extend(_source_entries(source, rows, diff_fn))
|
|
316
|
+
|
|
317
|
+
# Newest-first total order: history_date DESC, rank ASC, history_id DESC.
|
|
318
|
+
entries.sort(key=lambda item: (item[0][0], -item[0][1], item[0][2]), reverse=True)
|
|
319
|
+
|
|
320
|
+
has_more = len(entries) > page_size
|
|
321
|
+
page = entries[:page_size]
|
|
322
|
+
next_cursor: ChangelogCursor | None = None
|
|
323
|
+
if has_more and page:
|
|
324
|
+
key = page[-1][0]
|
|
325
|
+
next_cursor = ChangelogCursor(history_date=key[0], rank=key[1], history_id=key[2])
|
|
326
|
+
|
|
327
|
+
return [entry for _, entry in page], next_cursor
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _row_to_entry(
|
|
331
|
+
source: ChangelogSource,
|
|
332
|
+
row: Any,
|
|
333
|
+
diffs: dict[int, list[dict[str, Any]]],
|
|
334
|
+
) -> dict[str, Any] | None:
|
|
335
|
+
"""Turn one historical row into a changelog entry, or ``None`` to drop it.
|
|
336
|
+
|
|
337
|
+
An ``updated`` row whose entire diff is excluded fields (a pure CPM/sync bump)
|
|
338
|
+
carries no team-readable signal and is dropped — matching the per-object
|
|
339
|
+
history views. ``created`` / ``deleted`` rows are always kept (the change type
|
|
340
|
+
is itself the signal) and carry an empty ``changes`` list to keep the feed
|
|
341
|
+
scannable.
|
|
342
|
+
"""
|
|
343
|
+
change_type = _HISTORY_TYPE_TO_CHANGE.get(row.history_type, "updated")
|
|
344
|
+
changes: list[dict[str, Any]] = []
|
|
345
|
+
if change_type == "updated":
|
|
346
|
+
changes = diffs.get(row.history_id, [])
|
|
347
|
+
if not changes:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
"id": f"{source.object_type}:{row.history_id}",
|
|
352
|
+
"object_type": source.object_type,
|
|
353
|
+
"object_id": _object_id_for(source, row),
|
|
354
|
+
"object_label": _label_for(source, row),
|
|
355
|
+
"change_type": change_type,
|
|
356
|
+
"history_date": row.history_date,
|
|
357
|
+
"history_user": row.history_user,
|
|
358
|
+
"changes": changes,
|
|
359
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Which tracked fields a history diff shows, hides, and promotes — decided once.
|
|
2
|
+
|
|
3
|
+
Task field changes reach users through **three** renderers, and until #3435 each
|
|
4
|
+
carried its own exclusion list:
|
|
5
|
+
|
|
6
|
+
* the task drawer's Activity tab — ``apps/projects/views.py`` (``TaskHistoryView``),
|
|
7
|
+
Task-only, FK values resolved to labels;
|
|
8
|
+
* the project **Activity** page and the per-object history routes —
|
|
9
|
+
``apps/history/views.py::_compute_diffs``, every project-scoped historical model,
|
|
10
|
+
raw values;
|
|
11
|
+
* the board activity feed — ``apps/projects/board_activity.py``, a curated
|
|
12
|
+
**allow-list** (``_BOARD_DIFF_FIELDS``) rather than allow-by-exclusion.
|
|
13
|
+
|
|
14
|
+
The first two disagreed on what to hide, and only the first had a label map, so the
|
|
15
|
+
project Activity page rendered raw column names for every change the drawer rendered
|
|
16
|
+
as prose. Decision (#3435): the two allow-by-exclusion pipelines **converge** on this
|
|
17
|
+
module. They stay separate functions because they differ in what they *resolve* (the
|
|
18
|
+
drawer batch-resolves FK ids to names; the generic pipeline carries the ids), but
|
|
19
|
+
*which* fields are compared, hidden, and promoted is decided here, and both emit the
|
|
20
|
+
model **field name** (``assignee``), never the column (``assignee_id``), so one client
|
|
21
|
+
label map (``activityFormat.ts::fieldLabel``) serves every surface. The board feed
|
|
22
|
+
keeps its allow-list — a board reader wants six fields, not "everything but the
|
|
23
|
+
noise" — and is pinned against the privacy set by test rather than by sharing code.
|
|
24
|
+
|
|
25
|
+
The sets are grouped by the *reason* a field is hidden, because the reasons carry
|
|
26
|
+
different obligations. A noise exclusion may be promoted back when it is a record's
|
|
27
|
+
only change (#3306); an access-control exclusion never may, and conflating the two
|
|
28
|
+
turns a labelling cleanup into a disclosure bug.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from collections.abc import Callable, Iterable
|
|
34
|
+
from typing import TypeVar
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T")
|
|
37
|
+
|
|
38
|
+
# django-simple-history's own columns. Present on every ``Historical*`` row and never
|
|
39
|
+
# a user change; the drawer never sees them because it iterates the live model's
|
|
40
|
+
# fields, but the generic pipeline iterates the historical model's and must drop them.
|
|
41
|
+
HISTORY_DIFF_META_COLUMNS: frozenset[str] = frozenset(
|
|
42
|
+
{"history_id", "history_date", "history_change_reason", "history_user", "history_type"}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Engine outputs and sync counters. ``HistoricalTask`` no longer carries any of these
|
|
46
|
+
# (``_HISTORY_EXCLUDED_TASK`` strips them at tracking time), but the generic pipeline
|
|
47
|
+
# diffs eight other models whose tracking lists differ, so they are named here as well:
|
|
48
|
+
# a model that does track them must still never show a recalculation as an edit.
|
|
49
|
+
HISTORY_DIFF_ENGINE_AND_SYNC: frozenset[str] = frozenset(
|
|
50
|
+
{
|
|
51
|
+
"early_start",
|
|
52
|
+
"early_finish",
|
|
53
|
+
"late_start",
|
|
54
|
+
"late_finish",
|
|
55
|
+
"total_float",
|
|
56
|
+
"free_float",
|
|
57
|
+
"is_critical",
|
|
58
|
+
"scheduled_start",
|
|
59
|
+
"server_version",
|
|
60
|
+
"sync_seq",
|
|
61
|
+
"deleted_version",
|
|
62
|
+
"deleted_at",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Low-signal bookkeeping hidden from every diff surface (ADR-0096 Part 1, #874). These
|
|
67
|
+
# are *display* decisions: the historical model tracks the column, a reader simply
|
|
68
|
+
# gains nothing from a row about it. Members of this set — and only this set — are
|
|
69
|
+
# eligible for promotion when they are a record's only change.
|
|
70
|
+
HISTORY_DIFF_NOISE: frozenset[str] = frozenset(
|
|
71
|
+
{
|
|
72
|
+
"short_id", # immutable, system-assigned identifier
|
|
73
|
+
"status_changed_at", # derived bookkeeping timestamp
|
|
74
|
+
"blocked_since", # derived bookkeeping timestamp
|
|
75
|
+
"sprint_pending", # transient ADR-0102 scope-injection flag
|
|
76
|
+
# sprint-backlog reorder bookkeeping: drag-reorder rewrites the team's
|
|
77
|
+
# within-sprint sequence on every affected sibling, so surfacing it would
|
|
78
|
+
# flood the timeline with integer noise — mirrors the other rank/bookkeeping
|
|
79
|
+
# exclusions (#1885). Deliberate ordering signals (priority_rank) stay visible.
|
|
80
|
+
"sprint_rank",
|
|
81
|
+
# Promoted back into the diff when it is a record's ONLY change — see
|
|
82
|
+
# ``HISTORY_DIFF_PROMOTED_WHEN_ALONE`` below.
|
|
83
|
+
"parent_governance_inherited", # internal inheritance bookkeeping
|
|
84
|
+
"recurrence_occurrence_date", # system-set during recurrence expansion
|
|
85
|
+
"recurrence_rule", # FK to the rule object; recurrence shown via is_recurring
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# The one deliberate per-surface difference. A soft delete is a ``~`` row whose only
|
|
90
|
+
# tracked change is ``is_deleted`` (``VersionedModel.soft_delete`` saves the flag; a
|
|
91
|
+
# hard ``-`` row exists only for the purge task). On a surface scoped to ONE object —
|
|
92
|
+
# the task drawer — that flag is not a field change worth a row: a deleted task has no
|
|
93
|
+
# drawer to read it in, and a restore re-opens the same drawer to the same task. On the
|
|
94
|
+
# cross-object project Activity page the same row is the ONLY record that the delete
|
|
95
|
+
# happened at all, so hiding it there would make every soft delete invisible — the
|
|
96
|
+
# #3306 "write with no record" shape, for the most consequential write there is.
|
|
97
|
+
HISTORY_DIFF_OBJECT_SCOPED_EXCLUDED: frozenset[str] = frozenset({"is_deleted"})
|
|
98
|
+
|
|
99
|
+
# Excluded for an ACCESS-CONTROL reason, not a noise one, and therefore never
|
|
100
|
+
# promotable and never a per-surface judgment call.
|
|
101
|
+
#
|
|
102
|
+
# ``blocked_reason`` is contributor voice (ADR-0124, #1135 — the "Morgan surveillance
|
|
103
|
+
# boundary"): ``TaskSerializer`` read-gates it to the assignee and @-mentioned users
|
|
104
|
+
# via ``can_read_blocker_reason()``, while every history endpoint is Viewer+ for every
|
|
105
|
+
# project member. ``HistoricalTask`` deliberately still tracks it (it is NOT in
|
|
106
|
+
# ``_HISTORY_EXCLUDED_TASK``), so this exclusion is the only thing keeping the reason
|
|
107
|
+
# — and every past reason — off a team-readable feed. The structured blocker signal
|
|
108
|
+
# (``blocker_type`` / ``blocked_since`` / ``blocked_by`` / ``blocking_task``) is
|
|
109
|
+
# team-shareable and stays in the diff.
|
|
110
|
+
HISTORY_DIFF_PRIVACY_GATED: frozenset[str] = frozenset({"blocked_reason"})
|
|
111
|
+
|
|
112
|
+
# Noise exclusions surfaced anyway when they are the ONLY thing a record changed (#3306).
|
|
113
|
+
#
|
|
114
|
+
# ``parent_governance_inherited`` is genuinely bookkeeping alongside a governance
|
|
115
|
+
# change: every ordinary governance write moves the two together, so surfacing both
|
|
116
|
+
# would double the diff rows on the highest-frequency case and turn the summary verb
|
|
117
|
+
# from "changed governance" into "updated 2 fields". That call stands.
|
|
118
|
+
#
|
|
119
|
+
# But the bit can also move *alone*, and then the exclusion plus the empty-diff drop
|
|
120
|
+
# erase the write entirely. The concrete case is a classification cascade onto a root
|
|
121
|
+
# already at the requested ``governance_class`` with ``parent_governance_inherited=True``:
|
|
122
|
+
# declaring the class on the root sets the bit to False, which writes the row, bumps
|
|
123
|
+
# ``server_version``, records an undo-ledger row and broadcasts ``tasks_bulk_mutated``
|
|
124
|
+
# — and left the task's Activity tab showing nothing at all.
|
|
125
|
+
#
|
|
126
|
+
# Narrowing the drop rule instead (rendering every empty-diff ``~`` record) was
|
|
127
|
+
# rejected: that is the bare "Updated" pill issue 874 removed, and it would resurrect
|
|
128
|
+
# it for every ``sprint_rank`` reorder and every transient ``sprint_pending`` flip.
|
|
129
|
+
# Promotion keeps the exclusion's intent — no noise beside a change the user can
|
|
130
|
+
# already read — while guaranteeing that no write is invisible.
|
|
131
|
+
HISTORY_DIFF_PROMOTED_WHEN_ALONE: frozenset[str] = frozenset({"parent_governance_inherited"})
|
|
132
|
+
|
|
133
|
+
# Everything a diff never compares, on any surface. Privacy-gated names are excluded
|
|
134
|
+
# here at *compute* time, not at render time, so no pipeline ever holds the value in a
|
|
135
|
+
# structure a later refactor could accidentally serialize.
|
|
136
|
+
HISTORY_DIFF_HARD_EXCLUDED: frozenset[str] = (
|
|
137
|
+
HISTORY_DIFF_META_COLUMNS | HISTORY_DIFF_ENGINE_AND_SYNC | HISTORY_DIFF_PRIVACY_GATED
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# What the drawer's routine tuple excludes — the name the drawer has always used.
|
|
141
|
+
HISTORY_DIFF_DISPLAY_EXCLUDED: frozenset[str] = (
|
|
142
|
+
HISTORY_DIFF_HARD_EXCLUDED | HISTORY_DIFF_NOISE | HISTORY_DIFF_OBJECT_SCOPED_EXCLUDED
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_compared(name: str, *, object_scoped: bool) -> bool:
|
|
147
|
+
"""Whether a pipeline should diff the field at all.
|
|
148
|
+
|
|
149
|
+
Hard exclusions never are. Noise is compared only if it is promotable, so a
|
|
150
|
+
promoted field can be rendered when it stands alone; the rest of the noise is
|
|
151
|
+
skipped before comparison rather than filtered afterwards, which is what keeps
|
|
152
|
+
the drawer's FK-id collection from resolving ids nobody will see.
|
|
153
|
+
"""
|
|
154
|
+
if name in HISTORY_DIFF_HARD_EXCLUDED:
|
|
155
|
+
return False
|
|
156
|
+
if object_scoped and name in HISTORY_DIFF_OBJECT_SCOPED_EXCLUDED:
|
|
157
|
+
return False
|
|
158
|
+
if name in HISTORY_DIFF_NOISE:
|
|
159
|
+
return name in HISTORY_DIFF_PROMOTED_WHEN_ALONE
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def visible_changes(changes: Iterable[T], field_name: Callable[[T], str]) -> list[T]:
|
|
164
|
+
"""Apply the promote-when-alone rule to one record's compared changes.
|
|
165
|
+
|
|
166
|
+
Routine changes render as they are. A change in
|
|
167
|
+
:data:`HISTORY_DIFF_PROMOTED_WHEN_ALONE` renders only when the record has no
|
|
168
|
+
routine change to show — it rescues an otherwise invisible write without adding
|
|
169
|
+
a row beside a change the reader can already see. Generic over the change shape
|
|
170
|
+
so the drawer's ``(field, old, new)`` tuples and the generic pipeline's dicts run
|
|
171
|
+
the same rule; ``field_name`` extracts the model field name from either.
|
|
172
|
+
"""
|
|
173
|
+
items = list(changes)
|
|
174
|
+
routine = [c for c in items if field_name(c) not in HISTORY_DIFF_PROMOTED_WHEN_ALONE]
|
|
175
|
+
if routine:
|
|
176
|
+
return routine
|
|
177
|
+
return [c for c in items if field_name(c) in HISTORY_DIFF_PROMOTED_WHEN_ALONE]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Serializers for the object change history API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rest_framework import serializers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HistoryUserSerializer(serializers.Serializer[Any]):
|
|
11
|
+
"""Minimal user representation in a history record."""
|
|
12
|
+
|
|
13
|
+
id = serializers.UUIDField()
|
|
14
|
+
display_name = serializers.SerializerMethodField()
|
|
15
|
+
|
|
16
|
+
def get_display_name(self, obj: Any) -> str:
|
|
17
|
+
full = f"{obj.first_name} {obj.last_name}".strip()
|
|
18
|
+
return full or obj.username
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FieldDiffSerializer(serializers.Serializer[Any]):
|
|
22
|
+
"""A single field change within a history record.
|
|
23
|
+
|
|
24
|
+
``field`` is the model **field name** (``assignee``, ``sprint``), never the
|
|
25
|
+
database column (``assignee_id``) — the same key the per-task history endpoint
|
|
26
|
+
emits, so one client label map serves every history surface.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
field = serializers.CharField()
|
|
30
|
+
old = serializers.JSONField(allow_null=True)
|
|
31
|
+
new = serializers.JSONField(allow_null=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HistoryRecordSerializer(serializers.Serializer[Any]):
|
|
35
|
+
"""One change record: when it happened, what changed, and (for Admins) by whom.
|
|
36
|
+
|
|
37
|
+
``history_user`` is null for callers below Admin, so a client must treat it as
|
|
38
|
+
optional rather than assume the actor is always present.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# This docstring is published as the ``HistoryRecord`` component description in
|
|
42
|
+
# docs/api/openapi.json, so it is integrator-facing copy — keep it about the
|
|
43
|
+
# contract, not the mechanism. The mechanism: the view sets ``hide_user=True``
|
|
44
|
+
# on the serializer context for callers below ``Role.ADMIN`` (redaction, not
|
|
45
|
+
# omission — the key is always present), and computes ``diff`` via a
|
|
46
|
+
# ``prev_record`` comparison injected through context so this serializer needs
|
|
47
|
+
# no ORM access.
|
|
48
|
+
|
|
49
|
+
id = serializers.SerializerMethodField()
|
|
50
|
+
history_date = serializers.DateTimeField()
|
|
51
|
+
history_type = serializers.CharField()
|
|
52
|
+
history_change_reason = serializers.CharField(allow_null=True)
|
|
53
|
+
history_user = serializers.SerializerMethodField()
|
|
54
|
+
diff = serializers.SerializerMethodField()
|
|
55
|
+
|
|
56
|
+
def get_id(self, obj: Any) -> str:
|
|
57
|
+
return str(obj.history_id)
|
|
58
|
+
|
|
59
|
+
def get_history_user(self, obj: Any) -> dict[str, Any] | None:
|
|
60
|
+
if self.context.get("hide_user"):
|
|
61
|
+
return None
|
|
62
|
+
user = obj.history_user
|
|
63
|
+
if user is None:
|
|
64
|
+
return None
|
|
65
|
+
return HistoryUserSerializer(user).data
|
|
66
|
+
|
|
67
|
+
def get_diff(self, obj: Any) -> list[dict[str, Any]]:
|
|
68
|
+
diffs: dict[int, list[dict[str, Any]]] = self.context.get("diffs", {})
|
|
69
|
+
return diffs.get(obj.history_id, [])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ChangelogEntrySerializer(serializers.Serializer[Any]):
|
|
73
|
+
"""One row in the unified project changelog (ADR-0201).
|
|
74
|
+
|
|
75
|
+
The instance is a plain dict from ``changelog.build_project_changelog`` — the
|
|
76
|
+
aggregator already merged, ordered, and diffed across the historical tables,
|
|
77
|
+
so the serializer only renders. ``user`` is exposed only to Owner/Admin
|
|
78
|
+
callers (``hide_user=True`` in context returns null), reusing the same gate as
|
|
79
|
+
:class:`HistoryRecordSerializer`.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
id = serializers.CharField()
|
|
83
|
+
object_type = serializers.CharField()
|
|
84
|
+
object_id = serializers.CharField()
|
|
85
|
+
object_label = serializers.CharField()
|
|
86
|
+
change_type = serializers.ChoiceField(choices=["created", "updated", "deleted"])
|
|
87
|
+
history_date = serializers.DateTimeField()
|
|
88
|
+
user = serializers.SerializerMethodField()
|
|
89
|
+
changes = FieldDiffSerializer(many=True)
|
|
90
|
+
|
|
91
|
+
def get_user(self, obj: dict[str, Any]) -> dict[str, Any] | None:
|
|
92
|
+
if self.context.get("hide_user"):
|
|
93
|
+
return None
|
|
94
|
+
user = obj.get("history_user")
|
|
95
|
+
if user is None:
|
|
96
|
+
return None
|
|
97
|
+
return HistoryUserSerializer(user).data
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ChangelogResponseSerializer(serializers.Serializer[Any]):
|
|
101
|
+
"""Response envelope for the unified project changelog (ADR-0201)."""
|
|
102
|
+
|
|
103
|
+
results = ChangelogEntrySerializer(many=True)
|
|
104
|
+
next_cursor = serializers.CharField(allow_null=True)
|