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.
Files changed (36) hide show
  1. django_admin_react-0.1.0a2/PKG-INFO +323 -0
  2. django_admin_react-0.1.0a2/README.md +291 -0
  3. django_admin_react-0.1.0a2/django_admin_react/api/dates.py +216 -0
  4. django_admin_react-0.1.0a2/django_admin_react/api/serializers.py +320 -0
  5. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/detail.py +6 -10
  6. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/list.py +21 -16
  7. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/writes.py +4 -20
  8. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/pyproject.toml +6 -2
  9. django_admin_react-0.1.0a1/PKG-INFO +0 -237
  10. django_admin_react-0.1.0a1/README.md +0 -206
  11. django_admin_react-0.1.0a1/django_admin_react/api/serializers.py +0 -183
  12. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/LICENSE +0 -0
  13. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/README.md +0 -0
  14. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/__init__.py +0 -0
  15. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/README.md +0 -0
  16. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/__init__.py +0 -0
  17. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/permissions.py +0 -0
  18. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/registry.py +0 -0
  19. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/urls.py +0 -0
  20. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/README.md +0 -0
  21. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/__init__.py +0 -0
  22. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/create.py +0 -0
  23. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/destroy.py +0 -0
  24. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/registry.py +0 -0
  25. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/api/views/update.py +0 -0
  26. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/apps.py +0 -0
  27. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/conf.py +0 -0
  28. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/.vite/manifest.json +0 -0
  29. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/assets/index-CKxeWYBA.css +0 -0
  30. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/assets/index-itk7hrnq.js +0 -0
  31. {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
  32. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/static/admin_react/index.html +0 -0
  33. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/templates/admin_react/README.md +0 -0
  34. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/templates/admin_react/index.html +0 -0
  35. {django_admin_react-0.1.0a1 → django_admin_react-0.1.0a2}/django_admin_react/urls.py +0 -0
  36. {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
+ | ![Login](docs/screenshots/01-admin-login.png) | ![Admin index](docs/screenshots/02-admin-index.png) |
120
+
121
+ | Library / Authors — list view | Library / Author — detail view |
122
+ | -------------------------------------------------------------- | --------------------------------------------------------------- |
123
+ | ![Author list](docs/screenshots/03-admin-library-list.png) | ![Author detail](docs/screenshots/05-admin-library-detail.png) |
124
+
125
+ | Mobile (375 px) | API: `GET /api/v1/registry/` JSON |
126
+ | --------------------------------------------------------------------- | ------------------------------------------------------- |
127
+ | ![Mobile list](docs/screenshots/04-admin-library-list-mobile.png) | ![Registry JSON](docs/screenshots/06-registry-api-json.png) |
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
+ | ![Login](docs/screenshots/01-admin-login.png) | ![Admin index](docs/screenshots/02-admin-index.png) |
89
+
90
+ | Library / Authors — list view | Library / Author — detail view |
91
+ | -------------------------------------------------------------- | --------------------------------------------------------------- |
92
+ | ![Author list](docs/screenshots/03-admin-library-list.png) | ![Author detail](docs/screenshots/05-admin-library-detail.png) |
93
+
94
+ | Mobile (375 px) | API: `GET /api/v1/registry/` JSON |
95
+ | --------------------------------------------------------------------- | ------------------------------------------------------- |
96
+ | ![Mobile list](docs/screenshots/04-admin-library-list-mobile.png) | ![Registry JSON](docs/screenshots/06-registry-api-json.png) |
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).