Как заказать модель по родительской категории в Django? - PullRequest
1 голос
/ 16 мая 2019

У меня есть модель "Категория" с ForeignKey для "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',
    ]

EDIT !!! Имена родительской категории не должны отображаться вместе с категорией. Я написал это так, чтобы показать, как модель должна быть заказана. Отображение списка будет просто:

- OS
-- Windows
--- Windows 7
--- Windows 8
--- Windows 10
-- Mac
-- Linux
--- Debian
---- Ubuntu
--- Fedora
---- CentOS
---- Oracle Linux

Ответы [ 2 ]

0 голосов
/ 17 мая 2019

Для меня сработало добавление нового поля "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
0 голосов
/ 16 мая 2019

Чтобы иметь возможность сделать заказ по нему, необходимо аннотировать набор запросов в 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]' ссылается на 'относительное_имя', которое не является атрибутом '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,
            ),
            (),
        ))

PS

Oracle, кажется, также поддерживает это, хотя и с другим синтаксисом: Рекурсивный SQL-запрос к самореферентной таблице (Oracle)

PPS

Если в конечном итоге вам необходимо сохранить родительское имя для модели, аннотирование выглядит примерно так:

qs = qs.annotate(relative_name=Concat(F('parent_name'), Value('/'), F('name')))

PPPS

Можно добавить две аннотации, одну для отображения значений, а другую для сортировки.На самом деле, глядя на ваш вопрос еще раз, я думаю, что это будет необходимо, потому что ваш пример имеет subcat -- cat, а не cat -- subcat, как я предполагал выше.Для этого нам понадобятся две аннотации, одна из которых будет возвращена методом relative_name modeladmin, а другая - для relative_name.admin_order_field.

...