-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconf.py
More file actions
207 lines (186 loc) · 10.2 KB
/
conf.py
File metadata and controls
207 lines (186 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
"""Lazy settings wrapper for django_admin_react.
All package settings live under a single optional dict
``settings.DJANGO_ADMIN_REACT``. Defaults are applied lazily so that
adding the app to ``INSTALLED_APPS`` does not require a settings entry.
Usage in package code:
from django_admin_react.conf import settings
settings.MAX_PAGE_SIZE
Nothing in the package should read ``django.conf.settings.DJANGO_ADMIN_REACT``
directly — go through this module so defaults are consistent.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from django.conf import settings as django_settings
# Built-in fallback for the ``--dar-primary`` accent color when the
# consumer hasn't set ``PRIMARY_COLOR`` AND their ``AdminSite`` has no
# ``site_primary_color`` attribute. Re-exported so ``views.py`` can
# pick up the same constant instead of stringifying its own.
DEFAULT_PRIMARY_COLOR = "#2563eb"
DEFAULTS: dict[str, Any] = {
"ADMIN_SITE": "django.contrib.admin.site",
# The list page size derives from the model's
# ``ModelAdmin.list_per_page`` (Django's changelist source of truth,
# Rule #1 / #281), so the SPA pages like the HTML admin with no extra
# setting. ``DEFAULT_PAGE_SIZE`` is the fallback only when
# ``list_per_page`` is missing / invalid. ``MAX_PAGE_SIZE`` always caps
# ``?page_size`` (a DoS guard).
"DEFAULT_PAGE_SIZE": 25,
"MAX_PAGE_SIZE": 200,
"ENABLE_PROFILING": False,
# Branding — consumer overrides surface in the SPA shell. Both are
# rendered server-side into the SPA index template so the SPA
# picks them up on first paint (no FOUC).
#
# ``BRAND_TITLE`` — optional override for *both* the sidebar
# header and the browser-tab title. ``None``
# (default) derives them from the AdminSite,
# mirroring Django admin: ``site_header`` →
# sidebar header, ``site_title`` → tab title
# (falling back to ``site_header``), else the
# package name (#281). A consumer who already
# set ``site_header`` / ``site_title`` on their
# ``AdminSite`` needs no branding setting at all.
# Plain text; no HTML.
# ``BRAND_LOGO_URL`` — optional override for the logo / favicon URL,
# written into the SPA's ``<link rel="icon">``.
# ``None`` (default) reads ``site_logo`` off the
# configured ``AdminSite`` if the consumer set
# that attribute (#281), else keeps the no-op
# ``data:,`` placeholder. Either an absolute URL
# or a path under your ``STATIC_URL``.
"BRAND_TITLE": None,
"BRAND_LOGO_URL": None,
# ``PRIMARY_COLOR`` — the accent color for primary buttons, links, and
# active states (#437). Injected into the SPA template as the
# ``--dar-primary`` CSS variable so a consumer can brand the admin with
# no React rebuild. Must be a hex color (``#rgb`` / ``#rgba`` /
# ``#rrggbb`` / ``#rrggbbaa``); anything else is rejected at render and
# falls back to ``DEFAULT_PRIMARY_COLOR`` below, since the value is
# written into a ``<style>`` block and must not be able to inject CSS.
#
# ``None`` (default) means "consumer didn't explicitly set this" — the
# SPA reads ``site_primary_color`` off the configured ``AdminSite``
# next, then falls back to ``DEFAULT_PRIMARY_COLOR``. Mirrors
# ``BRAND_TITLE`` / ``BRAND_LOGO_URL``: setting wins as the
# per-deployment override, AdminSite attr is the structural default,
# built-in default last (#631).
"PRIMARY_COLOR": None,
# ``REACT_LOGIN`` — React-rendered login is the **default** so the
# SPA fully replaces the Django admin URL surface end-to-end (owner
# directive 2026-05-28). ``SpaIndexView`` serves the React shell to
# anonymous users (with the CSRF cookie set) and the in-SPA login
# form POSTs to the API package's ``/api/v1/login/``. A consumer
# who wants the legacy HTML-admin login back can opt out with
# ``"REACT_LOGIN": False`` — the package's own ``<mount>/login/``
# endpoint is still mounted in either mode. The auth *mechanism* is
# unchanged in both modes (Django's ``authenticate``/``login``
# behind the JSON endpoint); only the UI surface differs. The
# shell carries no user data — serving it to anonymous users
# discloses nothing the static bundle wouldn't, and every data API
# call still returns 403 until the user authenticates.
"REACT_LOGIN": True,
# PWA (Issue #86) — all optional; sane defaults make the manifest
# work with zero config. See ``django_admin_react/pwa.py`` +
# ``docs/ux/pwa.md``.
#
# ``PWA_NAME`` — installed-app name. ``None`` (default) falls
# back to the AdminSite ``site_header``, then
# ``"Django admin"``.
# ``PWA_SHORT_NAME`` — home-screen label. Defaults to ``"Admin"``.
# ``PWA_ICONS`` — list of ``{src, sizes, type[, purpose]}``
# dicts. ``None`` (default) uses the shipped
# 192/512/maskable set under
# ``static/dar/icons/``.
# ``API_URL_PREFIX`` — absolute URL prefix the SPA calls for every
# JSON request (#559). Default ``None`` keeps the inline include the
# package ships today (`<spa-mount>/api/v1/`), so existing consumers
# are unaffected. Override when the consumer mounts
# ``django_admin_rest_api.urls`` separately and the SPA should talk
# to **that** mount instead — for example
# ``DJANGO_ADMIN_REACT = {"API_URL_PREFIX": "/api/api/v1/"}`` lets
# the SPA and any other client share a single REST mount. When set,
# `django_admin_react.urls` skips the inline `api/v1/` include so
# there is no double-mount.
"API_URL_PREFIX": None,
# ``LEGACY_ADMIN_URL_PREFIX`` — opt-in escape-hatch banner (#577).
# When set, every SPA page renders a thin notice banner at the top
# linking to **the same page** under the legacy Django admin's URL
# prefix. Useful during a progressive migration: end-users with
# muscle memory for ``/admin/`` (or wherever the legacy admin is
# mounted) can return to the classic surface in one click, and the
# bug list grows from "what they clicked back for." Default
# ``None`` keeps behaviour unchanged — no banner, no extra
# requests, no SPA overhead.
#
# The value matches the prefix the consumer registered the legacy
# admin under in ``urls.py`` — e.g. ``"admin/"`` for
# ``urlpatterns = [path("admin/", legacy_admin.urls), ...]``. The
# SPA computes the matching legacy URL by swapping its own mount
# for this value (both admins use the same ``app_label/model_name``
# URL shape, so it's a straight prefix swap with no per-route
# mapping). When the matching legacy route doesn't exist, the
# legacy admin 404s — same outcome as visiting that URL directly.
"LEGACY_ADMIN_URL_PREFIX": None,
# ``REACT_ADMIN_URL_PREFIX`` — sibling of ``LEGACY_ADMIN_URL_PREFIX``
# (#584). When **both** are set, the legacy Django admin renders a
# mirror strip at the top of every page linking the same path under
# the React admin's mount — so a user on either surface can swap
# to the other in one click. The value is the prefix the SPA was
# mounted at in ``urls.py`` — e.g. ``"admin2/"`` for
# ``urlpatterns = [path("admin2/", include("django_admin_react.urls")), ...]``.
# When only ``LEGACY_ADMIN_URL_PREFIX`` is set, only the SPA-side
# strip renders (reverse direction stays off — no implicit guess
# at the consumer's chosen mount).
"REACT_ADMIN_URL_PREFIX": None,
"PWA_NAME": None,
"PWA_SHORT_NAME": None,
"PWA_ICONS": None,
}
@dataclass(frozen=True)
class _PackageSettings:
"""Resolved package settings.
Real implementation lands in PR #2. For now this is a stub so other
modules can import the typed attribute names.
"""
ADMIN_SITE: str = DEFAULTS["ADMIN_SITE"]
DEFAULT_PAGE_SIZE: int = DEFAULTS["DEFAULT_PAGE_SIZE"]
MAX_PAGE_SIZE: int = DEFAULTS["MAX_PAGE_SIZE"]
ENABLE_PROFILING: bool = DEFAULTS["ENABLE_PROFILING"]
BRAND_TITLE: str | None = DEFAULTS["BRAND_TITLE"]
BRAND_LOGO_URL: str | None = DEFAULTS["BRAND_LOGO_URL"]
PRIMARY_COLOR: str | None = DEFAULTS["PRIMARY_COLOR"]
REACT_LOGIN: bool = DEFAULTS["REACT_LOGIN"]
API_URL_PREFIX: str | None = DEFAULTS["API_URL_PREFIX"]
LEGACY_ADMIN_URL_PREFIX: str | None = DEFAULTS["LEGACY_ADMIN_URL_PREFIX"]
REACT_ADMIN_URL_PREFIX: str | None = DEFAULTS["REACT_ADMIN_URL_PREFIX"]
PWA_NAME: str | None = DEFAULTS["PWA_NAME"]
PWA_SHORT_NAME: str | None = DEFAULTS["PWA_SHORT_NAME"]
PWA_ICONS: list[dict[str, str]] | None = DEFAULTS["PWA_ICONS"]
def _load() -> _PackageSettings:
"""Merge the consumer's overrides with ``DEFAULTS``.
Unknown keys raise ``ValueError`` so a typo in
``settings.DJANGO_ADMIN_REACT`` is caught at startup rather than
silently ignored. Returns an immutable ``_PackageSettings``.
"""
user_overrides = getattr(django_settings, "DJANGO_ADMIN_REACT", {}) or {}
merged = {**DEFAULTS, **user_overrides}
# Reject unknown keys defensively to surface typos early.
unknown = set(merged) - set(DEFAULTS)
if unknown:
raise ValueError("Unknown DJANGO_ADMIN_REACT keys: " + ", ".join(sorted(unknown)))
return _PackageSettings(**merged)
# Lazily resolve on first access; cache.
_cached: _PackageSettings | None = None
def __getattr__(name: str) -> Any: # pragma: no cover — thin shim
"""Module-level ``__getattr__`` (PEP 562) so callers can write
``from django_admin_react.conf import settings`` or
``conf.MAX_PAGE_SIZE`` without a separate accessor.
First access triggers ``_load()`` and caches the result; later
accesses return cached attributes. Reload requires re-importing
the module (matches Django's own settings semantics).
"""
global _cached
if _cached is None:
_cached = _load()
return getattr(_cached, name)