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 can you implement nested categories in your Django project. You can dynamically create categories whenever you wanted with Django Admin Panel, So let's get started.

Let's implement categories for a blog website, consider following in models.py of the my_posts app in your Django project.

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 with same slug of a parent
                                                 #category

    def __str__(self):                           # __str__ method elaborate later in post
        full_path = [self.name]                  #use __unicode__ instead of __str__ in
        k = self.parent                          #if you using python2

        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, every post will have a category associated with it ( it may be null, it is not a required field ).

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 will do the same thing with Django admin, make sure your terminal have the current working directory that contains manage.py then run following migration commands.

python3 manage.py makemigrations
python3 manage.py migrate

 

Admin.py

Now in admin.py of your Django app add following lines,

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 find category there.

category in admin panel

 

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

adding main category

 

Note that parent is kept null here I mean "linux" is added as main category, now to illustrate the nested categories add one more main category say "python", now add a category "news" as subcategory of "python", you have to select "python" as parent category of "news", like in 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 for the category in a way to do it.

Now 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 sake of understanding category only 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 website and blogs this approach is good to go. If you are working on some big project like a news website 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 :)