web-dev-qa-db-fra.com

Comment restreindre les choix de clés étrangères aux objets associés uniquement dans django

J'ai une relation étrangère bidirectionnelle similaire à la suivante

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 restreindre les choix de 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 cela fait que l'interface d'administration ne répertorie aucun enfant.

48
Jeff Mc

Je viens de rencontrer ForeignKey.limit_choices_to dans les documents Django. Je ne sais pas encore comment cela fonctionne, mais ce pourrait être la bonne chose ici.

Mise à jour: ForeignKey.limit_choices_to permet de spécifier une constante, un objet appelable ou un objet Q pour restreindre les choix autorisés pour la clé. Une constante est évidemment inutile ici, car elle ne sait rien des objets impliqués.

L'utilisation d'un appelable (fonction ou méthode de classe ou tout objet appelable) semble plus prometteuse. Toutefois, le problème de la façon d'accéder aux informations nécessaires à partir de l'objet HttpRequest reste. L'utilisation de stockage local du thread peut être une solution.

2. Mise à jour: Voici ce qui a fonctionné pour moi:

J'ai créé un middleware comme décrit dans le lien ci-dessus. Il extrait un ou plusieurs arguments de la partie GET de la demande, tels que "produit = 1", et stocke ces informations dans les sections locales du thread.

Ensuite, il existe une méthode de classe dans le modèle qui lit la variable locale du 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é pour que les pages d'administration normales fonctionnent correctement.

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

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

Le hic, c'est que vous devez fournir les informations pour restreindre les choix via la demande. Je ne vois pas de moyen d'accéder à "soi" ici.

31
Ber

La "bonne" façon de procéder consiste à 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
        fields = "__all__" # for Django 1.8+


    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
28
s29

La nouvelle "bonne" façon de procéder, au moins depuis Django 1.1 est en remplaçant 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 de 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 m'attends à ce que ce soit quelque part sur le moi, mais je ne suis pas sûr.

14
White Box Dev

Ce n'est pas ainsi 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 tentiez d'accéder aux enfants à partir du parent, vous feriez parent_object.child_set.all(). Si vous définissez un related_name dans le champ myparent, c'est ce que vous appelleriez. Ex: related_name='children', Alors vous feriez parent_object.children.all()

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

12
Eric Holscher

Si vous n'avez besoin que des limitations de l'interface d'administration Django, cela peut fonctionner. Je l'ai basé sur cette réponse d'un autre forum - bien que ce soit pour les relations ManyToMany, vous devriez pouvoir remplacer formfield_for_foreignkey pour que cela fonctionne. Dans 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)
6
wasabigeek

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 consiste à valider le modèle. Cela vous permet de générer 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

@Ber: J'ai ajouté une validation au modèle similaire à celle-ci

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)

ce qui fonctionne exactement comme je le souhaite, mais ce serait vraiment bien si cette validation pouvait restreindre les choix dans la liste déroulante de l'interface d'administration plutôt que de valider après le choix.

3
Jeff Mc

J'essaie de faire quelque chose de similaire. Il semble que tout le monde disant "vous ne devriez avoir une clé étrangère que dans un sens" a peut-être mal compris ce que vous essayez de faire.

C'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.

J'ai pensé que je pourrais peut-être 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, cela donne une erreur car la fonction ne reçoit pas d'argument auto :(

Il semble que la seule façon est de mettre la logique dans tous les formulaires qui utilisent ce modèle (c'est-à-dire de passer un ensemble de requêtes aux choix pour votre champ de formulaire). Ce qui est facile à faire, mais ce serait plus DRY d'avoir cela au niveau du modèle. Votre remplacement de la méthode de sauvegarde du modèle semble être un bon moyen d'empêcher les choix invalides de passer.

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

2
Anentropic

Une autre approche consisterait à ne pas avoir fk "favoritechild" comme champ sur le modèle de Parent.

Au lieu de cela, vous pourriez avoir un champ booléen is_favourite sur l'enfant.

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

De cette façon, vous éluderiez tout le problème de faire en sorte que les enfants ne deviennent le favori du parent auquel ils appartiennent.

Le code d'affichage serait légèrement différent mais la logique de filtrage serait simple.

Dans l'administrateur, vous pouvez même avoir un modèle en ligne pour les modèles enfant qui expose la case à cocher is_favourite (si vous n'avez que quelques enfants par parent), sinon l'administrateur devrait être fait du côté de l'enfant.

1
Anentropic