Skip to main content

Define your Django models once โ€” instantly get an admin panel, REST API, GraphQL and full-text search. Zero boilerplate.

Project description

๐Ÿš€ SnapAdmin โ€” Declarative Django Admin & API Package

SnapAdmin is a declarative Django package that eliminates admin and API boilerplate. Define your model fields once โ€” get a feature-rich Django admin (powered by Unfold), a full REST API with Swagger docs, a dynamic GraphQL API, and optional Elasticsearch full-text search. Every surface (REST, GraphQL, Swagger, ES) can be switched on or off with a single setting, and expensive ?search= API queries are automatically routed to Elasticsearch when a model's data is mirrored there โ€” plain listings stay on the database.

Python Django License: MIT

๐Ÿ“š Full Documentation โ€” Configuration guide, API reference, examples


โšก The Core Idea โ€” 3 Steps, Full Stack

Define a model. Configure settings. Everything works.

# 1. Define a model
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)

    # 2. (Optional) Enable Elasticsearch + GDPR cleanup
    # es_storage_mode = snap_models.EsStorageMode.DUAL  # DB + ES in sync
    # data_retention_days = 365  # Auto-delete records older than 1 year

That's it. You instantly get:

What How
Django Admin Auto-registered, filtered, searchable โ€” no admin.py needed
REST API Full CRUD at /api/product/ with Swagger docs
GraphQL Dynamic schema at /api/graphql/
ES Search Optional โ€” enable with es_storage_mode = EsStorageMode.DUAL
GDPR Cleanup Optional โ€” enable with data_retention_days on any model

Elasticsearch Storage Modes

Mode Where data lives When to use
DB_ONLY (default) PostgreSQL only Any model where search speed isn't critical
DUAL PostgreSQL + Elasticsearch Full-text search on large product/article catalogs
ES_ONLY Elasticsearch only High-frequency write logs, analytics events

Searchable fields are declared in es_mapping (a {field: ES mapping} dict). Search through es_search() โ€” a fuzzy multi_match when ES is on, with an automatic ORM fallback when it's off, so the same call works everywhere (snap_search() is an alias):

# Fuzzy, typo-tolerant full-text search (default limit=20)
results = Product.es_search("wireles headphones")   # typo still matches
top5    = Product.es_search("laptop", limit=5)
browse  = Product.es_search(limit=100)              # no query โ†’ match-all, newest first

# ES_ONLY models are read *only* through es_search() (no DB table)
logs = SearchLog.es_search("error 404")

# DB_ONLY models answer too โ€” falls back to an ORM icontains query
Article.es_search("django")   # works even with ELASTICSEARCH_ENABLED=False

For DB_ONLY/DUAL it returns a Django QuerySet; for ES_ONLY a lightweight EsQuerySet of instances built from the index.

Full-text queries target only the text-capable fields of es_mapping (with lenient: true), so mixed mappings with numeric/date fields never break a search. Index-level settings โ€” custom analyzers, shards โ€” go into es_index_settings (applied when the index is first created):

class Product(snap_models.SnapModel):
    es_storage_mode = snap_models.EsStorageMode.DUAL
    es_mapping = {
        "name":  {"type": "text", "analyzer": "de_analyzer"},
        "price": {"type": "float"},
    }
    es_index_settings = {
        "analysis": {"analyzer": {"de_analyzer": {"type": "german"}}},
        "number_of_shards": 1,
    }

Don't want to write mappings at all? Set es_auto_mapping = True and the mapping is derived from your model fields โ€” CharField/TextField become text with a .raw keyword subfield (exact match + aggregations), Email/Slug/URL/UUID/IP โ†’ keyword, integers/FK โ†’ long, Decimal โ†’ scaled_float, dates โ†’ date, JSONField โ†’ object. Anything you declare in es_mapping overrides the derived entry for that field:

class SearchLog(snap_models.SnapModel):
    query         = snap.SnapCharField(max_length=255, searchable=True)
    results_count = snap.SnapIntegerField()

    es_storage_mode = snap_models.EsStorageMode.ES_ONLY
    es_auto_mapping = True          # mapping derived from the fields above
    # es_mapping = {"query": {"type": "search_as_you_type"}}   # optional override

To change settings/mappings of an existing index: delete it, then run Product.es_reindex_all() โ€” it recreates the index with the current definition and streams all rows through the bulk API (one round-trip per 500 docs, flat memory). Every swallowed ES failure (index creation, indexing, search fallback) is logged as a structlog warning, so outages are visible instead of silent.


๐Ÿ‘€ What You'll See

After running docker compose up --build and visiting http://localhost:8000/admin/:

Django Admin (powered by Unfold)

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SnapAdmin                           ๐Ÿ” Search...    admin โ–พโ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  DEMO APP    โ”‚  Products                         + Add     โ”‚
โ”‚  Categories  โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚
โ”‚  Tags        โ”‚ โ”‚ Name            Price  In Stock  Category โ”‚โ”‚
โ”‚  Products    โ”‚ โ”‚ Premium Laptop  $249   โ— Active   Audio   โ”‚โ”‚
โ”‚  Customers   โ”‚ โ”‚ Ergonomic Mouse $89    โ— Active   Access. โ”‚โ”‚
โ”‚  Orders      โ”‚ โ”‚ USB-C Hub       $49    โ—‹ Out      Electr. โ”‚โ”‚
โ”‚  Audit Logs  โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚
โ”‚  Showcase    โ”‚  Sidebar filters: Price range โ”‚ Available   โ”‚
โ”‚  SYSTEM      โ”‚                   Category    โ”‚             โ”‚
โ”‚  Dashboard   โ”‚                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

REST API Docs (Swagger UI) โ€” http://localhost:8000/api/docs/

GET  /api/product/         List all products (filterable, paginated)
POST /api/product/         Create a product
GET  /api/product/{id}/    Retrieve a product
PUT  /api/product/{id}/    Update a product
DEL  /api/product/{id}/    Delete a product
GET  /api/customer/        โ€ฆsame for every SnapModel

