web-dev-qa-db-fra.com

Limitez les choix de clés étrangères dans select sous une forme en ligne dans admin

La logique du modèle est:

  • Un Building a plusieurs Rooms
  • Un Room peut se trouver dans un autre Room (un placard, par exemple - ForeignKey sur 'self')
  • Un Room ne peut se trouver qu'à l'intérieur d'un autre Room dans le même bâtiment (c'est la partie 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'inline affichera uniquement les pièces du bâtiment actuel (c'est ce que je veux). Le problème, cependant, est que pour le inside_room déroulant, il affiche toutes les pièces du tableau Pièces (y compris celles des autres bâtiments).

Dans la ligne de rooms, je dois limiter le inside_room choix pour seulement rooms qui se trouvent dans le building actuel (l'enregistrement de bâtiment étant actuellement modifié par le formulaire principal BuildingAdmin).

Je ne peux pas trouver un moyen de le faire avec un limit_choices_to dans le modèle, je ne peux pas non plus comprendre comment remplacer correctement le jeu de formulaires en ligne de l'administrateur (je pense que je devrais être en quelque sorte créer un formulaire en ligne personnalisé, transmettre l'ID de bâtiment du formulaire principal au jeu en ligne personnalisé, puis limiter le jeu de requêtes pour les choix du terrain sur la base de cela - mais je ne peux pas comprendre comment le faire).

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

68
mightyhal

Instance de demande utilisée comme conteneur temporaire pour obj. Substitution de la méthode Inline formfield_for_foreignkey pour modifier le jeu de requêtes. 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)
96
nogus

Après avoir lu ce post et expérimenté beaucoup, je pense avoir trouvé une réponse plutôt définitive à cette question. Comme c'est un modèle de conception qui est souvent utilisé, j'ai écrit un Mixin pour le Django admin pour l'utiliser).

La limitation (dynamique) du jeu de requêtes pour les champs ForeignKey est désormais aussi simple que de sous-classer LimitedAdminMixin et de définir une méthode get_filters(obj) pour renvoyer les filtres appropriés. Alternativement, une propriété filters peut être définie sur l'administrateur si le filtrage dynamique n'est pas requis.

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 comme vous les spécifieriez normalement dans la méthode filter() des ensembles de requêtes.

17
Mathijs

Il y a limit_choices_to option ForeignKey qui permet de limiter les choix d'administration disponibles pour l'objet

14
user1022684

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

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)

en 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 désormais être utilisé pour filtrer les choix et ainsi de suite

quelque chose comme:

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

Cette question et réponse est très similaire et fonctionne pour un formulaire d'administration normal

À l'intérieur d'une ligne - et c'est là qu'elle s'effondre ... Je ne peux tout simplement pas accéder aux données du formulaire principal pour obtenir la valeur de clé étrangère dont j'ai besoin dans ma limite (ou à l'un des enregistrements de la ligne pour saisir la valeur) .

Voici mon admin.py. Je suppose que je cherche la magie pour remplacer le ???? avec - si je branche une valeur codée en dur (disons, 1), cela fonctionne bien et limite correctement les choix disponibles en 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)
4
mightyhal

J'ai trouvé un solution assez élégante qui fonctionne bien pour les formulaires en ligne.

Appliqué à mon modèle, où je filtre le champ inside_room pour ne renvoyer que les pièces qui se trouvent dans le même bâtiment:

#spaces/admin.py
class RoomInlineForm(ModelForm):
  def __init__(self, *args, **kwargs):
    super(RoomInlineForm, self).__init__(*args, **kwargs)  #On init...
  if 'instance' in kwargs:
    building = kwargs['instance'].building
  else:
    building_id = Tuple(i[0] for i in self.fields['building'].widget.choices)[1]
    building = Building.objects.get(id=building_id)
  self.fields['inside_room'].queryset = Room.objects.filter(building__exact=building)

Fondamentalement, si un mot clé "instance" est transmis au formulaire, c'est un enregistrement existant affiché dans la ligne, et donc je peux simplement récupérer le bâtiment de l'instance. S'il ne s'agit pas d'une instance, il s'agit de l'une des lignes "supplémentaires" vides de la ligne, et elle parcourt donc les champs de formulaire masqués de la ligne qui stockent la relation implicite vers la page principale et récupère la valeur id à partir de cela. Ensuite, il saisit l'objet de construction en fonction de ce building_id. Enfin, ayant maintenant le bâtiment, nous pouvons définir le jeu de requêtes des listes déroulantes pour afficher uniquement les éléments pertinents.

