Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,12 @@
# (run the test suites in CI); the no-CI revisit is recorded in
# `SECURITY.md` §8 / `docs/agents/decisions.md` / OQ-A-001.
#
# SCOPE: the backend job runs `pytest` (the regression that motivated
# #452 was a broken test). The frontend job runs the full `pnpm` gate —
# typecheck + lint + test + build — which is already self-consistent and
# green. Enforcing the *Python lint* gate in CI is a deliberate near-term
# follow-up: `scripts/lint.sh` currently runs two formatters (`ruff
# format` + `black`) whose output conflicts, so it is not yet satisfiable
# on a clean tree. That gets de-conflicted and the small existing lint
# debt cleared first (follow-up off #452), then the lint step is added
# here.
# SCOPE: the backend job runs the Python lint gate (ruff check + ruff
# format --check + mypy + bandit) and then `pytest`. The frontend job
# runs the full `pnpm` gate — typecheck + lint + test + build. The Python
# lint stack was collapsed onto a single Ruff-based chain in #651/#652
# (black/isort/flake8 removed), which made the gate satisfiable on a
# clean tree and let it be wired in here.
#
# SECURITY POSTURE:
# - Least-privilege: top-level `contents: read`; no job needs write.
Expand Down Expand Up @@ -87,6 +84,18 @@ jobs:
- name: Pin Django to the matrix version
run: poetry run pip install "django~=${{ matrix.django }}.0"

# Python lint gate (#651/#652): a single Ruff-based stack —
# ruff check (lint, incl. `I` import order) + ruff format --check +
# mypy (strict subset on the package) + bandit (security). Black,
# standalone isort, and flake8 were removed. This is the same gate
# scripts/lint.sh runs locally.
- name: Lint (ruff + mypy + bandit)
run: |
poetry run ruff check django_admin_react tests
poetry run ruff format --check django_admin_react tests
poetry run mypy django_admin_react
poetry run bandit -r django_admin_react -c pyproject.toml -q

# pytest with coverage (per pyproject `addopts`), including
# tests/test_security.py. `filterwarnings = ["error"]` means a new
# warning fails the run.
Expand Down
45 changes: 20 additions & 25 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
# pre-commit install
#
# Then every `git commit` runs these hooks against the staged diff.
# Anything below the [BLOCK] threshold in
# `docs/agents/security-expert/REVIEW_CHECKLIST.md` §1 aborts the commit.
# The security rules these enforce are documented in `SECURITY.md` §3.
#
# This file is the commit-time half of the quality gate, run together
# with `scripts/lint.sh` before a PR. CI (`.github/workflows/ci.yml`)
# currently runs the test suites, not these hooks; wiring the lint/security
# hooks into CI is a follow-up (#452).
# runs the Python lint gate (ruff check + ruff format --check + mypy +
# bandit) and the test suites; these hooks add the secret-scan + pygrep
# house rules at commit time.

repos:
# -------------------------------------------------------------------
Expand All @@ -24,7 +24,9 @@ repos:
args: ["protect", "--staged", "--no-banner", "--redact"]

# -------------------------------------------------------------------
# Python formatting + linting (same tools scripts/lint.sh uses).
# Python lint + format + import order — Ruff is the single source of
# truth (#651/#652). Black, standalone isort, and flake8 were removed;
# ruff-format owns formatting and the `I` rules own import sorting.
# -------------------------------------------------------------------
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
Expand All @@ -35,18 +37,6 @@ repos:
- id: ruff-format
files: ^(django_admin_react|tests|examples)/

- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.8.0
hooks:
- id: black
files: ^(django_admin_react|tests|examples)/

- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
files: ^(django_admin_react|tests|examples)/

# -------------------------------------------------------------------
# Python security lint (bandit). Runs only over the package, not tests
# (asserts in tests are fine and would otherwise be noisy).
Expand All @@ -63,18 +53,17 @@ repos:

