<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mahmoud Ahmed</title>
    <description>The latest articles on DEV Community by Mahmoud Ahmed (@pythondeveloper6).</description>
    <link>https://dev.to/pythondeveloper6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F540743%2F5b397b1b-37b2-4002-a6cf-e57a99091928.jpeg</url>
      <title>DEV Community: Mahmoud Ahmed</title>
      <link>https://dev.to/pythondeveloper6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pythondeveloper6"/>
    <language>en</language>
    <item>
      <title>celery-manager: See, Control and Pause Your Celery Tasks Without Touching Your Beat Schedule</title>
      <dc:creator>Mahmoud Ahmed</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:52:38 +0000</pubDate>
      <link>https://dev.to/pythondeveloper6/celery-manager-see-control-and-pause-your-celery-tasks-without-touching-your-beat-schedule-1g4o</link>
      <guid>https://dev.to/pythondeveloper6/celery-manager-see-control-and-pause-your-celery-tasks-without-touching-your-beat-schedule-1g4o</guid>
      <description>&lt;h1&gt;
  
  
  celery-manager: See, Control and Pause Your Celery Tasks Without Touching Your Beat Schedule
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A dashboard and control API for Celery, for Django, Flask and FastAPI projects.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8miifffy1l1ra0gb2ak4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8miifffy1l1ra0gb2ak4.png" alt="celery-manager task list" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Celery is great until you need to know what it's actually doing.&lt;/p&gt;

&lt;p&gt;A task failed overnight. Which arguments did it get? Which worker ran it? What did it log before the traceback? A customer import has been running for 20 minutes. Is it at row 10 or row 10,000? And one I hit all the time: &lt;strong&gt;"Can we skip tomorrow's 9:00 report, just once?"&lt;/strong&gt; Usually that means editing the beat schedule, deploying, remembering to revert it, and deploying again.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://github.com/Pythondeveloper6/celery-manager" rel="noopener noreferrer"&gt;celery-manager&lt;/a&gt;&lt;/strong&gt; to answer those questions from one screen. It's a dashboard and control API for Celery that shows what every task is doing, lets you act on tasks, and can &lt;strong&gt;pause a single upcoming run of a periodic task without changing your beat schedule or restarting anything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It works with any &lt;strong&gt;Django, Flask or FastAPI&lt;/strong&gt; project that already uses Celery.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔍 &lt;strong&gt;Every run is visible&lt;/strong&gt;, whether or not the task is decorated: status, arguments, worker, queue, timings, exceptions and full tracebacks.&lt;/li&gt;
&lt;li&gt;🗂 &lt;strong&gt;History and search&lt;/strong&gt;: paginated history, filters by status, task name or time range, and free-text search over names, IDs and arguments.&lt;/li&gt;
&lt;li&gt;📈 &lt;strong&gt;Live progress&lt;/strong&gt;: call &lt;code&gt;report_progress()&lt;/code&gt; inside a task and watch a progress bar move.&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Log capture&lt;/strong&gt;: a task's &lt;code&gt;logging&lt;/code&gt; output is shown next to the run.&lt;/li&gt;
&lt;li&gt;🎛 &lt;strong&gt;Control&lt;/strong&gt;: start any registered task, cancel or terminate runs, and retry them as they were or with edited arguments, one at a time or in bulk.&lt;/li&gt;
&lt;li&gt;⏸ &lt;strong&gt;Schedule control&lt;/strong&gt;: skip one upcoming periodic run, or pause a whole schedule until you resume it or until a set time.&lt;/li&gt;
&lt;li&gt;🌳 &lt;strong&gt;Workflows&lt;/strong&gt;: chains, groups and chords drawn as a tree on each run's page.&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Insights&lt;/strong&gt;: volume, success rate and runtimes over time, a page for each task, and failures grouped by cause with "retry all".&lt;/li&gt;
&lt;li&gt;👷 &lt;strong&gt;Workers and queues&lt;/strong&gt;: live worker state, pool controls, runtime rate limits, queue depth and purge.&lt;/li&gt;
&lt;li&gt;🔗 &lt;strong&gt;Share and export&lt;/strong&gt;: filters live in the URL, plus saved views and CSV or JSON export.&lt;/li&gt;
&lt;li&gt;🧾 &lt;strong&gt;Audit log&lt;/strong&gt;: every control action is recorded with the user who took it.&lt;/li&gt;
&lt;li&gt;🎨 &lt;strong&gt;Light and dark themes&lt;/strong&gt;, plus your own wordmark, accent colour and logo.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How it works: three small parts
&lt;/h2&gt;

