2008-10-24 4 views
40

J'ai deux sens relation étrangère semblable au suivantComment puis-je limiter les choix clés étrangères à des objets connexes que dans django

class Parent(models.Model): 
    name = models.CharField(max_length=255) 
    favoritechild = models.ForeignKey("Child", blank=True, null=True) 

class Child(models.Model): 
    name = models.CharField(max_length=255) 
    myparent = models.ForeignKey(Parent) 

Comment puis-je limiter les choix pour Parent.favoritechild aux seuls enfants dont le parent est lui-même? J'ai essayé

class Parent(models.Model): 
    name = models.CharField(max_length=255) 
    favoritechild = models.ForeignKey("Child", blank=True, null=True, limit_choices_to = {"myparent": "self"}) 

mais qui provoque l'interface d'administration de ne pas la liste des enfants.

+0

vous ne devriez pas utiliser "null = True", je pense. Regardez dans le doc django – Ber

+0

Le null = True fait référence à CharFields. Ici, il est parfaitement correct d'avoir null = True (sinon les parents ne peuvent pas être sauvés sans un enfant) –

Répondre

27

Je viens de rencontrer ForeignKey.limit_choices_to dans les documents Django. Je ne sais pas encore comment cela fonctionne, mais il pourrait juste être le bon pense ici.

Mise à jour: ForeignKey.limit_choices_to permet de spécifier une constante, un appelable ou un objet Q pour restreindre les choix autorisés pour la clé. Une constante n'est évidemment pas utile ici, puisqu'elle ne sait rien des objets impliqués. L'utilisation d'un callable (méthode de fonction ou de classe ou tout objet pouvant être appelé) semble plus prometteuse. Le problème reste comment accéder aux informations nécessaires à partir de l'objet HttpRequest. L'utilisation de thread local storage peut être une solution.

2. Mise à jour: Voici ce que tu as travaillé pour moi:

J'ai créé un milieu tel que décrit articles dans le lien ci-dessus. Il extrait un ou plusieurs arguments de la partie GET de la requête, comme "product = 1" et stocke cette information dans les locals de threads.

Ensuite, il existe une méthode de classe dans le modèle qui lit la variable locale de thread et renvoie une liste d'ID pour limiter le choix d'un champ de clé étrangère.

@classmethod 
def _product_list(cls): 
    """ 
    return a list containing the one product_id contained in the request URL, 
    or a query containing all valid product_ids if not id present in URL 

    used to limit the choice of foreign key object to those related to the current product 
    """ 
    id = threadlocals.get_current_product() 
    if id is not None: 
     return [id] 
    else: 
     return Product.objects.all().values('pk').query 

Il est important de renvoyer une requête contenant tous les identifiants possibles si aucun n'a été sélectionné afin que les pages d'administration normales fonctionnent correctement.

Le champ de clé étrangère est alors déclarée comme:

product = models.ForeignKey(
    Product, 
    limit_choices_to={ 
     id__in=BaseModel._product_list, 
    }, 
) 

Le hic est que vous devez fournir les informations pour limiter les choix par la demande. Je ne vois pas un moyen d'accéder à "soi" ici.

+6

Une solution intéressante, mais l'utilisation des threads se sent comme un tel hack ... – Cerin

+0

@Cerin: Les threads sont utilisés de toute façon dans Django. threadlocals est juste un moyen de transmettre des informations de la demande à un thread de manière sûre pour les cas où 'self' n'est pas disponible pour faire référence aux données de la requête. – Ber

+2

Aucun thread réel n'est utilisé dans l'exemple ci-dessus, il utilise simplement des threadlocals pour émuler une portée plus large. Vous pouvez le faire de manière équivalente avec une valeur stockée sur, disons, la classe Model ou n'importe où ailleurs qui aura une portée suffisamment large pour être accessible aux deux endroits. – jbg

12

Ce n'est pas comme ça que fonctionne django. Vous ne créeriez la relation que dans un sens.

class Parent(models.Model): 
    name = models.CharField(max_length=255) 

class Child(models.Model): 
    name = models.CharField(max_length=255) 
    myparent = models.ForeignKey(Parent) 

Et si vous essayez d'accéder aux enfants du parent, vous feriez parent_object.child_set.all(). Si vous définissez un related_name dans le champ myparent, alors c'est ce que vous appelleriez comme. Ex: related_name='children', alors vous feriez parent_object.children.all()

