Getting started with PayPal on Django

A brief walkthrough on how to accept payments on your Django application, using the PayPal APIs

By Peter Georgeson

Introduction

This tutorial describes how to integrate Django with PayPal by implementing a simple online store selling downloadable content.

Django is a powerful, Python-based web framework suitable for rapidly building web applications. It has an active community, is easy to use, and drives a number of prominent sites.

PayPal offers an easy way to send and receive payments, has a comprehensive API, and is suitable for developers to use for online transactions.

Requirements

Paypal Accounts

If you don’t already have one, create a PayPal developer account at https://developer.paypal.com/ for testing. After creating your main developer account, you will also need to create seller and buyer test accounts.

If you want to start accepting real money, you will also need to sign up for a live account at https://www.paypal.com/

PDT (Payment Data Transfer)

PayPal provides a number of methods for authenticating a transaction, once the buyer returns to your site. This example uses the PDT method of authenticating transactions, so you need to enable PDT on your seller account.

Instructions for doing this are at https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/howto_html_paymentdatatransfer

When updating the configuration settings on your Django application, use the PDT token obtained here.

Django

You need Django installed to run the application. Installation instructions for Django can be found at http://www.djangoproject.com/download/

Sample Application

A fully working sample application is available from https://github.com/supernifty/django-paypal-store-example

It is highly recommended that you download the code to follow along with the snippets shown in this tutorial.

Running the Sample Application

Configuration

To get the application up and running in your environment, some settings on the Django application need to be updated. Open the file samplesite/settings.py and edit the following settings:

Database

To generate the basic schema with some simple data, run the command:


python manage.py syncdb
 

Web Server

To start the web server, issue the command:


python manage.py runserver
 

You can check that the test server is running by starting your web browser and browsing to http://127.0.0.1:8000/

A Basic Payment Workflow

The sample application demonstrates how you might use Django and PayPal to build a store for users to purchase downloadable content, whether they be e-books, software, images or any other electronic, sellable file. The application follows a simple workflow, described below.

The model

In this example, we want to track available resources, and what purchases a user has made. In a Django application, the database model is defined in models.py:


from django.db import models
 
class Resource( models.Model ):
  '''resources available for purchase'''
  name = models.CharField( max_length=250 )
  location = models.CharField( max_length=250 )
  price = models.DecimalField( decimal_places=2, max_digits=7 )
 
class Purchase( models.Model ):
  '''purchases'''
  resource = models.ForeignKey( Resource )
  purchaser = models.ForeignKey( User )
  purchased_at = models.DateTimeField(auto_now_add=True)
  tx = models.CharField( max_length=250 )
 

The Resource class defined above stores the available downloadable content; the Purchase class stores purchase details.

URL mappings

urls.py maps a requested URL to a view defined in views.py using regular expressions:


    (r'^$', 'samplesite.sampleapp.views.home' ),
    (r'^download/(?P<id>\d+)/$', 'samplesite.sampleapp.views.download' ), # view a purchase
    (r'^purchased/(?P<uid>\d+)/(?P<id>\d+)/$', 'samplesite.sampleapp.views.purchased' ), # purchase callback
 

In the sample application, just three mappings are required - the home page; the page for a user to request a resource; and the page displayed after a successful purchase.

Step 1 - List Content Available for Purchase

The first step in the user experience is to show the visitor what is available for purchase. This is easily achieved in views.py:


def home( request ):
  return render_to_response('home.html', { 'list': models.Resource.objects.all() } )
 

The home method makes available to home.html the list of all resource objects. Then in the home.html template, we iterate over this list of available resources and display them to the user:


{% for item in list %}
<li><a href="/download/{{ item.id }}/">{{ item.name }} - ${{ item.price|floatformat:2 }}</a></li>
{% endfor %}
 

Here's what it looks like in a browser:

Available Resources

Step 2 - Attempt to Access Content

If the user clicks on one of the download links, they will first be prompted to login. This enables the application to check if the user has already purchased the content, and then after purchase, to mark the content as having been purchased by this user.

Django provides a user authentication module which can be activated using an appropriate decorator in views.py:


@login_required
def download( request, id ):
 

The @login_required decorator simply checks to see if the HTTP request has a valid user session attached, and if not, redirects the user to a login page.

Authentication