GraphQL Playground โ€” http://localhost:8000/api/graphql/

query {
  allProducts(first: 10) {
    edges { node { id name price available } }
  }
}

System Dashboard โ€” http://localhost:8000/admin/snapadmin/dashboard/

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  System Dashboard                          v0.1.0a5     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ Product  โ”‚ โ”‚ Customer โ”‚ โ”‚  Order   โ”‚ โ”‚ AuditLog โ”‚  โ”‚
โ”‚  โ”‚  50 rows โ”‚ โ”‚  50 rows โ”‚ โ”‚  50 rows โ”‚ โ”‚  50 rows โ”‚  โ”‚
โ”‚  โ”‚  [dual]  โ”‚ โ”‚ [db_only]โ”‚ โ”‚ [db_only]โ”‚ โ”‚ 90d ret. โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚  Cron Jobs                                              โ”‚
โ”‚  reindex_products_to_es   daily at 02:00               โ”‚
โ”‚  purge_expired_data        daily at 03:00               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ“ฆ SnapAdmin Package Features

The core snapadmin package provides everything you need to bootstrap your project's admin and API:

Feature Description
Declarative Admin Configure list_display, search_fields, list_filter directly in your models using SnapField.
Beautiful UI Native integration with django-unfold for a modern, responsive admin experience.
Status Badges Easily add color-coded HTML badges for choices and status fields.
Advanced Layout Support for horizontal field rows and tabbed interfaces within the admin form.
Range Filters Built-in date and numeric range filters for efficient data exploration.
Change Logging Automatic tracking of field-level changes (old โ†’ new) with a dedicated history view.
Automatic REST API Instantly generated CRUD endpoints for every SnapModel with zero extra code.
Dynamic GraphQL API Automatically generated GraphQL schema with support for complex data fetching.
Token Auth Expirable API tokens with granular model-level access control. Keys are hashed at rest (SHA-256) and shown only once, at creation.
Configurable Easily enable/disable REST API, GraphQL, Swagger docs, and search modes via settings.
Elasticsearch Ready Multi-mode storage (DB_ONLY, DUAL, ES_ONLY) for blazing fast search.
Smart ES Query Routing ?search= REST queries on DUAL models run on Elasticsearch automatically (fuzzy, relevance-ranked); plain listings stay on the DB. Toggle globally (SNAPADMIN_ES_QUERY_ROUTING) or per model (es_query_routing).
Auto ES Mapping es_auto_mapping = True derives the index mapping from your model fields (text + .raw keyword subfields, dates, numerics); es_mapping entries override per field, es_index_settings adds analyzers/shards.
Secured GraphQL Every resolver enforces authentication (session or API token) + per-model Django permissions โ€” the same contract as REST. search/first/offset arguments included.
API Field Privacy api_exclude_fields hides sensitive columns from REST, GraphQL and schema introspection while the admin keeps showing them.
GDPR Data Retention Per-model data_retention_days parameter with automatic Celery cleanup task.
Error Monitoring & Email Alerts Optional middleware records every unhandled exception / 5xx as a browsable ErrorEvent; emails a spike alert when N errors hit within 15 minutes and a daily grouped digest (Celery Beat or cron) โ€” thresholds, window, recipients and send time all configurable.
3-2-1 Database Backups Scheduled DB dumps (SQLite copy / pg_dump, gzip) shipped to three configurable destinations โ€” a local directory, a network server (mounted share) and a remote offsite FTP/FTPS โ€” each with its own frequency and retention.
Offline Mode Per-model offline_mode toggle: prefetches the last offline_cache_limit rows into IndexedDB, polls /api/health/ for real backend availability, shows dynamic toasts + a saved-objects panel, and syncs on reconnect.
Large-Dataset Tuning Auto-derived list_select_related (no admin N+1), plus per-model list_per_page / show_full_result_count knobs for million-row tables.
Structured Logging Integrated structlog for readable local logs and JSON logs in production.

๐Ÿ— Package Architecture

snapadmin/
โ”œโ”€โ”€ api/             # REST & GraphQL API core: views, serializers, auth
โ”œโ”€โ”€ management/      # Custom management commands
โ”œโ”€โ”€ migrations/      # Core package migrations (e.g., APIToken)
โ”œโ”€โ”€ static/          # UI assets (CSS, JS, SVG logos)
โ”œโ”€โ”€ templates/       # Custom admin templates & dashboard
โ”œโ”€โ”€ fields.py        # SnapField definitions with admin introspection
โ”œโ”€โ”€ models.py        # SnapModel base, EsManager, and core logic
โ””โ”€โ”€ urls.py          # Auto-configurable API and documentation routes

๐Ÿš€ Quickstart: Installation

From PyPI (Recommended)

pip install django-snapadmin

From GitHub (Latest/Development)

pip install git+https://github.com/drofji/django-snapadmin.git

๐Ÿ›  Usage & Configuration

1. Configure Settings

Add the required apps to INSTALLED_APPS in settings.py. Order matters โ€” unfold and its contrib apps must come before django.contrib.admin, and django_ckeditor_5 must be present because SnapModel imports the CKEditor 5 widget at load time:

INSTALLED_APPS = [
    # Theme โ€” must precede django.contrib.admin
    "unfold",
    "unfold.contrib.filters",
    "unfold.contrib.forms",
    "unfold.contrib.inlines",

    # WYSIWYG (required โ€” imported by SnapModel)
    "django_ckeditor_5",

    # Django core
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # SnapAdmin stack (all required)
    "rest_framework",
    "drf_spectacular",
    "django_filters",
    "graphene_django",
    "snapadmin",

    # Optional โ€” only with the [celery] extra
    # "django_celery_beat",
    # "django_celery_results",

    # Your apps โ€ฆ
]

Installing django-snapadmin pulls in django-unfold, django-ckeditor-5, djangorestframework, drf-spectacular, django-filter and graphene-django automatically โ€” you only need to list them in INSTALLED_APPS.

2. Define your Model

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)
    # Group fields into a single horizontal row
    price = snap.SnapDecimalField(max_digits=10, decimal_places=2, row="pricing")
    available = snap.SnapBooleanField(default=True, row="pricing")

