annotate donations/views.py @ 1184:fbfb1a9c70e2

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