&lt;p&gt;celery-manager has three parts that share &lt;strong&gt;one SQLAlchemy-backed store&lt;/strong&gt; (SQLite by default, or PostgreSQL, MySQL, or any database SQLAlchemy supports):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;REST API and dashboard.&lt;/strong&gt; A FastAPI app that serves the JSON API under &lt;code&gt;/api&lt;/code&gt; and the dashboard at &lt;code&gt;/&lt;/code&gt;. You can mount it in your own app or run it standalone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events listener&lt;/strong&gt; (&lt;code&gt;celery-manager events&lt;/code&gt;). A long-running process that records status changes from Celery's event bus, &lt;strong&gt;for every task, decorated or not.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;@track&lt;/code&gt; decorator.&lt;/strong&gt; Optional and per task. It adds log capture, enables pausing and cancelling individual scheduled runs, and records the run as started before your function runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the listener works from Celery's own events, &lt;strong&gt;you get visibility into existing tasks without changing any task code.&lt;/strong&gt; &lt;code&gt;@track&lt;/code&gt; is only for the extra features.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--pre&lt;/span&gt; celery-manager

&lt;span class="c"&gt;# optional extras&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--pre&lt;/span&gt; &lt;span class="s2"&gt;"celery-manager[standalone]"&lt;/span&gt;   &lt;span class="c"&gt;# adds uvicorn&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--pre&lt;/span&gt; &lt;span class="s2"&gt;"celery-manager[flask]"&lt;/span&gt;        &lt;span class="c"&gt;# adds Flask and a2wsgi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create the dashboard app
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CeleryManagerConfig&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager.api.app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_app&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CeleryManagerConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;celery_app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;your_celery_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///celery_manager.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# or any SQLAlchemy URL
&lt;/span&gt;    &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CELERY_MANAGER_SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# private + random
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate a secret key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import secrets; print(secrets.token_urlsafe(32))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;create_app&lt;/code&gt; refuses to start if the key is empty or still the &lt;code&gt;"change-me"&lt;/code&gt; placeholder.&lt;/p&gt;

&lt;p&gt;Serve it with any ASGI server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvicorn your_module:app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Local development over plain HTTP?&lt;/strong&gt; The session cookie is &lt;code&gt;Secure&lt;/code&gt; by default, so your browser won't send it to &lt;code&gt;http://localhost&lt;/code&gt; and you'll look logged out after signing in. Pass &lt;code&gt;cookie_secure=False&lt;/code&gt; for local development only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Turn on Celery events
&lt;/h3&gt;

&lt;p&gt;Add these to the Celery config shared by web, worker and beat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker_send_task_events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;   &lt;span class="c1"&gt;# same as running the worker with -E
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_send_sent_event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;      &lt;span class="c1"&gt;# publisher emits task-sent (carries the queue)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Run the events listener
&lt;/h3&gt;

&lt;p&gt;Run it next to your worker and beat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;celery-manager events &lt;span class="nt"&gt;--celery-app&lt;/span&gt; your_module:your_celery_app &lt;span class="nt"&gt;--db-url&lt;/span&gt; sqlite:///celery_manager.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the same &lt;code&gt;--db-url&lt;/code&gt; as in your config.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Create your first user
&lt;/h3&gt;

