7 votes

Comment implémenter le tri dans Django Admin pour les propriétés de modèle calculées sans réécrire la logique deux fois ?

Dans mon modèle Django, j'ai défini une @property qui a bien fonctionné et la propriété peut être affichée dans le list_display de l'administrateur sans aucun problème.

J'ai besoin de cette propriété non seulement dans l'administration mais aussi dans ma logique de code à d'autres endroits également, il est donc logique de l'avoir comme propriété pour mon modèle.

Maintenant, je voulais rendre la colonne de cette propriété triable, et avec l'aide de la documentation de Django sur l'objet When, cette question StackOverflow pour le calcul de F() et ce lien pour le tri, j'ai réussi à construire la solution fonctionnelle ci-dessous.

La raison pour laquelle je pose une question ici est : en fait, j'ai implémenté ma logique deux fois, une fois en python et une fois sous forme d'expression, ce qui va à l'encontre de la règle de conception consistant à implémenter la même logique une seule fois. Je voulais donc demander si j'ai raté une meilleure solution à mon problème. Toute idée est appréciée.

Voici le modèle (identifiants modifiés) :

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    # [plusieurs_attributs, Meta, __str__() supprimés pour plus de lisibilité]

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

Voici l'administration du modèle :

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'

Si vous n'avez pas d'autre moyen, j'apprécierais également une confirmation du fait en tant que réponse.

5voto

John Moutafis Points 11297

TL/DR : Oui, votre solution semble suivre la seule voie logique.


Eh bien, ce que vous avez composé ici semble être la manière recommandée par les sources que vous listez dans votre question et pour une bonne raison.

Quelle est cependant cette bonne raison ?
Je n'ai pas trouvé de réponse définitive, dans le code, à cela mais j'imagine que cela a à voir avec la façon dont le décorateur @property fonctionne en Python.

Lorsque nous définissons une propriété avec le décorateur, nous ne pouvons pas y ajouter des attributs et puisque admin_order_field est un attribut, nous ne pouvons pas l'inclure là-dedans. Cette affirmation semble être renforcée par la documentation list_display de l'Admin Django où le passage suivant existe :

Les éléments de list_display peuvent également être des propriétés. Veuillez noter cependant, qu'en raison de la façon dont fonctionnent les propriétés en Python, définir short_description sur une propriété n'est possible qu'en utilisant la fonction property() et non avec le décorateur @property.

Cette citation combinée à cette question-réponse : AttributeError: 'property' object has no attribute 'admin_order_field' semble expliquer pourquoi il n'est pas possible d'avoir un élément pouvant être ordonné directement dans le panneau administratif à partir d'une propriété de modèle.


Cela expliqué (probablement ?) il est temps pour quelques acrobaties mentales !!

Dans la partie de la documentation précédemment mentionnée, nous pouvons également voir que le admin_order_field peut accepter des expressions de requête depuis la version 2.1 :

Les expressions de requête peuvent être utilisées dans admin_order_field. Par exemple :

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')

Ceci, en combinaison avec la partie précédente sur la méthode property(), nous permet de remanier votre code et de déplacer essentiellement la partie annotation vers le modèle :

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)

Enfin, dans le admin.py, nous avons seulement besoin de :

class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d")

3voto

Ivan Points 1298

Bien que je pense que votre solution est très bonne (voire meilleure), une autre approche peut consister à extraire la requête admin au gestionnaire du modèle :

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" )

Dans ce cas, vous n'avez pas besoin de la propriété car chaque objet aura l'attribut s_d, bien que cela soit vrai uniquement pour les objets existants (de la base de données). Si vous créez un nouvel objet en Python et que vous essayez d'accéder à obj.s_d, vous obtiendrez une erreur. Un autre inconvénient est que chaque requête sera annotée avec cet attribut même si vous ne l'utilisez pas, mais cela peut être résolu en personnalisant la requête du gestionnaire.

2voto

GwynBleidD Points 10017

Malheureusement, c'est impossible dans la version stable actuelle de Django (jusqu'à 2.2) en raison du fait que Django admin ne reprend pas admin_order_field des propriétés de l'objet.

Heureusement, cela sera possible dans la prochaine version de Django (3.0 et ultérieures) qui devrait être publiée le 2 décembre.

La façon d'y parvenir :

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    # [plusieurs attributs, Meta, __str__() enlevés pour plus de lisibilité]

    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)

Alternativement, vous pouvez créer un décorateur qui ajoutera n'importe quel attribut à la fonction, avant de la convertir en propriété :

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("[...]")
    # [plusieurs attributs, Meta, __str__() enlevés pour plus de lisibilité]

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

0voto

jbiz Points 83

Une autre solution possible pourrait être de convertir la propriété s_d en un champ de modèle et de remplacer la méthode de sauvegarde du modèle pour la garder à jour.

# 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" )

Le tri par défaut dans admin.py sera appliqué et la valeur de s_d sera mise à jour à chaque fois que le modèle est enregistré.

Il y a un inconvénient à cette méthode si vous prévoyez de faire beaucoup d'opérations en masse, telles que bulk_create, update, ou delete.

Les méthodes de modèle modifiées ne sont pas appelées lors d'opérations en masse

Notez que la méthode delete() pour un objet n'est pas nécessairement appelée lors de la suppression d'objets en masse en utilisant un QuerySet ou en raison d'une suppression en cascade. Pour vous assurer que la logique de suppression personnalisée est exécutée, vous pouvez utiliser des signaux pre_delete et/ou post_delete.

Malheureusement, il n'y a pas de solution de contournement lors de la création ou de la mise à jour d'objets en masse, car aucun des save(), pre_save, et post_save ne sont appelés.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X