Skip to main content

A drop-in React single-page admin for Django, driven entirely by ModelAdmin.

Project description

django-admin-react

A drop-in React single-page admin for any Django 5+ project. Same pip install, same INSTALLED_APPS, same urls.py include() — and your ModelAdmin classes drive everything. No React code on your side.

Pre-alpha. Available on PyPI as an alpha. Pin tightly; expect breaking changes between alpha releases. Track progress on the Project board and the Issues list.


Install

pip install django-admin-react
# settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django_admin_react",   # ← add this
    # ... your own apps
]
# urls.py
from django.urls import include, path

urlpatterns = [
    path("admin/", include("django_admin_react.urls")),
    # any prefix is fine:
    # path("admin-react/", include("django_admin_react.urls")),
    # path("staff/",       include("django_admin_react.urls")),
]

That is the entire integration. Log in as a staff user → modern, Tailwind-styled SPA driven by your existing ModelAdmin classes.

The wheel ships the pre-built React bundle. You do not need Node, pnpm, or any frontend toolchain to install or run.

Optional configuration

All settings are optional. Defaults shown:

DJANGO_ADMIN_REACT = {
    "ADMIN_SITE": "django.contrib.admin.site",   # dotted path to AdminSite instance
    "DEFAULT_PAGE_SIZE": 25,
    "MAX_PAGE_SIZE": 200,
    "ENABLE_PROFILING": False,
}

Requirements

  • Python: 3.10+
  • Django: 5.0, 5.1, 5.2, 6.0 (and any later 6.x)
  • Database: anything Django supports — the package is ORM-only, no direct SQL.
  • Auth: Django's built-in session + CSRF. Works with custom AUTH_USER_MODEL, custom AUTHENTICATION_BACKENDS, and custom AdminSite.has_permission.

Screenshots

Captured live with scripts/screenshots.sh from examples/project/ — real pixels, not mockups. The React SPA shell is in flight; until then, the images below show the legacy HTML admin running against the example apps — i.e., the experience django-admin-react modernises. Once the SPA renders, this section regenerates from the same script.

Login (the entry door) Admin index (legacy)
Login Admin index
Library / Authors — list view Library / Author — detail view
Author list Author detail
Mobile (375 px) API: GET /api/v1/registry/ JSON
Mobile list Registry JSON

