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,51 @@
|
|
|
1
|
+
"""The one constant-time comparison primitive for caller-controlled input (#2881, #2929).
|
|
2
|
+
|
|
3
|
+
``hmac.compare_digest`` (which ``secrets.compare_digest`` *is*) raises
|
|
4
|
+
``TypeError`` when either ``str`` argument contains a non-ASCII character. DRF
|
|
5
|
+
does not convert ``TypeError``, so any site that hands it a request-derived
|
|
6
|
+
string turns one byte of Latin-1 into an unhandled 500 — on precisely the
|
|
7
|
+
endpoints that are reachable anonymously, because those are the ones that
|
|
8
|
+
compare a secret against something a caller sent.
|
|
9
|
+
|
|
10
|
+
That defect shipped twice: once in the Git-webhook receiver (#2881), where the
|
|
11
|
+
500 was also an oracle — it is only reachable *after* the "is automation
|
|
12
|
+
configured?" check, so its mere shape told an anonymous caller that a project
|
|
13
|
+
has automation enabled with a secret set, exactly the fact the receiver's
|
|
14
|
+
uniform 404 exists to hide — and again in the SSO OIDC callback (#2929), where
|
|
15
|
+
the same line had the same flaw and the same-named test exercised it with an
|
|
16
|
+
ASCII value.
|
|
17
|
+
|
|
18
|
+
Two copies of the guard is how a third site appears silently, so this module is
|
|
19
|
+
the single home. ``tests/test_constant_time_compare_sites.py`` enforces that:
|
|
20
|
+
no other module in ``trueppm_api`` may call ``compare_digest`` directly.
|
|
21
|
+
|
|
22
|
+
Comparing **bytes** removes the raising path entirely rather than special-casing
|
|
23
|
+
the input. WSGI/ASGI hand header values over as ``Latin-1``-decoded text
|
|
24
|
+
(RFC 9110 §5.5), so ``latin-1`` round-trips the bytes a client actually sent;
|
|
25
|
+
the ``utf-8`` fallback covers input that carries a real code point above U+00FF
|
|
26
|
+
(a query parameter, a cookie, or a synthetic caller such as a test). Every
|
|
27
|
+
``expected`` value at our call sites is ASCII — a hex HMAC digest, a
|
|
28
|
+
``secrets.token_urlsafe`` secret, or a server-minted OAuth ``state`` — so no
|
|
29
|
+
encoding choice can make a mismatched value compare equal.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import hmac
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def constant_time_equal(provided: str, expected: str) -> bool:
|
|
38
|
+
"""Constant-time compare caller-controlled ``provided`` against ``expected``.
|
|
39
|
+
|
|
40
|
+
Never raises on non-ASCII input. Both arguments may be attacker-controlled:
|
|
41
|
+
the SSO callback compares a query parameter against a cookie, and both sides
|
|
42
|
+
of that come from the request.
|
|
43
|
+
"""
|
|
44
|
+
return hmac.compare_digest(_to_bytes(provided), _to_bytes(expected))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _to_bytes(value: str) -> bytes:
|
|
48
|
+
try:
|
|
49
|
+
return value.encode("latin-1")
|
|
50
|
+
except UnicodeEncodeError:
|
|
51
|
+
return value.encode("utf-8")
|
trueppm_api/core/csp.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Content-Security-Policy response-header middleware (#897).
|
|
2
|
+
|
|
3
|
+
A strict CSP is the primary defense-in-depth control against XSS: even if an
|
|
4
|
+
injection lands, ``script-src 'self'`` forbids inline and remote script
|
|
5
|
+
execution, ``base-uri 'none'`` blocks base-tag hijacking, and
|
|
6
|
+
``frame-ancestors 'none'`` prevents clickjacking. The policy is assembled from
|
|
7
|
+
settings so the ``connect-src`` host (e.g. the wss:// endpoint) and the
|
|
8
|
+
theme-init script hash can be overridden per deploy without code changes.
|
|
9
|
+
|
|
10
|
+
The CSP is set as an HTTP response header (not only a ``<meta>`` tag) because
|
|
11
|
+
``frame-ancestors`` and a handful of other directives are ignored when delivered
|
|
12
|
+
via ``<meta>`` — the header is authoritative.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
|
|
19
|
+
from django.conf import settings
|
|
20
|
+
from django.http import HttpRequest, HttpResponse
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_policy() -> str:
|
|
24
|
+
"""Assemble the CSP header value from ``settings.CSP_DIRECTIVES``.
|
|
25
|
+
|
|
26
|
+
Each directive maps to a list of source expressions; they are joined into the
|
|
27
|
+
standard ``name src1 src2; name2 ...`` header syntax. Centralizing the policy
|
|
28
|
+
in settings keeps per-deploy overrides (connect-src host, script hash) in one
|
|
29
|
+
place rather than scattered string concatenation.
|
|
30
|
+
"""
|
|
31
|
+
directives: dict[str, list[str]] = settings.CSP_DIRECTIVES
|
|
32
|
+
parts = []
|
|
33
|
+
for name, sources in directives.items():
|
|
34
|
+
if sources:
|
|
35
|
+
parts.append(f"{name} {' '.join(sources)}")
|
|
36
|
+
else:
|
|
37
|
+
# Valueless directive (e.g. nothing) is uncommon; emit the name alone.
|
|
38
|
+
parts.append(name)
|
|
39
|
+
return "; ".join(parts)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ContentSecurityPolicyMiddleware:
|
|
43
|
+
"""Attach a ``Content-Security-Policy`` header to every response."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
|
|
46
|
+
self.get_response = get_response
|
|
47
|
+
# Build once at process start — the policy is static per deploy.
|
|
48
|
+
self.policy = _build_policy()
|
|
49
|
+
|
|
50
|
+
def __call__(self, request: HttpRequest) -> HttpResponse:
|
|
51
|
+
response = self.get_response(request)
|
|
52
|
+
# Do not clobber a per-view CSP if one was already set deliberately.
|
|
53
|
+
response.setdefault("Content-Security-Policy", self.policy)
|
|
54
|
+
return response
|
trueppm_api/core/db.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""The one interpolated-SQL site in the API: atomic counter allocation (#2567).
|
|
2
|
+
|
|
3
|
+
Two counters are drawn the same way and must be: ``VersionedModel.server_version``
|
|
4
|
+
(a row's own save count, ADR-0142) and ``Project.last_sync_version`` (the allocator
|
|
5
|
+
for a project's ``sync_seq`` sequence, ADR-0686). Both need the *new* value in the
|
|
6
|
+
same statement that produces it::
|
|
7
|
+
|
|
8
|
+
UPDATE <table> SET <col> = <col> + 1 WHERE <pk> = %s RETURNING <col>
|
|
9
|
+
|
|
10
|
+
The Django ORM cannot express that. ``QuerySet.update()`` returns a row count, not
|
|
11
|
+
values, and ``db_returning`` applies to INSERT only — so the alternative is
|
|
12
|
+
``update(**{col: F(col) + 1})`` followed by a separate ``SELECT``. That is two
|
|
13
|
+
statements, and outside an explicit transaction the re-read can observe a *different*
|
|
14
|
+
writer's allocation. For ``sync_seq`` that is #2491 — the delta permanently skipping
|
|
15
|
+
rows — reintroduced as a race; the single statement is also what takes and holds the
|
|
16
|
+
row write lock that ADR-0686 documents as load-bearing. So this module keeps the raw
|
|
17
|
+
SQL, and keeps it in exactly one place.
|
|
18
|
+
|
|
19
|
+
**Why one place.** Both call sites carried their own copy of the statement until
|
|
20
|
+
#2567. `sonar-project.properties` documents the pattern as a reviewed false positive
|
|
21
|
+
under a path-pinned `python:S2077` criterion, and when #2491 added the second copy in
|
|
22
|
+
a new file the glob did not follow it — a reviewed-safe pattern resurfaced as a
|
|
23
|
+
new-code Security finding. One helper means one criterion with nowhere to drift to.
|
|
24
|
+
|
|
25
|
+
**Why it is safe.** Nothing user-controlled reaches the statement. The interpolated
|
|
26
|
+
parts are *identifiers* read off the model ``_meta`` and passed through the backend's
|
|
27
|
+
own quoter (``connection.ops.quote_name``); the pk is bound as a parameter. Callers
|
|
28
|
+
name the field, not the column, so a caller cannot supply a column name at all.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from django.db import connections, models, router
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def increment_returning(
|
|
39
|
+
model: type[models.Model],
|
|
40
|
+
field_name: str,
|
|
41
|
+
pk: Any,
|
|
42
|
+
*,
|
|
43
|
+
instance: models.Model | None = None,
|
|
44
|
+
) -> int | None:
|
|
45
|
+
"""Atomically add 1 to one integer column and return its new value.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
model: The concrete model owning the column.
|
|
49
|
+
field_name: Model field name (not a column name) to increment.
|
|
50
|
+
pk: Primary key of the row to increment.
|
|
51
|
+
instance: Passed to the database router when the write target depends on
|
|
52
|
+
the row itself; omit when the model alone decides.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The post-increment value, or ``None`` when the ``UPDATE`` matched no row.
|
|
56
|
+
A caller sees ``None`` when the pk vanished under it (a concurrent hard
|
|
57
|
+
delete) or never existed — each decides what that means, so this function
|
|
58
|
+
does not guess.
|
|
59
|
+
"""
|
|
60
|
+
# Forward ``instance`` as a router hint only when there is one — a router that
|
|
61
|
+
# reads ``hints["instance"]`` would trip over a present-but-None value.
|
|
62
|
+
hints: dict[str, Any] = {} if instance is None else {"instance": instance}
|
|
63
|
+
connection = connections[router.db_for_write(model, **hints)]
|
|
64
|
+
quote = connection.ops.quote_name
|
|
65
|
+
pk_field = model._meta.pk
|
|
66
|
+
field = model._meta.get_field(field_name)
|
|
67
|
+
# ``get_field`` also yields reverse relations, which have no column at all.
|
|
68
|
+
# Callers name a concrete integer counter, so narrow the union rather than
|
|
69
|
+
# letting a typo reach the SQL as ``None``.
|
|
70
|
+
assert isinstance(field, models.Field)
|
|
71
|
+
column_name = field.column
|
|
72
|
+
# A concrete model always has a pk and real column names; narrow the stubs'
|
|
73
|
+
# broadened ``str | None`` so the quoted identifiers are str.
|
|
74
|
+
assert pk_field is not None and pk_field.column is not None
|
|
75
|
+
assert column_name is not None
|
|
76
|
+
table = quote(model._meta.db_table)
|
|
77
|
+
column = quote(column_name)
|
|
78
|
+
pk_col = quote(pk_field.column)
|
|
79
|
+
with connection.cursor() as cursor:
|
|
80
|
+
# nosemgrep: formatted-sql-query, sqlalchemy-execute-raw-query
|
|
81
|
+
cursor.execute(
|
|
82
|
+
f"UPDATE {table} SET {column} = {column} + 1 " # nosec B608
|
|
83
|
+
f"WHERE {pk_col} = %s RETURNING {column}",
|
|
84
|
+
[pk],
|
|
85
|
+
)
|
|
86
|
+
row = cursor.fetchone()
|
|
87
|
+
return int(row[0]) if row is not None else None
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Session settings applied to every PostgreSQL connection the API or a worker opens.
|
|
2
|
+
|
|
3
|
+
**Why JIT is off (#3829).** PostgreSQL JIT-compiles any query whose *estimated* cost
|
|
4
|
+
crosses ``jit_above_cost`` (default 100,000), and also optimizes and inlines it past
|
|
5
|
+
``jit_optimize_above_cost`` / ``jit_inline_above_cost`` (500,000). The task list's
|
|
6
|
+
annotated page query crosses all three once planner statistics describe a ~2,000-task
|
|
7
|
+
project, and PostgreSQL then spends ~4.4 s compiling a query that executes in ~50 ms —
|
|
8
|
+
on every page the Schedule fetches, so opening that project took ~41 s. JIT pays off on
|
|
9
|
+
long analytic scans; every query this application issues is short and repeated, so the
|
|
10
|
+
compile cost is never recovered.
|
|
11
|
+
|
|
12
|
+
**Why a ``SET`` on connect rather than ``OPTIONS={"options": "-c jit=off"}``.** A startup
|
|
13
|
+
parameter is rejected outright by PgBouncer in transaction-pooling mode, which the
|
|
14
|
+
sizing guide recommends for large installs — that would turn a performance fix into a
|
|
15
|
+
connection failure. A ``SET`` cannot fail to connect. Behind a transaction pooler it is
|
|
16
|
+
best-effort (the setting lands on whichever server connection served it), so operators
|
|
17
|
+
running one are told to set it server-side as well.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def disable_jit(sender: Any, connection: BaseDatabaseWrapper, **kwargs: Any) -> None:
|
|
28
|
+
"""``connection_created`` receiver: turn PostgreSQL JIT off for this session.
|
|
29
|
+
|
|
30
|
+
Django sets autocommit before sending ``connection_created``, so the ``SET`` is
|
|
31
|
+
session-scoped rather than confined to a transaction that ``ATOMIC_REQUESTS`` or a
|
|
32
|
+
rollback would discard.
|
|
33
|
+
"""
|
|
34
|
+
if connection.vendor != "postgresql":
|
|
35
|
+
return
|
|
36
|
+
with connection.cursor() as cursor:
|
|
37
|
+
cursor.execute("SET jit = off")
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Demo read-only mode: deny-by-default write refusal keyed on the deployment (#3912, #3924).
|
|
2
|
+
|
|
3
|
+
The interactive demo publishes a login. Anything that grants authority through the
|
|
4
|
+
*account* is therefore reachable by the whole internet, and no seeded role can help:
|
|
5
|
+
``POST /projects/`` is open to any authenticated user and mints the creator an
|
|
6
|
+
``OWNER`` membership. So the guarantee cannot be "the demo user's role forbids
|
|
7
|
+
writes" -- it has to be "this deployment refuses writes", independent of who asks.
|
|
8
|
+
|
|
9
|
+
This module is that guarantee, and it is the whole of it: every other decision in the
|
|
10
|
+
ADR is defense in depth over this one.
|
|
11
|
+
|
|
12
|
+
**Why middleware and not a DRF permission class.** A ``DemoReadOnly`` class appended
|
|
13
|
+
to ``DEFAULT_PERMISSION_CLASSES`` silently does nothing here: DRF *replaces* its
|
|
14
|
+
defaults for any view that sets ``permission_classes`` explicitly, and effectively
|
|
15
|
+
every view in this codebase does. Middleware runs ahead of routing and cannot be
|
|
16
|
+
overridden per view. It also inverts the default for code that does not exist yet -- an
|
|
17
|
+
endpoint added next release by someone who has never heard of the demo is refused by
|
|
18
|
+
construction, not permitted until someone notices.
|
|
19
|
+
|
|
20
|
+
**What is and is not covered.** Every unsafe HTTP request under ``/api/`` is refused.
|
|
21
|
+
Not covered, by design: ``/admin/`` (closed by ``DJANGO_ADMIN_ENABLED`` and at the
|
|
22
|
+
edge), and WebSocket traffic, which is an ASGI ``websocket`` scope that never enters
|
|
23
|
+
HTTP middleware (``/ws/`` is closed at the edge by the demo's web tier). A ``GET`` that writes
|
|
24
|
+
as a side effect is also outside a method fence; the middleware refuses methods, not
|
|
25
|
+
side effects.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
|
|
32
|
+
from django.conf import settings
|
|
33
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
34
|
+
from django.http import HttpRequest, HttpResponse, JsonResponse
|
|
35
|
+
|
|
36
|
+
#: The stable machine-readable refusal code, documented in ``api/errors.md``.
|
|
37
|
+
DEMO_READ_ONLY_CODE = "demo_read_only"
|
|
38
|
+
|
|
39
|
+
#: The refusal's human-readable ``detail``. The interactive-demo web tier's nginx
|
|
40
|
+
#: fence (packages/helm/templates/web/configmap.yaml) refuses writes before they reach
|
|
41
|
+
#: Django and must return this exact body, because the web app recognizes a demo
|
|
42
|
+
#: refusal by its body. The chart cannot import this constant, so
|
|
43
|
+
#: tests/core/test_demo_read_only.py asserts the chart's copy equals it.
|
|
44
|
+
DEMO_READ_ONLY_DETAIL = (
|
|
45
|
+
"This is a read-only demo. Your change was not saved. "
|
|
46
|
+
"Nothing in this deployment can be modified."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
#: Methods that cannot change server state by HTTP's own contract. Everything *not*
|
|
50
|
+
#: in this set is treated as a write -- including methods this API never serves
|
|
51
|
+
#: (``TRACE``, ``CONNECT``, a made-up verb) -- so an unrecognized method is refused
|
|
52
|
+
#: rather than waved through. Listing the safe set, not the unsafe one, is what makes
|
|
53
|
+
#: the fence fail closed.
|
|
54
|
+
SAFE_METHODS: frozenset[str] = frozenset({"GET", "HEAD", "OPTIONS"})
|
|
55
|
+
|
|
56
|
+
#: The complete set of writes permitted while the mode is on, as ``(METHOD, path)``.
|
|
57
|
+
#:
|
|
58
|
+
#: THIS IS THE WHOLE GUARANTEE. One careless addition re-opens a write path, so
|
|
59
|
+
#: ``tests/core/test_demo_read_only.py`` pins the exact contents and fails when the
|
|
60
|
+
#: set grows. Every entry must say why it is here. Matched against the resolver's
|
|
61
|
+
#: ``path_info`` exactly: no prefix match, no normalization, so
|
|
62
|
+
#: ``/api/v1/auth/token/x`` and ``/api/v1/auth/token`` (no slash) are both refused.
|
|
63
|
+
ALLOWED_WRITES: frozenset[tuple[str, str]] = frozenset(
|
|
64
|
+
{
|
|
65
|
+
# Login. The visitor has to be able to sign in as the seeded account. Issuing
|
|
66
|
+
# a token is itself a database write (simplejwt's token blacklist records an
|
|
67
|
+
# ``OutstandingToken`` row for every refresh token minted), which is one reason
|
|
68
|
+
# a read-only *database* user was rejected as the mechanism: it would break
|
|
69
|
+
# login.
|
|
70
|
+
("POST", "/api/v1/auth/token/"),
|
|
71
|
+
# Exchanges the httpOnly refresh cookie for a new short-lived access token.
|
|
72
|
+
# Without it the visitor is logged out when the access token expires. Rotation
|
|
73
|
+
# writes (blacklists the old refresh token, records the new one).
|
|
74
|
+
("POST", "/api/v1/auth/token/refresh/"),
|
|
75
|
+
# Clears the refresh cookie and blacklists the caller's own token. It can only
|
|
76
|
+
# revoke the caller's own session, so it carries no data-write value.
|
|
77
|
+
("POST", "/api/v1/auth/logout/"),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
_TRUE_WORDS = frozenset({"true", "1", "yes", "on"})
|
|
82
|
+
_FALSE_WORDS = frozenset({"false", "0", "no", "off", ""})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_demo_read_only(raw: str | None) -> bool:
|
|
86
|
+
"""Parse ``TRUEPPM_DEMO_READ_ONLY`` strictly; refuse to boot on an ambiguous value.
|
|
87
|
+
|
|
88
|
+
``environ.Env.bool`` treats every string outside its truthy list as ``False``, so
|
|
89
|
+
a typo such as ``ture`` would silently *disable* the one control the demo's safety
|
|
90
|
+
rests on. Here an unrecognized value raises instead: an operator who fat-fingers
|
|
91
|
+
the flag gets a process that will not start, not a demo that looks read-only and
|
|
92
|
+
is not. Unset and empty mean off, so a normal install is unaffected.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
raw: The raw environment value, or ``None`` when the variable is unset.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
``True`` when the mode is on, ``False`` when it is off.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ImproperlyConfigured: If ``raw`` is set to anything other than a recognized
|
|
102
|
+
boolean word.
|
|
103
|
+
"""
|
|
104
|
+
if raw is None:
|
|
105
|
+
return False
|
|
106
|
+
word = raw.strip().lower()
|
|
107
|
+
if word in _TRUE_WORDS:
|
|
108
|
+
return True
|
|
109
|
+
if word in _FALSE_WORDS:
|
|
110
|
+
return False
|
|
111
|
+
raise ImproperlyConfigured(
|
|
112
|
+
f"TRUEPPM_DEMO_READ_ONLY={raw!r} is not a recognized boolean. Use true/false "
|
|
113
|
+
"(or 1/0, yes/no, on/off). Refusing to start: a misspelled value would "
|
|
114
|
+
"otherwise leave the demo writable while looking read-only."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def parse_demo_login_hint(raw: str | None) -> dict[str, str] | None:
|
|
119
|
+
"""Parse ``TRUEPPM_DEMO_LOGIN_HINT`` into the credential the login page prints.
|
|
120
|
+
|
|
121
|
+
The interactive demo publishes one shared account, so the login screen has to
|
|
122
|
+
*show* it — a visitor with no credential is a visitor who leaves. The value is a
|
|
123
|
+
``username:password`` pair, and it is deliberately its own variable rather than
|
|
124
|
+
something derived from whatever password the seed job used: nothing should be able
|
|
125
|
+
to turn a real password into published copy by accident, so the operator states the
|
|
126
|
+
published pair explicitly or the panel does not render at all.
|
|
127
|
+
|
|
128
|
+
Splits on the **first** colon only, so a password may contain colons.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
raw: The raw environment value, or ``None`` when the variable is unset.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
``{"username": …, "password": …}``, or ``None`` when unset or empty. Whitespace
|
|
135
|
+
around the **whole** value is stripped before the split, so a trailing newline
|
|
136
|
+
from a heredoc or a ConfigMap block scalar does not become part of the
|
|
137
|
+
password; whitespace *inside* the password is preserved.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ImproperlyConfigured: If ``raw`` is set to something that is not a
|
|
141
|
+
``username:password`` pair. Refusing to boot is the same trade-off
|
|
142
|
+
``parse_demo_read_only`` makes one function up: a demo whose one
|
|
143
|
+
advertised credential is a typo is a demo nobody can sign in to, and an
|
|
144
|
+
operator would rather find that at boot than in a visitor's bug report.
|
|
145
|
+
"""
|
|
146
|
+
if raw is None:
|
|
147
|
+
return None
|
|
148
|
+
value = raw.strip()
|
|
149
|
+
if not value:
|
|
150
|
+
return None
|
|
151
|
+
username, sep, password = value.partition(":")
|
|
152
|
+
if not sep or not username.strip() or not password:
|
|
153
|
+
raise ImproperlyConfigured(
|
|
154
|
+
"TRUEPPM_DEMO_LOGIN_HINT must be 'username:password' with both halves "
|
|
155
|
+
"non-empty. Refusing to start rather than publishing a demo whose only "
|
|
156
|
+
"advertised credential is unusable."
|
|
157
|
+
)
|
|
158
|
+
return {"username": username.strip(), "password": password}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
#: URL schemes the access-gate privacy link may use. The value is rendered as an
|
|
162
|
+
#: ``href`` on a public pre-auth page, so it is held to the same bar as any other
|
|
163
|
+
#: link an unauthenticated visitor is handed -- ``https://`` only. ``javascript:``
|
|
164
|
+
#: and ``data:`` must be unreachable even though the value comes from the operator
|
|
165
|
+
#: rather than a visitor -- a chart value copied between deployments is not a
|
|
166
|
+
#: trusted input -- and ``http://`` is refused too rather than rendering a plaintext
|
|
167
|
+
#: link on an otherwise-TLS page (#3997).
|
|
168
|
+
_ALLOWED_PRIVACY_URL_SCHEMES = ("https://",)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_demo_access_gate(
|
|
172
|
+
raw_provider: str | None, raw_privacy_url: str | None
|
|
173
|
+
) -> dict[str, str | None] | None:
|
|
174
|
+
"""Parse the external access gate an operator declares in front of the demo.
|
|
175
|
+
|
|
176
|
+
A demo host may sit behind a third-party identity gate -- Cloudflare Access,
|
|
177
|
+
Authelia, Authentik, Google IAP -- that makes every visitor hand over an email
|
|
178
|
+
address *before* any TruePPM code runs. ADR-1197 D8 records that such a gate is
|
|
179
|
+
**not** a security control here, and that its email collection is a PII asset the
|
|
180
|
+
project does not otherwise hold. #3969 resolves the disclosure half of that: the
|
|
181
|
+
app says so, in the app, when and only when a gate is actually declared.
|
|
182
|
+
|
|
183
|
+
**This is an operator assertion and nothing verifies it.** The gate lives in a
|
|
184
|
+
third-party dashboard outside the cluster; no request the API can make will
|
|
185
|
+
confirm it is armed, and turning it off in that dashboard leaves this value
|
|
186
|
+
untouched. A stale ``true`` therefore over-discloses (claims a collection that
|
|
187
|
+
stopped) and an unset value under-discloses (a real gate nobody declared). Header
|
|
188
|
+
corroboration (``Cf-Access-Jwt-Assertion``) was considered and rejected in the
|
|
189
|
+
#3969 architect pass: it would be stripped by the chart's own web tier and fail
|
|
190
|
+
*closed*, which is the dangerous direction for a disclosure. Declaring fails open.
|
|
191
|
+
|
|
192
|
+
The provider name is deliberately operator-supplied rather than a constant. A
|
|
193
|
+
self-hoster running this same demo mode behind Authelia must not be made to
|
|
194
|
+
publish a notice naming Cloudflare, which is exactly the false claim #3969's
|
|
195
|
+
scope note forbids.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
raw_provider: ``TRUEPPM_DEMO_ACCESS_GATE_PROVIDER`` -- the gate's display
|
|
199
|
+
name, e.g. ``"Cloudflare Access"``. Unset or empty means no gate is
|
|
200
|
+
declared, which is the default and the normal-install case.
|
|
201
|
+
raw_privacy_url: ``TRUEPPM_DEMO_ACCESS_GATE_PRIVACY_URL`` -- optional link to
|
|
202
|
+
the privacy statement covering what that gate collects.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
``{"provider": …, "privacy_url": … | None}``, or ``None`` when no provider is
|
|
206
|
+
declared. A privacy URL without a provider is ``None`` too: the link is an
|
|
207
|
+
attribute of a declared gate, not a disclosure on its own.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
ImproperlyConfigured: If the privacy URL is not https. Refusing to boot
|
|
211
|
+
follows the same trade-off as the parsers above -- a disclosure carrying a
|
|
212
|
+
dead or hostile link is worse than a deployment that will not start, and
|
|
213
|
+
the operator finds out now rather than from a visitor.
|
|
214
|
+
"""
|
|
215
|
+
provider = (raw_provider or "").strip()
|
|
216
|
+
if not provider:
|
|
217
|
+
return None
|
|
218
|
+
privacy_url = (raw_privacy_url or "").strip()
|
|
219
|
+
if privacy_url and not privacy_url.lower().startswith(_ALLOWED_PRIVACY_URL_SCHEMES):
|
|
220
|
+
raise ImproperlyConfigured(
|
|
221
|
+
f"TRUEPPM_DEMO_ACCESS_GATE_PRIVACY_URL={raw_privacy_url!r} must start with "
|
|
222
|
+
"https://. It is rendered as a link on the pre-auth login page, so any "
|
|
223
|
+
"other scheme -- including plain http:// -- is refused rather than "
|
|
224
|
+
"sanitized at use."
|
|
225
|
+
)
|
|
226
|
+
return {"provider": provider, "privacy_url": privacy_url or None}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def parse_demo_reset_schedule(raw: str | None) -> str | None:
|
|
230
|
+
"""Parse ``TRUEPPM_DEMO_RESET_SCHEDULE`` into the cadence the demo bar states (#4152).
|
|
231
|
+
|
|
232
|
+
The chart renders the CronJob's own ``demo.reset.schedule`` cron expression onto
|
|
233
|
+
this variable — empty when ``demo.reset.enabled`` is false (ADR-1197 D9) — so the
|
|
234
|
+
login page and the shell's demo bar can state the cadence from the one value the
|
|
235
|
+
CronJob is actually driven by, rather than a hardcoded number that silently drifts
|
|
236
|
+
the moment an operator retunes the schedule.
|
|
237
|
+
|
|
238
|
+
Deliberately **not** validated as a cron expression here. Unlike
|
|
239
|
+
:func:`parse_demo_read_only`, a malformed value here is cosmetic, not a safety
|
|
240
|
+
guarantee: the worst case is the web falling back to "resets periodically" (see
|
|
241
|
+
``DemoModeBar``'s daily-cadence parser), never a control that looks armed and is
|
|
242
|
+
not. The CronJob's own schedule field is what Kubernetes validates.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
raw: The raw environment value, or ``None``/empty when unset or when the reset
|
|
246
|
+
is disabled.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
The stripped cron expression, or ``None`` when unset, empty, or whitespace-only.
|
|
250
|
+
"""
|
|
251
|
+
if raw is None:
|
|
252
|
+
return None
|
|
253
|
+
value = raw.strip()
|
|
254
|
+
return value or None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _is_api_path(path_info: str) -> bool:
|
|
258
|
+
"""Whether ``path_info`` addresses the API, ignoring any run of leading slashes.
|
|
259
|
+
|
|
260
|
+
Stripping every leading slash means ``//api/v1/...`` is still treated as the API
|
|
261
|
+
and refused. The resolver would 404 that path today; the point is that the fence
|
|
262
|
+
does not depend on that staying true.
|
|
263
|
+
"""
|
|
264
|
+
return path_info.lstrip("/").startswith("api/")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class DemoReadOnlyMiddleware:
|
|
268
|
+
"""Refuse every unsafe request under ``/api/`` when demo read-only mode is on.
|
|
269
|
+
|
|
270
|
+
Keyed on the **real** HTTP method (``request.method``). Nothing here reads
|
|
271
|
+
``X-HTTP-Method-Override``, a ``_method`` form field, or any other client-supplied
|
|
272
|
+
substitute, and neither does Django or DRF, so a ``POST`` cannot be downgraded to
|
|
273
|
+
a ``GET`` (or the reverse) by a header. ``tests/core/test_demo_read_only.py`` pins
|
|
274
|
+
that.
|
|
275
|
+
|
|
276
|
+
Placed **ahead of session and authentication** (see ``MIDDLEWARE``) so anonymous
|
|
277
|
+
requests are refused too -- password-reset request, the SSO views, the inbound
|
|
278
|
+
integration receivers and the ``AllowAny`` workspace views. The password-reset
|
|
279
|
+
request is the one that matters most: it sends mail to an arbitrary address, so
|
|
280
|
+
refusing it is what stops the demo being used as an email relay. It also runs
|
|
281
|
+
before the view's ``ATOMIC_REQUESTS`` transaction opens, so a refused request
|
|
282
|
+
costs no database work at all.
|
|
283
|
+
|
|
284
|
+
Fail-closed: the flag is read on every request and only an exact ``False`` lets a
|
|
285
|
+
request through, so a non-boolean setting value refuses rather than disables. Off
|
|
286
|
+
by default, and when off the cost is one settings lookup and a return.
|
|
287
|
+
|
|
288
|
+
Deliberately synchronous, like its neighbors in ``MIDDLEWARE``: there is no I/O to
|
|
289
|
+
await, and Django adapts sync middleware in an async stack.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
|
|
293
|
+
self.get_response = get_response
|
|
294
|
+
|
|
295
|
+
def __call__(self, request: HttpRequest) -> HttpResponse:
|
|
296
|
+
if getattr(settings, "DEMO_READ_ONLY", False) is False:
|
|
297
|
+
return self.get_response(request)
|
|
298
|
+
|
|
299
|
+
method = (request.method or "").upper()
|
|
300
|
+
if (
|
|
301
|
+
method not in SAFE_METHODS
|
|
302
|
+
and _is_api_path(request.path_info)
|
|
303
|
+
and (method, request.path_info) not in ALLOWED_WRITES
|
|
304
|
+
):
|
|
305
|
+
# Plain JsonResponse, not a DRF Response: this runs before the view, so
|
|
306
|
+
# there is no exception handler in play and nothing to roll back.
|
|
307
|
+
return JsonResponse(
|
|
308
|
+
{
|
|
309
|
+
"detail": DEMO_READ_ONLY_DETAIL,
|
|
310
|
+
"code": DEMO_READ_ONLY_CODE,
|
|
311
|
+
},
|
|
312
|
+
status=403,
|
|
313
|
+
)
|
|
314
|
+
return self.get_response(request)
|