Lire la docshttp://docs.djangoproject.com/en/dev/topics/db/models/#many-to-one-relationships pour plus.

3

Voulez-vous restreindre les choix disponibles dans l'interface d'administration lors de la création/modification d'une instance de modèle?

Une façon de procéder est la validation du modèle. Cela vous permet d'élever une erreur dans l'interface d'administration si le champ étranger n'est pas le bon choix.

Bien sûr, la réponse d'Eric est correcte: vous n'avez vraiment besoin que d'une clé étrangère, de l'enfant au parent ici.

3

@Ber: J'ai ajouté la validation du modèle similaire à celui

class Parent(models.Model): 
    name = models.CharField(max_length=255) 
    favoritechild = models.ForeignKey("Child", blank=True, null=True) 
    def save(self, force_insert=False, force_update=False): 
    if self.favoritechild is not None and self.favoritechild.myparent.id != self.id: 
     raise Exception("You must select one of your own children as your favorite") 
    super(Parent, self).save(force_insert, force_update) 

qui fonctionne exactement comme je veux, mais ce serait vraiment bien si cette validation pourrait restreindre les choix dans le menu déroulant dans l'interface d'administration plutôt que de valider après le choix.

2

Je suis en train de faire quelque chose de similaire. Il semble que tout le monde dit «vous ne devriez avoir qu'une clé étrangère dans un sens» a peut-être mal compris ce que vous essayez de faire.

Il est dommage que limit_choices_to = {"myparent": "self"} que vous vouliez faire ne fonctionne pas ... cela aurait été propre et simple. Malheureusement, le 'soi' n'est pas évalué et passe par une chaîne simple.

Je pensais que je pouvais faire:

class MyModel(models.Model): 
    def _get_self_pk(self): 
     return self.pk 
    favourite = models.ForeignKey(limit_choices_to={'myparent__pk':_get_self_pk}) 

Mais hélas qui donne une erreur parce que la fonction ne soit pas passé une auto arg :(

Il semble que la seule façon est de mettre la logique dans toutes les formes qui utilisent ce modèle (c.-à-d. passer un jeu de requête aux choix de votre champ de formulaire) Ce qui est facile à faire, mais il serait plus sec d'avoir ceci au niveau du modèle. le modèle semble être une bonne façon d'éviter des choix invalides passer à travers.

Mise à jour
Voir ma réponse plus tard pour une autre façon https://stackoverflow.com/a/3753916/202168

1

Une autre approche serait de ne pas avoir fk « de favouritechild » comme un champ sur le modèle parent. Au lieu de cela, vous pouvez avoir un champ booléen is_favourite sur l'enfant. A la place, vous pouvez utiliser un champ booléen is_favourite.

Cela peut aider: https://github.com/anentropic/django-exclusivebooleanfield

De cette façon, vous voulez contourner le problème des enfants assurant ne pouvait être le favori du parent auquel ils appartiennent.

Le code de vue serait légèrement différent, mais la logique de filtrage serait simple.

Dans l'admin, vous pourriez même avoir une ligne pour les modèles enfants qui ont exposé la case is_favourite (si vous avez seulement quelques enfants par parent) sinon l'administrateur devrait être fait du côté de l'enfant.

+0

Mais il est logique que chaque Parent ait son propre ensemble d'enfants préférés. Avoir un booléen signifierait que tous les parents ont le même ensemble de favoris - ce que je ne pense pas que Jeff veut. – wasabigeek

+0

non, car chaque 'Enfant' n'appartient qu'à un seul 'Parent'. Si c'était une relation many-to-many, ce serait comme vous le dites, auquel cas le booléen irait sur le modèle 'through' – Anentropic

+0

C'est vrai. Comment l'auriez-vous affiché dans l'admin en utilisant cette méthode? Basé sur la question, il devrait être visible lors de la modification d'un parent. Je ne pense pas que limit_choices_to fonctionne avec des modèles traversants ou des clés étrangères par défaut. – wasabigeek

13

Le nouveau "bon" moyen de faire cela, au moins depuis Django 1.1 est en remplaçant le AdminModel.formfield_for_foreignkey (self, db_field, request, ** kwargs).

Voir http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey

Pour ceux qui ne veulent pas suivre le lien ci-dessous est un exemple de fonction qui est proche pour les modèles questions ci-dessus.

class MyModelAdmin(admin.ModelAdmin): 
    def formfield_for_foreignkey(self, db_field, request, **kwargs): 
     if db_field.name == "favoritechild": 
      kwargs["queryset"] = Child.objects.filter(myparent=request.object_id) 
     return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs) 

