developer tip

다 대다 필드를위한 Django ModelForm

optionbox 2020. 10. 29. 08:01
반응형

다 대다 필드를위한 Django ModelForm


다음 모델과 형식을 고려하십시오.

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

ToppingForm을 볼 때 토핑에 어떤 피자를 넣을지 선택할 수 있으며 모든 것이 멋지게 보입니다.

내 질문은 다음과 같습니다. Pizza와 Topping 사이의 다 대다 관계를 활용하고 피자에서 토핑을 선택할 수 있도록하는 Pizza에 대한 ModelForm을 어떻게 정의합니까?


나는 새를 추가 하시려면 이곳을 것 같아요 ModelMultipleChoiceField당신에게 PizzaForm, 그리고 장고 당신을 위해 자동으로 수행되지 않으므로 수동 모델 필드가 양식 필드를 연결합니다.

다음 스 니펫이 도움이 될 수 있습니다.

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

이것은 PizzaForm다음 심지어 관리자에서, 모든 곳에서 사용할 수 있습니다 :

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

노트

save()방법은 너무 자세한 조금 수도 있지만 당신은 지원이 필요하지 않은 경우 당신은 그것을 단순화 할 수 있습니다 commit=False상황이 그와 같이 될 것입니다 :

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

질문이 100 %인지 확실하지 않으므로 다음 가정으로 실행하겠습니다.

각각 Pizza은 많은를 가질 수 있습니다 Topping. 각각 Topping은 많은를 가질 수 있습니다 Pizza. A는하지만 ToppingA를 추가 Pizza, 그 Topping후 자동적으로이있을 것이다 Pizza, 그리고 반대로 그.

이 경우 가장 좋은 방법은 Django가 매우 잘 지원하는 관계 테이블입니다. 다음과 같이 보일 수 있습니다.

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

예:

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

To be honest, I would put the many-to-many relation into the Pizza model. I think this closer to reality. Imagine a person that orders several pizzas. He wouldn't say "I would like cheese on pizza one and two and tomatoes on pizza one and three" but probably "One pizza with cheese, one pizza with cheese and tomatoes,...".

Of course it is possible to get the form working in your way but I would go with:

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

Another simple way to achieve this is to create an intermediary table and using inline fields to get it done. Please refer to this https://docs.djangoproject.com/en/1.2/ref/contrib/admin/#working-with-many-to-many-intermediary-models

Some sample code below

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)

I'm not sure if this is what your looking for, but are you aware that Pizza has the topping_set attribute? Using that attribute you could easily add a new topping in your ModelForm.

new_pizza.topping_set.add(new_topping)

We had similar problem in our app, which used django admin. There is many to many relation between users and groups and one can't easily add users to a group. I have created a patch for django, that does this, but there isn't much attention to it ;-) You can read it and try to apply similar solution to your pizza/topping problem. This way being inside a topping, you can easily add related pizzas or vice versa.


I did something similar based in code of Clément with a user admin form:

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

참고URL : https://stackoverflow.com/questions/2216974/django-modelform-for-many-to-many-fields

반응형