# SnapAdmin (django-snapadmin)

> Declarative Django admin and API generator. Define a model's fields once with `Snap*Field` and get a themed Django admin, a REST API with Swagger docs, a GraphQL endpoint, and optional Elasticsearch search. Every surface is a single `SNAPADMIN_*` settings toggle. Requires Python >= 3.10 and Django >= 5.2; the package is beta, so pin an exact version.

Install with `pip install django-snapadmin`, add `snapadmin` to `INSTALLED_APPS`, include `snapadmin.urls` in the root URLconf, then:

```python
# models.py
from snapadmin import fields as snap, models as snap_models

class Product(snap_models.SnapModel):
    name      = snap.SnapCharField(max_length=200, searchable=True, show_in_list=True)
    price     = snap.SnapDecimalField(max_digits=10, decimal_places=2, filterable=True)
    available = snap.SnapBooleanField(default=True, filterable=True)

# settings.py
SNAPADMIN_REST_API_ENABLED = True
SNAPADMIN_GRAPHQL_ENABLED = True
SNAPADMIN_SWAGGER_ENABLED = True

# admin.py
from snapadmin.models import SnapModel
SnapModel.register_all_admins()
```

Key facts an agent should not have to infer:

- Snap-only field kwargs (`searchable`, `filterable`, `show_in_list`, `wysiwyg`, …) configure the admin and API and are stripped before Django sees them — they add no migration. `snap_field(field, **kwargs)` sets the same kwargs on a plain `django.db.models.Field` instance in place, for a third-party field package or a model that cannot be rewritten onto the `Snap*Field` classes.
- Rich-text fields (`wysiwyg=True` / `SnapRichTextField`) sanitize HTML in `pre_save()`, so every ORM write path stores clean markup, and again when the changelist renders. Opt out per field with `safe_html=True` or `auto_sanitize=False`; `QuerySet.update()` bypasses it because Django never calls `pre_save()` there.
- Module import paths are the public contract and are never moved or renamed; `from snapadmin import SnapModel` and `from snapadmin.models import SnapModel` both work (the top-level re-exports are lazy).
- A model becomes a SnapAdmin model in one of two ways: subclassing `SnapModel`, or decorating a plain `django.db.models.Model` with `@snap_model(...)`. Both end in the same registry — `snapadmin.registry.is_registered(model)` is the gate every surface asks, `get_model_meta(model, name, default)` reads a setting (decorator value first, class attribute second). The decorator is metadata only: it adds no field (so no migration) and attaches no `EsManager`, no `purge_expired()`, no generated admin — the ES, retention and admin sweeps skip a decorated plain model, which is why it accepts no `es_*`/`data_retention_*` keywords.
- The Unfold theme is optional (`[theme]` extra). Without it SnapAdmin renders on the stock Django admin.
- `django-unfold` ships no translation catalogs, so SnapAdmin translates the theme's own interface strings in its ten catalogs (`snapadmin/theme_i18n.py` declares the msgids). A project's own `LOCALE_PATHS` still take precedence over both.
- Optional stacks never break an import: `snapadmin.tasks` imports without Celery (calling a task runs it in-process; `.delay()` raises `ImproperlyConfigured` naming the `[celery]` extra), and the core boots with DRF/graphene absent.
- Startup misconfiguration surfaces as Django system checks — warnings `snapadmin.W001`–`W007` and errors `snapadmin.E001`–`E005` (a `SNAPADMIN_MASKED_FIELDS` or `SNAPADMIN_MASKING_RULES` key/field that does not resolve, or an unusable masking pattern; those fail *open*, masking nothing, so they are errors). Read them before debugging behaviour.
- The audit trail stores its diff as `SnapadminAuditLog.changes = {field: {"old": …, "new": …}}`. Those key names are the on-disk format and are never renamed. Values keep their JSON-native type (numbers, booleans, null stay themselves); anything else is stored as text, as are rows written by releases before this one, so a consumer must accept both.
- Three console scripts, easy to conflate: `snapadmin-new` generates a project you keep (SQLite, no Docker, `migrate`+`runserver` work immediately); `snapadmin-demo` fetches and runs a throwaway demo project; `snapadmin-init` is read-only and only prints snippets to paste into an existing project. All three are stdlib-only and import no Django at module level.
- A `demo/` tree extracted by `snapadmin-demo` is a plain directory: `pip install -U django-snapadmin` upgrades the package but not that tree, which keeps its old models and templates. Re-run `snapadmin-demo` to refresh it — the run names the release the existing tree came from and drops files the new release no longer ships; `snapadmin_info` reports the mismatch too.
- Run `snapadmin-info` for a diagnostic report of what is enabled in a live project. It is a `manage.py` command with shell shims, so `snapadmin-info`, `snapadmin_info` and `python manage.py snapadmin_info` are the same thing; likewise for `snapadmin-license-check`. It suppresses Django's automatic system-check dump and reports the counts in its own section instead. Each section is isolated: a collector that raises renders as `Title: unavailable — …` (`collector_error` in `--json`) without aborting the report, and a crashed health probe still fails `--health-check`.

