{"id":50315891,"url":"https://github.com/martincastroalvarez/django-admin-react","last_synced_at":"2026-06-01T03:01:20.998Z","repository":{"id":360538345,"uuid":"1249062052","full_name":"MartinCastroAlvarez/django-admin-react","owner":"MartinCastroAlvarez","description":"A drop-in React single-page admin for Django — driven entirely by your existing ModelAdmin. Dark mode, mobile cards, drag-and-drop column layout with freezable columns, keyboard a11y, JSON-aware detail rendering, and a bidirectional bridge to the classic admin during migration. Pip install and go — no React code on your side.","archived":false,"fork":false,"pushed_at":"2026-05-29T20:35:12.000Z","size":7303,"stargazers_count":10,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-30T01:20:03.839Z","etag":null,"topics":["admin-dashboard","admin-panel","backend","django","django-admin","django-rest","frontend","modeladmin","python","react","rest-api","single-page-application","spa","tailwindcss"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/django-admin-react/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MartinCastroAlvarez.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-25T10:06:59.000Z","updated_at":"2026-05-29T20:34:56.000Z","dependencies_parsed_at":null,"dependency_job_id":"05cd03af-036e-4c18-bc68-bd123ae533a2","html_url":"https://github.com/MartinCastroAlvarez/django-admin-react","commit_stats":null,"previous_names":["martincastroalvarez/django-admin-react"],"tags_count":34,"template":false,"template_full_name":null,"purl":"pkg:github/MartinCastroAlvarez/django-admin-react","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-react","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-react/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-react/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-react/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MartinCastroAlvarez","download_url":"https://codeload.github.com/MartinCastroAlvarez/django-admin-react/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-react/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33716339,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["admin-dashboard","admin-panel","backend","django","django-admin","django-rest","frontend","modeladmin","python","react","rest-api","single-page-application","spa","tailwindcss"],"created_at":"2026-05-29T00:01:44.146Z","updated_at":"2026-05-31T02:00:38.385Z","avatar_url":"https://github.com/MartinCastroAlvarez.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# django-admin-react\n\n[![Latest on Django Packages](https://img.shields.io/badge/PyPI-django--admin--react--tags-8c3c26.svg)](https://djangopackages.org/packages/p/django-admin-react/)\n\nA drop-in **React single-page admin** for any Django 5+ project. Same\n`pip install`, same `INSTALLED_APPS`, same `urls.py include()` — and\nyour `ModelAdmin` classes drive everything. No React code on your side.\n\n```python\n# settings.py\nINSTALLED_APPS = [\n    # ...\n    \"django.contrib.admin\",\n    \"django_admin_react\",   # the React SPA — includes the JSON API for you\n]\n\n# urls.py\nurlpatterns = [\n    path(\"admin/\",       admin.site.urls),\n    path(\"admin-react/\", include(\"django_admin_react.urls\")),  # SPA + API in one include\n]\n```\n\n**One `INSTALLED_APPS` line + one URL include is the entire integration.** `pip install django-admin-react` transitively pulls in the [JSON API](https://pypi.org/project/django-admin-rest-api/) and the [MCP adapter](https://pypi.org/project/django-admin-mcp-api/); `django_admin_react.urls` includes the API endpoints at `\u003cmount\u003e/api/v1/…`, so the SPA finds its wire surface with zero configuration. (Mount the API a second time at your own prefix only if a non-SPA client also needs it.)\n\n\u003e **Production / Stable.** Available on PyPI; the SPA + the API\n\u003e ([`django-admin-rest-api`](https://pypi.org/project/django-admin-rest-api/))\n\u003e + the MCP adapter\n\u003e ([`django-admin-mcp-api`](https://pypi.org/project/django-admin-mcp-api/))\n\u003e all share the v1 wire contract. Track progress on the\n\u003e [Project board](https://github.com/users/MartinCastroAlvarez/projects/3)\n\u003e and the [Issues list](https://github.com/MartinCastroAlvarez/django-admin-react/issues).\n\n## Three repos, one product\n\nThe project is split into three independently-published, cross-referenced repos so each piece can be consumed on its own merits:\n\n| Repo | PyPI | Role |\n|---|---|---|\n| **[`django-admin-rest-api`](https://github.com/MartinCastroAlvarez/django-admin-api)** | [`django-admin-rest-api`](https://pypi.org/project/django-admin-rest-api/) | The JSON REST API for the Django admin — same permissions, same `ModelAdmin`, no new features. The wire surface. |\n| **`django-admin-react`** *(this repo)* | [`django-admin-react`](https://pypi.org/project/django-admin-react/) | The React SPA frontend. A **super-layer** that depends on `django-admin-rest-api` for every wire call. |\n| **[`django-admin-mcp-api`](https://github.com/MartinCastroAlvarez/django-admin-mcp)** | [`django-admin-mcp-api`](https://pypi.org/project/django-admin-mcp-api/) | Wire-protocol-only **MCP** adapter (call, manifest, …) over `django-admin-rest-api` — lets agents reach the same `ModelAdmin`-driven REST surface, no new functionality / permissions / validation. |\n\nThe wire contract itself lives in the **API repo** (`docs/api-contract.md` there). This README is about the SPA. The migration from \"self-contained\" to the 3-repo split is tracked in [META #544](https://github.com/MartinCastroAlvarez/django-admin-react/issues/544).\n\n---\n\n## Why django-admin-react\n\nThe Django admin is a 20-year-old hypertext app: full-page reloads,\nmid-2000s aesthetics, no real mobile support, no client-side state.\nIt is also the most powerful piece of Django: `ModelAdmin` already\nencodes your permissions, querysets, forms, fieldsets, search,\nordering, and inlines.\n\n`django-admin-react` keeps every line of `ModelAdmin` you already\nhave and replaces only the UI:\n\n| What you write                       | What the React SPA does with it                                              |\n| ------------------------------------ | ---------------------------------------------------------------------------- |\n| `list_display`                       | Renders columns in a virtualised, sortable, mobile-collapsing table.         |\n| `search_fields`                      | Renders a search bar that hits `get_search_results` verbatim.                |\n| `list_filter`                        | Renders a sidebar drawer (desktop) / bottom-sheet (mobile) + filter chips.   |\n| `date_hierarchy`                     | Renders a year → month → day drill-down strip.                                |\n| `list_editable` / `list_per_page`    | Renders inline-editable cells + paginated list with deep links.              |\n| `actions`                            | Renders a bulk-actions menu wired to the same `ModelAdmin.actions`.          |\n| `fieldsets` / `readonly_fields`      | Renders the detail form respecting groups + read-only rules.                 |\n| `autocomplete_fields`                | Renders type-ahead pickers that hit `\u003cmodel\u003e/autocomplete/?q=…`.             |\n| `inlines = [TabularInline, ...]`     | Renders inlines as tables / card stacks alongside the parent.                |\n| `has_*_permission`                   | Hides Add / Save / Delete buttons accordingly; never invents a permission.   |\n| `get_queryset(request)`              | Every list, search, and detail lookup starts here. Never `Model.objects.all()`. |\n\nThe SPA is **metadata-driven** — it learns your models, fields, and\npermissions at runtime from `GET /api/v1/registry/`. Add a new\n`ModelAdmin` and refresh; no rebuild, no codegen.\n\n---\n\n## Screenshots\n\nReal captures of the **django-admin-react SPA** rendering the bundled\n`examples/` apps — driven entirely by each app's `ModelAdmin`.\n\n### Light + dark — your `ModelAdmin` decides the chrome, the theme is operator/user choice\n\n| Registry / home (dark)                                                                                                            | List view — `list_display` + filters + actions                                                                                |\n| --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| ![Registry dark](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/02-spa-registry.png) | ![List light](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/03-spa-list.png) |\n\n| List view (dark)                                                                                                                  | Detail view                                                                                                                  |\n| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |\n| ![List dark](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/07-spa-list-dark.png) | ![Detail light](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/05-spa-detail.png) |\n\n| Detail view (dark)                                                                                                                       | Sign in (package login)                                                                                                            |\n| ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| ![Detail dark](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/08-spa-detail-dark.png) | ![Sign in](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/01-spa-login.png)        |\n\n### Phone-shaped (375 px) — `RecordCardList` fallback, full feature parity\n\n| Mobile list (cards)                                                                                                                 | Mobile detail (stacked fieldsets)                                                                                                       |\n| ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |\n| ![Mobile list](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/04-spa-list-mobile.png) | ![Mobile detail](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/09-spa-detail-mobile.png) |\n\n### One API, many surfaces\n\nThe SPA is one consumer of the wire format. The same JSON powers the\nReact app, the [MCP layer](https://pypi.org/project/django-admin-mcp-api/),\nand any client you write:\n\n![Registry JSON](https://raw.githubusercontent.com/MartinCastroAlvarez/django-admin-react/main/docs/screenshots/06-registry-api-json.png)\n\nScreenshots are captured deterministically against the\n[`examples/`](./examples) apps' fixtures — no real names, emails,\naccount numbers, or PII.\n\n---\n\n## Install\n\n```bash\npip install django-admin-react\n```\n\nThis pulls in the JSON API ([`django-admin-rest-api`](https://pypi.org/project/django-admin-rest-api/))\nand the MCP adapter ([`django-admin-mcp-api`](https://pypi.org/project/django-admin-mcp-api/))\nas transitive dependencies. The **two-line `INSTALLED_APPS` + one-line\nURL include** at the top of this README is the *entire* integration.\nMount at any prefix you like — `/admin-react/`, `/staff/`,\n`/back-office/` — just don't collide with `django.contrib.admin`'s\nown mount.\n\nLog in as a staff user → modern, Tailwind-styled SPA driven by your\nexisting `ModelAdmin` classes.\n\nThe wheel ships the **pre-built React bundle**. You do **not** need\nNode, pnpm, or any frontend toolchain to install or run.\n\n### Optional configuration\n\nAll settings are optional. Defaults shown:\n\n```python\nDJANGO_ADMIN_REACT = {\n    \"ADMIN_SITE\": \"django.contrib.admin.site\",   # dotted path to AdminSite instance\n    \"DEFAULT_PAGE_SIZE\": 25,    # fallback only; the list page size derives\n                                # from ModelAdmin.list_per_page (Django parity).\n    \"MAX_PAGE_SIZE\": 200,\n    \"ENABLE_PROFILING\": False,\n\n    # Branding — all optional. The defaults derive from your AdminSite\n    # (site_header / site_title / site_logo), so if you already branded\n    # the HTML admin you need nothing here. Rendered server-side into the\n    # SPA shell, so title + favicon are present on first paint (no FOUC).\n    \"BRAND_TITLE\": None,        # str | None — override for BOTH brand strings.\n    \"BRAND_LOGO_URL\": None,     # str | None — favicon + sidebar logo;\n                                # falls back to AdminSite.site_logo. Absolute\n                                # URL or a path under your STATIC_URL.\n    \"PRIMARY_COLOR\": \"#2563eb\", # accent for primary buttons, links, and\n                                # active states. Hex only (validated);\n                                # injected as the --dar-primary CSS var, so\n                                # rebranding needs no React rebuild.\n\n    # Auth + API mount\n    \"REACT_LOGIN\": True,        # bool — React-rendered login is the default;\n                                # the SPA shell is served to anonymous users\n                                # and posts to /api/v1/login/. Set False to\n                                # opt back into the legacy admin HTML login.\n    \"API_URL_PREFIX\": None,     # str | None — point the SPA at a separately-\n                                # mounted django-admin-rest-api (e.g.\n                                # \"/api/api/v1/\"). Default None keeps the\n                                # inline include the package ships today.\n}\n```\n\n#### Branding (`BRAND_TITLE` + `BRAND_LOGO_URL`)\n\nBoth default to `None` and **derive from your `AdminSite`**, mirroring\nDjango admin — so if you already customised the HTML admin's branding,\nyou need no settings here at all.\n\n**Sidebar header** resolution:\n\n1. `DJANGO_ADMIN_REACT[\"BRAND_TITLE\"]` — explicit override.\n2. `\u003cyour AdminSite\u003e.site_header` — reused automatically.\n3. `\"Django Admin\"` — last-resort fallback.\n\n**Browser-tab `\u003ctitle\u003e`** resolution (Django uses `site_title` for the\ntab, `site_header` for the on-page header):\n\n1. `DJANGO_ADMIN_REACT[\"BRAND_TITLE\"]` — explicit override.\n2. `\u003cyour AdminSite\u003e.site_title` — Django's tab-title source.\n3. `\u003cyour AdminSite\u003e.site_header` — fallback.\n4. `\"Django Admin\"` — last-resort fallback.\n\n`BRAND_LOGO_URL` accepts either an absolute URL or a path the browser\ncan resolve under your `STATIC_URL`. When unset, a `site_logo` attribute\non your `AdminSite` is used (Django has no logo by default, so set it as\na constant on your custom site). It is used both as the favicon\n(`\u003clink rel=\"icon\"\u003e` in the SPA shell) and as the small logo next to\nthe brand title in the sidebar.\n\n```python\n# settings.py\nDJANGO_ADMIN_REACT = {\n    \"BRAND_TITLE\":    \"Acme\",\n    \"BRAND_LOGO_URL\": \"/static/acme/logo.svg\",\n}\n```\n\nBoth values are written into the SPA index template as standard\n`\u003cmeta\u003e` tags (`dar-brand-title`, `dar-brand-logo`); the React shell\nreads them at boot, so the first paint already carries the consumer's\nbrand. No flash of the package's defaults.\n\n### Requirements\n\n- **Python**: 3.10+\n- **Django**: 5.0, 5.1, 5.2, 6.0 (and any later 6.x)\n- **Database**: anything Django supports — the package is ORM-only,\n  no direct SQL.\n- **Auth**: Django's built-in session + CSRF. Works with custom\n  `AUTH_USER_MODEL`, custom `AUTHENTICATION_BACKENDS`, and custom\n  `AdminSite.has_permission`.\n\n### Production: static files (and media for file uploads)\n\nThe wheel ships the pre-built bundle under the package's `static/` and\nserves it through `{% static %}`. With `DEBUG = True`, Django's\nstaticfiles app serves it automatically — nothing to do. **In\nproduction** you collect + serve static files like any Django app:\n\n```python\n# settings.py\nSTATIC_URL = \"/static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"   # where collectstatic gathers files\n```\n\n```bash\npython manage.py collectstatic --no-input\n```\n\nThen serve `STATIC_ROOT` from your web server / CDN — or let\n[WhiteNoise](https://whitenoise.readthedocs.io/) do it:\n\n```python\nMIDDLEWARE = [\n    \"django.middleware.security.SecurityMiddleware\",\n    \"whitenoise.middleware.WhiteNoiseMiddleware\",   # right after SecurityMiddleware\n    # ...\n]\n```\n\n\u003e If the SPA shell loads but its JS/CSS 404 (blank page, console errors),\n\u003e this `collectstatic` step is what's missing.\n\n**File / image fields.** Editing `FileField` / `ImageField` needs\nDjango's media settings:\n\n```python\n# settings.py\nMEDIA_URL = \"/media/\"\nMEDIA_ROOT = BASE_DIR / \"media\"\n```\n\nUploads go through your configured file storage\n(`STORAGES[\"default\"]` / `DEFAULT_FILE_STORAGE`); in production serve\n`MEDIA_ROOT` from your web server or object storage as usual.\n\n\u003e ⚠️ **Serving user-uploaded media has security implications** (access-gating, stored-file XSS). See [`SECURITY.md` §9](SECURITY.md) before exposing `MEDIA_URL` in production — `FileField`/`ImageField` are writable.\n\n### Running side-by-side with the legacy admin\n\nA common rollout: keep `/admin/` on the legacy HTML admin, mount the\nReact SPA at `/admin-react/`, and migrate users at your own pace.\nBoth run off the same `ModelAdmin` registrations — there is no\nduplicate state.\n\n```python\nurlpatterns = [\n    path(\"admin/\",        admin.site.urls),                          # legacy, unchanged\n    path(\"admin-react/\",  include(\"django_admin_react.urls\")),       # SPA\n]\n```\n\n#### Experience-toggle strip (optional)\n\nDuring the rollout, show a thin **persistent** strip at the top of\nevery page on **both** admins that links to the same page on the\nother admin. Users can switch surfaces in one click, regardless of\nwhich one they're on:\n\n```python\n# settings.py\nDJANGO_ADMIN_REACT = {\n    \"LEGACY_ADMIN_URL_PREFIX\": \"admin/\",     # the legacy admin's mount\n    \"REACT_ADMIN_URL_PREFIX\":  \"admin2/\",    # this package's mount\n}\n```\n\nBoth values must match the prefixes you used in `urls.py`. When set:\n\n- The **React SPA** renders a strip linking the same path under the\n  legacy admin's mount (with `?query=string` preserved and a trailing\n  slash, since Django admin URLs require one).\n- The **legacy Django admin** renders the mirror strip linking the\n  matching React URL.\n\nSet `LEGACY_ADMIN_URL_PREFIX` alone if you only want the SPA → legacy\ndirection (reverse direction stays off).\n\n##### `INSTALLED_APPS` ordering\n\nFor the legacy-side strip, list `django_admin_react` **before**\n`django.contrib.admin`. Django's template loader resolves\n`admin/base_site.html` left-to-right and the first match wins —\nthe package's override of that template injects the strip:\n\n```python\nINSTALLED_APPS = [\n    \"django_admin_react\",            # ← BEFORE django.contrib.admin\n    \"django.contrib.admin\",\n    # ...\n]\n```\n\nIf you don't enable the legacy-side strip (`REACT_ADMIN_URL_PREFIX`\nunset) the ordering doesn't matter — the override is a no-op for\nconsumers who haven't opted in.\n\n##### UX contract\n\nThe strip is **subtle and persistent**: one line tall, neutral\nchrome, no dismiss control. Operators turn it on/off via the\nsettings; end-users do not. When you remove the settings (or set\nthem to `None`), the strips disappear on the next page load —\ncompleting the migration.\n\n---\n\n## Extend without writing React\n\nEverything below is **just `ModelAdmin`**. No JavaScript. No new\nclasses. The UI follows whatever your admin declares.\n\n### Pick what columns appear on the list view\n\n```python\n@admin.register(Invoice)\nclass InvoiceAdmin(admin.ModelAdmin):\n    list_display = (\"number\", \"customer\", \"status\", \"total\", \"issued_at\")\n```\n\n### Make columns sortable\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    list_display = (\"number\", \"customer\", \"status\", \"total\", \"issued_at\")\n    sortable_by  = (\"issued_at\", \"total\")        # everything else is fixed\n```\n\n### Add free-text search\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    search_fields = (\"number\", \"customer__name\", \"notes__icontains\")\n    # The SPA wires `?q=\u003cterm\u003e` to `ModelAdmin.get_search_results` verbatim.\n```\n\n### Default ordering\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    ordering = (\"-issued_at\",)\n```\n\n### Hide a field from the form\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    exclude         = (\"internal_audit_hash\",)   # never reaches the SPA\n    readonly_fields = (\"total\",)                 # rendered as read-only\n```\n\nThe SPA respects `exclude` and `readonly_fields` exactly the way the\nlegacy admin does. Sensitive-named fields (`password`, `secret`,\n`token`, `api_key`, `hash`, `private_key`, `session`, `nonce`, `salt`)\nare filtered on top of those rules as defense-in-depth.\n\n### Group fields into sections\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    fieldsets = (\n        (\"Identity\",  {\"fields\": (\"number\", \"customer\")}),\n        (\"Money\",     {\"fields\": (\"subtotal\", \"tax\", \"total\")}),\n        (\"Lifecycle\", {\"fields\": (\"status\", \"issued_at\", \"paid_at\")}),\n        (\"Internal\",  {\"fields\": (\"notes\",), \"classes\": (\"collapse\",)}),\n    )\n```\n\n### Surface filters in the sidebar\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    list_filter = (\"status\", \"issued_at\", \"customer\")\n    # Boolean / choices / FK / date / SimpleListFilter all supported.\n```\n\n### Drill down by date\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    date_hierarchy = \"issued_at\"\n    # SPA renders a year → month → day strip wired to ?year=\u0026month=\u0026day=\n```\n\n### Edit cells inline on the list view\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    list_editable = (\"status\",)\n    # SPA: click cell → input swap → blur/Enter saves via PATCH /\u003capp\u003e/\u003cmodel\u003e/bulk/\n```\n\n### Add custom admin actions\n\nDeclare actions the stock-Django way; the SPA surfaces them in both places automatically. **One `@admin.action` declaration → two surfaces:** the changelist multi-select dropdown **and** a per-object button on the detail page. No `django-object-actions`, no `change_actions = (...)` redeclaration, no parameter-name gymnastics:\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    actions = (\"mark_paid\",)\n\n    @admin.action(description=\"Mark selected as paid\")\n    def mark_paid(self, request, queryset):\n        queryset.update(status=\"paid\", paid_at=timezone.now())\n```\n\nThat single declaration shows up in the changelist's bulk-actions dropdown (operating on every selected row) **and** as a button on the detail page (operating on the single visible row, dispatched as a one-row queryset).\n\n#### Optional: detail-only actions\n\nIf you want an action to render **only** on the detail page (not in the changelist dropdown), give its third parameter a single-object shape — name it `obj_id` / `object_id` / `pk` / `id` / `object_pk`, or annotate it `str` / `int` / a `Model` subclass. The API's signature classifier (api 1.0.6+) marks those as `target: \"detail\"`:\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    actions = (\"mark_paid\", \"regenerate_pdf\")\n\n    @admin.action(description=\"Mark selected as paid\")\n    def mark_paid(self, request, queryset):\n        ...  # changelist + detail (batch shape)\n\n    @admin.action(description=\"Regenerate PDF\")\n    def regenerate_pdf(self, request, obj_id: str):\n        invoice = self.model.objects.get(pk=obj_id)\n        invoice.regenerate_pdf()\n        # detail page only — the stock Django changelist runner\n        # expects a queryset, so this shape won't run from there.\n```\n\nClassifier rules (api 1.0.6+):\n\n| Third parameter | Target | Where it renders |\n|---|---|---|\n| name `queryset` / `qs`, or `QuerySet` annotation | `batch` (default) | Changelist multi-select **and** detail page |\n| name `obj_id` / `object_id` / `pk` / `id` / `object_pk` | `detail` | Detail page only |\n| annotation `str` / `int` / `Model` subclass | `detail` | Detail page only |\n| anything else | `batch` (default, preserves stock Django) | Changelist multi-select **and** detail page |\n\nSame `@admin.action` decorator regardless. Same `ModelAdmin.actions` tuple. Same audit trail. The signature picks the surface; the default surfaces on both.\n\n### Per-row permission gating\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    def has_add_permission(self, request):\n        return request.user.has_perm(\"billing.create_invoice\")\n\n    def has_change_permission(self, request, obj=None):\n        if obj is None:\n            return request.user.has_perm(\"billing.change_invoice\")\n        return obj.owner_id == request.user.id   # row-level rule\n\n    def has_delete_permission(self, request, obj=None):\n        return False    # nobody deletes invoices\n\n    def has_view_permission(self, request, obj=None):\n        return request.user.has_perm(\"billing.view_invoice\")\n```\n\nThe SPA hides the **Add** / **Save** / **Delete** buttons automatically\nbased on these. UI never invents a permission; it asks `ModelAdmin`.\n\n### Restrict the queryset\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    def get_queryset(self, request):\n        qs = super().get_queryset(request)\n        if request.user.is_superuser:\n            return qs\n        return qs.filter(owner=request.user)\n```\n\nThe list view never sees rows the queryset excludes. **No\n`Model.objects.all()` in the package** — every list, search, and\ndetail lookup starts at `ModelAdmin.get_queryset(request)`.\n\n### Custom save hook\n\n```python\nclass InvoiceAdmin(admin.ModelAdmin):\n    def save_model(self, request, obj, form, change):\n        obj.last_edited_by = request.user\n        super().save_model(request, obj, form, change)\n```\n\nWrites always go through `ModelAdmin.get_form()` → `form.is_valid()`\n→ `save_model()`. Signals, audit logs, and post-save hooks all fire\nexactly like they do in `/admin/`.\n\n### Use a custom `AdminSite`\n\n```python\n# myproject/admin.py\nfrom django.contrib.admin import AdminSite\n\nclass StaffAdminSite(AdminSite):\n    site_header = \"Operations Console\"\n    site_title  = \"Ops\"\n    index_title = \"Welcome\"\n\n    def has_permission(self, request):\n        return request.user.is_active and request.user.is_staff and \\\n               request.user.groups.filter(name=\"ops\").exists()\n\nstaff_admin = StaffAdminSite(name=\"staff\")\n\n# myproject/settings.py\nDJANGO_ADMIN_REACT = {\n    \"ADMIN_SITE\": \"myproject.admin.staff_admin\",\n}\n```\n\nThe SPA inherits the custom site's permission gate and the\n`ModelAdmin` registrations on that site — no parallel registry.\n\n### Plug in custom field types\n\n```python\n# yourapp/admin_react.py\nfrom django_admin_react.api.serializers import register_field_type\nfrom yourapp.fields import MoneyField\n\nregister_field_type(MoneyField, vocab_type=\"decimal\")\n# SPA renders MoneyField with the built-in decimal widget; no React\n# code required.\n```\n\nCoining a brand-new `vocab_type` (with a matching SPA widget) is an\n**API-repo** concern — open the issue at\n[`MartinCastroAlvarez/django-admin-api`](https://github.com/MartinCastroAlvarez/django-admin-api).\n\n### Pre-built `get_*` overrides still work\n\n`get_form`, `get_fieldsets`, `get_fields`, `get_exclude`,\n`get_readonly_fields`, `get_search_results`, `get_list_display`,\n`get_sortable_by`, `get_list_filter`, `get_actions` — all of them\nare called by the SPA the same way the HTML admin calls them. If\nyou customised them for `/admin/`, the SPA already honours those\ncustomisations.\n\n---\n\n## Feature status\n\nAll three packages are **Production / Stable** on PyPI. The\n`ModelAdmin`-driven REST API + the React SPA + the MCP adapter\nall share the v1 wire contract. Per-feature live status below.\n\n| `ModelAdmin` surface                                   | Backend (REST API)                                              |\n| ------------------------------------------------------ | --------------------------------------------------------------- |\n| Registry / list / detail / create / update / delete    | ✅                                                              |\n| `list_display`, `sortable_by`, `search_fields`         | ✅                                                              |\n| `list_filter` (boolean / choice / FK / date / Simple)  | ✅                                                              |\n| `date_hierarchy`                                       | ✅                                                              |\n| `list_editable` + bulk PATCH                           | ✅                                                              |\n| `actions` — batch + detail (signature-classified)      | ✅                                                              |\n| `autocomplete_fields` / `raw_id_fields`                | ✅                                                              |\n| `ManyToManyField` read + write                         | ✅                                                              |\n| `inlines` (TabularInline / StackedInline) — read + write | ✅                                                            |\n| `FileField` / `ImageField` — read                      | ✅                                                              |\n| `FileField` / `ImageField` — multipart upload          | 🟡 [#241](https://github.com/MartinCastroAlvarez/django-admin-react/issues/241) |\n| `JSONField` / `ArrayField` / range — read              | ✅                                                              |\n| range fields — write coercion                          | 🟡 [#238](https://github.com/MartinCastroAlvarez/django-admin-react/issues/238) |\n| `register_field_type` + per-model extension hook       | ✅                                                              |\n| React login / logout (Django session + CSRF)           | ✅                                                              |\n| Password set / change (`UserAdmin` parity)             | ✅                                                              |\n| Session-expiry re-login contract                       | ✅                                                              |\n| OpenAPI 3.1 schema at `/api/v1/schema/`                | ✅                                                              |\n| PWA manifest + service worker (cache-purge on logout)  | ✅                                                              |\n\n✅ = shipped. 🟡 = not yet built (tracked).\n\n---\n\n## The API surface\n\nThe SPA is a thin client over a small, closed REST surface. You can\nalso use these endpoints from any HTTP client (curl, your own\nfrontend, a script).\n\n| Method  | Path                                              | Purpose                                                                       |\n| ------- | ------------------------------------------------- | ----------------------------------------------------------------------------- |\n| `GET`   | `/api/v1/registry/`                               | All apps + models the current user can see, with their permissions.           |\n| `GET`   | `/api/v1/schema/`                                 | OpenAPI 3.1 schema for the envelopes + closed type vocabulary.                |\n| `GET`   | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/`                          | Paginated list. Honours `?search=`, `?ordering=`, `?page=`, `list_filter`.    |\n| `POST`  | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/`                          | Create. Runs `ModelAdmin.get_form()` + `form.is_valid()` + `save_model()`.    |\n| `GET`   | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003cpk\u003e/`                     | Detail with serialised fields, `permissions`, `inlines`, `panels`.            |\n| `PATCH` | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003cpk\u003e/`                     | Partial update. Same form pipeline as POST.                                   |\n| `DELETE`| `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003cpk\u003e/`                     | Hard delete via `ModelAdmin.delete_model()`.                                  |\n| `PATCH` | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/bulk/`                     | `list_editable` round-trip for multiple rows.                                 |\n| `POST`  | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003caction\u003e/`                 | Invoke a registered `ModelAdmin.actions` entry on a queryset.                 |\n| `GET`   | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/autocomplete/?q=…`         | `autocomplete_fields` lookup. Permission-gated on the **target** model.       |\n\nEvery endpoint is **staff-only by default** (or whatever\n`AdminSite.has_permission` returns), CSRF-required on unsafe\nmethods, and emits `Cache-Control: no-store`. Full wire contract\nlives in the API repo:\n[`MartinCastroAlvarez/django-admin-api`](https://github.com/MartinCastroAlvarez/django-admin-api).\n\n---\n\n## Examples\n\nSix runnable example projects ship with the repo under\n[`examples/`](examples/):\n\n| Project    | What it exercises                                                                                  |\n| ---------- | -------------------------------------------------------------------------------------------------- |\n| `library/` | `Author`, `Book`, `Genre` — basic CRUD, FKs, M2M, `search_fields`, `list_filter`.                  |\n| `fintech/` | `Account`, `Transaction` — permissions, queryset narrowing, custom actions.                        |\n| `blog/`    | `Post`, `Tag`, `Comment` — `list_editable`, `inlines`, `date_hierarchy`.                           |\n| `ecommerce/` | `Product`, `Order`, `LineItem` — fieldsets, readonly, `register_field_type` for `MoneyField`.    |\n| `hr/`      | `Employee`, `Department` — `autocomplete_fields`, `raw_id_fields`, organisational filters.         |\n| `project/` | Glue project that mounts every example app for an end-to-end demo.                                 |\n\nBoot any of them with:\n\n```bash\ncd examples/project\npython manage.py migrate\npython manage.py loaddata seed\npython manage.py runserver\n# → http://127.0.0.1:8000/admin/    (legacy admin)\n# → http://127.0.0.1:8000/admin-react/  (the React SPA)\n```\n\n---\n\n## What you get\n\n- **Plug-and-play**: works with any `ModelAdmin` you already have.\n- **Shared auth**: Django sessions, CSRF, staff permissions. No new\n  user model, no parallel permission system.\n- **Responsive, modern UI**: React + Tailwind + React Query, served\n  as a single bundle from `django_admin_react/static/admin_react/`.\n- **Extensible by editing `ModelAdmin`**, not React. Per-model SPA\n  extension hooks for the cases that genuinely need them.\n- **Configurable URL prefix** — `/admin/`, `/admin-react/`, anywhere.\n- **Conservative \u0026 secure-by-default** — never exposes models the\n  admin doesn't already expose; never writes fields the admin form\n  excludes; CSRF on every unsafe method; `Cache-Control: no-store`\n  on every API response; sensitive-name denylist on top of the\n  admin's own `exclude` rules.\n- **Boring + auditable** — no parallel permission system, no\n  client-side workarounds for backend permissions, conservative\n  serializer with `str()` fallback.\n\n---\n\n## License\n\nMIT — see [`LICENSE`](LICENSE).\n\n## Security\n\nPlease report security issues privately through GitHub's Private\nVulnerability Reporting on the repository (Security → Advisories).\nSee [`SECURITY.md`](SECURITY.md). Do **not** open a public issue.\n\n## Contributing\n\nOpen an [Issue](https://github.com/MartinCastroAlvarez/django-admin-react/issues/new)\nor a [Discussion](https://github.com/MartinCastroAlvarez/django-admin-react/discussions)\nbefore sending a PR for anything non-trivial. **API-side contributions** (any\n`/api/v1/...` endpoint, the wire contract, permission gates, serializer\ndenylist) go to [`MartinCastroAlvarez/django-admin-api`](https://github.com/MartinCastroAlvarez/django-admin-api)\n— this repo owns only the **React SPA super-layer** on top.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartincastroalvarez%2Fdjango-admin-react","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmartincastroalvarez%2Fdjango-admin-react","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartincastroalvarez%2Fdjango-admin-react/lists"}