如何在Django中按父类别订购模型?

时间:2019-05-16 14:32:27

标签: python django django-models blogs

我有一个模型“ 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

3 个答案:

答案 0 :(得分:0)

为了能够对其进行排序,您需要在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'

这将为管理员添加一列,并允许您对其进行点击排序。

一件事,这将使您无法对该列进行默认排序。这将失败:

class PrivateContentCategoryAdmin(admin.ModelAdmin):
   ...
   ordering = ('relative_name',)
  

错误:
  :(admin.E033)'ordering [0]'的值涉及'relative_name',而不是'cats.PrivateContentCategory'的属性。

这是Django中一个长期存在的错误:https://code.djangoproject.com/ticket/17522
有很多解决方法,但是我要离开话题了……

因此,显然,第二个问题是我们需要在此处构造相对名称,而不是那个F('name')。我可能是错的,但是我认为唯一支持该功能的数据库引擎是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

答案 1 :(得分:0)

对我有用的是在模型中添加一个新字段“ 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

答案 2 :(得分:0)

更清洁,更有效的解决方案是使用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)