&lt;p&gt;There's no default account, which is intentional. Create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager.auth.users&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_user&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager.db.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;init_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_session_factory&lt;/span&gt;

&lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_session_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;init_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a-real-password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign in, and every task run your workers process shows up in the dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going further with &lt;code&gt;@track&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Put &lt;code&gt;@track&lt;/code&gt; &lt;strong&gt;under&lt;/strong&gt; &lt;code&gt;@app.task&lt;/code&gt;, and call &lt;code&gt;celery_manager.setup()&lt;/code&gt; in the &lt;strong&gt;worker&lt;/strong&gt; process. &lt;code&gt;create_app()&lt;/code&gt; only sets up the dashboard process, so without &lt;code&gt;setup()&lt;/code&gt; the decorator does nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# myproject/tasks.py — imported by the Celery worker
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;celery_manager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CeleryManagerConfig&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myproject.celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;

&lt;span class="n"&gt;celery_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CeleryManagerConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;celery_app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///celery_manager.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# the same DB the dashboard uses
&lt;/span&gt;    &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a-real-secret-in-production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="nd"&gt;@app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@celery_manager.track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schedule_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send-report-every-2-hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;schedule_name&lt;/code&gt; should match the key in your &lt;code&gt;beat_schedule&lt;/code&gt;. &lt;code&gt;bind=True&lt;/code&gt; is optional, so &lt;code&gt;@track&lt;/code&gt; also works on plain tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.task&lt;/span&gt;
&lt;span class="nd"&gt;@celery_manager.track&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Live progress bars
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ite346u3djrvtn2hdns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ite346u3djrvtn2hdns.png" alt="Task detail with live progress" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;report_progress&lt;/span&gt;


&lt;span class="nd"&gt;@app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@celery_manager.track&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;import_customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;handle_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;report_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;imported &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature is &lt;code&gt;report_progress(current, total=None, message=None)&lt;/code&gt;. A few details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's throttled.&lt;/strong&gt; Database writes happen at most once every 0.5 seconds per run, so you can call it in a tight loop. The final call (&lt;code&gt;current &amp;gt;= total&lt;/code&gt;) always gets written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never raises into your task.&lt;/strong&gt; A progress problem won't crash a real job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave out &lt;code&gt;total&lt;/code&gt;&lt;/strong&gt; for open-ended work. The dashboard then shows a running count and message instead of a percentage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Logs and tracebacks next to the run
&lt;/h3&gt;

&lt;p&gt;When a tracked task fails, its page shows the traceback, the captured log output and a &lt;strong&gt;Retry&lt;/strong&gt; button together:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4mw1wc02z32knmkq2u69.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4mw1wc02z32knmkq2u69.png" alt="Failed task with traceback and logs" width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The headline feature: pause a schedule without editing it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwcaoufwyd86fgbmiuja7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwcaoufwyd86fgbmiuja7.png" alt="Schedules page" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;celery-manager never edits your beat schedule.&lt;/strong&gt; Beat keeps sending the task as usual, and &lt;code&gt;@track&lt;/code&gt; makes that run skip itself when you've paused it. So there's no config change, no deploy, and nothing to remember to revert.&lt;/p&gt;

