Filtering by subcategories

Hi everyone who reading this :slight_smile: My english is not so good but I will try my best to explain my problem.
Ok, let’s start. I have two models - Category and Service. Category has field «parent_category» with ForeignKey ‘self’ to implement subcategories functionality. I can print my main categories and filter services by them. Also when I click on main categories, its subcategories prints fine. But when I click on subcategories, services are not filtered by them. How can I implement filtering by clicking on subcategories? Honestly say this is just my 2nd project on Django. So don’t judge strickly, please. I’m writing here in not my native language because I’m already starting to despair :frowning: This forum is my last hope.

models.py

from django.db import models
from django.urls import reverse

class Category(models.Model):
    name = models.CharField(max_length=255, db_index=True)
    slug = models.SlugField(max_length=255, db_index=True, unique=True)
    parent_category = models.ForeignKey('self', related_name='children', on_delete=models.CASCADE, null=True, blank=True)

    class Meta:
        ordering = (['name',])
        verbose_name = 'Категория'
        verbose_name_plural = 'Категории'

    def __str__(self):
        return self.name
    
    def get_absolute_url(self):
        if self.parent_category:
            return reverse('main:service_list_by_category', args=[self.parent_category.slug, self.slug])
        return reverse('main:service_list_by_category', args=[self.slug])


class Service(models.Model):
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='services')
    name = models.CharField(max_length=255, db_index=True)
    slug = models.SlugField(max_length=255, db_index=True)
    description = models.TextField(blank=True)
    preparation = models.TextField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=0)
    available = models.BooleanField(default=True)

    class Meta:
        ordering = (['name',])
        index_together = (('id', 'slug'))
        verbose_name = 'Услуга'
        verbose_name_plural = 'Услуги'

    def __str__(self):
        return self.name
views.py

from django.shortcuts import render
from .models import Category, Service

def service_list(request, category_slug=None):
    services = Service.objects.filter(available=True)
    main_categories = Category.objects.filter(parent_category=None)
    sub_category = None
    
    if category_slug:
        sub_category = Category.objects.get(slug=category_slug)

        if sub_category.children.exists():
            categories = [sub_category] + list(sub_category.children.all())
            services = services.filter(category__in=categories)
        else: 
            services = services.filter(category=sub_category)

    return render(request, 
                  'main/list.html',
                  {'sub_category': sub_category,
                   'main_categories': main_categories,
                   'services': services})
list.html

{% extends 'layout/base.html' %}
{% block content %}
<section>
    <div class="container">
        <div class="container">
            {% for category in main_categories %}
                <a href="{{ category.get_absolute_url }}">{{ category.name }}</a>
            {% endfor %}
        </div>
        <div class="container">
            {% for subcategory in sub_category.children.all %}
                <a href="{{ category.get_absolute_url }}">{{ subcategory.name }}</a>
            {% endfor %}
        </div>
    </div>
    <div class="container" style="margin-top: 100px">
        {% for service in services %}
            <h6>{{ service.name }}</h6>
            <p>{{ service.description }}</p>
            <p>{{ service.price }}</p>
            <small>{{ parent_category }}/{{ sub_category }}</small>
        {% endfor %}
    </div>
</section>
{% endblock %}
urls.py

from django.urls import path
from . import views

app_name = 'main'

urlpatterns = [
    path('', views.service_list, name='service_list'), 
    path('<category_slug>/', views.service_list, name='service_list_by_category'),
]

Seems the problem is: there is no check if a service belongs to a subcategory.
I can successfully check if a service belongs to a category (main category). But can’t to do the same for a subcategory. I know that I have to do that in views.py file but I have no idea yet how.

Hi, I think your question is well written and explains the situation fairly well. The first step is confirming that the following code is actually run:

Can you use a debugger or add a print statement to confirm this runs when you want it to?

If it does run and you’re seeing the filtering work as you expect it to then we need more information. Do any services get printed? Do too many get printed or not enough? Have you explored your data in a manage.py shell / REPL?

Thank you for your response. I will try the first step later and will get back to you with the result. I have to go now. Have a good day.

Hello again Tim,

  1. I tried to print services (print(services)) after the code you mentioned above and everything works fine.

  2. To answer your other question - after (print(services)), I got all services of a main category and names of its subcategories.

  3. No, I have not explored my data in a shell. But every time I click on a subcategory, I keep getting a response:

[22/Sep/2023 15:44:32] “GET /lechenie/ HTTP/1.1” 200 7705,

*although /lechenie/ is the main category :thinking:

Please let me know further steps. I am very grateful for your help in resolving this issue. You do really a lot for me.

Should the URL be for the subcategory not the category?

