开发者

Is save_m2m() required in the Django forms save() method when commit=False?

开发者 https://www.devze.com 2023-03-28 18:06 出处:网络
The docs seem pretty firm that this is indeed the case.... https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#the-save-method

The docs seem pretty firm that this is indeed the case....

https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#the-save-method

And I specifically refer to this section:

Another side effect of using commit=False is seen when your model has a many-to-many relation开发者_JS百科 with another model. If your model has a many-to-many relation and you specify commit=False when you save a form, Django cannot immediately save the form data for the many-to-many relation. This is because it isn't possible to save many-to-many data for an instance until the instance exists in the database.

To work around this problem, every time you save a form using commit=False, Django adds a save_m2m() method to your ModelForm subclass. After you've manually saved the instance produced by the form, you can invoke save_m2m() to save the many-to-many form data.

I am pretty new to django and stumbled upon this information yesterday.

However, I have a view where I do not invoke the save_m2m() method but it does in fact save the m2m data.

Here is my view:

class SubscriberCreateView(AuthCreateView):
    model = Subscriber
    template_name = "forms/app.html"
    form_class = SubscriberForm
    success_url = "/app/subscribers/"

    def get_form_kwargs(self):
        kwargs = super(SubscriberCreateView, self).get_form_kwargs()
        kwargs.update({'user': self.request.user})
        return kwargs

    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.user = self.request.user
        try:
            self.object.full_clean()
        except ValidationError:
            form._errors["email"] = ErrorList([u"This subscriber email is already in your account."])
            return super(SubscriberCreateView, self).form_invalid(form)
        return super(SubscriberCreateView, self).form_valid(form)

My model:

class Subscriber(models.Model):

    STATUS_CHOICES = (
        (1, ('Subscribed')),
        (2, ('Unsubscribed')),
        (3, ('Marked as Spam')),
        (4, ('Bounced')),
        (5, ('Blocked')),
        (6, ('Disabled')),
    )

    user = models.ForeignKey(User)
    status = models.IntegerField(('status'), choices=STATUS_CHOICES, default=1)
    email = models.EmailField()
    subscriber_list = models.ManyToManyField('SubscriberList')
    first_name = models.CharField(max_length=70, blank=True)
    last_name = models.CharField(max_length=70, blank=True)
    phone = models.CharField(max_length=20, blank=True)
    facebook_id = models.CharField(max_length=40, blank=True)
    twitter_id = models.CharField(max_length=40, blank=True)
    address1 = models.CharField(max_length=100, blank=True)
    address2 = models.CharField(max_length=100, blank=True)
    postcode = models.CharField(max_length=10, blank=True)
    city = models.CharField(max_length=30, blank=True)
    country = models.CharField(max_length=30, blank=True)
    date_joined = models.DateTimeField(auto_now_add=True)
    date_updated = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = (
            ('user', 'email',),
        )
    
    def __unicode__(self):
        return self.email

My form:

class SubscriberForm(ModelForm):
    def __init__(self, user, *args, **kwargs):
        super (SubscriberForm, self).__init__(*args, **kwargs)
        self.fields['subscriber_list'].queryset = SubscriberList.objects.filter(user=user)
    
    class Meta:
        model = Subscriber
        exclude = ('user', 'facebook_id', 'twitter_id')

Why does my view work, then? (meaning, the m2m relation of one of the fields in the form is in fact saved when the form is processed.)


One of the parent classes is performing the full save of the model object and its m2m relations. I can't know for sure because I don't have the declaration of AuthCreateView, but the naming convention indicates that it stems from "CreateView". If so, the inheritance of your View goes like this, SubscriberCreateView -> AuthCreateView -> CreateView -> BaseCreateView -> ModelFormMixin. ModelFormMixin has a form_valid() method that you are (probably) calling with super().

Here's the entire method from Django 1.4:

def form_valid(self, form):
    self.object = form.save()
    return super(ModelFormMixin, self).form_valid(form)

So there you have it. However, let me point some potential confusion. @Wogan is astute when pointing out that you don't save your object. The way your code stands, you use your unsaved model instance for validation and then it is discarded because ModelFormMixin re-assigns self.object.

  1. This means self.object might not be what you expect if you access it later on.
  2. You lose the self.object.user information. (Because user is a required field on the Model and you exclude it in the Form, I would expect the save() to fail. So the parent AuthCreateView must be doing something. Of course it might be handling the entire save() and never hitting ModelFormMixin at all.)

To avoid this confusion, simply don't assign your instance to self.object. Perhaps: validate_obj = form.save(commit=False)


It's a simple answer if we know how to use save(commit=False).

save method with commit=False does not change your database:

"If you call save() with commit=False, then it will return an object that hasn't yet been saved to the database. In this case, it's up to you to call save() on the resulting model instance. This is useful if you want to do custom processing on the object before saving it, or if you want to use one of the specialized model saving options."

So in the case of many-to-many relationship it's impossible to save m2m data without saving an object to a database. Usual save( commit=False ) can change an object as you saved part of data (not data that assign to m2m relationship). You really can't store m2m relationship in memory only.

If you want to work with that m2m data of model object after save(commit=False) save_m2m() required. You should do this when you use save(commit=False) otherwise you may get an intermediate object after save(commit=False) that does not correspond to your database (or database model) properly. Sometimes it may be normal (if you don't touch the data which are involved in processing of model's m2m part). To restore the conformity call save_m2m any time you call save(commit=False).

View the save_m2m implementation:

def save_m2m():
    cleaned_data = form.cleaned_data
    for f in opts.many_to_many:
        if fields and f.name not in fields:
            continue
        if f.name in cleaned_data:
            f.save_form_data(instance, cleaned_data[f.name])


I noticed that you do not actually save the object that you get via your form.save(commit=False) call. So it appears to me that your data is actually being saved somewhere else - probably in AuthCreateView's form_valid method (or another ancestor class). This would explain why the many-to-many objects are being correctly saved.

0

精彩评论

暂无评论...
验证码 换一张
取 消