&lt;p&gt;From the &lt;strong&gt;Schedules&lt;/strong&gt; page you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skip one upcoming run.&lt;/strong&gt; Open a schedule, see its next runs, and skip or cancel just one. The rest of the schedule carries on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pause the whole schedule&lt;/strong&gt; for 1 hour, until tomorrow at 9:00, or until a custom time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you resume, the schedule continues from the current time. &lt;strong&gt;Runs missed while paused are not run all at once afterwards.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Full control from the dashboard
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcmzvmdkh3z7yacs7pgx1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcmzvmdkh3z7yacs7pgx1.png" alt="Start task sheet" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start task.&lt;/strong&gt; Pick any registered task. Its arguments are &lt;strong&gt;prefilled from the function signature&lt;/strong&gt;, and you can choose a queue or a delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancel&lt;/strong&gt; a run that hasn't started, or &lt;strong&gt;Terminate&lt;/strong&gt; one that's already running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry&lt;/strong&gt;, or &lt;strong&gt;Retry with changes&lt;/strong&gt; to edit the arguments first. You can select several failed runs and retry them together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk cancel&lt;/strong&gt; every waiting or running task that matches your filters. The count is shown before you confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers.&lt;/strong&gt; Shut a worker down, add or remove pool processes, and start or stop consuming from a queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits.&lt;/strong&gt; Change a task's rate limit on running workers &lt;strong&gt;without a deploy&lt;/strong&gt;. The change lasts until the worker restarts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purge a queue.&lt;/strong&gt; You have to type the queue name to confirm, so it's hard to do by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Everything in the dashboard is also available through the REST API&lt;/strong&gt;, so you can script it or connect it to your own tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Workflows, insights and grouped failures
&lt;/h2&gt;

&lt;p&gt;Chains, groups and chords are drawn as a &lt;strong&gt;tree&lt;/strong&gt;, so when one member of a chord fails you can see exactly which one:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx8pvwugeji5qq2qdb2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx8pvwugeji5qq2qdb2z.png" alt="Workflow tree" width="800" height="794"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insights&lt;/strong&gt; show volume, success rate and runtime trends, with a page for each task:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhjjwy399ycgwrt7vuex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhjjwy399ycgwrt7vuex.png" alt="Insights" width="800" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failures are grouped by exception signature.&lt;/strong&gt; Instead of 300 separate failed rows, you see "these 300 runs failed with the same &lt;code&gt;ConnectionError&lt;/code&gt;", and you can retry the whole group at once:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd8z4q5p2pbcay8oavr27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd8z4q5p2pbcay8oavr27.png" alt="Failures grouped by cause" width="799" height="694"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Plugging it into your stack
&lt;/h2&gt;

&lt;p&gt;The dashboard is a standard ASGI app, so it fits in most places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FastAPI.&lt;/strong&gt; Mount it next to your own routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager.api.app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_app&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CeleryManagerConfig&lt;/span&gt;

&lt;span class="n"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_session_factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CeleryManagerConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;celery_app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;celery_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///celery_manager.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/celery-manager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flask.&lt;/strong&gt; Wrap it with &lt;code&gt;a2wsgi&lt;/code&gt; (install &lt;code&gt;celery-manager[flask]&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;a2wsgi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ASGIMiddleware&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;werkzeug.middleware.dispatcher&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DispatcherMiddleware&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery_manager.api.app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_app&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wsgi_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DispatcherMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wsgi_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/celery-manager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ASGIMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Django.&lt;/strong&gt; The recommended setup is to run the dashboard as its own uvicorn process next to Django, optionally behind your reverse proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvicorn yourmodule:dashboard &lt;span class="nt"&gt;--port&lt;/span&gt; 8001 &lt;span class="nt"&gt;--root-path&lt;/span&gt; /celery-manager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository includes a working reference in &lt;code&gt;examples/django_demo/run_dashboard.py&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For live push updates, run the dashboard as its own process rather than through the Flask/WSGI adapter.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a real database.&lt;/strong&gt; SQLite is fine for local development. In production, point &lt;code&gt;db_url&lt;/code&gt; at PostgreSQL or MySQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run migrations as a deploy step&lt;/strong&gt; with &lt;code&gt;celery-manager migrate&lt;/code&gt;, and set &lt;code&gt;auto_migrate=False&lt;/code&gt; so the app doesn't upgrade the schema itself at startup:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  celery-manager migrate &lt;span class="nt"&gt;--db-url&lt;/span&gt; postgresql://admin@db/celery_manager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep &lt;code&gt;cookie_secure=True&lt;/code&gt;&lt;/strong&gt; (the default) anywhere outside local development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS tip:&lt;/strong&gt; with Celery 5.6 and billiard 4.3, prefork workers on macOS can fail every task with &lt;code&gt;ValueError: not enough values to unpack&lt;/code&gt;. This is an upstream issue. Use &lt;code&gt;--pool=threads&lt;/code&gt; or &lt;code&gt;--pool=solo&lt;/code&gt; locally.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Status: early alpha
&lt;/h2&gt;