If I change


{{ category.get_absolute_url }}

on

{{ subcategory.get_absolute_url }}

I get the error:

# NoReverseMatch at /lechenie/

Reverse for 'service_list_by_category' with arguments...

If categories and subcategories will work correctly, I will get URL somethins like that: /name_of_main_category/name_of_subcategory/

That error is valid because the get_absolute_url for the case of having a parent category has a bug in it. These two reverse calls point to the same url, but they have different arguments. Django’s doesn’t handle dynamic URL params super well. So you’re better off creating a new URL called service_list_by_subcategory that takes both category and subcategory as arguments. Does that make sense?

Tim, I edited my code this way:

urlpatterns = [
    path('', views.service_list, name='service_list'), 
    path('<category_slug>/', views.service_list, name='service_list_by_category'),
    path('<category_slug>/<sub_category_slug>', views.service_list, name='service_list_by_subcategory'),
]
def get_absolute_url(self):
        if self.parent_category:
            return reverse('main:service_list_by_subcategory', args=[self.parent_category.slug, self.sub_category.slug])
        return reverse('main:service_list_by_category', args=[self.slug])

But everything works as before :frowning: I still can not filter services by subcategories.

What’s your view now?

Views.py is the same. I changed nothing’s there.

That doesn’t sound right.

If you have:

path('<category_slug>/<sub_category_slug>', views.service_list, name='service_list_by_subcategory')

And service_list doesn’t raise an exception, then it must have been updated to accept the sub_category_slug argument which didn’t previously exist. You need to update service_list to look up the category differently when it gets the sub_category_slug.

I can add the sub_category argument, but I don’t understand how I have to else edit view to look up the category as you said.

If you have the slug for a subcategory how would you fetch that instance from the database?

When you have that code, you need to use it in your view when you have a sub_category_slug value.

What I did:

I added sub_category_slug=None as argument of service_list.

def service_list(request, category_slug=None, sub_category_slug=None):

And then I added the following rows of code after first if block:

if sub_category_slug:
        sub_category = Category.objects.get(slug=sub_category_slug)
        services = services.filter(sub_category__in=categories)

But everything works as before. I really don’t understand what I have to do.

You’re looking for sub_category_services = services.filter(category=sub_category) which will return the services associated with this subcategory.

Tim, I added sub_category_services but still no result. My view looks this way now:

def service_list(request, category_slug=None, sub_category_slug=None):
    services = Service.objects.filter(available=True)
    main_categories = Category.objects.filter(parent_category=None)
    sub_category = None
    sub_category_services = None
    
    if category_slug:
        sub_category = Category.objects.get(slug=category_slug)
        
        if sub_category.children.exists():
            categories = [sub_category] + list(sub_category.children.all())
            services = services.filter(category__in=categories)
        else: 
            services = services.filter(category=sub_category)
    
    if sub_category_slug:
        sub_category = Category.objects.get(slug=sub_category_slug)
        sub_category_services = services.filter(category=sub_category)

    context = {'sub_category': sub_category, 
               'main_categories': main_categories, 
               'services': services,
               'sub_category_services': sub_category_services}

    return render(request, 'main/list.html', context)

Also I noticed one thing:

if category_slug:
        sub_category = Category.objects.get(slug=category_slug)
  • here sub_category prints main categories. I checked this.

Now I’m really got confused about everything and already thinking about other solutions (django-mttp maybe). Honestly say I thought everything will be more simple. I spent already a week to solve this issue. I feel myself like a loser.

Alright give your self a bit of a break here. You’re encountering a challenging topic and trying to get help from someone using a non-native language. That’s pretty challenging.

You need to remember what this view is all responsible for. It’s fetching the services for a specific category or subcategory.

While you need to fetch the services for a category differently than how you fetch services for a subcategory, what do you do with those services may still be the same.

Meaning your template context should probably have the following keys:

  • main_categories for listing the top level categories
  • subcategories for when your view is for a top level category meaning it doesn’t have a sub_category_slug specified.
  • parent_category for either when your view is for a top level category or the subcategory’s parent when your subcategory is specified.
  • sub_category which is only set when the view has a value for sub_category_slug
  • services a collection of services that are related to a parent category or services that are related to a subcategory.

If my assumptions are correct about what you need to fetch and use in the template, it should be easier for you to write the view.

You may even want to start with separate views, urls and templates for the top level category flow vs subcategory flow. After you have both working as you want, you can look for the similarities and refactor them back into one view and/or template.

You may also benefit from exploring your data model using the shell / REPL via manage.py shell

Thank you for your detailed answer. I will try to follow these steps and rewrite views in the next few days. Enjoy your day.