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 deRooms
- Un
Room
peut être à l'intérieur d'un autreRoom
(un placard, par exemple--ForeignKey sur 'auto') - Un
Room
ne peut être à l'intérieur d'un autreRoom
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...
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)
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()
.
il y a limit_choices_to option ForeignKey qui permet de limiter les choix administrateur disponibles pour l'objet
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
à 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)
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...
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.
Dans django 1.6:
form = SpettacoloForm( instance = spettacolo )
form.fields['teatro'].queryset = Teatro.objects.filter( utente = request.user ).order_by( "nome" ).all()
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. :)
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)