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@1185
|
161 if business not in 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')
|