&lt;p&gt;The current release is &lt;strong&gt;&lt;code&gt;0.1.0a4&lt;/code&gt;&lt;/strong&gt; and needs &lt;strong&gt;Python 3.10+&lt;/strong&gt;. To be upfront about what that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API may still change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no role system yet. Any signed-in user can perform any action.&lt;/strong&gt; Every action is recorded in the audit log, but you should still only give access to people you trust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My recommendation: &lt;strong&gt;use it on internal tools for now, and pin the exact version.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Pythondeveloper6/celery-manager" rel="noopener noreferrer"&gt;github.com/Pythondeveloper6/celery-manager&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;PyPI:&lt;/strong&gt; &lt;a href="https://pypi.org/project/celery-manager/" rel="noopener noreferrer"&gt;pypi.org/project/celery-manager&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://pythondeveloper6.github.io/celery-manager/" rel="noopener noreferrer"&gt;pythondeveloper6.github.io/celery-manager&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run Celery in production, I'd really like to know what question you most often can't answer about your tasks. That's what decides what I build next. Issues, ideas and stars on GitHub are all welcome.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mahmoud Ahmed&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>celery</category>
      <category>django</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>Meet django-vantage: A Django Admin That Configures Itself</title>
      <dc:creator>Mahmoud Ahmed</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:51:26 +0000</pubDate>
      <link>https://dev.to/pythondeveloper6/meet-django-vantage-a-django-admin-that-configures-itself-4l7b</link>
      <guid>https://dev.to/pythondeveloper6/meet-django-vantage-a-django-admin-that-configures-itself-4l7b</guid>
      <description>&lt;h1&gt;
  
  
  Meet django-vantage: A Django Admin That Configures Itself
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Columns, filters, search, charts, Kanban boards, dark mode and a JSON API, without writing an &lt;code&gt;admin.py&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fruhjxtnbii8ntqpa5uvh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fruhjxtnbii8ntqpa5uvh.webp" alt="django-vantage orders list" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/Pythondeveloper6/django-vantage" rel="noopener noreferrer"&gt;django-vantage&lt;/a&gt;&lt;/strong&gt;, a Django admin panel that is &lt;strong&gt;smart&lt;/strong&gt; and &lt;strong&gt;beautiful&lt;/strong&gt;. Every model works with zero configuration. When you want something different, you override only that one thing and Vantage keeps inferring the rest.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The idea: smart by default, easy to override
&lt;/h2&gt;

&lt;p&gt;Vantage is built on three principles.&lt;/p&gt;

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SiteConfiguration row  &amp;gt;  settings.VANTAGE  &amp;gt;  conf.DEFAULTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Up and running in 60 seconds
&lt;/h2&gt;

&lt;p&gt;Vantage is in alpha, so install it with &lt;code&gt;--pre&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--pre&lt;/span&gt; django-vantage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to &lt;code&gt;INSTALLED_APPS&lt;/code&gt;, after the Django contrib apps and before your own apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.contrib.auth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.contrib.contenttypes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.contrib.sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.contrib.messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.contrib.staticfiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vantage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# your apps
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;LOGIN_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/accounts/login/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;LOGIN_REDIRECT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/admin/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# urls.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vantage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;

&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accounts/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vantage.authviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run migrations. Vantage keeps its own tables for site configuration, the audit log, saved views and per-list preferences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all. Every model in your project now has a list view, a form, filters and search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Node.js required.&lt;/strong&gt; Django is the only runtime dependency, and the CSS and JavaScript ship prebuilt.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the "smart" part works
&lt;/h2&gt;

