La logique du modèle est:
Building
a plusieurs Rooms
Room
peut se trouver dans un autre Room
(un placard, par exemple - ForeignKey sur 'self')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 ...
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)
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.
Il y a limit_choices_to option ForeignKey qui permet de limiter les choix d'administration disponibles pour l'objet
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
À 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)
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 ...
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)
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.
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 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. :)