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