DEV Community

Cover image for Meet django-vantage: A Django Admin That Configures Itself
Mahmoud Ahmed
Mahmoud Ahmed

Posted on

Meet django-vantage: A Django Admin That Configures Itself

Meet django-vantage: A Django Admin That Configures Itself

Columns, filters, search, charts, Kanban boards, dark mode and a JSON API, without writing an admin.py.

django-vantage orders list

If you've built a few Django projects, you know the routine. You add a model, open admin.py, and type the same things again: list_display, list_filter, search_fields, ordering, list_select_related. Then you do it for the next model, and the one after that.

Django's admin is one of the best features in any web framework. Still, almost every project spends time on two jobs: telling the admin things it could work out from the model, and fixing up a UI that hasn't changed much in years.

So I built django-vantage, a Django admin panel that is smart and beautiful. Every model works with zero configuration. When you want something different, you override only that one thing and Vantage keeps inferring the rest.

In this post I'll show what it does, how it decides what to show you, and how to try it in about a minute.


The idea: smart by default, easy to override

Vantage is built on three principles.

1. Smart. Columns, filters, search fields, sort order, form widgets and query optimisation are all inferred from your model. You don't need any configuration to get a useful admin.

2. Easy. You can override any inferred default on its own. If you set list_display, only the columns change. Filters, search and widgets stay inferred. Inference works per attribute, so customising one thing doesn't mean you have to configure everything.

3. Configurable from the UI. Many settings can also be changed from the panel itself (columns, filters, stats, dashboard, sidebar, branding), so they apply without a deploy. Code and UI configuration combine in one fixed order:

SiteConfiguration row  >  settings.VANTAGE  >  conf.DEFAULTS
Enter fullscreen mode Exit fullscreen mode

Up and running in 60 seconds

Vantage is in alpha, so install it with --pre:

pip install --pre django-vantage
Enter fullscreen mode Exit fullscreen mode

Add it to INSTALLED_APPS, after the Django contrib apps and before your own apps:

# settings.py
INSTALLED_APPS = [
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "vantage",
    # your apps
]

LOGIN_URL = "/accounts/login/"
LOGIN_REDIRECT_URL = "/admin/"
Enter fullscreen mode Exit fullscreen mode

Mount it. You can also include Vantage's styled sign-in, sign-out and password-change views. Their route names match django.contrib.auth.urls, so you can switch back at any time:

# urls.py
from django.urls import include, path
from vantage import site

urlpatterns = [
    path("admin/", site.urls),
    path("accounts/", include("vantage.authviews")),
]
Enter fullscreen mode Exit fullscreen mode

Run migrations. Vantage keeps its own tables for site configuration, the audit log, saved views and per-list preferences:

python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

That's all. Every model in your project now has a list view, a form, filters and search.

No Node.js required. Django is the only runtime dependency, and the CSS and JavaScript ship prebuilt.


How the "smart" part works

Inference follows fixed, documented rules, so you can predict what you'll get. Here is roughly what Vantage does with each model.

Columns (list_display). Every concrete field gets a score and the top six become columns. Choice fields rank highest, followed by booleans, foreign keys, emails and URLs, and short CharFields. Large text, JSON, binary, file and primary-key fields are never shown as columns. Audit timestamps like created_at go last.

The link column (list_link). Vantage looks for a field named name, title, label, headline, subject or display_name. If there isn't one, it uses the first plain CharField. If there's no CharField, it uses the primary key.

Filters (list_filter). Up to five filters: choices and booleans first, then one date field (a domain date such as ships_on is preferred over an audit timestamp), then foreign keys.

Search (list_search). Only text-like fields are searched. Indexed and unique fields come first. Vantage also follows one hop into each foreign key, so you can search orders by customer__name without configuring anything.

Ordering. Vantage uses your Meta.ordering if you set one. Otherwise it sorts newest first on created_at, created, date_joined, timestamp or date. If none of those exist, it sorts by descending primary key.

Queries. select_related and prefetch_related are worked out from the fields the page actually renders, which avoids N+1 queries.

Sensitive fields are never exposed. Fields whose names look like credentials (password, token, api_key, salt, hash, pin, cvv, and others) are never shown, linked, filtered or searched.

