Limiter les choix de clés étrangères dans select dans un format inline dans admin

La logique du modèle est:

  • Un Building a beaucoup de Rooms
  • Un Room peut être à l'intérieur d'un autre Room (un placard, par exemple--ForeignKey sur 'auto')
  • Un Room ne peut être à 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)

le inline affichera seulement les pièces dans le bâtiment actuel (qui est ce que je veux). Le problème, cependant, est que pour le inside_room baisse, il affiche toutes les chambres dans la table de chambres (y compris ceux dans d'autres bâtiments).

dans l'inline de rooms , je dois limiter les inside_room choix à seulement rooms qui sont dans le courant building (le dossier de construction étant actuellement modifié par la forme principale BuildingAdmin ).

Je n'arrive pas à trouver un moyen de le faire avec un limit_choices_to dans le modèle, et je ne peux pas non plus trouver comment exactement Outrepasser le formset inline de l'administrateur correctement (j'ai l'impression que je devrais d'une façon ou d'une autre créer une forme inline personnalisée, passer le building_id de la forme principale à la forme inline personnalisée, puis limiter le queryset pour les choix du champ basés sur cela--mais je ne peux juste pas envelopper ma tête autour de la façon de le faire).

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

64

10 réponses

utilise l'instance de requête comme conteneur temporaire pour obj. Overrided inline method formfield_fore_foreignkey to modify 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)
93
répondu nogus 2011-08-09 00:28:56

après avoir lu ce post et avoir fait beaucoup d'expériences, je pense que j'ai trouvé une réponse plutôt définitive à cette question. Comme il s'agit d'un modèle de conception qui est souvent utilisé, j'ai écrit un Mixin pour L'administrateur de Django pour faire usage de celui-ci.

(dynamiquement) limiter le queryset pour les champs de clé étrangère est maintenant aussi simple que de sous-classer LimitedAdminMixin et de définir une méthode get_filters(obj) pour retourner les filtres pertinents. Alternateively, a La propriété filters peut être définie sur l'administrateur si le filtrage dynamique n'est pas requis.

exemple d'usage:

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écifiez normalement dans la méthode des querysets filter() .

15
répondu Mathijs 2011-02-15 20:14:30

il y a limit_choices_to option ForeignKey qui permet de limiter les choix administrateur disponibles pour l'objet

13
répondu user1022684 2016-06-27 07:31:25

vous pouvez créer quelques classes personnalisées qui transmettront ensuite une référence à l'instance mère du 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)

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 de soi.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
8
répondu alav 2012-09-12 15:51:29

cette question et réponse est très similaire, et fonctionne pour un formulaire d'administration régulière

à L'intérieur d'une ligne--et c'est là que ça tombe en morceaux... 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 inline pour saisir la valeur).

Voici mon admin.py. Je suppose que je cherche la magie pour remplacer le ???? --si je branche une valeur codée en dur (1), il fonctionne très bien et correctement limite 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)
4
répondu mightyhal 2017-05-23 12:26:19

j'ai trouvé une solution assez élégante qui fonctionne bien pour les formes en ligne.

appliqué à mon modèle, où je filtre le champ inside_room pour retourner Seulement les pièces qui sont 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 passé au formulaire, c'est un enregistrement existant montrant dans la ligne, et donc je peux juste saisir le bâtiment de l'instance. Si une instance, c'est l'une de la vierge "extra" rangs dans la ligne, et donc, il va à travers les champs de formulaire masqués de la ligne qui stockent l'implicite de la relation de retour à la page principale, et s'empare de la valeur de l'id. Ensuite, il saisit l'objet de construction basé sur ce building_id. Enfin, maintenant que nous avons le bâtiment, nous pouvons définir la série de points de chute pour n'afficher que les articles pertinents.

plus élégant que ma solution d'origine, qui s'est écrasé et brûlé comme inline(mais a fonctionné -- bien, si vous ne vous dérange pas sauvegarder le formulaire à mi-chemin pour faire remplir les champs -- 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, ça marchait si la pièce existait déjà. Sinon, il lancerait une erreur (DoesNotExist), donc je l'attraperais et puis je cacherais le champ (puisqu'il n'y avait aucun moyen, de la part de l'administrateur, de le limiter au bon bâtiment, puisque tout le disque de la salle était neuf, et aucun bâtiment n'était encore installé!)...une fois que vous appuyez sur Enregistrer, il sauve le bâtiment et sur recharger il pourrait limiter la choix...

j'ai juste besoin de trouver un moyen de faire passer les filtres de clé étrangère d'un champ à l'autre dans un nouvel enregistrement--c.-à-d., nouvel enregistrement, sélectionnez un bâtiment, et il limite automatiquement les choix dans la boîte de sélection inside_room--avant que l'enregistrement soit sauvegardé. Mais c'est pour un autre jour...

4
répondu mightyhal 2009-12-09 02:39:23

si Daniel, après avoir édité votre question, n'a pas répondu - Je ne pense pas que je serai d'une grande aide... :- )

je vais suggérer que vous essayez d'imposer à l'administrateur django une logique qui serait mieux implémentée que votre propre groupe de vues, formulaires et gabarits.

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

2
répondu cethegeek 2009-12-01 16:05:38

Dans django 1.6:

 form = SpettacoloForm( instance = spettacolo )
 form.fields['teatro'].queryset = Teatro.objects.filter( utente = request.user ).order_by( "nome" ).all()
2
répondu max4ever 2014-07-18 15:08:48

je dois admettre, je n'ai pas suivi exactement ce que vous essayez de faire, mais je pense que c'est assez complexe que vous pourriez vouloir envisager de ne pas baser votre site sur l'administrateur.

j'ai construit un site une fois qui a commencé avec l'interface d'administration simple, mais est finalement devenu si personnalisé qu'il est devenu très difficile de travailler avec dans les contraintes de l'administrateur. J'aurais été mieux si j'avais à peine commencé à partir de 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 ce que vous essayez de faire n'est pas documenté (c.-à-d. implique d'outrepasser les méthodes admin, de scruter le code source admin, etc.) alors vous êtes probablement mieux de ne pas utiliser l'administrateur. Juste moi deux cents. :)

1
répondu user27478 2009-12-01 17:09:36

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

qui permettent à l'utilisateur de sélectionner le mauvais élément dans popup

pour que ça marche enfin 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)
0
répondu Daniil Mashkin 2018-05-11 18:52:15