48

I've made a nice form, and a big complicated 'add' function for handling it. It starts like this...

def add(req):
    if req.method == 'POST':
        form = ArticleForm(req.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = req.user
            # more processing ...

Now I don't really want to duplicate all that functionality in the edit() method, so I figured edit could use the exact same template, and maybe just add an id field to the form so the add function knew what it was editing. But there's a couple problems with this

  1. Where would I set article.id in the add func? It would have to be after form.save because that's where the article gets created, but it would never even reach that, because the form is invalid due to unique constraints (unless the user edited everything). I can just remove the is_valid check, but then form.save fails instead.
  2. If the form actually is invalid, the field I dynamically added in the edit function isn't preserved.

So how do I deal with this?

mpen
  • 272,448
  • 266
  • 850
  • 1,236

2 Answers2

114

If you are extending your form from a ModelForm, use the instance keyword argument. Here we pass either an existing instance or a new one, depending on whether we're editing or adding an existing article. In both cases the author field is set on the instance, so commit=False is not required. Note also that I'm assuming only the author may edit their own articles, hence the HttpResponseForbidden response.

from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render, reverse


@login_required
def edit(request, id=None, template_name='article_edit_template.html'):
    if id:
        article = get_object_or_404(Article, pk=id)
        if article.author != request.user:
            return HttpResponseForbidden()
    else:
        article = Article(author=request.user)

    form = ArticleForm(request.POST or None, instance=article)
    if request.POST and form.is_valid():
        form.save()

        # Save was successful, so redirect to another page
        redirect_url = reverse(article_save_success)
        return redirect(redirect_url)

    return render(request, template_name, {
        'form': form
    })

And in your urls.py:

(r'^article/new/$', views.edit, {}, 'article_new'),
(r'^article/edit/(?P<id>\d+)/$', views.edit, {}, 'article_edit'),

The same edit view is used for both adds and edits, but only the edit url pattern passes an id to the view. To make this work well with your form you'll need to omit the author field from the form:

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        exclude = ('author',)
Daniel Naab
  • 22,690
  • 8
  • 54
  • 55
  • Yes, it's a `ModelForm`. I needed `commit=False` for other reasons. An article is composed up of a whole bunch of stuff (including some m2m relations). I don't *think* it wanted to work with `instance`. I'll give this a try though. – mpen Dec 06 '09 at 07:06
  • 1
    In that case, I'd suggest putting the m2m relations (et al) saving/validation in the form instead of the view... either override the save method or possibly, look into formsets. I guess it depends on the context of what you're working with... – Daniel Naab Dec 06 '09 at 07:29
  • 5
    Great and thorough example! Thanks! I learned more than the just the solution to this question. – hobbes3 May 26 '12 at 12:21
  • 3
    Great Answer! I think its missing an example of the template action form tag. How could I know when to call article_new or article_edit in the template? Need to check form.instance.id ? – nsbm Aug 01 '12 at 13:52
  • 3
    @DanielNaab I know this is very old but I'm so close to getting it working.. what do I set the form `action` attribute to? Would it be `r'^article/new/$'` or `r'^article/edit/(?P\d+)/$'`? Thanks! – sdweldon Aug 01 '14 at 14:19
  • 4
    @stmyd Technically, you can just do `action="."`, because you want to post to the same URL you're rending the form from. – Daniel Naab Aug 01 '14 at 18:17
  • @DanielNaab: from django docs 1.5 on ward, I always use url(r'^(?P\d+)/$', views.detail, name='detail'). For your urls.py like (r'^article/new/$', views.edit, {}, 'article_new'), could you please advise what {} and 'article_new' represent? Secondly, is it correct that both new and edit action use the same template which is 'article_edit_template.html'? Thank you! – Harry Feb 24 '15 at 03:35
  • @learnJQueryUI: Those are the kwargs to pass into the view and the view name. All the parameters in the tuples (when not using the url() function) are passed as parameters to RegexURLPattern: https://github.com/django/django/blob/master/django/core/urlresolvers.py#L201-L248 – Daniel Naab Feb 25 '15 at 02:53
  • Great write-up, very helpful. A little thing missing from `urls.py`, namely the template to pass to the view function as an argument. Please consider updating with: `{ 'template_name' : 'article_new_template.html' }` – Roy Prins Feb 11 '17 at 15:13
3

You can have hidden ID field in form and for edit form it will be passed with the form for add form you can set it in req.POST e.g.

formData =  req.POST.copy()
formData['id'] = getNewID()

and pass that formData to form

Anurag Uniyal
  • 85,954
  • 40
  • 175
  • 219