Plus élégant que ma solution d'origine, qui s'est écrasée et brûlée en ligne (mais a fonctionné - eh bien, si cela ne vous dérange pas d'enregistrer le formulaire à mi-chemin pour que les listes déroulantes se remplissent - pour les formulaires individuels):

class RoomForm(forms.ModelForm): # For the individual rooms
  class Meta:
mode = Room
  def __init__(self, *args, **kwargs):  # Limits inside_room choices to same building only
    super(RoomForm, self).__init__(*args, **kwargs)  #On init...
try:
  self.fields['inside_room'].queryset = Room.objects.filter( 
    building__exact=self.instance.building)   # rooms with the same building as this room
    except:                  #and hide this field (why can't I exclude?)
    self.fields['inside_room']=forms.CharField( #Add room throws DoesNotExist error
        widget=forms.HiddenInput,   
        required=False,
        label='Inside Room (save room first)')

Pour les non-inlines, cela fonctionnait si la pièce existait déjà. Sinon, cela lancerait une erreur (DoesNotExist), donc je l'attraperais et cacherais le champ (car il n'y avait aucun moyen, de la part de l'administrateur, de le limiter au bon bâtiment, car tout l'enregistrement de la pièce était nouveau, et aucun bâtiment n'a encore été défini!) ... une fois que vous appuyez sur enregistrer, il enregistre le bâtiment et au rechargement, il peut limiter les choix ...

J'ai juste besoin de trouver un moyen de mettre en cascade les filtres de clé étrangère d'un champ à un autre dans un nouvel enregistrement - c'est-à-dire, nouvel enregistrement, sélectionner un bâtiment, et cela limite automatiquement les choix dans la boîte de sélection inside_room - avant que l'enregistrement ne soit enregistré. Mais c'est pour un autre jour ...

4
mightyhal

Le problème dans la réponse @nogus, il y a toujours une mauvaise URL dans le popup /?_to_field=id&_popup=1

qui permet à l'utilisateur de sélectionner le mauvais élément dans la fenêtre contextuelle

Pour enfin le faire fonctionner, j'ai dû changer field.widget.rel.limit_choices_to dict

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':
            building = request._obj_
            if building is not None:
                field.queryset = field.queryset.filter(
                    building__exact=building)
                # widget changed to filter by building
                field.widget.rel.limit_choices_to = {'building_id': building.id}
            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)
4
Daniil Mashkin

Si Daniel, après avoir édité votre question, n'a pas répondu - je ne pense pas que je serai très utile ... :-)

Je vais suggérer que vous essayez de forcer l'ajustement dans l'administration Django une logique qui serait mieux implémentée comme votre propre groupe de vues, de formulaires et de modèles.

Je ne pense pas qu'il soit possible d'appliquer ce type de filtrage à InlineModelAdmin.

2
cethegeek

Dans Django 1.6:

 form = SpettacoloForm( instance = spettacolo )
 form.fields['teatro'].queryset = Teatro.objects.filter( utente = request.user ).order_by( "nome" ).all()
2
max4ever

Je dois admettre que je n'ai pas suivi exactement ce que vous essayez de faire, mais je pense que c'est suffisamment complexe pour que vous souhaitiez peut-être ne pas baser votre site sur l'administrateur.

J'ai construit un site une fois qui a commencé avec la simple interface d'administration, mais est finalement devenu tellement personnalisé qu'il est devenu très difficile de travailler avec les contraintes de l'administrateur. J'aurais été mieux si j'avais recommencé à zéro - plus de travail au début, mais beaucoup plus de flexibilité et moins de douleur à la fin. Ma règle de base serait que si ce que vous essayez de faire n'est pas documenté (c.-à-d. Implique de remplacer les méthodes d'administration, de scruter le code source de l'administrateur, etc.), vous feriez probablement mieux de ne pas utiliser l'administrateur. Juste moi deux cents. :)

1
user27478