2009-08-04 21 views
33

Tengo un objeto con una relación muchos a muchos con otro objeto.
En el Administrador de Django esto da como resultado una lista muy larga en un cuadro de selección múltiple.Filtro Cuadro ManyToMany en Django Admin

Me gustaría filtrar la relación ManyToMany, así que solo busco categorías disponibles en la ciudad que el cliente haya seleccionado.

¿Esto es posible? ¿Tendré que crear un widget para eso? Y si es así, ¿cómo copio el comportamiento del campo estándar ManyToMany, ya que también me gustaría la función filter_horizontal?

Estos son mis modelos simplificados:

class City(models.Model): 
    name = models.CharField(max_length=200) 


class Category(models.Model): 
    name = models.CharField(max_length=200) 
    available_in = models.ManyToManyField(City) 


class Customer(models.Model): 
    name = models.CharField(max_length=200) 
    city = models.ForeignKey(City) 
    categories = models.ManyToManyField(Category) 

Respuesta

36

Ok, esta es mi solución usando las clases anteriores. Agregué varios filtros más para filtrarlo correctamente, pero quería que el código fuera legible aquí.

Esto es exactamente lo que estaba buscando, y me encontré mi solución aquí: http://www.slideshare.net/lincolnloop/customizing-the-django-admin#stats-bottom (diapositiva 50)

Añadir lo siguiente a mi admin.py:

class CustomerForm(forms.ModelForm): 
    def __init__(self, *args, **kwargs): 
     super(CustomerForm, self).__init__(*args, **kwargs) 
     wtf = Category.objects.filter(pk=self.instance.cat_id); 
     w = self.fields['categories'].widget 
     choices = [] 
     for choice in wtf: 
      choices.append((choice.id, choice.name)) 
     w.choices = choices 


class CustomerAdmin(admin.ModelAdmin): 
    list_per_page = 100 
    ordering = ['submit_date',] # didnt have this one in the example, sorry 
    search_fields = ['name', 'city',] 
    filter_horizontal = ('categories',) 
    form = CustomerForm 

Esto filtra las categorías" "lista sin eliminar ninguna funcionalidad! (es decir: todavía puedo tener mi querido filter_horizontal :))

El ModelForms es muy poderoso, estoy un poco sorprendido de que no esté cubierto más en la documentación/libro.

+0

Noté que después de agregar este código en un proyecto, tengo que el cuadro de opciones seleccionado (estaría bajo las categorías elegidas elegidas en su ejemplo) está vacío incluso después de seleccionar una opción del campo 'Categorías disponibles'. ¿Me perdí algo en la implementación de esto? – Silfheed

+0

Reducción adicional mediante la comprensión de listas: self.fields ['categories']. Widget.choices = [(choice.id, choice.name) para elegir en wtf] –

+2

¿qué es 'cat_id'? – Sevenearths

0
Category.objects.filter(available_in=cityobject) 

Eso debería hacerlo. La vista debe tener la ciudad que el usuario seleccionó, ya sea en la solicitud o como un parámetro para esa función de vista.

+0

Pero estoy hablando del administrador django, ¿estás diciendo que debería duplicar la vista estándar y agregar lo anterior? – schmilblick

+0

Ah, me extrañé por completo la parte "Django Admin" del título de su pregunta. Sigo pensando que este es el enfoque correcto, aunque no estoy exactamente seguro de dónde lo pondrías, o si esto es posible. – AlbertoPL

1

Dado que selecciona la ciudad y las categorías del cliente de la misma forma, necesitará algunos javascript para reducir dinámicamente el selector de categorías a solo las categorías disponibles en la ciudad seleccionada.

+0

No me entusiasma iterar en decenas de miles de elementos DOM con javascript y comparando con otra lista enorme. Diría que Javascript definitivamente no es el camino a seguir, esto tiene que hacerse al final cuando se seleccionan las categorías de la base de datos. – schmilblick

12

Por lo que puedo entender, es que básicamente desea filtrar las opciones mostradas según algunos criterios (categoría según la ciudad).

Puedes hacer exactamente eso usando el atributo limit_choices_to de models.ManyToManyField. Por lo que cambiar su definición como modelo ...

class Customer(models.Model): 
    name = models.CharField(max_length=200) 
    city = models.ForeignKey(City) 
    categories = models.ManyToManyField(Category, limit_choices_to = {'available_in': cityId}) 

Esto debería funcionar, como limit_choices_to, está disponible para este propósito.

Pero hay que tener en cuenta que limit_choices_to no tiene ningún efecto cuando se usa en ManyToManyField con una tabla intermedia personalizada. Espero que esto ayude.

+0

¡Parece que podría funcionar! Sin embargo ... me hizo darme cuenta de que tengo que volver a modelar mis modelos :) Estoy leyendo en los documentos que el administrador no se preocupa por el limit_choices_to también, ¿cuál es su opinión sobre eso? – schmilblick

+0

Estoy tratando de hacer exactamente lo mismo de la manera descrip @sim, pero me sale un error de 'ValueError at/admin/foo/bar /: literal inválido para int() con la base 10: 'city''. ¿Hay algo que me falta con la forma de implementar este método de filtrado? – nhinkle

+0

@nhinkle Se supone que esa 'ciudad' en el valor significa Id del objeto de ciudad al que desea que se limiten las categorías. Mis disculpas. Editaré la respuesta para ser más claro. – simplyharsh

0

Como dice Ryan, tiene que haber algún javascript para cambiar dinámicamente las opciones según lo que el usuario seleccione. La solución publicada funciona si la ciudad se guarda y se vuelve a cargar el formulario de administrador, eso es cuando el filtro funciona, pero piensa en una situación donde un usuario quiere editar un objeto y luego cambia la ciudad desplegable pero las opciones en categoría no se actualizan.

4

Otra forma es con formfield_for_manytomany en Django Admin.

class MyModelAdmin(admin.ModelAdmin): 
    def formfield_for_manytomany(self, db_field, request, **kwargs): 
     if db_field.name == "cars": 
      kwargs["queryset"] = Car.objects.filter(owner=request.user) 
     return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs) 

Considerando que los "autos" son el campo ManyToMany.

Compruebe this link para obtener más información.