82 votes

Limiter les choix de clés étrangères dans la sélection dans un formulaire en ligne dans l'administration

La logique est du modèle est :

  • A Building a beaucoup Rooms
  • A Room peut être à l'intérieur d'un autre Room (un placard, par exemple--ForeignKey sur 'self')
  • A Room ne peut être qu'à l'intérieur d'un autre Room dans le même bâtiment (c'est la partie la plus délicate)

Voici le code que j'ai :

#spaces/models.py
from django.db import models    

class Building(models.Model):
    name=models.CharField(max_length=32)
    def __unicode__(self):
        return self.name

class Room(models.Model):
    number=models.CharField(max_length=8)
    building=models.ForeignKey(Building)
    inside_room=models.ForeignKey('self',blank=True,null=True)
    def __unicode__(self):
        return self.number

et :

#spaces/admin.py
from ex.spaces.models import Building, Room
from django.contrib import admin

class RoomAdmin(admin.ModelAdmin):
    pass

class RoomInline(admin.TabularInline):
    model = Room
    extra = 2

class BuildingAdmin(admin.ModelAdmin):
    inlines=[RoomInline]

admin.site.register(Building, BuildingAdmin)
admin.site.register(Room)

L'affichage en ligne n'affichera que les pièces du bâtiment actuel (ce qui est ce que je veux). Le problème, cependant, est que pour le inside_room il affiche toutes les pièces de la table Pièces (y compris celles des autres bâtiments).

Dans l'inline de rooms j'ai besoin de limiter les inside_room de choisir uniquement rooms qui sont dans le courant building (le dossier du bâtiment est actuellement en cours de modification par le principal BuildingAdmin ).

Je n'arrive pas à trouver un moyen de le faire avec un limit_choices_to dans le modèle, et je n'arrive pas non plus à comprendre comment remplacer correctement le jeu de formulaires en ligne de l'administrateur (j'ai l'impression que je devrais créer un formulaire en ligne personnalisé, passer le building_id du formulaire principal au formulaire en ligne personnalisé, puis limiter le jeu de questions pour les choix du champ en fonction de cela - mais je n'arrive pas à comprendre comment le faire).

Peut-être que c'est trop complexe pour le site d'administration, mais cela semble être quelque chose qui serait généralement utile...

106voto

nogus Points 430

Utilise l'instance de requête comme conteneur temporaire pour obj. Méthode Inline surchargée Formfield_for_foreignkey pour modifier le queryset. Cela fonctionne au moins sur django 1.2.3.

class RoomInline(admin.TabularInline):

    model = Room

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):

        field = super(RoomInline, self).formfield_for_foreignkey(db_field, request, **kwargs)

        if db_field.name == 'inside_room':
            if request._obj_ is not None:
                field.queryset = field.queryset.filter(building__exact = request._obj_)  
            else:
                field.queryset = field.queryset.none()

        return field

class BuildingAdmin(admin.ModelAdmin):

    inlines = (RoomInline,)

    def get_form(self, request, obj=None, **kwargs):
        # just save obj reference for future processing in Inline
        request._obj_ = obj
        return super(BuildingAdmin, self).get_form(request, obj, **kwargs)

1 votes

Ceci m'a épargné beaucoup de tracas. J'avais besoin de filtrer des choix, mais par une variable de session. Cette réponse m'a permis de le faire avec 5 lignes de code. Merci.

4 votes

Merci beaucoup ! Une alternative est d'assigner kwargs['queryset'] avant d'appeler super comme indiqué dans la documentation : docs.djangoproject.com/fr/dev/ref/contrib/admin/

0 votes

Ce code m'a aussi fait gagner des tonnes de temps. Merci beaucoup de l'avoir publié

18voto

Mathijs Points 415

Après avoir lu cet article et fait de nombreuses expériences, je pense avoir trouvé une réponse assez définitive à cette question. Comme il s'agit d'un modèle de conception qui est souvent utilisé, j'ai rédigé une Mixin pour l'administration de Django pour s'en servir.

La limitation (dynamique) de l'ensemble de requêtes pour les champs ForeignKey est maintenant aussi simple que la sous-classification de l'option LimitedAdminMixin et en définissant une get_filters(obj) pour renvoyer les filtres pertinents. Alternativement, un filters peut être définie sur l'administrateur si le filtrage dynamique n'est pas nécessaire.

