Как реализовать сортировку в Django Admin для вычисляемых свойств модели без написания логики дважды? - PullRequest
5 голосов
/ 13 октября 2019

В моем Django model я определил @property, который работал хорошо, и это свойство может быть показано администратору list_display без проблем.

Мне нужно это свойство не только в админке, но и в логике кода в других местах, поэтому имеет смысл использовать его как свойство для моей модели.

Теперь я хотел сделать столбец этого свойства сортируемым, и с помощью документации Django объекта When , этот вопрос StackOverflow для F () - вычисления и эта ссылка для сортировки Мне удалось построить рабочее решение, показанное ниже.

Причина постановки вопроса здесь такова: фактически я реализовал свою логику дважды, один раз заpython и один раз в форме выражения, что противоречит правилу разработки, заключающемуся в реализации одной и той же логики только один раз. Поэтому я хотел спросить, пропустил ли я лучшее решение своей проблемы. Любые идеи приветствуются.

Это модель (идентификаторы изменены):

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    # [several_attributes, Meta, __str__() removed for readability]

    @property
    def s_d(self):
        if self.fr:
            return self.de
        else:
            return self.gd + self.na

Это модель администратора :

class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d", "gd", "na", "de", "fr" )

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        queryset = queryset.annotate(
            _s_d=Case(
                When(fr=True, then='s_d'),
                When(fr=False, then=F('gd') + F('na')),
                default=Value(0),
                output_field=IntegerField(),
            )
        )
        return queryset

    def s_d(self, obj):
        return obj._s_d
    s_d.admin_order_field = '_s_d'

Если нет другого пути, я также был бы признателен за подтверждение этого факта в качестве ответа.

Ответы [ 4 ]

4 голосов
/ 16 октября 2019

TL / DR: Да, ваше решение, кажется, следует единственному подходу, который имеет смысл.


Хорошо, то, что вы здесь сочли, кажется рекомендуемым способом изисточники, которые вы перечислили в своем вопросе и по уважительной причине.

В чем причина?
Я не нашел в этом коде окончательного ответа, но япредставьте, что это связано с тем, как @property декоратор работает в Python.

Когда мы устанавливаем свойство с помощью декоратора, мы не можем добавлять к нему атрибуты, а поскольку admin_order_field является атрибутом, у нас его там не может быть. Это утверждение, по-видимому, подкреплено документацией Администратора Django list_display , где существует следующий отрывок:

Элементы list_display также могут быть свойствами. Однако обратите внимание, что из-за того, как свойства работают в Python, установка short_description для свойства возможна только при использовании функции property(), а не с декоратором @property.

Эта цитата в сочетании с этим QA: AttributeError: объект 'property' не имеет атрибута 'admin_order_field' , кажется, объясняет, почему невозможно получить заказ из свойства модели непосредственно вПанель администратора.


Это объяснило (вероятно?), что пришло время для некоторой умственной гимнастики !!

В ранее упомянутой части документации мы также видим, что admin_order_field может принимать выражения запроса начиная с версии 2.1:

Выражения запроса могут использоваться в admin_order_field. Например:

from django.db.models import Value
from django.db.models.functions import Concat

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    def full_name(self):
        return self.first_name + ' ' + self.last_name
    full_name.admin_order_field = Concat('first_name', Value(' '), 'last_name')

Это в сочетании с предыдущей частью о методе property() позволяет нам реорганизовать ваш код и существенно переместить часть annotation в модель:

class De(models.Model):
    ...
    def calculate_s_d(self):
        if self.fr:
            return self.de
        else:
            return self.gd + self.na

    calculate_s_d.admin_order_field = Case(
        When(fr=True, then='s_d'),
        When(fr=False, then=F('gd') + F('na')),
        default=Value(0),
        output_field=IntegerField(),
    )

    s_d = property(calculate_s_d)

Наконец, на admin.py нам нужно только:

class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d")
3 голосов
/ 16 октября 2019

Хотя я думаю, что ваше решение очень хорошее (или даже лучше), другой подход может заключаться в извлечении запроса администратора для менеджера модели:

class DeManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().annotate(
            s_d=Case(
                When(fr=True, then='s_d'),
                When(fr=False, then=F('gd') + F('na')),
                default=Value(0),
                output_field=IntegerField(),
            )
        )


class De(models.Model):
    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    objects = DeManager()


class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d", "gd", "na", "de", "fr" )

В этом случае вам не нужно свойствопотому что каждый объект будет иметь атрибут s_d, хотя это верно только для существующих объектов (из базы данных). Если вы создадите новый объект в Python и попытаетесь получить доступ к obj.s_d, вы получите ошибку. Другим недостатком является то, что каждый запрос будет аннотирован этим атрибутом, даже если вы его не используете, но это можно решить, настроив набор запросов менеджера.

2 голосов
/ 21 октября 2019

К сожалению, это невозможно в текущей стабильной версии Django (до 2.2) из-за того, что администратор Django не извлекает admin_order_field из свойств объекта.

К счастью, это будет возможно в следующей версии Django (3.0 ивверх), который должен быть выпущен 2 декабря.

Способ достижения этого:

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    # [several_attributes, Meta, __str__() removed for readability]

    def s_d(self):
        if self.fr:
            return self.de
        else:
            return self.gd + self.na
    s_d.admin_order_field = '_s_d'
    s_d = property(s_d)

В качестве альтернативы, вы можете создать некоторый декоратор, который добавит любой атрибут в функцию, прежде чем преобразовать егок недвижимости:

def decorate(**kwargs):
    def wrap(function):
        for name, value in kwargs.iteritems():
            setattr(function, name, value)

        return function
    return wrap

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    # [several_attributes, Meta, __str__() removed for readability]

    @property
    @decorate(admin_order_field='_s_d')
    def s_d(self):
        if self.fr:
            return self.de
        else:
            return self.gd + self.na
0 голосов
/ 23 октября 2019

Другим возможным решением может быть преобразование свойства s_d в поле модели и переопределение метода сохранения модели, чтобы поддерживать его в актуальном состоянии.

# models.py

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    s_d = models.SmallIntegerField("[...]", blank=True)

    # [several_attributes, Meta, __str__() removed for readability]

    def save(self, *args, **kwargs):
        if self.fr:
            self.s_d = self.de
        else:
            self.s_d = self.gd + self.na
        super().save(*args, **kwargs)

# admin.py

class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d", "gd", "na", "de", "fr" )

Сортировка по умолчанию в admin.py будетприменяется и значение s_d будет обновляться при каждом сохранении модели.

В этом методе есть предостережение , если вы планируете выполнять много массовых операций, таких какbulk_create, update или delete.

Переопределенные методы модели не вызываются при массовых операциях

Обратите внимание, что метод delete () для объекта не обязательновызывается при массовом удалении объектов с использованием QuerySet или в результате каскадного удаления. Чтобы обеспечить выполнение настроенной логики удаления, вы можете использовать сигналы pre_delete и / или post_delete.

К сожалению, не существует обходного пути при массовом создании или обновлении объектов, поскольку ни один из методов save (), pre_save иpost_save называются.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...