Skip to content

Commit e669511

Browse files
feat: add django.core.checks for misconfiguration (#667)
New django_admin_react/checks.py registers a manage.py check validator with actionable hints: django_admin_rest_api missing from INSTALLED_APPS (Error), unimportable ADMIN_SITE dotted path (Error), unknown DJANGO_ADMIN_REACT keys (Error, at startup vs a lazy ValueError), API_URL_PREFIX requiring the consumer to mount the API (Warning), and a missing built bundle/Vite manifest (Warning). Registered in AppConfig.ready(). Adds django_admin_rest_api to the test project's INSTALLED_APPS (per its design) and a full-coverage test_checks.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 232d975 commit e669511

4 files changed

Lines changed: 294 additions & 0 deletions

File tree

django_admin_react/apps.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
from django.apps import AppConfig
9+
from django.core.checks import register
910

1011

1112
class DjangoAdminReactConfig(AppConfig):
@@ -25,3 +26,15 @@ class DjangoAdminReactConfig(AppConfig):
2526
label = "django_admin_react"
2627
verbose_name = "Django Admin React"
2728
default_auto_field = "django.db.models.BigAutoField"
29+
30+
def ready(self) -> None:
31+
"""Register the package's system checks at app-load (#667).
32+
33+
Importing + registering here (not at module import time) keeps the
34+
checks tied to the app registry being ready, matching Django's
35+
documented pattern. The import is local so adding the app has no
36+
eager import cost beyond the AppConfig itself.
37+
"""
38+
from django_admin_react.checks import check_django_admin_react
39+
40+
register(check_django_admin_react)

django_admin_react/checks.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""System checks for django_admin_react (#667).
2+
3+
Registered against ``django.core.checks`` so common misconfigurations
4+
surface at ``manage.py check`` (and on every ``runserver`` boot) with an
5+
actionable hint — instead of a lazy ``ValueError`` on first settings
6+
access, a runtime 500, or a silent template fallback.
7+
8+
What we validate:
9+
10+
- ``django_admin_rest_api`` is installed (the package implements no API of
11+
its own — every JSON endpoint lives in that sibling package).
12+
- ``DJANGO_ADMIN_REACT["ADMIN_SITE"]`` (dotted path) actually imports.
13+
- ``DJANGO_ADMIN_REACT`` has no unknown keys (the same guard ``conf._load``
14+
enforces lazily — surfaced here at startup as a clean check error).
15+
- ``API_URL_PREFIX`` mounting is coherent: when it is set, the package
16+
skips its inline ``api/v1/`` include, so the consumer must mount
17+
``django_admin_rest_api.urls`` themselves — a Warning reminds them.
18+
- The built SPA bundle / Vite manifest exists (a Warning, not an Error —
19+
the package serves a friendly "not built" shell, and the manifest is
20+
absent in a source checkout / before ``pnpm build``).
21+
22+
Severities follow Django's convention: an ``Error`` blocks (the package
23+
cannot work); a ``Warning`` is a likely-misconfiguration heads-up that
24+
does not hard-block.
25+
"""
26+
27+
from __future__ import annotations
28+
29+
from typing import Any
30+
31+
from django.core.checks import Error
32+
from django.core.checks import Warning as CheckWarning
33+
34+
# Stable check IDs (Django convention ``<app_label>.E/W###``) so a consumer
35+
# can silence a specific one via ``SILENCED_SYSTEM_CHECKS`` if they must.
36+
ID_REST_API_MISSING = "django_admin_react.E001"
37+
ID_ADMIN_SITE_IMPORT = "django_admin_react.E002"
38+
ID_UNKNOWN_SETTINGS = "django_admin_react.E003"
39+
ID_API_PREFIX_MOUNT = "django_admin_react.W001"
40+
ID_BUNDLE_MISSING = "django_admin_react.W002"
41+
42+
43+
def check_django_admin_react(app_configs: Any, **kwargs: Any) -> list[Any]:
44+
"""Run every package configuration check.
45+
46+
Registered as a single callable (rather than many) so the import
47+
surface stays small and the ordering is explicit. ``app_configs`` /
48+
``kwargs`` are the Django check-framework signature; unused here
49+
because the checks are global to the package, not per-app.
50+
"""
51+
errors: list[Any] = []
52+
errors.extend(_check_rest_api_installed())
53+
errors.extend(_check_admin_site_imports())
54+
errors.extend(_check_settings_keys())
55+
errors.extend(_check_api_prefix_coherence())
56+
errors.extend(_check_bundle_built())
57+
return errors
58+
59+
60+
def _check_rest_api_installed() -> list[Any]:
61+
from django.apps import apps as django_apps
62+
63+
if django_apps.is_installed("django_admin_rest_api"):
64+
return []
65+
return [
66+
Error(
67+
"'django_admin_rest_api' is not in INSTALLED_APPS.",
68+
hint=(
69+
"django-admin-react is a React SPA over django-admin-rest-api "
70+
"and implements no API of its own. Add 'django_admin_rest_api' "
71+
"to INSTALLED_APPS (it ships as a dependency)."
72+
),
73+
id=ID_REST_API_MISSING,
74+
)
75+
]
76+
77+
78+
def _check_admin_site_imports() -> list[Any]:
79+
from django.utils.module_loading import import_string
80+
81+
from django_admin_react import conf as dar_conf
82+
83+
dotted = dar_conf.ADMIN_SITE
84+
try:
85+
import_string(dotted)
86+
except ImportError as exc:
87+
return [
88+
Error(
89+
f"DJANGO_ADMIN_REACT['ADMIN_SITE'] = {dotted!r} could not be imported " f"({exc}).",
90+
hint=(
91+
"Point ADMIN_SITE at the dotted path of your AdminSite "
92+
"instance (e.g. 'myproject.admin.site' or the default "
93+
"'django.contrib.admin.site')."
94+
),
95+
id=ID_ADMIN_SITE_IMPORT,
96+
)
97+
]
98+
return []
99+
100+
101+
def _check_settings_keys() -> list[Any]:
102+
from django.conf import settings as django_settings
103+
104+
from django_admin_react.conf import DEFAULTS
105+
106+
overrides = getattr(django_settings, "DJANGO_ADMIN_REACT", {}) or {}
107+
unknown = sorted(set(overrides) - set(DEFAULTS))
108+
if not unknown:
109+
return []
110+
return [
111+
Error(
112+
"Unknown DJANGO_ADMIN_REACT key(s): " + ", ".join(unknown) + ".",
113+
hint=("Remove the typo'd key(s). Valid keys are: " + ", ".join(sorted(DEFAULTS)) + "."),
114+
id=ID_UNKNOWN_SETTINGS,
115+
)
116+
]
117+
118+
119+
def _check_api_prefix_coherence() -> list[Any]:
120+
from django_admin_react import conf as dar_conf
121+
122+
prefix = dar_conf.API_URL_PREFIX
123+
if prefix is None:
124+
# Inline mount is active; the package mounts the API itself. Nothing
125+
# the consumer must do.
126+
return []
127+
# The override is set, so `django_admin_react.urls` skips the inline
128+
# `api/v1/` include — the consumer MUST mount the REST API themselves at
129+
# that prefix or every SPA data call 404s.
130+
return [
131+
CheckWarning(
132+
f"DJANGO_ADMIN_REACT['API_URL_PREFIX'] = {prefix!r} is set, so this "
133+
"package does NOT mount the REST API inline.",
134+
hint=(
135+
"Mount the sibling API yourself at that prefix, e.g.\n"
136+
f" path({prefix!r}, include('django_admin_rest_api.api.urls'))\n"
137+
"or unset API_URL_PREFIX to use the package's inline 'api/v1/' mount."
138+
),
139+
id=ID_API_PREFIX_MOUNT,
140+
)
141+
]
142+
143+
144+
def _check_bundle_built() -> list[Any]:
145+
# Imported lazily so the check module has no import-time dependency on
146+
# the view layer.
147+
from django_admin_react.views import _MANIFEST_PATH
148+
149+
if _MANIFEST_PATH.is_file():
150+
return []
151+
return [
152+
CheckWarning(
153+
"The built SPA bundle / Vite manifest was not found at " f"{_MANIFEST_PATH}.",
154+
hint=(
155+
"Install the published wheel (which ships the built bundle), or "
156+
"build the frontend from source with 'pnpm --dir frontend build'. "
157+
"Until then the SPA shell renders a 'not built yet' page."
158+
),
159+
id=ID_BUNDLE_MISSING,
160+
)
161+
]

tests/test_checks.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""System-check tests (#667).
2+
3+
Exercise every branch of ``django_admin_react.checks`` — the happy path
4+
(no findings with the test project's correct config) and each failure
5+
mode via ``override_settings`` / monkeypatching, asserting the right
6+
check ID and that the message carries an actionable hint.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import pytest
12+
from django.test import override_settings
13+
14+
from django_admin_react import checks as dar_checks
15+
16+
17+
def _ids(findings: list) -> set[str]:
18+
return {f.id for f in findings}
19+
20+
21+
def test_all_checks_pass_with_the_test_project_config() -> None:
22+
# The test project installs django_admin_rest_api, a valid ADMIN_SITE,
23+
# no unknown keys, the inline API mount, and a built manifest is present
24+
# in the package's static dir — so the only finding we tolerate is the
25+
# bundle-missing warning when running from a source checkout.
26+
findings = dar_checks.check_django_admin_react(None)
27+
blocking = [f for f in findings if f.id != dar_checks.ID_BUNDLE_MISSING]
28+
assert blocking == [], blocking
29+
30+
31+
@override_settings(DJANGO_ADMIN_REACT={"NOPE_TYPO": 1})
32+
def test_unknown_settings_key_is_an_error_with_hint() -> None:
33+
findings = dar_checks._check_settings_keys()
34+
assert _ids(findings) == {dar_checks.ID_UNKNOWN_SETTINGS}
35+
assert "NOPE_TYPO" in findings[0].msg
36+
assert findings[0].hint
37+
38+
39+
def test_bad_admin_site_dotted_path_is_an_error() -> None:
40+
# ADMIN_SITE is read off the cached conf module; reload it under the
41+
# override so the bad value takes effect.
42+
import importlib
43+
44+
from django_admin_react import conf as dar_conf
45+
46+
with override_settings(DJANGO_ADMIN_REACT={"ADMIN_SITE": "does.not.exist.site"}):
47+
importlib.reload(dar_conf)
48+
try:
49+
findings = dar_checks._check_admin_site_imports()
50+
finally:
51+
# Restore the cached conf for the rest of the suite.
52+
importlib.reload(dar_conf)
53+
assert _ids(findings) == {dar_checks.ID_ADMIN_SITE_IMPORT}
54+
assert findings[0].hint
55+
56+
57+
def test_api_prefix_set_emits_a_mount_warning() -> None:
58+
import importlib
59+
60+
from django_admin_react import conf as dar_conf
61+
62+
with override_settings(DJANGO_ADMIN_REACT={"API_URL_PREFIX": "/api/api/v1/"}):
63+
importlib.reload(dar_conf)
64+
try:
65+
findings = dar_checks._check_api_prefix_coherence()
66+
finally:
67+
importlib.reload(dar_conf)
68+
assert _ids(findings) == {dar_checks.ID_API_PREFIX_MOUNT}
69+
assert "/api/api/v1/" in findings[0].msg
70+
assert findings[0].hint
71+
72+
73+
def test_api_prefix_unset_emits_no_warning() -> None:
74+
# Default test config leaves API_URL_PREFIX unset → inline mount → quiet.
75+
assert dar_checks._check_api_prefix_coherence() == []
76+
77+
78+
def test_rest_api_missing_is_an_error(monkeypatch: pytest.MonkeyPatch) -> None:
79+
from django.apps import apps as django_apps
80+
81+
real_is_installed = django_apps.is_installed
82+
83+
def fake_is_installed(label: str) -> bool:
84+
if label == "django_admin_rest_api":
85+
return False
86+
return real_is_installed(label)
87+
88+
monkeypatch.setattr(django_apps, "is_installed", fake_is_installed)
89+
findings = dar_checks._check_rest_api_installed()
90+
assert _ids(findings) == {dar_checks.ID_REST_API_MISSING}
91+
assert "INSTALLED_APPS" in findings[0].hint
92+
93+
94+
def test_bundle_missing_is_a_warning(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
95+
from django_admin_react import views
96+
97+
# Point the manifest path at a file that doesn't exist.
98+
monkeypatch.setattr(views, "_MANIFEST_PATH", tmp_path / "missing-manifest.json")
99+
findings = dar_checks._check_bundle_built()
100+
assert _ids(findings) == {dar_checks.ID_BUNDLE_MISSING}
101+
assert findings[0].hint
102+
103+
104+
def test_bundle_present_emits_no_warning(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
105+
from django_admin_react import views
106+
107+
manifest = tmp_path / "manifest.json"
108+
manifest.write_text("{}", encoding="utf-8")
109+
monkeypatch.setattr(views, "_MANIFEST_PATH", manifest)
110+
assert dar_checks._check_bundle_built() == []
111+
112+
113+
def test_check_is_registered_with_django() -> None:
114+
from django.core.checks import registry
115+
116+
registered = {c for c in registry.registry.get_checks()}
117+
assert dar_checks.check_django_admin_react in registered

tests/test_project/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
# invisible on every legacy admin page). Documented in the
2525
# README's "Experience-toggle strip" section.
2626
"django_admin_react",
27+
# The sibling REST API package — django_admin_react is a thin SPA layer
28+
# over it, and the package's system check (#667) expects it installed.
29+
"django_admin_rest_api",
2730
"django.contrib.admin",
2831
"django.contrib.auth",
2932
"django.contrib.contenttypes",

0 commit comments

Comments
 (0)