You can check the result yourself:

>>> from vantage import inference
>>> inference.infer_all(Order)
{'list_display': ['number', 'status', 'customer', 'shipping_city', 'total', 'created_at'],
 'list_link': 'number',
 'list_filter': ['status', 'created_at', 'customer'],
 'list_search': ['number', 'shipping_city', 'note', 'customer__name'],
 'ordering': ['-created_at']}
Enter fullscreen mode Exit fullscreen mode

See it, then own it: vantage_scaffold

My favourite command in the project:

python manage.py vantage_scaffold shop            # print what was inferred
python manage.py vantage_scaffold shop --write    # save it as shop/panel.py
Enter fullscreen mode Exit fullscreen mode

It prints a real panel.py that contains everything Vantage inferred:

@register(Order)
class OrderPanel(ModelPanel):
    list_display = [
        "number",
        "status",
        "customer",
        "shipping_city",
        "total",
        "created_at",
    ]
    list_link = "number"
    list_filter = ["status", "created_at", "customer"]
    list_search = ["number", "shipping_city", "note", "customer__name"]
    ordering = ["-created_at"]
Enter fullscreen mode Exit fullscreen mode

The file is ordinary code. Edit whatever you want, and delete any attribute to hand it back to inference. If you delete the whole file, your models stay registered.


Overriding only what you care about

Create a panel.py next to your models.py:

# blog/panel.py
from vantage import ModelPanel, register

from blog.models import Post


@register(Post)
class PostPanel(ModelPanel):
    icon = "file-text"
    group = "Content"
    list_display = ["title", "author", "status", "published_at"]
Enter fullscreen mode Exit fullscreen mode

Only the columns change. Filters, search, sorting and form widgets are still inferred.

Run python manage.py check and Vantage validates your panels and settings, including typo suggestions:

Unrecognised key(s) in the VANTAGE setting:
  'SITE_TTLE' is not a Vantage setting. Did you mean 'SITE_TITLE'?
Enter fullscreen mode Exit fullscreen mode

A tour of the features

A real data grid

Dashboard

The list view gives you saved views, grouping, density settings, a column picker, inline editing, and trash and restore. Bulk actions can apply to all matching rows, not just the current page. Select 40,000 filtered records and act on all of them.

There's also a command palette for jumping to any model or record from the keyboard:

Command palette

Stats and charts, declared in Python

You declare stat cards and charts on the panel, and they're computed on the rows currently filtered. Charts render as inline SVG with no JavaScript charting library.

from django.db.models import Avg, Count, Q, Sum
from vantage import Chart, ModelPanel, Stat, register


@register(Order)
class OrderPanel(ModelPanel):
    list_stats = [
        Stat("Revenue", Sum("total"), format="currency", trend="created_at"),
        Stat("Orders", Count("id")),
        Stat("Average order", Avg("total"), format="currency"),
        Stat("Refunded", Count("id", filter=Q(status="refunded")), tone="destructive"),
    ]

    list_charts = [
        Chart.timeseries("Revenue over time", date="created_at", value=Sum("total")),
        Chart.breakdown("By status", group_by="status", value=Count("id"), kind="donut"),
    ]
Enter fullscreen mode Exit fullscreen mode
  • trend="created_at" compares the value with the previous period of the same length.
  • Chart.timeseries picks day, week or month buckets based on the date range of your data, unless you set bucket yourself.
  • If the ORM can't express what you need, pass a callable: Stat("Churn rate", lambda qs: churn_for(qs), format="percent"). If it raises an exception, the card shows — and the rest of the page still works.

Kanban, calendar, gallery and map views

Kanban board

The same list can be shown as a table, a Kanban board, a month calendar, a gallery or a map. Users pick the grouping field on the page.

@register(Order)
class OrderPanel(ModelPanel):
    list_views = ["table", "kanban", "calendar"]   # first one is the default
    kanban_field = "status"                         # board columns
    calendar_field = "ships_on"                     # where records land
    kanban_fields = ["status", "priority"]          # what "Group by" offers
    card_fields = ["customer", "total"]             # what a card shows
