I have a model “Category” with a ForeignKey to “parent_category”.
How can I order this model in the Django admin list view like:
- category 1
-- subcategory 1 of category 1
--- subsubcategory 1 of subcategory 1 of category 1
-- subcategory 2 of category 1
-- subcategory 3 of category 1
- category 2
-- subcategory 1 of category 2
-- subcategory 2 of category 2
I have tried the following but this won’t work. So I need some help to order the function ‘get_relative_name’.
class PrivateContentCategory(models.Model):
name = models.CharField(
max_length=250,
verbose_name=_('Naam'),
)
slug = models.SlugField(
verbose_name=_('Url'),
blank=True,
)
parent_category = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
related_name='child_category_list',
verbose_name=_('Hoofdcategorie'),
blank=True,
null=True,
)
def __str__(self):
str = self.name
parent_category_obj = self.parent_category
while parent_category_obj is not None:
str = parent_category_obj.name + ' --> ' + str
parent_category_obj = parent_category_obj.parent_category
return str
def get_relative_name(self):
str = self.name
parent_category_obj = self.parent_category
while parent_category_obj is not None:
str = '--' + str
parent_category_obj = parent_category_obj.parent_category
get_relative_name.short_description = _('Naam')
get_relative_name.admin_order_field = [
'parent_category__parent_category',
'name',
]
EDIT!!!
The names of the parent category should not be displayed with the category. I had written it like this to display how the model should be ordered. The display of the list will just be:
- OS
-- Windows
--- Windows 7
--- Windows 8
--- Windows 10
-- Mac
-- Linux
--- Debian
---- Ubuntu
--- Fedora
---- CentOS
---- Oracle Linux
3
Answers
What worked for me was to add a new field "absolute_name" to the model which will be auto populated with a pre_save signal. After an instance is saved, this field will conain the names for all parent_categories of the instance before it own name. At last, I just needed to order the instance on this field:
In order to be able to order by it, you need to annotate the queryset in the modeladmin, so a method on the model won’t help.
admin.py
This will add a column to the admin, and allow you to click-sort it.
One thing this will not allow you to do it to have a default sort on that column. This will fail:
This is a long-standing bug in Django: https://code.djangoproject.com/ticket/17522
There are ways around it, but I am getting off topic…
So the second problem is, obviously, we need to construct the relative names there, instead of that
F('name')
. I could be wrong, but I think the only DB engine that supports this on-the-fly is Postgres. If you are using a different DB engine, then I guess you are left with having to denormalize your data a bit and have a column with full parent name on every child.There could be better ways to do this, but here is how I have done it:
admin.py
P.S.
Oracle seems to support it as well, though with a different syntax: SQL recursive query on self referencing table (Oracle)
P.P.S.
If you end up having to keep parent name on the model, then the annotate looks something like this:
P.P.P.S.
It is possible to add two annotations, one for displaying values and another for sorting. Actually, looking at your question again, I think this will be needed because your example has
subcat -- cat
and notcat -- subcat
as I assumed above. For this we need two annotations, one of which will be returned fromrelative_name
modeladmin method and the other will be forrelative_name.admin_order_field
.A much cleaner, more efficient solution is to use django-mptt:
If you want to generate a
<select>
dropdown in forms using this model:This also works in the admin: