Categories are essential for any website because it is easy for users to access content sorted by categories. Categories may have their subcategories, and subcategories may also have subcategories and so on. So in this post, I'll explain how to implement nested categories in a Django project. You can create categories with Django Admin Panel and then associate it with content like an article or post, So let's get started.

Let's implement categories for blog posts, consider my_posts app in the project, add following models in it

Models.py

class Category(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField()
    parent = models.ForeignKey('self',blank=True, null=True ,related_name='children')

    class Meta:
        unique_together = ('slug', 'parent',)    #enforcing that there can not be two
        verbose_name_plural = "categories"       #categories under a parent with same 
                                                 #slug 

    def __str__(self):                           # __str__ method elaborated later in
        full_path = [self.name]                  # post.  use __unicode__ in place of
                                                 # __str__ if you are using python 2
        k = self.parent                          

        while k is not None:
            full_path.append(k.name)
            k = k.parent

        return ' -> '.join(full_path[::-1])

 

Next, we'll use category in Post model as a foreign key.

class Post(models.Model):
    user =  models.ForeignKey(settings.AUTH_USER_MODEL,default=1)
    title = models.CharField(max_length=120)
    category = models.ForeignKey('Category', null=True, blank=True)
    content = HTMLField('Content')
    draft = models.BooleanField(default=False)
    publish = models.DateField(auto_now=False,auto_now_add=False,)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.title


    def get_cat_list(self):           #for now ignore this instance method,
        k = self.category
        breadcrumb = ["dummy"]
        while k is not None:
            breadcrumb.append(k.slug)
            k = k.parent

        for i in range(len(breadcrumb)-1):
            breadcrumb[i] = '/'.join(breadcrumb[-1:i-1:-1])
        return breadcrumb[-1:0:-1]

 

So with Category being a foreign key in Post model, a category can be associated with post.

Open the terminal and change the current working directory to one that contains manage.py then run following migration commands.  

python3 manage.py makemigrations
python3 manage.py migrate

 

Now we make some Category object from the interactive shell so open Django interactive shell by typing following command in terminal.

python3 manage.py shell

Then in shell, you can create categories as illustrated below

>>> from my_posts.models import *
>>> fruits = Category.objects.create(name='fruits', slug='fruits')
>>> fruits
<Category: fruits>
>>> beverages = Category.objects.create( name='bevarages', slug='beverages' )
>>>
>>> berry = Category.objects.create( name = 'berry', slug = 'berry', parent = fruits )
>>> citrus_fruits = Category.objects.create(name='citrus fruits', slug='citrus-fruits', parent=fruits)
>>>
>>> hot_bev = Category.objects.create( name = 'hot', slug = 'hot' , parent = beverages )
>>> cold_bev = Category.objects.create( name = 'cold', slug = 'cold', parent = beverages )
>>>
>>> Category.objects.all()
<QuerySet [<Category: fruits -> berry>, <Category: bevarages>, <Category: fruits -> citrus fruits>, 
<Category: bevarages -> cold>, <Category: fruits>, <Category: bevarages -> hot>]>
>>>
>>> Category.objects.get(name='fruits')
<Category: fruits>
>>> f = Category.objects.get(name='fruits', parent=None)
>>> f
<Category: fruits>

>>> f.children.all()
<QuerySet [<Category: fruits -> berry>, <Category: fruits -> citrus fruits>]>
>>> b =  f.children.get( name='berry', parent=f)
>>> b
<Category: fruits -> berry>

>>> Category.objects.all().delete()            #to delete all the Category objects
(6, {'my_posts.Category': 6})

 

 

Now we'll add categories with Django Admin Panel, so add following code to admin.py

Admin.py

from .models import Category

admin.site.register(Category)

 

then run the server by 

python3 manage.py runserver

 

Now go to admin panel from your browser, you'll see category there.

category in admin panel

 

 

Now we add some main categories, by main categories I mean the categories having a null parent. Go to categories and click "Add Category" and add the category like in the image below.

 

 

adding main category

 

 

Now to illustrate the nested categories add one more main category say "python", now add a category "news" as a subcategory of "python", you have to select "python" as the parent category of "news", like in the image below.

 

 

adding subcategory

 

 

Now for sake of well understanding add one more category "2017" having  "news"  as parent category like in the image below.

 

subcategory of subcategory

 

 

Now the categories we've created are showed as follow.

 

categories

 

 

You may be wondering how '-- >' are appended after categories ( which is useful in distinguishing between same-named subcategories of a different parent at any level ), this is since we have defined the __str__ method in Category model.

Let's add a post to illustrate how to associate a category with it, so go to the home in admin panel then go to post and click 'add post' button.

adding post

 

 

Urls.py

As you can see in the image, we have added the post in python > news > 2017. Next, we have to add URL pattern for the category, so go to urls.py and add the following line in 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('/')
    category_queryset = list(Category.objects.all())
    all_slugs = [ x.slug for x in category_queryset ]
    parent = None
    for slug in category_slug:
        if slug in all_slugs:
            parent = get_object_or_404(Category,slug=slug,parent=parent)
        else:
            instance = get_object_or_404(Post, slug=slug)
            breadcrumbs_link = instance.get_cat_list()
            category_name = [' '.join(i.split('/')[-1].split('-')) for i in breadcrumbs_link]
            breadcrumbs = zip(breadcrumbs_link, category_name)
            return render(request, "postDetail.html", {'instance':instance,'breadcrumbs':breadcrumbs})

    return render(request,"categories.html",{'post_set':parent.post_set.all(),'sub_categories':parent.children.all()})

 

Note that the last element of the category_slug list in show_category could either be a Post object or a category object, 

For example the parameter hierarchy may be '/python/news' or it may be  'python/news/2017/instagram-makes-a-move-to-python-3/'  in former case the last element of category_slug would be a category object but in later case it is a Post object,  so in for loop tracing categories with hierarchical relation so if the last element be a Post object then it will be rendered with "postDetail.html" template, otherwise if it be a Category object then it will be rendered with "categories.html" template.

In context dictionary, 'breadcrumbs' are there because you may want to have the breadcrumb in your post detail page.

 

Templates/categories.html

Add following code in "categories.html".

{% extends 'base.html' %}
{% load static  %}
{% block content %}
<br>
{% if sub_categories %}
    <h3>Sub Categories</h3>
    {% for i in sub_categories %}
        <a href="{{ i.slug }}"> {{ i.name }} </a>
    {% endfor %}
{% endif %}

<div class="row small-up-1 medium-up-3" >
{% if post_set %}
{% for i in post_set %}
    <div class="columns">
        <div class=" card-article-hover card">
          <a href="{{ i.slug }}">
            <img  src="{{ i.cover_photo.url }}">
          </a>
          <div class="card-section">
            <a href="{{ i.slug }}">
              <h6 class="article-title">{{ i.title | truncatechars:30}}</h6>
            </a>
          </div>
          <div class="card-divider flex-container align-middle">
            <a href="" class="author">{{ i.user.get_full_name }}</a>
          </div>
          <div class="hover-border">
          </div>
        </div>
    </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 ).

<ul class="breadcrumbs">
  <li><a href="/">Home</a></li>
      {% for slug,name in breadcrumbs %}
          <li><a href="/category/{{ slug }}">{{ name }}</a></li>
      {% endfor %}
</ul>

 

So breadcrumbs in post detail page would look like in the image below.

 

illustrating breadcrumbs in post detail page

 

 

And our Category pages would look like images below

 

 

 

Note that we have only a subcategory in Python/news and no post so only subcategory there. But in '2017' there is no subcategory and a simple post which showed in the image below.

 

 

 

You may be wondering about the cover_image attribute used in template code and hence reflected in card view of the post in above photo, the cover_photo is a field in Post model and for brevity, I did not include the cover_photo attribute in code. If you have any queries regarding it then comment below.

 

CONCLUSION

Although this approach is not most efficient because we are making many database queries, but for small websites and blogs this approach is good to go. If you are working on some projects like a news website or e-commerce website which contain many level of nested categories then you may want to find a comparatively efficient way to implement category with Modified Preorder Tree Traversal (MPTT), there is Django package available for it, read a post on how to implement categories with django-mptt here

If you have any query let me know in the comments below.

Happy coding :)