Enter fullscreen mode Exit fullscreen mode

Forms with a layout DSL

Order form

Tabs, Tab, Section and Row nest however you like:

from vantage import ModelPanel, Row, Section, Tab, Tabs, register


@register(Post)
class PostPanel(ModelPanel):
    layout = Tabs(
        Tab("Content",
            Section(Row("title", "slug"), "body"),
            Section(Row("status", "published_at"), title="Publishing", collapsed=True),
        ),
        Tab("SEO", Section("meta_title", "meta_description")),
    )
Enter fullscreen mode Exit fullscreen mode

One safety detail I like: fields you leave out of the layout aren't dropped. They're collected into an extra "Other" tab, so a required field can't quietly disappear from the form.

Editable inlines work as you'd expect:

from vantage.inlines import Inline


class OrderItemInline(Inline):
    model = OrderItem
    fields = ["product", "quantity", "unit_price"]
    extra = 1
    max_num = 50


@register(Order)
class OrderPanel(ModelPanel):
    inlines = [OrderItemInline]
Enter fullscreen mode Exit fullscreen mode

Tip: set max_num explicitly on inlines for now. In the current alpha, leaving it unset raises an error.

A cascade-delete preview

Before you delete a record, Vantage shows exactly what else will be deleted. It uses the same Collector as the real delete, so the preview always matches what happens. If a delete is blocked by a protected relation, you get a plain explanation instead of an error page:

"This customer cannot be deleted while Orders still reference it. Remove or reassign those first."

Undo that respects your teammates

Deletes, bulk edits and inline edits show an undo toast. Undo never overwrites a newer change made by someone else in the meantime.

Permissions, roles and sensitive data

  • Three permission layers, enforced on views, bulk actions and API calls.
  • Read-only mode and impersonation.
  • Multi-tenancy that fails closed: if tenant scoping can't be resolved, nothing is shown.
  • A permission matrix for Django groups, so you can see who can do what in one place.
  • Sensitive fields such as passwords, tokens and card numbers are masked by default. Revealing one is restricted and logged.

Role matrix

There's also a read-only data-quality audit for each model, and a schema map that shows how your models relate:

Schema map

A JSON API for every panel

Every panel also has a JSON API that goes through the same permission and validation path as the UI, with cursor pagination and optimistic concurrency. You don't have to keep a separate serializer layer in sync with your admin.

Theming from one colour

Give Vantage a brand colour and it derives full light and dark palettes, adjusting lightness until the text passes WCAG AA contrast (4.5:1). RTL layouts are supported too.

# settings.py
VANTAGE = {
    "SITE_TITLE":  "Acme",
    "LOGO":        "img/acme.svg",
    "LOGO_DARK":   "img/acme-light.svg",
    "BRAND_COLOR": "#0ea5e9",
    "BRAND_RADIUS": "0.5rem",
    "THEME":       "system",
    "ENVIRONMENT": "Staging — data here is not real",  # banner on every page
}
Enter fullscreen mode Exit fullscreen mode

A superuser can change any of these later from the settings screen, without a deploy.


Requirements

  • Python 3.12+
  • Django 5.2, 6.0 or 6.1
  • Runtime dependencies: only Django
  • Optional integrations (used when installed, never required): django-guardian, django-rules, django-mptt, django-treebeard, django-simple-history

Status: alpha, and I'm being honest about it

The current release is 0.2.0a1. It has more than 4,600 tests, but the API may still change before 1.0. My recommendation: use it on internal tools for now, and pin the exact version.


Try the demo

The repository includes a demo wholesale shop with orders, customers and products. One command migrates it, seeds data and serves it on port 8765:

git clone https://github.com/Pythondeveloper6/django-vantage
cd django-vantage
./example/run.sh
Enter fullscreen mode Exit fullscreen mode

Sign in with admin / admin12345. The demo's example/shop/panel.py is the shortest practical reference for everything in this post.


Links

If you try it, I'd really like to hear what Vantage inferred correctly and what it got wrong for your models. Those cases are what improve the inference rules. Issues, ideas and stars on GitHub are all welcome.

Thanks for reading!

Mahmoud Ahmed

Top comments (0)