&lt;p&gt;Inference follows fixed, documented rules, so you can predict what you'll get. Here is roughly what Vantage does with each model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Columns (&lt;code&gt;list_display&lt;/code&gt;).&lt;/strong&gt; 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 &lt;code&gt;CharField&lt;/code&gt;s. Large text, JSON, binary, file and primary-key fields are never shown as columns. Audit timestamps like &lt;code&gt;created_at&lt;/code&gt; go last.&lt;/p&gt;

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

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

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

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

&lt;p&gt;&lt;strong&gt;Queries.&lt;/strong&gt; &lt;code&gt;select_related&lt;/code&gt; and &lt;code&gt;prefetch_related&lt;/code&gt; are worked out from the fields the page actually renders, which avoids N+1 queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensitive fields are never exposed.&lt;/strong&gt; Fields whose names look like credentials (&lt;code&gt;password&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;salt&lt;/code&gt;, &lt;code&gt;hash&lt;/code&gt;, &lt;code&gt;pin&lt;/code&gt;, &lt;code&gt;cvv&lt;/code&gt;, and others) are never shown, linked, filtered or searched.&lt;/p&gt;

&lt;p&gt;You can check the result yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vantage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;inference&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;infer_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;list_display&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shipping_city&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;list_link&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;list_filter&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;list_search&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shipping_city&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customer__name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
 &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ordering&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-created_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  See it, then own it: &lt;code&gt;vantage_scaffold&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;My favourite command in the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python manage.py vantage_scaffold shop            &lt;span class="c"&gt;# print what was inferred&lt;/span&gt;
python manage.py vantage_scaffold shop &lt;span class="nt"&gt;--write&lt;/span&gt;    &lt;span class="c"&gt;# save it as shop/panel.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints a real &lt;code&gt;panel.py&lt;/code&gt; that contains everything Vantage inferred:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipping_city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;list_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;list_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;list_search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipping_city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer__name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ordering&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is ordinary code. Edit whatever you want, and &lt;strong&gt;delete any attribute to hand it back to inference.&lt;/strong&gt; If you delete the whole file, your models stay registered.&lt;/p&gt;




&lt;h2&gt;
  
  
  Overriding only what you care about
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;panel.py&lt;/code&gt; next to your &lt;code&gt;models.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# blog/panel.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vantage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;blog.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Post&lt;/span&gt;


&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file-text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the columns change. Filters, search, sorting and form widgets are still inferred.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;python manage.py check&lt;/code&gt; and Vantage validates your panels and settings, including typo suggestions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unrecognised key(s) in the VANTAGE setting:
  'SITE_TTLE' is not a Vantage setting. Did you mean 'SITE_TITLE'?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A tour of the features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A real data grid
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fftlpg4z01ui7abpwr397.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fftlpg4z01ui7abpwr397.webp" alt="Dashboard" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;There's also a &lt;strong&gt;command palette&lt;/strong&gt; for jumping to any model or record from the keyboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ju7ststzvml700dw6yh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ju7ststzvml700dw6yh.webp" alt="Command palette" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Stats and charts, declared in Python
&lt;/h3&gt;

&lt;p&gt;You declare stat cards and charts on the panel, and they're &lt;strong&gt;computed on the rows currently filtered&lt;/strong&gt;. Charts render as inline SVG with no JavaScript charting library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vantage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Stat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;