Once we have an authenticated user, the application checks to see if they have already purchased the requested content. If so, it is provided to the user. If the item has not been purchased, the user is redirected to a page prompting them to purchase the item:


  try:
    purchased = models.Purchase.objects.get( resource=resource, purchaser=request.user )
    # ...download content...
  except models.Purchase.DoesNotExist:
    return render_to_response('purchase.html', { 'resource': resource, ...
 

Step 3 - Purchase Content

The purchase.html template file displays details of the content to be purchased and a form that will post this data to PayPal. Important details forwarded to PayPal include the amount to pay, what is being purchased, and a URL to forward the user to once payment is complete.


<p>Purchase <b>{{ resource.name }}</b> for <b>${{ resource.price|floatformat:2 }}</b></p>
<form action="{{ paypal_url }}" method="post">
  <input type="hidden" name="amount" value="{{ resource.price }}">
  <input type="hidden" name="item_name" value="{{ resource.name }}">
  <input type="hidden" name="return" value="{{ paypal_return_url }}/purchased/{{ user.id }}/{{ resource.id }}/">
  ...
</form>
 

Purchase

Once the user clicks the “Pay Now” button, they are redirected to PayPal, and the user goes through the payment process. On completion, they are directed back to the return URL specified in the FORM POST above. The return URL includes details of the purchasing user and the resource being purchased.

PayPal

Step 4 - Verify Payment

After payment, the user’s browser is redirected to the return URL - the “purchased” page on the sample application, which has the format:


http://127.0.0.1:8000/purchased/1/2/?tx=4AF90390YT785663B&st=Completed&amt=0.20
 

The URL contains the user ID and the resource ID, which are first extracted in views.py:


def purchased( request, uid, id ):
    resource = get_object_or_404( models.Resource, pk=id )
    user = get_object_or_404( User, pk=uid )
 

PayPal includes a bunch of extra GET parameters on the return URL, which allows us to verify the payment. Without this, it would be trivial for a user to manually type in the URL to make it appear as though they had just made a purchase.

The application verifies the payment, first by checking that the transaction ID has not already been used:


  try:
    existing = models.Purchase.objects.get( tx=tx )
    return render_to_response('error.html', { 'error': "Duplicate transaction" }, context_instance=RequestContext(request) )
  except models.Purchase.DoesNotExist:
    ...
 

If the transaction ID is not a duplicate, it is then verified using PayPal's PDT validation. If all is well, a Purchase record is added and the user is shown a page indicating a successful purchase, with a link to the original download page.


tx = request.REQUEST['tx']
result = paypal.Verify( tx )
if result.success() and resource.price == result.amount(): # valid
  purchase = models.Purchase( resource=resource, purchaser=user, tx=tx )
  purchase.save()
  return render_to_response('purchased.html', { 'resource': resource }, ...
 

Purchased

An unsuccessful transaction results in an error page, which is easy to generate by changing the transaction ID in the URL.

PDT Validation

The sample application uses PDT validation to check that a valid transaction occurred at PayPal.

PDT validation works by PayPal redirecting the user back to your site and your return URL after the transaction has been completed. This return URL has a number of parameters added to it by PayPal. Your site validates the transaction by making an HTTPS request back to PayPal using these extra parameters.


class Verify( object ):
  '''builds result, results, response'''
  def __init__( self, tx ):
      ...
      post = dict()
      post[ 'cmd' ] = '_notify-synch'
      post[ 'tx' ] = tx
      post[ 'at' ] = settings.PAYPAL_PDT_TOKEN
      self.response = urllib.urlopen( settings.PAYPAL_PDT_URL, urllib.urlencode(post)).read()
      lines = self.response.split( '\n' )
      self.result = lines[0].strip()
      self.results = dict()
      for line in lines[1:]: # skip first line
        linesplit = line.split( '=', 2 )
        if len( linesplit ) == 2:
          self.results[ linesplit[0].strip() ] = urllib.unquote(linesplit[1].strip())
 
  def success( self ):
    return self.result == 'SUCCESS' and self.results[ 'payment_status' ] == 'Completed'
 

An advantage of PDT is that it does not require a public facing URL so will work within your development environment.

A disadvantage of PDT is that it requires the user to return to your site after payment. If the user makes a payment, but doesn’t make it back to your site, the transaction will not be recorded by the application. In such instances manual intervention would be required for the transaction to be recorded by the application. i.e. you’d need to check your PayPal transactions and manually add the purchase. Hopefully, most users would be keen to return to your site to collect the content they have just purchased.

The alternative to PDT is IPN (Instant Payment Notification). IPN requires a public URL for PayPal to notify your application with transaction details. Unlike PDT, PayPal guarantees that your IPN URL will receive notification of a successful transaction. The sample application does not implement IPN, but an IPN implementation is similar to PDT. A recommended approach is to implement both protocols and discard duplicate notifications.

Step 5 - Successful Purchase

Once the content has been verified it is added to the Purchase table. The user can then download this content at any time via their profile page at http://127.0.0.1:8000/accounts/profile/


@login_required
def profile( request ):
  '''show resources that a user has purchased'''
  return render_to_response('registration/profile.html', { 'list': models.Purchase.objects.filter( purchaser=request.user ) }, context_instance=RequestContext(request) )
 

User Profile

Further Enhancements

This simple application of Django and PayPal demonstrates the basic implementation and workflow of an online store offering downloadable content. However, it is not complete and should not be used as a live online store without further development:

Conclusion

This tutorial demonstrates a basic workflow for accepting payments with Django using PayPal as the payment provider. It could form the basis for a more complete e-commerce website based on these technologies.

Further Information and Other Resources