Free Download Dashtic – Django Admin & Dashboard Template

The Overhaul Log: Gutting and Rebuilding a Django Backend

It was a grim Tuesday morning, chucking it down with rain, when I finally lost my temper with my own server setup. I look after a ragtag collection of web properties. Some of them are boring local business sites that barely get a hundred hits a month, but a few of them are high-traffic beasts. The main headache generator is an entertainment portal where visitors basically just hang around to play HTML5 arcade games. The front-facing side of that portal is solid. I have it sitting behind a heavy caching layer, so when a traffic spike hits, the CDN takes the brunt of it and my actual application servers barely blink.

The problem was entirely behind closed doors. The administrative backend—the place where my moderators review comments, where I upload new game assets, and where I check the daily user registration logs—was a total dog's breakfast.

When I first built the site years ago, I relied heavily on the default Django admin interface. If you know Django, you know the built-in admin is a fantastic tool for getting a project off the ground. It gives you out-of-the-box CRUD (Create, Read, Update, Delete) operations. But it is fundamentally designed for data entry, not for complex business logic, custom workflows, or visual analytics. As the site grew, I started hacking the default admin. I was overriding core templates, writing massive, ugly custom actions in admin.py, and bolting on janky jQuery scripts to try and make it do things it was never meant to do.

Eventually, the whole thing became unmaintainable. The UI looked like a spreadsheet from 1998, and trying to train a new moderator to use it required a 40-page Google Doc. I knew I had to build a custom dashboard from scratch, completely separate from the default Django admin URL space. This log details that restructuring process, the architectural decisions I made, the mistakes I had to backtrack on, and the reality of deploying a heavily modified UI shell in a production Python environment.

The SPA Trap: Why I Stayed with Django Templates

When you tell modern web developers that your backend UI is clunky and needs a rewrite, nine out of ten will immediately tell you to decouple the architecture. "Mate, just build a REST API with Django Rest Framework and write the frontend in React or Vue. It's the only way."

I strongly disagree, especially from an operations perspective for a solo developer or a very small team.

This is one of the most common mistakes I see system admins and backend guys make. They get lured by the slickness of a Single Page Application (SPA) and completely underestimate the maintenance overhead. If I went the React route, I wouldn't just be fixing my UI. I would be taking on a second, entirely separate codebase. I would have to set up Node.js build pipelines, manage Webpack or Vite configurations, deal with a massive node_modules folder, handle client-side routing, and manually recreate all the form validation logic that Django already handles natively. I would also have to figure out token-based authentication (JWT) just to let my moderators log in safely.

It’s an absolute trap. My business logic was already written in Python. My form validation was rock solid in my forms.py files. My session management was handled by Django's robust middleware. I didn't want to throw all of that away just to get a sidebar that didn't reload the page.

I made a firm architectural decision: I was going to stick with Django's native Model-View-Template (MVT) architecture. The server would continue to render HTML and send it to the browser. What needed to change wasn't the transport mechanism; it was the HTML and CSS itself. I just needed a modern, well-structured UI shell that I could drop my Django template tags into.

Finding the Scaffold: The Structural Shell

I am a backend guy. My idea of frontend design is making sure the text isn't the exact same color as the background. Writing a responsive CSS grid, styling form inputs, and making sure a sidebar collapses properly on a mobile phone takes me weeks of frustrating trial and error.

I decided to grab a pre-built HTML template to use as the scaffolding. I spent a few nights digging through directories, looking for something specific. I didn't want a React template, and I didn't want a raw Bootstrap file that I'd have to wire up from absolute scratch.

I eventually settled on the Dashtic – Django Admin & Dashboard Template. I didn't pick it because of the color scheme (which I ended up changing anyway) or the dummy charts. I picked it strictly for the directory structure. It was specifically formatted for Django environments out of the box. The static files (CSS, JS, images) were already organized in a way that collectstatic would understand, and the HTML files were already broken down into base layouts and includes ({% include 'partials/sidebar.html' %}).