# -------------------------------------------------------------------
# House rules — local hooks that enforce package-specific invariants
# from docs/agents/security-expert/REVIEW_CHECKLIST.md.
# from SECURITY.md §3.
# -------------------------------------------------------------------
- repo: local
hooks:
# No partial / redacted token references anywhere in the diff.
#
# The `exclude` list covers the small set of files that
# legitimately *document* the forbidden patterns (e.g., the
# rule itself in SECURITY.md / ACCEPTANCE.md, the review
# checklist, the security test that scans for them, and forum
# review files that quote the rule when reviewing it). All
# other files in the repo MUST be free of these substrings.
# rule itself in SECURITY.md and the security test that scans for
# them). All other files in the repo MUST be free of these
# substrings.
- id: no-partial-tokens
name: No partial token redactions (e.g., ghp_…XYZ)
language: pygrep
Expand All @@ -83,10 +72,7 @@ repos:
exclude: |
(?x)^(
SECURITY\.md
|ACCEPTANCE\.md
|\.pre-commit-config\.yaml
|docs/threat-model\.md
|docs/agents/security-expert/.*
|tests/test_security\.py
|scripts/README\.md
)$
Expand Down Expand Up @@ -118,3 +104,12 @@ repos:
language: pygrep
entry: "from ['\"]@dar/api['\"]"
files: '^frontend/packages/(list|details|models|shell)/'

# Doc-reference integrity (#653): fail if a docstring/comment cites
# a *.md file or §N section that no longer exists.
- id: doc-ref-guard
name: No dangling *.md / §N doc references
language: system
entry: poetry run pytest tests/test_doc_refs.py -q
pass_filenames: false
files: '\.(py|yaml)$'
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Changelog

