trueppm-api 0.4.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +14 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +164 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +2837 -0
- trueppm_api/apps/access/serializers.py +946 -0
- trueppm_api/apps/access/services.py +667 -0
- trueppm_api/apps/access/signals.py +103 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +95 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1931 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +108 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +418 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +177 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +837 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +403 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +622 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +519 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +731 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1106 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +277 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +500 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1231 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +436 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +447 -0
- trueppm_api/apps/notifications/email_backend.py +330 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1197 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +961 -0
- trueppm_api/apps/notifications/services.py +1451 -0
- trueppm_api/apps/notifications/tasks.py +711 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +904 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +551 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +969 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +523 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +466 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +449 -0
- trueppm_api/apps/projects/backfill.py +287 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +204 -0
- trueppm_api/apps/projects/config_notice.py +1325 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +93 -0
- trueppm_api/apps/projects/decisions_views.py +94 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +353 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +8946 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +575 -0
- trueppm_api/apps/projects/program_schedule.py +745 -0
- trueppm_api/apps/projects/program_views.py +3220 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/exporter.py +1644 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2390 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +126 -0
- trueppm_api/apps/projects/seed/replay.py +1691 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1149 -0
- trueppm_api/apps/projects/serializers.py +12310 -0
- trueppm_api/apps/projects/services.py +6966 -0
- trueppm_api/apps/projects/share_serializers.py +132 -0
- trueppm_api/apps/projects/share_services.py +325 -0
- trueppm_api/apps/projects/share_views.py +374 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +909 -0
- trueppm_api/apps/projects/signal_privacy_views.py +476 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +266 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1152 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1917 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +258 -0
- trueppm_api/apps/projects/template_views.py +799 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1011 -0
- trueppm_api/apps/projects/utilization.py +885 -0
- trueppm_api/apps/projects/views.py +20572 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1749 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +119 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +516 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +283 -0
- trueppm_api/apps/scheduling/serializers.py +564 -0
- trueppm_api/apps/scheduling/services.py +1182 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +1984 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2564 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +437 -0
- trueppm_api/apps/sso/services.py +1385 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +320 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +471 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +225 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +371 -0
- trueppm_api/apps/sync/serializers.py +687 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +501 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +930 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +434 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +117 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +526 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +144 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +372 -0
- trueppm_api/apps/webhooks/serializers.py +318 -0
- trueppm_api/apps/webhooks/tasks.py +663 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +383 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +368 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +242 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +919 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +555 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1502 -0
- trueppm_api/asgi.py +42 -0
- trueppm_api/celery.py +31 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +916 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +949 -0
- trueppm_api/core/password_reset.py +500 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +884 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +344 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2455 -0
- trueppm_api/settings/dev.py +273 -0
- trueppm_api/settings/prod.py +228 -0
- trueppm_api/urls.py +153 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
- trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
- trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
"""Serializers for the access app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from django.contrib.auth import get_user_model
|
|
9
|
+
from django.db.models import Q, QuerySet
|
|
10
|
+
from drf_spectacular.utils import extend_schema_field, inline_serializer
|
|
11
|
+
from rest_framework import serializers
|
|
12
|
+
|
|
13
|
+
from trueppm_api.apps.access.groups import ALL_AUTO_GROUP_KEYS
|
|
14
|
+
from trueppm_api.apps.access.models import (
|
|
15
|
+
ExternalStakeholder,
|
|
16
|
+
ProgramMembership,
|
|
17
|
+
ProgramUserDefinedMentionGroup,
|
|
18
|
+
ProjectMembership,
|
|
19
|
+
Role,
|
|
20
|
+
UserDefinedMentionGroup,
|
|
21
|
+
program_role_label,
|
|
22
|
+
)
|
|
23
|
+
from trueppm_api.apps.profiles.models import DateFormat, RoleContext
|
|
24
|
+
from trueppm_api.apps.workspace.models import WorkspaceRole
|
|
25
|
+
from trueppm_api.apps.workspace.permissions import (
|
|
26
|
+
_workspace_membership_role,
|
|
27
|
+
workspace_role_for_user,
|
|
28
|
+
)
|
|
29
|
+
from trueppm_api.apps.workspace.serializers import display_name_for
|
|
30
|
+
|
|
31
|
+
User = get_user_model()
|
|
32
|
+
|
|
33
|
+
# A mention group key must be a valid mention token (the name class of
|
|
34
|
+
# notifications.services._MENTION_RE) so it is actually addressable as @name.
|
|
35
|
+
_GROUP_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
36
|
+
|
|
37
|
+
# Refusal copy for an unreachable membership target. Deliberately identical for
|
|
38
|
+
# "no such account" and "an account you may not name" so the field leaks no
|
|
39
|
+
# existence oracle — the same reasoning as ``ProjectSerializer.copy_settings_from``.
|
|
40
|
+
_UNREACHABLE_TARGET_ERROR = (
|
|
41
|
+
"No such user, or not someone you can add. You can add people you already share a "
|
|
42
|
+
"project or program with; ask a workspace admin to invite anyone else."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_USER_IMMUTABLE_ERROR = (
|
|
46
|
+
"A membership's user cannot be changed. Remove this member and add the other account."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_TARGET_HELP_TEXT = (
|
|
50
|
+
"The stock auth.User integer primary key of the account to add. Bounded to accounts "
|
|
51
|
+
"you can already reach: a workspace Admin may name any active account; anyone else "
|
|
52
|
+
"may name themselves or somebody already on a project or program roster they belong "
|
|
53
|
+
"to. Deactivated accounts are never accepted. An id outside that set is refused with "
|
|
54
|
+
"400 and the same message a nonexistent id gets, so this field is not an existence "
|
|
55
|
+
"oracle — use a workspace invite to bring in anyone further out. Accepted on add "
|
|
56
|
+
"only; a membership's user cannot be changed."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def reachable_membership_targets(actor: Any, *, request: Any = None) -> QuerySet[Any]:
|
|
61
|
+
"""Accounts ``actor`` may name as the target of a membership write (#3641).
|
|
62
|
+
|
|
63
|
+
Membership writes take a bare ``auth.User`` integer primary key and answer with
|
|
64
|
+
the target's ``user_detail`` — including their email address. Left unbounded that
|
|
65
|
+
turns a self-minted project into an address-lookup oracle over sequential ids
|
|
66
|
+
(``POST /api/v1/projects/`` is ungated and mints the caller as Owner), which is
|
|
67
|
+
the harvest #815 and #891 were fixed to prevent. So the target set is bounded to
|
|
68
|
+
accounts the caller can *already* read, and a membership grant can therefore never
|
|
69
|
+
disclose an account they could not already see.
|
|
70
|
+
|
|
71
|
+
Two tiers, both mirroring rules the install already enforces elsewhere:
|
|
72
|
+
|
|
73
|
+
* **Workspace ADMIN or above** reaches every active account. That is the branch
|
|
74
|
+
``WorkspaceMemberListView`` already takes for the directory listing, and the
|
|
75
|
+
principal that already owns ``/workspace/invites/`` and
|
|
76
|
+
``/workspace/groups/{id}/members/``. It is not self-grantable: an implicit role
|
|
77
|
+
is MEMBER (superuser bootstrap aside) and an explicit row is written only by an
|
|
78
|
+
existing admin (``workspace_role_for_user``).
|
|
79
|
+
* **Everyone else** reaches themselves plus the people already introduced to a
|
|
80
|
+
project or program roster they belong to — the same "must already be a member"
|
|
81
|
+
constraint ``validate_lead`` and the mention-group ``_member_user_or_400``
|
|
82
|
+
helper apply, with the caller exempt exactly as in
|
|
83
|
+
``seed.importer._resolve_accounts`` (#1057).
|
|
84
|
+
|
|
85
|
+
The caller's *own* membership must be live, but the target's need not be: a revoked
|
|
86
|
+
row is still evidence that somebody with reach introduced that account to this
|
|
87
|
+
roster, and requiring a live one would make re-adding a member you just removed
|
|
88
|
+
impossible — the revive path ``_revive_revoked_membership`` exists for (#3410).
|
|
89
|
+
|
|
90
|
+
So "reachable" means **ever introduced to a roster I am on**, not *currently
|
|
91
|
+
readable there*. The deliberate consequence: somebody revoked from a project before
|
|
92
|
+
the caller joined it is nameable even though the caller never saw them on it. That
|
|
93
|
+
is bounded to accounts already adjacent to the caller's own projects, never the
|
|
94
|
+
install, and it is the price of keeping re-add working. Do not silently narrow this
|
|
95
|
+
to the live set without restoring the re-add path some other way.
|
|
96
|
+
|
|
97
|
+
``is_active=False`` accounts are excluded at every tier, matching ``UserSearchView``:
|
|
98
|
+
the deactivated pool is admin-only state (#1724) and must not be reachable through
|
|
99
|
+
a roster write either.
|
|
100
|
+
|
|
101
|
+
Bringing in somebody outside that set is the invite path
|
|
102
|
+
(``POST /api/v1/workspace/invites/``). It is keyed on an email address the inviter
|
|
103
|
+
must already possess, so it discloses nothing — it is the reason this narrowing does
|
|
104
|
+
not strand onboarding.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
actor: The requesting user. An anonymous or deactivated principal reaches nobody.
|
|
108
|
+
request: The DRF request, when there is one. Resolving the workspace role
|
|
109
|
+
through it populates the per-request cache the permission layer reads, so a
|
|
110
|
+
workspace-scoped permission class added to these viewsets later does not
|
|
111
|
+
silently resolve the same role a second time.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
A ``User`` queryset of the accounts ``actor`` may name.
|
|
115
|
+
"""
|
|
116
|
+
role = (
|
|
117
|
+
_workspace_membership_role(request)
|
|
118
|
+
if request is not None
|
|
119
|
+
else workspace_role_for_user(actor)
|
|
120
|
+
)
|
|
121
|
+
if role is None:
|
|
122
|
+
return User.objects.none()
|
|
123
|
+
|
|
124
|
+
active = User.objects.filter(is_active=True)
|
|
125
|
+
if role >= WorkspaceRole.ADMIN:
|
|
126
|
+
return active
|
|
127
|
+
|
|
128
|
+
# Subqueries rather than a join + ``.distinct()``: a caller on many projects would
|
|
129
|
+
# otherwise fan the join out per shared roster row, and a bare ``.distinct()`` is
|
|
130
|
+
# silently defeated by any model ordering the queryset picks up.
|
|
131
|
+
# The actor's own row must be live and its scope must still exist — a trashed
|
|
132
|
+
# project's roster is unreadable (``_get_project_or_404`` filters it out), so it
|
|
133
|
+
# must not confer reach either. Archived is deliberately NOT excluded: an archived
|
|
134
|
+
# project is read-only, not invisible, and its members still see each other.
|
|
135
|
+
actor_project_ids = ProjectMembership.objects.filter(
|
|
136
|
+
user=actor, is_deleted=False, project__is_deleted=False
|
|
137
|
+
).values("project_id")
|
|
138
|
+
actor_program_ids = ProgramMembership.objects.filter(
|
|
139
|
+
user=actor, is_deleted=False, program__is_deleted=False
|
|
140
|
+
).values("program_id")
|
|
141
|
+
return active.filter(
|
|
142
|
+
Q(pk=actor.pk)
|
|
143
|
+
| Q(
|
|
144
|
+
pk__in=ProjectMembership.objects.filter(project_id__in=actor_project_ids).values(
|
|
145
|
+
"user_id"
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
| Q(
|
|
149
|
+
pk__in=ProgramMembership.objects.filter(program_id__in=actor_program_ids).values(
|
|
150
|
+
"user_id"
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class _ReachableMembershipTargetMixin:
|
|
157
|
+
"""Bounds a membership write serializer's ``user`` field to reachable accounts.
|
|
158
|
+
|
|
159
|
+
``user`` is declared with an **empty** queryset on the concrete serializers, so a
|
|
160
|
+
serializer built without a request context resolves nobody rather than everybody —
|
|
161
|
+
the field is its own IDOR gate, failing closed. Here the real, caller-scoped
|
|
162
|
+
queryset is swapped in (the structural precedent is
|
|
163
|
+
``ProjectSerializer.__init__``'s ``copy_settings_from`` narrowing).
|
|
164
|
+
|
|
165
|
+
On **update** the field is made read-only and a supplied ``user`` is refused
|
|
166
|
+
outright: reassigning an existing row onto another account is the same harvest
|
|
167
|
+
through a second door, and it also silently rewrites who holds access while
|
|
168
|
+
keeping the row's ``joined_at`` access evidence (#3410). Refusing rather than
|
|
169
|
+
ignoring keeps a 200 from meaning two different things to the caller.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
173
|
+
super().__init__(*args, **kwargs)
|
|
174
|
+
field = self.fields.get("user") # type: ignore[attr-defined]
|
|
175
|
+
if not isinstance(field, serializers.PrimaryKeyRelatedField):
|
|
176
|
+
return
|
|
177
|
+
if self.instance is not None: # type: ignore[attr-defined]
|
|
178
|
+
field.read_only = True
|
|
179
|
+
return
|
|
180
|
+
request = self.context.get("request") # type: ignore[attr-defined]
|
|
181
|
+
actor = getattr(request, "user", None)
|
|
182
|
+
if actor is not None and getattr(actor, "is_authenticated", False):
|
|
183
|
+
field.queryset = reachable_membership_targets(actor, request=request)
|
|
184
|
+
|
|
185
|
+
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
|
186
|
+
# ``user`` is read-only on update, so DRF drops it silently. Surface the
|
|
187
|
+
# refusal instead — a caller that asked to reassign the row must not read a
|
|
188
|
+
# 200 as "done". Checked against ``initial_data`` because the field never
|
|
189
|
+
# reaches ``attrs``; the body may not be a dict, so do not assume ``in`` works.
|
|
190
|
+
if self.instance is not None: # type: ignore[attr-defined]
|
|
191
|
+
raw = getattr(self, "initial_data", None)
|
|
192
|
+
if isinstance(raw, dict) and "user" in raw:
|
|
193
|
+
raise serializers.ValidationError({"user": _USER_IMMUTABLE_ERROR})
|
|
194
|
+
return super().validate(attrs) # type: ignore[misc,no-any-return]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class _UserSummarySerializer(serializers.ModelSerializer): # type: ignore[type-arg]
|
|
198
|
+
class Meta:
|
|
199
|
+
model = User
|
|
200
|
+
fields = ["id", "username", "email"]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ProjectMembershipReadSerializer(serializers.ModelSerializer[ProjectMembership]):
|
|
204
|
+
"""Response serializer — includes user_detail and role_label for list/retrieve.
|
|
205
|
+
|
|
206
|
+
role — integer ordinal (canonical wire format; use for comparisons)
|
|
207
|
+
role_label — human-readable label e.g. "Project Manager" (display only)
|
|
208
|
+
joined_at — when this membership row was first created. Per-project access
|
|
209
|
+
evidence, but NOT proof of uninterrupted access: re-adding a
|
|
210
|
+
previously removed member revives their original row and keeps
|
|
211
|
+
this date, so the span may contain one or more revoked intervals
|
|
212
|
+
(#3410). Use reinstated_at as the discriminator — it is null iff
|
|
213
|
+
this membership has never lapsed (#3436).
|
|
214
|
+
role_changed_at — when the role last changed, or null if unchanged since joining
|
|
215
|
+
reinstated_at — when this membership was last revived after a revoke, or null if
|
|
216
|
+
it has never been revoked and re-added. Stamped on every revive
|
|
217
|
+
(advances on a second revive rather than accumulating history) —
|
|
218
|
+
the durable server-side fact that lets a caller, human or machine,
|
|
219
|
+
tell a never-lapsed membership apart from a revived one (#3436).
|
|
220
|
+
other_active_project_count — how many OTHER active (non-archived, non-deleted) projects this
|
|
221
|
+
user belongs to, excluding the current one. A resource-load
|
|
222
|
+
signal for the assigner (#598). The full count is shown; it is a
|
|
223
|
+
number only and leaks no project identities.
|
|
224
|
+
other_active_project_names — names of those other projects, but ONLY the ones the *requesting*
|
|
225
|
+
user is OWNER of (visibility gate — never reveal the name of a
|
|
226
|
+
project the requester cannot already see). Empty for non-OWNERs.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
user_detail = _UserSummarySerializer(source="user", read_only=True)
|
|
230
|
+
role_label = serializers.SerializerMethodField()
|
|
231
|
+
other_active_project_count = serializers.SerializerMethodField()
|
|
232
|
+
other_active_project_names = serializers.SerializerMethodField()
|
|
233
|
+
|
|
234
|
+
def get_role_label(self, obj: ProjectMembership) -> str:
|
|
235
|
+
"""Display name for ``role`` in **project** vocabulary.
|
|
236
|
+
|
|
237
|
+
Ordinal 400 reads "Project Admin" and 300 reads "Project Manager". The
|
|
238
|
+
identically-named field on a *program* membership deliberately names the
|
|
239
|
+
same two ordinals "Program Admin" and "Program Manager" — see
|
|
240
|
+
``ProgramMembershipReadSerializer.get_role_label``. One ordinal, two
|
|
241
|
+
containers, two names.
|
|
242
|
+
"""
|
|
243
|
+
return Role(obj.role).label
|
|
244
|
+
|
|
245
|
+
def get_other_active_project_count(self, obj: ProjectMembership) -> int:
|
|
246
|
+
# list/retrieve annotate this on the queryset (one Subquery, no N+1). create/
|
|
247
|
+
# partial_update serialize a fresh, un-annotated instance — fall back to a
|
|
248
|
+
# single count query there (rare, one row).
|
|
249
|
+
annotated = getattr(obj, "other_active_count", None)
|
|
250
|
+
if annotated is not None:
|
|
251
|
+
return int(annotated)
|
|
252
|
+
return (
|
|
253
|
+
ProjectMembership.objects.filter(
|
|
254
|
+
user_id=obj.user_id,
|
|
255
|
+
is_deleted=False,
|
|
256
|
+
project__is_deleted=False,
|
|
257
|
+
project__is_archived=False,
|
|
258
|
+
)
|
|
259
|
+
.exclude(project_id=obj.project_id)
|
|
260
|
+
.values("project_id")
|
|
261
|
+
.distinct()
|
|
262
|
+
.count()
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def get_other_active_project_names(self, obj: ProjectMembership) -> list[str]:
|
|
266
|
+
# Names are visibility-gated and prebuilt once per request by the viewset
|
|
267
|
+
# (see ProjectMembershipViewSet._build_other_project_names_map). When the map
|
|
268
|
+
# is absent (create/partial_update responses) return [] — the client re-fetches
|
|
269
|
+
# the list, which carries the gated names.
|
|
270
|
+
names_map: dict[Any, list[str]] = self.context.get("other_project_names_map") or {}
|
|
271
|
+
return names_map.get(obj.user_id, [])
|
|
272
|
+
|
|
273
|
+
class Meta:
|
|
274
|
+
model = ProjectMembership
|
|
275
|
+
fields = [
|
|
276
|
+
"id",
|
|
277
|
+
"server_version",
|
|
278
|
+
"project",
|
|
279
|
+
"user",
|
|
280
|
+
"user_detail",
|
|
281
|
+
"role",
|
|
282
|
+
"role_label",
|
|
283
|
+
"joined_at",
|
|
284
|
+
"role_changed_at",
|
|
285
|
+
"reinstated_at",
|
|
286
|
+
"other_active_project_count",
|
|
287
|
+
"other_active_project_names",
|
|
288
|
+
]
|
|
289
|
+
read_only_fields = [
|
|
290
|
+
"id",
|
|
291
|
+
"server_version",
|
|
292
|
+
"project",
|
|
293
|
+
"user",
|
|
294
|
+
"user_detail",
|
|
295
|
+
"role_label",
|
|
296
|
+
"joined_at",
|
|
297
|
+
"role_changed_at",
|
|
298
|
+
"reinstated_at",
|
|
299
|
+
"other_active_project_count",
|
|
300
|
+
"other_active_project_names",
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class ProjectMembershipWriteSerializer(
|
|
305
|
+
_ReachableMembershipTargetMixin, serializers.ModelSerializer[ProjectMembership]
|
|
306
|
+
):
|
|
307
|
+
"""Write serializer — accepts user and role; project is injected from URL.
|
|
308
|
+
|
|
309
|
+
``user`` is the stock ``auth.User`` **integer** primary key (TruePPM has no custom
|
|
310
|
+
user model), and it is bounded to :func:`reachable_membership_targets` — see that
|
|
311
|
+
function for who a caller may name and why. It is accepted on add only; a
|
|
312
|
+
``partial_update`` that carries it is refused (#3641).
|
|
313
|
+
|
|
314
|
+
``role`` is optional (ADR-0363, #157): when omitted on add, the viewset falls
|
|
315
|
+
back to the project's ``default_member_role``. It remains required for a role
|
|
316
|
+
*change* (``partial_update`` always sends it), and the model column stays
|
|
317
|
+
non-null — the fallback is resolved before ``save``.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
# Empty by construction — the mixin swaps in the caller-scoped queryset. Without a
|
|
321
|
+
# request context the field resolves nobody, so it fails closed.
|
|
322
|
+
user = serializers.PrimaryKeyRelatedField(
|
|
323
|
+
queryset=User.objects.none(),
|
|
324
|
+
error_messages={"does_not_exist": _UNREACHABLE_TARGET_ERROR},
|
|
325
|
+
help_text=_TARGET_HELP_TEXT,
|
|
326
|
+
)
|
|
327
|
+
role = serializers.IntegerField(
|
|
328
|
+
required=False,
|
|
329
|
+
help_text=(
|
|
330
|
+
"Optional on add — defaults to the project's default_member_role "
|
|
331
|
+
"(ADR-0363). Required to change an existing member's role."
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
class Meta:
|
|
336
|
+
model = ProjectMembership
|
|
337
|
+
fields = ["user", "role"]
|
|
338
|
+
|
|
339
|
+
def validate_role(self, value: int) -> int:
|
|
340
|
+
# Role must be a valid Role ordinal.
|
|
341
|
+
valid = {r.value for r in Role}
|
|
342
|
+
if value not in valid:
|
|
343
|
+
raise serializers.ValidationError(f"Invalid role. Choose from {sorted(valid)}.")
|
|
344
|
+
return value
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class ProjectMembershipUpdateSerializer(ProjectMembershipWriteSerializer):
|
|
348
|
+
"""PATCH body — ``role`` only.
|
|
349
|
+
|
|
350
|
+
A separate class rather than ``read_only_fields`` on the add serializer so the
|
|
351
|
+
published schema tells the truth: ``user`` is genuinely not part of an update
|
|
352
|
+
body, and an integrator reading ``PatchedProjectMembershipWriteRequest`` would
|
|
353
|
+
otherwise still see it advertised as writable. Sending it anyway is refused by
|
|
354
|
+
the inherited ``validate`` rather than silently dropped (#3641).
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
class Meta(ProjectMembershipWriteSerializer.Meta):
|
|
358
|
+
fields = ["role"]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class ProgramMembershipReadSerializer(serializers.ModelSerializer[ProgramMembership]):
|
|
362
|
+
"""Response serializer for ProgramMembership — mirrors the project version.
|
|
363
|
+
|
|
364
|
+
``joined_at`` carries the same caveat as its project twin: it is the date the row
|
|
365
|
+
was first created, and a revoked member who is later re-added keeps it, so the
|
|
366
|
+
span may contain revoked intervals (#3410). It is not proof of uninterrupted
|
|
367
|
+
access — ``reinstated_at`` is the discriminator (null iff never revoked and
|
|
368
|
+
re-added, #3436).
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
user_detail = _UserSummarySerializer(source="user", read_only=True)
|
|
372
|
+
# ``role_label`` is the *access* role's display name (Owner/Admin/…), a
|
|
373
|
+
# computed mirror of the ``role`` ordinal — NOT the freeform functional title.
|
|
374
|
+
# The freeform PO/PM/Tech-Lead label is the distinct ``role_title`` field
|
|
375
|
+
# below (#565); the two never collide.
|
|
376
|
+
role_label = serializers.SerializerMethodField()
|
|
377
|
+
|
|
378
|
+
def get_role_label(self, obj: ProgramMembership) -> str:
|
|
379
|
+
"""Display name for ``role`` in **program** vocabulary.
|
|
380
|
+
|
|
381
|
+
Ordinal 400 reads "Program Admin" and 300 reads "Program Manager" — the
|
|
382
|
+
same names ``Program.my_role_label`` uses, from the same map. An ordinal
|
|
383
|
+
outside the five OSS roles has no name and reads ``"Role <ordinal>"``.
|
|
384
|
+
"""
|
|
385
|
+
# Why this is not ``Role.label``: the enum's labels are project-scoped, so
|
|
386
|
+
# serializing through them called a *program* membership "Project Admin"
|
|
387
|
+
# here while the program card called the same membership "Program Admin"
|
|
388
|
+
# — one fact, two answers, and only TruePPM's own web client knew which to
|
|
389
|
+
# believe (#3503). ``program_role_label`` is the one definition of that
|
|
390
|
+
# vocabulary and the program card calls it too.
|
|
391
|
+
#
|
|
392
|
+
# The fallback covers an Enterprise custom-band ordinal (ADR-0072), which
|
|
393
|
+
# the OSS edition cannot name. The field is a non-null string in the
|
|
394
|
+
# published schema and the ordinal is already on the wire beside it in
|
|
395
|
+
# ``role``, so echoing it degrades rather than borrowing a neighbouring
|
|
396
|
+
# role's name — and rather than the ``ValueError`` this used to raise,
|
|
397
|
+
# which DRF does not convert and which 500'd the whole response.
|
|
398
|
+
return program_role_label(obj.role) or f"Role {obj.role}"
|
|
399
|
+
|
|
400
|
+
class Meta:
|
|
401
|
+
model = ProgramMembership
|
|
402
|
+
fields = [
|
|
403
|
+
"id",
|
|
404
|
+
"server_version",
|
|
405
|
+
"program",
|
|
406
|
+
"user",
|
|
407
|
+
"user_detail",
|
|
408
|
+
"role",
|
|
409
|
+
"role_label",
|
|
410
|
+
"role_title",
|
|
411
|
+
"joined_at",
|
|
412
|
+
"role_changed_at",
|
|
413
|
+
"reinstated_at",
|
|
414
|
+
]
|
|
415
|
+
read_only_fields = [
|
|
416
|
+
"id",
|
|
417
|
+
"server_version",
|
|
418
|
+
"program",
|
|
419
|
+
"user",
|
|
420
|
+
"user_detail",
|
|
421
|
+
"role_label",
|
|
422
|
+
"joined_at",
|
|
423
|
+
"role_changed_at",
|
|
424
|
+
"reinstated_at",
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class ProgramMembershipWriteSerializer(
|
|
429
|
+
_ReachableMembershipTargetMixin, serializers.ModelSerializer[ProgramMembership]
|
|
430
|
+
):
|
|
431
|
+
"""Write serializer — accepts user, role, and the freeform role_title.
|
|
432
|
+
|
|
433
|
+
``user`` is the stock ``auth.User`` **integer** primary key, bounded to
|
|
434
|
+
:func:`reachable_membership_targets` on add and refused entirely on
|
|
435
|
+
``partial_update`` (#3641) — the project twin carries the same constraint for the
|
|
436
|
+
same reason.
|
|
437
|
+
|
|
438
|
+
``program`` is injected from the URL. ``role_title`` (#565) is optional; the
|
|
439
|
+
view gates *who* may set it (a role change stays Owner-only, while a
|
|
440
|
+
role_title-only PATCH is allowed at Admin+).
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
# Empty by construction — see the project twin.
|
|
444
|
+
user = serializers.PrimaryKeyRelatedField(
|
|
445
|
+
queryset=User.objects.none(),
|
|
446
|
+
error_messages={"does_not_exist": _UNREACHABLE_TARGET_ERROR},
|
|
447
|
+
help_text=_TARGET_HELP_TEXT,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
class Meta:
|
|
451
|
+
model = ProgramMembership
|
|
452
|
+
fields = ["user", "role", "role_title"]
|
|
453
|
+
|
|
454
|
+
def validate_role(self, value: int) -> int:
|
|
455
|
+
valid = {r.value for r in Role}
|
|
456
|
+
if value not in valid:
|
|
457
|
+
raise serializers.ValidationError(f"Invalid role. Choose from {sorted(valid)}.")
|
|
458
|
+
return value
|
|
459
|
+
|
|
460
|
+
def validate_role_title(self, value: str | None) -> str:
|
|
461
|
+
# Collapse whitespace-only / empty submissions to "" so "unset" is a single
|
|
462
|
+
# canonical state (empty string, never NULL — per the model's DJ001 default).
|
|
463
|
+
return (value or "").strip()
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class ProgramMembershipUpdateSerializer(ProgramMembershipWriteSerializer):
|
|
467
|
+
"""PATCH body — ``role`` and ``role_title`` only; see the project twin for why."""
|
|
468
|
+
|
|
469
|
+
class Meta(ProgramMembershipWriteSerializer.Meta):
|
|
470
|
+
fields = ["role", "role_title"]
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class UserDefinedMentionGroupReadSerializer(serializers.ModelSerializer[UserDefinedMentionGroup]):
|
|
474
|
+
"""Response serializer for a user-defined @mention group (ADR-0212, #515).
|
|
475
|
+
|
|
476
|
+
``members`` is the curated member set (summary detail); ``member_count`` is a
|
|
477
|
+
convenience for the list UI. ``muted_by_me`` reflects whether the *requesting*
|
|
478
|
+
user has muted this group — the per-user override that suppresses its
|
|
479
|
+
mentions for them.
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
members = _UserSummarySerializer(many=True, read_only=True)
|
|
483
|
+
member_count = serializers.SerializerMethodField()
|
|
484
|
+
muted_by_me = serializers.SerializerMethodField()
|
|
485
|
+
|
|
486
|
+
def get_member_count(self, obj: UserDefinedMentionGroup) -> int:
|
|
487
|
+
# `members` is already prefetched by the viewset (it also backs the
|
|
488
|
+
# ``members`` field), so counting the loaded rows in Python is free. A
|
|
489
|
+
# DB-side ``.count()`` here would be a wasted round trip, not a saving.
|
|
490
|
+
members = obj.members.all()
|
|
491
|
+
return len(members)
|
|
492
|
+
|
|
493
|
+
def get_muted_by_me(self, obj: UserDefinedMentionGroup) -> bool:
|
|
494
|
+
user = getattr(self.context.get("request"), "user", None)
|
|
495
|
+
if user is None or not getattr(user, "is_authenticated", False):
|
|
496
|
+
return False
|
|
497
|
+
# muted_by is prefetched by the viewset.
|
|
498
|
+
return any(m.pk == user.pk for m in obj.muted_by.all())
|
|
499
|
+
|
|
500
|
+
class Meta:
|
|
501
|
+
model = UserDefinedMentionGroup
|
|
502
|
+
fields = [
|
|
503
|
+
"id",
|
|
504
|
+
"server_version",
|
|
505
|
+
"project",
|
|
506
|
+
"name",
|
|
507
|
+
"description",
|
|
508
|
+
"email_default_on",
|
|
509
|
+
"members",
|
|
510
|
+
"member_count",
|
|
511
|
+
"muted_by_me",
|
|
512
|
+
]
|
|
513
|
+
read_only_fields = fields
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _validated_mention_group_name(value: str) -> str:
|
|
517
|
+
"""Normalize and validate a @mention group name, shared by the project- and
|
|
518
|
+
program-scoped write serializers (#3903) — everything except the per-scope
|
|
519
|
+
uniqueness check, which needs each serializer's own queryset/context.
|
|
520
|
+
"""
|
|
521
|
+
# Accept a leading @ from the client for convenience; store without it.
|
|
522
|
+
name = value.strip().lstrip("@").strip()
|
|
523
|
+
if not name:
|
|
524
|
+
raise serializers.ValidationError("Group name cannot be empty.")
|
|
525
|
+
if len(name) > 32:
|
|
526
|
+
raise serializers.ValidationError("Group name must be 32 characters or fewer.")
|
|
527
|
+
if not _GROUP_NAME_RE.match(name):
|
|
528
|
+
raise serializers.ValidationError(
|
|
529
|
+
"Group name may only contain letters, digits, and the characters . _ -"
|
|
530
|
+
)
|
|
531
|
+
# An auto-group name (@admins, @scrum-team, @program-pms, …) must never
|
|
532
|
+
# be shadowed — project- and program-scoped keys alike.
|
|
533
|
+
if name.lower() in ALL_AUTO_GROUP_KEYS:
|
|
534
|
+
raise serializers.ValidationError(
|
|
535
|
+
f"'@{name}' is a reserved automatic group and cannot be used."
|
|
536
|
+
)
|
|
537
|
+
return name
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class UserDefinedMentionGroupWriteSerializer(serializers.ModelSerializer[UserDefinedMentionGroup]):
|
|
541
|
+
"""Write serializer for create/rename/edit — ``project`` is injected from URL.
|
|
542
|
+
|
|
543
|
+
Membership and mute are managed through dedicated viewset actions (different
|
|
544
|
+
RBAC), so this serializer covers only the group's own attributes.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
class Meta:
|
|
548
|
+
model = UserDefinedMentionGroup
|
|
549
|
+
fields = ["name", "description", "email_default_on"]
|
|
550
|
+
|
|
551
|
+
def validate_name(self, value: str) -> str:
|
|
552
|
+
name = _validated_mention_group_name(value)
|
|
553
|
+
# Case-insensitive project-uniqueness (the DB constraint is the backstop;
|
|
554
|
+
# this returns a friendly field error instead of a 500 on the race loser).
|
|
555
|
+
project_id = str(self.context.get("project_id"))
|
|
556
|
+
qs = UserDefinedMentionGroup.objects.filter(
|
|
557
|
+
project_id=project_id, name__iexact=name, is_deleted=False
|
|
558
|
+
)
|
|
559
|
+
if self.instance is not None:
|
|
560
|
+
qs = qs.exclude(pk=self.instance.pk)
|
|
561
|
+
if qs.exists():
|
|
562
|
+
raise serializers.ValidationError(
|
|
563
|
+
f"A group named '@{name}' already exists in this project."
|
|
564
|
+
)
|
|
565
|
+
return name
|
|
566
|
+
|
|
567
|
+
def validate_description(self, value: str | None) -> str:
|
|
568
|
+
return (value or "").strip()
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
class ProgramUserDefinedMentionGroupReadSerializer(
|
|
572
|
+
serializers.ModelSerializer[ProgramUserDefinedMentionGroup]
|
|
573
|
+
):
|
|
574
|
+
"""Response serializer for a program-scoped @mention group (ADR-0248, #516).
|
|
575
|
+
|
|
576
|
+
The program-scoped mirror of :class:`UserDefinedMentionGroupReadSerializer`.
|
|
577
|
+
``members`` is the curated member set (drawn from across the program's
|
|
578
|
+
projects); ``member_count`` is a list-UI convenience; ``muted_by_me`` reflects
|
|
579
|
+
whether the *requesting* user has muted this group.
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
members = _UserSummarySerializer(many=True, read_only=True)
|
|
583
|
+
member_count = serializers.SerializerMethodField()
|
|
584
|
+
muted_by_me = serializers.SerializerMethodField()
|
|
585
|
+
|
|
586
|
+
def get_member_count(self, obj: ProgramUserDefinedMentionGroup) -> int:
|
|
587
|
+
# members is prefetched by the viewset (it also backs the members field),
|
|
588
|
+
# so counting the loaded rows in Python is free (no extra round trip).
|
|
589
|
+
members = obj.members.all()
|
|
590
|
+
return len(members)
|
|
591
|
+
|
|
592
|
+
def get_muted_by_me(self, obj: ProgramUserDefinedMentionGroup) -> bool:
|
|
593
|
+
user = getattr(self.context.get("request"), "user", None)
|
|
594
|
+
if user is None or not getattr(user, "is_authenticated", False):
|
|
595
|
+
return False
|
|
596
|
+
# muted_by is prefetched by the viewset.
|
|
597
|
+
return any(m.pk == user.pk for m in obj.muted_by.all())
|
|
598
|
+
|
|
599
|
+
class Meta:
|
|
600
|
+
model = ProgramUserDefinedMentionGroup
|
|
601
|
+
fields = [
|
|
602
|
+
"id",
|
|
603
|
+
"server_version",
|
|
604
|
+
"program",
|
|
605
|
+
"name",
|
|
606
|
+
"description",
|
|
607
|
+
"email_default_on",
|
|
608
|
+
"members",
|
|
609
|
+
"member_count",
|
|
610
|
+
"muted_by_me",
|
|
611
|
+
]
|
|
612
|
+
read_only_fields = fields
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class ProgramUserDefinedMentionGroupWriteSerializer(
|
|
616
|
+
serializers.ModelSerializer[ProgramUserDefinedMentionGroup]
|
|
617
|
+
):
|
|
618
|
+
"""Write serializer for program-group create/rename/edit — ``program`` from URL.
|
|
619
|
+
|
|
620
|
+
Membership and mute are managed through dedicated viewset actions (different
|
|
621
|
+
RBAC), so this serializer covers only the group's own attributes.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
class Meta:
|
|
625
|
+
model = ProgramUserDefinedMentionGroup
|
|
626
|
+
fields = ["name", "description", "email_default_on"]
|
|
627
|
+
|
|
628
|
+
def validate_name(self, value: str) -> str:
|
|
629
|
+
name = _validated_mention_group_name(value)
|
|
630
|
+
# Case-insensitive program-uniqueness (the DB constraint is the backstop;
|
|
631
|
+
# this returns a friendly field error instead of a 500 on the race loser).
|
|
632
|
+
program_id = str(self.context.get("program_id"))
|
|
633
|
+
qs = ProgramUserDefinedMentionGroup.objects.filter(
|
|
634
|
+
program_id=program_id, name__iexact=name, is_deleted=False
|
|
635
|
+
)
|
|
636
|
+
if self.instance is not None:
|
|
637
|
+
qs = qs.exclude(pk=self.instance.pk)
|
|
638
|
+
if qs.exists():
|
|
639
|
+
raise serializers.ValidationError(
|
|
640
|
+
f"A group named '@{name}' already exists in this program."
|
|
641
|
+
)
|
|
642
|
+
return name
|
|
643
|
+
|
|
644
|
+
def validate_description(self, value: str | None) -> str:
|
|
645
|
+
return (value or "").strip()
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class ExternalStakeholderSerializer(serializers.ModelSerializer[ExternalStakeholder]):
|
|
649
|
+
"""CRUD serializer for a program's external stakeholder registry (#1658, ADR-0264).
|
|
650
|
+
|
|
651
|
+
``created_by`` is echoed as the adder's display name (never the raw user row);
|
|
652
|
+
``program`` comes from the URL and is never accepted from the body (IDOR-safe —
|
|
653
|
+
the viewset scopes and stamps it). Case-insensitive per-program email uniqueness
|
|
654
|
+
is validated here so the client gets a friendly field error instead of the DB
|
|
655
|
+
constraint's 500 on the race loser.
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
created_by = serializers.SerializerMethodField()
|
|
659
|
+
|
|
660
|
+
class Meta:
|
|
661
|
+
model = ExternalStakeholder
|
|
662
|
+
fields = ["id", "name", "email", "note", "created_by", "created_at", "updated_at"]
|
|
663
|
+
read_only_fields = ["id", "created_by", "created_at", "updated_at"]
|
|
664
|
+
|
|
665
|
+
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
666
|
+
def get_created_by(self, obj: ExternalStakeholder) -> str | None:
|
|
667
|
+
user = obj.created_by
|
|
668
|
+
if user is None:
|
|
669
|
+
return None
|
|
670
|
+
return display_name_for(user.first_name, user.last_name, user.username)
|
|
671
|
+
|
|
672
|
+
def validate_name(self, value: str) -> str:
|
|
673
|
+
name = value.strip()
|
|
674
|
+
if not name:
|
|
675
|
+
raise serializers.ValidationError("Name cannot be empty.")
|
|
676
|
+
return name
|
|
677
|
+
|
|
678
|
+
def validate_note(self, value: str | None) -> str:
|
|
679
|
+
return (value or "").strip()
|
|
680
|
+
|
|
681
|
+
def validate_email(self, value: str) -> str:
|
|
682
|
+
email = value.strip()
|
|
683
|
+
# Case-insensitive per-program uniqueness across LIVE rows (the DB
|
|
684
|
+
# constraint is the backstop; this returns a friendly 400 on the race loser
|
|
685
|
+
# and on the common re-add-of-existing case).
|
|
686
|
+
program_id = str(self.context.get("program_id"))
|
|
687
|
+
qs = ExternalStakeholder.objects.filter(
|
|
688
|
+
program_id=program_id, email__iexact=email, is_deleted=False
|
|
689
|
+
)
|
|
690
|
+
if self.instance is not None:
|
|
691
|
+
qs = qs.exclude(pk=self.instance.pk)
|
|
692
|
+
if qs.exists():
|
|
693
|
+
raise serializers.ValidationError(
|
|
694
|
+
"A stakeholder with this email already exists in this program."
|
|
695
|
+
)
|
|
696
|
+
return email
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _user_display_name(obj: Any) -> str:
|
|
700
|
+
"""First + last name, falling back to ``username`` when both name fields
|
|
701
|
+
are blank — shared by every serializer that surfaces a user's display
|
|
702
|
+
name (#3903)."""
|
|
703
|
+
name = f"{obj.first_name} {obj.last_name}".strip()
|
|
704
|
+
return name if name else obj.username
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _user_initials(obj: Any) -> str:
|
|
708
|
+
"""Up to two initials from first/last name, falling back to the first two
|
|
709
|
+
characters of ``username`` when both name fields are blank (#3903)."""
|
|
710
|
+
parts: list[str] = []
|
|
711
|
+
if obj.first_name:
|
|
712
|
+
parts.append(obj.first_name[0].upper())
|
|
713
|
+
if obj.last_name:
|
|
714
|
+
parts.append(obj.last_name[0].upper())
|
|
715
|
+
if parts:
|
|
716
|
+
return "".join(parts[:2])
|
|
717
|
+
return str(obj.username[:2].upper())
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
class UserSearchResultSerializer(serializers.Serializer[Any]):
|
|
721
|
+
"""Read-only serializer for GET /api/v1/users/search/ results (ADR-0061).
|
|
722
|
+
|
|
723
|
+
Deliberately omits ``email`` (#815): returning it let any authenticated caller
|
|
724
|
+
paginate the typeahead to harvest every user's email. Identity for the invite
|
|
725
|
+
typeahead is carried by username + display_name + initials; the endpoint still
|
|
726
|
+
*matches* on email so invite-by-email works, but never echoes the value back.
|
|
727
|
+
"""
|
|
728
|
+
|
|
729
|
+
id = serializers.CharField()
|
|
730
|
+
username = serializers.CharField()
|
|
731
|
+
display_name = serializers.SerializerMethodField()
|
|
732
|
+
initials = serializers.SerializerMethodField()
|
|
733
|
+
|
|
734
|
+
def get_display_name(self, obj: Any) -> str:
|
|
735
|
+
return _user_display_name(obj)
|
|
736
|
+
|
|
737
|
+
def get_initials(self, obj: Any) -> str:
|
|
738
|
+
return _user_initials(obj)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class TokenPostureSerializer(serializers.Serializer[Any]):
|
|
742
|
+
"""Nested shape of ``MeSerializer.token`` — schema-only, never instantiated
|
|
743
|
+
directly (``MeSerializer.get_token`` returns a plain dict; this class only gives
|
|
744
|
+
``extend_schema_field`` something faithful to point at, per #2938).
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
scopes = serializers.ListField(
|
|
748
|
+
child=serializers.CharField(), help_text="Scopes this token carries, e.g. ['mcp:read']."
|
|
749
|
+
)
|
|
750
|
+
is_agent = serializers.BooleanField(
|
|
751
|
+
help_text="Whether trueppm_api.apps.projects.models.is_agent_token() treats this "
|
|
752
|
+
"token as MCP/agent traffic — false for a legacy:full personal access token, "
|
|
753
|
+
"which the operator kill switch and team opt-out do not govern."
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class MeSerializer(serializers.Serializer[Any]):
|
|
758
|
+
"""Read-only serializer for GET /api/v1/auth/me/."""
|
|
759
|
+
|
|
760
|
+
id = serializers.UUIDField()
|
|
761
|
+
username = serializers.CharField()
|
|
762
|
+
display_name = serializers.SerializerMethodField()
|
|
763
|
+
initials = serializers.SerializerMethodField()
|
|
764
|
+
email = serializers.EmailField()
|
|
765
|
+
# Contributor-tier role signal (#855/#856). The web client gates the admin
|
|
766
|
+
# settings nav and the "Signal-only" notification default on this, instead of
|
|
767
|
+
# re-deriving "am I an admin anywhere" by fanning out per-project membership
|
|
768
|
+
# calls. API-first: the tier verdict is a server fact, MCP-reachable.
|
|
769
|
+
# - max_project_role: highest project Role ordinal across the user's
|
|
770
|
+
# memberships (null if they belong to no projects).
|
|
771
|
+
# - workspace_role: the user's *effective* WorkspaceRole ordinal — an
|
|
772
|
+
# explicit membership if present, else the implicit role every
|
|
773
|
+
# authenticated user holds (OWNER for a Django superuser bootstrapping a
|
|
774
|
+
# fresh install, else MEMBER); null only for a deactivated membership.
|
|
775
|
+
# Resolved by workspace.permissions.workspace_role_for_user so this signal
|
|
776
|
+
# can never drift from what workspace RBAC actually enforces.
|
|
777
|
+
# - can_access_admin_settings: true iff Admin+ in any project OR Admin+ at
|
|
778
|
+
# the workspace (the implicit superuser OWNER counts) — the single boolean
|
|
779
|
+
# the settings shell gates on.
|
|
780
|
+
max_project_role = serializers.SerializerMethodField()
|
|
781
|
+
workspace_role = serializers.SerializerMethodField()
|
|
782
|
+
can_access_admin_settings = serializers.SerializerMethodField()
|
|
783
|
+
# Role-based app front door (ADR-0129). The web router reads these and
|
|
784
|
+
# navigates — it holds no role→surface policy itself. API-first: the
|
|
785
|
+
# destination is a server fact, identical for web, mobile, and MCP clients.
|
|
786
|
+
# - default_landing: the user's stored preference ("auto" if unset).
|
|
787
|
+
# - landing: {intent, path, resolved_by} — the resolved front door.
|
|
788
|
+
default_landing = serializers.SerializerMethodField()
|
|
789
|
+
landing = serializers.SerializerMethodField()
|
|
790
|
+
# Per-user nav visibility (ADR-0139). The web shell reads this to hide the
|
|
791
|
+
# view tabs the user opted out of; it is a global per-user list, layered on
|
|
792
|
+
# top of the per-project methodology preset client-side. API-first: the
|
|
793
|
+
# hidden set is a server fact, identical for web, mobile, and MCP clients.
|
|
794
|
+
hidden_views = serializers.SerializerMethodField()
|
|
795
|
+
# Active role-context "lens" (#412, ADR-0162). A presentation-only preference
|
|
796
|
+
# the web shell reads to pick a dual-hat user's default project view and the
|
|
797
|
+
# view-tab emphasis ("pm" / "scrum_master" / "unified"; "unified" if unset).
|
|
798
|
+
# It NEVER gates access — RBAC remains the sole authority; this is read here
|
|
799
|
+
# only so the lens can be reflected without a flash of the wrong view.
|
|
800
|
+
role_context = serializers.SerializerMethodField()
|
|
801
|
+
# Account-wide Do-Not-Disturb (#1707, ADR-0292). Read-only projection so the
|
|
802
|
+
# web current-user query carries the DND state and the bell reflects it with no
|
|
803
|
+
# extra request; the authoritative read/write surface is
|
|
804
|
+
# /api/v1/me/notification-settings/. Non-creating on this hot GET path — the
|
|
805
|
+
# absence of a settings row reads as DND off.
|
|
806
|
+
dnd_enabled = serializers.SerializerMethodField()
|
|
807
|
+
# Personal display frame (#1953, ADR-0410). Read-only projections the web client
|
|
808
|
+
# reads to render timestamps/dates in the viewer's frame. API-first: these are
|
|
809
|
+
# server facts (identical for web, mobile, MCP) but purely presentational — the
|
|
810
|
+
# API itself always emits aware-UTC ISO-8601; an agent ignores them.
|
|
811
|
+
# - timezone: IANA zone for instant timestamps, or "auto" (browser zone).
|
|
812
|
+
# - date_format: style for all displayed dates ("auto"/"iso"/"us"/"eu").
|
|
813
|
+
timezone = serializers.SerializerMethodField()
|
|
814
|
+
date_format = serializers.SerializerMethodField()
|
|
815
|
+
# Caller's own credential posture (#2938). ``is_agent_token()`` decides on every
|
|
816
|
+
# request whether the caller is governed by the MCP control layer (kill switch,
|
|
817
|
+
# team opt-out, row filtering) but reports it nowhere — so a member who points
|
|
818
|
+
# trueppm-mcp at a personal legacy:full token has no way to learn the switches
|
|
819
|
+
# they think are protecting them do not apply. This is the fix: the one endpoint
|
|
820
|
+
# the MCP client already calls at boot (verify_auth) now echoes the caller's own
|
|
821
|
+
# scopes and agent posture. Disclosing it leaks nothing — it is the caller's own
|
|
822
|
+
# credential. Null for a session/JWT caller, which carries no token at all.
|
|
823
|
+
token = serializers.SerializerMethodField()
|
|
824
|
+
|
|
825
|
+
def get_max_project_role(self, obj: Any) -> int | None:
|
|
826
|
+
# Memoized: get_can_access_admin_settings also needs this, so without the
|
|
827
|
+
# cache /auth/me would run the same aggregate twice per response.
|
|
828
|
+
if not hasattr(self, "_max_project_role"):
|
|
829
|
+
from django.db.models import Max
|
|
830
|
+
|
|
831
|
+
value = ProjectMembership.objects.filter(user=obj, is_deleted=False).aggregate(
|
|
832
|
+
_max=Max("role")
|
|
833
|
+
)["_max"]
|
|
834
|
+
self._max_project_role: int | None = int(value) if value is not None else None
|
|
835
|
+
return self._max_project_role
|
|
836
|
+
|
|
837
|
+
def get_workspace_role(self, obj: Any) -> int | None:
|
|
838
|
+
# Memoized: get_can_access_admin_settings also reads this. Delegates to the
|
|
839
|
+
# canonical resolver so the superuser-bootstrap and deactivated-status
|
|
840
|
+
# rules match what workspace RBAC enforces (no shadow copy — ADR-0087 §6).
|
|
841
|
+
if not hasattr(self, "_workspace_role"):
|
|
842
|
+
from trueppm_api.apps.workspace.permissions import workspace_role_for_user
|
|
843
|
+
|
|
844
|
+
self._workspace_role: int | None = workspace_role_for_user(obj)
|
|
845
|
+
return self._workspace_role
|
|
846
|
+
|
|
847
|
+
def get_can_access_admin_settings(self, obj: Any) -> bool:
|
|
848
|
+
from trueppm_api.apps.workspace.models import WorkspaceRole
|
|
849
|
+
|
|
850
|
+
proj = self.get_max_project_role(obj)
|
|
851
|
+
ws = self.get_workspace_role(obj)
|
|
852
|
+
return (proj is not None and proj >= Role.ADMIN) or (
|
|
853
|
+
ws is not None and ws >= WorkspaceRole.ADMIN
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
def _prefs(self, obj: Any) -> tuple[str, list[str], str, str, str]:
|
|
857
|
+
# Memoized single read of (default_landing, hidden_views, role_context,
|
|
858
|
+
# timezone, date_format): every pref getter needs a UserProfile column, so
|
|
859
|
+
# reading them in one .only() query keeps /auth/me at one profile read
|
|
860
|
+
# regardless of how many fields consume it.
|
|
861
|
+
if not hasattr(self, "_prefs_cache"):
|
|
862
|
+
from trueppm_api.apps.profiles.services import get_profile_prefs
|
|
863
|
+
|
|
864
|
+
self._prefs_cache: tuple[str, list[str], str, str, str] = get_profile_prefs(obj)
|
|
865
|
+
return self._prefs_cache
|
|
866
|
+
|
|
867
|
+
def get_default_landing(self, obj: Any) -> str:
|
|
868
|
+
return self._prefs(obj)[0]
|
|
869
|
+
|
|
870
|
+
@extend_schema_field(
|
|
871
|
+
inline_serializer(
|
|
872
|
+
"Landing",
|
|
873
|
+
{
|
|
874
|
+
"intent": serializers.CharField(),
|
|
875
|
+
"path": serializers.CharField(),
|
|
876
|
+
"resolved_by": serializers.CharField(),
|
|
877
|
+
},
|
|
878
|
+
)
|
|
879
|
+
)
|
|
880
|
+
def get_landing(self, obj: Any) -> dict[str, str]:
|
|
881
|
+
from trueppm_api.apps.profiles.services import resolve_landing
|
|
882
|
+
|
|
883
|
+
# Reuse the already-computed preference and max project role so the
|
|
884
|
+
# resolver doesn't re-query UserProfile / re-aggregate Max(role) — both
|
|
885
|
+
# are memoized above and computed for sibling fields on this same request.
|
|
886
|
+
landing = resolve_landing(
|
|
887
|
+
obj,
|
|
888
|
+
pref=self.get_default_landing(obj),
|
|
889
|
+
max_role=self.get_max_project_role(obj),
|
|
890
|
+
)
|
|
891
|
+
return {
|
|
892
|
+
"intent": landing.intent,
|
|
893
|
+
"path": landing.path,
|
|
894
|
+
"resolved_by": landing.resolved_by,
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
@extend_schema_field(serializers.ListField(child=serializers.CharField()))
|
|
898
|
+
def get_hidden_views(self, obj: Any) -> list[str]:
|
|
899
|
+
return self._prefs(obj)[1]
|
|
900
|
+
|
|
901
|
+
# Reuse the model's own choices (not a hardcoded copy) so this enum is
|
|
902
|
+
# byte-identical to UserProfileSerializer.role_context — drf-spectacular then
|
|
903
|
+
# collapses both into one shared ``RoleContextEnum`` component instead of
|
|
904
|
+
# emitting divergent ``MeRoleContextEnum`` / ``UserProfileRoleContextEnum``
|
|
905
|
+
# duplicates for the same pm/scrum_master/unified set.
|
|
906
|
+
@extend_schema_field(serializers.ChoiceField(choices=RoleContext.choices))
|
|
907
|
+
def get_role_context(self, obj: Any) -> str:
|
|
908
|
+
return self._prefs(obj)[2]
|
|
909
|
+
|
|
910
|
+
@extend_schema_field(serializers.CharField())
|
|
911
|
+
def get_timezone(self, obj: Any) -> str:
|
|
912
|
+
return self._prefs(obj)[3]
|
|
913
|
+
|
|
914
|
+
# Reuse the model's own choices (not a hardcoded copy) so drf-spectacular
|
|
915
|
+
# collapses this and UserProfileSerializer.date_format into one shared
|
|
916
|
+
# ``DateFormatEnum`` component instead of divergent Me/UserProfile duplicates.
|
|
917
|
+
@extend_schema_field(serializers.ChoiceField(choices=DateFormat.choices))
|
|
918
|
+
def get_date_format(self, obj: Any) -> str:
|
|
919
|
+
return self._prefs(obj)[4]
|
|
920
|
+
|
|
921
|
+
@extend_schema_field(serializers.BooleanField())
|
|
922
|
+
def get_dnd_enabled(self, obj: Any) -> bool:
|
|
923
|
+
from trueppm_api.apps.notifications.models import UserNotificationSettings
|
|
924
|
+
|
|
925
|
+
return (
|
|
926
|
+
UserNotificationSettings.objects.filter(user=obj)
|
|
927
|
+
.values_list("dnd_enabled", flat=True)
|
|
928
|
+
.first()
|
|
929
|
+
or False
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
@extend_schema_field(TokenPostureSerializer(allow_null=True))
|
|
933
|
+
def get_token(self, obj: Any) -> dict[str, Any] | None:
|
|
934
|
+
from trueppm_api.apps.projects.models import ApiToken, is_agent_token
|
|
935
|
+
|
|
936
|
+
request = self.context.get("request")
|
|
937
|
+
auth = getattr(request, "auth", None) if request is not None else None
|
|
938
|
+
if not isinstance(auth, ApiToken):
|
|
939
|
+
return None
|
|
940
|
+
return {"scopes": list(auth.scopes or []), "is_agent": is_agent_token(auth)}
|
|
941
|
+
|
|
942
|
+
def get_display_name(self, obj: Any) -> str:
|
|
943
|
+
return _user_display_name(obj)
|
|
944
|
+
|
|
945
|
+
def get_initials(self, obj: Any) -> str:
|
|
946
|
+
return _user_initials(obj)
|