This saved me the initial three days of grunt work translating raw HTML into Django's template language. I pulled the files onto my local development machine, created a new Django app called dashboard, and started the teardown process.

Phase 1: Ripping Out the Bloat

The reality of using any commercial or open-source template is that they are built to look good in a demo. They include every charting library, every calendar plugin, and every map widget known to mankind. If you just drop the whole thing onto your server, your page load times will be abysmal, and your users will be downloading 4 megabytes of JavaScript they never execute.

My first task was aggressive pruning. I opened the base.html file provided in the scaffold.

I looked at the <head> tag. There were about fifteen CSS files linked. I deleted the ones for vector maps, the ones for the specialized calendar, and the ones for the drag-and-drop file uploaders I wasn't going to use.

Then I scrolled to the bottom where the JavaScript was loaded. I gutted it. I ripped out Chart.js, I ripped out the mapping scripts, I ripped out the redundant jQuery plugins. I reduced the payload down to the absolute bare minimum required to make the sidebar toggle work and the dropdown menus function.

This pruning is critical for stability. Every third-party library you leave in your code is a potential point of failure, a potential security vulnerability, and extra weight your server has to push. From an ops perspective, less code is always better.

Once I had stripped the base template down to a naked, fast-loading shell, I started wiring it into my existing Django project.

Phase 2: Wiring the Base Template and Static Files

Django handles static files in a very specific way. In development, the local server serves them. In production, you run a command called collectstatic which gathers all your CSS, JS, and images from across your various apps and dumps them into a single directory, usually served directly by Nginx or a middleware like Whitenoise.

The scaffold I brought in had its own static folder. I moved this into my new dashboard app directory: dashboard/static/dashboard/. (Using that inner folder is a Django best practice to prevent namespace collisions if two different apps have a file named style.css).

Then came the tedious part. I had to open base.html and replace every single hardcoded path with a Django template tag.

Instead of <link rel="stylesheet" href="assets/css/style.css">, I had to ensure {% load static %} was at the top of the file, and rewrite it as <link rel="stylesheet" href="{% static 'dashboard/css/style.css' %}">.

I spent a few hours doing this for every image, every script, and every font file. If you miss even one, you end up with a broken layout or a 404 error in your browser console that you'll have to hunt down later.

Next, I established the block structure. A good Django layout relies heavily on template inheritance. The base.html file acts as the master wrapper. It contains the <html> tags, the sidebar, and the top navigation.

Inside the main content <div>, I defined {% block content %}{% endblock %}.

This meant that when I created the page to view registered users, my users.html file only needed to be a few lines long:

{% extends 'dashboard/base.html' %}

{% block content %}
  <h1>Registered Users</h1>

{% endblock %}

Getting this inheritance structure locked in early meant I never had to copy and paste the sidebar code again. If I needed to add a new link to the menu, I edited partials/sidebar.html once, and it updated across the entire custom admin area.

Phase 3: The Routing and View Logic Shift

With the visual shell working locally, I had to figure out how to route traffic to it securely. I was abandoning the admin/ namespace for my daily tasks.

I set up a new urls.py inside my dashboard app. I mapped it to site.com/manage/.

Now I had to write the views. In the old days, I might have written functional views, but for a structured dashboard, Class-Based Views (CBVs) are vastly superior. They allow you to reuse logic cleanly.

I created a base view that all my dashboard views would inherit from. Security was the absolute priority here. I couldn't have random visitors stumbling into the moderation queue.

Django provides a mixin called LoginRequiredMixin, but that only checks if a user is logged in. I needed to ensure they actually had staff privileges. I also used UserPassesTestMixin.

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import TemplateView

class DashboardBaseView(LoginRequiredMixin, UserPassesTestMixin, TemplateView):
    login_url = '/login/'

    def test_func(self):
        # Only allow staff or superusers into the dashboard
        return self.request.user.is_staff or self.request.user.is_superuser

