web-dev-qa-db-fra.com

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 affichez le ToppingForm, il vous permet de choisir les pizzas sur lesquelles les garnitures se déroulent et tout est juste dandy.

Mes questions sont les suivantes: comment définir un ModelForm pour pizza qui me permet de profiter de la relation plusieurs-à-plusieurs entre pizza et nappage et me permet de choisir ce que les garnitures vont sur la pizza?

71
theycallmemorty

Je suppose que vous auriez ici à ajouter un nouveau ModelMultipleChoiceField à votre PizzaForm, et à lier manuellement ce champ de formulaire avec le champ modèle, comme Django ne le fera pas faites-le automatiquement pour vous.

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

Ce PizzaForm peut ensuite être utilisé partout, même dans l'administrateur:

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

Remarque

La méthode save() peut être un peu trop verbeuse, mais vous pouvez la simplifier si vous n'avez pas besoin de prendre en charge la situation commit=False, Ce sera alors comme ça:

def save(self):
  instance = forms.ModelForm.save(self)
  instance.topping_set.clear()
  instance.topping_set.add(*self.cleaned_data['toppings'])
  return instance
121
Clément

Je ne suis pas certain d'avoir la question à 100%, donc je vais courir avec cette hypothèse:

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

Dans ce cas, votre meilleur pari est une table de relations, qui Django supporte assez bien. Cela 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>'
15
Jack M.

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 avec du fromage, une pizza avec du fromage et des tomates, ...".

Bien sûr, il est possible de faire fonctionner le formulaire à votre guise mais j'irais avec:

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

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

Quelques exemples 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)
3
Hoang HUA

Je ne sais pas si c'est ce que vous cherchez, mais savez-vous que Pizza a le topping_set attribut? En utilisant cet attribut, vous pouvez facilement ajouter une nouvelle garniture dans votre ModelForm.

new_pizza.topping_set.add(new_topping)
2
buckley

Nous avons eu un problème similaire dans notre application, qui utilisait Django admin. Il existe de nombreuses relations entre les utilisateurs et les groupes et on ne peut pas facilement ajouter des utilisateurs à un groupe. J'ai créé un - patch pour Django, cela fait cela, mais il n'y a pas beaucoup d'attention ;-) Vous pouvez le lire et essayer d'appliquer une solution similaire à votre problème de pizza/topping. De cette façon, étant à l'intérieur d'une garniture, vous pouvez facilement ajouter des pizzas connexes ou vice versa.

2
gruszczy

J'ai fait quelque chose de similaire basé sur le code de Clément avec un formulaire d'administration utilisateur:

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