
I’ve praised Django Admin in the past. It’s one of the most useful features of Django.
Despite all the advantages of Django Admin, it could still use a few improvements. One such change is adding a navigation menu in Django Admin, which we’re “fixing” in this tutorial.
When I started working with Django Admin I found it counter-intuitive to go back to homepage every time I needed to move to another module’s page. It’s something I wasn’t used to. I’m sure you’ve felt the same way (that’s why you’re here, I’m hoping!).
As developers, we’re used to navigating from one module to another directly from the navigation menu in every Admin Panel we’ve ever used. (The same goes for the client or employer you’re developing the admin panel for.) Sooner or later, you’ll likely find yourself tinkering with Django Admin templates, trying to add links to all the modules in the menu. This can get tricky.
Since the purpose of this post is to explain the procedure to add a navigation menu, we won’t be paying much attention to the design part. I’ll be using Bootstrap Navbar Component to create the UI, but you can use any other library or create your own UI from scratch.
Here’s what what our navigation menu in Django Admin will look like at the end of this tutorial:
Let’s create a new Django Project first.
I’m using Python 3.6 and Django 2.1, but these instructions are more or less similar for other recent versions of Python & Django.
Create a new directory and execute the command below to create a python virtual environment, and activate it.
# Creates a virtual environment in subdirectory "venv"python -m venv venv
# Activate itsource venv/bin/activate # Linuxvenv\Scripts\activate # Windows
While you’re in the virtual environment, install Django.
pip install Django # to install the latest stable releasepip install Django==2.1 # to install a specific version
Once installed, you may configure the database and other settings. Without diving deeper into the setup instructions, let’s quickly create a project and fire up the development server.
django-admin startproject navbar # create a Django project
python manage.py migrate # run migrations
python manage.py createsuperuser # create a super user
python manage.py runserver # start the development server
Now, when you navigate to http://127.0.0.1:8000/ it should show you Django’s default welcome page.
To access the admin panel, go to http://127.0.0.1:8000/admin/ then login using the credentials you created with python manage.py createsuperuser
command.
This is what you get out of the box. A fully functional Django Admin — without navigation menu bar.
Now that we have a functional Django Admin, let’s add a navigation menu.
We won’t be writing any Python/Django code yet. We’ll just replace Django Admin’s default header with a static bootstrap Navbar
.
First create a new app. Creating a separate app has its benefits. I’ll cover that in a bit.
python manage.py startapp my_admin
Once done, create directories /templates/admin
in my_admin app. Copy default Django Admin’s base.html
and base_site.html
to this directory.
In my case the default directory was found at /venv/Lib/site-packages/django/contrib/admin/templates/admin/
.
We’ll be working on the newly copied files, i.e. files in /my_admin/templates/admin/
.
Now, to use bootstrap Navbar
, first we need to add relevant bootstrap CSS and JavaScript files. To do so, download bootstrap source files from their official website. We’re downloading source files because we won’t be using all features of Bootstrap. We’ll be using SASS preprocessor to pick only the parts we need.
Extract the downloaded files to /my_admin/static/admin/
With that completed, let’s start editing some HTML.
Open /my_admin/templates/admin/base.html
and add the following snippet in the <head>
section to add bootstrap CSS. Make sure you write correct path to bootstrap.scss
file.
{% load compress %}{% compress css %}<link rel=”stylesheet” href=”{% static “admin/bootstrap-4.1.3/scss/bootstrap.scss” %}” type=”text/x-scss” charset=”utf-8">{% endcompress %}
Since we just need the Navbar
component, you can remove the imports from bootstrap.scss
which are not required, otherwise it might conflict with existing Django CSS. Here’s my version of bootstrap.scss
for reference:
/*!
Bootstrap v4.1.3 (https://getbootstrap.com/)
Copyright 2011-2018 The Bootstrap Authors
Copyright 2011-2018 Twitter, Inc.
Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)*/
@import "functions";@import "variables";@import "mixins";//@import "root";//@import "reboot";//@import "type";//@import "images";//@import "code";//@import "grid";//@import "tables";//@import "forms";//@import "buttons";@import "transitions";@import "dropdown";//@import "button-group";//@import "input-group";//@import "custom-forms";//@import "nav";@import "navbar";//@import "card";//@import "breadcrumb";//@import "pagination";//@import "badge";//@import "jumbotron";//@import "alert";//@import "progress";//@import "media";//@import "list-group";//@import "close";//@import "modal";//@import "tooltip";//@import "popover";//@import "carousel";@import "utilities";//@import "print";
Similarly, add the following links to bootstrap JS modules. To do so, paste the following code just before closing </body>
tag.
{# bootstrap js #}
{% compress js %}<script src=”{% static ‘admin/bootstrap-4.1.3/js/dist/util.js’ %}” type=”text/javascript” charset=”utf-8"><script src=”{% static ‘admin/bootstrap-4.1.3/js/dist/dropdown.js’ %}” type=”text/javascript” charset=”utf-8"><script src=”{% static ‘admin/bootstrap-4.1.3/js/dist/collapse.js’ %}” type=”text/javascript” charset=”utf-8">{% endcompress %}
{# END bootstrap js #}
. . .
"
.scss”
FilesYou may have noticed that we’re using .scss
files in HTML directly, and the compress
block is not supported by Django out of the box. If you’re using lint checks, your code editor is probably showing warnings by now.
This tag is provided by django-compressor to process .scss
(and other) files. You may choose to use any other library or none at all. For the sake of this tutorial we’re using django-compressor.
To install and configure Django Compressor, follow these steps:
pip install django-compressor django-libsass
while you’re in Python virtual environment.compressor
to INSTALLED_APPS
in settings.py
.settings.py
.COMPRESS_PRECOMPILERS = ((‘text/x-scss’, ‘django_libsass.SassCompiler’),)
to settings.py
.settings.py
:STATICFILES_FINDERS = (‘django.contrib.staticfiles.finders.FileSystemFinder’,‘django.contrib.staticfiles.finders.AppDirectoriesFinder’,# other finders..‘compressor.finders.CompressorFinder’,)
Detailed installation instructions of Django Compressor are available on readthedocs.
. . .
Now, let’s replace the default header with Bootstrap Navbar
. Don’t bother with Django code yet, we’ll make the menu dynamic in next section. Delete (or comment out) the existing header code, it looks similar to this:
<div id="header"> <div id="branding"> {% block branding %}{% endblock %} </div> {% block usertools %} {% if has_permission %} <div id="user-tools"> {% block welcome-msg %} {% trans 'Welcome,' %} <strong>{% firstof user.get_short_name user.get_username %}</strong>. {% endblock %} {% block userlinks %} {% if site_url %} <a href="{{ site_url }}">{% trans 'View site' %}</a> / {% endif %} {% if user.is_active and user.is_staff %} {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %} <a href="{{ docsroot }}">{% trans 'Documentation' %}</a> / {% endif %} {% endif %} {% if user.has_usable_password %} <a href="{% url 'admin:password_change' %}">{% trans 'Change password' %}</a> / {% endif %} <a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a> {% endblock %} </div> {% endif %} {% endblock %} {% block nav-global %}{% endblock %}</div>
…and replace it with bootstrap Navbar
. The following code is taken from Bootstrap documentation, we’ll modify it later.
<nav class=”navbar navbar-expand-lg navbar-light bg-light”> <a class=”navbar-brand” href=”#”>Navbar</a> <button class=”navbar-toggler” type=”button” data-toggle=”collapse” data-target=”#navbarSupportedContent” aria-controls=”navbarSupportedContent” aria-expanded=”false” aria-label=”Toggle navigation”> <span class=”navbar-toggler-icon”></span> </button> <div class=”collapse navbar-collapse” id=”navbarSupportedContent”> <ul class=”navbar-nav mr-auto”> <li class=”nav-item active”><a class=”nav-link” href=”#”>Home <span class=”sr-only”>(current)</span></a></li> <li class=”nav-item”><a class=”nav-link” href=”#”>Link</a></li> <li class=”nav-item dropdown”> <a class=”nav-link dropdown-toggle” href=”#” id=”navbarDropdown” role=”button” data-toggle=”dropdown” aria-haspopup=”true” aria-expanded=”false”> Dropdown</a> <div class=”dropdown-menu” aria-labelledby=”navbarDropdown”> <a class=”dropdown-item” href=”#”>Action</a> <a class=”dropdown-item” href=”#”>Another action</a> <div class=”dropdown-divider”></div> <a class=”dropdown-item” href=”#”>Something else here</a> </div> </li> <li class=”nav-item”><a class=”nav-link disabled” href=”#”>Disabled</a></li> </ul> <form class=”form-inline my-2 my-lg-0"> <input class=”form-control mr-sm-2" type=”search” placeholder=”Search” aria-label=”Search”> <button class=”btn btn-outline-success my-2 my-sm-0" type=”submit”>Search</button> </form> </div></nav>
Also add the following snippet before closing </head>
tag to avoid django CSS from overriding bootstrap’s CSS.
<style type=”text/css”> .navbar ul li { list-style-type: none; padding: 0; } .navbar .dropdown-item { width: auto; }</style>
Our template is ready to show bootstrap Navbar.
Just one thing though. We need to add my_admin
to INSTALLED_APPS
in settings.py
. Let’s do that quickly, and start the development server again.
Navigate to http://localhost:8000/admin/.
Voila! There’s the Navbar.
Let’s make it compatible with Django default Admin so that it plays nice with existing template files and makes modification easier.
. . .
First, replace <a class=”navbar-brand” href=”#”>Navbar</a>
with {% block branding %}{% endblock %}
. base_site.html
specifies this part in Django.
Let’s quickly switch to base_site.html
and replace the following block of code:
<h1 id=”site-name”><a href=”{% url ‘admin:index’ %}”>{{ site_header|default:_(‘Django administration’) }}</a></h1>
…with this:
<a href=”{% url ‘admin:index’ %}” class=”navbar-brand”>{{ site_header|default:_(‘Django administration’) }}</a>
Back to editing base.html
. Replace Navbar
menu items with the following code to dynamically populate menu with list of apps.
{% if app_list %} <ul class="navbar-nav mr-auto"> {% for app in app_list %} <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="{{ app.app_url }}" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ app.name }}</a> <div class="dropdown-menu" aria-labelledby="navbarDropdown"> {% for model in app.models %} {% if model.admin_url %} <a class="dropdown-item" href="{{ model.admin_url }}">{{ model.name }}</a> {% endif %} {% endfor %} </div> </li> {% endfor %} </ul>{% elif user.is_authenticated %} <span class="navbar-text">You don't have permission to edit anything.</span>{% endif %}
Replace the bootstrap search form with the following code to display user action links.
{% block usertools %} {% if has_permission %} <ul class="navbar-nav my-2 my-lg-0"> <li class="nav-item dropdown dropleft text-right"> <a class="nav-link dropdown-toggle" href="#" id="nav-user-icon" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> {% block welcome-msg %}<strong>{% firstof user.get_short_name user.get_username %}</strong>{% endblock %} </a> <div class="dropdown-menu" aria-labelledby="nav-user-icon"> {% block userlinks %} {% if site_url %}<a class="dropdown-item" href="{{ site_url }}">{% trans 'View site' %}</a>{% endif %} {% if user.is_active and user.is_staff %} {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %}<a class="dropdown-item" href="{{ docsroot }}">{% trans 'Documentation' %}</a>{% endif %} {% endif %} {% if user.has_usable_password %}<a class="dropdown-item" href="{% url 'admin:password_change' %}">{% trans 'Change password' %}</a>{% endif %} <a class="dropdown-item" href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a> {% endblock %} </div> </li> </ul> {% endif %}{% endblock %}{% block nav-global %}{% endblock %}
Run the development server again, then navigate to Django admin. It works, right?
There’s a tiny issue though. If you navigate to any other page except the homepage, the navigation menu shows different items based on the current page. We did all this effort to show a menu with all items at all pages, didn’t we? Let’s fix that.
We’re using app_list
tag provided by default in Django, but it does not return all apps’ list at all times. It takes current page as context, and modifies the result accordingly. The solution? We need a custom tag which can return all items at all times.
Create a python package named templatetags
in my_admin
app, and create a file utils.py
in it. Paste the following code in this file.from django import template
from django.contrib import adminregister = template.Library()class CustomRequest: def __init__(self, user): self.user = [email protected]_tag(takes_context=True)def get_app_list(context, **kwargs): custom_request = CustomRequest(context['request'].user) app_list = admin.site.get_app_list(custom_request) return app_list
Next, in settings.py
add the following to OPTIONS
in TEMPLATES
variable:
'libraries': { 'common_utils': 'my_admin.templatetags.common_utils',},
TEMPLATES
might look something like this after adding it:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], 'libraries': { 'common_utils': 'my_admin.templatetags.common_utils', }, }, },]
Next, go back to base.html
and make the following changes:
{% load common_utils %}
somewhere at the top of file to load our custom template tags.{% get_app_list as all_app_list %}
above {% if app_list %}
.app_list
variable to all_app_list
.Your header in base.html
might look something like this:
<nav class="navbar navbar-expand-lg navbar-light bg-light">
{% block branding %}{% endblock %}
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
{% load common_utils %}
{% get_app_list as all_app_list %}
{% if all_app_list %}
<ul class="navbar-nav mr-auto">
{% for app in all_app_list %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="{{ app.app_url }}" id="navbarDropdown"
role="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">{{ app.name }}</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{% for model in app.models %}
{% if model.admin_url %}
<a class="dropdown-item"
href="{{ model.admin_url }}">{{ model.name }}</a>
{% endif %}
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
{% elif user.is_authenticated %}
<span class="navbar-text">You don't have permission to edit anything.</span>
{% endif %}
{% block usertools %}
{% if has_permission %}
<ul class="navbar-nav my-2 my-lg-0">
<li class="nav-item dropdown dropleft text-right">
<a class="nav-link dropdown-toggle" href="#" id="nav-user-icon" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% block welcome-msg %}
<strong>{% firstof user.get_short_name user.get_username %}</strong>{% endblock %}
</a>
<div class="dropdown-menu" aria-labelledby="nav-user-icon">
{% block userlinks %}
{% if site_url %}
<a class="dropdown-item" href="{{ site_url }}">{% trans 'View site' %}</a>
{% endif %}
{% if user.is_active and user.is_staff %}
{% url 'django-admindocs-docroot' as docsroot %}
{% if docsroot %}
<a class="dropdown-item"
href="{{ docsroot }}">{% trans 'Documentation' %}</a>
{% endif %}
{% endif %}
{% if user.has_usable_password %}
<a class="dropdown-item"
href="{% url 'admin:password_change' %}">{% trans 'Change password' %}</a>
{% endif %}
<a class="dropdown-item"
href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>
{% endblock %}
</div>
</li>
</ul>
{% endif %}
{% endblock %}
{% block nav-global %}{% endblock %}
</div>
</nav>
We’re done! Everything should work as expected.
. . .
You might be tempted to edit the core files directly or not using existing Django template blocks
, but the advantage of the above approach is that we get a modular standalone app. You can literally use this app with any standard Django Admin project and you’ll get a working navigation menu bar with no extra efforts.
We’ve used bootstrap in this tutorial to avoid spending time in creating UI. In your project you may use any other navigation menu component or create your own component from scratch.
What we are essentially doing is getting list of apps from our custom template tag and showing them in the menu as links. You can use the same approach and create material design drawer menu instead of Navbar! The best part is that you can override any part of Navbar
as you would have done with default Django Admin template.
The project I created for this tutorial is available on github for reference: https://github.com/TheKalpit/Bootstrap-Navbar-in-Django-Admin
Thanks for reading!
Were you able to create Navbar easily using this tutorial? Do you have any other tips to improve this tutorial? Let me know your experience in comments below.
If you like this post, you might also like, How to write an API in 3 lines of code with Django REST framework
Thanks to William Wickey for help editing.
Originally published:
September 12, 2018