annotate donations/views.py @ 989:2908859c2fe4

Smilies now use relative links. This is for upcoming switch to SSL. Currently we do not need absolute URLs for smilies. If this changes we can add it later.
author Brian Neal <bgneal@gmail.com>
date Thu, 29 Oct 2015 20:54:34 -0500
parents ee87ea74d46b
children e1c03da72818
rev   line source
bgneal@35 1 """
bgneal@35 2 Views for the donations application.
bgneal@35 3 """
bgneal@36 4 import urllib2
bgneal@36 5 import decimal
bgneal@36 6 import datetime
bgneal@316 7 import logging
bgneal@36 8
bgneal@35 9 from django.shortcuts import render_to_response
bgneal@35 10 from django.template import RequestContext
bgneal@35 11 from django.conf import settings
bgneal@35 12 from django.contrib.sites.models import Site
bgneal@36 13 from django.http import HttpResponse
bgneal@36 14 from django.http import HttpResponseServerError
bgneal@36 15 from django.contrib.auth.models import User
bgneal@238 16 from django.views.decorators.csrf import csrf_exempt
bgneal@238 17
bgneal@35 18
bgneal@35 19 from donations.models import Donation
bgneal@35 20
bgneal@63 21 PP_DATE_FMT = '%H:%M:%S %b %d, %Y'
bgneal@35 22
bgneal@36 23 def paypal_params():
bgneal@36 24 """
bgneal@36 25 This function returns a tuple where the 1st element is the Paypal
bgneal@36 26 URL and the 2nd element is the Paypal business email. This information
bgneal@36 27 depends on the setting DONATIONS_DEBUG.
bgneal@36 28 """
bgneal@36 29 if settings.DONATIONS_DEBUG:
bgneal@35 30 form_action = 'https://www.sandbox.paypal.com/cgi-bin/webscr'
bgneal@35 31 business = settings.DONATIONS_BUSINESS_DEBUG
bgneal@35 32 else:
bgneal@35 33 form_action = 'https://www.paypal.com/cgi-bin/webscr'
bgneal@35 34 business = settings.DONATIONS_BUSINESS
bgneal@35 35
bgneal@36 36 return form_action, business
bgneal@36 37
bgneal@36 38
bgneal@316 39 def verify_request(params):
bgneal@316 40 """
bgneal@316 41 Send the parameters back to Paypal and return the response string.
bgneal@316 42 """
bgneal@316 43 # If we are doing localhost-type unit tests, just return whatever
bgneal@316 44 # the test wants us to...
bgneal@316 45 if hasattr(settings, 'DONATIONS_DEBUG_VERIFY_RESPONSE'):
bgneal@316 46 return settings.DONATIONS_DEBUG_VERIFY_RESPONSE
bgneal@316 47
bgneal@316 48 req = urllib2.Request(paypal_params()[0], params)
bgneal@316 49 req.add_header("Content-type", "application/x-www-form-urlencoded")
bgneal@316 50 try:
bgneal@316 51 response = urllib2.urlopen(req)
bgneal@316 52 except URLError, e:
bgneal@316 53 logging.exception('IPN: exception verifying IPN: %s', e)
bgneal@316 54 return None
bgneal@316 55
bgneal@316 56 return response.read()
bgneal@316 57
bgneal@316 58
bgneal@36 59 def index(request):
bgneal@36 60 gross, net, donations = Donation.objects.monthly_stats()
bgneal@36 61 current_site = Site.objects.get_current()
bgneal@36 62 form_action, business = paypal_params()
bgneal@36 63
bgneal@35 64 return render_to_response('donations/index.html', {
bgneal@35 65 'goal': settings.DONATIONS_GOAL,
bgneal@35 66 'gross': gross,
bgneal@35 67 'net': net,
bgneal@35 68 'left': settings.DONATIONS_GOAL - net,
bgneal@35 69 'donations': donations,
bgneal@35 70 'form_action': form_action,
bgneal@35 71 'business': business,
bgneal@35 72 'anonymous': settings.DONATIONS_ANON_NAME,
bgneal@35 73 'item_name': settings.DONATIONS_ITEM_NAME,
bgneal@35 74 'item_number': settings.DONATIONS_ITEM_NUM,
bgneal@35 75 'item_anon_number': settings.DONATIONS_ITEM_ANON_NUM,
bgneal@35 76 'domain': current_site.domain,
bgneal@35 77 },
bgneal@35 78 context_instance = RequestContext(request))
bgneal@35 79
bgneal@35 80
bgneal@238 81 @csrf_exempt
bgneal@35 82 def ipn(request):
bgneal@36 83 """
bgneal@36 84 This function is the IPN listener and handles the IPN POST from Paypal.
bgneal@39 85 The algorithm here roughly follows the outline described in chapter 2
bgneal@39 86 of Paypal's IPNGuide.pdf "Implementing an IPN Listener".
bgneal@316 87
bgneal@39 88 """
bgneal@39 89 # Log some info about this IPN event
bgneal@39 90 ip = request.META.get('REMOTE_ADDR', '?')
bgneal@39 91 parameters = request.POST.copy()
bgneal@316 92 logging.info('IPN from %s; post data: %s', ip, parameters.urlencode())
bgneal@35 93
bgneal@39 94 # Now we follow the instructions in chapter 2 of the Paypal IPNGuide.pdf.
bgneal@39 95 # Create a request that contains exactly the same IPN variables and values in
bgneal@39 96 # the same order, preceded with cmd=_notify-validate
bgneal@39 97 parameters['cmd']='_notify-validate'
bgneal@36 98
bgneal@316 99 # Post the request back to Paypal (either to the sandbox or the real deal),
bgneal@316 100 # and read the response:
bgneal@316 101 status = verify_request(parameters.urlencode())
bgneal@39 102 if status != 'VERIFIED':
bgneal@316 103 logging.warning('IPN: Payapl did not verify; status was %s', status)
bgneal@39 104 return HttpResponse()
bgneal@36 105
bgneal@39 106 # Response was VERIFIED; act on this if it is a Completed donation,
bgneal@39 107 # otherwise don't handle it (we are just a donations application. Here
bgneal@39 108 # is where we could be expanded to be a more general payment processor).
bgneal@36 109
bgneal@39 110 payment_status = parameters.get('payment_status')
bgneal@39 111 if payment_status != 'Completed':
bgneal@316 112 logging.info('IPN: payment_status is %s; we are done.', payment_status)
bgneal@39 113 return HttpResponse()
bgneal@39 114
bgneal@39 115 # Is this a donation to the site?
bgneal@39 116 item_number = parameters.get('item_number')
bgneal@316 117 if (item_number == settings.DONATIONS_ITEM_NUM or
bgneal@316 118 item_number == settings.DONATIONS_ITEM_ANON_NUM):
bgneal@39 119 process_donation(item_number, parameters)
bgneal@39 120 else:
bgneal@39 121 logging.info('IPN: not a donation; done.')
bgneal@39 122
bgneal@39 123 return HttpResponse()
bgneal@36 124
bgneal@36 125
bgneal@36 126 def process_donation(item_number, params):
bgneal@36 127 """
bgneal@39 128 A few validity and duplicate checks are made on the donation params.
bgneal@316 129 If everything is ok, construct a donation object from the parameters and
bgneal@39 130 store it in the database.
bgneal@316 131
bgneal@36 132 """
bgneal@36 133 # Has this transaction been processed before?
bgneal@39 134 txn_id = params.get('txn_id')
bgneal@39 135 if txn_id is None:
bgneal@39 136 logging.error('IPN: missing txn_id')
bgneal@36 137 return
bgneal@39 138
bgneal@36 139 try:
bgneal@36 140 donation = Donation.objects.get(txn_id__exact=txn_id)
bgneal@36 141 except Donation.DoesNotExist:
bgneal@36 142 pass
bgneal@36 143 else:
bgneal@39 144 logging.warning('IPN: duplicate txn_id')
bgneal@36 145 return # no exception, this is a duplicate
bgneal@36 146
bgneal@36 147 # Is the email address ours?
bgneal@36 148 business = params.get('business')
bgneal@36 149 if business != paypal_params()[1]:
bgneal@316 150 logging.warning('IPN: invalid business: %s', business)
bgneal@36 151 return
bgneal@36 152
bgneal@36 153 # is this a payment received?
bgneal@36 154 txn_type = params.get('txn_type')
bgneal@36 155 if txn_type != 'web_accept':
bgneal@316 156 logging.warning('IPN: invalid txn_type: %s', txn_type)
bgneal@36 157 return
bgneal@316 158
bgneal@36 159 # Looks like a donation, save it to the database.
bgneal@36 160 # Determine which user this came from, if any.
bgneal@36 161 # The username is stored in the custom field if the user was logged in when
bgneal@36 162 # the donation was made.
bgneal@36 163 user = None
bgneal@65 164 if 'custom' in params and params['custom']:
bgneal@36 165 try:
bgneal@36 166 user = User.objects.get(username__exact=params['custom'])
bgneal@36 167 except User.DoesNotExist:
bgneal@36 168 pass
bgneal@36 169
bgneal@36 170 is_anonymous = item_number == settings.DONATIONS_ITEM_ANON_NUM
bgneal@36 171 test_ipn = params.get('test_ipn') == '1'
bgneal@36 172
bgneal@36 173 first_name = params.get('first_name', '')
bgneal@36 174 last_name = params.get('last_name', '')
bgneal@36 175 payer_email = params.get('payer_email', '')
bgneal@36 176 payer_id = params.get('payer_id', '')
bgneal@36 177 memo = params.get('memo', '')
bgneal@36 178 payer_status = params.get('payer_status', '')
bgneal@36 179
bgneal@36 180 try:
bgneal@36 181 mc_gross = decimal.Decimal(params['mc_gross'])
bgneal@36 182 mc_fee = decimal.Decimal(params['mc_fee'])
bgneal@36 183 except KeyError, decimal.InvalidOperation:
bgneal@39 184 logging.error('IPN: invalid/missing mc_gross or mc_fee')
bgneal@36 185 return
bgneal@36 186
bgneal@61 187 payment_date = params.get('payment_date')
bgneal@61 188 if payment_date is None:
bgneal@61 189 logging.error('IPN: missing payment_date')
bgneal@36 190 return
bgneal@36 191
bgneal@61 192 # strip off the timezone
bgneal@61 193 payment_date = payment_date[:-4]
bgneal@61 194 try:
bgneal@61 195 payment_date = datetime.datetime.strptime(payment_date, PP_DATE_FMT)
bgneal@61 196 except ValueError:
bgneal@316 197 logging.error('IPN: invalid payment_date "%s"', params['payment_date'])
bgneal@61 198 return
bgneal@36 199
bgneal@61 200 try:
bgneal@61 201 donation = Donation(
bgneal@61 202 user=user,
bgneal@61 203 is_anonymous=is_anonymous,
bgneal@61 204 test_ipn=test_ipn,
bgneal@61 205 txn_id=txn_id,
bgneal@61 206 txn_type=txn_type,
bgneal@61 207 first_name=first_name,
bgneal@61 208 last_name=last_name,
bgneal@61 209 payer_email=payer_email,
bgneal@61 210 payer_id=payer_id,
bgneal@61 211 memo=memo,
bgneal@61 212 payer_status=payer_status,
bgneal@61 213 mc_gross=mc_gross,
bgneal@61 214 mc_fee=mc_fee,
bgneal@61 215 payment_date=payment_date)
bgneal@61 216 except:
bgneal@61 217 logging.exception('IPN: exception during donation creation')
bgneal@61 218 else:
bgneal@61 219 donation.save()
bgneal@61 220 logging.info('IPN: donation saved')
bgneal@36 221