Je ne suis pas sûr de savoir comment obtenir l'objet en cours de modification. Je pense que c'est en fait quelque part sur soi mais je ne suis pas sûr.

+0

un moyen de piratage probablement sale peut être analysé comme suit à partir de l'URL:' desired_id = request.META ['PATH_INFO']. strip ('/'). split ('/') [- 1] 'J'adorerais voir une meilleure façon de le faire de manière propre. – andi

+1

Il ya une faute de frappe dans la dernière ligne: 'formfield_for_manytomany' ->' formfield_for_foreignkey' – sebastibe

22

La «bonne» façon de le faire est d'utiliser un formulaire personnalisé. De là, vous pouvez accéder à self.instance, qui est l'objet actuel. Exemple -

from django import forms 
from django.contrib import admin 
from models import * 

class SupplierAdminForm(forms.ModelForm): 
    class Meta: 
     model = Supplier 

    def __init__(self, *args, **kwargs): 
     super(SupplierAdminForm, self).__init__(*args, **kwargs) 
     if self.instance: 
      self.fields['cat'].queryset = Cat.objects.filter(supplier=self.instance) 

class SupplierAdmin(admin.ModelAdmin): 
    form = SupplierAdminForm 
+0

Hey @ s29 Je suis votre suggestion, mais je reçois une instance de 'nom global '' n'est pas défini ', Pouvez-vous m'aider, s'il vous plaît ? – slackmart

+0

@sgmart, vous avez probablement omis "self" de self.instance? – s29

+0

Cette solution fonctionne très bien dans les cas où vous avez besoin de 'self.instance' pour effectuer la sélection. – Bartvds

3

Si vous avez seulement besoin des limitations dans l'interface d'administration de Django, cela peut fonctionner. Je l'ai basé sur this answer à partir d'un autre forum - bien que ce soit pour les relations ManyToMany, vous devriez être en mesure de remplacer formfield_for_foreignkey pour que cela fonctionne. En admin.py:

class ParentAdmin(admin.ModelAdmin): 
    def get_form(self, request, obj=None, **kwargs): 
     self.instance = obj 
     return super(ParentAdmin, self).get_form(request, obj=obj, **kwargs) 

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs): 
     if db_field.name == 'favoritechild' and self.instance:  
      kwargs['queryset'] = Child.objects.filter(myparent=self.instance.pk) 
     return super(ChildAdmin, self).formfield_for_foreignkey(db_field, request=request, **kwargs) 
+0

alors ... comment changer le 'favouritechild'? vous avez limité le jeu de requête à une seule ligne – Anentropic

+1

Bon point. J'ai modifié la requête, devinez que cela devrait fonctionner. – wasabigeek

+0

Oui, cela semble correct – Anentropic

-1
from django.contrib import admin 
from sopin.menus.models import Restaurant, DishType 

class ObjInline(admin.TabularInline): 
    def __init__(self, parent_model, admin_site, obj=None): 
     self.obj = obj 
     super(ObjInline, self).__init__(parent_model, admin_site) 

class ObjAdmin(admin.ModelAdmin): 

    def get_inline_instances(self, request, obj=None): 
     inline_instances = [] 
     for inline_class in self.inlines: 
      inline = inline_class(self.model, self.admin_site, obj) 
      if request: 
       if not (inline.has_add_permission(request) or 
         inline.has_change_permission(request, obj) or 
         inline.has_delete_permission(request, obj)): 
        continue 
       if not inline.has_add_permission(request): 
        inline.max_num = 0 
      inline_instances.append(inline) 

     return inline_instances 



class DishTypeInline(ObjInline): 
    model = DishType 

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs): 
     field = super(DishTypeInline, self).formfield_for_foreignkey(db_field, request, **kwargs) 
     if db_field.name == 'dishtype': 
      if self.obj is not None: 
       field.queryset = field.queryset.filter(restaurant__exact = self.obj) 
      else: 
       field.queryset = field.queryset.none() 

     return field 

class RestaurantAdmin(ObjAdmin): 
    inlines = [ 
     DishTypeInline 
    ] 
+1

Ajoutez quelques explications à votre réponse. Poster juste du code - n'aide pas beaucoup et n'enseigne pas trop. – admix

Questions connexes