83 votes

Django ModelForm pour les champs plusieurs à plusieurs

Considérez les modèles et la forme suivants:

 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
 

Lorsque vous visualisez le ToppingForm, il vous permet de choisir les pizzas qui vont avec les garnitures et que tout est dandy.

Ma question est la suivante: comment définir un ModelForm pour Pizza qui me permet de tirer parti de la relation plusieurs à plusieurs entre Pizza et Topping et me permet de choisir les ingrédients de Toppings sur la pizza?

137voto

Clément Points 4224

Je suppose que vous avez ici pour ajouter un nouveau ModelMultipleChoiceField votre PizzaForm, et manuellement lien de champ de formulaire avec le champ de modèle, que Django n'allons pas le faire automatiquement pour vous.

L'extrait de code suivant peut être utile :

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 'instance' in kwargs:
            # 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()
           for topping in self.cleaned_data['toppings']:
               instance.topping_set.add(topping)
        self.save_m2m = save_m2m

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

        return instance

Cette PizzaForm peut ensuite être utilisé partout, même dans l'admin :

# 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)

Note

L' save() méthode peut-être un peu trop verbeux, mais vous pouvez simplifier si vous n'avez pas besoin de soutenir l' commit=False situation, il sera alors comme ça :

def save(self):
  instance = forms.ModelForm.save(self)
  instance.topping_set.clear()
  for topping in self.cleaned_data['toppings']:
    instance.topping_set.add(topping)

17voto

Jack M. Points 8224

Je ne suis pas certain que je reçois la question à 100%, donc je vais courir avec cette hypothèse:

Chaque Pizza peut avoir de nombreux Toppings. Chaque Topping peut avoir de nombreux Pizzas. Mais si un Topping est ajouté à un Pizzaque Topping alors automatiquement aura un Pizza, et vice versa.

Dans ce cas, votre meilleur pari est un tableau de relations, qui Django supporte assez bien. Il pourrait ressembler à ceci:

models.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

forms.py

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

Exemple:

>>> 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>'

7voto

Felix Kling Points 247451

Pour être honnête, je mettrais la relation plusieurs à plusieurs dans le modèle Pizza . Je pense que cela se rapproche de la réalité. Imaginez une personne qui commande plusieurs pizzas. Il ne dirait pas "Je voudrais du fromage sur la pizza un et deux et des tomates sur la pizza un et trois" mais probablement "Une pizza au fromage, une pizza au fromage et aux tomates, ...".

Bien sûr, il est possible que le formulaire fonctionne comme vous le souhaitez, mais je choisirais:

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

4voto

Hoang HUA Points 46

Un autre moyen simple d'y parvenir est de créer une table intermédiaire et en ligne champs pour y arriver. Veuillez vous référer à cette https://docs.djangoproject.com/en/1.2/ref/contrib/admin/#working-with-many-to-many-intermediary-models

Un exemple de code ci-dessous

models.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)

2voto

gruszczy Points 14097

Nous avons eu le même problème dans notre application, qui a utilisé django admin. Il y a beaucoup beaucoup de relation entre les utilisateurs et les groupes, et on ne peut pas ajouter des utilisateurs à un groupe. J'ai créé un patch pour django, qui fait cela, mais il n'y a pas beaucoup d'attention à lui ;-) Vous pouvez le lire et d'essayer d'appliquer la même solution à votre pizza/garniture de problème. Cette façon d'être à l'intérieur d'une garniture, vous pouvez facilement ajouter des pizzas ou vice versa.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X