python - queryset - ¿Cómo restrinjo las opciones de claves externas a objetos relacionados solo en django?
models py python (10)
¿Desea restringir las opciones disponibles en la interfaz de administrador al crear / editar una instancia de modelo?
Una forma de hacerlo 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, de niño a padre aquí.
Tengo una relación extranjera 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 los hijos cuyo padre es él 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 administrador no enumere ningún elemento secundario.
@Ber: He agregado la 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 yo 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 elecció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í.
Actualización: ForeignKey.limit_choices_to permite especificar una constante, un objeto invocable o un objeto Q para restringir las opciones permitidas para la clave. Obviamente, una constante no sirve de nada, ya que no sabe nada sobre los objetos involucrados.
Usar un método invocable (función o clase o cualquier objeto invocable) parece más prometedor. Sin embargo, el problema de cómo acceder a la información necesaria del objeto HttpRequest permanece. Usar el almacenamiento local de subprocesos puede ser una solución.
2. Actualización: Esto es lo que funcionó para mí:
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 hilo.
A continuación, hay un método de clase en el modelo que lee la variable local de subprocesos 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 ID posibles si no se seleccionó ninguno para que las páginas de administración normales funcionen correctamente.
El campo clave externa 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 al "yo" aquí.
Así no es como funciona django. Solo crearías la relación en una dirección.
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 intentabas acceder a los hijos del padre, harías parent_object.child_set.all()
. Si establece un related_name en el campo myparent, entonces eso es a lo que se referiría. Ej: related_name=''children''
, entonces harías parent_object.children.all()
Estoy tratando de hacer algo similar. Parece que todos los que dicen "solo deberías tener una clave foránea en una dirección" quizás hayan entendido mal lo que intentas hacer.
Es una pena que limit_choices_to = {"myparent": "self"} que quisiste hacer no funcione ... eso hubiera sido simple y limpio. Lamentablemente, el "yo" no se evalúa y se procesa 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, por desgracia, eso da un error porque la función no pasa un auto arg :(
Parece que la única forma es poner la lógica en todas las formas que usan este modelo (es decir, pasar un conjunto de preguntas a las opciones para su campo de formulario). Lo cual es fácil de hacer, pero sería más SECO tener esto a nivel de modelo. Su anulación del método de guardado del modelo parece una buena manera de evitar que las opciones no válidas lleguen a su fin.
Actualizar
Ver mi respuesta posterior para otra forma https://.com/a/3753916/202168
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
La nueva forma "correcta" de hacerlo, al menos desde Django 1.1, es anulando el AdminModel.formfield_for_foreignkey (self, db_field, request, ** kwargs).
Para aquellos que no quieren seguir el siguiente enlace, hay una función de ejemplo que está cerca de 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)
No estoy seguro de cómo obtener el objeto actual que se está editando. Espero que esté realmente en algún lado, pero no estoy seguro.
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 para muchas 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)
Un enfoque alternativo sería no tener ''favouritechild'' fk como un campo en el modelo Parent.
En cambio, podría tener un campo booleano is_favourite en el Niño.
Esto puede ayudar: https://github.com/anentropic/django-exclusivebooleanfield
De esta forma evitaría todo el problema de garantizar que los niños solo puedan convertirse en 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 directa.
En el administrador, incluso podría tener un en línea para los modelos Child que expuso la casilla de verificación is_favourite (si solo tiene unos pocos hijos por padre), de lo contrario, el administrador tendría que hacerse 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
]