Every single view I built for the new UI—whether it was the GameUploadView, the UserModerationView, or the TrafficLogsView—inherited from this DashboardBaseView. This architectural decision ensured that I could never accidentally leave a route unprotected. The permissions logic was baked into the foundation.

Phase 4: Context Processors and the Global UI State

Here is a problem you run into very quickly when building a complex UI layout.

In my new structural shell, the top navigation bar had a little bell icon that displayed a red badge with the number of pending user comments that needed moderation. It also displayed the logged-in user's avatar and their current role.

Because this top navigation bar is part of base.html, it appears on every single page of the dashboard.

If I was naive, I would go into the get_context_data method of my GameUploadView and write a database query to count the pending comments, and pass it to the template. Then I would have to go into the TrafficLogsView and write the exact same query again. I would be duplicating code across twenty different views just to populate the top navigation bar.

This is a terrible ops practice. It leads to bloated code and forgotten queries.

The correct Django solution to this problem is writing a Context Processor.

A Context Processor is a simple Python function that runs before every single template is rendered across your entire project. It takes the request object and returns a dictionary of data that gets injected directly into the template context globally.

I created a file called context_processors.py inside my dashboard app:

from .models import Comment

def dashboard_global_data(request):
    # Only run these queries if the user is in the dashboard area and is staff
    if request.user.is_authenticated and request.user.is_staff and '/manage/' in request.path:
        pending_comments_count = Comment.objects.filter(status='pending').count()

        return {
            'global_pending_comments': pending_comments_count,
            'user_role_display': request.user.groups.first().name if request.user.groups.exists() else 'Staff'
        }
    return {}

I added this function path to the context_processors list in my main settings.py file.

Suddenly, the variable {{ global_pending_comments }} was available in every single template file in my project. The top navigation bar in base.html could render the red notification badge effortlessly, and I didn't have to pollute my individual Class-Based Views with redundant database queries.

It’s these kinds of structural decisions that make maintaining a Django backend a pleasure rather than a chore. You let the framework do the heavy lifting.

Phase 5: The Nightmare of Form Rendering

If I had to point to the single most frustrating part of this entire rebuild, it was getting Django's form engine to play nicely with the HTML provided by the new structural template.

Django forms are brilliant on the backend. You define a form class, tell it what model it links to, and Django handles the validation, the security, and the database saving. In your HTML template, you simply type {{ form }} and Django spits out all the <input> fields.

The problem is that Django spits out raw, unstyled HTML inputs.

The new UI layout I was using relied heavily on custom CSS classes to make the forms look good. A text input needed to have the class form-control input-lg custom-border.

If I just used {{ form }}, the form looked terrible. The inputs spilled out of their containers and the layout broke.

There are a few ways to fix this. The amateur way is to manually write out the HTML for every single form field in your template, bypassing Django's rendering entirely.

<input type="text" name="title" class="form-control" value="{{ form.title.value }}">
{{ form.title.errors }}

Doing this destroys one of Django's main benefits. If you change a field in your database model later, you have to remember to go manually update the HTML file. It’s brittle.

The robust way to handle this—and what I implemented—is to override the widget attributes directly inside the Python forms.py file.

When I built the form for moderators to edit user details, I explicitly told Django to attach the required CSS classes to the HTML it generates.

from django import forms
from .models import CustomUser

class UserEditForm(forms.ModelForm):
    class Meta:
        model = CustomUser
        fields = ['username', 'email', 'is_active']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Loop through all fields and apply the CSS classes the UI template expects
        for field_name, field in self.fields.items():
            field.widget.attrs['class'] = 'form-control form-control-modern'

            # Add specific attributes if needed
            if field_name == 'email':
                field.widget.attrs['placeholder'] = 'Enter valid email address'

By keeping the CSS class assignment in the Python layer, I could go back to simply using {{ form }} (or looping through {{ form.fields }}) in my HTML template. The forms rendered perfectly, matching the styling of the new dashboard, while still retaining all of Django's native CSRF protection and backend validation.

