django-admin-react 0.1.0a1__tar.gz → 0.1.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.1.0a2/PKG-INFO +323 -0
- django_admin_react-0.1.0a2/README.md +291 -0
- django_admin_react-0.1.0a2/django_admin_react/api/dates.py +216 -0
- django_admin_react-0.1.0a2/django_admin_react/api/serializers.py +320 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/detail.py +6 -10
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/list.py +21 -16
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/writes.py +4 -20
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/pyproject.toml +6 -2
- django_admin_react-0.1.0a1/PKG-INFO +0 -237
- django_admin_react-0.1.0a1/README.md +0 -206
- django_admin_react-0.1.0a1/django_admin_react/api/serializers.py +0 -183
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/LICENSE +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/README.md +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/__init__.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/README.md +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/__init__.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/permissions.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/registry.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/urls.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/README.md +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/__init__.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/create.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/destroy.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/registry.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/update.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/apps.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/conf.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/.vite/manifest.json +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/assets/index-CKxeWYBA.css +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/assets/index-itk7hrnq.js +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/assets/index-itk7hrnq.js.map +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/index.html +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/templates/admin_react/README.md +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/templates/admin_react/index.html +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/urls.py +0 -0
- {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/views.py +0 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-admin-react
|
|
3
|
+
Version: 0.1.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
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install django-admin-react
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# settings.py
|
|
53
|
+
INSTALLED_APPS = [
|
|
54
|
+
"django.contrib.admin",
|
|
55
|
+
"django.contrib.auth",
|
|
56
|
+
"django.contrib.contenttypes",
|
|
57
|
+
"django.contrib.sessions",
|
|
58
|
+
"django.contrib.messages",
|
|
59
|
+
"django.contrib.staticfiles",
|
|
60
|
+
"django_admin_react", # ← add this
|
|
61
|
+
# ... your own apps
|
|
62
|
+
]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# urls.py
|
|
67
|
+
from django.urls import include, path
|
|
68
|
+
|
|
69
|
+
urlpatterns = [
|
|
70
|
+
path("admin/", include("django_admin_react.urls")),
|
|
71
|
+
# any prefix is fine:
|
|
72
|
+
# path("admin-react/", include("django_admin_react.urls")),
|
|
73
|
+
# path("staff/", include("django_admin_react.urls")),
|
|
74
|
+
]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That is the entire integration. Log in as a staff user → modern,
|
|
78
|
+
Tailwind-styled SPA driven by your existing `ModelAdmin` classes.
|
|
79
|
+
|
|
80
|
+
The wheel ships the **pre-built React bundle**. You do **not** need
|
|
81
|
+
Node, pnpm, or any frontend toolchain to install or run.
|
|
82
|
+
|
|
83
|
+
### Optional configuration
|
|
84
|
+
|
|
85
|
+
All settings are optional. Defaults shown:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
DJANGO_ADMIN_REACT = {
|
|
89
|
+
"ADMIN_SITE": "django.contrib.admin.site", # dotted path to AdminSite instance
|
|
90
|
+
"DEFAULT_PAGE_SIZE": 25,
|
|
91
|
+
"MAX_PAGE_SIZE": 200,
|
|
92
|
+
"ENABLE_PROFILING": False,
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Requirements
|
|
97
|
+
|
|
98
|
+
- **Python**: 3.10+
|
|
99
|
+
- **Django**: 5.0, 5.1, 5.2, 6.0 (and any later 6.x)
|
|
100
|
+
- **Database**: anything Django supports — the package is ORM-only, no
|
|
101
|
+
direct SQL.
|
|
102
|
+
- **Auth**: Django's built-in session + CSRF. Works with custom
|
|
103
|
+
`AUTH_USER_MODEL`, custom `AUTHENTICATION_BACKENDS`, and custom
|
|
104
|
+
`AdminSite.has_permission`.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Screenshots
|
|
109
|
+
|
|
110
|
+
> **Captured live** with `scripts/screenshots.sh` from
|
|
111
|
+
> `examples/project/` — real pixels, not mockups. The React SPA
|
|
112
|
+
> shell is in flight; until then, the images below show the
|
|
113
|
+
> **legacy HTML admin** running against the example apps — i.e.,
|
|
114
|
+
> the experience `django-admin-react` modernises. Once the SPA
|
|
115
|
+
> renders, this section regenerates from the same script.
|
|
116
|
+
|
|
117
|
+
| Login (the entry door) | Admin index (legacy) |
|
|
118
|
+
| ------------------------------------------------- | ------------------------------------------------------- |
|
|
119
|
+
|  |  |
|
|
120
|
+
|
|
121
|
+
| Library / Authors — list view | Library / Author — detail view |
|
|
122
|
+
| -------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
123
|
+
|  |  |
|
|
124
|
+
|
|
125
|
+
| Mobile (375 px) | API: `GET /api/v1/registry/` JSON |
|
|
126
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
127
|
+
|  |  |
|
|
128
|
+
|
|
129
|
+
> **Note for the next PyPI release:** these image refs need to be
|
|
130
|
+
> absolute URLs (`https://raw.githubusercontent.com/.../main/docs/screenshots/...`)
|
|
131
|
+
> for the PyPI page to render them. The switch happens in the
|
|
132
|
+
> `0.1.0a2` release PR, which also requires the repo to be public so
|
|
133
|
+
> `raw.githubusercontent.com` resolves without auth. Until then,
|
|
134
|
+
> relative paths render correctly on the GitHub README.
|
|
135
|
+
|
|
136
|
+
Every screenshot uses a deterministic synthetic seed (no real
|
|
137
|
+
people, accounts, or PII).
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Extend without writing React
|
|
142
|
+
|
|
143
|
+
Everything below is **just `ModelAdmin`**. No JavaScript. No new
|
|
144
|
+
classes. The UI follows whatever your admin declares.
|
|
145
|
+
|
|
146
|
+
### Pick what columns appear on the list view
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
@admin.register(Invoice)
|
|
150
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
151
|
+
list_display = ("number", "customer", "status", "total", "issued_at")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Make columns sortable
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
158
|
+
list_display = ("number", "customer", "status", "total", "issued_at")
|
|
159
|
+
sortable_by = ("issued_at", "total") # everything else is fixed
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Add free-text search
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
166
|
+
search_fields = ("number", "customer__name", "notes__icontains")
|
|
167
|
+
# The SPA wires `?q=<term>` to `ModelAdmin.get_search_results` verbatim.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Default ordering
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
174
|
+
ordering = ("-issued_at",)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Hide a field from the form
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
181
|
+
exclude = ("internal_audit_hash",) # never reaches the SPA
|
|
182
|
+
readonly_fields = ("total",) # rendered as read-only
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The SPA respects `exclude` and `readonly_fields` exactly the way the
|
|
186
|
+
legacy admin does. Sensitive-named fields (`password`, `secret`,
|
|
187
|
+
`token`, `api_key`, `hash`, `private_key`, `session`, `nonce`, `salt`)
|
|
188
|
+
are filtered on top of those rules as defense-in-depth.
|
|
189
|
+
|
|
190
|
+
### Group fields into sections
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
194
|
+
fieldsets = (
|
|
195
|
+
("Identity", {"fields": ("number", "customer")}),
|
|
196
|
+
("Money", {"fields": ("subtotal", "tax", "total")}),
|
|
197
|
+
("Lifecycle", {"fields": ("status", "issued_at", "paid_at")}),
|
|
198
|
+
("Internal", {"fields": ("notes",), "classes": ("collapse",)}),
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Per-row permission gating
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
206
|
+
def has_add_permission(self, request):
|
|
207
|
+
return request.user.has_perm("billing.create_invoice")
|
|
208
|
+
|
|
209
|
+
def has_change_permission(self, request, obj=None):
|
|
210
|
+
if obj is None:
|
|
211
|
+
return request.user.has_perm("billing.change_invoice")
|
|
212
|
+
return obj.owner_id == request.user.id # row-level rule
|
|
213
|
+
|
|
214
|
+
def has_delete_permission(self, request, obj=None):
|
|
215
|
+
return False # nobody deletes invoices
|
|
216
|
+
|
|
217
|
+
def has_view_permission(self, request, obj=None):
|
|
218
|
+
return request.user.has_perm("billing.view_invoice")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The SPA hides the **Add** / **Save** / **Delete** buttons automatically
|
|
222
|
+
based on these. UI never invents a permission; it asks `ModelAdmin`.
|
|
223
|
+
|
|
224
|
+
### Restrict the queryset
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
228
|
+
def get_queryset(self, request):
|
|
229
|
+
qs = super().get_queryset(request)
|
|
230
|
+
if request.user.is_superuser:
|
|
231
|
+
return qs
|
|
232
|
+
return qs.filter(owner=request.user)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The list view never sees rows the queryset excludes. **No
|
|
236
|
+
`Model.objects.all()` in the package** — every list, search, and
|
|
237
|
+
detail lookup starts at `ModelAdmin.get_queryset(request)`.
|
|
238
|
+
|
|
239
|
+
### Custom save hook
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
243
|
+
def save_model(self, request, obj, form, change):
|
|
244
|
+
obj.last_edited_by = request.user
|
|
245
|
+
super().save_model(request, obj, form, change)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Writes always go through `ModelAdmin.get_form()` → `form.is_valid()`
|
|
249
|
+
→ `save_model()`. Signals, audit logs, and post-save hooks all fire
|
|
250
|
+
exactly like they do in `/admin/`.
|
|
251
|
+
|
|
252
|
+
### Use a custom `AdminSite`
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
# myproject/admin.py
|
|
256
|
+
from django.contrib.admin import AdminSite
|
|
257
|
+
|
|
258
|
+
class StaffAdminSite(AdminSite):
|
|
259
|
+
site_header = "Operations Console"
|
|
260
|
+
site_title = "Ops"
|
|
261
|
+
index_title = "Welcome"
|
|
262
|
+
|
|
263
|
+
def has_permission(self, request):
|
|
264
|
+
return request.user.is_active and request.user.is_staff and \
|
|
265
|
+
request.user.groups.filter(name="ops").exists()
|
|
266
|
+
|
|
267
|
+
staff_admin = StaffAdminSite(name="staff")
|
|
268
|
+
|
|
269
|
+
# myproject/settings.py
|
|
270
|
+
DJANGO_ADMIN_REACT = {
|
|
271
|
+
"ADMIN_SITE": "myproject.admin.staff_admin",
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The SPA inherits the custom site's permission gate and the
|
|
276
|
+
`ModelAdmin` registrations on that site — no parallel registry.
|
|
277
|
+
|
|
278
|
+
### Pre-built form / queryset overrides still work
|
|
279
|
+
|
|
280
|
+
`get_form`, `get_fieldsets`, `get_fields`, `get_exclude`,
|
|
281
|
+
`get_readonly_fields`, `get_search_results`, `get_list_display`,
|
|
282
|
+
`get_sortable_by` — all of them are called by the SPA the same way the
|
|
283
|
+
HTML admin calls them. If you customised them for `/admin/`, the SPA
|
|
284
|
+
already honours those customisations.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## What you get
|
|
289
|
+
|
|
290
|
+
- **Plug-and-play**: works with any `ModelAdmin` you already have.
|
|
291
|
+
- **Shared auth**: Django sessions, CSRF, staff permissions. No new
|
|
292
|
+
user model, no parallel permission system.
|
|
293
|
+
- **Responsive, modern UI**: React + Tailwind + React Query, served
|
|
294
|
+
as a single bundle from `django_admin_react/static/admin_react/`.
|
|
295
|
+
- **Extensible by editing `ModelAdmin`**, not React.
|
|
296
|
+
- **Configurable URL prefix** — `/admin/`, `/admin-react/`, anywhere.
|
|
297
|
+
- **Conservative & secure-by-default** — never exposes models the
|
|
298
|
+
admin doesn't already expose; never writes fields the admin form
|
|
299
|
+
excludes; CSRF on every unsafe method; `Cache-Control: no-store`
|
|
300
|
+
on every API response; sensitive-name denylist on top of the
|
|
301
|
+
admin's own `exclude` rules.
|
|
302
|
+
- **Boring + auditable** — no parallel permission system, no
|
|
303
|
+
client-side workarounds for backend permissions, conservative
|
|
304
|
+
serializer with `str()` fallback.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
MIT — see [`LICENSE`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/LICENSE).
|
|
311
|
+
|
|
312
|
+
## Security
|
|
313
|
+
|
|
314
|
+
Please report security issues privately through GitHub's Private
|
|
315
|
+
Vulnerability Reporting on the repository (Security → Advisories).
|
|
316
|
+
See [`SECURITY.md`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/SECURITY.md).
|
|
317
|
+
Do **not** open a public issue.
|
|
318
|
+
|
|
319
|
+
## Contributing
|
|
320
|
+
|
|
321
|
+
Humans and AI agents both welcome. Start with
|
|
322
|
+
[`CONTRIBUTING.md`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/CONTRIBUTING.md).
|
|
323
|
+
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# django-admin-react
|
|
2
|
+
|
|
3
|
+
A drop-in **React single-page admin** for any Django 5+ project. Same
|
|
4
|
+
`pip install`, same `INSTALLED_APPS`, same `urls.py include()` — and
|
|
5
|
+
your `ModelAdmin` classes drive everything. No React code on your side.
|
|
6
|
+
|
|
7
|
+
> **Pre-alpha.** Available on PyPI as an alpha. Pin tightly; expect
|
|
8
|
+
> breaking changes between alpha releases. Track progress on the
|
|
9
|
+
> [Project board](https://github.com/users/MartinCastroAlvarez/projects/3)
|
|
10
|
+
> and the [Issues list](https://github.com/MartinCastroAlvarez/django-admin-react/issues).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install django-admin-react
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# settings.py
|
|
22
|
+
INSTALLED_APPS = [
|
|
23
|
+
"django.contrib.admin",
|
|
24
|
+
"django.contrib.auth",
|
|
25
|
+
"django.contrib.contenttypes",
|
|
26
|
+
"django.contrib.sessions",
|
|
27
|
+
"django.contrib.messages",
|
|
28
|
+
"django.contrib.staticfiles",
|
|
29
|
+
"django_admin_react", # ← add this
|
|
30
|
+
# ... your own apps
|
|
31
|
+
]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# urls.py
|
|
36
|
+
from django.urls import include, path
|
|
37
|
+
|
|
38
|
+
urlpatterns = [
|
|
39
|
+
path("admin/", include("django_admin_react.urls")),
|
|
40
|
+
# any prefix is fine:
|
|
41
|
+
# path("admin-react/", include("django_admin_react.urls")),
|
|
42
|
+
# path("staff/", include("django_admin_react.urls")),
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That is the entire integration. Log in as a staff user → modern,
|
|
47
|
+
Tailwind-styled SPA driven by your existing `ModelAdmin` classes.
|
|
48
|
+
|
|
49
|
+
The wheel ships the **pre-built React bundle**. You do **not** need
|
|
50
|
+
Node, pnpm, or any frontend toolchain to install or run.
|
|
51
|
+
|
|
52
|
+
### Optional configuration
|
|
53
|
+
|
|
54
|
+
All settings are optional. Defaults shown:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
DJANGO_ADMIN_REACT = {
|
|
58
|
+
"ADMIN_SITE": "django.contrib.admin.site", # dotted path to AdminSite instance
|
|
59
|
+
"DEFAULT_PAGE_SIZE": 25,
|
|
60
|
+
"MAX_PAGE_SIZE": 200,
|
|
61
|
+
"ENABLE_PROFILING": False,
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Requirements
|
|
66
|
+
|
|
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`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Screenshots
|
|
78
|
+
|
|
79
|
+
> **Captured live** with `scripts/screenshots.sh` from
|
|
80
|
+
> `examples/project/` — real pixels, not mockups. The React SPA
|
|
81
|
+
> shell is in flight; until then, the images below show the
|
|
82
|
+
> **legacy HTML admin** running against the example apps — i.e.,
|
|
83
|
+
> the experience `django-admin-react` modernises. Once the SPA
|
|
84
|
+
> renders, this section regenerates from the same script.
|
|
85
|
+
|
|
86
|
+
| Login (the entry door) | Admin index (legacy) |
|
|
87
|
+
| ------------------------------------------------- | ------------------------------------------------------- |
|
|
88
|
+
|  |  |
|
|
89
|
+
|
|
90
|
+
| Library / Authors — list view | Library / Author — detail view |
|
|
91
|
+
| -------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
92
|
+
|  |  |
|
|
93
|
+
|
|
94
|
+
| Mobile (375 px) | API: `GET /api/v1/registry/` JSON |
|
|
95
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
96
|
+
|  |  |
|
|
97
|
+
|
|
98
|
+
> **Note for the next PyPI release:** these image refs need to be
|
|
99
|
+
> absolute URLs (`https://raw.githubusercontent.com/.../main/docs/screenshots/...`)
|
|
100
|
+
> for the PyPI page to render them. The switch happens in the
|
|
101
|
+
> `0.1.0a2` release PR, which also requires the repo to be public so
|
|
102
|
+
> `raw.githubusercontent.com` resolves without auth. Until then,
|
|
103
|
+
> relative paths render correctly on the GitHub README.
|
|
104
|
+
|
|
105
|
+
Every screenshot uses a deterministic synthetic seed (no real
|
|
106
|
+
people, accounts, or PII).
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Extend without writing React
|
|
111
|
+
|
|
112
|
+
Everything below is **just `ModelAdmin`**. No JavaScript. No new
|
|
113
|
+
classes. The UI follows whatever your admin declares.
|
|
114
|
+
|
|
115
|
+
### Pick what columns appear on the list view
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
@admin.register(Invoice)
|
|
119
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
120
|
+
list_display = ("number", "customer", "status", "total", "issued_at")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Make columns sortable
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
127
|
+
list_display = ("number", "customer", "status", "total", "issued_at")
|
|
128
|
+
sortable_by = ("issued_at", "total") # everything else is fixed
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Add free-text search
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
135
|
+
search_fields = ("number", "customer__name", "notes__icontains")
|
|
136
|
+
# The SPA wires `?q=<term>` to `ModelAdmin.get_search_results` verbatim.
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Default ordering
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
143
|
+
ordering = ("-issued_at",)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Hide a field from the form
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
150
|
+
exclude = ("internal_audit_hash",) # never reaches the SPA
|
|
151
|
+
readonly_fields = ("total",) # rendered as read-only
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The SPA respects `exclude` and `readonly_fields` exactly the way the
|
|
155
|
+
legacy admin does. Sensitive-named fields (`password`, `secret`,
|
|
156
|
+
`token`, `api_key`, `hash`, `private_key`, `session`, `nonce`, `salt`)
|
|
157
|
+
are filtered on top of those rules as defense-in-depth.
|
|
158
|
+
|
|
159
|
+
### Group fields into sections
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
163
|
+
fieldsets = (
|
|
164
|
+
("Identity", {"fields": ("number", "customer")}),
|
|
165
|
+
("Money", {"fields": ("subtotal", "tax", "total")}),
|
|
166
|
+
("Lifecycle", {"fields": ("status", "issued_at", "paid_at")}),
|
|
167
|
+
("Internal", {"fields": ("notes",), "classes": ("collapse",)}),
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Per-row permission gating
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
175
|
+
def has_add_permission(self, request):
|
|
176
|
+
return request.user.has_perm("billing.create_invoice")
|
|
177
|
+
|
|
178
|
+
def has_change_permission(self, request, obj=None):
|
|
179
|
+
if obj is None:
|
|
180
|
+
return request.user.has_perm("billing.change_invoice")
|
|
181
|
+
return obj.owner_id == request.user.id # row-level rule
|
|
182
|
+
|
|
183
|
+
def has_delete_permission(self, request, obj=None):
|
|
184
|
+
return False # nobody deletes invoices
|
|
185
|
+
|
|
186
|
+
def has_view_permission(self, request, obj=None):
|
|
187
|
+
return request.user.has_perm("billing.view_invoice")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The SPA hides the **Add** / **Save** / **Delete** buttons automatically
|
|
191
|
+
based on these. UI never invents a permission; it asks `ModelAdmin`.
|
|
192
|
+
|
|
193
|
+
### Restrict the queryset
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
197
|
+
def get_queryset(self, request):
|
|
198
|
+
qs = super().get_queryset(request)
|
|
199
|
+
if request.user.is_superuser:
|
|
200
|
+
return qs
|
|
201
|
+
return qs.filter(owner=request.user)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The list view never sees rows the queryset excludes. **No
|
|
205
|
+
`Model.objects.all()` in the package** — every list, search, and
|
|
206
|
+
detail lookup starts at `ModelAdmin.get_queryset(request)`.
|
|
207
|
+
|
|
208
|
+
### Custom save hook
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
212
|
+
def save_model(self, request, obj, form, change):
|
|
213
|
+
obj.last_edited_by = request.user
|
|
214
|
+
super().save_model(request, obj, form, change)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Writes always go through `ModelAdmin.get_form()` → `form.is_valid()`
|
|
218
|
+
→ `save_model()`. Signals, audit logs, and post-save hooks all fire
|
|
219
|
+
exactly like they do in `/admin/`.
|
|
220
|
+
|
|
221
|
+
### Use a custom `AdminSite`
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
# myproject/admin.py
|
|
225
|
+
from django.contrib.admin import AdminSite
|
|
226
|
+
|
|
227
|
+
class StaffAdminSite(AdminSite):
|
|
228
|
+
site_header = "Operations Console"
|
|
229
|
+
site_title = "Ops"
|
|
230
|
+
index_title = "Welcome"
|
|
231
|
+
|
|
232
|
+
def has_permission(self, request):
|
|
233
|
+
return request.user.is_active and request.user.is_staff and \
|
|
234
|
+
request.user.groups.filter(name="ops").exists()
|
|
235
|
+
|
|
236
|
+
staff_admin = StaffAdminSite(name="staff")
|
|
237
|
+
|
|
238
|
+
# myproject/settings.py
|
|
239
|
+
DJANGO_ADMIN_REACT = {
|
|
240
|
+
"ADMIN_SITE": "myproject.admin.staff_admin",
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The SPA inherits the custom site's permission gate and the
|
|
245
|
+
`ModelAdmin` registrations on that site — no parallel registry.
|
|
246
|
+
|
|
247
|
+
### Pre-built form / queryset overrides still work
|
|
248
|
+
|
|
249
|
+
`get_form`, `get_fieldsets`, `get_fields`, `get_exclude`,
|
|
250
|
+
`get_readonly_fields`, `get_search_results`, `get_list_display`,
|
|
251
|
+
`get_sortable_by` — all of them are called by the SPA the same way the
|
|
252
|
+
HTML admin calls them. If you customised them for `/admin/`, the SPA
|
|
253
|
+
already honours those customisations.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## What you get
|
|
258
|
+
|
|
259
|
+
- **Plug-and-play**: works with any `ModelAdmin` you already have.
|
|
260
|
+
- **Shared auth**: Django sessions, CSRF, staff permissions. No new
|
|
261
|
+
user model, no parallel permission system.
|
|
262
|
+
- **Responsive, modern UI**: React + Tailwind + React Query, served
|
|
263
|
+
as a single bundle from `django_admin_react/static/admin_react/`.
|
|
264
|
+
- **Extensible by editing `ModelAdmin`**, not React.
|
|
265
|
+
- **Configurable URL prefix** — `/admin/`, `/admin-react/`, anywhere.
|
|
266
|
+
- **Conservative & secure-by-default** — never exposes models the
|
|
267
|
+
admin doesn't already expose; never writes fields the admin form
|
|
268
|
+
excludes; CSRF on every unsafe method; `Cache-Control: no-store`
|
|
269
|
+
on every API response; sensitive-name denylist on top of the
|
|
270
|
+
admin's own `exclude` rules.
|
|
271
|
+
- **Boring + auditable** — no parallel permission system, no
|
|
272
|
+
client-side workarounds for backend permissions, conservative
|
|
273
|
+
serializer with `str()` fallback.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT — see [`LICENSE`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/LICENSE).
|
|
280
|
+
|
|
281
|
+
## Security
|
|
282
|
+
|
|
283
|
+
Please report security issues privately through GitHub's Private
|
|
284
|
+
Vulnerability Reporting on the repository (Security → Advisories).
|
|
285
|
+
See [`SECURITY.md`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/SECURITY.md).
|
|
286
|
+
Do **not** open a public issue.
|
|
287
|
+
|
|
288
|
+
## Contributing
|
|
289
|
+
|
|
290
|
+
Humans and AI agents both welcome. Start with
|
|
291
|
+
[`CONTRIBUTING.md`](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/CONTRIBUTING.md).
|