¿Cómo restrinjo las opciones de claves externas a objetos relacionados solo en django?

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

  •  04-07-2019
  •  | 
  •  

Pregunta

Tengo una relación externa bidireccional similar a la siguiente

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)

¿Cómo restrinjo las opciones para Parent.favoritechild a solo niños cuyo padre es el mismo? Lo intenté

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

pero eso hace que la interfaz de administración no incluya ningún elemento secundario.

¿Fue útil?

Solución

Acabo de encontrar ForeignKey.limit_choices_to en los documentos de Django. Aún no estoy seguro de cómo funciona esto, pero podría ser lo correcto aquí.

Actualizar: ForeignKey.limit_choices_to permite especificar una constante, un objeto que se puede llamar o Q para restringir las opciones permitidas para la clave. Obviamente, una constante no sirve para nada aquí, ya que no sabe nada sobre los objetos involucrados.

Parece más prometedor usar un llamable (método de función o clase o cualquier objeto llamable). Sin embargo, el problema de cómo acceder a la información necesaria desde el objeto HttpRequest permanece. Usar almacenamiento local de subprocesos puede ser una solución.

2. Actualización: Esto es lo que me ha funcionado:

Creé un middleware como se describe en el enlace de arriba. Extrae uno o más argumentos de la parte GET de la solicitud, como " producto = 1 " y almacena esta información en los locales de subprocesos.

A continuación, hay un método de clase en el modelo que lee la variable local del hilo y devuelve una lista de identificadores para limitar la elección de un campo de clave externa.

@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

Es importante devolver una consulta que contenga todos los identificadores posibles si no se seleccionó ninguno para que las páginas de administración normales funcionen bien.

El campo de clave foránea se declara como:

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

El problema es que debe proporcionar la información para restringir las opciones a través de la solicitud. No veo una forma de acceder a " self " aquí.

Otros consejos

La forma "correcta" de hacerlo es usar un formulario personalizado. Desde allí, puede acceder a self.instance, que es el objeto actual. Ejemplo -

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

El nuevo " derecha " De esta manera, al menos desde que Django 1.1 es sobreescribiendo AdminModel.formfield_for_foreignkey (self, db_field, request, ** kwargs).

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

Para aquellos que no quieran seguir el siguiente enlace es una función de ejemplo que está cerca para los modelos de preguntas anteriores.

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)

Solo no estoy seguro de cómo obtener el objeto actual que se está editando. Supongo que en realidad está en el yo en alguna parte, pero no estoy seguro.

No es así como funciona django. Solo crearías la relación de una manera.

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

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

Y si estuviera tratando de acceder a los hijos del padre, lo haría parent_object.child_set.all () . Si establece un nombre relacionado en el campo myparent, entonces se lo denominaría. Ej: related_name = 'children' , entonces deberías hacer parent_object.children.all()

Lea la docs http://docs.djangoproject.com/ es / dev / topics / db / models / # many-to-one-relationships para obtener más información.

Si solo necesita las limitaciones en la interfaz de administración de Django, esto podría funcionar. Lo basé en esta respuesta de otro foro: aunque es para las relaciones ManyToMany, deberías poder reemplazar formfield_for_foreignkey para que funcione. 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)

¿Desea restringir las opciones disponibles en la interfaz de administración al crear / editar una instancia de modelo?

Una forma de hacer esto es la validación del modelo. Esto le permite generar un error en la interfaz de administración si el campo externo no es la opción correcta.

Por supuesto, la respuesta de Eric es correcta: solo necesitas una clave externa, desde el niño hasta el padre aquí.

@Ber: he agregado validación al modelo similar a este

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)

que funciona exactamente como quiero, pero sería muy bueno si esta validación pudiera restringir las opciones en el menú desplegable en la interfaz de administración en lugar de validar después de la opción.

Estoy tratando de hacer algo similar. Parece que todos los que dicen 'solo debes tener una clave foránea de una manera' tal vez hayan entendido mal lo que intentas hacer.

Es una vergüenza que limit_choices_to = {" myparent " ;: " self "} que no quisiste hacer ... eso hubiera sido limpio y simple. Desafortunadamente, el "yo" no se evalúa y pasa como una cadena simple.

Pensé que tal vez podría hacer:

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

Pero, desgraciadamente, eso da un error porque la función no se pasa a sí misma como argumento :(

Parece que la única forma es poner la lógica en todas las formas que usan este modelo (es decir, pasar un queryset a las opciones para su campo de formulario). Lo que se hace fácilmente, pero sería más SECO tener esto en el nivel del modelo. Su anulación del método de guardado del modelo parece una buena manera de evitar que se realicen elecciones no válidas.

Actualizar
Vea mi respuesta posterior para otra forma https://stackoverflow.com/a/3753916/202168

Un enfoque alternativo sería no tener fk 'favouritechild' como un campo en el modelo principal.

En su lugar, podría tener un campo booleano is_favourite en el Niño.

Esto puede ayudar: https://github.com/anentropic/django-exclusivebooleanfield

De esa manera, evitaría todo el problema de asegurarse de que los niños solo puedan ser los favoritos de los padres a los que pertenecen.

El código de vista sería ligeramente diferente, pero la lógica de filtrado sería sencilla.

En el administrador, incluso podría tener un modelo en línea para niños que expusiera la casilla de verificación is_favourite (si solo tiene unos pocos hijos por padre) de lo contrario, el administrador tendría que hacerlo desde el lado del niño.

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
    ]
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top