This phase took me a full weekend to get right across all the different forms (file uploads, date pickers, dropdowns), but it was the key to making the custom backend actually usable without throwing away the framework's security features.

Phase 6: Dealing with the N+1 Query Killer

As the new dashboard started to take shape, it looked fantastic. I had a clean sidebar, slick tables, and nice profile cards. Because the UI was so much better, I found myself wanting to display more data on the screen at once.

On the main user list page, instead of just showing the username and email, I decided to also show the name of their most recently played game, and the total number of comments they had left on the arcade portal.

I updated my template to look something like this:

{% for user in users %}
  <tr>
    <td>{{ user.username }}</td>
    <td>{{ user.recent_game.title }}</td>
    <td>{{ user.comment_set.count }}</td>
  </tr>
{% endfor %}

I refreshed the page locally. It loaded fine.

Then I deployed it to my staging server, which had a copy of the production database with hundreds of thousands of users. I clicked the "Users" link in the sidebar.

The browser tab spun. And spun. And spun. After about twenty seconds, it finally loaded.

I had introduced a massive N+1 query problem. This is a classic trap when moving to a denser, more complex UI layout.

In the code above, Django first makes one query to get the list of users (let's say 50 users for a paginated page). That’s query number 1.

Then, as the template loops through those 50 users, it hits user.recent_game.title. Because that data isn't in memory, Django makes a trip to the database to get the game title. That’s 50 new queries.

Then it hits user.comment_set.count. That’s another database trip. That’s another 50 queries.

To render one single page of 50 users, my server was making 101 separate database queries. No wonder it choked.

When you revamp a UI to show more relational data, you must optimize your ORM calls in the view. I had to go back to my UserListView and rewrite the queryset to tell Django to grab all the related data in one massive join before it even touched the template.

class UserListView(DashboardBaseView):
    template_name = 'dashboard/user_list.html'
    paginate_by = 50

    def get_queryset(self):
        # The fix: use select_related for foreign keys and prefetch_related for reverse relations/many-to-many
        return CustomUser.objects.select_related('recent_game').prefetch_related('comment_set').all().order_by('-date_joined')

By adding select_related and prefetch_related, I told the Django ORM to do the heavy lifting at the database level. It performed a SQL JOIN to grab the games, and a single separate query to grab all the comments for those 50 users, caching them in Python memory.

I refreshed the staging server. The page load time dropped from twenty seconds down to roughly 150 milliseconds. The total number of database queries dropped from 101 down to exactly 3.

This is the ops reality of UI redesigns. You can't just slap new HTML variables into a template and expect the server to handle it. You have to understand how the visual changes impact the underlying data fetching logic.

Phase 7: Mobile Navigation and JavaScript Conflicts

One of the secondary reasons for this entire rebuild was that my old custom admin panel was completely unusable on a mobile phone. If a moderator pinged me on Slack while I was at the pub saying a spam bot was hammering the arcade site, trying to log in and ban the IP address from my phone was an exercise in pure frustration. The tables required horizontal scrolling, and the buttons were too small to tap accurately.

The new structural template promised full mobile responsiveness. And it was, mechanically. But when I integrated it into my Django setup, the mobile sidebar toggle button stopped working. I would tap the hamburger icon on my phone, and nothing would happen.

I had to put on my debugging hat and dive into the browser console.

The issue stemmed from how I had restructured the static files and how the template's proprietary JavaScript was attempting to bind to the DOM.

The original template assumed that the entire HTML structure was present immediately on a raw page load. However, because I was using Django's template inheritance ({% block %}), some of the DOM elements were loading in a slightly different order than the script expected. Furthermore, I had stripped out jQuery from the header because I loathe loading it globally, moving it to the footer to improve page render times.

The sidebar toggle script, which was sitting in a file called app.js, was firing before jQuery had actually initialized. It threw a silent error and died.

I had to write a small patch in my base.html footer to ensure that any initialization scripts only ran after the entire document was ready and the core libraries were loaded.

<script src="{% static 'dashboard/js/jquery.min.js' %}"></script>
<script src="{% static 'dashboard/js/app.js' %}"></script>

<script>
  $(document).ready(function() {
      // Manually trigger the sidebar binding if the screen width is mobile
      if ($(window).width() < 768) {
          bindMobileSidebar(); 
      }
  });
</script>

It was a minor bodge job, but it fixed the issue perfectly. Having a functional mobile backend changes your life as an admin. I can now restart server processes, ban users, and approve game uploads while sitting on a train, using an interface that actually responds to touch correctly.

Phase 8: Deployment and the Reality of collectstatic

After a few weeks of working on this in my evenings, the custom dashboard was feature-complete. It was secure, it was fast, and the queries were optimized. It was time to push it to the live server.

Deploying static files in a Django production environment is always a bit stressful.

My server stack uses Gunicorn as the Python WSGI HTTP Server, sitting behind Nginx as the reverse proxy. Nginx is brilliant at serving static files (CSS, JS, images) quickly, but it needs to know exactly where they are. It shouldn't be asking Python to serve a .png file.

I SSH'd into my digital ocean droplet, pulled the latest code from my Git repository, and ran the critical command:

python manage.py collectstatic --noinput

Django went through every single app in my project, grabbed all the static files—including the hundreds of new CSS and JS files from my custom dashboard layout—and copied them into the master /var/www/myproject/static/ directory.

I had previously configured Whitenoise, a middleware that allows Python web apps to serve their own static files if Nginx isn't available. But for this environment, I relied on Nginx. I updated my Nginx server block to ensure the caching was aggressive for these new assets.

location /static/ {
    alias /var/www/myproject/static/;
    expires 30d;
    add_header Cache-Control "public, max-age=2592000";
}

By setting a 30-day cache expiry, I ensured that when my moderators logged in on Monday morning, their browser would download the new layout CSS and JS exactly once. Every subsequent click and page navigation throughout the week would pull those assets directly from their local browser cache.

I restarted the Gunicorn service and watched the logs.

sudo systemctl restart gunicorn

The real test of a structural rewrite isn't just how it looks; it's how it impacts the server hardware. I opened htop to monitor the CPU and RAM usage.

When the moderators logged in and started their daily tasks, I watched the worker processes. In the old, bloated setup, a moderator clicking through a paginated list of users would cause a Gunicorn worker to spike its memory usage to around 150MB just to render the messy, unoptimized views and run the N+1 queries.

With the new architecture, the optimized ORM calls and the clean HTML output meant the workers were processing requests faster and dropping them sooner. The memory footprint per worker hovered comfortably around 90MB. The CPU load remained flat.

By building a proper, dedicated dashboard interface instead of hacking the default Django admin, I had actually reduced the hardware strain on my server.

Final Thoughts on the Restructure

Looking back at the whole process, deciding to build a custom backend interface using standard Django templates was absolutely the right call.

It wasn't a trivial amount of work. Gutting a pre-built structural layout, mapping all the static files, overriding the default form widgets, and rewriting the database queries took the better part of a month. There were nights where I was pulling my hair out trying to figure out why a dropdown menu was hiding behind a table header due to a z-index conflict.

But the end result is a highly stable, highly secure administrative tool. I didn't have to write a single line of React. I didn't have to learn a new frontend state management library. I leveraged the exact same Python business logic, the same authentication middleware, and the same robust ORM that powers the rest of the application.

The interface is completely distinct from the default /admin URL, which I have now locked down tightly to superusers only for emergency database interventions.

If you are a sysadmin or a backend developer sitting on a messy legacy application, do not feel pressured into throwing away your server-rendered templates just because SPAs are the current trend. Find a clean structural shell, strip out the bloat, wire it up properly to your views, optimize your queries, and let the server do what it is good at. The operational peace of mind is worth the effort. ```

评论 0