bgneal@35: """ bgneal@35: Views for the donations application. bgneal@35: """ bgneal@36: import urllib2 bgneal@36: import decimal bgneal@36: import datetime bgneal@316: import logging bgneal@36: bgneal@1031: from django.shortcuts import render 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.contrib.auth.models import User bgneal@238: from django.views.decorators.csrf import csrf_exempt bgneal@1079: from django.views.generic import TemplateView bgneal@238: 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@316: def verify_request(params): bgneal@316: """ bgneal@316: Send the parameters back to Paypal and return the response string. bgneal@316: """ bgneal@316: # If we are doing localhost-type unit tests, just return whatever bgneal@316: # the test wants us to... bgneal@316: if hasattr(settings, 'DONATIONS_DEBUG_VERIFY_RESPONSE'): bgneal@316: return settings.DONATIONS_DEBUG_VERIFY_RESPONSE bgneal@316: bgneal@316: req = urllib2.Request(paypal_params()[0], params) bgneal@316: req.add_header("Content-type", "application/x-www-form-urlencoded") bgneal@316: try: bgneal@316: response = urllib2.urlopen(req) bgneal@1031: except urllib2.URLError as ex: bgneal@1031: logging.exception('IPN: exception verifying IPN: %s', ex) bgneal@316: return None bgneal@316: bgneal@316: return response.read() bgneal@316: bgneal@316: bgneal@36: def index(request): bgneal@36: gross, net, donations = Donation.objects.monthly_stats() bgneal@1087: goal = settings.DONATIONS_GOAL bgneal@36: current_site = Site.objects.get_current() bgneal@36: form_action, business = paypal_params() bgneal@36: bgneal@1087: pct = max(0, min(100, int(net / goal * 100))) bgneal@1087: bgneal@1031: return render(request, 'donations/index.html', { bgneal@1087: 'goal': goal, bgneal@35: 'gross': gross, bgneal@35: 'net': net, bgneal@35: 'left': settings.DONATIONS_GOAL - net, bgneal@1087: 'pct': pct, bgneal@35: 'donations': donations, bgneal@35: 'form_action': form_action, bgneal@1184: 'business': business[0], 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@1079: 'V3_DESIGN': True, bgneal@1031: }) bgneal@35: bgneal@35: bgneal@1079: class ThanksView(TemplateView): bgneal@1079: template_name = 'donations/thanks.html' bgneal@1079: bgneal@1079: def get_context_data(self, **kwargs): bgneal@1079: context = super(ThanksView, self).get_context_data(**kwargs) bgneal@1079: context['V3_DESIGN'] = True bgneal@1079: return context bgneal@1079: bgneal@1079: bgneal@238: @csrf_exempt 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@316: bgneal@39: """ bgneal@39: # Log some info about this IPN event bgneal@39: ip = request.META.get('REMOTE_ADDR', '?') bgneal@39: parameters = request.POST.copy() bgneal@316: 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@316: # Post the request back to Paypal (either to the sandbox or the real deal), bgneal@316: # and read the response: bgneal@316: status = verify_request(parameters.urlencode()) bgneal@39: if status != 'VERIFIED': bgneal@316: 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@316: 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@316: if (item_number == settings.DONATIONS_ITEM_NUM or bgneal@316: 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@316: If everything is ok, construct a donation object from the parameters and bgneal@39: store it in the database. bgneal@316: bgneal@36: """ 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@1185: if business not in paypal_params()[1]: bgneal@316: 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@316: logging.warning('IPN: invalid txn_type: %s', txn_type) bgneal@36: return bgneal@316: 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@316: 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')