1. Skip to navigation
  2. Skip to content

Simpler NewForms Save

In my last post on Customizing NewForms Nick pointed out that I had forgot my save() method in the form class definition. The problem was a simple copy and paste error but as I began to copy in the save() method it occurred to me that there might be a simpler way to handle saving in NewForms.

The Normal Save

Normally your form class save() method will look something like this:


def save(self, id=None, commit=True):
    if id is None:
        instance = Link()
    else:
        instance = Link.objects.get(pk=id)

    instance.title = self.cleaned_data['title']
    instance.url   = self.cleaned_data['url']
    instance.notes = self.cleaned_data['notes']

    if commit:
        instance.save()

    return instance

In the above situation I’m using a combined add/edit save method. If the id is passed in then I’m using it to look up the object and update the attributes accordingly. If there is no id then I’m just creating a new instance and saving it to the database, provided the commit option is set.

You may prefer to do the lookup in the view and pass in the instance, but either way you’re going to end up with something that resembles the above code in some form or fashion.

At this point in the process the validation has already occurred. We could call the is_valid() method prior to doing the save if we wanted to, or we can rely on the view to handle that for us.

The important thing to notice about the above code is that it is very generic in nature. If we were working with a Foo model then the code would look like the following:


def save(self, id=None, commit=True):
    if id is None:
        instance = Foo()
    else:
        instance = Foo.objects.get(pk=id)

    instance.attribute = self.cleaned_data['attribute']
    ...

    if commit:
        instance.save()

    return instance

Simplifying the Save

It occured to me that I might be able to simplify this process a bit. It then occured to me that the NewForms source code must be doing this already in order to set up the save() method code for the form_for_model and form_for_instance convienence methods.

After digging in a bit I figured out that there is a save_instance method that essentially uses some meta programming to simplify the above code. save_instance is part of the django.newforms.models module. By importing the method and passing in the appropriate values we get a lot of functionality for free. Now my save() method looks like the following:


def save(self, id=None, commit=True):
    if id is None:
        instance = Link()
    else:
        instance = Link.objects.get(pk=id)
    return forms.models.save_instance(self, instance, commit=commit)

This same structure will work for any form we would be saving.

Notice that we’re just passing in an instance of our model and whatever we have specified for the commit option. the save_instance method handles the rest for us. The save_instance method actually accepts a couple other arguments, so if you’re inclined you can accept and pass those along as well. Here’s a list of the arguments:

  • form – an instance of the form that contains the cleaned_data values.
  • instance – The instance model that will be updated with the new values. Note in the case of a new object, we’re just instantiating the model and passing it in.
  • fields (None) – A restricted list of fields that should be updated. This would be in the case where the fields we want to update are not all the fields in the form instance.
  • fail_message (‘saved’) – If there are errors on the form instance a ValueError exception is raised with the following formatted message:

"The %s could not be %s because the data didn't validate." % (opts.object_name, fail_message)) 

The fail_message is used to format the message. (In case you’re curious opts.object_name is the models meta object_name, which is generally just the model class name.) Usually the default will suffice.

  • commit (True) – If this is true then the changes to the model instance will be saved to the database, otherwise the instance model is updated but the changes are not saved.

Note, the save_instance method always returns back the model instance with the updated values.

Possible Further Enhancements

The above is nice, but it would be great to take it one step further. I’d like to see NewForms automatically add a default save() method to the Form class. Although truthfully it is not something likely to happen since the process of loading the instance could change widely. The above solution is a good compromise in keeping our code DRY, yet still providing us with the flexibility we need.

Update – Some of the folks in the Django IRC Channel pointed me to a discussion on on the discussion group. This looks like it has the potential for addressing this issue by holding an instance of the model in the Form, along with doing away with form_for_model and form_for_instance methods. We’ll have to watch this one closely.

Other Approaches

Another approach to solving this problem, by some, has been to coerce form_for_instance, and using it’s save() method. If you’ve looked into the options for form_for_instance you will have noticed that it accepts a form option. Using the form option you can call form_for_instance and pass in your custom form class. For instance:


form = forms.form_for_instance(Entry, form=EntryForm)

The problem with this approach is that the form_for_instance method will not load up the custom form class with the instance initial data. (See Changeset 3632 for more information on this). So the solution is to do something sort of funky, such as:


instance = Entry.objects.get(pk=id)
Form = forms.form_for_instance(instance, form=EntryForm)

form_instance = Form(request.POST) 
if form_instance.is_valid():
  form_instance.save()

Warning I didn’t test the above but pulled it out of some code I’ve seen flying around. It’s just too ugly for my tastes, and so the solution I’ve proposed above will suffice for now.

I’ve only been playing around with this approach for a couple of hours so I haven’t had a chance to really bang on it. If I notice some problems I’ll post more about it here. Until then, enjoy.


Discussion