Skip to content

Commit 30a5d2d

Browse files
MartinCastroAlvarezmartin-castro-laminr-aiclaude
authored
docs(readme)+chore(deps): PyPI page polish + Django 6 support (#75)
README rework, scoped to the PyPI consumer's needs: - Drop "Option B — install from source" (contributor-only, confused the install path). - Drop "Documentation map" (those links are on GitHub once the user is in the repo; they're not what a `pip install` consumer needs). - Drop "Developer scripts" (lint.sh / build.sh / deploy.sh — contributor-only). - Convert relative `docs/screenshots/*.png` image refs to absolute `https://raw.githubusercontent.com/.../main/...` URLs so PyPI's renderer can resolve them. - Expand "Extend without writing React" from 5 ModelAdmin knobs to ~10 with concrete examples: list_display, sortable_by, search_fields, ordering, exclude/readonly_fields, fieldsets, has_*_permission (with row-level), get_queryset, save_model, custom AdminSite + DJANGO_ADMIN_REACT['ADMIN_SITE'] pointer. - New "What's not yet supported (tracked)" table mapping each unsupported feature to its tracker Issue (#54..#65) with a practical workaround per row. - Convert internal doc links (CONTRIBUTING.md, SECURITY.md, LICENSE) to absolute github.com URLs so they survive on the PyPI page. - Update the Security section to point at GitHub's Private Vulnerability Reporting (drops the `security@<TO-BE-CONFIGURED>` placeholder for end-users). pyproject.toml: - Bump Django range from `>=5.0,<6.0` to `>=5.0,<7.0` so 6.x releases install cleanly. Comment documents the bump policy. - Add `Framework :: Django :: 6.0` classifier. - Add `Programming Language :: Python :: 3.13` classifier. PyPI rendering caveat: the image URLs only resolve once the repo is public; the new PyPI release page only updates when a new version (0.1.0a2) is published. README + classifier changes are the prerequisite; the public flip + new release are the trigger. Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 66a0165 commit 30a5d2d

2 files changed

Lines changed: 202 additions & 95 deletions

File tree

README.md

Lines changed: 197 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ A drop-in **React single-page admin** for any Django 5+ project. Same
44
`pip install`, same `INSTALLED_APPS`, same `urls.py include()` — and
55
your `ModelAdmin` classes drive everything. No React code on your side.
66

