trueppm-api 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +27 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +109 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +459 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +178 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +863 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +597 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +528 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +280 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +551 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +566 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +993 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +537 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +541 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +458 -0
- trueppm_api/apps/projects/backfill.py +459 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1244 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1979 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/mc_admission.py +181 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2908 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +446 -0
- trueppm_api/apps/sso/services.py +1473 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +440 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +371 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +254 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/demo_read_only.py +314 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +1221 -0
- trueppm_api/core/password_reset.py +513 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/core/worker_selfheal.py +205 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Self-service password reset endpoints (#765, ADR-0209).
|
|
2
|
+
|
|
3
|
+
Two unauthenticated (``AllowAny``) endpoints drive the forgot-password flow:
|
|
4
|
+
|
|
5
|
+
- ``POST /api/v1/auth/password/reset/`` — accepts an email and, if it maps to an
|
|
6
|
+
account, emails a one-time reset link. The response is **identical** whether or
|
|
7
|
+
not the address exists, so it leaks no signal about which emails have accounts
|
|
8
|
+
(no user enumeration). Both branches return ``200`` with the same body.
|
|
9
|
+
- ``POST /api/v1/auth/password/reset/confirm/`` — accepts ``uid`` + ``token`` +
|
|
10
|
+
``new_password``, validates the stateless token, enforces the password policy,
|
|
11
|
+
sets the new password, and revokes every other session the account holds.
|
|
12
|
+
|
|
13
|
+
Token scheme (ADR-0209): Django's built-in ``default_token_generator``
|
|
14
|
+
(``PasswordResetTokenGenerator``) over a URL-safe-base64-encoded user PK — the same
|
|
15
|
+
mechanism Django's own ``PasswordResetConfirmView`` uses. The token is a keyed HMAC
|
|
16
|
+
that is stateless (no DB row), single-use by construction (it stops validating once
|
|
17
|
+
the password hash changes), and expires after ``settings.PASSWORD_RESET_TIMEOUT``
|
|
18
|
+
(30 minutes). We deliberately do not invent a bespoke token store.
|
|
19
|
+
|
|
20
|
+
Both endpoints carry a dedicated ``password_reset`` throttle scope (5/min per IP),
|
|
21
|
+
which is the primary defense against enumeration-by-probing and against looping the
|
|
22
|
+
request endpoint to email-bomb a victim (ADR-0209).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from django.conf import settings
|
|
31
|
+
from django.contrib.auth import get_user_model
|
|
32
|
+
from django.contrib.auth.password_validation import validate_password
|
|
33
|
+
from django.contrib.auth.tokens import default_token_generator
|
|
34
|
+
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
35
|
+
from django.core.mail import EmailMultiAlternatives
|
|
36
|
+
from django.db import transaction
|
|
37
|
+
from django.utils.encoding import force_bytes, force_str
|
|
38
|
+
from django.utils.html import escape
|
|
39
|
+
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
|
40
|
+
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
|
41
|
+
from rest_framework import serializers, status
|
|
42
|
+
from rest_framework.permissions import AllowAny
|
|
43
|
+
from rest_framework.request import Request
|
|
44
|
+
from rest_framework.response import Response
|
|
45
|
+
from rest_framework.throttling import ScopedRateThrottle
|
|
46
|
+
from rest_framework.views import APIView
|
|
47
|
+
|
|
48
|
+
from trueppm_api.core.throttling import resolve_client_ip
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("trueppm.auth")
|
|
51
|
+
|
|
52
|
+
User = get_user_model()
|
|
53
|
+
|
|
54
|
+
# Minimum length surfaced in the UI requirements checklist ("At least 10
|
|
55
|
+
# characters"). Enforced server-side here so the client checklist is advisory only
|
|
56
|
+
# — the server is the authority (ADR-0209).
|
|
57
|
+
_MIN_PASSWORD_LENGTH = 10
|
|
58
|
+
|
|
59
|
+
# Identical success body for the request endpoint regardless of whether the address
|
|
60
|
+
# has an account. Defined once so the two branches cannot drift apart and re-open an
|
|
61
|
+
# enumeration signal.
|
|
62
|
+
_REQUEST_OK_DETAIL = "If an account exists for that address, a password reset link is on its way."
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PasswordResetRequestSerializer(serializers.Serializer): # type: ignore[type-arg]
|
|
66
|
+
"""Request body for the reset-link endpoint: a single email address."""
|
|
67
|
+
|
|
68
|
+
email = serializers.EmailField()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PasswordResetConfirmSerializer(serializers.Serializer): # type: ignore[type-arg]
|
|
72
|
+
"""Request body for the confirm endpoint: uid + token + the new password.
|
|
73
|
+
|
|
74
|
+
Field-level validation only checks *shape* (non-empty strings). The token
|
|
75
|
+
validity check and the user-dependent password policy run in the view so it can
|
|
76
|
+
return distinct, non-leaking error codes (``invalid_token`` vs
|
|
77
|
+
``weak_password``) that the frontend maps to different screens.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
uid = serializers.CharField(trim_whitespace=False)
|
|
81
|
+
token = serializers.CharField(trim_whitespace=False)
|
|
82
|
+
# trim_whitespace=False: a trailing/leading space is a legitimate part of a
|
|
83
|
+
# password and must not be silently stripped before hashing.
|
|
84
|
+
new_password = serializers.CharField(write_only=True, trim_whitespace=False)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def enforce_reset_password_policy(new_password: str, user: Any) -> None:
|
|
88
|
+
"""Validate ``new_password`` against the reset-flow policy (ADR-0209).
|
|
89
|
+
|
|
90
|
+
Aggregates every violation into one ``DjangoValidationError`` so the API can
|
|
91
|
+
return the full list at once (the frontend renders them as an inline checklist),
|
|
92
|
+
rather than surfacing one error per round-trip. Policy:
|
|
93
|
+
|
|
94
|
+
1. At least ``_MIN_PASSWORD_LENGTH`` (10) characters.
|
|
95
|
+
2. Contains at least one number OR one symbol (a non-alphanumeric character).
|
|
96
|
+
3. Passes Django's configured ``AUTH_PASSWORD_VALIDATORS`` (common-password,
|
|
97
|
+
numeric-only, and attribute-similarity checks — run with ``user`` so the
|
|
98
|
+
similarity validator can compare against the account's own attributes).
|
|
99
|
+
4. Is not identical to the account's current password ("not a previously used
|
|
100
|
+
password" in the UI — the migration-free interpretation: you cannot reset to
|
|
101
|
+
the password you already have; full N-hash history is a deferred follow-up).
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
DjangoValidationError: with a ``messages`` list of all violations found.
|
|
105
|
+
"""
|
|
106
|
+
errors: list[str] = []
|
|
107
|
+
|
|
108
|
+
if len(new_password) < _MIN_PASSWORD_LENGTH:
|
|
109
|
+
errors.append(f"Password must be at least {_MIN_PASSWORD_LENGTH} characters.")
|
|
110
|
+
|
|
111
|
+
if not any(c.isdigit() or not c.isalnum() for c in new_password):
|
|
112
|
+
errors.append("Password must include at least one number or symbol.")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
validate_password(new_password, user)
|
|
116
|
+
except DjangoValidationError as exc:
|
|
117
|
+
errors.extend(exc.messages)
|
|
118
|
+
|
|
119
|
+
# check_password is a constant-time-ish hash comparison; only meaningful when the
|
|
120
|
+
# account already has a usable password (it always does on this flow).
|
|
121
|
+
if user is not None and user.has_usable_password() and user.check_password(new_password):
|
|
122
|
+
errors.append("Your new password must be different from your current password.")
|
|
123
|
+
|
|
124
|
+
if errors:
|
|
125
|
+
# De-duplicate while preserving order — the min-length rule and Django's
|
|
126
|
+
# MinimumLengthValidator can both fire for a very short password.
|
|
127
|
+
seen: set[str] = set()
|
|
128
|
+
deduped = [m for m in errors if not (m in seen or seen.add(m))] # type: ignore[func-returns-value]
|
|
129
|
+
raise DjangoValidationError(deduped)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _client_ip(request: Request) -> str | None:
|
|
133
|
+
"""Best-effort client IP for the token-revocation audit rows (#2878).
|
|
134
|
+
|
|
135
|
+
**Validated, not merely extracted, and that is load-bearing here.** The value lands
|
|
136
|
+
in a ``GenericIPAddressField`` via ``bulk_create``, which does not run
|
|
137
|
+
``full_clean``, inside the password-reset confirm transaction — and
|
|
138
|
+
``ATOMIC_REQUESTS`` is on. An unparseable header therefore does not degrade to a
|
|
139
|
+
null column: it raises (``ValidationError`` from Django's IPv6 cleaner on
|
|
140
|
+
``1.2.3.4:5678``, or a psycopg ``DataError`` on ``inet`` for RFC-7239's literal
|
|
141
|
+
``unknown``), the whole request rolls back, and **the user's password is not
|
|
142
|
+
changed** — silently, only for users who happen to hold an active token, on a
|
|
143
|
+
deployment whose ingress is merely misconfigured. Account recovery is the last
|
|
144
|
+
thing that may be taken down by an audit field, so anything that is not an address
|
|
145
|
+
becomes ``None``.
|
|
146
|
+
|
|
147
|
+
``None`` rather than a placeholder string for the same reason: ``"unknown"`` is not
|
|
148
|
+
a valid ``inet``.
|
|
149
|
+
|
|
150
|
+
The address the per-IP throttles key on (``core.throttling.resolve_client_ip``),
|
|
151
|
+
never the client-written left-most ``X-Forwarded-For`` entry. Recorded only,
|
|
152
|
+
never used for a security decision.
|
|
153
|
+
"""
|
|
154
|
+
return resolve_client_ip(request)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _reset_link(uid: str, token: str) -> str:
|
|
158
|
+
"""Build the absolute frontend confirm URL, or "" when the base URL is unset.
|
|
159
|
+
|
|
160
|
+
Mirrors the invite/notification/export idiom (``workspace/tasks.py``): read
|
|
161
|
+
``FRONTEND_BASE_URL``, strip a trailing slash, and return "" when it is not
|
|
162
|
+
configured so the email builder can omit the link rather than emit a broken
|
|
163
|
+
relative URL.
|
|
164
|
+
|
|
165
|
+
The credential rides in the URL **fragment**, not the path (#3553). ``uid`` and
|
|
166
|
+
``token`` together are a 30-minute bearer credential for the account, and a
|
|
167
|
+
path carrying them is exported by the SPA's telemetry envelope, by its
|
|
168
|
+
route-error boundary, and to any cross-origin destination as ``Referer``. A
|
|
169
|
+
fragment is never transmitted to any server at all, so none of the three can
|
|
170
|
+
see it — and neither can a path-logging proxy in front of the SPA.
|
|
171
|
+
|
|
172
|
+
The SPA route is ``/reset-password/confirm``; it reads ``uid``/``token`` from
|
|
173
|
+
``window.location.hash`` (``packages/web/src/features/auth/passwordReset/resetLink.ts``).
|
|
174
|
+
"""
|
|
175
|
+
base = (getattr(settings, "FRONTEND_BASE_URL", "") or "").rstrip("/")
|
|
176
|
+
if not base:
|
|
177
|
+
return ""
|
|
178
|
+
return f"{base}/reset-password/confirm#uid={uid}&token={token}"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _render_password_reset_email(user: Any, reset_url: str) -> tuple[str, str, str]:
|
|
182
|
+
"""Render (subject, plain-text body, html body) for the reset email.
|
|
183
|
+
|
|
184
|
+
Follows the inline-render idiom used by invites/notifications/exports (no
|
|
185
|
+
templates directory exists in the API). Returns both a plain-text body and an
|
|
186
|
+
HTML alternative so the message is sent multipart. The reset link is included
|
|
187
|
+
only when configured; otherwise the copy tells the user to contact their admin
|
|
188
|
+
(matching how every other email degrades when ``FRONTEND_BASE_URL`` is unset).
|
|
189
|
+
"""
|
|
190
|
+
subject = "Reset your TruePPM password"
|
|
191
|
+
greeting_name = user.get_short_name() or user.get_username()
|
|
192
|
+
|
|
193
|
+
if reset_url:
|
|
194
|
+
link_line_text = reset_url
|
|
195
|
+
# escape() defends against a display name / URL containing HTML-significant
|
|
196
|
+
# characters; the URL itself is server-built so it is trusted, but escaping
|
|
197
|
+
# is cheap insurance and keeps the HTML well-formed.
|
|
198
|
+
link_line_html = f'<a href="{escape(reset_url)}">Reset your password</a>'
|
|
199
|
+
cta_text = "Click the link below to choose a new password:"
|
|
200
|
+
else:
|
|
201
|
+
link_line_text = "Contact your workspace administrator to reset your password."
|
|
202
|
+
link_line_html = "Contact your workspace administrator to reset your password."
|
|
203
|
+
cta_text = "We could not build a reset link for this deployment."
|
|
204
|
+
|
|
205
|
+
text_body = "\n".join(
|
|
206
|
+
[
|
|
207
|
+
f"Hi {greeting_name},",
|
|
208
|
+
"",
|
|
209
|
+
"We received a request to reset the password for your TruePPM account.",
|
|
210
|
+
cta_text,
|
|
211
|
+
"",
|
|
212
|
+
link_line_text,
|
|
213
|
+
"",
|
|
214
|
+
"This link is valid for 30 minutes. For your security, using it will sign "
|
|
215
|
+
"you out of every other device.",
|
|
216
|
+
"",
|
|
217
|
+
"If you did not request a password reset, you can safely ignore this email "
|
|
218
|
+
"— your password will not change.",
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
html_body = "".join(
|
|
223
|
+
[
|
|
224
|
+
f"<p>Hi {escape(greeting_name)},</p>",
|
|
225
|
+
"<p>We received a request to reset the password for your TruePPM account. "
|
|
226
|
+
f"{escape(cta_text)}</p>",
|
|
227
|
+
f"<p>{link_line_html}</p>",
|
|
228
|
+
"<p>This link is valid for <strong>30 minutes</strong>. For your security, "
|
|
229
|
+
"using it will sign you out of every other device.</p>",
|
|
230
|
+
"<p>If you did not request a password reset, you can safely ignore this "
|
|
231
|
+
"email — your password will not change.</p>",
|
|
232
|
+
]
|
|
233
|
+
)
|
|
234
|
+
return subject, text_body, html_body
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def send_password_reset_email(user: Any) -> bool:
|
|
238
|
+
"""Generate a reset token for ``user`` and email the link. Best-effort.
|
|
239
|
+
|
|
240
|
+
Synchronous by design (ADR-0209 §Durable Execution): the reset token is
|
|
241
|
+
stateless and fully re-requestable, so there is no persisted state to reconcile
|
|
242
|
+
if the send fails — the user simply clicks "Resend". A failed SMTP send is logged
|
|
243
|
+
and swallowed so it never turns into a 500 (which would itself leak that the
|
|
244
|
+
address exists). Returns True on a successful send, False otherwise; callers on
|
|
245
|
+
the request endpoint ignore the result to keep the response identical either way.
|
|
246
|
+
"""
|
|
247
|
+
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
|
248
|
+
token = default_token_generator.make_token(user)
|
|
249
|
+
reset_url = _reset_link(uid, token)
|
|
250
|
+
|
|
251
|
+
recipient = getattr(user, "email", "") or ""
|
|
252
|
+
if not recipient:
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
subject, text_body, html_body = _render_password_reset_email(user, reset_url)
|
|
256
|
+
# Route through the workspace SMTP transport (#712, ADR-0213) so BYO-SMTP
|
|
257
|
+
# installs deliver reset mail on the same transport as everything else; a
|
|
258
|
+
# no-op fall back to the global backend when the workspace is unconfigured.
|
|
259
|
+
from trueppm_api.apps.notifications.delivery_limits import (
|
|
260
|
+
note_unbudgeted_send,
|
|
261
|
+
workspace_throttle_per_min,
|
|
262
|
+
)
|
|
263
|
+
from trueppm_api.apps.notifications.email_backend import (
|
|
264
|
+
resolve_email_connection,
|
|
265
|
+
resolve_from_email,
|
|
266
|
+
resolve_reply_to,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# A configured-but-unusable transport now raises rather than silently rerouting
|
|
270
|
+
# (#2886 item 2). Swallow it here for the same reason the send failure below is
|
|
271
|
+
# swallowed: a 500 on this endpoint would itself leak that the address exists.
|
|
272
|
+
try:
|
|
273
|
+
connection = resolve_email_connection()
|
|
274
|
+
except Exception:
|
|
275
|
+
logger.warning("password reset email: mail transport unusable")
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
# This is one user-blocking transactional message, so it *charges* the shared
|
|
279
|
+
# per-minute delivery budget but is never refused by it (#2887 item 3). Refusing a
|
|
280
|
+
# reset because a batch of invites spent the minute would be a far worse failure
|
|
281
|
+
# than briefly exceeding a soft rate; charging it makes the queued drains yield.
|
|
282
|
+
note_unbudgeted_send(workspace_throttle_per_min())
|
|
283
|
+
|
|
284
|
+
msg = EmailMultiAlternatives(
|
|
285
|
+
subject=subject,
|
|
286
|
+
body=text_body,
|
|
287
|
+
from_email=resolve_from_email(),
|
|
288
|
+
to=[recipient],
|
|
289
|
+
reply_to=resolve_reply_to() or None,
|
|
290
|
+
connection=connection,
|
|
291
|
+
)
|
|
292
|
+
msg.attach_alternative(html_body, "text/html")
|
|
293
|
+
try:
|
|
294
|
+
msg.send(fail_silently=False)
|
|
295
|
+
except Exception:
|
|
296
|
+
# Best-effort: a lost email has no persisted state to reconcile (the token
|
|
297
|
+
# is stateless and re-requestable). Never re-raise — a 500 here would itself
|
|
298
|
+
# leak that the address exists.
|
|
299
|
+
# No secret logged: the only interpolated value is user.pk (a UUID); the
|
|
300
|
+
# literal merely contains the word "password", which trips the heuristic.
|
|
301
|
+
# nosemgrep: python-logger-credential-disclosure
|
|
302
|
+
logger.warning("password reset email failed for user %s", user.pk, exc_info=True)
|
|
303
|
+
return False
|
|
304
|
+
return True
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class PasswordResetRequestView(APIView):
|
|
308
|
+
"""Request a password reset link. Always returns 200 (no user enumeration).
|
|
309
|
+
|
|
310
|
+
Unauthenticated (``AllowAny``) by design — a user who has forgotten their
|
|
311
|
+
password cannot authenticate. The response body is identical whether or not the
|
|
312
|
+
email maps to an account, so the endpoint reveals nothing about which addresses
|
|
313
|
+
have accounts. The residual timing signal (the exists branch does the extra work
|
|
314
|
+
of generating a token and sending mail) is accepted and bounded by the
|
|
315
|
+
``password_reset`` throttle (ADR-0209).
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
permission_classes = [AllowAny] # noqa: RUF012
|
|
319
|
+
authentication_classes: list[Any] = [] # noqa: RUF012
|
|
320
|
+
throttle_classes = [ScopedRateThrottle] # noqa: RUF012
|
|
321
|
+
throttle_scope = "password_reset"
|
|
322
|
+
|
|
323
|
+
@extend_schema(
|
|
324
|
+
summary="Request a password reset link",
|
|
325
|
+
description=(
|
|
326
|
+
"Sends a one-time password reset link to the address if — and only if — "
|
|
327
|
+
"it belongs to an account. The response is identical whether or not the "
|
|
328
|
+
"address exists, so it never reveals which emails have accounts. Rate "
|
|
329
|
+
"limited (5/min per IP)."
|
|
330
|
+
),
|
|
331
|
+
request=PasswordResetRequestSerializer,
|
|
332
|
+
responses={200: OpenApiResponse(description="Reset link sent if the account exists.")},
|
|
333
|
+
auth=[],
|
|
334
|
+
tags=["auth"],
|
|
335
|
+
)
|
|
336
|
+
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
|
337
|
+
serializer = PasswordResetRequestSerializer(data=request.data)
|
|
338
|
+
serializer.is_valid(raise_exception=True)
|
|
339
|
+
email = serializer.validated_data["email"]
|
|
340
|
+
|
|
341
|
+
# Case-insensitive lookup: emails are stored lower-cased on create (invite
|
|
342
|
+
# accept lower-cases; SSO lower-cases), but match defensively either way.
|
|
343
|
+
# Only ACTIVE accounts with a usable password receive a reset link — an SSO-
|
|
344
|
+
# only account (unusable password) or a deactivated account gets the same
|
|
345
|
+
# silent 200, never a link. ``order_by("pk")`` makes the recipient
|
|
346
|
+
# deterministic (the oldest account) in the pathological case where two
|
|
347
|
+
# active accounts share an email case-insensitively — the stock User model
|
|
348
|
+
# does not enforce email uniqueness.
|
|
349
|
+
user = (
|
|
350
|
+
User.objects.filter(email__iexact=email, is_active=True)
|
|
351
|
+
.exclude(password="")
|
|
352
|
+
.order_by("pk")
|
|
353
|
+
.first()
|
|
354
|
+
)
|
|
355
|
+
if user is not None and user.has_usable_password():
|
|
356
|
+
send_password_reset_email(user)
|
|
357
|
+
|
|
358
|
+
return Response({"detail": _REQUEST_OK_DETAIL}, status=status.HTTP_200_OK)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class PasswordResetConfirmView(APIView):
|
|
362
|
+
"""Confirm a password reset: validate uid+token, set the password, revoke sessions.
|
|
363
|
+
|
|
364
|
+
Unauthenticated (``AllowAny``) — possession of a valid, unexpired token *is* the
|
|
365
|
+
authorization. Returns:
|
|
366
|
+
|
|
367
|
+
- ``200`` on success (password changed, all other sessions revoked, and the
|
|
368
|
+
account's Personal Access Tokens, public share links, and git-automation
|
|
369
|
+
webhook secret revoked/cleared — #4006).
|
|
370
|
+
- ``400 {"code": "invalid_token"}`` for any bad/unknown/expired uid+token — the
|
|
371
|
+
same shape for all three so confirm carries no enumeration signal either. The
|
|
372
|
+
frontend maps this to the "expired link" screen.
|
|
373
|
+
- ``400 {"code": "weak_password", "messages": [...]}`` when the new password
|
|
374
|
+
fails the policy. The frontend renders the messages inline.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
permission_classes = [AllowAny] # noqa: RUF012
|
|
378
|
+
authentication_classes: list[Any] = [] # noqa: RUF012
|
|
379
|
+
throttle_classes = [ScopedRateThrottle] # noqa: RUF012
|
|
380
|
+
throttle_scope = "password_reset"
|
|
381
|
+
|
|
382
|
+
@extend_schema(
|
|
383
|
+
summary="Confirm a password reset",
|
|
384
|
+
description=(
|
|
385
|
+
"Validates the uid + token from the reset link and sets the new "
|
|
386
|
+
"password. On success, every other active session for the account is "
|
|
387
|
+
"revoked (all refresh tokens are blacklisted), the account's Personal "
|
|
388
|
+
"Access Tokens are revoked, and its public share links and "
|
|
389
|
+
"git-automation webhook secret are revoked/cleared. Returns 400 with "
|
|
390
|
+
"`code: invalid_token` for an invalid or expired link, or `code: "
|
|
391
|
+
"weak_password` with a `messages` list when the password fails policy."
|
|
392
|
+
),
|
|
393
|
+
request=PasswordResetConfirmSerializer,
|
|
394
|
+
responses={
|
|
395
|
+
200: OpenApiResponse(
|
|
396
|
+
description="Password reset; sessions, PATs, and durable grants revoked."
|
|
397
|
+
),
|
|
398
|
+
400: OpenApiResponse(description="Invalid/expired token or weak password."),
|
|
399
|
+
},
|
|
400
|
+
auth=[],
|
|
401
|
+
tags=["auth"],
|
|
402
|
+
)
|
|
403
|
+
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
|
404
|
+
serializer = PasswordResetConfirmSerializer(data=request.data)
|
|
405
|
+
serializer.is_valid(raise_exception=True)
|
|
406
|
+
uid = serializer.validated_data["uid"]
|
|
407
|
+
token = serializer.validated_data["token"]
|
|
408
|
+
new_password = serializer.validated_data["new_password"]
|
|
409
|
+
|
|
410
|
+
user = self._resolve_user(uid)
|
|
411
|
+
# Constant error for every token-side failure (bad uid, unknown user, wrong
|
|
412
|
+
# or expired token) so the response never distinguishes them — no enumeration.
|
|
413
|
+
if user is None or not default_token_generator.check_token(user, token):
|
|
414
|
+
return Response(
|
|
415
|
+
{
|
|
416
|
+
"code": "invalid_token",
|
|
417
|
+
"detail": "This password reset link is invalid or has expired.",
|
|
418
|
+
},
|
|
419
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
enforce_reset_password_policy(new_password, user)
|
|
424
|
+
except DjangoValidationError as exc:
|
|
425
|
+
return Response(
|
|
426
|
+
{
|
|
427
|
+
"code": "weak_password",
|
|
428
|
+
"detail": "Password does not meet the requirements.",
|
|
429
|
+
"messages": list(exc.messages),
|
|
430
|
+
},
|
|
431
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Revoke every other session (ADR-0209): the confirm request itself is
|
|
435
|
+
# unauthenticated, so this signs the account out of every device where it was
|
|
436
|
+
# logged in. The password save and the session revocation run in one
|
|
437
|
+
# transaction so the account can never end up password-changed-but-sessions-
|
|
438
|
+
# live (or vice versa) — defense in depth that does not rely solely on the
|
|
439
|
+
# global ATOMIC_REQUESTS setting.
|
|
440
|
+
from trueppm_api.apps.access.services import (
|
|
441
|
+
revoke_all_personal_access_tokens,
|
|
442
|
+
revoke_all_refresh_tokens,
|
|
443
|
+
revoke_personal_durable_grants,
|
|
444
|
+
)
|
|
445
|
+
from trueppm_api.apps.access.signals import evict_all_live_project_sockets
|
|
446
|
+
|
|
447
|
+
with transaction.atomic():
|
|
448
|
+
# Password already validated above by enforce_reset_password_policy(),
|
|
449
|
+
# which runs django...validate_password(new_password, user) plus the
|
|
450
|
+
# reset-flow policy; set_password is only reached once that passed.
|
|
451
|
+
# nosemgrep: unvalidated-password
|
|
452
|
+
user.set_password(new_password)
|
|
453
|
+
user.save(update_fields=["password"])
|
|
454
|
+
revoke_all_refresh_tokens(user)
|
|
455
|
+
# A password change also invalidates the user's Personal Access Tokens
|
|
456
|
+
# (ADR-0214): a PAT bears the user's full authority, so a "my credentials
|
|
457
|
+
# may be compromised" reset must cut off long-lived personal credentials,
|
|
458
|
+
# not just live sessions. Project/program tokens are org assets and are
|
|
459
|
+
# deliberately left untouched. Same atomic block so password-changed and
|
|
460
|
+
# PAT-revoked commit together or not at all. Each revoked token now writes
|
|
461
|
+
# an ApiTokenAuditEntry (#2878) — `actor=user` because a reset is the
|
|
462
|
+
# account owner asserting compromise, even though the request itself is
|
|
463
|
+
# unauthenticated, and the source IP so an incident review can tell a
|
|
464
|
+
# self-service rotation from a takeover.
|
|
465
|
+
revoke_all_personal_access_tokens(
|
|
466
|
+
user,
|
|
467
|
+
actor=user,
|
|
468
|
+
reason="password_reset",
|
|
469
|
+
source_ip=_client_ip(request),
|
|
470
|
+
)
|
|
471
|
+
# A password reset is the same "my credentials are compromised" signal
|
|
472
|
+
# the off-boarding path acts on, but before #4006 it only revoked
|
|
473
|
+
# sessions and PATs — not the non-token durable grants that are
|
|
474
|
+
# "exactly as durable as a personal access token" per
|
|
475
|
+
# revoke_personal_durable_grants()'s own docstring: public share links
|
|
476
|
+
# this user minted, any git-automation webhook secret they configured,
|
|
477
|
+
# (#4461) any outgoing webhook they registered, and (#4589) any pending
|
|
478
|
+
# workspace invite they sent. Without this, an
|
|
479
|
+
# attacker who held a live session could mint a share link, rotate an
|
|
480
|
+
# automation secret, or keep an outgoing webhook delivering, and all
|
|
481
|
+
# three survived a reset untouched. Same atomic block as the other two
|
|
482
|
+
# revocations — `actor=user` for the same reason: the reset itself is
|
|
483
|
+
# unauthenticated, but it is the account owner asserting compromise.
|
|
484
|
+
revoke_personal_durable_grants(user, actor=user)
|
|
485
|
+
# A reset revokes every credential above, but a socket opened before the
|
|
486
|
+
# reset stays open until it disconnects on its own — the WS consumer
|
|
487
|
+
# resolves identity once, at connect, and never re-checks it (#4462,
|
|
488
|
+
# the #2850 class). Same eviction loop _evict_on_account_deactivation
|
|
489
|
+
# uses, deferred to commit the same way.
|
|
490
|
+
evict_all_live_project_sockets(user.pk)
|
|
491
|
+
|
|
492
|
+
return Response(
|
|
493
|
+
{"detail": "Your password has been reset. Please sign in with your new password."},
|
|
494
|
+
status=status.HTTP_200_OK,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
@staticmethod
|
|
498
|
+
def _resolve_user(uid: str) -> Any | None:
|
|
499
|
+
"""Decode the urlsafe-base64 uid to a user, or None on any decode/lookup miss.
|
|
500
|
+
|
|
501
|
+
A malformed uid, a uid that decodes to a non-existent PK, or a decode error
|
|
502
|
+
all resolve to None — the caller returns the same ``invalid_token`` response
|
|
503
|
+
for all of them, so a probing attacker cannot tell a bad uid from a valid uid
|
|
504
|
+
with a wrong token.
|
|
505
|
+
"""
|
|
506
|
+
try:
|
|
507
|
+
pk = force_str(urlsafe_base64_decode(uid))
|
|
508
|
+
except (TypeError, ValueError, OverflowError, UnicodeDecodeError):
|
|
509
|
+
return None
|
|
510
|
+
try:
|
|
511
|
+
return User.objects.get(pk=pk, is_active=True)
|
|
512
|
+
except (User.DoesNotExist, User.MultipleObjectsReturned, ValueError, TypeError):
|
|
513
|
+
return None
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Shared rendering for ``PROTECT``-ed delete refusals (#2364).
|
|
2
|
+
|
|
3
|
+
Django raises :class:`~django.db.models.ProtectedError` when a ``DELETE`` would
|
|
4
|
+
orphan rows behind an ``on_delete=PROTECT`` foreign key. It is not an
|
|
5
|
+
``APIException``, so DRF's stock handler returns ``None`` for it and the request
|
|
6
|
+
escapes as an unhandled **500** — a server error for what is really a
|
|
7
|
+
well-formed, authorized request that conflicts with current state. **409** is the
|
|
8
|
+
honest status.
|
|
9
|
+
|
|
10
|
+
Two layers use this module, and the split is deliberate:
|
|
11
|
+
|
|
12
|
+
* :func:`protected_error_response` builds the *rich* body — a
|
|
13
|
+
``reference_count`` plus a bounded ``references`` sample naming what still
|
|
14
|
+
points at the row — for the endpoints where a client can realistically act on
|
|
15
|
+
the list (detach the calendar, retag the resource). Views opt in per-endpoint
|
|
16
|
+
because only the view knows how to name a blocker usefully.
|
|
17
|
+
* :func:`trueppm_exception_handler` falls back to a generic, **count-only** 409 for
|
|
18
|
+
every other ``PROTECT``-ed delete. That net is what stops the *next* ``PROTECT``
|
|
19
|
+
FK from reintroducing the 500 — the failure mode that produced this module, where
|
|
20
|
+
a delete path written against the FKs that existed at the time silently rotted as
|
|
21
|
+
new ones landed. It never names rows: it fires on paths that have not reasoned
|
|
22
|
+
about their permission gate at all, so it cannot know whether the caller is
|
|
23
|
+
authorized to see them.
|
|
24
|
+
|
|
25
|
+
Responses are *returned*, never raised as an ``APIException``: DRF coerces every
|
|
26
|
+
value in an exception ``detail`` to ``ErrorDetail`` (a ``str`` subclass), which
|
|
27
|
+
would ship ``reference_count`` as ``"3"`` instead of ``3``. That is safe because
|
|
28
|
+
``ProtectedError`` is raised while the collector walks the FKs, *before* any
|
|
29
|
+
DELETE is issued — there is no partial state for ``ATOMIC_REQUESTS`` to roll back.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from collections.abc import Callable, Iterable
|
|
35
|
+
|
|
36
|
+
from django.db import models
|
|
37
|
+
from rest_framework import status
|
|
38
|
+
from rest_framework.response import Response
|
|
39
|
+
|
|
40
|
+
#: Cap on the referencing rows echoed back on a 409. A shared library row can be
|
|
41
|
+
#: referenced by hundreds of others; the client needs enough to name the blockers,
|
|
42
|
+
#: not the whole set.
|
|
43
|
+
REFERENCE_SAMPLE_SIZE = 10
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def describe_reference(obj: models.Model) -> dict[str, str]:
|
|
47
|
+
"""Render one ``PROTECT``-ing row as a ``{type, id, name}`` entry.
|
|
48
|
+
|
|
49
|
+
The default shape for any blocker a user can recognize by name. Views pass a
|
|
50
|
+
custom callable to :func:`protected_error_response` when the blocker is a join
|
|
51
|
+
row the user never sees and the actionable object is its parent.
|
|
52
|
+
"""
|
|
53
|
+
return {
|
|
54
|
+
"type": obj._meta.model_name or "",
|
|
55
|
+
"id": str(obj.pk),
|
|
56
|
+
"name": str(getattr(obj, "name", "") or obj._meta.verbose_name),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def protected_error_response(
|
|
61
|
+
protected_objects: Iterable[models.Model],
|
|
62
|
+
*,
|
|
63
|
+
detail: str,
|
|
64
|
+
code: str,
|
|
65
|
+
describe: Callable[[models.Model], dict[str, str]] | None = None,
|
|
66
|
+
) -> Response:
|
|
67
|
+
"""Build the 409 body for a refused delete.
|
|
68
|
+
|
|
69
|
+
``describe`` is **opt-in on purpose**: naming the blocking rows discloses data,
|
|
70
|
+
and only the endpoint knows whether its permission gate authorizes the caller to
|
|
71
|
+
see those names. Omitting it returns the count alone — always safe, because a
|
|
72
|
+
caller who just tried the delete already knows the row exists, and a bare count
|
|
73
|
+
reveals nothing about *what* references it.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
protected_objects: ``ProtectedError.protected_objects`` — the rows whose
|
|
77
|
+
``PROTECT`` foreign keys blocked the delete.
|
|
78
|
+
detail: Human-readable refusal, shown to the user as-is.
|
|
79
|
+
code: Stable machine-readable slug the client branches on.
|
|
80
|
+
describe: Renders one blocker. Pass one only after confirming the endpoint's
|
|
81
|
+
permission gate authorizes the caller to see every name it can emit —
|
|
82
|
+
and have it drop or redact rows the caller cannot see. Omit to return
|
|
83
|
+
``reference_count`` with no ``references`` key at all.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A ``409`` response carrying ``detail``, ``code`` and an integer
|
|
87
|
+
``reference_count``, plus — when ``describe`` is given — a ``references``
|
|
88
|
+
sample capped at :data:`REFERENCE_SAMPLE_SIZE`.
|
|
89
|
+
"""
|
|
90
|
+
# Sort before sampling so the truncated list is deterministic — an arbitrary
|
|
91
|
+
# 10-of-200 that reshuffles per request makes the refusal impossible to act on.
|
|
92
|
+
blockers = sorted(protected_objects, key=lambda o: (o._meta.model_name or "", str(o.pk)))
|
|
93
|
+
body: dict[str, object] = {
|
|
94
|
+
"detail": detail,
|
|
95
|
+
"code": code,
|
|
96
|
+
"reference_count": len(blockers),
|
|
97
|
+
}
|
|
98
|
+
if describe is not None:
|
|
99
|
+
body["references"] = [describe(o) for o in blockers[:REFERENCE_SAMPLE_SIZE]]
|
|
100
|
+
return Response(body, status=status.HTTP_409_CONFLICT)
|