djangoplay-web / Apps / DjangoPlay — utilities App
DocsDjangoPlay WebAppsDjangoPlay — utilities App

DjangoPlay — utilities App

utilities is DjangoPlay's shared infrastructure app — it owns no significant domain data of its own (a single model, ExportJob) but supplies the plumbing that every other app's Django Admin pages, ...

16 min readApplies to v1.2.2
On this page ▾
  1. 1. Summary
  2. 2. Architecture
  3. 2.1 Admin framework (admin/)
  4. 2.2 Generic DRF API layer (api/)
  5. 2.3 The admin registry — a second, independent access-control layer
  6. 2.4 Export subsystem
  7. 2.5 Generic AJAX endpoints (views/, urls.py)
  8. 2.6 Shared low-level helpers
  9. 3. Implementation Details
  10. 3.1 admin/app_registry.py::REGISTRY_CONFIG + configs/admin_registry.json
  11. 3.2 services/is_admin.py::PlatformAdminService
  12. 3.3 admin/cascade.py::CascadeHandler
  13. 3.4 admin/mixins/history.py::HistoryMixin
  14. 3.5 Export throttling
  15. 3.6 admin/filters/factory.py::changelist_filter()
  16. 4. Cross-App Integration Points
  17. 5. Security-Relevant Observations
  18. 6. Known Gaps / Things Worth Confirming With the Team
  19. 7. Quick Reference — Tracing "Why Is This App/Model Visible (or Not) Here?"