7-
> **Pre-alpha (`0.1.0a1`).** Available on PyPI as an alpha; install
8-
> from source for the latest. Track progress on the
7+
> **Pre-alpha.** Available on PyPI as an alpha. Pin tightly; expect
8+
> breaking changes between alpha releases. Track progress on the
99
> [Project board](https://github.com/users/MartinCastroAlvarez/projects/3)
1010
> and the [Issues list](https://github.com/MartinCastroAlvarez/django-admin-react/issues).
1111
1212
---
1313

14-
## Install in your Django project
15-
16-
### Option A — once on PyPI (planned)
14+
## Install
1715

1816
```bash
1917
pip install django-admin-react
@@ -51,57 +49,28 @@ Tailwind-styled SPA driven by your existing `ModelAdmin` classes.
5149
The wheel ships the **pre-built React bundle**. You do **not** need
5250
Node, pnpm, or any frontend toolchain to install or run.
5351

54-
### Option B — install from source (today)
55-
56-
```bash
57-
git clone https://github.com/MartinCastroAlvarez/django-admin-react.git
58-
cd django-admin-react
59-
./scripts/build.sh # builds the SPA + Python wheel
60-
pip install dist/*.whl # install into your venv
61-
```
62-
63-
Then wire it into your project as in Option A.
64-
6552
### Optional configuration
6653

6754
All settings are optional. Defaults shown:
6855

6956
```python
7057
DJANGO_ADMIN_REACT = {
71-
"ADMIN_SITE": "django.contrib.admin.site", # dotted path
58+
"ADMIN_SITE": "django.contrib.admin.site", # dotted path to AdminSite instance
7259
"DEFAULT_PAGE_SIZE": 25,
7360
"MAX_PAGE_SIZE": 200,
7461
"ENABLE_PROFILING": False,
7562
}
7663
```
7764

78-
---
79-
80-
## Extend without writing React
81-
82-
Just edit your `ModelAdmin` — the UI follows. No JS required.
83-
84-
```python
85-
# yourapp/admin.py
86-
from django.contrib import admin
87-
from .models import Invoice
88-
89-
@admin.register(Invoice)
90-
class InvoiceAdmin(admin.ModelAdmin):
91-
list_display = ("number", "customer", "total", "issued_at")
92-
search_fields = ("number", "customer__name")
93-
readonly_fields = ("total",) # ← UI hides the input
94-
list_filter = ("status",) # ← UI surfaces the filter
65+
### Requirements
9566

96-
def has_add_permission(self, request):
97-
return request.user.has_perm("billing.create_invoice")
98-
#
99-
# UI hides the Add button automatically when this returns False.
100-
```
101-
102-
Permissions, querysets, search, validation — all your `ModelAdmin`'s
103-
answers, surfaced verbatim in the React UI. The package never invents
104-
its own permission model.
67+
- **Python**: 3.10+
68+
- **Django**: 5.0, 5.1, 5.2, 6.0 (and any later 6.x)
69+
- **Database**: anything Django supports — the package is ORM-only, no
70+
direct SQL.
71+
- **Auth**: Django's built-in session + CSRF. Works with custom
72+
`AUTH_USER_MODEL`, custom `AUTHENTICATION_BACKENDS`, and custom
73+
`AdminSite.has_permission`.
10574

10675
---
10776

@@ -112,30 +81,196 @@ its own permission model.
11281
> shell is in flight; until then, the images below show the
11382
> **legacy HTML admin** running against the example apps — i.e.,
11483
> the experience `django-admin-react` modernises. Once the SPA
115-
> renders, this section regenerates from the same script. Tracked
116-
> on the [Project board](https://github.com/users/MartinCastroAlvarez/projects/3).
84+
> renders, this section regenerates from the same script.
11785
11886
| Login (the entry door) | Admin index (legacy) |
11987
| ------------------------------------------------- | ------------------------------------------------------- |
120-
| ![Login](docs/screenshots/01-admin-login.png) | ![Admin index](docs/screenshots/02-admin-index.png) |
88+
| ![Login](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/01-admin-login.png) | ![Admin index](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/02-admin-index.png) |
12189

12290
| Library / Authors — list view | Library / Author — detail view |
12391
| -------------------------------------------------------------- | --------------------------------------------------------------- |
124-
| ![Author list](docs/screenshots/03-admin-library-list.png) | ![Author detail](docs/screenshots/05-admin-library-detail.png) |
92+
| ![Author list](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/03-admin-library-list.png) | ![Author detail](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/05-admin-library-detail.png) |
12593

12694
| Mobile (375 px) | API: `GET /api/v1/registry/` JSON |
12795
| --------------------------------------------------------------------- | ------------------------------------------------------- |
128-
| ![Mobile list](docs/screenshots/04-admin-library-list-mobile.png) | ![Registry JSON](docs/screenshots/06-registry-api-json.png) |
96+
| ![Mobile list](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/04-admin-library-list-mobile.png) | ![Registry JSON](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/06-registry-api-json.png) |
12997

13098
Every screenshot uses a deterministic synthetic seed (no real
131-
people, accounts, or PII). Regenerate with:
99+
people, accounts, or PII).
132100

133-
```bash
134-
bash scripts/screenshots.sh
101+
---
102+
103+
## Extend without writing React
104+
105+
Everything below is **just `ModelAdmin`**. No JavaScript. No new
106+
classes. The UI follows whatever your admin declares.
107+
108+
### Pick what columns appear on the list view
109+
110+
```python
111+
@admin.register(Invoice)
112+
class InvoiceAdmin(admin.ModelAdmin):
113+
list_display = ("number", "customer", "status", "total", "issued_at")
114+
```
115+
116+
### Make columns sortable
117+
118+
```python
119+
class InvoiceAdmin(admin.ModelAdmin):
120+
list_display = ("number", "customer", "status", "total", "issued_at")
121+
sortable_by = ("issued_at", "total") # everything else is fixed
122+
```
123+
124+
### Add free-text search
125+
126+
```python
127+
class InvoiceAdmin(admin.ModelAdmin):
128+
search_fields = ("number", "customer__name", "notes__icontains")
129+
# The SPA wires `?q=<term>` to `ModelAdmin.get_search_results` verbatim.
135130
```
136131

137-
The script boots a one-off SQLite DB, seeds fixtures, captures via
138-
Playwright (Chromium), and writes the six PNGs above.
132+
### Default ordering
133+
134+
```python
135+
class InvoiceAdmin(admin.ModelAdmin):
136+
ordering = ("-issued_at",)
137+
```
138+
139+
### Hide a field from the form
140+
141+
```python
142+
class InvoiceAdmin(admin.ModelAdmin):
143+
exclude = ("internal_audit_hash",) # never reaches the SPA
144+
readonly_fields = ("total",) # rendered as read-only
145+
```
146+
147+
The SPA respects `exclude` and `readonly_fields` exactly the way the
148+
legacy admin does. Sensitive-named fields (`password`, `secret`,
149+
`token`, `api_key`, `hash`, `private_key`, `session`, `nonce`, `salt`)
150+
are filtered on top of those rules as defense-in-depth.
151+
152+
### Group fields into sections
153+
154+
```python
155+
class InvoiceAdmin(admin.ModelAdmin):
156+
fieldsets = (
157+
("Identity", {"fields": ("number", "customer")}),
158+
("Money", {"fields": ("subtotal", "tax", "total")}),
159+
("Lifecycle", {"fields": ("status", "issued_at", "paid_at")}),
160+
("Internal", {"fields": ("notes",), "classes": ("collapse",)}),
161+
)
162+
```
163+
164+
### Per-row permission gating
165+
166+
```python
167+
class InvoiceAdmin(admin.ModelAdmin):
168+
def has_add_permission(self, request):
169+
return request.user.has_perm("billing.create_invoice")
170+
171+
def has_change_permission(self, request, obj=None):
172+
if obj is None:
173+
return request.user.has_perm("billing.change_invoice")
174+
return obj.owner_id == request.user.id # row-level rule
175+
176+
def has_delete_permission(self, request, obj=None):
177+
return False # nobody deletes invoices
178+
179+
def has_view_permission(self, request, obj=None):
180+
return request.user.has_perm("billing.view_invoice")
181+
```
182+
183+
The SPA hides the **Add** / **Save** / **Delete** buttons automatically
184+
based on these. UI never invents a permission; it asks `ModelAdmin`.
185+
186+
### Restrict the queryset
187+
188+
```python
189+
class InvoiceAdmin(admin.ModelAdmin):
190+
def get_queryset(self, request):
191+
qs = super().get_queryset(request)
192+
if request.user.is_superuser:
193+
return qs
194+
return qs.filter(owner=request.user)
195+
```
196+
197+
The list view never sees rows the queryset excludes. **No
198+
`Model.objects.all()` in the package** — every list, search, and
199+
detail lookup starts at `ModelAdmin.get_queryset(request)`.
200+
201+
### Custom save hook
202+
203+
```python
204+
class InvoiceAdmin(admin.ModelAdmin):
205+
def save_model(self, request, obj, form, change):
206+
obj.last_edited_by = request.user
207+
super().save_model(request, obj, form, change)
208+
```
209+
210+
Writes always go through `ModelAdmin.get_form()``form.is_valid()`
211+
`save_model()`. Signals, audit logs, and post-save hooks all fire
212+
exactly like they do in `/admin/`.
213+
214+
### Use a custom `AdminSite`
215+
216+
```python
217+
# myproject/admin.py
218+
from django.contrib.admin import AdminSite
219+
220+
class StaffAdminSite(AdminSite):
221+
site_header = "Operations Console"
222+
site_title = "Ops"
223+
index_title = "Welcome"
224+
225+
def has_permission(self, request):
226+
return request.user.is_active and request.user.is_staff and \
227+
request.user.groups.filter(name="ops").exists()
228+
229+
staff_admin = StaffAdminSite(name="staff")
230+
231+
# myproject/settings.py
232+
DJANGO_ADMIN_REACT = {
233+
"ADMIN_SITE": "myproject.admin.staff_admin",
234+
}
235+
```
236+
237+
The SPA inherits the custom site's permission gate and the
238+
`ModelAdmin` registrations on that site — no parallel registry.
239+
240+
### Pre-built form / queryset overrides still work
241+
242+
`get_form`, `get_fieldsets`, `get_fields`, `get_exclude`,
243+
`get_readonly_fields`, `get_search_results`, `get_list_display`,
244+
`get_sortable_by` — all of them are called by the SPA the same way the
245+
HTML admin calls them. If you customised them for `/admin/`, the SPA
246+
already honours those customisations.
247+
248+
---
249+
250+
## What's not yet supported (tracked)
251+
252+
The pre-alpha intentionally ships a small backend. The following
253+
`ModelAdmin` features are on the roadmap but not in the alpha — open
254+
issues with acceptance signals:
255+
256+
| Feature | Issue | Workaround for now |
257+
|---|---|---|
258+
| `inlines` (`TabularInline` / `StackedInline`) | [#54](https://github.com/MartinCastroAlvarez/django-admin-react/issues/54) | Edit the child model directly under its own admin URL |
259+
| `ManyToManyField` read + write | [#55](https://github.com/MartinCastroAlvarez/django-admin-react/issues/55) | Exclude the field; manage relation through a dedicated admin |
260+
| `list_filter` (the left sidebar) | [#56](https://github.com/MartinCastroAlvarez/django-admin-react/issues/56) | Use `search_fields` for the same intent |
261+
| `FileField` / `ImageField` upload | [#57](https://github.com/MartinCastroAlvarez/django-admin-react/issues/57) | Use the legacy admin for these models |
262+
| `ModelAdmin.actions` + bulk select | [#58](https://github.com/MartinCastroAlvarez/django-admin-react/issues/58) | One-off DRF endpoint or management command |
263+
| `autocomplete_fields` / `raw_id_fields` | [#59](https://github.com/MartinCastroAlvarez/django-admin-react/issues/59) | Keep FK target tables small for now |
264+
| `JSONField` / `ArrayField` / range types (and `register_field_type` hook) | [#60](https://github.com/MartinCastroAlvarez/django-admin-react/issues/60) | Mark as `readonly`; edit via shell |
265+
| `list_editable` + bulk PATCH | [#61](https://github.com/MartinCastroAlvarez/django-admin-react/issues/61) | Edit row-by-row |
266+
| `date_hierarchy` drill-down | [#62](https://github.com/MartinCastroAlvarez/django-admin-react/issues/62) | `?ordering=-date_field` and scroll |
267+
| Session-expiry / re-login modal | [#63](https://github.com/MartinCastroAlvarez/django-admin-react/issues/63) | Browser refresh on 403 |
268+
| OpenAPI / JSON Schema endpoint | [#64](https://github.com/MartinCastroAlvarez/django-admin-react/issues/64) | Hand-maintain types from `docs/api-contract.md` |
269+
| Per-model React extension points | [#65](https://github.com/MartinCastroAlvarez/django-admin-react/issues/65) | Use the legacy admin for model-specific UIs |
270+
271+
Mount the SPA at a **second path** (`path("admin2/", include("django_admin_react.urls"))`)
272+
alongside `/admin/` while the gaps close. Both run off the same
273+
`ModelAdmin` registrations.
139274

140275
---
141276

@@ -150,59 +285,27 @@ Playwright (Chromium), and writes the six PNGs above.
150285
- **Configurable URL prefix**`/admin/`, `/admin-react/`, anywhere.
151286
- **Conservative & secure-by-default** — never exposes models the
152287
admin doesn't already expose; never writes fields the admin form
153-
excludes; CSRF on every unsafe method.
288+
excludes; CSRF on every unsafe method; `Cache-Control: no-store`
289+
on every API response; sensitive-name denylist on top of the
290+
admin's own `exclude` rules.
154291
- **Boring + auditable** — no parallel permission system, no
155292
client-side workarounds for backend permissions, conservative
156293
serializer with `str()` fallback.
157294

158295
---
159296

160-
## Documentation map
161-
162-
| Doc | Topic |
163-
| ---------------------------------------------------------------- | -------------------------------------------- |
164-
| [Project board](https://github.com/users/MartinCastroAlvarez/projects/3) | Live status of in-flight / planned work — priority, area, phase |
165-
| [Issues](https://github.com/MartinCastroAlvarez/django-admin-react/issues) | Backlog with acceptance criteria |
166-
| [Discussions](https://github.com/MartinCastroAlvarez/django-admin-react/discussions) | Announcements, Q&A, ideas, show-and-tell |
167-
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Design contract |
168-
| [`SECURITY.md`](SECURITY.md) | Threat model, guarantees, required tests |
169-
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Dev workflow |
170-
| [`CLAUDE.md`](CLAUDE.md) | Rules for AI contributors |
171-
| [`docs/api-contract.md`](docs/api-contract.md) | Full API spec |
172-
| [`docs/data-layer.md`](docs/data-layer.md) | `@dar/data` design (SWR + debounce) |
173-
| [`docs/agents/pr-workflow.md`](docs/agents/pr-workflow.md) | How agents send / review / merge PRs |
174-
| [`docs/agents/autonomy-policy.md`](docs/agents/autonomy-policy.md) | What's auto-mergeable vs human-only |
175-
| [`docs/agents/decisions.md`](docs/agents/decisions.md) | Decisions log |
176-
| [`scripts/README.md`](scripts/README.md) | `lint.sh` / `build.sh` / `deploy.sh` |
177-
| [`examples/README.md`](examples/README.md) | Demo Django apps |
178-
179-
---
180-
181-
## Developer scripts
182-
183-
```bash
184-
./scripts/lint.sh # ruff + black + isort + flake8 + pylint + mypy + bandit + prettier + tsc
185-
./scripts/build.sh # pnpm install + vite build + poetry build (wheel ships pre-built SPA)
186-
./scripts/deploy.sh # poetry publish to PyPI (requires POETRY_PYPI_TOKEN_PYPI)
187-
```
188-
189-
The Merger runs `./scripts/lint.sh` before every merge — there is
190-
no CI in this repo by design.
191-
192-
---
193-
194297
## License
195298

196-
MIT — see [`LICENSE`](LICENSE).
299+
MIT — see [`LICENSE`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/LICENSE).
197300

198301
## Security
199302

200-
Please report security issues privately. See
201-
[`SECURITY.md`](SECURITY.md) §4. Do **not** open a public issue.
303+
Please report security issues privately through GitHub's Private
304+
Vulnerability Reporting on the repository (Security → Advisories).
305+
See [`SECURITY.md`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/SECURITY.md).
306+
Do **not** open a public issue.
202307

203308
## Contributing
204309

205310
Humans and AI agents both welcome. Start with
206-
[`CONTRIBUTING.md`](CONTRIBUTING.md). AI agents must also read
207-
[`CLAUDE.md`](CLAUDE.md), [`docs/agents/pr-workflow.md`](docs/agents/pr-workflow.md),
208-
and [`docs/agents/autonomy-policy.md`](docs/agents/autonomy-policy.md).
311+
[`CONTRIBUTING.md`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/CONTRIBUTING.md).

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ classifiers = [
1616
"Framework :: Django :: 5.0",
1717
"Framework :: Django :: 5.1",
1818
"Framework :: Django :: 5.2",
19+
"Framework :: Django :: 6.0",
1920
"Intended Audience :: Developers",
2021
"License :: OSI Approved :: MIT License",
2122
"Operating System :: OS Independent",
2223
"Programming Language :: Python :: 3",
2324
"Programming Language :: Python :: 3.10",
2425
"Programming Language :: Python :: 3.11",
2526
"Programming Language :: Python :: 3.12",
27+
"Programming Language :: Python :: 3.13",
2628
"Topic :: Internet :: WWW/HTTP :: Site Management",
2729
"Topic :: Software Development :: Libraries :: Python Modules",
2830
]
@@ -37,7 +39,9 @@ include = [
3739

3840
[tool.poetry.dependencies]
3941
python = "^3.10"
40-
django = ">=5.0,<6.0"
42+
# Django 5.0 → 5.2 LTS → 6.x. Bump the upper bound when 7.0 ships and
43+
# we've verified the package still passes its test matrix on it.
44+
django = ">=5.0,<7.0"
4145

4246
[tool.poetry.group.dev.dependencies]
4347
pytest = "^8.0"

0 commit comments

Comments
 (0)