Django ModelForms - 'instance' not working as expected

Chris Lawlor picture Chris Lawlor · Oct 6, 2010 · Viewed 7.8k times · Source

I have a modelform that will either create a new model or edit an existing one - this is simple and should work, but for some reason I'm getting a new instance every time.

The scenario is this is the first step in an ecommerce order. The user must fill out some info describing the order (which is stored in the model). I create the model, save it, then redirect to the next view for the user to enter their cc info. I stick the model in the session so I don't have to do a DB lookup in the next view. There is a link in the template for the second (cc info) view that lets the user go back to the first view to edit their order.

# forms.py

class MyForm(forms.ModelForm):
    class Meta:
        fields = ('field1', 'field2')
        model = MyModel

# views.py

def create_or_update(request):
    if request.method == 'POST':
        form = MyForm(request.POST)
        if form.is_valid():
            m = form.save(commit=False)
            # update some other fields that aren't in the form
            m.field3 = 'blah'
            m.field4 = 'blah'
            m.save()
            request.session['m'] = m
            return HttpResponseRedirect(reverse('enter_cc_info'))
        # invalid form, render template
        ...
    else:
        # check to see if we're coming back to edit an existing model
        # this part works, I get an instance as expected
        m = request.session.get('m', None)
        if m:
            instance = get_object_or_None(MyModel, id=m.id)
            if instance:
                form = MyForm(instance=instance)
            else:
                # can't find it in the DB, but it's in the session
                form = MyForm({'field1': m.field1, 'field2': m.field2})
        else:
            form = MyForm()

    # render the form
    ...

If I step through in the debugger when I go back to the view to edit an order that the form is created with the instance set to the previously created model, as expected. However, when the form is processed in the subsequent POST, it creates a new instance of the model when form.save() is called.

I believe this is because I've restricted the fields in the form, so there is nowhere in the rendered HTML to store the id (or other reference) to the existing model. However, I tried adding both a 'pk' and an 'id' field (not at the same time), but then my form doesn't render at all.

I suspect I'm making this more complicated than it needs to be, but I'm stuck at the moment and could use some feedback. Thanks in advance.

Answer

Manoj Govindan picture Manoj Govindan · Oct 6, 2010

This is interesting. Here is my stab at it. Consider this line:

form = MyForm(request.POST)

Can you inspect the contents of request.POST? Specifically, check if there is any information regarding which instance of the model is being edited. You'll find that there is none. In other words, each time you save the form on POST a new instance will be created.

Why does this happen? When you create a form passing the instance=instance keyword argument you are telling the Form class to return an instance for an instance of the model. However when you render the form to the template, this information is used only to fill in the fields. That is, the information about the specific instance is lost. Naturally when you post pack there is way to connect to the old instance.

How can you prevent this? A common idiom is to use the primary key as part of the URL and look up an instance on POST. Then create the form. In your case this would mean:

def create_or_update(request, instance_id):
#                             ^^^^^ 
#                             URL param
    if request.method == 'POST':
        instance = get_object_or_None(Model, pk = instance_id)
        # ^^^^^
        # Look up the instance

        form = MyForm(request.POST, instance = instance)
        #                           ^^^^^^^
        #                           pass the instance now.
        if form.is_valid():
              ....