如何在 Django 中按父类别订购模型?
How do I order a model by parent category in django?
我有一个模型 "Category",其外键为 "parent_category"。
我如何在 Django 管理列表视图中订购此模型,如:
- 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
我尝试了以下方法,但这行不通。所以我需要一些帮助来订购函数 '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',
]
编辑!!!
父类别的名称不应与类别一起显示。我这样写是为了显示模型应该如何排序。列表的显示将只是:
- OS
-- Windows
--- Windows 7
--- Windows 8
--- Windows 10
-- Mac
-- Linux
--- Debian
---- Ubuntu
--- Fedora
---- CentOS
---- Oracle Linux
为了能够通过它进行排序,您需要在 modeladmin 中注释查询集,因此模型上的方法无济于事。
admin.py
from django.db.models.expressions import F
...
@admin.register(PrivateContentCategory)
class PrivateContentCategoryAdmin(admin.ModelAdmin):
list_display = (
'name',
'relative_name',
)
def get_queryset(self, request):
qs = super().get_queryset(request) # type: QuerySet
qs = qs.annotate(relative_name=F('name')) # for now :)
return qs
def relative_name(self, obj: PrivateContentCategory):
return obj.relative_name
relative_name.admin_order_field = 'relative_name'
这将为管理员添加一列,并允许您click-sort它。
有一件事不允许您对该列进行默认排序。这将失败:
class PrivateContentCategoryAdmin(admin.ModelAdmin):
...
ordering = ('relative_name',)
ERRORS:
<class 'cats.admin.PrivateContentCategoryAdmin'>: (admin.E033) The value of 'ordering[0]' refers to 'relative_name', which is not an attribute of 'cats.PrivateContentCategory'.
这是 Django 中的一个 long-standing 错误:https://code.djangoproject.com/ticket/17522
有很多解决方法,但我离题了...
所以第二个问题是,显然,我们需要在那里构造相对名称,而不是 F('name')
。我可能是错的,但我认为唯一支持此 on-the-fly 的数据库引擎是 Postgres。如果您使用的是不同的数据库引擎,那么我想您将不得不对数据进行一些非规范化处理,并在每个子项上都有一个包含完整父名称的列。
可能有更好的方法来做到这一点,但我是这样做的:
admin.py
...
from django.db.models.expressions import RawSQL
relative_name_query = '''
WITH RECURSIVE "relative_names" as (
SELECT "id", "parent_category_id", CAST("name" AS TEXT)
FROM "{table}"
WHERE "parent_category_id" IS NULL
UNION ALL
SELECT "t"."id", "t"."parent_category_id", CONCAT_WS('/', "r"."name", "t"."name")
FROM "{table}" "t"
JOIN "relative_names" "r" ON "t"."parent_category_id" = "r"."id"
)
SELECT "name"
FROM "relative_names" WHERE "relative_names"."id" = "{table}"."id"
'''
@admin.register(PrivateContentCategory)
class PrivateContentCategoryAdmin(admin.ModelAdmin):
...
# instead of that F('name') line:
qs = qs.annotate(relative_name=RawSQL(
relative_name_query.format(
table=qs.model._meta.db_table,
),
(),
))
P.S.
Oracle 似乎也支持它,但语法不同:SQL recursive query on self referencing table (Oracle)
P.P.S.
如果您最终不得不在模型上保留父名称,则注释看起来像这样:
qs = qs.annotate(relative_name=Concat(F('parent_name'), Value('/'), F('name')))
P.P.P.S.
可以添加两个注释,一个用于显示值,另一个用于排序。实际上,再次查看您的问题,我认为这将是必需的,因为您的示例具有 subcat -- cat
而不是我上面假设的 cat -- subcat
。为此,我们需要两个注释,其中一个将从 relative_name
modeladmin 方法返回,另一个用于 relative_name.admin_order_field
.
对我有用的是向模型添加一个新字段 "absolute_name",该字段将自动填充 pre_save 信号。保存实例后,该字段将包含实例所有 parent_categories 的名称,然后在其自身名称之前。最后,我只需要在这个字段上订购实例:
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,
)
absolute_name = models.TextField(
verbose_name=_('Absolute naam'),
blank=True,
)
def __str__(self):
return self.absolute_name
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
return str
get_relative_name.short_description = _('Naam')
get_relative_name.admin_order_field = [
'absolute_name',
]
class Meta:
verbose_name = _('Privé inhoud categorie')
verbose_name_plural = _('Privé inhoud categorieën')
ordering = [
'absolute_name',
]
@receiver(models.signals.pre_save, sender=PrivateContentCategory)
def pre_save_private_content_category_obj(sender, instance, **kwargs):
# START Generate instance.absolute_name
instance.absolute_name = instance.name
parent_category_obj = instance.parent_category
while parent_category_obj is not None:
instance.absolute_name = parent_category_obj.name + ' --> ' + instance.absolute_name
parent_category_obj = parent_category_obj.parent_category
# END Generate instance.absolute_name
更简洁、更高效的解决方案是使用 django-mptt:
from mptt.models import MPTTModel
from mptt.fields import TreeForeignKey
class PrivateContentCategory(MPTTModel):
name = models.CharField(max_length=250)
slug = models.SlugField(blank=True)
parent_category = TreeForeignKey(
'self',
on_delete=models.SET_NULL,
related_name='child_category_list',
blank=True,
null=True,
)
class MPTTMeta:
order_insertion_by = ['name']
如果您想使用此模型在表单中生成 <select>
下拉菜单:
from mptt.forms import TreeNodeMultipleChoiceField
class SomeForm(forms.Form):
category = TreeNodeMultipleChoiceField(
queryset = PrivateContentCategory.objects.all()
)
这在管理员中也有效:
from mptt.admin import MPTTModelAdmin
class PrivateContentCategoryAdmin(MPTTModelAdmin):
mptt_level_indent = 20
admin.site.register(PrivateContentCategory, PrivateContentCategoryAdmin)
我有一个模型 "Category",其外键为 "parent_category"。 我如何在 Django 管理列表视图中订购此模型,如:
- 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
我尝试了以下方法,但这行不通。所以我需要一些帮助来订购函数 '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',
]
编辑!!! 父类别的名称不应与类别一起显示。我这样写是为了显示模型应该如何排序。列表的显示将只是:
- OS
-- Windows
--- Windows 7
--- Windows 8
--- Windows 10
-- Mac
-- Linux
--- Debian
---- Ubuntu
--- Fedora
---- CentOS
---- Oracle Linux
为了能够通过它进行排序,您需要在 modeladmin 中注释查询集,因此模型上的方法无济于事。
admin.py
from django.db.models.expressions import F
...
@admin.register(PrivateContentCategory)
class PrivateContentCategoryAdmin(admin.ModelAdmin):
list_display = (
'name',
'relative_name',
)
def get_queryset(self, request):
qs = super().get_queryset(request) # type: QuerySet
qs = qs.annotate(relative_name=F('name')) # for now :)
return qs
def relative_name(self, obj: PrivateContentCategory):
return obj.relative_name
relative_name.admin_order_field = 'relative_name'
这将为管理员添加一列,并允许您click-sort它。
有一件事不允许您对该列进行默认排序。这将失败:
class PrivateContentCategoryAdmin(admin.ModelAdmin):
...
ordering = ('relative_name',)
ERRORS:
<class 'cats.admin.PrivateContentCategoryAdmin'>: (admin.E033) The value of 'ordering[0]' refers to 'relative_name', which is not an attribute of 'cats.PrivateContentCategory'.
这是 Django 中的一个 long-standing 错误:https://code.djangoproject.com/ticket/17522
有很多解决方法,但我离题了...
所以第二个问题是,显然,我们需要在那里构造相对名称,而不是 F('name')
。我可能是错的,但我认为唯一支持此 on-the-fly 的数据库引擎是 Postgres。如果您使用的是不同的数据库引擎,那么我想您将不得不对数据进行一些非规范化处理,并在每个子项上都有一个包含完整父名称的列。
可能有更好的方法来做到这一点,但我是这样做的:
admin.py
...
from django.db.models.expressions import RawSQL
relative_name_query = '''
WITH RECURSIVE "relative_names" as (
SELECT "id", "parent_category_id", CAST("name" AS TEXT)
FROM "{table}"
WHERE "parent_category_id" IS NULL
UNION ALL
SELECT "t"."id", "t"."parent_category_id", CONCAT_WS('/', "r"."name", "t"."name")
FROM "{table}" "t"
JOIN "relative_names" "r" ON "t"."parent_category_id" = "r"."id"
)
SELECT "name"
FROM "relative_names" WHERE "relative_names"."id" = "{table}"."id"
'''
@admin.register(PrivateContentCategory)
class PrivateContentCategoryAdmin(admin.ModelAdmin):
...
# instead of that F('name') line:
qs = qs.annotate(relative_name=RawSQL(
relative_name_query.format(
table=qs.model._meta.db_table,
),
(),
))
P.S.
Oracle 似乎也支持它,但语法不同:SQL recursive query on self referencing table (Oracle)
P.P.S.
如果您最终不得不在模型上保留父名称,则注释看起来像这样:
qs = qs.annotate(relative_name=Concat(F('parent_name'), Value('/'), F('name')))
P.P.P.S.
可以添加两个注释,一个用于显示值,另一个用于排序。实际上,再次查看您的问题,我认为这将是必需的,因为您的示例具有 subcat -- cat
而不是我上面假设的 cat -- subcat
。为此,我们需要两个注释,其中一个将从 relative_name
modeladmin 方法返回,另一个用于 relative_name.admin_order_field
.
对我有用的是向模型添加一个新字段 "absolute_name",该字段将自动填充 pre_save 信号。保存实例后,该字段将包含实例所有 parent_categories 的名称,然后在其自身名称之前。最后,我只需要在这个字段上订购实例:
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,
)
absolute_name = models.TextField(
verbose_name=_('Absolute naam'),
blank=True,
)
def __str__(self):
return self.absolute_name
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
return str
get_relative_name.short_description = _('Naam')
get_relative_name.admin_order_field = [
'absolute_name',
]
class Meta:
verbose_name = _('Privé inhoud categorie')
verbose_name_plural = _('Privé inhoud categorieën')
ordering = [
'absolute_name',
]
@receiver(models.signals.pre_save, sender=PrivateContentCategory)
def pre_save_private_content_category_obj(sender, instance, **kwargs):
# START Generate instance.absolute_name
instance.absolute_name = instance.name
parent_category_obj = instance.parent_category
while parent_category_obj is not None:
instance.absolute_name = parent_category_obj.name + ' --> ' + instance.absolute_name
parent_category_obj = parent_category_obj.parent_category
# END Generate instance.absolute_name
更简洁、更高效的解决方案是使用 django-mptt:
from mptt.models import MPTTModel
from mptt.fields import TreeForeignKey
class PrivateContentCategory(MPTTModel):
name = models.CharField(max_length=250)
slug = models.SlugField(blank=True)
parent_category = TreeForeignKey(
'self',
on_delete=models.SET_NULL,
related_name='child_category_list',
blank=True,
null=True,
)
class MPTTMeta:
order_insertion_by = ['name']
如果您想使用此模型在表单中生成 <select>
下拉菜单:
from mptt.forms import TreeNodeMultipleChoiceField
class SomeForm(forms.Form):
category = TreeNodeMultipleChoiceField(
queryset = PrivateContentCategory.objects.all()
)
这在管理员中也有效:
from mptt.admin import MPTTModelAdmin
class PrivateContentCategoryAdmin(MPTTModelAdmin):
mptt_level_indent = 20
admin.site.register(PrivateContentCategory, PrivateContentCategoryAdmin)