bgneal@35: """
bgneal@35: Views for the donations application.
bgneal@35: """
bgneal@36: import urllib2
bgneal@36: import decimal
bgneal@36: import datetime
bgneal@36: 
bgneal@35: from django.shortcuts import render_to_response
bgneal@35: from django.template import RequestContext
bgneal@35: from django.conf import settings
bgneal@35: from django.contrib.sites.models import Site
bgneal@36: from django.http import HttpResponse
bgneal@36: from django.http import HttpResponseServerError
bgneal@36: from django.contrib.auth.models import User
bgneal@35: 
bgneal@35: from donations.models import Donation
bgneal@35: 
bgneal@63: PP_DATE_FMT = '%H:%M:%S %b %d, %Y'
bgneal@35: 
bgneal@36: def paypal_params():
bgneal@36:     """
bgneal@36:     This function returns a tuple where the 1st element is the Paypal
bgneal@36:     URL and the 2nd element is the Paypal business email. This information
bgneal@36:     depends on the setting DONATIONS_DEBUG.
bgneal@36:     """
bgneal@36:     if settings.DONATIONS_DEBUG:
bgneal@35:         form_action = 'https://www.sandbox.paypal.com/cgi-bin/webscr'
bgneal@35:         business = settings.DONATIONS_BUSINESS_DEBUG
bgneal@35:     else:
bgneal@35:         form_action = 'https://www.paypal.com/cgi-bin/webscr'
bgneal@35:         business = settings.DONATIONS_BUSINESS
bgneal@35: 
bgneal@36:     return form_action, business
bgneal@36: 
bgneal@36: 
bgneal@36: def index(request):
bgneal@36:     gross, net, donations = Donation.objects.monthly_stats()
bgneal@36:     current_site = Site.objects.get_current()
bgneal@36:     form_action, business = paypal_params()
bgneal@36: 
bgneal@35:     return render_to_response('donations/index.html', {
bgneal@35:         'goal': settings.DONATIONS_GOAL,
bgneal@35:         'gross': gross,
bgneal@35:         'net': net,
bgneal@35:         'left': settings.DONATIONS_GOAL - net,
bgneal@35:         'donations': donations,
bgneal@35:         'form_action': form_action,
bgneal@35:         'business': business,
bgneal@35:         'anonymous': settings.DONATIONS_ANON_NAME,
bgneal@35:         'item_name': settings.DONATIONS_ITEM_NAME,
bgneal@35:         'item_number': settings.DONATIONS_ITEM_NUM,
bgneal@35:         'item_anon_number': settings.DONATIONS_ITEM_ANON_NUM,
bgneal@35:         'domain': current_site.domain,
bgneal@35:         },
bgneal@35:         context_instance = RequestContext(request))
bgneal@35: 
bgneal@35: 
bgneal@35: def ipn(request):
bgneal@36:     """
bgneal@36:     This function is the IPN listener and handles the IPN POST from Paypal.
bgneal@39:     The algorithm here roughly follows the outline described in chapter 2
bgneal@39:     of Paypal's IPNGuide.pdf "Implementing an IPN Listener".
bgneal@39:     """
bgneal@39:     import logging
bgneal@35: 
bgneal@39:     # Log some info about this IPN event
bgneal@39:     ip = request.META.get('REMOTE_ADDR', '?')
bgneal@39:     parameters = request.POST.copy()
bgneal@39:     logging.info('IPN from %s; post data: %s' % (ip, parameters.urlencode()))
bgneal@35: 
bgneal@39:     # Now we follow the instructions in chapter 2 of the Paypal IPNGuide.pdf.
bgneal@39:     # Create a request that contains exactly the same IPN variables and values in
bgneal@39:     # the same order, preceded with cmd=_notify-validate
bgneal@39:     parameters['cmd']='_notify-validate'
bgneal@36: 
bgneal@39:     # Post the request back to Paypal (either to the sandbox or the real deal).
bgneal@39:     req = urllib2.Request(paypal_params()[0], parameters.urlencode())
bgneal@39:     req.add_header("Content-type", "application/x-www-form-urlencoded")
bgneal@39:     response = urllib2.urlopen(req)
bgneal@36: 
bgneal@39:     # Wait for the response from Paypal, which should be either VERIFIED or INVALID.
bgneal@39:     status = response.read()
bgneal@39:     if status != 'VERIFIED':
bgneal@39:         logging.warning('IPN: Payapl did not verify; status was %s' % status)
bgneal@39:         return HttpResponse()
bgneal@36: 
bgneal@39:     # Response was VERIFIED; act on this if it is a Completed donation, 
bgneal@39:     # otherwise don't handle it (we are just a donations application. Here
bgneal@39:     # is where we could be expanded to be a more general payment processor).
bgneal@36: 
bgneal@39:     payment_status = parameters.get('payment_status')
bgneal@39:     if payment_status != 'Completed':
bgneal@39:         logging.info('IPN: payment_status is %s; we are done.' % payment_status)
bgneal@39:         return HttpResponse()
bgneal@39: 
bgneal@39:     # Is this a donation to the site?
bgneal@39:     item_number = parameters.get('item_number')
bgneal@39:     if item_number == settings.DONATIONS_ITEM_NUM or \
bgneal@39:        item_number == settings.DONATIONS_ITEM_ANON_NUM:
bgneal@39:         process_donation(item_number, parameters)
bgneal@39:     else:
bgneal@39:         logging.info('IPN: not a donation; done.')
bgneal@39: 
bgneal@39:     return HttpResponse()
bgneal@36: 
bgneal@36: 
bgneal@36: def process_donation(item_number, params):
bgneal@36:     """
bgneal@39:     A few validity and duplicate checks are made on the donation params.
bgneal@39:     If everything is ok, construct a donation object from the parameters and 
bgneal@39:     store it in the database.
bgneal@36:     """
bgneal@39:     import logging
bgneal@39: 
bgneal@36:     # Has this transaction been processed before?
bgneal@39:     txn_id = params.get('txn_id')
bgneal@39:     if txn_id is None:
bgneal@39:         logging.error('IPN: missing txn_id')
bgneal@36:         return
bgneal@39: 
bgneal@36:     try:
bgneal@36:         donation = Donation.objects.get(txn_id__exact=txn_id)
bgneal@36:     except Donation.DoesNotExist:
bgneal@36:         pass
bgneal@36:     else:
bgneal@39:         logging.warning('IPN: duplicate txn_id')
bgneal@36:         return      # no exception, this is a duplicate
bgneal@36: 
bgneal@36:     # Is the email address ours?
bgneal@36:     business = params.get('business')
bgneal@36:     if business != paypal_params()[1]:
bgneal@39:         logging.warning('IPN: invalid business: %s' % business)
bgneal@36:         return
bgneal@36: 
bgneal@36:     # is this a payment received?
bgneal@36:     txn_type = params.get('txn_type')
bgneal@36:     if txn_type != 'web_accept':
bgneal@39:         logging.warning('IPN: invalid txn_type: %s' % txn_type)
bgneal@36:         return
bgneal@61:     
bgneal@36:     # Looks like a donation, save it to the database.
bgneal@36:     # Determine which user this came from, if any.
bgneal@36:     # The username is stored in the custom field if the user was logged in when
bgneal@36:     # the donation was made.
bgneal@36:     user = None
bgneal@65:     if 'custom' in params and params['custom']:
bgneal@36:         try:
bgneal@36:             user = User.objects.get(username__exact=params['custom'])
bgneal@36:         except User.DoesNotExist:
bgneal@36:             pass
bgneal@36: 
bgneal@36:     is_anonymous = item_number == settings.DONATIONS_ITEM_ANON_NUM
bgneal@36:     test_ipn = params.get('test_ipn') == '1'
bgneal@36: 
bgneal@36:     first_name = params.get('first_name', '')
bgneal@36:     last_name = params.get('last_name', '')
bgneal@36:     payer_email = params.get('payer_email', '')
bgneal@36:     payer_id = params.get('payer_id', '')
bgneal@36:     memo = params.get('memo', '')
bgneal@36:     payer_status = params.get('payer_status', '')
bgneal@36: 
bgneal@36:     try:
bgneal@36:         mc_gross = decimal.Decimal(params['mc_gross'])
bgneal@36:         mc_fee = decimal.Decimal(params['mc_fee'])
bgneal@36:     except KeyError, decimal.InvalidOperation:
bgneal@39:         logging.error('IPN: invalid/missing mc_gross or mc_fee')
bgneal@36:         return
bgneal@36: 
bgneal@61:     payment_date = params.get('payment_date')
bgneal@61:     if payment_date is None:
bgneal@61:         logging.error('IPN: missing payment_date')
bgneal@36:         return
bgneal@36: 
bgneal@61:     # strip off the timezone
bgneal@61:     payment_date = payment_date[:-4]
bgneal@61:     try:
bgneal@61:         payment_date = datetime.datetime.strptime(payment_date, PP_DATE_FMT)
bgneal@61:     except ValueError:
bgneal@61:         logging.error('IPN: invalid payment_date "%s"' % params['payment_date'])
bgneal@61:         return
bgneal@36: 
bgneal@61:     try:
bgneal@61:         donation = Donation(
bgneal@61:             user=user,
bgneal@61:             is_anonymous=is_anonymous,
bgneal@61:             test_ipn=test_ipn,
bgneal@61:             txn_id=txn_id,
bgneal@61:             txn_type=txn_type,
bgneal@61:             first_name=first_name,
bgneal@61:             last_name=last_name,
bgneal@61:             payer_email=payer_email,
bgneal@61:             payer_id=payer_id,
bgneal@61:             memo=memo,
bgneal@61:             payer_status=payer_status,
bgneal@61:             mc_gross=mc_gross,
bgneal@61:             mc_fee=mc_fee,
bgneal@61:             payment_date=payment_date)
bgneal@61:     except:
bgneal@61:         logging.exception('IPN: exception during donation creation')
bgneal@61:     else:
bgneal@61:         donation.save()
bgneal@61:         logging.info('IPN: donation saved')
bgneal@36: