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