&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Average order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refunded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refunded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;tone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;destructive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;list_charts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeseries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revenue over time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;breakdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;By status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;donut&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;trend="created_at"&lt;/code&gt; compares the value with the previous period of the same length.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Chart.timeseries&lt;/code&gt; picks day, week or month buckets based on the date range of your data, unless you set &lt;code&gt;bucket&lt;/code&gt; yourself.&lt;/li&gt;
&lt;li&gt;If the ORM can't express what you need, pass a callable: &lt;code&gt;Stat("Churn rate", lambda qs: churn_for(qs), format="percent")&lt;/code&gt;. If it raises an exception, the card shows &lt;code&gt;—&lt;/code&gt; and the rest of the page still works.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Kanban, calendar, gallery and map views
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93v3k6l033binzm7kxoq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93v3k6l033binzm7kxoq.webp" alt="Kanban board" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_views&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kanban&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calendar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# first one is the default
&lt;/span&gt;    &lt;span class="n"&gt;kanban_field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                         &lt;span class="c1"&gt;# board columns
&lt;/span&gt;    &lt;span class="n"&gt;calendar_field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ships_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                     &lt;span class="c1"&gt;# where records land
&lt;/span&gt;    &lt;span class="n"&gt;kanban_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# what "Group by" offers
&lt;/span&gt;    &lt;span class="n"&gt;card_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;             &lt;span class="c1"&gt;# what a card shows
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Forms with a layout DSL
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzh3r3la3un4nmzt3gea.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzh3r3la3un4nmzt3gea.webp" alt="Order form" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Tabs&lt;/code&gt;, &lt;code&gt;Tab&lt;/code&gt;, &lt;code&gt;Section&lt;/code&gt; and &lt;code&gt;Row&lt;/code&gt; nest however you like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vantage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tabs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;


&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Tabs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Publishing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collapsed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta_title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta_description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Editable inlines work as you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vantage.inlines&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Inline&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderItemInline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Inline&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderItem&lt;/span&gt;
    &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unit_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;max_num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;


&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelPanel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;inlines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OrderItemInline&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; set &lt;code&gt;max_num&lt;/code&gt; explicitly on inlines for now. In the current alpha, leaving it unset raises an error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  A cascade-delete preview
&lt;/h3&gt;

&lt;p&gt;Before you delete a record, Vantage shows &lt;strong&gt;exactly what else will be deleted&lt;/strong&gt;. It uses the same &lt;code&gt;Collector&lt;/code&gt; 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:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"This customer cannot be deleted while Orders still reference it. Remove or reassign those first."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Undo that respects your teammates
&lt;/h3&gt;

&lt;p&gt;Deletes, bulk edits and inline edits show an &lt;strong&gt;undo toast&lt;/strong&gt;. Undo &lt;strong&gt;never overwrites a newer change&lt;/strong&gt; made by someone else in the meantime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissions, roles and sensitive data
&lt;/h3&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07mu9ageu3lp4rqtcfjr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07mu9ageu3lp4rqtcfjr.webp" alt="Role matrix" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's also a &lt;strong&gt;read-only data-quality audit&lt;/strong&gt; for each model, and a &lt;strong&gt;schema map&lt;/strong&gt; that shows how your models relate:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkp89xtocs28v613mj7wb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkp89xtocs28v613mj7wb.webp" alt="Schema map" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A JSON API for every panel
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Theming from one colour
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;VANTAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SITE_TITLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOGO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;img/acme.svg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOGO_DARK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;img/acme-light.svg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BRAND_COLOR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#0ea5e9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BRAND_RADIUS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5rem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;THEME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Staging — data here is not real&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# banner on every page
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A superuser can change any of these later from the settings screen, without a deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; 3.12+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Django&lt;/strong&gt; 5.2, 6.0 or 6.1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime dependencies:&lt;/strong&gt; only Django&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional integrations&lt;/strong&gt; (used when installed, never required): django-guardian, django-rules, django-mptt, django-treebeard, django-simple-history&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Status: alpha, and I'm being honest about it
&lt;/h2&gt;

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




&lt;h2&gt;
  
  
  Try the demo
&lt;/h2&gt;