Note for the next PyPI release: these image refs need to be absolute URLs (https://raw.githubusercontent.com/.../main/docs/screenshots/...) for the PyPI page to render them. The switch happens in the 0.1.0a2 release PR, which also requires the repo to be public so raw.githubusercontent.com resolves without auth. Until then, relative paths render correctly on the GitHub README.

Every screenshot uses a deterministic synthetic seed (no real people, accounts, or PII).


Extend without writing React

Everything below is just ModelAdmin. No JavaScript. No new classes. The UI follows whatever your admin declares.

Pick what columns appear on the list view

@admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
    list_display = ("number", "customer", "status", "total", "issued_at")

Make columns sortable

class InvoiceAdmin(admin.ModelAdmin):
    list_display   = ("number", "customer", "status", "total", "issued_at")
    sortable_by    = ("issued_at", "total")        # everything else is fixed

Add free-text search

class InvoiceAdmin(admin.ModelAdmin):
    search_fields  = ("number", "customer__name", "notes__icontains")
    # The SPA wires `?q=<term>` to `ModelAdmin.get_search_results` verbatim.

Default ordering

class InvoiceAdmin(admin.ModelAdmin):
    ordering = ("-issued_at",)

Hide a field from the form

class InvoiceAdmin(admin.ModelAdmin):
    exclude         = ("internal_audit_hash",)   # never reaches the SPA
    readonly_fields = ("total",)                 # rendered as read-only

The SPA respects exclude and readonly_fields exactly the way the legacy admin does. Sensitive-named fields (password, secret, token, api_key, hash, private_key, session, nonce, salt) are filtered on top of those rules as defense-in-depth.

Group fields into sections

class InvoiceAdmin(admin.ModelAdmin):
    fieldsets = (
        ("Identity",     {"fields": ("number", "customer")}),
        ("Money",        {"fields": ("subtotal", "tax", "total")}),
        ("Lifecycle",    {"fields": ("status", "issued_at", "paid_at")}),
        ("Internal",     {"fields": ("notes",), "classes": ("collapse",)}),
    )

Per-row permission gating

class InvoiceAdmin(admin.ModelAdmin):
    def has_add_permission(self, request):
        return request.user.has_perm("billing.create_invoice")

    def has_change_permission(self, request, obj=None):
        if obj is None:
            return request.user.has_perm("billing.change_invoice")
        return obj.owner_id == request.user.id   # row-level rule

    def has_delete_permission(self, request, obj=None):
        return False    # nobody deletes invoices

    def has_view_permission(self, request, obj=None):
        return request.user.has_perm("billing.view_invoice")

The SPA hides the Add / Save / Delete buttons automatically based on these. UI never invents a permission; it asks ModelAdmin.

Restrict the queryset

class InvoiceAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(owner=request.user)

The list view never sees rows the queryset excludes. No Model.objects.all() in the package — every list, search, and detail lookup starts at ModelAdmin.get_queryset(request).

Custom save hook

class InvoiceAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        obj.last_edited_by = request.user
        super().save_model(request, obj, form, change)

Writes always go through ModelAdmin.get_form()form.is_valid()save_model(). Signals, audit logs, and post-save hooks all fire exactly like they do in /admin/.

Use a custom AdminSite

# myproject/admin.py
from django.contrib.admin import AdminSite

class StaffAdminSite(AdminSite):
    site_header = "Operations Console"
    site_title  = "Ops"
    index_title = "Welcome"

    def has_permission(self, request):
        return request.user.is_active and request.user.is_staff and \
               request.user.groups.filter(name="ops").exists()

staff_admin = StaffAdminSite(name="staff")

# myproject/settings.py
DJANGO_ADMIN_REACT = {
    "ADMIN_SITE": "myproject.admin.staff_admin",
}

The SPA inherits the custom site's permission gate and the ModelAdmin registrations on that site — no parallel registry.

Pre-built form / queryset overrides still work

get_form, get_fieldsets, get_fields, get_exclude, get_readonly_fields, get_search_results, get_list_display, get_sortable_by — all of them are called by the SPA the same way the HTML admin calls them. If you customised them for /admin/, the SPA already honours those customisations.


What you get

  • Plug-and-play: works with any ModelAdmin you already have.
  • Shared auth: Django sessions, CSRF, staff permissions. No new user model, no parallel permission system.
  • Responsive, modern UI: React + Tailwind + React Query, served as a single bundle from django_admin_react/static/admin_react/.
  • Extensible by editing ModelAdmin, not React.
  • Configurable URL prefix/admin/, /admin-react/, anywhere.
  • Conservative & secure-by-default — never exposes models the admin doesn't already expose; never writes fields the admin form excludes; CSRF on every unsafe method; Cache-Control: no-store on every API response; sensitive-name denylist on top of the admin's own exclude rules.
  • Boring + auditable — no parallel permission system, no client-side workarounds for backend permissions, conservative serializer with str() fallback.

License

MIT — see LICENSE.

Security

Please report security issues privately through GitHub's Private Vulnerability Reporting on the repository (Security → Advisories). See SECURITY.md. Do not open a public issue.

Contributing

Humans and AI agents both welcome. Start with CONTRIBUTING.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_admin_react-0.1.0a2.tar.gz (296.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_admin_react-0.1.0a2-py3-none-any.whl (311.2 kB view details)

Uploaded Python 3

File details

Details for the file django_admin_react-0.1.0a2.tar.gz.

File metadata

  • Download URL: django_admin_react-0.1.0a2.tar.gz
  • Upload date:
  • Size: 296.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.4 CPython/3.12.7 Darwin/23.6.0

File hashes

Hashes for django_admin_react-0.1.0a2.tar.gz
Algorithm Hash digest
SHA256 e8142139976c92cef0103546788e24e37252fafafa5e6efcecbffe6bdc299a0e
MD5 4904942561c327d6ec7fb058331cdf45
BLAKE2b-256 e670e53ab1d61927f0a3cf808ad16c4de9217b0ca71db790785d476d6542378f

See more details on using hashes here.

File details

Details for the file django_admin_react-0.1.0a2-py3-none-any.whl.

File metadata

File hashes

Hashes for django_admin_react-0.1.0a2-py3-none-any.whl
Algorithm Hash digest
SHA256 8208f698878b50dfd64e1d9968990dae4bb1680c49897e84a28c0c96ef79f3db
MD5 af648c22a6985278bd01fddda587e823
BLAKE2b-256 e88449f00b6fc8f3ae33948544350a3daf2bd504bc3a366d1ed2f996f6d2b9c4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page