29 votes

Django : accès à l'instance du modèle depuis ModelAdmin ?

J'ai un modèle pour les commandes dans une application de boutique en ligne, avec une clé primaire auto-incrémentée et une clé étrangère à elle-même, puisque les commandes peuvent être divisées en plusieurs commandes, mais la relation avec la commande originale doit être maintenue.

class Order(models.Model):
    ordernumber = models.AutoField(primary_key=True)
    parent_order = models.ForeignKey('self', null=True, blank=True, related_name='child_orders')
    # .. other fields not relevant here

J'ai enregistré une classe OrderAdmin pour le site d'administration. Pour la vue détaillée, j'ai inclus parent_order dans le fieldsets attribut. Bien sûr, par défaut, cela liste toutes les commandes dans une boîte de sélection, mais ce n'est pas le comportement souhaité. Au lieu de cela, pour les commandes qui n'ont pas de commande mère (c'est-à-dire qui n'ont pas été scindées d'une autre commande ; parent_order est NULL/None), aucun ordre ne doit être affiché. Pour les commandes qui ont été fractionnées, cela ne devrait afficher que la commande mère unique.

Une nouvelle méthode ModelAdmin est disponible, formfield_for_foreignkey Il est possible de filtrer l'ensemble de requêtes dans cette application. Imaginons que nous regardions la vue détaillée de la commande n° 11234, qui a été séparée de la commande n° 11208. Le code est le suivant

def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == 'parent_order':
        # kwargs["queryset"] = Order.objects.filter(child_orders__ordernumber__exact=11234)
        return db_field.formfield(**kwargs)
    return super(OrderAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

La ligne commentée fonctionne lorsqu'elle est exécutée dans un shell Python, renvoyant un queryset à un seul élément contenant la commande #11208 pour #11234 et toutes les autres commandes qui ont pu être scindées à partir de celle-ci.

Bien sûr, nous ne pouvons pas coder en dur le numéro de commande à cet endroit. Nous avons besoin d'une référence au ordernumber de l'instance de commande dont nous regardons la page détaillée. Comme ceci :

kwargs["queryset"] = Order.objects.filter(child_orders__ordernumber__exact=?????)

Je n'ai pas trouvé de moyen efficace de remplacer ? ???? par une référence à l'instance "actuelle" de l'ordre, et j'ai creusé assez profondément. self à l'intérieur de formfield_for_foreignkey fait référence à l'instance ModelAdmin, et bien qu'elle ait une fonction model ce n'est pas l'instance du modèle de commande (c'est une référence ModelBase ; self.model() retourne une instance, mais son numéro d'ordre est None).

Une solution pourrait consister à extraire le numéro de commande de request.path (/admin/orders/order/11234/), mais c'est vraiment très laid. J'aimerais vraiment qu'il y ait une meilleure solution.

43voto

Daniel Roseman Points 199743

Je pense que vous devriez aborder ce problème d'une manière légèrement différente - en modifiant le ModelForm, plutôt que la classe admin. Quelque chose comme ceci :

class OrderForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(OrderForm, self).__init__(*args, **kwargs)
        self.fields['parent_order'].queryset = Order.objects.filter(
            child_orders__ordernumber__exact=self.instance.pk)

class OrderAdmin(admin.ModelAdmin):
    form = OrderForm

5voto

Erwin Julius Points 190

J'ai modélisé ma classe en ligne de cette façon. La façon dont elle récupère l'identifiant du formulaire parent pour filtrer les données en ligne est un peu moche, mais elle fonctionne. Elle filtre les unités par société à partir du formulaire parent.

Le concept original est expliqué ici http://www.stereoplex.com/blog/filtering-dropdown-lists-in-the-django-admin

class CompanyOccupationInline(admin.TabularInline):

    model = Occupation
    # max_num = 1
    extra = 0
    can_delete = False
    formset = RequiredInlineFormSet

    def formfield_for_dbfield(self, field, **kwargs):

        if field.name == 'unit':
            parent_company = self.get_object(kwargs['request'], Company)
            units = Unit.objects.filter(company=parent_company)
            return forms.ModelChoiceField(queryset=units)
        return super(CompanyOccupationInline, self).formfield_for_dbfield(field, **kwargs)

    def get_object(self, request, model):
        object_id = request.META['PATH_INFO'].strip('/').split('/')[-1]
        try:
            object_id = int(object_id)
        except ValueError:
            return None
        return model.objects.get(pk=object_id)

3voto

jenniwren Points 53

La réponse ci-dessus d'Erwin Julius a fonctionné pour moi, sauf que j'ai trouvé que le nom "get_object" entre en conflit avec une fonction Django, donc nommez la fonction "my_get_object".

class CompanyOccupationInline(admin.TabularInline):

    model = Occupation
    # max_num = 1
    extra = 0
    can_delete = False
    formset = RequiredInlineFormSet

    def formfield_for_dbfield(self, field, **kwargs):

        if field.name == 'unit':
            parent_company = self.my_get_object(kwargs['request'], Company)
            units = Unit.objects.filter(company=parent_company)
            return forms.ModelChoiceField(queryset=units)
        return super(CompanyOccupationInline, self).formfield_for_dbfield(field, **kwargs)

    def my_get_object(self, request, model):
        object_id = request.META['PATH_INFO'].strip('/').split('/')[-1]
        try:
            object_id = int(object_id)
        except ValueError:
            return None
        return model.objects.get(pk=object_id)

Il m'a dit de ne pas "répondre" aux réponses des autres, mais je n'ai pas encore le droit de "répondre". Je cherche cela depuis un moment et j'espère que cela sera utile aux autres. Je ne suis pas non plus autorisée à voter en faveur de la réponse, sinon je le ferais !

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