Doc generated by inspecting all files under webapp/utilities/, plus the primary cross-app callers: paystream/custom_site/admin_site.py and paystream/custom_site/admin_console_views.py (consumers of admin/app_builder.py and admin/dashboard_metadata.py), paystream/urlconf/base.py (mounts utilities.urls at the site root), paystream/app_settings/middleware.py, and webapp/configs/*.json (the runtime config files utilities reads).

1. Summary

  1. A custom Django Admin framework (admin/) — BaseAdminPage and its mixins (soft-delete actions, audit-field display, change history, cascading delete/restore, AJAX-populated dropdowns/filters, CSV export) that essentially every ModelAdmin in the platform inherits from instead of django.contrib.admin.ModelAdmin directly.
  2. A generic DRF API layer (api/) — BaseListAPIView, BaseDetailAPIView, BaseViewSet, BaseBulkAPIView/BaseBulkUpdateAPIView, Redis-backed response caching, throttling, and reusable filter mixins (date range, FK, name search, trigram search) that other apps' viewsets subclass.
  3. The "admin registry" (admin/app_registry.py + configs/admin_registry.json) — the runtime source of truth for which Django apps/models are visible in the custom console UI at all, independent of (and layered on top of) policyengine's role-based permissions.
  4. A CSV export subsystem (services/exports/, models/export_job.py) — a generic, any-model CSV export pipeline with per-user/per-IP throttling, wired into the admin via AdminExportMixin.
  5. Generic, app-agnostic AJAX endpoints (views/generic/, urls.py) — one filter-options endpoint and one form-options endpoint that serve dropdown/autocomplete data for any Django app/model without per-app registration, plus two narrower endpoints (audit filter options, invoices recipient-address lookup).

Around this, utilities also carries a grab-bag of shared low-level helpers that don't fit neatly into a single domain app: phone/postal-code/address validators used by locations and fincore, a national-ID reference table (constants/national_ids.py), a signed-token generator used by users' verification flows, a domain-allowlist validator for signup emails, and the platform's PlatformAdminService (the one place "superuser" is further narrowed to two specific hardcoded emails).

The app's own README-equivalent (admin/mixins/permissions_bridge.py's docstring) is explicit that policyengine remains the source of truth for permissions and that utilities provides only optional, explicit, opt-in UI-level overrides — in practice, though, utilities ends up being a second, independent access-control layer (the registry) that every request also has to pass, and the two layers are not always kept in sync (see §5 and §6).


2. Architecture

utilities has no clean single entry point — it's five loosely related subsystems sharing one Django app namespace. This section maps each one.

2.1 Admin framework (admin/)

plaintext
admin/mixins/base_admin.py        BaseAdmin (ModelAdmin composition root)
  ├─ admin/mixins/fieldsets.py    FieldsetMixin — injects a "Metadata" audit fieldset
  ├─ admin/mixins/admin_form.py   CustomAdminFormMixin
  ├─ admin/mixins/history.py      HistoryMixin — diffs simple_history records into a display-ready timeline
  ├─ admin/mixins/soft_delete.py  SoftDeleteMixin — soft_delete / restore / hard_delete admin actions
  └─ admin/mixins/queryset.py     OptimizedQuerysetMixin — select_related/prefetch_related hooks

admin/base_console_admin.py       BaseConsoleAdmin — tab/section form-layout resolution for the custom change form UI
admin/export_mixin.py             AdminExportMixin — adds /export-selected/ and /export-all/ admin URLs
admin/base_admin_page.py          BaseAdminPage(AdminExportMixin, BaseAdmin, BaseConsoleAdmin) — what every app's ModelAdmin actually inherits from
admin/cascade.py                  CascadeHandler — generic, reflection-based recursive soft-delete/restore/hard-delete across FK/O2O reverse relations
admin/app_registry.py             build_app_registry() / get_app_registry() — the registry (§2.3)
admin/app_builder.py              build_app_list() — permission-filtered app/model list for the admin UI
admin/dashboard_metadata.py       get_dashboard_apps() / get_global_search_data() — same idea, for the console dashboard + global search
admin/admin_metadata.py           MODEL_ICON_MAP / APP_ICON_MAP — static icon lookup tables
admin/registry_config_loader.py   get_registry_config() — loads configs/admin_registry.json with a 6-second in-memory TTL cache
admin/filters/                    ~15 files of reusable/app-specific admin SimpleListFilter subclasses (§4.5)
admin/export_fields.py            get_export_fields() — reflection-based default export-field-list generator
admin/paginators.py               LimitedAdminPaginator — hard-caps admin changelist pagination at 5 pages
admin/url_utils.py                get_all_subdomains() / get_subdomain() — subdomain URL resolution (§4.6)

BaseAdminPage is the composition root: class BaseAdminPage(AdminExportMixin, BaseAdmin, BaseConsoleAdmin). Every app's ModelAdmin classes (verified via the admin_metadata.py icon map, which lists models from fincore, users, teamcentral, helpdesk, locations, industries, entities, invoices, and the third-party genericissuetracker) are expected to subclass this rather than Django's own ModelAdmin.

2.2 Generic DRF API layer (api/)

plaintext
api/generic_views.py       BaseListAPIView, BaseDetailAPIView, BaseFilteredListAPIView,
                            BaseBulkAPIView, BaseHistoryListAPIView
api/generic_viewsets.py    BaseViewSet(SerializerByActionMixin, SoftDeleteMixin, CacheMixin, ModelViewSet)
api/bulk_views.py          BaseBulkUpdateAPIView — PATCH-based bulk field update with history tracking
api/pagination.py          StandardResultsSetPagination (page_size=50, max=100)
api/rate_limits.py         CustomThrottle (50/hr, IP-keyed), CustomSearchThrottle (100/hr), TokenThrottle (50/hr, user-keyed)
api/mixins/redis_cache.py  CacheMixin — raw redis_client-backed get/set/invalidate-by-pattern
api/mixins/soft_delete.py  SoftDeleteMixin — perform_destroy/perform_restore wrapping model.soft_delete()/restore()
api/mixins/serializer_resolution.py  SerializerByActionMixin — read_serializer_class vs write_serializer_class by action
api/filters/                Composable filter mixins: date_range, foreign_key, text_search (icontains), trigram_search (pg_trgm)
api/generic_api_exceptions.py  GenericAPIError — the default error_class every base view/viewset falls back to

Two independent caching mechanisms exist side by side: BaseListAPIView/BaseDetailAPIView (in generic_views.py) use Django's cache framework directly with SHA-256-hashed query-param cache keys; BaseViewSet (in generic_viewsets.py) uses its own CacheMixin, which talks to core.utils.redis_client directly with json.dumps/json.loads. Both default to long TTLs (BaseListAPIView: 172800s / 2 days; BaseDetailAPIView: 86400s / 1 day; BaseViewSet: 172800s / 2 days).

2.3 The admin registry — a second, independent access-control layer

get_app_registry() (admin/app_registry.py) builds, on every call (no persistent caching beyond the 6-second registry_config_loader TTL), a dict of every installed Django app/model not in a hardcoded exclude_apps/exclude_models set, merged with configs/admin_registry.json. This registry answers two questions independently of Django's permission system:

  • is_app_enabled_for_user() / is_model_enabled_for_user() — is this app/model visible at all, per the enabled flag and model_exclusions list in admin_registry.json, for this user? (PlatformAdminService.is_platform_admin() — see §4.4 — always bypasses this.)
  • The registry's exclude_apps set (admin, account, allauth, auth, contenttypes, sessions, genericissuetracker, sites, socialaccount, token_blacklist, policyengine, apidocs, mailer, utilities, aicore) determines which apps show up in the console UI's app list / global search / dashboard stats at all, regardless of what Django permissions a user holds.

This is explicitly not the same mechanism as policyengine's Group/Permission system (confirmed by both app_builder.py's and dashboard_metadata.py's own docstrings: "Visibility driven by Django permissions ONLY — Do NOT filter by admin_registry here — Registry enforcement happens in views (404/403)"). In practice this means:

  • List-building functions (build_app_list, get_dashboard_apps, get_global_search_data) filter by Django's native has_view_permission()/has_perm() only — a model excluded from the registry can still appear in these lists if there's no explicit registry check in that particular function (see §6, point 3, for a confirmed inconsistency here).
  • Actual page access to a given app/model (via the custom console changelist/single-app views, not shown in this app but the consumers of is_model_enabled_for_user) is where registry exclusion is enforced as a 404/403.

services/exports/export_throttle.py and ui/dashboard/services/registry_permissions_service.py both independently consume the registry for their own purposes (§4.3, §5).

2.4 Export subsystem

plaintext
models/export_job.py                 ExportJob — status machine: pending → processing → completed | failed
services/exports/export_service.py   ExportService.create_export() / process_export() — orchestrator
services/exports/export_throttle.py  ExportThrottleService — per-email/per-IP limits from configs/throttling.json
services/exports/export_queryset.py  ExportQuerysetService — reflection-based model resolution + filter cleaning
services/exports/export_generator.py ExportGeneratorService.generate_csv() — streaming CSV write via .iterator(chunk_size=2000)
services/exports/export_delivery.py  ExportDeliveryService.deliver() — no-op for "download"; "email" branch is an unimplemented stub
admin/export_mixin.py                AdminExportMixin — /export-selected/ and /export-all/ admin URLs, wired via get_export_fields()

Despite ExportJob modeling an asynchronous job (status, started_at/completed_at/failed_at, processed_records), AdminExportMixin.export_selected_view/export_all_view call ExportService.process_export() synchronously, inline in the admin HTTP request/response cycle — there is no Celery task dispatch here even though the platform has Celery configured (paystream/app_settings/celery.py is loaded in paystream/settings/base.py). For any export large enough to matter, this blocks the admin worker for the full duration of the CSV write. See §6.

2.5 Generic AJAX endpoints (views/, urls.py)

Four endpoints, all mounted at the site root by path("", include("utilities.urls", namespace="utilities")) in paystream/urlconf/base.py:

Path View Scope
/admin/form-options/ ChangeformOptionsView Any app/model, FK-dependent autocomplete for add/edit forms
/admin/filter-options/ ChangelistFilterAPIView Any app/model, FK-dependent dropdown for changelist filter toolbar
/admin/audit/filter-options/ AuditFilterOptionsAPIView audit.AuditEvent only — distinct-value lookup for 4 allowlisted fields
/admin/invoices/form-options/ InvoiceFormOptionsView invoices app only — resolves an entity's billing addresses via fincore.Address
/health/ HealthCheckView Simulated health-check response selector (no real dependency checks)

The two generic endpoints are deliberately app-agnostic by design (their own module docstrings describe this as a replacement for "the old approach" of per-app FILTER_CHAIN registration) — see §6 for what this means for authentication.

2.6 Shared low-level helpers

  • commons/ — email/username validators, decimal-safe conversion, a domain-allowlist validator (backed by commons/valid_domains.json, not present in this pass), a secure token generator (secrets-based, URL-safe, configurable prefix — used by verification-link flows), and helpers.py (site-name resolution, an identity-state resolver that calls into users.services.identity_query_service.IdentityQueryService).
  • constants/ — login-flow status codes, a large static reference table of national ID document names by country (national_ids.py — reference data, not enforced/validated against anywhere in this app), TemplateRegistry (a central catalog of template paths and one permission-aware template selector for apidocs), and email unsubscribe-category configuration.
  • utils/ — phone number, postal code, address, and currency validators (utils/general/, utils/locations/), business-entity status-ratio and validation helpers (utils/entities/), and small data-sync utilities (utils/data_sync/) for loading env/paths and company-name normalization.
  • signals/disable_signals.py — a context manager for temporarily disconnecting pre_save/post_save receivers for given models (bulk-import / data-migration use case; not invoked anywhere within utilities itself).
  • context_processors/report_bug.py — a template context processor gating a "report a bug" UI affordance by URL allowlist or admin-namespace detection.
  • services/is_admin.py — PlatformAdminService (§4.4).

3. Implementation Details

3.1 admin/app_registry.py::REGISTRY_CONFIG + configs/admin_registry.json

REGISTRY_CONFIG (Python, hardcoded) supplies only exclude_apps/exclude_models. configs/admin_registry.json (loaded via registry_config_loader.get_registry_config(), 6-second TTL cache, falls back to {} silently on any read/parse error) supplies per-app enabled flags, a primary_model, and model_exclusions. The two are merged with {**REGISTRY_CONFIG, **(get_registry_config() or {})} — the JSON file's top-level exclude_apps/exclude_models keys would fully overwrite the Python ones if present, but the current JSON file doesn't define them, so in practice they're additive. As of this pass, admin_registry.json explicitly configures 8 apps (entities, industries, locations, audit, teamcentral, fincore, invoices, users, helpdesk); users is the only one with "enabled": false. It also defines a stats block (order + mapping) that ui/dashboard/services/dashboard_stats_service.py reads for the console dashboard's stat cards.

3.2 services/is_admin.py::PlatformAdminService

python
PLATFORM_ADMIN_EMAILS = {"redstar@djangoplay.org", "shekhar@djangoplay.org"}

is_platform_admin(user) = user.is_authenticated and user.is_superuser and user.email in PLATFORM_ADMIN_EMAILS

This is a narrower check than Django's own is_superuser — a Django superuser whose email isn't in this hardcoded set is not a "platform admin" by this app's definition, and therefore does not get the registry-bypass treatment in is_app_enabled_for_user/is_model_enabled_for_user/get_dashboard_apps/ExportThrottleService.allow_export. This two-tier admin model (Django superuser vs. "platform admin") is easy to miss when reasoning about who can see what — see §6.

3.3 admin/cascade.py::CascadeHandler

Fully reflection-based: walks obj._meta.get_fields() for auto_created and not concrete (i.e., reverse FK/O2O accessors), recurses into children, and — critically — re-queries via model.all_objects (not the default manager) so already-soft-deleted children are still included in the cascade, preventing a cascade restore from silently skipping rows a prior cascade already touched. Soft-delete cascades children first then self; restore cascades self first then children; hard-delete cascades children first then self, calling obj.save() immediately before obj.delete() (to force a final simple_history snapshot pre-deletion). All three set obj._history_user/obj._change_reason on each object rather than passing them as save()/delete() kwargs — this only works if the model's own save/delete logic reads those specific attribute names (verified as the pattern HistoryMixin and SoftDeleteMixin's admin actions expect, not independently verified against every model's own soft_delete()/restore() implementation in this pass).

3.4 admin/mixins/history.py::HistoryMixin

Reconstructs a human-readable change timeline from simple_history records without a dedicated audit-log table — it diffs consecutive HistoricalRecords rows field-by-field, detects create/soft-delete/restore/hard-delete events by pattern-matching the free-text history_change_reason string ("Soft deleted", "Restored", "Permanently deleted" / "Hard deleted" substrings), separates "activity" fields (currently only last_login) from ordinary field-change history, and supports an optional Redis-cached "snapshot" payload (_history_cache_key) to diff against when there's no previous historical record to compare against (e.g., right after a bulk import that bypassed normal save-triggered history). Explicitly excludes password and standard audit fields (id, slug, created_at, updated_at, created_by, updated_by, deleted_at, deleted_by, is_active) from the diff. On any exception, fails soft — returns an empty history structure and logs a warning rather than breaking the admin page.

3.5 Export throttling

ExportThrottleService.allow_export() bypasses entirely for PlatformAdminService.is_platform_admin(); otherwise defers to mailer.throttling.flow_throttle.allow_flow() with flow name "export_all" (full export) or "export_selected" (filtered export), configured in configs/throttling.json: export_selected = 2/day per email, 20/day per IP; export_all = 1/day per email, 25/day per IP. This is the same shared throttling mechanism mailer uses for its own flows (configs/email_flow_limits.json follows an identical per-flow schema for signup/password-reset/support-request/bug-report emails) — utilities doesn't own the throttle engine, just calls into it.

3.6 admin/filters/factory.py::changelist_filter()

A single factory function that dynamically builds a SimpleListFilter subclass in one of three modes based on what's passed in: field_name pointing at a FK (mode="fk", lookups populated from the related model, respecting deleted_at/is_active if present), field_name pointing at a plain field (mode="field", lookups from the field's choices or a distinct-values query), or no field_name at all (mode="pk", lookups directly from the current model's own rows). This is what lets most app-specific filters in admin/filters/{locations,teamcentral,helpdesk,invoices,mailer,users}.py be one-liners like CityFilter = changelist_filter("city") rather than hand-written SimpleListFilter classes.


4. Cross-App Integration Points

App How it touches utilities
Every app with a Django Admin (fincore, users, teamcentral, helpdesk, locations, industries, entities, invoices, and the third-party genericissuetracker) Their ModelAdmin classes subclass utilities.admin.mixins.BaseAdminPage (soft-delete actions, history tab, audit fieldset, CSV export, AJAX filters/form dropdowns). admin_metadata.py's MODEL_ICON_MAP hardcodes an icon per model across all of these apps — a new model in any of them needs a manual admin_metadata.py edit to get a non-default icon.
policyengine Two-way: policyengine.components.permissions.get_action_based_permissions is imported by api/generic_views.py, api/bulk_views.py, and api/generic_viewsets.py (though BaseViewSet.get_permissions() actually overrides this with its own inline DynamicPermission class doing a raw request.user.has_perm() check — see §6). Conversely, ui/dashboard/services/registry_permissions_service.assign_app_registry_permissions calls policyengine.services.ssopolicies.setup_role_based_group and then filters its output through get_app_registry() before assigning to user.user_permissions — this is the "registry-filtered" permission-assignment path documented in the policyengine app doc (§2.3/§5 there), and admin/app_registry.py::REGISTRY_CONFIG["exclude_apps"] is one of three independently-maintained app-exclusion lists identified as a known gap in that doc.
mailer services/exports/export_throttle.py calls mailer.throttling.flow_throttle.allow_flow directly — export throttling and email-flow throttling share one engine and one config-file schema (throttling.json / email_flow_limits.json), but are two separate files maintained independently.
users commons/helpers.py::employee_state_by_email calls users.services.identity_query_service.IdentityQueryService.get_by_email. admin/base_admin_page.py imports users.views.ui.errors.custom_403 for its custom add-permission-denied page.
fincore views/invoices/form_options.py::InvoiceFormOptionsView queries fincore.models.Address directly (entity → billing addresses) — the one non-generic, hardcoded cross-app AJAX endpoint in this app.
audit views/audit/filter_options.py::AuditFilterOptionsAPIView queries audit.models.audit_event.AuditEvent directly for its 4-field distinct-value filter endpoint.
apidocs constants/template_registry.py::TemplateRegistry.get_api_stats_template decides between apidocs/stats_public.html and apidocs/stats_private.html based on user.has_perm("policyengine.view_apidocs_stats") (the special, non-model permission policyengine.ensure_special_permissions() creates — see the policyengine doc §4.4). api/filters/trigram_search.py imports apidocs.utils.querysanitizer.add_sanitization_filter_to_logger for log redaction.
core api/mixins/redis_cache.py::CacheMixin uses core.utils.redis_client directly (bypassing Django's cache framework, unlike the rest of the API layer). models/export_job.py::ExportJob extends core.models.TimeStampedModel/AuditFieldsModel, the platform's shared abstract base models.
paystream admin/app_builder.py, admin/dashboard_metadata.py, and admin/mixins/decorators.py::AdminIconDecorator all import paystream.custom_site.admin_site.admin_site — the platform's single custom AdminSite instance that every app's admin classes register against instead of Django's default django.contrib.admin.site. paystream/urlconf/base.py mounts utilities.urls at the site root ("") and separately wires the console/admin URL patterns (admin_single_app, admin_custom_changelist) that app_builder.py/dashboard_metadata.py reverse() against.
Django's sessions app ui/dashboard/services/session_service.py::get_active_user_count queries django.contrib.sessions.models.Session directly, decoding each session to extract _auth_user_id — an O(n) full-table scan over all non-expired sessions on every dashboard load, with no caching.

5. Security-Relevant Observations

  1. The two generic AJAX endpoints have no authentication or authorization check in the view code itself. ChangeformOptionsView and ChangelistFilterAPIView (views/generic/) are plain django.views.View subclasses — no permission_classes, no LoginRequiredMixin, no @login_required. They are mounted at the site root (path("", include("utilities.urls", ...))), not under admin_site.urls, so they don't inherit whatever login enforcement Django's AdminSite applies to its own registered views. The platform's global MIDDLEWARE list (paystream/app_settings/middleware.py) contains no login-enforcement middleware (only AuthenticationMiddleware, which attaches request.user but doesn't require it be authenticated). Both views do apply some automatic guardrails by design — they only serve models reachable via django.apps.apps.get_model(), respect deleted_at/is_active if those fields exist, and cap results at 50 — but as written, any unauthenticated caller who can guess an app_label/model_name pair can enumerate id/display-text pairs for any model in the project, including ones a given user has no Django permission to view. AuditFilterOptionsAPIView has the same shape (no auth check), scoped to AuditEvent's 4 allowlisted fields. Worth confirming with the team whether this is intentional (e.g., relying on the URLs being unlinked/undiscoverable) or a gap that should get an IsAuthenticated check.
  2. Two-tier "admin" concept. PlatformAdminService.is_platform_admin() requires both is_superuser=True and membership in a two-email hardcoded allowlist (services/is_admin.py). Anywhere this is used as a bypass (registry visibility, export throttling), a Django superuser outside that allowlist is treated as an ordinary user — easy to misread as "superusers always see everything" when reading the registry code in isolation.
  3. ExportQuerysetService.build_queryset and views/generic/*.py both default to model.all_objects rather than the model's normal (soft-delete-filtering) default manager, when the model defines it. This is deliberate for the export/lookup use cases described (admins need to export/find soft-deleted rows too), but means any admin with export or dropdown-resolution access can retrieve data on soft-deleted records that wouldn't otherwise surface in list views.
  4. Email delivery for exports is an unimplemented stub. ExportDeliveryService.deliver()'s "email" branch is empty (comment: # future: attach export / send email / signed urls / S3 links) — if delivery_method="email" is ever actually set on an ExportJob today, the job silently completes with no email sent and no error surfaced.

6. Known Gaps / Things Worth Confirming With the Team

  1. If an export large enough to noticeably block the web worker actually shows up, that's the point to revisit Celery — with a real "your export is ready" notification UX designed up front, not bolted onto the existing button.

7. Quick Reference — Tracing "Why Is This App/Model Visible (or Not) Here?"

For a new engineer debugging console UI visibility:

  1. Is it in Django's app registry at all? apps.get_app_configs() — if the app isn't installed, nothing else matters.
  2. Is it in admin/app_registry.py::REGISTRY_CONFIG["exclude_apps"]? If yes, it's invisible everywhere get_app_registry() is consulted, full stop, regardless of permissions (policyengine, apidocs, mailer, utilities, aicore, plus Django/allauth infra apps, are always excluded).
  3. Does it have an entry in configs/admin_registry.json["apps"]? If yes and "enabled": false (currently only users), is_app_enabled_for_user returns False for everyone except PlatformAdminService.is_platform_admin() users. If a model is listed in that app's model_exclusions, is_model_enabled_for_user returns False for it specifically — but only in code paths that actually call it (§6, point 3).
  4. Does the requesting user have the relevant Django permission (view_/change_/add_/delete_ + app_label.model_name)? This is what actually gates build_app_list, get_dashboard_apps, and get_global_search_data at the per-model level — check the policyengine app doc for how that permission got assigned to the user in the first place (registry-filtered user_permissions.set() vs. direct .groups.add() — two different mechanisms with different outcomes).
  5. Is the user request.user.is_superuser? Bypasses get_dashboard_apps's permission check entirely (shows all registry-listed apps) — but note this is a different, broader bypass than PlatformAdminService.is_platform_admin(), which additionally requires the hardcoded email allowlist and is used by the registry visibility functions instead.
  6. For AJAX-populated dropdowns/filters specifically (/admin/form-options/, /admin/filter-options/): these bypass steps 3–5 almost entirely (§5, point 1) — they only check that the model exists and (if present on the model) deleted_at is None/is_active is True. Don't assume "the field is hidden in the admin" means "the underlying data is unreachable via these endpoints."