Django ModelForm para campos de varios a varios

79

Considere los siguientes modelos y formas:

class Pizza(models.Model):
    name = models.CharField(max_length=50)

class Topping(models.Model):
    name = models.CharField(max_length=50)
    ison = models.ManyToManyField(Pizza, blank=True)

class ToppingForm(forms.ModelForm):
    class Meta:
        model = Topping

Cuando ve el ToppingForm, le permite elegir en qué pizzas van los ingredientes y todo es excelente.

Mi pregunta es: ¿Cómo defino un ModelForm para pizza que me permita aprovechar la relación de muchos a muchos entre Pizza y Topping y me permita elegir qué Toppings van a la pizza?

ellos llaman la memoria
fuente
Entonces, de sus comentarios a continuación: Cada uno Pizzapuede tener muchos Toppings. Cada uno Toppingpuede tener muchos Pizzas. Pero si agrego a Toppinga Pizza, ¿eso Pizzaautomáticamente tiene a Topping, y viceversa?
Jack M.

Respuestas:

132

Supongo que tendría que agregar uno nuevo ModelMultipleChoiceFielda su PizzaFormy vincular manualmente ese campo de formulario con el campo del modelo, ya que Django no lo hará automáticamente por usted.

El siguiente fragmento puede resultar útil:

class PizzaForm(forms.ModelForm):
    class Meta:
        model = Pizza

    # Representing the many to many related field in Pizza
    toppings = forms.ModelMultipleChoiceField(queryset=Topping.objects.all())

    # Overriding __init__ here allows us to provide initial
    # data for 'toppings' field
    def __init__(self, *args, **kwargs):
        # Only in case we build the form from an instance
        # (otherwise, 'toppings' list should be empty)
        if kwargs.get('instance'):
            # We get the 'initial' keyword argument or initialize it
            # as a dict if it didn't exist.                
            initial = kwargs.setdefault('initial', {})
            # The widget for a ModelMultipleChoiceField expects
            # a list of primary key for the selected data.
            initial['toppings'] = [t.pk for t in kwargs['instance'].topping_set.all()]

        forms.ModelForm.__init__(self, *args, **kwargs)

    # Overriding save allows us to process the value of 'toppings' field    
    def save(self, commit=True):
        # Get the unsave Pizza instance
        instance = forms.ModelForm.save(self, False)

        # Prepare a 'save_m2m' method for the form,
        old_save_m2m = self.save_m2m
        def save_m2m():
           old_save_m2m()
           # This is where we actually link the pizza with toppings
           instance.topping_set.clear()
           instance.topping_set.add(*self.cleaned_data['toppings'])
        self.save_m2m = save_m2m

        # Do we need to save all changes now?
        if commit:
            instance.save()
            self.save_m2m()

        return instance

Esto PizzaFormse puede usar en todas partes, incluso en el administrador:

# yourapp/admin.py
from django.contrib.admin import site, ModelAdmin
from yourapp.models import Pizza
from yourapp.forms import PizzaForm

class PizzaAdmin(ModelAdmin):
  form = PizzaForm

site.register(Pizza, PizzaAdmin)

Nota

El save()método puede ser demasiado detallado, pero puede simplificarlo si no necesita respaldar la commit=Falsesituación, entonces será así:

def save(self):
  instance = forms.ModelForm.save(self)
  instance.topping_set.clear()
  instance.topping_set.add(*self.cleaned_data['toppings'])
  return instance
Clemente
fuente
Esto se ve bien pero no entiendo del todo el código, especialmente la 'instancia', save_m2m y old_save_m2m :)
Viet
1
@Viet: en la documentación de django sobre formularios ( docs.djangoproject.com/en/dev/topics/forms/modelforms/… ), puede ver que django agrega automáticamente un save_m2mmétodo a su ModelFormcuando lo llama save(commit=False). Esto es exactamente lo que estoy haciendo aquí, agregando un save_m2mmétodo para guardar objetos y ingredientes relacionados , y este método llama al original save_m2m.
Clément
3
¿Cómo es esta solución mejor que la de Jack M., es decir, introduciendo un modelo intermedio? Esta solución parece requerir mucho más código.
mb21
¿Podría esta lógica ser reutilizable para cualquier M2M inverso, usando por ejemplo un mixin, un decorador u otra cosa?
David D.
16