&lt;p&gt;The repository includes a demo wholesale shop with orders, customers and products. One command migrates it, seeds data and serves it on port 8765:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Pythondeveloper6/django-vantage
&lt;span class="nb"&gt;cd &lt;/span&gt;django-vantage
./example/run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Pythondeveloper6/django-vantage" rel="noopener noreferrer"&gt;github.com/Pythondeveloper6/django-vantage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;PyPI:&lt;/strong&gt; &lt;a href="https://pypi.org/project/django-vantage/" rel="noopener noreferrer"&gt;pypi.org/project/django-vantage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://pythondeveloper6.github.io/django-vantage/" rel="noopener noreferrer"&gt;pythondeveloper6.github.io/django-vantage&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mahmoud Ahmed&lt;/em&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Django Context Processor</title>
      <dc:creator>Mahmoud Ahmed</dc:creator>
      <pubDate>Mon, 02 Jan 2023 09:57:41 +0000</pubDate>
      <link>https://dev.to/pythondeveloper6/django-context-processor-359c</link>
      <guid>https://dev.to/pythondeveloper6/django-context-processor-359c</guid>
      <description>&lt;p&gt;&lt;strong&gt;today we will take about the django context processor&lt;/strong&gt; &lt;br&gt;
first lets imagine that we are building an Ecommerce Website, so our website will have products where clients can add to the cart then move to the checkout page and complete the process by adding their location and payment information or complete shopping and add more products to the cart&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnxbhta6cj4xrbv6q336c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnxbhta6cj4xrbv6q336c.jpg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;so in every page of the site the client can see the cart with the product that he added before and their quantity and price&lt;/p&gt;

&lt;p&gt;so when developing this, we need to include the cart with all of it’s information in every view in our project, and this a really a bad solution, because Django uses DRY concept which means Do Not Repeat Yourself, here the context processor comes to fix this issue, &lt;/p&gt;

&lt;p&gt;the context processor is a way to include something in all the site &lt;/p&gt;

&lt;p&gt;or what if we want to add the site footer , so the footer we be in all the pages so we need to include it the footer information in all the views or create a context processor for the site footer&lt;/p&gt;

&lt;p&gt;so now we should create a context processor for get footer or cart information instead of including that information in all the views in our project, but how can we create a context processor&lt;/p&gt;

&lt;p&gt;Creating Context Processor &lt;br&gt;
1 — I will create a new folder in the main project folder with the name of utils I always do this to add all the utils files in this folder (you can name it any name you want) or you can create the file in the app directory &lt;/p&gt;

&lt;p&gt;2 — create a new python file with a meaningful name so I will call it settings_context.py for getting footer information then start adding the context processor code in it &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvah8ziwziwgdk5x876t7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvah8ziwziwgdk5x876t7.png" alt=" " width="720" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;so here i imported the footer model then I added a function to get the footer information, it is just a normal Django view that retrieves only one context ‘footer’&lt;/p&gt;

&lt;p&gt;in the screenshot I also added another view to get the site logo now we need to include this view in our context processor in our settings.py&lt;/p&gt;

&lt;p&gt;3 - so we will open settings.py then move to the templates part then add our context processors for footer and the other for the logo&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqvgq8ueoembaqeynak9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqvgq8ueoembaqeynak9.png" alt=" " width="720" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and if you created a context for cart you will include it also&lt;/p&gt;

&lt;p&gt;so now the footer and the logo from our context will be available in all the project templates and we can use it like any context we will include it in {footer.anyinfo} like any template context&lt;/p&gt;

&lt;p&gt;now you do not have to include those information in any view&lt;/p&gt;

&lt;p&gt;you should use the Django context processor any time you found &lt;br&gt;
yourself need something in all the project pages &lt;/p&gt;

&lt;p&gt;if you have any questions leave it below , follow me for more content related to Django , python , data science , machine learning and vuejs &lt;/p&gt;

</description>
      <category>django</category>
      <category>contextprocessor</category>
      <category>python</category>
    </item>
  </channel>
</rss>
