Mercurial > public > sg101
diff donations/views.py @ 581:ee87ea74d46b
For Django 1.4, rearranged project structure for new manage.py.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Sat, 05 May 2012 17:10:48 -0500 |
parents | gpp/donations/views.py@767cedc7d12a |
children | e1c03da72818 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/donations/views.py Sat May 05 17:10:48 2012 -0500 @@ -0,0 +1,221 @@ +""" +Views for the donations application. +""" +import urllib2 +import decimal +import datetime +import logging + +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.conf import settings +from django.contrib.sites.models import Site +from django.http import HttpResponse +from django.http import HttpResponseServerError +from django.contrib.auth.models import User +from django.views.decorators.csrf import csrf_exempt + + +from donations.models import Donation + +PP_DATE_FMT = '%H:%M:%S %b %d, %Y' + +def paypal_params(): + """ + This function returns a tuple where the 1st element is the Paypal + URL and the 2nd element is the Paypal business email. This information + depends on the setting DONATIONS_DEBUG. + """ + if settings.DONATIONS_DEBUG: + form_action = 'https://www.sandbox.paypal.com/cgi-bin/webscr' + business = settings.DONATIONS_BUSINESS_DEBUG + else: + form_action = 'https://www.paypal.com/cgi-bin/webscr' + business = settings.DONATIONS_BUSINESS + + return form_action, business + + +def verify_request(params): + """ + Send the parameters back to Paypal and return the response string. + """ + # If we are doing localhost-type unit tests, just return whatever + # the test wants us to... + if hasattr(settings, 'DONATIONS_DEBUG_VERIFY_RESPONSE'): + return settings.DONATIONS_DEBUG_VERIFY_RESPONSE + + req = urllib2.Request(paypal_params()[0], params) + req.add_header("Content-type", "application/x-www-form-urlencoded") + try: + response = urllib2.urlopen(req) + except URLError, e: + logging.exception('IPN: exception verifying IPN: %s', e) + return None + + return response.read() + + +def index(request): + gross, net, donations = Donation.objects.monthly_stats() + current_site = Site.objects.get_current() + form_action, business = paypal_params() + + return render_to_response('donations/index.html', { + 'goal': settings.DONATIONS_GOAL, + 'gross': gross, + 'net': net, + 'left': settings.DONATIONS_GOAL - net, + 'donations': donations, + 'form_action': form_action, + 'business': business, + 'anonymous': settings.DONATIONS_ANON_NAME, + 'item_name': settings.DONATIONS_ITEM_NAME, + 'item_number': settings.DONATIONS_ITEM_NUM, + 'item_anon_number': settings.DONATIONS_ITEM_ANON_NUM, + 'domain': current_site.domain, + }, + context_instance = RequestContext(request)) + + +@csrf_exempt +def ipn(request): + """ + This function is the IPN listener and handles the IPN POST from Paypal. + The algorithm here roughly follows the outline described in chapter 2 + of Paypal's IPNGuide.pdf "Implementing an IPN Listener". + + """ + # Log some info about this IPN event + ip = request.META.get('REMOTE_ADDR', '?') + parameters = request.POST.copy() + logging.info('IPN from %s; post data: %s', ip, parameters.urlencode()) + + # Now we follow the instructions in chapter 2 of the Paypal IPNGuide.pdf. + # Create a request that contains exactly the same IPN variables and values in + # the same order, preceded with cmd=_notify-validate + parameters['cmd']='_notify-validate' + + # Post the request back to Paypal (either to the sandbox or the real deal), + # and read the response: + status = verify_request(parameters.urlencode()) + if status != 'VERIFIED': + logging.warning('IPN: Payapl did not verify; status was %s', status) + return HttpResponse() + + # Response was VERIFIED; act on this if it is a Completed donation, + # otherwise don't handle it (we are just a donations application. Here + # is where we could be expanded to be a more general payment processor). + + payment_status = parameters.get('payment_status') + if payment_status != 'Completed': + logging.info('IPN: payment_status is %s; we are done.', payment_status) + return HttpResponse() + + # Is this a donation to the site? + item_number = parameters.get('item_number') + if (item_number == settings.DONATIONS_ITEM_NUM or + item_number == settings.DONATIONS_ITEM_ANON_NUM): + process_donation(item_number, parameters) + else: + logging.info('IPN: not a donation; done.') + + return HttpResponse() + + +def process_donation(item_number, params): + """ + A few validity and duplicate checks are made on the donation params. + If everything is ok, construct a donation object from the parameters and + store it in the database. + + """ + # Has this transaction been processed before? + txn_id = params.get('txn_id') + if txn_id is None: + logging.error('IPN: missing txn_id') + return + + try: + donation = Donation.objects.get(txn_id__exact=txn_id) + except Donation.DoesNotExist: + pass + else: + logging.warning('IPN: duplicate txn_id') + return # no exception, this is a duplicate + + # Is the email address ours? + business = params.get('business') + if business != paypal_params()[1]: + logging.warning('IPN: invalid business: %s', business) + return + + # is this a payment received? + txn_type = params.get('txn_type') + if txn_type != 'web_accept': + logging.warning('IPN: invalid txn_type: %s', txn_type) + return + + # Looks like a donation, save it to the database. + # Determine which user this came from, if any. + # The username is stored in the custom field if the user was logged in when + # the donation was made. + user = None + if 'custom' in params and params['custom']: + try: + user = User.objects.get(username__exact=params['custom']) + except User.DoesNotExist: + pass + + is_anonymous = item_number == settings.DONATIONS_ITEM_ANON_NUM + test_ipn = params.get('test_ipn') == '1' + + first_name = params.get('first_name', '') + last_name = params.get('last_name', '') + payer_email = params.get('payer_email', '') + payer_id = params.get('payer_id', '') + memo = params.get('memo', '') + payer_status = params.get('payer_status', '') + + try: + mc_gross = decimal.Decimal(params['mc_gross']) + mc_fee = decimal.Decimal(params['mc_fee']) + except KeyError, decimal.InvalidOperation: + logging.error('IPN: invalid/missing mc_gross or mc_fee') + return + + payment_date = params.get('payment_date') + if payment_date is None: + logging.error('IPN: missing payment_date') + return + + # strip off the timezone + payment_date = payment_date[:-4] + try: + payment_date = datetime.datetime.strptime(payment_date, PP_DATE_FMT) + except ValueError: + logging.error('IPN: invalid payment_date "%s"', params['payment_date']) + return + + try: + donation = Donation( + user=user, + is_anonymous=is_anonymous, + test_ipn=test_ipn, + txn_id=txn_id, + txn_type=txn_type, + first_name=first_name, + last_name=last_name, + payer_email=payer_email, + payer_id=payer_id, + memo=memo, + payer_status=payer_status, + mc_gross=mc_gross, + mc_fee=mc_fee, + payment_date=payment_date) + except: + logging.exception('IPN: exception during donation creation') + else: + donation.save() + logging.info('IPN: donation saved') +