Categories in Django with Breadcrumbs
An Efficient approach using mptt
Overview
Guide to create a hierarchical categories in a tree structure easily with Django.
We use the package django-mptt
which takes care of handling the tree
structure efficiently.
MPTT is a technique for storing hierarchical data in a database. The aim is to make retrieval operations very efficient.
In this way, we can also show breadcrumbs in our site with a link to each category and subcategories for each model.
1. Install mptt
1.1 Package
$ pip install django-mptt
1.2 Add to settings
Add the package3 to INSTALLED_APPS
settings configuration, in
settings.py
:
INSTALLED_APPS = (
'django.contrib.auth',
# ...
'mptt',
)
2. Configure models
We create a Categories
model extending from mptt.models.MPTTModel
instead of models.Model
, in models.py
. Also a Item
model to
assign categories to them.
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from mptt.models import MPTTModel, TreeForeignKey
class Category(MPTTModel):
name = models.CharField(max_length=settings.BLOG_TITLE_MAX_LENGTH, unique=True)
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
slug = models.SlugField(max_length=settings.BLOG_TITLE_MAX_LENGTH, null=True, blank=True)
description = models.TextField(null=True, blank=True)
class MPTTMeta:
order_insertion_by = ['name']
class Meta:
verbose_name_plural = 'Categories'
def __str__(self):
return self.name
def save(self, *args, **kwargs):
value = self.title
if not self.slug:
self.slug = slugify(value, allow_unicode=True)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('items-by-category', args=[str(self.slug)])
class Item(models.Model):
title = models.CharField(max_length=settings.BLOG_TITLE_MAX_LENGTH)
category = TreeForeignKey('Category', on_delete=models.CASCADE, null=True, blank=True)
slug = models.SlugField(
max_length=settings.BLOG_TITLE_MAX_LENGTH,
)
def __str__(self):
return self.title
def get_absolute_url(self):
kwargs = {
'slug': self.slug
}
return reverse('item-detail', kwargs=kwargs)
def save(self, *args, **kwargs):
if not self.slug:
value = self.title
self.slug = slugify(value, allow_unicode=True)
super().save(*args, **kwargs)
Note that the Category.parent
field is used by mptt
to keep track
of the tree structure.
3. Create Category Tree
We can start creating categories by adding models.Category
instances
and choosing parent=my_category
to create children, like
root1 = Category.objects.create(name="Root Category One")
first1 = Category.objects.create(name="Root One First Level Category One", parent=root1)
There is also an admin panel interface where we can add and rearrange
the tree structure graphically, with mptt.admin.DraggableMPTTAdmin
, so in admin.py
:
from django.contrib import admin
from .models import Category
from mptt.admin import DraggableMPTTAdmin
class CategoryAdmin(DraggableMPTTAdmin):
pass
admin.site.register(Category, CategoryAdmin )
admin.site.register(Item)
Run a server python runserver
and visit
http://localhost:8000/admin/YOUR-APP/category/
to create the tree
structure manually.
4. Handle requests
In urls.py
we create a path to access the category’s tree list:
from django.urls import path
from games.views import ItemsByCategoryView, CategoryListView
urlpatterns = [
// ...
path('', CategoryListView.as_view() , name='category-list'),
path('<str:slug>/', ItemsByCategoryView.as_view() , name='category-detail'),
]
and their associated views in views.py
:
from django.views import generic
from .models import Item, Category
class CategoryListView(generic.ListView):
model = Category
template_name = "items/category_list.html"
class ItemsByCategoryView(generic.ListView):
ordering = 'id'
paginate_by = 10
template_name = 'items/items_by_category.html'
def get_queryset(self):
# https://docs.djangoproject.com/en/3.1/topics/class-based-views/generic-display/#dynamic-filtering
# the following category will also be added to the context data
self.category = Category.objects.get(slug=self.kwargs['slug'])
queryset = Item.objects.filter(category=self.category)
# need to set ordering to get consistent pagination results
queryset = queryset.order_by(self.ordering)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = self.category
return context
This will expose two variables in our template, category
and
object_list
with the list of items.
5. Show categories
In templates/items/category_list.html
:
{% extends "blog/base.html" %}
{% load mptt_tags %}
{% block title %}List of categories{% endblock %}
{% block content %}
<div>
<div>
<ul>
{% recursetree object_list %}
<li>
<a href="{{node.get_absolute_url}}">{{node.name}}</a>
{% if not node.is_leaf_node %}
<ul class="children">
{{ children }}
</ul>
{% endif %}
</li>
{% endrecursetree %}
</ul>
</div>
</div>
{% endblock %}
And for item list by category, items/items_by_category.html
:
{% extends "blog/base.html" %}
{% block title %}{{category.name}} list of items{% endblock %}
{% block content %}
<main>
<section>
<div class="container">
<h1>{{category}} items</h1>
<p>{{category.description}}.</p>
</div>
</section>
<div>
{% for item in object_list%}
<div>
<a href="{{item.get_absolute_url}}">
<p>{{item}}</p>
</a>
</div>
{% endfor %}
{% if is_paginated %}
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">« first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
{% endif %}
</span>
</div>
{% endif %}
</div>
</main>
{% endblock %}
and a base.html
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
6. Add breadcrumbs
Breadcrumbs are useful to help users to know which level of the
website structure they are on. we can include the following partial
template as _breadcrumbs.html
:
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">🏠</a></li>
{% for node in ancestors %}
<li class="breadcrumb-item"><a href="{{node.get_absolute_url}}">{{node.name}}</a></li>
{% endfor %}
<li class="breadcrumb-item active" aria-current="page">{{ object }}</li>
</ol>
</nav>
And calling it from other templates with the usage of the include
4
tag, providing the following variables:
ancestors
: the category treeobject
: the current node
So in any template:
//...
{% include "blog/_breadcrumbs.html" with ancestors=object.category.get_ancestors object=object%}
Repo
The above code is available as a repo in Github: https://github.com/marcanuy/django-categories-example-app
With its demo working at: https://django-categories-example-app.herokuapp.com/
References
https://django-mptt.readthedocs.io/en/latest/install.html ↩︎
“pip is the package installer for Python” https://pip.pypa.io/en/stable/ ↩︎
https://django-mptt.readthedocs.io/en/latest/tutorial.html#add-mptt-to-installed-apps ↩︎
“Loads a template and renders it with the current context” https://docs.djangoproject.com/en/3.1/ref/templates/builtins/#std:templatetag-include ↩︎
- August 1, 2023
- How to create a reusable Django app and distribute it with PIP or publish to pypi.orgJune 29, 2021
- How To Serve Multiple Django Applications with uWSGI and Nginx in Ubuntu 20.04October 26, 2020
- How to add favicon to Django in 4 stepsSeptember 3, 2020
- Categories in Django with Breadcrumbs
- How To Migrate From SQLite To PostgreSQL In Django In 3 stepsAugust 28, 2020
- Practical guide to internationalize a Django app in 5 steps.August 24, 2020
- Disable new users singup when using Django's allauth packageSeptember 3, 2019
- How to add ads.txt to Django as requested by Google AdsenseAugust 30, 2019
- Have multiple submit buttons for the same Django formJuly 2, 2019
- Better Testing with Page Object Design in DjangoMay 1, 2019
- Generating slugs automatically in Django without packages - Two easy and solid approachesFebruary 14, 2019
- How to set up Django tests to use a free PostgreSQL database in HerokuFebruary 13, 2019
- Dynamically adding forms to a Django FormSet with an add button using jQueryFebruary 6, 2019
- Use of Django's static templatetag in css file to set a background imageFebruary 1, 2019
- Activate Django's manage.py commands completion in Bash in 2 stepsJanuary 29, 2019
- Sending Emails with Django using SendGrid in 3 easy stepsJanuary 9, 2019
- Adding Users to Your Django Project With A Custom User ModelSeptember 21, 2018
- Setting Up A Factory For One To Many Relationships In FactoryboyApril 17, 2018
- Generate UML class diagrams from django modelsMarch 24, 2018
- Set Up Ubuntu To Serve A Django Website Step By StepJuly 3, 2017
- Django Project Directory StructureJuly 16, 2016
- How to Have Different Django Settings for Development and Production, and environment isolationJune 10, 2016
- Django OverviewJune 2, 2016
Django Forms
- Adding a Cancel button in Django class-based views, editing views and formsJuly 15, 2019
- Using Django Model Primary Key in Custom Forms THE RIGHT WAYJuly 13, 2019
- Django formset handling with class based views, custom errors and validationJuly 4, 2019
- How To Use Bootstrap 4 In Django FormsMay 25, 2018
- Understanding Django FormsApril 30, 2018
- How To Create A Form In DjangoJuly 29, 2016
Articles
Subcategories
Except as otherwise noted, the content of this page is licensed under CC BY-NC-ND 4.0 . Terms and Policy.
Powered by SimpleIT Hugo Theme
·