Sorting Hierarchical Categories in Django
I had almost given up on my dream of hierarchical (multi-level) categories sorted by their self-referential parent to form a tree. Almost, but I finally figured it out.
I’m not sure if it’s a hack or the “one obvious way”, as Tim Peters puts it (probably not), but it works and I’m using it.
The whole problem starts with a simple model:
class Category(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey('self', blank=True,
null=True, related_name='child')
def __unicode__(self):
if self.parent:
prefix = str(self.parent)
else:
return self.name
return ' > '.join((prefix,self.name))
It works great for categories with sub categories. And when you call the __unicode__() method, it even spits out the whole ancestry to the category (eg. u'GrandParent > Parent > Category').
But alas, as soon as you try to sort them by their parents, you find out that the full name is not a real field and you can’t order by fake fields. Of course you might get the bright idea to add the order_with_respect_to = 'parent' Meta class attribute, but that will only lead you here.
One way (the only way?) to get around this is to add a sort field to the model and have that somehow get automagically updated every time the table changes. And here it is:
class Category(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey('self', blank=True,
null=True, related_name='child')
sort = models.IntegerField(default=0)
def __unicode__(self):
if self.parent:
prefix = str(self.parent)
else:
return self.name
return ' > '.join((prefix,self.name))
@classmethod
def re_sort(cls):
cats = sorted([ (x.__unicode__(), x) for x in cls.objects.all() ])
for i in range(len(cats)):
full_name, cat = cats[i]
cat.sort = i
super(Category, cat).save() # Call the "real" save()
def save(self, *args, **kwargs):
super(Category, self).save(*args, **kwargs) # Call the "real" save()
self.re_sort()
First we need something that can re-sort our categories by full name and assign a sort number to them. The re_sort classmethod hadles this by grabbing all the categories, sorting them by their full name and writing their index in the sorted list to the sort field. It needs the @classmethod decorator so we don’t get:
AttributeError: Manager isn't accessible via Category instances
Now that we have a class method that can handle updating the sort fields, we just need to hook it into our save method. Since we’re making calls to the super’s save method in both the save() and re_sort() methods, we don’t get a maximum recursion error.
Anyways, I hope this helps someone out. If you have any suggestions or questions, hit up the comments section.
Thank you man!
Thanks for your article. I am new at development and this will be a big help.
Was helpful to me! Thanks!