Exemple d'utilisation :

class MyInline(LimitedAdminInlineMixin, admin.TabularInline):
    def get_filters(self, obj):
        return (('<field_name>', dict(<filters>)),)

Ici, <field_name> est le nom du champ FK à filtrer et <filters> est une liste de paramètres tels que vous les spécifieriez normalement dans la commande filter() de querysets.

1 votes

Merci, ça marche très bien ! Beaucoup plus propre. (Et d'ailleurs, vous avez laissé dans votre code des instructions de journalisation qui ne vont nulle part).

18voto

user1022684 Points 31

Il y a limiter_choix_à Option ForeignKey qui permet de limiter les choix d'administration disponibles pour l'objet.

2 votes

Cela n'aide pas car la requête qui s'exécute dans le limit_choices_to n'a aucune référence à la "classe parent". Par exemple, si un modèle A a une clé étrangère vers B, et aussi vers C, et que C a une clé étrangère vers B, et que nous voulons nous assurer qu'un A ne fait référence qu'à un C qui fait référence au même B que A, la requête doit connaître A->B, ce qu'elle ne fait pas.

1 votes

Il peut être utile avec une combinaison de réponses de haut niveau, cf. stackoverflow.com/a/50298577/2207154

8voto

alav Points 41

Vous pouvez créer quelques classes personnalisées qui transmettront ensuite au formulaire une référence à l'instance parent.

from django.forms.models import BaseInlineFormSet
from django.forms import ModelForm

class ParentInstInlineFormSet(BaseInlineFormSet):
    def _construct_forms(self):
        # instantiate all the forms and put them in self.forms
        self.forms = []
        for i in xrange(self.total_form_count()):
            self.forms.append(self._construct_form(i, parent_instance=self.instance))

    def _get_empty_form(self, **kwargs):
        return super(ParentInstInlineFormSet, self)._get_empty_form(parent_instance=self.instance)
    empty_form = property(_get_empty_form)

class ParentInlineModelForm(ModelForm):
    def __init__(self, *args, **kwargs):
        self.parent_instance = kwargs.pop('parent_instance', None)
        super(ParentInlineModelForm, self).__init__(*args, **kwargs)

dans la classe RoomInline, il suffit d'ajouter :

class RoomInline(admin.TabularInline):
      formset = ParentInstInlineFormset
      form = RoomInlineForm #(or something)

Dans votre formulaire, vous avez maintenant accès dans la méthode init à self.parent_instance ! parent_instance peut maintenant être utilisé pour filtrer les choix et autres.

quelque chose comme :

class RoomInlineForm(ParentInlineModelForm):
    def __init__(self, *args, **kwargs):
        super(RoomInlineForm, self).__init__(*args, **kwargs)
        building = self.parent_instance
        #Filtering and stuff

0 votes

Merci pour cela ! C'est la première version qui a fonctionné pour mon application et elle est également claire et agréable.

5voto

mightyhal Points 432

Cette question et cette réponse sont très similaires et fonctionnent pour un formulaire d'administration ordinaire.

A l'intérieur d'un inline, et c'est là que tout s'écroule... Je ne peux pas accéder aux données du formulaire principal pour obtenir la valeur de la clé étrangère dont j'ai besoin dans ma limite (ou à l'un des enregistrements de l'inline pour obtenir la valeur).

Voici mon admin.py. Je suppose que je cherche la magie avec laquelle remplacer le ? ??? - si je mets une valeur codée en dur (disons 1), cela fonctionne bien et limite correctement les choix disponibles dans la ligne...

#spaces/admin.py
from demo.spaces.models import Building, Room
from django.contrib import admin
from django.forms import ModelForm

class RoomInlineForm(ModelForm):
  def __init__(self, *args, **kwargs):
    super(RoomInlineForm, self).__init__(*args, **kwargs)
    self.fields['inside_room'].queryset = Room.objects.filter(
                               building__exact=????)                       # <------

class RoomInline(admin.TabularInline):
  form = RoomInlineForm
  model=Room

class BuildingAdmin(admin.ModelAdmin):
  inlines=[RoomInline]

admin.site.register(Building, BuildingAdmin)
admin.site.register(Room)

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