Django Class Based Views

August 19, 2011

I needed to receive paypal IPN notifications, and the sample code I found was using a Django class based view. It didn't quite work with Django 1.3 for various reasons, so I had to work out what was going on.

The View Class

I'm using two classes here. The first subclasses django.views.generic.base.View, and provides generic functionality for processing a paypal IPN:

from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View
from progperc.paypal.models import IpnLog
import urllib

class EndPoint(View):
    """"
    Provides generic functionality for receiving a PayPal IPN message.  Delegates processing of message to a subclass.
    """

    # this is the text returned by the view in the HTTP response
    default_response_text = 'Nothing to see here'

    def do_post_callback(self, url, args):
        """
        POST to a given url with supplied parameters
        """
        return urllib.urlopen(url, args).read()

    def verify(self, pRawPostData):
        """
        This function posts the passed parameters back to paypal to ensure they are from paypal
        """           
        lPostData = "cmd=_notify-validate&%s" % pRawPostData
        lResponse = self.do_post_callback(settings.PAYPAL_IPN_VERIFY, lPostData)
        return lResponse == 'VERIFIED'

    def default_response(self):
        """
        Return a response for the POST to ensure we return a HTTP 200 code
        """
        return HttpResponse(self.default_response_text)

   def post(self, request, *args, **kwargs):
        """
        Handle the POST from paypal
        """
        data = dict(request.POST.items())
        # We need to post that BACK to PayPal to confirm it
        if self.verify(request.raw_post_data):
            self.on_process(data)
       else:
            self.on_process_invalid(data)
        return self.default_response()

    def on_process(self, data):
        """
        Overridden in subclass to process a valid IPN message
        """
        pass

    def on_process_invalid(self, data):

        """
        Overridden in subclass to process an invalid IPN message
        """
        pass

Note that this class implements the post method, which is passed the current request object. You can similarly implement get if that's what you need:

def get(self, request, *args, **kwargs):
        pass

def post(self, request, *args, **kwargs):
        pass

This call is done dynamically inside the framework, so it will support any HTTP method.

Subclass

We can then subclass this generic IPN functionality and apply specific code to run when a message is received. Here I'm doing the same thing in both cases, but you should get the idea:

class IpnEndPoint(EndPoint):
    """
    Views for processing paypal IPN
    """
    def on_process(self, data):
        # Do something with valid data from PayPal - e-mail it to yourself,
        # stick it in a database, generate a license key and e-mail it to the
        # user... whatever
        lLog = IpnLog()
        lLog.populate(data)
        lLog.type = 'valid'
        lLog.save()

    def on_process_invalid(self, data):
        # Do something with invalid data (could be from anywhere) - you 
        # should probably log this somewhere
        lLog = IpnLog()
        lLog.populate(data)
        lLog.type = 'invalid'
        lLog.save()

URL

This class-based view needs to be added into urls.py:

from django.conf.urls.defaults import *
from progperc.paypal.views import IpnEndPoint

urlpatterns = patterns('',
     (r'^ipn/$', IpnEndPoint.as_view()),
)

Note that we're calling the as_view() function on the view class (see Problems section below for what happens if you do this on an instance).

In our case, PayPal isn't posting back the CSRF token that Django expects, so we need to exempt this view from that processing. This can be done by using the decorator function directly in urls.py:

from django.views.decorators.csrf import csrf_exempt

urlpatterns = patterns('',
     (r'^ipn/$', csrf_exempt(IpnEndPoint.as_view())),
)

Problems

I had a problem with the exception This method is available only on the view class.. This was caused because I was using an instance in urls.py:

(r'^ipn/$', csrf_exempt(IpnEndPoint().as_view())), # this won't work!!

what I need to use was:

(r'^ipn/$', csrf_exempt(IpnEndPoint.as_view())),

References