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.
Files changed (54) hide show
  1. django_admin_react-0.2.0a2/PKG-INFO +534 -0
  2. django_admin_react-0.2.0a2/README.md +502 -0
  3. django_admin_react-0.2.0a2/django_admin_react/api/filters.py +320 -0
  4. django_admin_react-0.2.0a2/django_admin_react/api/inlines.py +252 -0
  5. django_admin_react-0.2.0a2/django_admin_react/api/inlines_write.py +226 -0
  6. django_admin_react-0.2.0a2/django_admin_react/api/panels.py +113 -0
  7. django_admin_react-0.2.0a2/django_admin_react/api/permissions.py +132 -0
  8. django_admin_react-0.2.0a2/django_admin_react/api/registry.py +354 -0
  9. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/serializers.py +113 -6
  10. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/urls.py +62 -0
  11. django_admin_react-0.2.0a2/django_admin_react/api/views/actions.py +153 -0
  12. django_admin_react-0.2.0a2/django_admin_react/api/views/auth.py +192 -0
  13. django_admin_react-0.2.0a2/django_admin_react/api/views/autocomplete.py +166 -0
  14. django_admin_react-0.2.0a2/django_admin_react/api/views/bulk.py +215 -0
  15. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/create.py +4 -2
  16. django_admin_react-0.2.0a2/django_admin_react/api/views/delete_preview.py +107 -0
  17. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/destroy.py +6 -2
  18. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/detail.py +63 -3
  19. django_admin_react-0.2.0a2/django_admin_react/api/views/history.py +164 -0
  20. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/list.py +39 -11
  21. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/registry.py +1 -1
  22. django_admin_react-0.2.0a2/django_admin_react/api/views/schema.py +484 -0
  23. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/update.py +57 -7
  24. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/writes.py +140 -27
  25. django_admin_react-0.2.0a2/django_admin_react/audit.py +42 -0
  26. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/conf.py +18 -0
  27. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/static/admin_react/.vite/manifest.json +2 -2
  28. django_admin_react-0.2.0a2/django_admin_react/static/admin_react/assets/index-BE3CZdBI.js +9 -0
  29. django_admin_react-0.2.0a2/django_admin_react/static/admin_react/assets/index-BE3CZdBI.js.map +1 -0
  30. django_admin_react-0.2.0a2/django_admin_react/static/admin_react/assets/index-xZLX3uph.css +1 -0
  31. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/static/admin_react/index.html +2 -2
  32. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/templates/admin_react/index.html +8 -2
  33. django_admin_react-0.2.0a2/django_admin_react/templates/admin_react/login.html +76 -0
  34. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/urls.py +11 -2
  35. django_admin_react-0.2.0a2/django_admin_react/views.py +302 -0
  36. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/pyproject.toml +16 -3
  37. django_admin_react-0.1.0a2/PKG-INFO +0 -323
  38. django_admin_react-0.1.0a2/README.md +0 -291
  39. django_admin_react-0.1.0a2/django_admin_react/api/permissions.py +0 -80
  40. django_admin_react-0.1.0a2/django_admin_react/api/registry.py +0 -200
  41. django_admin_react-0.1.0a2/django_admin_react/static/admin_react/assets/index-CKxeWYBA.css +0 -1
  42. django_admin_react-0.1.0a2/django_admin_react/static/admin_react/assets/index-itk7hrnq.js +0 -68
  43. django_admin_react-0.1.0a2/django_admin_react/static/admin_react/assets/index-itk7hrnq.js.map +0 -1
  44. django_admin_react-0.1.0a2/django_admin_react/views.py +0 -136
  45. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/LICENSE +0 -0
  46. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/README.md +0 -0
  47. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/__init__.py +0 -0
  48. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/README.md +0 -0
  49. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/__init__.py +0 -0
  50. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/dates.py +0 -0
  51. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/README.md +0 -0
  52. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/api/views/__init__.py +0 -0
  53. {django_admin_react-0.1.0a2 → django_admin_react-0.2.0a2}/django_admin_react/apps.py +0 -0
  54. {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
+ | ![Sign in](docs/screenshots/01-spa-login.png) | ![Registry](docs/screenshots/02-spa-registry.png) |
86
+
87
+ | List view (`list_display` + search) | Detail view |
88
+ | ------------------------------------------------------- | ---------------------------------------------------- |
89
+ | ![List](docs/screenshots/03-spa-list.png) | ![Detail](docs/screenshots/05-spa-detail.png) |
90
+
91
+ | Mobile (375 px) | API: `GET /api/v1/registry/` |
92
+ | ---------------------------------------------------------- | ---------------------------------------------------------- |
93
+ | ![Mobile](docs/screenshots/04-spa-list-mobile.png) | ![Registry JSON](docs/screenshots/06-registry-api-json.png) |
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
+