All notable changes to this project are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- **Python lint stack consolidated onto Ruff (#651, #652).** Removed Black,
standalone isort, and flake8 entirely (their `[tool.*]` config, dev
dependencies, pre-commit hooks, and `scripts/lint.sh` steps). Ruff now owns
lint + format + import order (the `I` rules), with mypy + bandit alongside —
resolving the three-formatter conflict (#452/#452-skew) and the Black 24-vs-26
pin skew. The now-green Python lint gate (ruff check + ruff format --check +
mypy + bandit) is wired into backend CI.
- **mypy tightened on the package (#655).** Enabled the `disallow_untyped_defs`
and `warn_return_any` strict subset for `django_admin_react`; typed the
`admin_site` view helpers as `AdminSite` (type-only import) instead of `Any`.
- **Frontend `@typescript-eslint/no-explicit-any` promoted from `off` to
`error` (#656)** to lock in the existing zero-`any` state, and added `/**`
JSDoc to the `Checkbox`, `Input`, `Spinner`, `EmptyState`, and
`DateHierarchyBar` primitives.

### Removed

- **Dead `django_admin_react/audit.py` module (#654).** It was imported nowhere
and had 0% coverage; the `LogEntry` access it duplicated belongs in the
sibling `django-admin-rest-api`.

### Fixed

- **Dangling documentation references (#653).** Repointed or removed docstring /
comment / pre-commit citations to docs that no longer exist (`docs/ux/pwa.md`,
`pwa.md`, `theming.md`, `ACCEPTANCE.md`, `REVIEW_CHECKLIST.md`,
`docs/threat-model.md`) so they target the surviving `ARCHITECTURE.md` /
`SECURITY.md` sections. Added a fast doc-reference guard
(`tests/test_doc_refs.py` + a pre-commit hook) that fails when a `*.md` file
or `§N` section cited in source no longer exists.
- **Stale comments (#654).** Removed the misleading "Real implementation lands
in PR #2" note on the shipped `_PackageSettings` dataclass and a dead
`# noqa: ARG002` line in `views.py` that suppressed nothing.
58 changes: 0 additions & 58 deletions django_admin_react/audit.py

This file was deleted.

9 changes: 6 additions & 3 deletions django_admin_react/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"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``.
# ``ARCHITECTURE.md`` §5.4.
#
# ``PWA_NAME`` — installed-app name. ``None`` (default) falls
# back to the AdminSite ``site_header``, then
Expand Down Expand Up @@ -152,8 +152,11 @@
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.
An immutable, frozen record of the merged
``settings.DJANGO_ADMIN_REACT`` overrides on top of :data:`DEFAULTS`,
built once by :func:`_load` and cached. Each field carries its
default; modules read the typed attribute names off the cached
instance via this module's :func:`__getattr__`.
"""

ADMIN_SITE: str = DEFAULTS["ADMIN_SITE"]
Expand Down
15 changes: 7 additions & 8 deletions django_admin_react/pwa.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""PWA surface: web app manifest + service worker (Issue #86).

Wire/UX contract: ``docs/ux/pwa.md``. The Security lane owns this
surface because its load-bearing properties are security ones:
Frontend build/ship context: ``ARCHITECTURE.md`` §5.4. The Security lane
owns this surface because its load-bearing properties are security ones:

- The **manifest** (``<mount>/web.manifest``) is served unauthenticated
(the install prompt fires before login) and is computed at request
Expand All @@ -15,8 +15,7 @@
no-store`` (so the package's no-store API reads are never cached),
never caches non-GET requests (mutation safety), and exposes a
cache-purge message used on logout so read-cached payloads can't
outlive the session (``pwa.md`` §5 — defense-in-depth atop session
expiry).
outlive the session (defense-in-depth atop session expiry).

Both views live **outside** ``api/`` because they're served at the
mount root, not under ``api/v1/``, and the manifest is intentionally
Expand All @@ -33,13 +32,13 @@
from django.shortcuts import render
from django.views.generic import View

from django_admin_react import conf as dar_conf

# Re-use the API package's admin-site lookup (this repo implements no
# API; the registry helper lives there). The PWA only needs the active
# `AdminSite.name` for the manifest's start URL.
from django_admin_rest_api.api.registry import get_admin_site

from django_admin_react import conf as dar_conf

# Theme colours keyed by the resolved colour scheme. Kept here (not in
# the SPA's CSS-var system) because the manifest is rendered server-side
# before any CSS loads; these are the install-banner / splash colours
Expand Down Expand Up @@ -79,7 +78,7 @@ def _mount(request: HttpRequest, suffix: str) -> str:
def _resolved_scheme(request: HttpRequest) -> str:
"""Resolve light/dark from the ``Sec-CH-Prefers-Color-Scheme`` hint.

Pairs with the theming client-hint path (``theming.md`` §2). Any
Pairs with the theming client-hint path (``ARCHITECTURE.md`` §5.3). Any
value other than a case-insensitive ``"dark"`` resolves to light —
the safe, neutral default when the hint is absent or unexpected.
"""
Expand All @@ -93,7 +92,7 @@ class ManifestView(View):
Unauthenticated by design (the install prompt needs it pre-login).
Carries no per-user data; every field is static or mount-/header-
derived. ``Cache-Control: no-store`` is **not** set — the manifest
is deliberately cacheable/network-first (``pwa.md`` §2.1).
is deliberately cacheable/network-first.
"""

http_method_names = ["get"]
Expand Down
2 changes: 1 addition & 1 deletion django_admin_react/templatetags/experience_toggle.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def experience_toggle_strip(context: dict[str, Any]) -> dict[str, Any]:
if not path.startswith(legacy_root):
return {"visible": False}

tail = path[len(legacy_root):]
tail = path[len(legacy_root) :]
query = request.META.get("QUERY_STRING", "") if hasattr(request, "META") else ""
target = react_root + tail + (("?" + query) if query else "")
return {"visible": True, "target": target, "react_root": react_root}
Loading
Loading