django-admin-react 0.1.0a2__tar.gz → 0.2.0a2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_admin_react-0.2.0a2/PKG-INFO +534 -0
- django_admin_react-0.2.0a2/README.md +502 -0
- django_admin_react-0.2.0a2/django_admin_react/api/filters.py +320 -0
- django_admin_react-0.2.0a2/django_admin_react/api/inlines.py +252 -0
- django_admin_react-0.2.0a2/django_admin_react/api/inlines_write.py +226 -0
- django_admin_react-0.2.0a2/django_admin_react/api/panels.py +113 -0
- django_admin_react-0.2.0a2/django_admin_react/api/permissions.py +132 -0
- django_admin_react-0.2.0a2/django_admin_react/api/registry.py +354 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/serializers.py +113 -6
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/urls.py +62 -0
- django_admin_react-0.2.0a2/django_admin_react/api/views/actions.py +153 -0
- django_admin_react-0.2.0a2/django_admin_react/api/views/auth.py +192 -0
- django_admin_react-0.2.0a2/django_admin_react/api/views/autocomplete.py +166 -0
- django_admin_react-0.2.0a2/django_admin_react/api/views/bulk.py +215 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/create.py +4 -2
- django_admin_react-0.2.0a2/django_admin_react/api/views/delete_preview.py +107 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/destroy.py +6 -2
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/detail.py +63 -3
- django_admin_react-0.2.0a2/django_admin_react/api/views/history.py +164 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/list.py +39 -11
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/registry.py +1 -1
- django_admin_react-0.2.0a2/django_admin_react/api/views/schema.py +484 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/update.py +57 -7
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/writes.py +140 -27
- django_admin_react-0.2.0a2/django_admin_react/audit.py +42 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/conf.py +18 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/static/admin_react/.vite/manifest.json +2 -2
- django_admin_react-0.2.0a2/django_admin_react/static/admin_react/assets/index-BE3CZdBI.js +9 -0
- django_admin_react-0.2.0a2/django_admin_react/static/admin_react/assets/index-BE3CZdBI.js.map +1 -0
- django_admin_react-0.2.0a2/django_admin_react/static/admin_react/assets/index-xZLX3uph.css +1 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/static/admin_react/index.html +2 -2
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/templates/admin_react/index.html +8 -2
- django_admin_react-0.2.0a2/django_admin_react/templates/admin_react/login.html +76 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/urls.py +11 -2
- django_admin_react-0.2.0a2/django_admin_react/views.py +302 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/pyproject.toml +16 -3
- django_admin_react-0.1.0a2/PKG-INFO +0 -323
- django_admin_react-0.1.0a2/README.md +0 -291
- django_admin_react-0.1.0a2/django_admin_react/api/permissions.py +0 -80
- django_admin_react-0.1.0a2/django_admin_react/api/registry.py +0 -200
- django_admin_react-0.1.0a2/django_admin_react/static/admin_react/assets/index-CKxeWYBA.css +0 -1
- django_admin_react-0.1.0a2/django_admin_react/static/admin_react/assets/index-itk7hrnq.js +0 -68
- django_admin_react-0.1.0a2/django_admin_react/static/admin_react/assets/index-itk7hrnq.js.map +0 -1
- django_admin_react-0.1.0a2/django_admin_react/views.py +0 -136
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/LICENSE +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/README.md +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/__init__.py +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/README.md +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/__init__.py +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/dates.py +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/README.md +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/__init__.py +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/apps.py +0 -0
- {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/templates/admin_react/README.md +0 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-admin-react
|
|
3
|
+
Version: 0.2.0a2
|
|
4
|
+
Summary: A drop-in React single-page admin for Django, driven entirely by ModelAdmin.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: django,admin,react,spa,tailwind
|
|
7
|
+
Author: django-admin-react contributors
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 5.0
|
|
13
|
+
Classifier: Framework :: Django :: 5.1
|
|
14
|
+
Classifier: Framework :: Django :: 5.2
|
|
15
|
+
Classifier: Framework :: Django :: 6.0
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Dist: django (>=5.0,<7.0)
|
|
27
|
+
Project-URL: Documentation, https://github.com/MartinCastroAlvarez/django-admin-react#readme
|
|
28
|
+
Project-URL: Homepage, https://github.com/MartinCastroAlvarez/django-admin-react
|
|
29
|
+
Project-URL: Repository, https://github.com/MartinCastroAlvarez/django-admin-react
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# django-admin-react
|
|
33
|
+
|
|
34
|
+
A drop-in **React single-page admin** for any Django 5+ project. Same
|
|
35
|
+
`pip install`, same `INSTALLED_APPS`, same `urls.py include()` — and
|
|
36
|
+
your `ModelAdmin` classes drive everything. No React code on your side.
|
|
37
|
+
|
|
38
|
+
> **Pre-alpha.** Available on PyPI as an alpha. Pin tightly; expect
|
|
39
|
+
> breaking changes between alpha releases. Track progress on the
|
|
40
|
+
> [Project board](https://github.com/users/MartinCastroAlvarez/projects/3)
|
|
41
|
+
> and the [Issues list](https://github.com/MartinCastroAlvarez/django-admin-react/issues).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Why django-admin-react
|
|
46
|
+
|
|
47
|
+
The Django admin is a 20-year-old hypertext app: full-page reloads,
|
|
48
|
+
mid-2000s aesthetics, no real mobile support, no client-side state.
|
|
49
|
+
It is also the most powerful piece of Django: `ModelAdmin` already
|
|
50
|
+
encodes your permissions, querysets, forms, fieldsets, search,
|
|
51
|
+
ordering, and inlines.
|
|
52
|
+
|
|
53
|
+
`django-admin-react` keeps every line of `ModelAdmin` you already
|
|
54
|
+
have and replaces only the UI:
|
|
55
|
+
|
|
56
|
+
| What you write | What the React SPA does with it |
|
|
57
|
+
| ------------------------------------ | ---------------------------------------------------------------------------- |
|
|
58
|
+
| `list_display` | Renders columns in a virtualised, sortable, mobile-collapsing table. |
|
|
59
|
+
| `search_fields` | Renders a search bar that hits `get_search_results` verbatim. |
|
|
60
|
+
| `list_filter` | Renders a sidebar drawer (desktop) / bottom-sheet (mobile) + filter chips. |
|
|
61
|
+
| `date_hierarchy` | Renders a year → month → day drill-down strip. |
|
|
62
|
+
| `list_editable` / `list_per_page` | Renders inline-editable cells + paginated list with deep links. |
|
|
63
|
+
| `actions` | Renders a bulk-actions menu wired to the same `ModelAdmin.actions`. |
|
|
64
|
+
| `fieldsets` / `readonly_fields` | Renders the detail form respecting groups + read-only rules. |
|
|
65
|
+
| `autocomplete_fields` | Renders type-ahead pickers that hit `<model>/autocomplete/?q=…`. |
|
|
66
|
+
| `inlines = [TabularInline, ...]` | Renders inlines as tables / card stacks alongside the parent. |
|
|
67
|
+
| `has_*_permission` | Hides Add / Save / Delete buttons accordingly; never invents a permission. |
|
|
68
|
+
| `get_queryset(request)` | Every list, search, and detail lookup starts here. Never `Model.objects.all()`. |
|
|
69
|
+
|
|
70
|
+
The SPA is **metadata-driven** — it learns your models, fields, and
|
|
71
|
+
permissions at runtime from `GET /api/v1/registry/`. Add a new
|
|
72
|
+
`ModelAdmin` and refresh; no rebuild, no codegen.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Screenshots
|
|
77
|
+
|
|
78
|
+
Real captures of the **django-admin-react SPA** rendering the bundled
|
|
79
|
+
`examples/` apps — driven entirely by each app's `ModelAdmin`.
|
|
80
|
+
Regenerate any time with `scripts/screenshots.sh` (Playwright against a
|
|
81
|
+
throwaway example server).
|
|
82
|
+
|
|
83
|
+
| Sign in (package login) | Registry / home |
|
|
84
|
+
| -------------------------------------------------- | ----------------------------------------------------- |
|
|
85
|
+
|  |  |
|
|
86
|
+
|
|
87
|
+
| List view (`list_display` + search) | Detail view |
|
|
88
|
+
| ------------------------------------------------------- | ---------------------------------------------------- |
|
|
89
|
+
|  |  |
|
|
90
|
+
|
|
91
|
+
| Mobile (375 px) | API: `GET /api/v1/registry/` |
|
|
92
|
+
| ---------------------------------------------------------- | ---------------------------------------------------------- |
|
|
93
|
+
|  |  |
|
|
94
|
+
|
|
95
|
+
Screenshots use deterministic synthetic fixtures (no real names,
|
|
96
|
+
emails, account numbers, or PII).
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Install
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install django-admin-react
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# settings.py
|
|
108
|
+
INSTALLED_APPS = [
|
|
109
|
+
"django.contrib.admin",
|
|
110
|
+
"django.contrib.auth",
|
|
111
|
+
"django.contrib.contenttypes",
|
|
112
|
+
"django.contrib.sessions",
|
|
113
|
+
"django.contrib.messages",
|
|
114
|
+
"django.contrib.staticfiles",
|
|
115
|
+
"django_admin_react", # ← add this
|
|
116
|
+
# ... your own apps
|
|
117
|
+
]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# urls.py
|
|
122
|
+
from django.urls import include, path
|
|
123
|
+
|
|
124
|
+
urlpatterns = [
|
|
125
|
+
path("admin/", include("django_admin_react.urls")),
|
|
126
|
+
# any prefix is fine:
|
|
127
|
+
# path("admin-react/", include("django_admin_react.urls")),
|
|
128
|
+
# path("staff/", include("django_admin_react.urls")),
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
That is the entire integration. Log in as a staff user → modern,
|
|
133
|
+
Tailwind-styled SPA driven by your existing `ModelAdmin` classes.
|
|
134
|
+
|
|
135
|
+
The wheel ships the **pre-built React bundle**. You do **not** need
|
|
136
|
+
Node, pnpm, or any frontend toolchain to install or run.
|
|
137
|
+
|
|
138
|
+
### Optional configuration
|
|
139
|
+
|
|
140
|
+
All settings are optional. Defaults shown:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
DJANGO_ADMIN_REACT = {
|
|
144
|
+
"ADMIN_SITE": "django.contrib.admin.site", # dotted path to AdminSite instance
|
|
145
|
+
"DEFAULT_PAGE_SIZE": 25,
|
|
146
|
+
"MAX_PAGE_SIZE": 200,
|
|
147
|
+
"ENABLE_PROFILING": False,
|
|
148
|
+
|
|
149
|
+
# Branding — rendered server-side into the SPA shell, so the
|
|
150
|
+
# consumer's title + favicon are present on first paint (no FOUC).
|
|
151
|
+
"BRAND_TITLE": None, # str | None — sidebar header + browser tab.
|
|
152
|
+
"BRAND_LOGO_URL": None, # str | None — used as the favicon and
|
|
153
|
+
# the sidebar logo. Absolute URL or a
|
|
154
|
+
# path under your STATIC_URL.
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Branding (`BRAND_TITLE` + `BRAND_LOGO_URL`)
|
|
159
|
+
|
|
160
|
+
Both default to `None`. Resolution order for the title:
|
|
161
|
+
|
|
162
|
+
1. `DJANGO_ADMIN_REACT["BRAND_TITLE"]` — explicit override.
|
|
163
|
+
2. `<your AdminSite>.site_header` — if you already set `site_header`
|
|
164
|
+
on a custom `AdminSite`, the SPA reuses it automatically. No need
|
|
165
|
+
to repeat yourself.
|
|
166
|
+
3. `"Django Admin"` — last-resort fallback.
|
|
167
|
+
|
|
168
|
+
`BRAND_LOGO_URL` accepts either an absolute URL or a path the browser
|
|
169
|
+
can resolve under your `STATIC_URL`. It is used both as the favicon
|
|
170
|
+
(`<link rel="icon">` in the SPA shell) and as the small logo next to
|
|
171
|
+
the brand title in the sidebar.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# settings.py
|
|
175
|
+
DJANGO_ADMIN_REACT = {
|
|
176
|
+
"BRAND_TITLE": "Acme",
|
|
177
|
+
"BRAND_LOGO_URL": "/static/acme/logo.svg",
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Both values are written into the SPA index template as standard
|
|
182
|
+
`<meta>` tags (`dar-brand-title`, `dar-brand-logo`); the React shell
|
|
183
|
+
reads them at boot, so the first paint already carries the consumer's
|
|
184
|
+
brand. No flash of the package's defaults.
|
|
185
|
+
|
|
186
|
+
### Requirements
|
|
187
|
+
|
|
188
|
+
- **Python**: 3.10+
|
|
189
|
+
- **Django**: 5.0, 5.1, 5.2, 6.0 (and any later 6.x)
|
|
190
|
+
- **Database**: anything Django supports — the package is ORM-only,
|
|
191
|
+
no direct SQL.
|
|
192
|
+
- **Auth**: Django's built-in session + CSRF. Works with custom
|
|
193
|
+
`AUTH_USER_MODEL`, custom `AUTHENTICATION_BACKENDS`, and custom
|
|
194
|
+
`AdminSite.has_permission`.
|
|
195
|
+
|
|
196
|
+
### Running side-by-side with the legacy admin
|
|
197
|
+
|
|
198
|
+
A common rollout: keep `/admin/` on the legacy HTML admin, mount the
|
|
199
|
+
React SPA at `/admin-react/`, and migrate users at your own pace.
|
|
200
|
+
Both run off the same `ModelAdmin` registrations — there is no
|
|
201
|
+
duplicate state.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
urlpatterns = [
|
|
205
|
+
path("admin/", admin.site.urls), # legacy, unchanged
|
|
206
|
+
path("admin-react/", include("django_admin_react.urls")), # SPA
|
|
207
|
+
]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Extend without writing React
|
|
213
|
+
|
|
214
|
+
Everything below is **just `ModelAdmin`**. No JavaScript. No new
|
|
215
|
+
classes. The UI follows whatever your admin declares.
|
|
216
|
+
|
|
217
|
+
### Pick what columns appear on the list view
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
@admin.register(Invoice)
|
|
221
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
222
|
+
list_display = ("number", "customer", "status", "total", "issued_at")
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Make columns sortable
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
229
|
+
list_display = ("number", "customer", "status", "total", "issued_at")
|
|
230
|
+
sortable_by = ("issued_at", "total") # everything else is fixed
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Add free-text search
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
237
|
+
search_fields = ("number", "customer__name", "notes__icontains")
|
|
238
|
+
# The SPA wires `?q=<term>` to `ModelAdmin.get_search_results` verbatim.
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Default ordering
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
245
|
+
ordering = ("-issued_at",)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Hide a field from the form
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
252
|
+
exclude = ("internal_audit_hash",) # never reaches the SPA
|
|
253
|
+
readonly_fields = ("total",) # rendered as read-only
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The SPA respects `exclude` and `readonly_fields` exactly the way the
|
|
257
|
+
legacy admin does. Sensitive-named fields (`password`, `secret`,
|
|
258
|
+
`token`, `api_key`, `hash`, `private_key`, `session`, `nonce`, `salt`)
|
|
259
|
+
are filtered on top of those rules as defense-in-depth.
|
|
260
|
+
|
|
261
|
+
### Group fields into sections
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
265
|
+
fieldsets = (
|
|
266
|
+
("Identity", {"fields": ("number", "customer")}),
|
|
267
|
+
("Money", {"fields": ("subtotal", "tax", "total")}),
|
|
268
|
+
("Lifecycle", {"fields": ("status", "issued_at", "paid_at")}),
|
|
269
|
+
("Internal", {"fields": ("notes",), "classes": ("collapse",)}),
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Surface filters in the sidebar
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
277
|
+
list_filter = ("status", "issued_at", "customer")
|
|
278
|
+
# Boolean / choices / FK / date / SimpleListFilter all supported.
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Drill down by date
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
285
|
+
date_hierarchy = "issued_at"
|
|
286
|
+
# SPA renders a year → month → day strip wired to ?year=&month=&day=
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Edit cells inline on the list view
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
293
|
+
list_editable = ("status",)
|
|
294
|
+
# SPA: click cell → input swap → blur/Enter saves via PATCH /<app>/<model>/bulk/
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Add custom admin actions
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
301
|
+
actions = ["mark_paid"]
|
|
302
|
+
|
|
303
|
+
@admin.action(description="Mark selected as paid")
|
|
304
|
+
def mark_paid(self, request, queryset):
|
|
305
|
+
queryset.update(status="paid", paid_at=timezone.now())
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The SPA renders a bulk-actions menu and posts to the same
|
|
309
|
+
`ModelAdmin.actions` machinery — same signatures, same audit
|
|
310
|
+
trail.
|
|
311
|
+
|
|
312
|
+
### Per-row permission gating
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
316
|
+
def has_add_permission(self, request):
|
|
317
|
+
return request.user.has_perm("billing.create_invoice")
|
|
318
|
+
|
|
319
|
+
def has_change_permission(self, request, obj=None):
|
|
320
|
+
if obj is None:
|
|
321
|
+
return request.user.has_perm("billing.change_invoice")
|
|
322
|
+
return obj.owner_id == request.user.id # row-level rule
|
|
323
|
+
|
|
324
|
+
def has_delete_permission(self, request, obj=None):
|
|
325
|
+
return False # nobody deletes invoices
|
|
326
|
+
|
|
327
|
+
def has_view_permission(self, request, obj=None):
|
|
328
|
+
return request.user.has_perm("billing.view_invoice")
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The SPA hides the **Add** / **Save** / **Delete** buttons automatically
|
|
332
|
+
based on these. UI never invents a permission; it asks `ModelAdmin`.
|
|
333
|
+
|
|
334
|
+
### Restrict the queryset
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
338
|
+
def get_queryset(self, request):
|
|
339
|
+
qs = super().get_queryset(request)
|
|
340
|
+
if request.user.is_superuser:
|
|
341
|
+
return qs
|
|
342
|
+
return qs.filter(owner=request.user)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The list view never sees rows the queryset excludes. **No
|
|
346
|
+
`Model.objects.all()` in the package** — every list, search, and
|
|
347
|
+
detail lookup starts at `ModelAdmin.get_queryset(request)`.
|
|
348
|
+
|
|
349
|
+
### Custom save hook
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
353
|
+
def save_model(self, request, obj, form, change):
|
|
354
|
+
obj.last_edited_by = request.user
|
|
355
|
+
super().save_model(request, obj, form, change)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Writes always go through `ModelAdmin.get_form()` → `form.is_valid()`
|
|
359
|
+
→ `save_model()`. Signals, audit logs, and post-save hooks all fire
|
|
360
|
+
exactly like they do in `/admin/`.
|
|
361
|
+
|
|
362
|
+
### Use a custom `AdminSite`
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
# myproject/admin.py
|
|
366
|
+
from django.contrib.admin import AdminSite
|
|
367
|
+
|
|
368
|
+
class StaffAdminSite(AdminSite):
|
|
369
|
+
site_header = "Operations Console"
|
|
370
|
+
site_title = "Ops"
|
|
371
|
+
index_title = "Welcome"
|
|
372
|
+
|
|
373
|
+
def has_permission(self, request):
|
|
374
|
+
return request.user.is_active and request.user.is_staff and \
|
|
375
|
+
request.user.groups.filter(name="ops").exists()
|
|
376
|
+
|
|
377
|
+
staff_admin = StaffAdminSite(name="staff")
|
|
378
|
+
|
|
379
|
+
# myproject/settings.py
|
|
380
|
+
DJANGO_ADMIN_REACT = {
|
|
381
|
+
"ADMIN_SITE": "myproject.admin.staff_admin",
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The SPA inherits the custom site's permission gate and the
|
|
386
|
+
`ModelAdmin` registrations on that site — no parallel registry.
|
|
387
|
+
|
|
388
|
+
### Plug in custom field types
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
# yourapp/admin_react.py
|
|
392
|
+
from django_admin_react.api.serializers import register_field_type
|
|
393
|
+
from yourapp.fields import MoneyField
|
|
394
|
+
|
|
395
|
+
register_field_type(MoneyField, vocab_type="decimal")
|
|
396
|
+
# SPA renders MoneyField with the built-in decimal widget; no React
|
|
397
|
+
# code required.
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
For coining a brand-new `vocab_type` (with a matching SPA widget)
|
|
401
|
+
see [`docs/extensions.md`](docs/extensions.md).
|
|
402
|
+
|
|
403
|
+
### Pre-built `get_*` overrides still work
|
|
404
|
+
|
|
405
|
+
`get_form`, `get_fieldsets`, `get_fields`, `get_exclude`,
|
|
406
|
+
`get_readonly_fields`, `get_search_results`, `get_list_display`,
|
|
407
|
+
`get_sortable_by`, `get_list_filter`, `get_actions` — all of them
|
|
408
|
+
are called by the SPA the same way the HTML admin calls them. If
|
|
409
|
+
you customised them for `/admin/`, the SPA already honours those
|
|
410
|
+
customisations.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Feature status (v0.1.0-alpha)
|
|
415
|
+
|
|
416
|
+
| Surface | Status |
|
|
417
|
+
| ------------------------------------------------------ | --------------------------------------------------------------- |
|
|
418
|
+
| Registry / list / detail / create / update / delete | ✅ Backend + SPA contract |
|
|
419
|
+
| `list_display`, `sortable_by`, `search_fields` | ✅ Backend + SPA contract |
|
|
420
|
+
| `list_filter` (boolean / choice / FK / date / Simple) | ✅ Backend; SPA implementation pending |
|
|
421
|
+
| `date_hierarchy` | ✅ Backend; SPA implementation pending |
|
|
422
|
+
| `list_editable` + bulk PATCH | ✅ Backend; SPA implementation pending |
|
|
423
|
+
| `actions` (custom + bulk runner) | ✅ Backend; SPA implementation pending |
|
|
424
|
+
| `autocomplete_fields` / `raw_id_fields` | ✅ Backend + SPA contract |
|
|
425
|
+
| `ManyToManyField` read + write | ✅ Backend; SPA implementation pending |
|
|
426
|
+
| `inlines` (TabularInline / StackedInline) — read | ✅ Backend; SPA implementation pending |
|
|
427
|
+
| `inlines` — write (formsets) | 🟡 Tracked in [#54](https://github.com/MartinCastroAlvarez/django-admin-react/issues/54) |
|
|
428
|
+
| `FileField` / `ImageField` — read | ✅ Backend + SPA contract |
|
|
429
|
+
| `FileField` / `ImageField` — multipart upload | 🟡 Tracked in [#57](https://github.com/MartinCastroAlvarez/django-admin-react/issues/57) |
|
|
430
|
+
| `JSONField` / `ArrayField` / range types | ✅ Backend |
|
|
431
|
+
| `register_field_type` + per-model SPA extension hook | ✅ Backend + extension contract |
|
|
432
|
+
| Session-expiry re-login modal | ✅ Wire contract; SPA implementation pending |
|
|
433
|
+
| OpenAPI 3.1 schema at `/api/v1/schema/` | ✅ Backend |
|
|
434
|
+
| Dark mode (no-flash server-side resolution) | 🟡 UX contract; tracked in [#84](https://github.com/MartinCastroAlvarez/django-admin-react/issues/84) |
|
|
435
|
+
| Mobile creative patterns (FAB / bottom-sheet / swipe) | 🟡 UX contract; tracked in [#85](https://github.com/MartinCastroAlvarez/django-admin-react/issues/85) |
|
|
436
|
+
| PWA (manifest + service worker + cache-on-logout) | 🟡 UX contract; tracked in [#86](https://github.com/MartinCastroAlvarez/django-admin-react/issues/86) |
|
|
437
|
+
|
|
438
|
+
Status meanings: ✅ ships in the current alpha; 🟡 contract or
|
|
439
|
+
backend lands in the alpha, SPA implementation in flight. See
|
|
440
|
+
[`ACCEPTANCE.md`](ACCEPTANCE.md) for the full criterion-by-criterion
|
|
441
|
+
list and [the issue tracker](https://github.com/MartinCastroAlvarez/django-admin-react/issues)
|
|
442
|
+
for live status.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## The API surface
|
|
447
|
+
|
|
448
|
+
The SPA is a thin client over a small, closed REST surface. You can
|
|
449
|
+
also use these endpoints from any HTTP client (curl, your own
|
|
450
|
+
frontend, a script).
|
|
451
|
+
|
|
452
|
+
| Method | Path | Purpose |
|
|
453
|
+
| ------- | ------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
454
|
+
| `GET` | `/api/v1/registry/` | All apps + models the current user can see, with their permissions. |
|
|
455
|
+
| `GET` | `/api/v1/schema/` | OpenAPI 3.1 schema for the envelopes + closed type vocabulary. |
|
|
456
|
+
| `GET` | `/api/v1/<app>/<model>/` | Paginated list. Honours `?search=`, `?ordering=`, `?page=`, `list_filter`. |
|
|
457
|
+
| `POST` | `/api/v1/<app>/<model>/` | Create. Runs `ModelAdmin.get_form()` + `form.is_valid()` + `save_model()`. |
|
|
458
|
+
| `GET` | `/api/v1/<app>/<model>/<pk>/` | Detail with serialised fields, `permissions`, `inlines`, `panels`. |
|
|
459
|
+
| `PATCH` | `/api/v1/<app>/<model>/<pk>/` | Partial update. Same form pipeline as POST. |
|
|
460
|
+
| `DELETE`| `/api/v1/<app>/<model>/<pk>/` | Hard delete via `ModelAdmin.delete_model()`. |
|
|
461
|
+
| `PATCH` | `/api/v1/<app>/<model>/bulk/` | `list_editable` round-trip for multiple rows. |
|
|
462
|
+
| `POST` | `/api/v1/<app>/<model>/<action>/` | Invoke a registered `ModelAdmin.actions` entry on a queryset. |
|
|
463
|
+
| `GET` | `/api/v1/<app>/<model>/autocomplete/?q=…` | `autocomplete_fields` lookup. Permission-gated on the **target** model. |
|
|
464
|
+
|
|
465
|
+
Every endpoint is **staff-only by default** (or whatever
|
|
466
|
+
`AdminSite.has_permission` returns), CSRF-required on unsafe
|
|
467
|
+
methods, and emits `Cache-Control: no-store`. Full wire contract:
|
|
468
|
+
[`docs/api-contract.md`](docs/api-contract.md).
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Examples
|
|
473
|
+
|
|
474
|
+
Six runnable example projects ship with the repo under
|
|
475
|
+
[`examples/`](examples/):
|
|
476
|
+
|
|
477
|
+
| Project | What it exercises |
|
|
478
|
+
| ---------- | -------------------------------------------------------------------------------------------------- |
|
|
479
|
+
| `library/` | `Author`, `Book`, `Genre` — basic CRUD, FKs, M2M, `search_fields`, `list_filter`. |
|
|
480
|
+
| `fintech/` | `Account`, `Transaction` — permissions, queryset narrowing, custom actions. |
|
|
481
|
+
| `blog/` | `Post`, `Tag`, `Comment` — `list_editable`, `inlines`, `date_hierarchy`. |
|
|
482
|
+
| `ecommerce/` | `Product`, `Order`, `LineItem` — fieldsets, readonly, `register_field_type` for `MoneyField`. |
|
|
483
|
+
| `hr/` | `Employee`, `Department` — `autocomplete_fields`, `raw_id_fields`, organisational filters. |
|
|
484
|
+
| `project/` | Glue project that mounts every example app for an end-to-end demo. |
|
|
485
|
+
|
|
486
|
+
Boot any of them with:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
cd examples/project
|
|
490
|
+
python manage.py migrate
|
|
491
|
+
python manage.py loaddata seed
|
|
492
|
+
python manage.py runserver
|
|
493
|
+
# → http://127.0.0.1:8000/admin/ (legacy admin)
|
|
494
|
+
# → http://127.0.0.1:8000/admin-react/ (the React SPA)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## What you get
|
|
500
|
+
|
|
501
|
+
- **Plug-and-play**: works with any `ModelAdmin` you already have.
|
|
502
|
+
- **Shared auth**: Django sessions, CSRF, staff permissions. No new
|
|
503
|
+
user model, no parallel permission system.
|
|
504
|
+
- **Responsive, modern UI**: React + Tailwind + React Query, served
|
|
505
|
+
as a single bundle from `django_admin_react/static/admin_react/`.
|
|
506
|
+
- **Extensible by editing `ModelAdmin`**, not React. Per-model SPA
|
|
507
|
+
extension hooks for the cases that genuinely need them.
|
|
508
|
+
- **Configurable URL prefix** — `/admin/`, `/admin-react/`, anywhere.
|
|
509
|
+
- **Conservative & secure-by-default** — never exposes models the
|
|
510
|
+
admin doesn't already expose; never writes fields the admin form
|
|
511
|
+
excludes; CSRF on every unsafe method; `Cache-Control: no-store`
|
|
512
|
+
on every API response; sensitive-name denylist on top of the
|
|
513
|
+
admin's own `exclude` rules.
|
|
514
|
+
- **Boring + auditable** — no parallel permission system, no
|
|
515
|
+
client-side workarounds for backend permissions, conservative
|
|
516
|
+
serializer with `str()` fallback.
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## License
|
|
521
|
+
|
|
522
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
523
|
+
|
|
524
|
+
## Security
|
|
525
|
+
|
|
526
|
+
Please report security issues privately through GitHub's Private
|
|
527
|
+
Vulnerability Reporting on the repository (Security → Advisories).
|
|
528
|
+
See [`SECURITY.md`](SECURITY.md). Do **not** open a public issue.
|
|
529
|
+
|
|
530
|
+
## Contributing
|
|
531
|
+
|
|
532
|
+
Humans and AI agents both welcome. Start with
|
|
533
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
534
|
+
|