No estoy seguro de obtener la pregunta al 100%, así que voy a ejecutar esta suposición:

Cada uno Pizzapuede tener muchos Toppings. Cada uno Toppingpuede tener muchos Pizzas. Pero si Toppingse agrega a a Pizza, Toppingentonces automáticamente tendrá a Pizza, y viceversa.

En este caso, su mejor opción es una tabla de relaciones, que Django admite bastante bien. Podría verse así:

modelos.py

class PizzaTopping(models.Model):
    topping = models.ForeignKey('Topping')
    pizza = models.ForeignKey('Pizza')
class Pizza(models.Model):     
    name = models.CharField(max_length=50) 
    topped_by = models.ManyToManyField('Topping', through=PizzaTopping)
    def __str__(self):
        return self.name
    def __unicode__(self):
        return self.name
class Topping(models.Model):   
    name=models.CharField(max_length=50)
    is_on = models.ManyToManyField('Pizza', through=PizzaTopping)
    def __str__(self):
        return self.name
    def __unicode__(self):
        return self.name

formularios.py

class PizzaForm(forms.ModelForm):
    class Meta:
        model = Pizza
class ToppingForm(forms.ModelForm):
    class Meta:
        model = Topping

Ejemplo:

>>> p1 = Pizza(name="Monday")
>>> p1.save()
>>> p2 = Pizza(name="Tuesday")
>>> p2.save()
>>> t1 = Topping(name="Pepperoni")
>>> t1.save()
>>> t2 = Topping(name="Bacon")
>>> t2.save()
>>> PizzaTopping(pizza=p1, topping=t1).save() # Monday + Pepperoni
>>> PizzaTopping(pizza=p2, topping=t1).save() # Tuesday + Pepperoni
>>> PizzaTopping(pizza=p2, topping=t2).save() # Tuesday + Bacon

>>> tform = ToppingForm(instance=t2) # Bacon
>>> tform.as_table() # Should be on only Tuesday.
u'<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Bacon" maxlength="50" /></td></tr>\n<tr><th><label for="id_is_on">Is on:</label></th><td><select multiple="multiple" name="is_on" id="id_is_on">\n<option value="1">Monday</option>\n<option value="2" selected="selected">Tuesday</option>\n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>'

>>> pform = PizzaForm(instance=p1) # Monday
>>> pform.as_table() # Should have only Pepperoni
u'<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Monday" maxlength="50" /></td></tr>\n<tr><th><label for="id_topped_by">Topped by:</label></th><td><select multiple="multiple" name="topped_by" id="id_topped_by">\n<option value="1" selected="selected">Pepperoni</option>\n<option value="2">Bacon</option>\n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>'

>>> pform2 = PizzaForm(instance=p2) # Tuesday
>>> pform2.as_table() # Both Pepperoni and Bacon
u'<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Tuesday" maxlength="50" /></td></tr>\n<tr><th><label for="id_topped_by">Topped by:</label></th><td><select multiple="multiple" name="topped_by" id="id_topped_by">\n<option value="1" selected="selected">Pepperoni</option>\n<option value="2" selected="selected">Bacon</option>\n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>'
Jack M.
fuente
AttributeError en / requirements / add / No se pueden establecer valores en ManyToManyField que especifica un modelo intermedio. En su lugar, utilice requirements.AssetRequirement's Manager.
Eloy Roldán Paredes
7

Para ser honesto, pondría la relación de muchos a muchos en el Pizzamodelo. Creo que esto se acerca más a la realidad. Imagínese una persona que pide varias pizzas. No diría "Me gustaría queso en la pizza uno y dos y tomates en la pizza uno y tres", sino probablemente "Una pizza con queso, una pizza con queso y tomates, ...".

Por supuesto, es posible hacer que el formulario funcione en su camino, pero yo iría con:

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
Felix Kling
fuente
5
Los modelos Pizza / Topping son solo un disfraz para mis modelos reales. El propósito de esta pregunta es porque quiero que Pizza ModelForm me permita elegir Toppings y quiero que Topping ModelForm me permita elegir Pizzas.
llaman la memoria
4

