In this tutorial, you will learn how to implement categories and breadcrumb in your Django project. Categories may be related to each other as child-parent, so categories are hierarchical data and to store and access them efficiently mptt i.e modified preorder tree traversal should be used in which the number of database queries made is minimum. There are some referral links provided at the end of the post to understand how hierarchical data are stored efficiently.

The previous post on how to implement categories in Django is well illustrated about how things work, but that approach is not efficient because number of database queries are made, so for small website like a blog, that approach may be acceptable, but for a website which will be having good amount of nested categories, should use mptt that I am going to explain.

Installation

pip3 install django-mptt

 

And add 'mptt' in INSTALLED_APPS in settings.py

We will be implementing categories and breadcrumb for a blog website. So consider following models for my_posts app in Django project.

 

Models.py

from mptt.models import MPTTModel, TreeForeignKey

class Post(models.Model):

    title = models.CharField(max_length=120)
    category = models.ForeignKey('Category', null=True, blank=True)
    content = HTMLField('Content')
    slug = models.SlugField(unique=True)
    timestamp = models.DateTimeField(auto_now=False, auto_now_add=True)
    
    def __str__(self):
        return self.title

    def get_slug_list_for_categories(self):
        try:
            ancestors = self.category.get_ancestors(include_self=True)
        except:
            ancestors = []
        else:
            ancestors = [ i.slug for i in ancestors]
        slugs = []
        for i in range(len(ancestors)):
            slugs.append('/'.join(ancestors[:i+1]))

        return slugs

 

And category model be :

class Category(MPTTModel):
    name = models.CharField(max_length=50, unique=True)
    parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
    slug = models.SlugField()

    class MPTTMeta:
        order_insertion_by = ['name']

    class Meta:
        unique_together = (('parent', 'slug',))
        verbose_name_plural = 'categories'

    def __str__(self):
        return self.name

 

Now run migration commands to create database tables. To show categories in admin panel add following to project_name/my_posts/admin.py

 

Admin.py

from mptt.admin import MPTTModelAdmin

admin.site.register(Post,PostAdmin)
admin.site.register(Category , MPTTModelAdmin) 

 

Now run the following command to collect static files from mptt, provided that you've already defined STATIC_ROOT in settings.py

python3 manage.py collectstatic

 

Doing these steps, you can now add some nested categories by selecting categories > ADD  CATEGORY of my_posts app in Django Admin panel, there is an image below to illustrate how to create a category object.

 

adding category

 

After adding some categories, we have following category tree

 

category tree

 

If you want to change the indentation pixels i.e the space before each category object so that the relation between them can be determined easily, add the following line of code in settings.py

  MPTT_ADMIN_LEVEL_INDENT = 20   //you can replace 20 with some other number
                                 //to change indentation space

 

The category tree showed above is not collapsable and expandable, you can tweak this by changing my_posts/admin.py as

from mptt.admin import DraggableMPTTAdmin

admin.site.register(Post,PostAdmin)
admin.site.register(Category, DraggableMPTTAdmin )

 

category tree with expand collpase button

 

Now go to posts > ADD POST then you will find category drop-down to associate a category to that post, this field is not a required field so you may leave it untouched. 

 

post creation with category

 

 

Urls.py

Next, we have to add URL pattern for the category, so go to urls.py and add the following line to the urlpatterns list.

url(r'^category/(?P<hierarchy>.+)/$', views.show_category, name='category'),

 

Views.py

Now in views.py add the following function.

def show_category(request,hierarchy= None):
    category_slug = hierarchy.split('/')
    parent = None
    root = Category.objects.all()

    for slug in category_slug[:-1]:
        parent = root.get(parent=parent, slug = slug)

    try:
        instance = Category.objects.get(parent=parent,slug=category_slug[-1])
    except:
        instance = get_object_or_404(Post, slug = category_slug[-1])
        return render(request, "postDetail.html", {'instance':instance})
    else:
        return render(request, 'categories.html', {'instance':instance})

 

Templates/categories.html

Add following code in categories.html.

{% extends 'base.html' %}
{% load static  %}

{% block head_title %} {{ instance.name }} {% endblock %}

{% block content %}
<br>
<div class="text-center"><h2>{{instance.name}}</h2></div>

{% if  instance.children.all %}
    <h4>Sub Categories</h4>
    {% for i in instance.children.all %}
        <a href="{{ i.slug }}"> {{ i.name }} </a><br>
    {% endfor %}

    <br><hr>
{% endif %}

{% if  instance.post_set.all %}
 {% for i in instance.post_set.all %}
  <h4>Posts</h4>
  <div class="row small-up-1 medium-up-3" >

   <div class="column">
    <a href="{{ i.slug }}">
      <div class="card" style="width: 300px; border-color: black">
         <div class="card-divider">
           <strong>{{ i.title | truncatechars:30}}</strong>
         </div>

         <div class="card-section">
          <small>{{ i.publish}} </small>
          <p>{{ i.content | safe | truncatechars_html:120 }}</p>
         </div>
      </div>
    </a>
   </div>
{% endfor %}

{% endif %}
</div>
{% endblock %}

 

The CSS framework used is Zurb's Foundation 6 you may use bootstrap as well.

 

Next,  add following code snippet inside the postDetail.html template to implement breadcrumbs in post detail page ( you should add it above post's title ).

{% load namify %}

<ul class="breadcrumbs">
    <li><a href="/">Home</a></li>
     
    {% for i in instance.get_slug_list_for_categories %}
       <li><a href="/category/{{ i }}">{{ i | get_name }}</a></li>
    {% endfor %}

</ul> 

 

Note that filter get_name is used to extract the name of categories from the slug.

To implement this filter create a directory templatetags at the same level as models.pyviews.py, etc in my_post app and create a empty python file __init__.py to ensure the directory is treated as a Python package and create another python file namify.py, add following code in it.

 

from django import template

register = template.Library()

@register.filter
def get_name(value):
    spam = value.split('/')[-1]         # assume value be /python/web-scrapping
                                        # spam would be 'web-scrapping'
    spam = ' '.join(spam.split('-'))    # now spam would be 'web scrapping'
    return spam

 

Finally, we have following post detail page for recently created post

 

post detail page with breadcrumb

 

The breadcrumb above the title of the post is useful in navigation, for example, if you click web scrapping then it will take you to the catogories.html page where you can navigate posts in web scrapping as well as its sub categories as in the following image.

category page

 

FINAL WORDS

Modified Preorder Tree Traversal is not just limited to categories but can also be used for efficiently accessing and storing hierarchical data. Although insert and move operations are not as efficient if the tree is changing frequently. However, overall mptt is currently the best solution for database queries overhead. If you have any query, ask me in comments below and do share the post if you like it.

 

RELATED PACKAGES 

 

USEFUL LINKS

https://github.com/django-mptt/django-mptt

https://www.caktusgroup.com/blog/2016/01/04/modified-preorder-tree-traversal-django/

https://www.sitepoint.com/hierarchical-data-database/

http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/

Happy Coding :)