I think you’re saying that if you have a detail view for a single category (Black jackets) you want to have its URL also include the slugs for its parent categories.
I’ve done it like this in a similar situation…
In my urls.py
:
from django.urls import path, re_path
from .views import CategoryDetailView
urlpatterns = [
# This must be last in the list because it will catch almost everything remaining:
re_path(
r"^shop/(?P<slugs>[\w\/-]+)/$", CategoryDetailView.as_view(), name="category_detail"
),
]
And then my views.py
:
from django.views.generic.detail import DetailView
from .models import Category
class CategoryDetailView(DetailView):
model = Category
slug_url_kwarg = "slugs"
def get_object(self, queryset=None):
"""
Get the Category from the `slugs`, which could be like
"man-clothes/jackets/black-jackets/"
Category.slug is not unique, so we first try to find a Category
using both its own slug ("black-jackets") and its depth (3, here).
But that might find more than one Category, in which case we
must also use its parents to narrow things down.
"""
obj = None
slugs = self.kwargs.get(self.slug_url_kwarg, None)
if slugs is None:
raise AttributeError("'CategoryDetailView' must be called with slugs in the URL")
else:
slugs_arr = slugs.split("/")
slug = slugs_arr[-1] # slug of this category
depth = len(slugs_arr)
categories = Category.objects.filter(slug=slug, depth=depth)
if len(categories) == 1:
obj = categories.first()
elif len(categories) > 1:
# Multiple categories with this slug+depth, so find which
# category has a parent matching the next slug up.
for category in categories:
parent = category.get_parent()
if parent.slug == slugs_arr[-2]:
obj = category
break
if obj is None:
raise Http404("No Categories found matching the query")
else:
return obj
BUT, note that this assumes the Category
model also has a depth
parameter indicating how deep in the “tree” it is. I’m using django-treebeard for my category hierarchy (I recommend it!) and my Category
model is like this:
from treebeard.mp_tree import MP_Node
from django.db import models
class Category(models.Model, MP_Node):
title = models.CharField(max_length=255, blank=False, null=False)
slug = models.SlugField(max_length=50, blank=False, null=False)
However, this leaves out one thing – how do you generate the URL for a Category, including all of its parent Categories’ slugs? The Category
model’s get_absolute_url()
method looks like this, using django-treebeard’s get_ancestors()
method:
def get_absolute_url(self):
# Join all the parent categories' slugs, eg:
# 'man-clothes/jackets/black-jackets'.
parent_slugs = "/".join([c.slug for c in self.get_ancestors()])
# If there are no parent slugs, it's a top-level category.
path = f"{parent_slugs}/{self.slug}" if parent_slugs else self.slug
return reverse("category_detail", kwargs={"slugs": path})