Otra forma sencilla de lograr esto es crear una tabla intermedia y usar campos en línea para hacerlo. Consulte este https://docs.djangoproject.com/en/1.2/ref/contrib/admin/#working-with-many-to-many-intermediary-models

Algún código de muestra a continuación

modelos.py

class Pizza(models.Model):
    name = models.CharField(max_length=50)

class Topping(models.Model):
    name = models.CharField(max_length=50)
    ison = models.ManyToManyField(Pizza, through='PizzaTopping')

class PizzaTopping(models.Model):
    pizza = models.ForeignKey(Pizza)
    topping = models.ForeignKey(Topping)

admin.py

class PizzaToppingInline(admin.TabularInline):
    model = PizzaTopping

class PizzaAdmin(admin.ModelAdmin):
    inlines = [PizzaToppingInline,]

class ToppingAdmin(admin.ModelAdmin):
    inlines = [PizzaToppingInline,]

admin.site.register(Pizza, PizzaAdmin)
admin.site.register(Topping, ToppingAdmin)
Hoang HUA
fuente
Supongo que esto solo tiene efecto en las páginas / formularios de administración. ¿Cómo crearía algo similar para usuarios anónimos / invitados y / o usuarios registrados para poder publicar, por ejemplo, una preferencia de pizza?
user1271930
2

No estoy seguro de si esto es lo que está buscando, pero ¿sabe que Pizza tiene el topping_setatributo? Usando ese atributo, podría agregar fácilmente un nuevo ingrediente en su ModelForm.

new_pizza.topping_set.add(new_topping)
Buckley
fuente
2

Tuvimos un problema similar en nuestra aplicación, que usaba django admin. Existe una relación de muchas a muchas entre usuarios y grupos y no se pueden agregar fácilmente usuarios a un grupo. He creado un parche para django, que hace esto, pero no se le presta mucha atención ;-) Puede leerlo e intentar aplicar una solución similar a su problema de pizza / aderezo. De esta manera, al estar dentro de un aderezo, puede agregar fácilmente pizzas relacionadas o viceversa.

gruszczy
fuente
0

Hice algo similar basado en el código de Clément con un formulario de administrador de usuario:

# models.py
class Clinica(models.Model):
  ...
  users = models.ManyToManyField(User, null=True, blank=True, related_name='clinicas')

# admin.py
class CustomUserChangeForm(UserChangeForm):
  clinicas = forms.ModelMultipleChoiceField(queryset=Clinica.objects.all())

  def __init__(self,*args,**kwargs):
    if 'instance' in kwargs:
      initial = kwargs.setdefault('initial',{})
      initial['clinicas'] = kwargs['instance'].clinicas.values_list('pk',flat=True)
    super(CustomUserChangeForm,self).__init__(*args,**kwargs)

  def save(self,*args,**kwargs):
    instance = super(CustomUserChangeForm,self).save(*args,**kwargs)
    instance.clinicas = self.cleaned_data['clinicas']
    return instance

  class Meta:
    model = User

admin.site.unregister(User)

UserAdmin.fieldsets += ( (u'Clinicas', {'fields': ('clinicas',)}), )
UserAdmin.form = CustomUserChangeForm

admin.site.register(User,UserAdmin)
usuario324541
fuente
0

También puede usar una tabla directa si desea agregar elementos que dependan de ambas claves primarias de la tabla en la relación. Muchas a muchas relaciones usan algo llamado tabla puente para almacenar cosas que dependen de ambas partes de la clave principal.

Por ejemplo, considere la siguiente relación entre Pedido y Producto en models.py

class Order(models.Model):
    date = models.DateField()
    status = models.CharField(max_length=30)

class Product(models.Model):
    name = models.CharField(max_length=50)
    desc = models.CharField(max_length=50)
    price = models.DecimalField(max_dights=7,decimal_places=2)
    qtyOnHand = models.Integer()
    orderLine = models.ManyToManyField(Order, through='OrderLine')

class OrderLine(models.Model):
    product = models.ForeignKey(Product)
    order = models.ForeignKey(Order)
    qtyOrd = models.Integer()

En su caso, lo que haría es poner ManyToMany en los ingredientes, porque le permite al usuario elegir qué ingredientes van a la pizza que desea. Solución simple pero poderosa.

Alfred Tsang
fuente