## Getting started

- [Installation](https://drofji.github.io/django-snapadmin/#installation): requirements, `INSTALLED_APPS` ordering (Unfold apps must precede `django.contrib.admin`), and the optional extras table.
- [New project with snapadmin-new](https://drofji.github.io/django-snapadmin/#scaffold): console script that generates a project you keep — `manage.py`, settings, one app with a worked `SnapModel`, SQLite, `.env`/`dist.env`; `migrate` then `runserver` work immediately, no Docker, no manual edits. `--full` adds a `Dockerfile`, `docker-compose.yml` and the Postgres/Redis/Elasticsearch wiring.
- [Quick start with snapadmin-demo](https://drofji.github.io/django-snapadmin/#snapadmin-demo): console script that fetches and runs a throwaway demo project.
- [Container health check](https://drofji.github.io/django-snapadmin/#healthcheck): `GET /api/health/` answers 200 healthy/degraded and **503** when the database is unreachable — the endpoint to point Docker, Compose, Coolify/Dokploy or Kubernetes probes at. Never probe `/admin/`: it answers 302 with the database down.
- [Integrate an existing project with snapadmin-init](https://drofji.github.io/django-snapadmin/#snapadmin-init): read-only inspection of a project that prints ready-to-paste snippets; it never edits source.
- [Integrating with your project](https://drofji.github.io/django-snapadmin/#integrating): wiring SnapAdmin into an existing Django codebase alongside plain `models.Model`.
- [Ecosystem compatibility](https://drofji.github.io/django-snapadmin/#compatibility): which Django versions, databases and third-party admin packages are supported.

## Declaring models

- [SnapModel reference](https://drofji.github.io/django-snapadmin/#snap-model): the declarative base, `register_all_admins()`, `es_storage_mode`, `data_retention_days`.
- [@snap_model for plain models](https://drofji.github.io/django-snapadmin/#snap-model-decorator): the decorator that opts a plain `django.db.models.Model` in without subclassing — its keyword table, and the table of what a decorated model deliberately does not get.
- [Snap fields](https://drofji.github.io/django-snapadmin/#snap-fields): every `Snap*Field` and its snap-only kwargs. [`snap_field()`](https://drofji.github.io/django-snapadmin/#snap-field-wrapper) sets the same kwargs on a plain Django field (a third-party field package, or a model that cannot be rewritten onto the `Snap*Field` classes) instead of requiring a `Snap*Field` subclass.
- [Advanced layout](https://drofji.github.io/django-snapadmin/#advanced-layout): fieldsets, inlines, ordering and grouping of the generated admin form.
- [Status badges](https://drofji.github.io/django-snapadmin/#status-badges): `SnapStatusBadgeField` and coloured choice rendering in list views.
- [Admin registration](https://drofji.github.io/django-snapadmin/#admin-registration): auto-registration, per-model opt-out, and hooking custom `ModelAdmin` behaviour.
- [Extending and overriding](https://drofji.github.io/django-snapadmin/#extending): replacing generated serializers, viewsets, admin classes and templates.

## APIs

- [REST API](https://drofji.github.io/django-snapadmin/#api-rest): generated CRUD routes, filtering, pagination, throttling, and the delete/write guards.
- [GraphQL API](https://drofji.github.io/django-snapadmin/#api-graphql): the generated Graphene schema and the GraphiQL playground.
- [Token management](https://drofji.github.io/django-snapadmin/#api-tokens): `APIToken`, hashed storage, and authenticating API calls.
- [Async background export](https://drofji.github.io/django-snapadmin/#async-export): `POST /api/exports/` streams a model's rows to a file on a Celery worker, with progress/ETA polling, cancellation and pluggable [row sources](https://drofji.github.io/django-snapadmin/#export-sources). `export_format` is `csv`, `json` (newline-delimited) or [`xlsx`](https://drofji.github.io/django-snapadmin/#export-xlsx). The line formats resume from a primary-key checkpoint after a crash; **xlsx does not** — a workbook is written whole, so a retry re-exports from the first row and a cancelled job leaves no partial file. `xlsx` needs the `[xlsx]` extra (openpyxl) and is rejected with a 400 without it; its cells keep their types and text starting with `=` is stored as text, never a formula.

## Operations

- [Elasticsearch integration](https://drofji.github.io/django-snapadmin/#elasticsearch): storage modes, query routing, reindexing, and database fallback.
- [Celery and periodic tasks](https://drofji.github.io/django-snapadmin/#celery): background tasks and the Beat schedules SnapAdmin registers.
- [Management-command rename](https://drofji.github.io/django-snapadmin/#command-rename): every command is `snapadmin_*`-prefixed; `db_backup`, `purge_expired_data` and `send_error_digest` are deprecated aliases of the prefixed names. Celery task names are unchanged.
- [GDPR data retention](https://drofji.github.io/django-snapadmin/#gdpr): `data_retention_days`, PII masking, and the purge command.
- [PII masking](https://drofji.github.io/django-snapadmin/#pii-masking): `SNAPADMIN_MASKED_FIELDS` declares which fields are sensitive; they are obfuscated in the admin changelist, dropped from the admin change form, and masked in REST, GraphQL and background exports for anyone without `snapadmin.view_raw_pii`. [`SNAPADMIN_MASKING_RULES`](https://drofji.github.io/django-snapadmin/#masking-rules) then sets the policy per field — a regex `pattern`+`replacement`, a flat `replacement` redaction, and/or a `permission` that unlocks that one field. Naming a field there also declares it sensitive. A pattern that will not compile, one that could backtrack catastrophically, or a value over 4096 characters falls back to the built-in masker, never to raw data.
- [Unalterable audit trail](https://drofji.github.io/django-snapadmin/#audit-trail): append-only `SnapadminAuditLog` for every admin create/update/delete, exported for a SIEM with `manage.py snapadmin_audit_export`. The admin renders each entry as a field-level diff and links every object to its [timeline](https://drofji.github.io/django-snapadmin/#audit-timeline) at `/admin/snapadmin/snapadminauditlog/timeline/<app_label>/<model>/<object_id>/` — capped at the 100 most recent entries per page, masked like every other surface.
- [Error monitoring and alerts](https://drofji.github.io/django-snapadmin/#error-monitoring): error capture, thresholds, cooldowns and digests.
- [Alert channels](https://drofji.github.io/django-snapadmin/#alert-channels): the same spike/digest/health alerts delivered by email and/or Slack, Discord, Teams, Telegram and plain-JSON webhooks, configured in `SNAPADMIN_ALERT_WEBHOOKS`. Posted with the stdlib — no new dependency. Thresholds, grouping and the cooldown are shared by all channels, so a webhook changes where an alert goes, never how often it fires; delivery is fail-soft and a run where every channel failed releases the cooldown instead of consuming it. Webhook URLs are secrets — never logged, never in an alert body, never in `snapadmin_info`.
- [3-2-1 database backups](https://drofji.github.io/django-snapadmin/#backups): local, network, SFTP and FTP targets with rotation. There is no S3 backup destination — use SFTP/FTPS or a mounted share.
- [Remote static/media/export storage](https://drofji.github.io/django-snapadmin/#remote-storage): one env var switches Django `STORAGES` to any S3-compatible provider (AWS, Hetzner Object Storage, MinIO, B2) via django-storages. Hetzner Storage Box is SFTP/CIFS, not S3.
- [Structured logging](https://drofji.github.io/django-snapadmin/#logging): the structlog wiring SnapAdmin expects.
- [Diagnostics — snapadmin_info](https://drofji.github.io/django-snapadmin/#snapadmin-info): the system-check/runtime/database/API/feature-adoption report, its flags, and the shell shims.
- [Licence audit — snapadmin_license_check](https://drofji.github.io/django-snapadmin/#license-check): dependency licences and their tiers.
- [Offline mode](https://drofji.github.io/django-snapadmin/#offline): running with no outbound network access.

## Configuration reference

- [Environment variables reference](https://drofji.github.io/django-snapadmin/#env-vars): every `SNAPADMIN_*` setting with its default. Start here for any "which setting turns X on" question.
- [Enterprise config](https://drofji.github.io/django-snapadmin/#enterprise-config): SSO, multi-database routing and hardening options.
- [Internationalization](https://drofji.github.io/django-snapadmin/#i18n): the ten shipped locales and how to add strings.
- [Theming and styles](https://drofji.github.io/django-snapadmin/#theming): the three CSS layers — one shared sheet plus exactly one theme layer (`admin-stock.css` without Unfold, `admin-unfold.css` with it) — and how to add your own.
- [Themed auth admin](https://drofji.github.io/django-snapadmin/#themed-auth-admin): with the Unfold theme installed, SnapAdmin re-registers Django's stock `User`/`Group` admins with Unfold's forms so the password row stays usable; `SNAPADMIN_THEME_AUTH_ADMIN = False` opts out.
- [Large-dataset performance](https://drofji.github.io/django-snapadmin/#performance) and [optimizations guide](https://drofji.github.io/django-snapadmin/#optimizations): estimated counts, pagination caps, and query-routing trade-offs.

## Project

- [README](https://github.com/drofji/django-snapadmin/blob/main/README.md): the same overview with the extras table and screenshots.
- [Changelog](https://github.com/drofji/django-snapadmin/blob/main/CHANGELOG.md): user-visible changes per release.
- [Migration guides](https://drofji.github.io/django-snapadmin/#migration-guides): upgrade steps between versions with breaking changes.
- [AI assistants](https://drofji.github.io/django-snapadmin/#ai-assistants): how this file and the in-package module map are meant to be used, and the tests that keep both honest.
- [Security policy](https://github.com/drofji/django-snapadmin/blob/main/SECURITY.md): supported versions, reporting, and the production-hardening checklist.
- [Third-party notices](https://github.com/drofji/django-snapadmin/blob/main/THIRD_PARTY_NOTICES.md): dependency licences; the base install is permissive-only (MIT/BSD/Apache) and safe for commercial use.
- [Source](https://github.com/drofji/django-snapadmin) · [PyPI](https://pypi.org/project/django-snapadmin/)

## Optional

- [Demo app model overview](https://drofji.github.io/django-snapadmin/#demo-models): the models the bundled demo declares, useful as worked examples.
- [Seed command](https://drofji.github.io/django-snapadmin/#demo-seed): populating the demo with sample data.