3. Register Admin

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

Theming & Styles

SnapAdmin ships its admin styling as two layers so it never forces theme assumptions on installs that don't use Unfold:

Stylesheet Scope When it loads
snapadmin/css/admin.css Theme-agnostic core (field sizing, Select2, action bar, :root design tokens) Always, on every SnapModel admin page
snapadmin/css/admin-unfold.css Unfold-specific overrides (.unfold-scoped rules, dark-mode borders, Add-button fix) Only when django-unfold is installed

The Unfold layer is opt-in: it is appended automatically (after the core sheet, so its rules win the cascade) only when Unfold is detected. A plain Django admin install โ€” or one on another theme โ€” gets the core sheet alone and is never styled with Unfold assumptions. The shared design tokens (--primary-color, --radius, โ€ฆ) live in the core sheet so both layers reference the same values.

Available Field Types

Field Django Equivalent SnapAdmin Extras
SnapCharField CharField searchable, filterable
SnapTextField TextField -
SnapRichTextField TextField wysiwyg=True preset - no extra arg needed
SnapEmailField EmailField -
SnapPhoneField CharField phone validation, max_length=20 preset
SnapColorField CharField hex color validation (#RRGGBB), max_length=7 preset
SnapSlugField SlugField max_length=50 preset
SnapURLField URLField -
SnapUUIDField UUIDField -
SnapIntegerField IntegerField filterable
SnapSmallIntegerField SmallIntegerField filterable
SnapPositiveIntegerField PositiveIntegerField filterable
SnapPositiveSmallIntegerField PositiveSmallIntegerField filterable
SnapPositiveBigIntegerField PositiveBigIntegerField filterable
SnapBigIntegerField BigIntegerField filterable
SnapFloatField FloatField filterable
SnapDecimalField DecimalField filterable
SnapDateField DateField date range filter
SnapDateTimeField DateTimeField date range filter
SnapTimeField TimeField -
SnapDurationField DurationField -
SnapBooleanField BooleanField filterable
SnapJSONField JSONField -
SnapGenericIPAddressField GenericIPAddressField -
SnapFileField FileField extension/size/encoding validation
SnapImageField ImageField -
SnapForeignKey ForeignKey autocomplete
SnapOneToOneField OneToOneField autocomplete
SnapManyToManyField ManyToManyField -
SnapFunctionField - computed display column
SnapStatusBadgeField - colored HTML badge column

โš™๏ธ Feature Toggles & Advanced Settings

Every SnapAdmin surface can be enabled or disabled independently via Django settings. Disabling a toggle removes the corresponding URL routes entirely (requests return 404):

# Feature toggles
SNAPADMIN_REST_API_ENABLED = True   # REST CRUD endpoints (/api/models/โ€ฆ, /api/tokens/โ€ฆ)
SNAPADMIN_GRAPHQL_ENABLED = True    # GraphQL endpoint (/api/graphql/)
SNAPADMIN_SWAGGER_ENABLED = True    # Swagger UI + ReDoc (/api/docs/, /api/redoc/)
ELASTICSEARCH_ENABLED = False       # Elasticsearch integration as a whole

# Smart ES query routing (see "REST API in Practice" below)
SNAPADMIN_ES_QUERY_ROUTING = True   # Route ?search= on DUAL models to Elasticsearch
SNAPADMIN_ES_SEARCH_LIMIT = 1000    # Max hits fetched from ES per routed search

# Observability & GraphQL security
SNAPADMIN_QUERY_BACKEND_HEADER = True  # X-Snap-Query-Backend header on list responses
SNAPADMIN_GRAPHQL_REQUIRE_AUTH = True  # Auth + per-model perms on every resolver
SNAPADMIN_GRAPHIQL_ENABLED = DEBUG     # GraphiQL playground โ€” keep out of production

Per-model opt-out from query routing (e.g. when the ES mirror of one model lags):

class Product(snap_models.SnapModel):
    es_storage_mode = snap_models.EsStorageMode.DUAL
    es_query_routing = False   # this model's API searches always run on the DB

Hide sensitive columns from the whole API surface (REST, GraphQL, schema introspection) while keeping them in the admin:

class AuditLog(snap_models.SnapModel):
    action     = snap.SnapCharField(max_length=100, searchable=True)
    user_email = snap.SnapEmailField()          # PII

    api_exclude_fields = ["user_email"]         # never leaves the server via API

๐Ÿ”Ž REST API in Practice โ€” Query Examples

All examples assume an API token (create one in the admin under API Tokens, or via POST /api/tokens/ โ€” the raw key is shown once):

TOKEN="your-40-char-token-key"
BASE="http://localhost:8000/api"

# Discover every available endpoint and its fields
curl -H "Authorization: Token $TOKEN" "$BASE/models/schema/"

# Plain listing โ€” paginated, served by the database
curl -H "Authorization: Token $TOKEN" "$BASE/models/demo/Product/?page=2"

# Field filters (auto-generated from field types, visible in Swagger)
curl -H "Authorization: Token $TOKEN" "$BASE/models/demo/Product/?available=true&price__gte=100"

# Full CRUD
curl -X POST -H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
     -d '{"name": "Laptop Pro", "price": "1499.00", "available": true}' \
     "$BASE/models/demo/Product/"
curl -X PATCH -H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
     -d '{"available": false}' "$BASE/models/demo/Product/42/"
curl -X DELETE -H "Authorization: Token $TOKEN" "$BASE/models/demo/Product/42/"

Full-text search โ€” automatically routed to Elasticsearch

?search= runs against the model's searchable=True fields. For a DUAL-storage model (data mirrored in ES) the very same request is executed on Elasticsearch โ€” fuzzy, typo-tolerant, relevance-ranked โ€” with no change to the URL or your client code. Filters and pagination still apply on top of the ES-ranked result:

# Product is DUAL โ†’ this search runs on Elasticsearch (typo still matches)
curl -i -H "Authorization: Token $TOKEN" "$BASE/models/demo/Product/?search=laptp"
# HTTP/1.1 200 OK
# X-Snap-Query-Backend: elasticsearch     โ† the search ran on ES
# {"count": 3, "results": [{"id": 42, "name": "Laptop Pro", โ€ฆ}, โ€ฆ]}

# Combine ES search with DB filters and pagination โ€” still one request
curl -H "Authorization: Token $TOKEN" \
     "$BASE/models/demo/Product/?search=laptop&available=true&page=1"

# The same URL on a DB_ONLY model transparently uses SQL icontains instead
curl -i -H "Authorization: Token $TOKEN" "$BASE/models/demo/Customer/?search=7"
# X-Snap-Query-Backend: database          โ† no ES mirror, DB handled it

Routing decision per request, in order:

Model mode ?search= present ES routing on Executed on
ES_ONLY any โ€” Elasticsearch (only source)
DUAL yes yes Elasticsearch (fuzzy multi_match, relevance order)
DUAL yes no Database (icontains over searchable fields)
DUAL no โ€” Database (native pagination, no ES round-trip)
DB_ONLY yes โ€” Database (icontains over searchable fields)

Every list response carries the X-Snap-Query-Backend: elasticsearch | database header, so you can always verify where a query ran โ€” including the case where ES failed mid-request and the DB fallback silently answered (the header then says database). Hide the header in production with SNAPADMIN_QUERY_BACKEND_HEADER = False.

GraphQL โ€” same tokens, same permissions

GraphQL enforces the same access contract as REST: every resolver requires an authenticated caller (admin session or Authorization: Token) holding the model's view permission; a token's allowed_models scope applies on top. List fields accept search (ES-routed for DUAL/ES_ONLY models), first and offset:

curl -H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
     -d '{"query": "{ allDemoProducts(search: \"laptop\", first: 10) { id name price } }"}' \
     "http://localhost:8000/api/graphql/"

# Anonymous callers get {"errors": [{"message": "Authentication required."}]}

GraphiQL (the interactive playground) follows DEBUG by default โ€” override with SNAPADMIN_GRAPHIQL_ENABLED. Auth enforcement can be disabled for private deployments with SNAPADMIN_GRAPHQL_REQUIRE_AUTH = False (not recommended).

๐Ÿ”‘ API Token Security

API tokens authenticate REST/GraphQL requests via the Authorization: Token <key> header.

from snapadmin.models import APIToken

token = APIToken.create_for_user(
    user=user,
    token_name="CI Pipeline",
    allowed_models=["myapp.Product", "myapp.Order"],  # optional scope
    expires_in_days=30,
)
print(token.token_key)  # raw key โ€” available ONLY here, right after creation
  • Hashed at rest โ€” only a SHA-256 token_digest and the non-secret 8-char token_prefix are stored. The raw token_key is never persisted; it is returned exactly once (the POST /api/tokens/ response, or a one-time admin message). Afterwards token_key is None and only token_prefix identifies the token. Authentication looks the presented key up by its digest.
  • allowed_models โ€” empty โ‰  unrestricted. An empty list means "any model the owning user already has Django permissions for"; the token scope is always AND-ed with user.has_perm. A non-empty list narrows access to exactly those "app_label.ModelName" entries.

GDPR Data Retention

Add automatic record cleanup to any model with two class attributes:

class AuditLog(snap_models.SnapModel):
    action = snap.SnapCharField(max_length=100)
    created_at = snap.SnapDateTimeField(auto_now_add=True)

    # Auto-delete records older than 90 days
    data_retention_days = 90
    data_retention_field = "created_at"  # default; can point to any DateTimeField

Records are removed by the purge_expired_data Celery task (schedule it with Celery Beat) or manually:

python manage.py purge_expired_data         # live run
python manage.py purge_expired_data --dry-run  # preview only

Or programmatically, per model โ€” returns the number of records purged:

AuditLog.purge_expired()              # delete expired rows now
AuditLog.purge_expired(dry_run=True)  # count only, delete nothing

The purge is storage-aware, so personal data never lingers in a secondary store:

Mode What gets purged
DB_ONLY Bulk delete from the database.
DUAL Database rows and the mirrored Elasticsearch documents.
ES_ONLY Range delete_by_query against the index on data_retention_field.

A plain QuerySet.delete() never calls each model's delete(), so a naรฏve bulk purge would leave the Elasticsearch copy behind. purge_expired() closes that gap for DUAL and ES_ONLY models (ES operations require ELASTICSEARCH_ENABLED=True).


๐Ÿšจ Error Monitoring & Email Alerts

Optional, zero-dependency error notifications. One middleware records every unhandled exception and 5xx response as an ErrorEvent (browsable in the admin under Error Events), and two email channels keep you informed:

  • Spike alert โ€” when SNAPADMIN_ERROR_ALERT_THRESHOLD errors occur within SNAPADMIN_ERROR_ALERT_WINDOW_MINUTES (default: 20 errors / 15 min), one email goes out immediately. A cooldown guarantees at most one alert per window โ€” no inbox floods.
  • Daily digest โ€” a grouped 24-hour report (identical errors are merged by exception class + endpoint, most frequent first). The digest is capped at SNAPADMIN_ERROR_DIGEST_MAX_GROUPS groups so it stays readable even on a bad day.

Prerequisite: working Django email settings (EMAIL_BACKEND, EMAIL_HOST, โ€ฆ โ€” i.e. a configured SMTP server) and DEFAULT_FROM_EMAIL. No emails are sent while the recipient lists are empty, so the feature is safely inert until you opt in.

1. Enable the middleware:

MIDDLEWARE = [
    # ... Django middleware ...
    "snapadmin.middleware.SnapErrorMonitorMiddleware",
]

2. Configure (all values shown are the defaults):

SNAPADMIN_ERROR_MONITOR_ENABLED = True        # master kill-switch (no MIDDLEWARE edit needed)

# Spike alert
SNAPADMIN_ERROR_ALERT_ENABLED = True
SNAPADMIN_ERROR_ALERT_THRESHOLD = 20          # errors ...
SNAPADMIN_ERROR_ALERT_WINDOW_MINUTES = 15     # ... within this window โ†’ email
SNAPADMIN_ERROR_ALERT_EMAILS = ["ops@example.com"]

# Daily digest
SNAPADMIN_ERROR_DIGEST_ENABLED = True
SNAPADMIN_ERROR_DIGEST_EMAILS = ["team@example.com"]  # falls back to ALERT_EMAILS
SNAPADMIN_ERROR_DIGEST_MAX_GROUPS = 20        # cap on distinct error groups per email

# Housekeeping: ErrorEvents older than this are purged by the digest task
SNAPADMIN_ERROR_RETENTION_DAYS = 30

3. Schedule the digest โ€” pick the send time via Celery Beat:

CELERY_BEAT_SCHEDULE = {
    "send-error-digest": {
        "task": "api.tasks.send_error_digest",
        "schedule": crontab(hour=8, minute=0),   # your choice of send time
    },
}

โ€ฆor without Celery, from cron:

0 8 * * *  python manage.py send_error_digest        # last 24h, grouped
python manage.py send_error_digest --hours 12        # custom window

Monitoring is fail-safe by design: storage or SMTP failures are logged (error_monitor_record_failed, error_monitor_alert_failed) and swallowed โ€” a broken mail server never breaks your pages. Try it in the demo: hit http://localhost:8000/demo/error/ a few times (DEBUG only) and watch Error Events fill up; alert emails land in the console with the default DEBUG email backend.


๐Ÿ’พ 3-2-1 Database Backups

Built-in scheduled backups following the classic 3-2-1 rule โ€” keep 3 copies of your data, on 2 different machines, 1 of them offsite:

Copy Destination Where it lives Default frequency
1 local A directory on the same server (SNAPADMIN_BACKUP_LOCAL_DIR) every 24 h
2 network A directory on another server on your network, reachable as a mounted NFS/SMB share (SNAPADMIN_BACKUP_NETWORK_DIR) every 24 h
3 remote An offsite server anywhere in the world via FTP/FTPS (SNAPADMIN_BACKUP_FTP_*) every 168 h (weekly)

Dumps are gzip-compressed โ€” a file copy for SQLite, pg_dump for PostgreSQL โ€” and each destination keeps the newest SNAPADMIN_BACKUP_KEEP dumps (oldest pruned automatically, including on the FTP server).

Configuration (each destination has its own schedule โ€” tune how often the local, network and remote copies are refreshed independently):

SNAPADMIN_BACKUP_ENABLED = True               # strictly opt-in (default: False)
SNAPADMIN_BACKUP_KEEP = 7                     # dumps kept per destination

# Copy 1 โ€” same server
SNAPADMIN_BACKUP_LOCAL_DIR = "/var/backups/snapadmin"
SNAPADMIN_BACKUP_LOCAL_EVERY_HOURS = 24       # daily

# Copy 2 โ€” another server on the same network (mounted share); empty = off
SNAPADMIN_BACKUP_NETWORK_DIR = "/mnt/backup-server/snapadmin"
SNAPADMIN_BACKUP_NETWORK_EVERY_HOURS = 24     # daily

# Copy 3 โ€” offsite FTP/FTPS; empty host = off
SNAPADMIN_BACKUP_FTP_HOST = "backup.example.com"
SNAPADMIN_BACKUP_FTP_PORT = 21
SNAPADMIN_BACKUP_FTP_USER = "backup"
SNAPADMIN_BACKUP_FTP_PASSWORD = "secret"
SNAPADMIN_BACKUP_FTP_DIR = "/snapadmin"
SNAPADMIN_BACKUP_FTP_TLS = True               # FTPS (recommended)
SNAPADMIN_BACKUP_REMOTE_EVERY_HOURS = 168     # weekly

Running โ€” the scheduler is a separate process from your web workers. Add the api.tasks.run_db_backups task to Celery Beat (an hourly check; each destination only fires when its own interval has elapsed โ€” last-run times persist across restarts):

CELERY_BEAT_SCHEDULE = {
    "run-db-backups": {
        "task": "api.tasks.run_db_backups",
        "schedule": crontab(minute=30),   # hourly due-check
    },
}

โ€ฆor without Celery, from cron:

30 * * * *  python manage.py db_backup            # ships only what is due
python manage.py db_backup --force                # all configured destinations, now
python manage.py db_backup --destination remote   # one destination, now

A failed destination (unreachable share, FTP down) is reported and logged but never cancels the other copies โ€” and it stays "due", so it is retried on the next pass.


๐Ÿ“ด Offline Mode

Make a model's admin list view survive a dropped connection with a single toggle:

class Customer(snap_models.SnapModel):
    first_name = snap.SnapCharField(max_length=100, show_in_form=True)
    last_name = snap.SnapCharField(max_length=100, show_in_form=True)

    # Cache this model's list view client-side and enable offline support
    offline_mode = True
    # Prefetch only the 50 most-recent rows for offline view (default: 100)
    offline_cache_limit = 50

When offline_mode = True, SnapAdmin injects snapadmin/js/offline.js into that model's admin pages only. It then:

  • Prefetches the most-recent offline_cache_limit rows (default 100) from GET /api/offline-data/<app>/<model>/ and stores them in the browser's IndexedDB on every visit (the rendered list is kept as a fallback snapshot).
  • Repaints the list from cache when the backend becomes unreachable, and shows a saved-objects panel โ€” how many objects are cached (out of the limit), when they were cached, and how many changes are queued for sync.
  • Queues mutations made while offline and replays them on reconnect, then refreshes the cache and shows a "synced N changes" toast.

Real backend health checks, not just navigator.onLine

Connectivity is decided by whether the Django backend actually answers, not by the OS network flag โ€” a laptop can hold a Wi-Fi link while the server is down or the VPN dropped. A lightweight connectivity.js loads on all SnapModel admin pages and:

  • Polls GET /api/health/ (every 15s by default โ€” set window.SNAPADMIN_HEALTH_INTERVAL to override) with a short timeout, and re-checks immediately on browser online/offline events and tab refocus. The backend is "up" only when it responds.
  • Publishes one shared state as a snapadmin:connectivity DOM event, so the connectivity layer and the per-model engine always agree.
  • Dynamic toasts, not static banners โ€” backend-lost / restored, "objects can't be shown right now" (non-cached pages), and "synced N changes" surface as auto-dismissing toasts.

On models that are not offline-capable, losing the backend shows a warning toast, blocks form submission, and disables the Save buttons until it returns (preventing silent data loss) while leaving the already-rendered page intact. Sidebar badges mark every model link so you can see which models sync offline: a green sync icon (spins while the backend is down) for offline-capable models, a muted no-offline icon for the rest.

The badge list and per-model cache limits are served by GET /api/offline-models/ (authenticated), with a localStorage fallback so badges still render while offline.

No settings, migrations, or extra dependencies are required โ€” it is pure client-side behavior gated per model. Models without the flag still get the connectivity warnings but ship no caching JS.


โšก Large-Dataset Performance

SnapAdmin is built to stay responsive as tables grow. Most of the tuning is automatic, and the rest is a handful of per-model knobs.

Automatic โ€” no admin N+1

For every model, register_admin() inspects the columns shown in the list view and auto-derives list_select_related from the ForeignKey columns among them. A list view that renders a related column (or a __str__ that walks a relation) therefore issues one joined query, not one query per row. Only the FKs actually displayed are joined โ€” relations you don't show are never pulled.

The auto-generated REST API does the same on its querysets: select_related() for ForeignKeys and prefetch_related() for many-to-many fields, with the field lists cached per model to keep introspection out of the hot path.

Per-model knobs

Override these class attributes on any SnapModel to tune the admin list view:

class AuditLog(snap_models.SnapModel):
    action = snap.SnapCharField(max_length=100, searchable=True)

    list_per_page = 50              # rows per page (default 100)
    list_max_show_all = 200         # cap on the "Show all" link
    show_full_result_count = False  # skip the unfiltered COUNT(*) on huge tables
Attribute Default When to change it
list_per_page 100 Lower it for wide rows or heavy templates.
list_max_show_all 200 Guards against a "Show all" on a million-row table.
show_full_result_count True Set False on very large tables โ€” the admin then skips the second, unfiltered COUNT(*) it runs to show the grand total, which is often the single most expensive query.

REST pagination

The REST API paginates by default (PageNumberPagination, PAGE_SIZE = 25), so large collections are never serialized in one response. Tune it via the REST_FRAMEWORK setting.

Offloading search to Elasticsearch

For DUAL models the expensive part โ€” full-text ?search= โ€” is routed to Elasticsearch automatically, while plain listings keep the database's native pagination (no row cap, no extra round-trip). ES_ONLY models are always served from ES, since no DB table exists. See REST API in Practice above for the routing matrix and the X-Snap-Query-Backend response header.

Benchmarking at scale

Two demo management commands let you reproduce the numbers on your own hardware:

# Bulk-seed 100k customers + orders (batched bulk_create, flat memory)
python manage.py seed_large --count 100000

# Time the Order changelist queryset with vs without list_select_related
python manage.py benchmark_list_view --model order

benchmark_list_view iterates the changelist queryset and touches each row's ForeignKey, so the unoptimized run pays the full N+1 cost while the optimized run issues a single joined query. Representative output on a seeded table:

๐Ÿ“Š  Result
   WITHOUT :    5,001 queries       584.5 ms
   WITH    :        1 queries        37.8 ms

   Query reduction : 5,001 โ†’ 1  (5001ร— fewer)
   Speedup         : 15.5ร— faster wall time

The query count for the unoptimized path scales linearly with row count (N + 1), while the optimized path stays flat at 1 โ€” exactly the N+1 elimination list_select_related is there to provide.

Going deeper

The section above covers SnapAdmin's knobs. For the broader data-access patterns behind them โ€” select_related vs prefetch_related, only()/values(), keyset vs offset pagination, indexing, the N+1 problem, the SQL/NoSQL trade-off, and denormalization โ€” see the Optimizations Guide in the full documentation.


๐Ÿ”ง Environment Variables Reference

Copy dist.env to .env and configure:

Variable Default Description
SECRET_KEY insecure placeholder Django secret key - must be changed in production
DEBUG True Enable Django debug mode - set False in production
ALLOWED_HOSTS localhost,... Comma-separated allowed hostnames
LOG_LEVEL INFO Log verbosity: DEBUG, INFO, WARNING, ERROR
JSON_LOGS False Structured JSON log output for production log aggregation
POSTGRES_DB snapadmin PostgreSQL database name
POSTGRES_USER snapadmin PostgreSQL username
POSTGRES_PASSWORD snapadmin PostgreSQL password
POSTGRES_HOST db PostgreSQL host (Docker service name or IP)
POSTGRES_PORT 5432 PostgreSQL port
REDIS_URL redis://redis:6379/0 Redis URL for Celery broker and result backend
ELASTICSEARCH_URL http://elasticsearch:9200 Elasticsearch cluster URL
ELASTICSEARCH_ENABLED False Enable ES integration; when False all models use DB_ONLY
SNAPADMIN_REST_API_ENABLED True Serve the REST CRUD endpoints (False removes the routes)
SNAPADMIN_SWAGGER_ENABLED True Serve Swagger UI + ReDoc
SNAPADMIN_GRAPHQL_ENABLED True Serve the GraphQL endpoint
SNAPADMIN_ES_QUERY_ROUTING True Route ?search= on DUAL models to Elasticsearch
SNAPADMIN_ES_SEARCH_LIMIT 1000 Max hits fetched from ES per routed search
SNAPADMIN_QUERY_BACKEND_HEADER True Expose the X-Snap-Query-Backend header on list responses
SNAPADMIN_GRAPHQL_REQUIRE_AUTH True Require auth + per-model perms on every GraphQL resolver
SNAPADMIN_GRAPHIQL_ENABLED DEBUG GraphiQL playground โ€” keep out of production
SNAPADMIN_THROTTLE_ANON 60/min DRF rate limit for anonymous callers
SNAPADMIN_THROTTLE_USER 600/min DRF rate limit for authenticated clients
SNAPADMIN_SEED_ADMIN_PASSWORD โ€” Password for the seeded superuser; admin/admin default allowed only with DEBUG=True
EMAIL_HOST / EMAIL_PORT localhost / 587 SMTP server for notification emails
EMAIL_HOST_USER / EMAIL_HOST_PASSWORD โ€” SMTP credentials
EMAIL_USE_TLS True Use STARTTLS for SMTP
DEFAULT_FROM_EMAIL snapadmin@localhost From address of alert/digest emails
SNAPADMIN_ERROR_MONITOR_ENABLED True Record unhandled exceptions / 5xx as ErrorEvents
SNAPADMIN_ERROR_ALERT_ENABLED True Enable the error spike alert email
SNAPADMIN_ERROR_ALERT_THRESHOLD 20 Errors within the window that trigger the alert
SNAPADMIN_ERROR_ALERT_WINDOW_MINUTES 15 Rolling window for the spike alert
SNAPADMIN_ERROR_ALERT_EMAILS โ€” Comma-separated alert recipients (empty = no alerts)
SNAPADMIN_ERROR_DIGEST_ENABLED True Enable the daily grouped error digest
SNAPADMIN_ERROR_DIGEST_EMAILS โ€” Digest recipients; falls back to alert emails
SNAPADMIN_ERROR_DIGEST_MAX_GROUPS 20 Max distinct error groups per digest email
SNAPADMIN_ERROR_DIGEST_HOUR / _MINUTE 8 / 0 Daily send time of the digest (Celery Beat)
SNAPADMIN_ERROR_RETENTION_DAYS 30 Purge ErrorEvents older than this
SNAPADMIN_BACKUP_ENABLED False Enable scheduled 3-2-1 database backups
SNAPADMIN_BACKUP_KEEP 7 Dumps kept per destination (oldest pruned)
SNAPADMIN_BACKUP_LOCAL_DIR ./backups Copy 1: directory on the same server
SNAPADMIN_BACKUP_LOCAL_EVERY_HOURS 24 How often the local copy refreshes
SNAPADMIN_BACKUP_NETWORK_DIR โ€” Copy 2: mounted share of a server on your network (empty = off)
SNAPADMIN_BACKUP_NETWORK_EVERY_HOURS 24 How often the network copy refreshes
SNAPADMIN_BACKUP_FTP_HOST / _PORT โ€” / 21 Copy 3: offsite FTP/FTPS server (empty host = off)
SNAPADMIN_BACKUP_FTP_USER / _PASSWORD โ€” FTP credentials
SNAPADMIN_BACKUP_FTP_DIR / Target directory on the FTP server
SNAPADMIN_BACKUP_FTP_TLS False Use FTPS (recommended for offsite)
SNAPADMIN_BACKUP_REMOTE_EVERY_HOURS 168 How often the offsite copy refreshes (weekly)
SNAPADMIN_AUTO_SEED False Auto-run seed_demo on startup (demo only)
TRAEFIK_DOMAIN yourdomain.com Production domain for docker-compose.traefik.prod.yml
TRAEFIK_ACME_EMAIL โ€” Email for Let's Encrypt certificate registration
TRAEFIK_DASHBOARD_USER admin Reference username (see TRAEFIK_DASHBOARD_CREDENTIALS)
TRAEFIK_DASHBOARD_PASSWORD changeme Reference password (see TRAEFIK_DASHBOARD_CREDENTIALS)
TRAEFIK_DASHBOARD_CREDENTIALS admin:$$apr1$$... Dashboard BasicAuth in htpasswd format

๐ŸŒŸ Demo Application Features

The repository includes a demo/ app and a sandbox/ project to showcase SnapAdmin's power:

  • Complete Project Setup: Ready-to-use Docker environment with PostgreSQL, Redis, and Elasticsearch.
  • Example Domain Models: Product, Customer, and Order models showing complex relationships.
  • Interactive Dashboard: A custom system dashboard with health checks and environment stats.
  • Seeder Command: python manage.py seed_demo to instantly populate your environment.
  • Celery Integration: Example background tasks for data indexing and stats generation.

๐Ÿณ Running the Demo (Docker)

git clone https://github.com/drofji/django-snapadmin.git
cd django-snapadmin
cp dist.env .env
docker compose up --build
  • Admin: http://localhost:8000/admin/ (admin / admin)
  • REST API Docs: http://localhost:8000/api/docs/
  • GraphQL API: http://localhost:8000/api/graphql/

With Elasticsearch (optional, adds ~512 MB RAM):

# 1. Enable in .env
echo "ELASTICSEARCH_ENABLED=True" >> .env

# 2. Start with ES profile
docker compose --profile es up --build

# 3. Also add Kibana for visualisation
docker compose --profile es --profile dev up --build

Building images with automatic retention

For the test/demo image, scripts/docker_build.sh builds, tags by build-day, and self-prunes so old images never pile up:

scripts/docker_build.sh                          # image=snapadmin-test, keep 3 build-days
IMAGE=myimg scripts/docker_build.sh              # custom image name
SNAPADMIN_IMAGE_KEEP_DAYS=5 scripts/docker_build.sh   # widen the window

Retention policy โ€” one build per day, keep the last N build-days (N defaults to 3, override via SNAPADMIN_IMAGE_KEEP_DAYS):

  • Collapse within a day โ€” images are tagged snapadmin-test:YYYY-MM-DD plus a moving :latest. Rebuilding the same calendar day re-points that day's tag at the new image; the superseded build becomes a dangling layer and is reclaimed.
  • Rolling N-day window โ€” the last build of each of the N most-recent build-days is kept; when an (N+1)-th distinct build-day appears, the oldest day's image is pruned.
  • History gaps are irrelevant โ€” "N days" means the last N build-days, not calendar days. Idle days never consume a slot.

Example: builds a month ago, a week ago, yesterday, and today leave exactly three images after today's build โ€” one each for a week ago, yesterday, and today; the month-ago image and all superseded same-day builds are gone.

The pruner can also run standalone (e.g. in CI), with a dry-run mode:

python -m scripts.docker_retention prune --image snapadmin-test --dry-run

๐ŸŒ Running with Traefik

Two Traefik overlay files are provided for routing requests through a reverse proxy.

Local development (HTTP)

Access the app at http://snapadmin.localhost/ without a port number:

docker compose -f docker-compose.yml -f docker-compose.traefik.local.yml up --build
URL Service
http://snapadmin.localhost/admin/ Django admin
http://traefik.localhost/ Traefik dashboard (BasicAuth)

On Windows, add to C:\Windows\System32\drivers\etc\hosts:

127.0.0.1 snapadmin.localhost traefik.localhost

Production (HTTPS + Let's Encrypt)

For production with automatic TLS certificates, set these values in .env:

TRAEFIK_DOMAIN=admin.mycompany.com
TRAEFIK_ACME_EMAIL=your@email.com
TRAEFIK_DASHBOARD_CREDENTIALS=admin:$$apr1$$...   # see dist.env for generation instructions
ALLOWED_HOSTS=admin.mycompany.com
DEBUG=False

Then start the production overlay:

docker compose -f docker-compose.yml -f docker-compose.traefik.prod.yml up -d
URL Service
https://admin.mycompany.com/admin/ Django admin (auto-TLS)
https://traefik.admin.mycompany.com/ Traefik dashboard (BasicAuth)

All HTTP traffic is automatically redirected to HTTPS.

Dashboard credentials

The default credentials in dist.env are admin / changeme. To change them:

# Generate htpasswd string and escape $ for Docker Compose
echo $(htpasswd -nb newuser newpassword) | sed -e 's/\$/\$\$/g'
# Paste result into TRAEFIK_DASHBOARD_CREDENTIALS in .env

๐Ÿ’ป Local Development Setup

# Clone and setup environment
git clone https://github.com/drofji/django-snapadmin.git
cd django-snapadmin
python -m venv .venv
source .venv/bin/activate

# Install in editable mode
pip install -r requirements.txt
pip install -e .

# Initialize DB and run
python manage.py migrate
python manage.py seed_demo
python manage.py runserver

๐Ÿ”„ Migrating from drofji-automatically-django-admin

The legacy package drofji-automatically-django-admin (import root drofji_autoadmin, last tag v1.1.0) is being retired. SnapAdmin is its direct successor โ€” same declarative admin, now with REST/GraphQL, the Unfold theme, Elasticsearch, GDPR retention and offline mode. The underlying Django fields are unchanged, so this is a rename + settings swap, not a data migration (the only new table is snapadmin_apitoken).

# 1. Swap the package
pip uninstall drofji-automatically-django-admin
pip install django-snapadmin            # or: pip install git+https://github.com/drofji/django-snapadmin.git

# 2. Rename the import root, base class and fields (repo-wide)
grep -rl drofji_autoadmin . | xargs sed -i '' 's/drofji_autoadmin/snapadmin/g'
grep -rl AutoAdmin       . | xargs sed -i '' 's/AutoAdmin/Snap/g'
#   drofji_autoadmin โ†’ snapadmin   |   AutoAdminModel โ†’ SnapModel   |   AutoAdminCharField โ†’ SnapCharField โ€ฆ

What you must change by hand:

Concern Old (drofji_autoadmin) New (snapadmin)
Theme apps admin_interface, colorfield Remove them; add unfold (+ unfold.contrib.*) and django_ckeditor_5 before django.contrib.admin
REST stack โ€” add rest_framework, drf_spectacular, django_filters, graphene_django, snapadmin
rangefilter present keep it
Admin registration automatic (inheritance) explicit โ€” add SnapModel.register_all_admins() to admin.py
Color fields colorfield.ColorField SnapColorField
APIs none optional: path("", include("snapadmin.urls")) for /api/, /api/docs/, /graphql/

Then run python manage.py migrate (creates only snapadmin_apitoken) and collectstatic if you serve static yourself.

โš ๏ธ Don't run both packages at once โ€” keeping drofji_autoadmin in INSTALLED_APPS alongside SnapAdmin makes both register the admin (AlreadyRegistered). Fully uninstall the old package and remove it from INSTALLED_APPS first.

The full step-by-step is in docs/index.html under Migration Guide โ†’ drofji_autoadmin โ†’ SnapAdmin.


๐Ÿ“œ License

MIT License โ€” see LICENSE.

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_snapadmin-0.1.0a5.tar.gz (123.4 kB view details)

Uploaded Source

Built Distribution

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

django_snapadmin-0.1.0a5-py3-none-any.whl (125.6 kB view details)

Uploaded Python 3

File details

Details for the file django_snapadmin-0.1.0a5.tar.gz.

File metadata

  • Download URL: django_snapadmin-0.1.0a5.tar.gz
  • Upload date:
  • Size: 123.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.4

File hashes

Hashes for django_snapadmin-0.1.0a5.tar.gz
Algorithm Hash digest
SHA256 61e3fc1c485ee9041d03ad3507671f54918d6d17e9451f1be1e4b35a92d428a0
MD5 5cafbea2ddeb6bc2c86b0b53e80b5fdc
BLAKE2b-256 72a85a5ba694a56327c83037c2cba2824135c3b8b5c62a609d3d6be66208e27c

See more details on using hashes here.

File details

Details for the file django_snapadmin-0.1.0a5-py3-none-any.whl.

File metadata

File hashes

Hashes for django_snapadmin-0.1.0a5-py3-none-any.whl
Algorithm Hash digest
SHA256 8243de9319a8759ba3b2fd45a241687486fdee0e6bcc48e45e10e313b9832314
MD5 f2d56c899ca43d83c07e4bc705ebc367
BLAKE2b-256 737df72bd3f3f400f1c0a7ecff75e73f45075332886ff5985aa52b157735cd43

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