diff --git a/README.md b/README.md index 1a0b063..a349c92 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Django PayPal ============= +Note +---- + +This is a fork of `dcramer/django-paypal`. This contains few of the fixes which are currently (as of 16Mar12) in the Pull Request queue of dcramer. I felt they were quite good changes, so I did not wait and created my own version; topped with some of my own fixes. + About ----- @@ -15,13 +20,14 @@ PayPal Payments Pro allows you to accept payments on your website. It contains t There is currently an active discussion over the handling of some of the finer points of the PayPal API and the evolution of this code base - check it out over at [Django PayPal on Google Groups](http://groups.google.com/group/django-paypal). +**Note:** When using this module for production code, then set `PAYPAL_TEST` to `False`. If you do not set this then it is assumed to be `True`! When this flag is enabled then all traffics are directed towards [Paypal Sandbox](https://developer.paypal.com). Make sure you have an account on that and have created some test accounts. Using PayPal Payments Standard IPN: ------------------------------- 1. Download the code from GitHub: - git clone git://github.com/johnboxall/django-paypal.git paypal + git clone git://github.com/applegrew/django-paypal.git paypal 1. Edit `settings.py` and add `paypal.standard.ipn` to your `INSTALLED_APPS` and `PAYPAL_RECEIVER_EMAIL`: @@ -66,6 +72,8 @@ Using PayPal Payments Standard IPN: {{ form.render }} + **Note:** Do not use `PayPalPaymentsForm` for production code. Instead at least use `PayPalEncryptedPaymentsForm`. (See the section - Using PayPal Payments Standard with Encrypted Buttons). If that is not possible then generate a [hosted button from Paypal](https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_ButtonMgrAPIIntro#id093VD0JE0Y4). + 1. When someone uses this button to buy something PayPal makes a HTTP POST to your "notify_url". PayPal calls this Instant Payment Notification (IPN). The view `paypal.standard.ipn.views.ipn` handles IPN processing. To set the @@ -128,6 +136,7 @@ Paypal Payment Data Transfer (PDT) allows you to display transaction details to INSTALLED_APPS = (... 'paypal.standard.pdt', ...) PAYPAL_IDENTITY_TOKEN = "xxx" + PAYPAL_RECEIVER_EMAIL = "yourpaypalemail@example.com" 1. Create a view that uses `PayPalPaymentsForm` just like in PayPal IPN. @@ -142,6 +151,19 @@ Paypal Payment Data Transfer (PDT) allows you to display transaction details to (r'^paypal/pdt/', include('paypal.standard.pdt.urls')), ... ) + **Alternatively**, you can use the pdt decorator to work with PDT information in one of your own views. + To do this, add the decorator to one of your views. + + # views.py + from paypal.standard.pdt.decorators import pdt + + @pdt + def view_func(request, *args, **kwargs): + ... + The decorator checks for any GET parameters corresponding to a PDT call and adds the keyword arguments `pdt_active`, `pdt_failed` and `pdt` to the view call. + +1. Set `PAYPAL_IGNORE_INVALID_PDT` to `True` to stop saving data about failed transactions. This might save you from an attack of bad inserts. + Using PayPal Payments Standard with Subscriptions: -------------------------------------------------- @@ -195,7 +217,7 @@ Use this method to encrypt your button so sneaky gits don't try to hack it. Than [https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert](https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert) - [https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert](https://www.sandbox.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert) + [https://www.sandbox.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert](https://www.sandbox.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert) 1. Copy your `cert id` - you'll need it in two steps. It's on the screen where you uploaded your public key. diff --git a/paypal/standard/conf.py b/paypal/standard/conf.py index 5c5fd45..66d8bc8 100644 --- a/paypal/standard/conf.py +++ b/paypal/standard/conf.py @@ -5,8 +5,7 @@ class PayPalSettingsError(Exception): TEST = getattr(settings, "PAYPAL_TEST", True) - - +IGNORE_INVALID_PDT = getattr(settings, "PAYPAL_IGNORE_INVALID_PDT", False) RECEIVER_EMAIL = settings.PAYPAL_RECEIVER_EMAIL @@ -15,7 +14,7 @@ class PayPalSettingsError(Exception): SANDBOX_POSTBACK_ENDPOINT = "https://www.sandbox.paypal.com/cgi-bin/webscr" # Images -IMAGE = getattr(settings, "PAYPAL_IMAGE", "http://images.paypal.com/images/x-click-but01.gif") +IMAGE = getattr(settings, "PAYPAL_IMAGE", "https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif") SUBSCRIPTION_IMAGE = getattr(settings, "PAYPAL_SUBSCRIPTION_IMAGE", "https://www.paypal.com/en_US/i/btn/btn_subscribeCC_LG.gif") DONATION_IMAGE = getattr(settings, "PAYPAL_DONATION_IMAGE", "https://www.paypal.com/en_US/i/btn/btn_donateCC_LG.gif") SANDBOX_IMAGE = getattr(settings, "PAYPAL_SANDBOX_IMAGE", "https://www.sandbox.paypal.com/en_US/i/btn/btn_buynowCC_LG.gif") diff --git a/paypal/standard/forms.py b/paypal/standard/forms.py index e9992a5..68abc97 100644 --- a/paypal/standard/forms.py +++ b/paypal/standard/forms.py @@ -59,6 +59,8 @@ class PayPalPaymentsForm(forms.Form): item_name = forms.CharField(widget=ValueHiddenInput()) item_number = forms.CharField(widget=ValueHiddenInput()) quantity = forms.CharField(widget=ValueHiddenInput()) + tax_rate = forms.FloatField(widget=ValueHiddenInput()) + tax = forms.FloatField(widget=ValueHiddenInput()) # Subscription Related. a1 = forms.CharField(widget=ValueHiddenInput()) # Trial 1 Price @@ -100,6 +102,12 @@ def __init__(self, button_type="buy", *args, **kwargs): self.button_type = button_type def render(self): + if TEST: + return self.sandbox(); + else: + return self.renderProd(); + + def renderProd(self): return mark_safe(u"""
%s diff --git a/paypal/standard/models.py b/paypal/standard/models.py index c07e2fd..c2f9dc2 100644 --- a/paypal/standard/models.py +++ b/paypal/standard/models.py @@ -3,7 +3,7 @@ from django.db import models from django.conf import settings from paypal.standard.helpers import duplicate_txn_id, check_secret -from paypal.standard.conf import RECEIVER_EMAIL, POSTBACK_ENDPOINT, SANDBOX_POSTBACK_ENDPOINT +from paypal.standard.conf import RECEIVER_EMAIL, POSTBACK_ENDPOINT, SANDBOX_POSTBACK_ENDPOINT, IGNORE_INVALID_PDT ST_PP_ACTIVE = 'Active' ST_PP_CANCELLED = 'Cancelled' @@ -245,8 +245,9 @@ def verify(self, item_check_callable=None): """ self.response = self._postback() - self._verify_postback() - if not self.flag: + self._verify_postback() + invalid_paypal_obj = self.flag + if not invalid_paypal_obj: if self.is_transaction(): if self.payment_status not in self.PAYMENT_STATUS_CHOICES: self.set_flag("Invalid payment_status. (%s)" % self.payment_status) @@ -262,7 +263,9 @@ def verify(self, item_check_callable=None): # @@@ Run a different series of checks on recurring payments. pass - self.save() + if not (invalid_paypal_obj and IGNORE_INVALID_PDT): + self.save() + self.send_signals() def verify_secret(self, form_instance, secret): diff --git a/paypal/standard/pdt/decorators.py b/paypal/standard/pdt/decorators.py new file mode 100644 index 0000000..4e73d58 --- /dev/null +++ b/paypal/standard/pdt/decorators.py @@ -0,0 +1,80 @@ +# -*- encoding: utf-8 -*- + +from paypal.standard.pdt.models import PayPalPDT +from paypal.standard.pdt.forms import PayPalPDTForm +from django.views.decorators.http import require_GET + +def pdt(dummy=None, item_check_callable=None): + """Parses out GET parameters corresponding to a paypal PDT request and adds `pdt_active`, `pdt_failed` and `pdt` to the call **kwargs. + + Payment data transfer implementation: http://tinyurl.com/c9jjmw + + `item_check_callable` (Optional) is a callable that must take an instance of PayPalPDT + as a parameter and return a tuple (False, None) if the item is valid. Should return (True, "reason") + if the item isn't valid. This function should check that `mc_gross`, `mc_currency` `item_name` and + `item_number` are all correct. + + `dummy` DO NOT set value for this. So when you want to set value for `item_check_callable` use named param. + So it would be @pdt(item_check_callable=func). When the `dummy` is a callable `f` then it behaves as just @pdt(f). + """ + + def inner_pdt(f): + #{ + @require_GET + def aux(request, *args, **kwargs): + pdt_obj = None + pdt_active = False + txn_id = request.GET.get('tx', None) + if txn_id is not None: + txn_id = txn_id.strip() + if not txn_id: #i.e. empty txn_id + txn_id = None + + failed = False + pdt_duplicate = False + if txn_id is not None: + pdt_active = True + # If an existing transaction with the id tx exists: use it + try: + pdt_obj = PayPalPDT.objects.get(txn_id=txn_id) + pdt_duplicate = True + except PayPalPDT.DoesNotExist: + # This is a new transaction so we continue processing PDT request + pass + + if pdt_obj is None: + form = PayPalPDTForm(request.GET) + if form.is_valid(): + try: + pdt_obj = form.save(commit=False) + except Exception, e: + error = repr(e) + failed = True + else: + error = form.errors + failed = True + + if failed: + pdt_obj = PayPalPDT() + pdt_obj.set_flag("Invalid form. %s" % error) + + pdt_obj.initialize(request) + + if not failed: + # The PDT object gets saved during verify + pdt_obj.verify(item_check_callable) + else: + pass # we ignore any PDT requests that don't have a transaction id + + #pdt_active = True => txn_id was not None + #pdt_failed = True => pdt_obj has invalid data + #pdt_duplicate = True => txn_id is known and already processed. pdt_obj contains that data. + kwargs.update({'pdt_active': pdt_active, 'pdt_failed': failed, 'pdt_obj': pdt_obj, 'pdt_duplicate': pdt_duplicate}) + return f(request, *args, **kwargs) + + return aux + #} + if hasattr(dummy, '__call__'): #This is to make sure that we can call @pdt without any parenthesis. + return inner_pdt(dummy) #dummy is function now + else: + return inner_pdt diff --git a/paypal/standard/pdt/models.py b/paypal/standard/pdt/models.py index 72e7d7d..0caac8b 100644 --- a/paypal/standard/pdt/models.py +++ b/paypal/standard/pdt/models.py @@ -7,7 +7,7 @@ from django.http import QueryDict from django.utils.http import urlencode from paypal.standard.models import PayPalStandardBase -from paypal.standard.conf import POSTBACK_ENDPOINT, SANDBOX_POSTBACK_ENDPOINT +from paypal.standard.conf import POSTBACK_ENDPOINT, SANDBOX_POSTBACK_ENDPOINT, IGNORE_INVALID_PDT from paypal.standard.pdt.signals import pdt_successful, pdt_failed # ### Todo: Move this logic to conf.py: @@ -76,11 +76,12 @@ def _verify_postback(self): except ValueError, e: pass - qd = QueryDict('', mutable=True) - qd.update(response_dict) - qd.update(dict(ipaddress=self.ipaddress, st=self.st, flag_info=self.flag_info)) - pdt_form = PayPalPDTForm(qd, instance=self) - pdt_form.save(commit=False) + if not (self.flag and IGNORE_INVALID_PDT): + qd = QueryDict('', mutable=True) + qd.update(response_dict) + qd.update(dict(ipaddress=self.ipaddress, st=self.st, flag_info=self.flag_info)) + pdt_form = PayPalPDTForm(qd, instance=self) + pdt_form.save(commit=False) def send_signals(self): # Send the PDT signals... diff --git a/paypal/standard/pdt/views.py b/paypal/standard/pdt/views.py index 0993411..401e92f 100644 --- a/paypal/standard/pdt/views.py +++ b/paypal/standard/pdt/views.py @@ -3,48 +3,12 @@ from django.template import RequestContext from django.shortcuts import render_to_response from django.views.decorators.http import require_GET -from paypal.standard.pdt.models import PayPalPDT -from paypal.standard.pdt.forms import PayPalPDTForm - +from paypal.standard.pdt.decorators import pdt @require_GET -def pdt(request, item_check_callable=None, template="pdt/pdt.html", context=None): +@pdt +def pdt(request, pdt_active=True, pdt_failed=False, pdt_obj=None, template="pdt/pdt.html", context=None): """Payment data transfer implementation: http://tinyurl.com/c9jjmw""" - context = context or {} - pdt_obj = None - txn_id = request.GET.get('tx') - failed = False - if txn_id is not None: - # If an existing transaction with the id tx exists: use it - try: - pdt_obj = PayPalPDT.objects.get(txn_id=txn_id) - except PayPalPDT.DoesNotExist: - # This is a new transaction so we continue processing PDT request - pass - - if pdt_obj is None: - form = PayPalPDTForm(request.GET) - if form.is_valid(): - try: - pdt_obj = form.save(commit=False) - except Exception, e: - error = repr(e) - failed = True - else: - error = form.errors - failed = True - - if failed: - pdt_obj = PayPalPDT() - pdt_obj.set_flag("Invalid form. %s" % error) - - pdt_obj.initialize(request) - - if not failed: - # The PDT object gets saved during verify - pdt_obj.verify(item_check_callable) - else: - pass # we ignore any PDT requests that don't have a transaction id - - context.update({"failed":failed, "pdt_obj":pdt_obj}) - return render_to_response(template, context, RequestContext(request)) \ No newline at end of file + context = context or {} + context.update({"failed":pdt_failed, "pdt_obj":pdt_obj}) + return render_to_response(template, context, RequestContext(request))