Comment limiter les choix de clés étrangères aux objets liés uniquement dans django

StackOverflow https://stackoverflow.com/questions/232435

  •  04-07-2019
  •  | 
  •  

Question

J'ai une relation étrangère à deux voies semblable au suivant

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 limiter les choix relatifs à 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 liste aucun enfant.

Était-ce utile?

La solution

Je viens de tomber sur ForeignKey.limit_choices_to dans la documentation Django. Je ne sais pas encore comment cela fonctionne, mais cela pourrait bien être la bonne chose ici.

Mettre à jour: ForeignKey.limit_choices_to permet de spécifier une constante, un objet pouvant être appelé ou un objet Q afin de limiter les choix autorisés pour la clé. Une constante n’est évidemment d'aucune utilité ici, puisqu'elle ne sait rien des objets impliqués.

L’utilisation d’un appelable (méthode de la fonction ou de la classe ou tout objet appelable) semble plus prometteuse. Cependant, le problème de l'accès aux informations nécessaires à partir de l'objet HttpRequest reste. Utiliser le stockage local des threads peut constituer 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 requête, tels que "product = 1", et stocke ces informations dans les sections locales du thread.

Il existe ensuite une méthode de classe dans le modèle qui lit la variable locale du thread et renvoie une liste d'identifiants limitant 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 ensuite déclaré comme:

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

Le problème est que vous devez fournir les informations permettant de restreindre les choix via la demande. Je ne vois pas le moyen d'accéder à " soi " ici.

Autres conseils

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

Le nouveau " right " Pour ce faire, au moins depuis que Django 1.1 est en redéfinissant AdminModel.formfield_for_foreignkey (self, db_field, request, ** kwargs).

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

Pour ceux qui ne veulent pas suivre le lien ci-dessous, voici un exemple de fonction proche des 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 suppose que c'est en fait quelque part sur le moi, mais je ne suis pas sûr.

Ce n'est pas ainsi que fonctionne Django. Vous ne créerez 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 essayiez d'accéder aux enfants du parent, vous le feriez parent_object.child_set.all () . Si vous définissez un objet related_name dans le champ myparent, c’est ce que vous appelleriez cela. Ex: related_name = 'children' , vous feriez alors parent_object.children.all ()

Lisez le docs . http://docs.djangoproject.com/ fr / dev / topics / db / models / # relations plusieurs-à-un pour plus d'informations.

Si vous n'avez besoin que des limitations de l'interface d'administration de Django, cela pourrait fonctionner. Je l'ai basée sur cette réponse depuis 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)

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

Une des façons de procéder est la validation du 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 seule clé étrangère, d’enfant à parent ici.

@Ber: j'ai ajouté une validation au modèle similaire à celui-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)

qui fonctionne exactement comme je le veux, 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.

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

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

Je pensais 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'autodéfense: (

Il semble que le seul moyen consiste à intégrer la logique dans tous les formulaires utilisant ce modèle (c.-à-d. en transmettant un ensemble de requêtes aux choix de votre champ de formulaire). Ce qui est facile à faire, mais il serait plus sec d’avoir cela au niveau du modèle. Redéfinir la méthode de sauvegarde du modèle semble être un bon moyen d’empêcher que des choix non valides ne se concrétisent.

Mettre à jour
Voir ma dernière réponse pour un autre moyen https://stackoverflow.com/a/3753916/202168

Une autre approche consisterait à ne pas utiliser le champ 'favouritechild' comme champ du modèle 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

Ainsi, vous éviterez tout le problème de l’assurance que les enfants ne pourront devenir que les favoris du parent auquel ils appartiennent.

Le code de vue 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 Child qui affiche la case à cocher is_favourite (si vous n’avez que quelques enfants par parent), sinon l’administrateur devra être fait du côté de l’enfant.

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
    ]
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top