Using Django’s FormPreview with @login_required

As part of Django’s ‘batteries included’ philosophy, it comes with the FormPreview class to make it easy to automate this workflow: “Display an HTML form, force a preview, then do something with the submission.” Also provided is the @login_required decorator which makes it easy and obvious to mark functions as requiring a logged-in user. Both these features are awesome by themselves, but wouldn’t it be great to combine them and observe how much the awesomeness increases?

FormPreview Review

First of all, let’s review how to use the FormPreview class. The general procedure is:

  1. Write a newforms Form class that models the form you want to present the user. (MessageForm)
  2. Extend FormPreview and override the done method to provide a behavior for what happens once the user previews and submits the form. (MessageFormPreview)
  3. Create an instance of your FormPreview and use it as the view callback in your URL configuration ((r'^message/$', MessageFormPreview(MessageForm)))

These steps should leave us with code similar to that below and a submission form with preview at /message/.

# forms.py
from django import newforms as forms
from django.contrib.formtools.preview import FormPreview

class MessageForm(forms.Form):
    message = forms.CharField()

class MessageFormPreview(FormPreview):
    def done(self, request, cleaned_data):
        # Do something with the cleaned_data, then redirect
        # to a "success" page.
        return HttpResponseRedirect('/form/success')

# urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('', url(r'^message/$', MessageFormPreview(MessageForm)) )

The Goal

The goal of this article is to easily secure this form preview behind the @login_required decorator. Ideally, we want to make a minimum amount of changes. Here are our requirements:

  1. Don’t make any changes to MessageForm or MessagePreviewForm.
  2. No addition functions

1st try: Decorated wrapper function

Unfortunately, the current @login_required decorator in Django trunk (revision 6652 as of this writing) doesn’t work with bound methods (≅ instance methods). Because of this, we’re left with only one way to do what we want, which violates the second requirement above. The gist of the technique is that we add a wrapper view function preview which only returns the FormPreview class. We then add the decorator to this new method. Here’s what the code looks like:

# views.py
from django.contrib.auth.decorators import login_required

@login_required
def preview(request):
	return MessageFormPreview(MessageForm)

# urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('', url(r'^message/$', 'views.preview') )

Requirement 1 is fulfilled because we haven’t made any changes to the forms.py. However, requirement 2 isn’t fulfilled because we had to add the preview function.

Patching the @login_required decorator

The next couple solutions require a patch to allow the @login_required decorator to work with bound methods. The patch is attached to ticket #4376 in the Django trac system. Hopefully, it will end up in the trunk sometime soon. Go ahead and grab it and apply it to your Django install and then we’ll continue. … … Done? Let’s go.

2nd try: Overriding __call__

You might have noticed above that we used a class (FormPreview) as the view function. This works because the when the view function is executed, it calls the __call__ instance method of FormPreview. This is the method, then, that we want to decorate. We can accomplish this by overriding it and adding the decorator to the overridden method. This is the code we end up with:

# forms.py
from django import newforms as forms
from django.contrib.formtools.preview import FormPreview
from django.contrib.auth.decorators import login_required

class MessageForm(forms.Form):
    message = forms.CharField()

class MessageFormPreview(FormPreview):
	@login_required
	def __call__(self, request, *args, **kwargs):
	    return super(MessageFormPreview, self).__call__(request, args, kwargs)

    def done(self, request, cleaned_data):
        # Do something with the cleaned_data, then redirect
        # to a "success" page.
        return HttpResponseRedirect('/form/success')

# urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('', url(r'^message/$', MessageFormPreview(MessageForm)))

You should notice immediately that we’ve violated both of the requirements that we set for ourselves. There’s a new function (the overridden __call__) and we added something to the MessageFormPreview class, which now limits its use to “login required” situations only.

3rd (and final) try: Decorate in the URLConf

This final and best solution didn’t come to me until I saw this post by James Bennett (who coincidentally went to college in my hometown). Instead of decorating the instance method in place as we did in try 2, we decorate it at the point where it is used in the urls.py file. This yields the finished code below.

# forms.py
from django import newforms as forms
from django.contrib.formtools.preview import FormPreview

class MessageForm(forms.Form):
    message = forms.CharField()

class MessageFormPreview(FormPreview):
    def done(self, request, cleaned_data):
        # Do something with the cleaned_data, then redirect
        # to a "success" page.
        return HttpResponseRedirect('/form/success')

# urls.py
from django.conf.urls.defaults import *
from django.contrib.auth.decorators import login_required

urlpatterns = patterns('', url(r'^message/$', login_required(MessageFormPreview(MessageForm))))

That good feeling you have in your heart is both of our requirements being satisfied. We have made no changes to the Form or FormPreview classes and we have added no functions. The only downside is that the patch for issue #4376 has not yet applied to the Django trunk.

Possibly Related Posts

1 Comment

  1. Luke Plant Said,

    November 7, 2007 @ 5:45 pm

    FYI, I committed the patch for you, so there is no downside now :-)

    Good work.

RSS feed for comments on this post