move to new payment flow (bug 627077)

This commit is contained in:
Andy McKay 2011-01-26 17:06:45 +00:00
Родитель 5bf877b415
Коммит f8f7eac524
18 изменённых файлов: 1092 добавлений и 245 удалений

Просмотреть файл

@ -25,10 +25,7 @@
<div class="aux{% if show_install %}-bottom{% endif %}">
<div class="button-wrapper">
<a class="button contribute prominent"
{# The id triggers a dropdown, so don't expose it if there isn't a
suggested amount (i.e., only one choice). #}
{% if has_suggested %}id="contribute-button"{% endif %}
<a class="button contribute prominent" id="contribute-button"
href="{{ url('addons.contribute', addon.id)|urlparams(src=src) }}">
<b></b>{{ _('Contribute') }}
</a>
@ -54,7 +51,7 @@
</div>{# /suggestion #}
</div>{# /aux #}
{% if has_suggested %}
<div id="contribute-box" class="jqmWindow">
<form action="{{ url('addons.contribute', addon.id) }}" method="get">
<input type="hidden" name="source" value="{{ src }}"/>
@ -89,7 +86,18 @@
{{ _('The maximum contribution amount is {0}.')|f(
settings.MAX_CONTRIBUTION|currencyfmt('USD')) }}
</div>
<div class="error" id="contrib-too-little">
{{ _('The minimum contribution amount is {0}.')|f(
0.01|currencyfmt('USD')) }}
</div>
<div class="error" id="contrib-not-entered">
{{ _('Contribution amount must be a number.') }}
</div>
<div class="error" id="paypal-error">
{{ _('There was an error communicating with paypal. Please try again later.') }}
</div>
<ul>
{% if has_suggested %}
<li>
<input type="radio" name="type" value="suggested"
id="contrib-suggested" checked="checked"/>
@ -99,9 +107,11 @@
addon.suggested_amount|currencyfmt('USD')) }}
</label>
</li>
{% endif %}
<li>
<input type="radio" name="type" value="onetime"
id="contrib-onetime"/>
id="contrib-onetime"
{% if not has_suggested %}checked="checked"{% endif %}/>
<label>
{# L10n: {0} is a currency symbol (e.g., $),
{1} is an amount input field #}
@ -109,15 +119,6 @@
'$', '<input type="text" name="onetime-amount" value=""/>')|safe }}
</label>
</li>
<li>
<input type="radio" name="type" value="monthly"
id="contrib-monthly"/>
<label for="contrib-monthly">
{# L10n: {0} is a currency symbol (e.g., $),
{1} is an amount input field #}
{{ _('A regular monthly contribution of {0} {1}')|f(
'$', '</label><input type="text" name="monthly-amount" value=""/>')|safe }}
</li>
</ul>
<h4 class="comment">
@ -134,7 +135,7 @@
<span class="cancel"><a href="#">{{ _('No Thanks') }}</a></span>
</form>
</div>{# /contribute-box #}
{% endif %}
</div>{# /notification #}

Просмотреть файл

@ -17,7 +17,6 @@
(None, addon.name)]) }}
{% set version = addon.current_version %}
<hgroup>
<h2 class="addon"{{ addon.name|locale_html }}>
<img src="{{ addon.icon_url }}" class="icon"/>

Просмотреть файл

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}{{ page_title(addon.name) }}{% endblock %}
{% block bodyclass %}inverse{% endblock %}
{% block site_header %}
{% endblock %}
{% block content %}
<hgroup>
<h2 class="addon"{{ addon.name|locale_html }}>
<img src="{{ addon.icon_url }}" class="icon"/>
<span>
{{ addon.name }}
</span>
{% if version and not addon.is_selfhosted() %}
<span class="version">{{ version.version }}</span>
{% endif %}
</h2>
</hgroup>
{% if status == 'cancel' %}
<h4>{{ _('Payment cancelled') }}</h4>
{% else %}
<h4>{{ _('Payment completed') }}</h4>
{% endif %}
<p><a href="{{ url('addons.detail', addon.slug) }}">{{ _('Return to the addon.') }}</a></p>
{% endblock %}

Просмотреть файл

@ -1,6 +1,6 @@
{% if addon.takes_contributions %}
{% set base = url('addons.contribute', addon.slug)|urlparams(src='direct') %}
{% set url = base|urlparams(type='suggested')|escape %}
{% set _url = base|urlparams(type='suggested')|escape %}
{% set amt = addon.suggested_amount|currencyfmt('USD') %}
<div class="contribute">
{% if addon.suggested_amount is not none %}
@ -8,20 +8,20 @@
{% set charity_name = addon.charity.name %}
{% if addon.charity_id == amo.FOUNDATION_ORG %}
{% trans %}
Support this add-on: <a href="{{ url }}">Contribute {{ amt }}</a> to the {{ charity_name }}
Support this add-on: <a href="{{ _url }}" class="suggested-amount">Contribute {{ amt }}</a> to the {{ charity_name }}
{% endtrans %}
{% else %}
{% trans %}
Support this add-on: <a href="{{ url }}">Contribute {{ amt }}</a> to {{ charity_name }}
Support this add-on: <a href="{{ _url }}" class="suggested-amount">Contribute {{ amt }}</a> to {{ charity_name }}
{% endtrans %}
{% endif %}
{% else %}
{% trans %}
Support this add-on: <a href="{{ url }}">Contribute {{ amt }}</a>
Support this add-on: <a href="{{ _url }}" class="suggested-amount">Contribute {{ amt }}</a>
{% endtrans %}
{% endif %}
{% else %}
<a href="{{ base }}">{{ _('Support this add-on') }}</a>
<a href="{{ url('addons.detail', addon.slug) }}#contribute-confirm" class="no-suggested-amount">{{ _('Support this add-on') }}</a>
{% endif %}
</div>
{% endif %}

Просмотреть файл

@ -74,7 +74,6 @@ class TestHelpers(test_utils.TestCase):
doc = PyQuery(s)
# make sure input boxes are rendered correctly (bug 555867)
assert doc('input[name=onetime-amount]').length == 1
assert doc('input[name=monthly-amount]').length == 1
class TestPerformanceNote(test_utils.TestCase):

Просмотреть файл

@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from email import utils
from decimal import Decimal
import re
import urlparse
import json
from django import test
from django.conf import settings
from django.core import mail
from django.core.cache import cache
from django.db import connection
from django.utils import translation
from django.utils.encoding import iri_to_uri
from mock import patch
@ -21,7 +21,6 @@ import amo
from amo.helpers import absolutify
from amo.urlresolvers import reverse
from amo.tests.test_helpers import AbuseBase, AbuseDisabledBase
from addons import views
from addons.models import Addon, AddonUser, Charity
from files.models import File
from stats.models import Contribution
@ -146,142 +145,96 @@ class TestContributeInstalled(test_utils.TestCase):
class TestContribute(test_utils.TestCase):
fixtures = ['base/apps', 'base/addon_3615', 'base/addon_592']
def setUp(self):
self.addon = Addon.objects.get(pk=592)
self.detail_url = reverse('addons.detail', args=[self.addon.slug])
@patch('paypal.get_paykey')
def client_get(self, get_paykey, **kwargs):
get_paykey.return_value = 'abc'
url = reverse('addons.contribute', args=kwargs.pop('rev'))
if 'qs' in kwargs:
url = url + kwargs.pop('qs')
return self.client.get(url, kwargs.get('data', {}))
def test_invalid_is_404(self):
"""we get a 404 in case of invalid addon id"""
response = self.client.get(reverse('addons.contribute', args=[1]))
response = self.client_get(rev=[1])
eq_(response.status_code, 404)
def test_redirect_params_no_type(self):
"""Test that we have the required ppal params when no type is given"""
response = self.client.get(reverse('addons.contribute',
args=['a592']), follow=True)
redirect_url = response.redirect_chain[0][0]
required_params = ['bn', 'business', 'charset', 'cmd', 'item_name',
'no_shipping', 'notify_url',
'return', 'item_number']
for param in required_params:
assert(redirect_url.find(param + '=') > -1), \
"param [%s] not found" % param
def test_params_common(self):
"""Test for the some of the common values"""
response = self.client_get(rev=['a592'])
eq_(response.status_code, 302)
con = Contribution.objects.all()[0]
eq_(con.charity_id, None)
eq_(con.addon_id, 592)
eq_(con.amount, Decimal('20.00'))
def test_redirect_params_common(self):
"""Test for the common values that do not change based on type,
Check that they have expected values"""
response = self.client.get(reverse('addons.contribute',
args=['a592']), follow=True)
redirect_url = response.redirect_chain[0][0]
assert(re.search('business=([^&]+)', redirect_url))
common_params = {'bn': r'-AddonID592',
'business': r'gmailsmime%40seantek.com',
'charset': r'utf-8',
'cmd': r'_donations',
'item_name': r'Contribution\+for\+Gmail\+S%2FMIME',
'no_shipping': r'1',
'notify_url': r'%2Fservices%2Fpaypal',
'return': r'x',
'item_number': r'[a-f\d]{32}'}
message = 'param [%s] unexpected value: given [%s], ' \
+ 'expected pattern [%s]'
for param, value_pattern in common_params.items():
match = re.search(r'%s=([^&]+)' % param, redirect_url)
assert(match and re.search(value_pattern, match.group(1))), \
message % (param, match.group(1), value_pattern)
def test_redirect_params_type_suggested(self):
"""Test that we have the required ppal param when type
suggested is given"""
request_params = '?type=suggested'
response = self.client.get(reverse('addons.contribute',
args=['a592']) + request_params,
follow=True)
redirect_url = response.redirect_chain[0][0]
required_params = ['amount', 'bn', 'business', 'charset',
'cmd', 'item_name', 'no_shipping', 'notify_url',
'return', 'item_number']
for param in required_params:
assert(redirect_url.find(param + '=') > -1), \
"param [%s] not found" % param
def test_redirect_params_type_onetime(self):
"""Test that we have the required ppal param when
type onetime is given"""
def test_custom_amount(self):
"""Test that we have the custom amount when given."""
request_params = '?type=onetime&onetime-amount=42'
response = self.client.get(reverse('addons.contribute',
args=['a592']) + request_params,
follow=True)
redirect_url = response.redirect_chain[0][0]
required_params = ['amount', 'bn', 'business', 'charset', 'cmd',
'item_name', 'no_shipping', 'notify_url',
'return', 'item_number']
for param in required_params:
assert(redirect_url.find(param + '=') > -1), \
"param [%s] not found" % param
response = self.client_get(rev=['a592'], qs=request_params)
eq_(response.status_code, 302)
eq_(Contribution.objects.all()[0].amount, Decimal('42.00'))
assert(redirect_url.find('amount=42') > -1)
def test_ppal_json_switch(self):
response = self.client_get(rev=['a592'], qs='?result_type=json')
eq_(response.status_code, 200)
response = self.client_get(rev=['a592'])
eq_(response.status_code, 302)
def test_ppal_return_url_not_relative(self):
response = self.client.get(reverse('addons.contribute',
args=['a592']), follow=True)
redirect_url = response.redirect_chain[0][0]
assert(re.search('\?|&return=https?%3A%2F%2F', redirect_url)), \
("return URL param did not start w/ "
"http%3A%2F%2F (http://) [%s]" % redirect_url)
def test_redirect_params_type_monthly(self):
"""Test that we have the required ppal param when
type monthly is given"""
request_params = '?type=monthly&monthly-amount=42'
response = self.client.get(reverse('addons.contribute',
args=['a592']) + request_params,
follow=True)
redirect_url = response.redirect_chain[0][0]
required_params = ['no_note', 'a3', 't3', 'p3', 'bn', 'business',
'charset', 'cmd', 'item_name', 'no_shipping',
'notify_url', 'return', 'item_number']
for param in required_params:
assert(redirect_url.find(param + '=') > -1), \
"param [%s] not found" % param
assert(redirect_url.find('cmd=_xclick-subscriptions') > -1), \
'param a3 was not 42'
assert(redirect_url.find('p3=12') > -1), 'param p3 was not 12'
assert(redirect_url.find('t3=M') > -1), 'param t3 was not M'
assert(redirect_url.find('a3=42') > -1), 'param a3 was not 42'
assert(redirect_url.find('no_note=1') > -1), 'param no_note was not 1'
def test_paypal_bounce(self):
"""Paypal is retarded and posts to this page."""
args = dict(args=['a3615'])
r = self.client.post(reverse('addons.thanks', **args))
self.assertRedirects(r, reverse('addons.detail', **args))
response = self.client_get(rev=['a592'], qs='?result_type=json')
assert json.loads(response.content)['url'].startswith('http')
def test_unicode_comment(self):
r = self.client.get(reverse('addons.contribute', args=['a592']),
{'comment': u'版本历史记录'})
eq_(r.status_code, 302)
assert r['Location'].startswith(settings.PAYPAL_CGI_URL)
res = self.client_get(rev=['a592'],
data={'comment': u'版本历史记录'})
eq_(res.status_code, 302)
assert settings.PAYPAL_FLOW_URL in res._headers['location'][1]
eq_(Contribution.objects.all()[0].comment, u'版本历史记录')
def test_organization(self):
c = Charity.objects.create(name='moz', url='moz.com', paypal='mozcom')
addon = Addon.objects.get(id=592)
addon.update(charity=c)
c = Charity.objects.create(name='moz', url='moz.com',
paypal='test@moz.com')
self.addon.update(charity=c)
r = self.client.get(reverse('addons.contribute', args=['a592']))
r = self.client_get(rev=['a592'])
eq_(r.status_code, 302)
qs = dict(urlparse.parse_qsl(r['Location']))
eq_(qs['item_name'], 'Contribution for moz')
eq_(qs['business'], 'mozcom')
contrib = Contribution.objects.get(addon=addon)
eq_(addon.charity_id, contrib.charity_id)
eq_(self.addon.charity_id,
self.addon.contribution_set.all()[0].charity_id)
def test_no_org(self):
addon = Addon.objects.get(id=592)
r = self.client.get(reverse('addons.contribute', args=['a592']))
r = self.client_get(rev=['a592'])
eq_(r.status_code, 302)
contrib = Contribution.objects.get(addon=addon)
eq_(contrib.charity_id, None)
eq_(self.addon.contribution_set.all()[0].charity_id, None)
def test_no_suggested_amount(self):
self.addon.update(suggested_amount=None)
res = self.client_get(rev=['a592'])
eq_(res.status_code, 302)
eq_(settings.DEFAULT_SUGGESTED_CONTRIBUTION,
self.addon.contribution_set.all()[0].amount)
def test_form_suggested_amount(self):
res = self.client.get(self.detail_url)
doc = pq(res.content)
eq_(len(doc('#contribute-box input')), 4)
def test_form_no_suggested_amount(self):
self.addon.update(suggested_amount=None)
res = self.client.get(self.detail_url)
doc = pq(res.content)
eq_(len(doc('#contribute-box input')), 3)
@patch('paypal.get_paykey')
def test_paypal_error_json(self, get_paykey, **kwargs):
get_paykey.return_value = None
res = self.client.get('%s?%s' % (
reverse('addons.contribute', args=[self.addon.slug]),
'result_type=json'))
assert not json.loads(res.content)['paykey']
class TestDeveloperPages(test_utils.TestCase):
@ -795,15 +748,15 @@ class TestStatus(test_utils.TestCase):
def new_version(self, status):
v = Version.objects.create(addon=self.addon)
f = File.objects.create(version=v, status=status)
File.objects.create(version=v, status=status)
return v
def test_public_new_lite_version(self):
v = self.new_version(amo.STATUS_LITE)
self.new_version(amo.STATUS_LITE)
eq_(self.addon.get_current_version(), self.version)
def test_public_new_nominated_version(self):
v = self.new_version(amo.STATUS_NOMINATED)
self.new_version(amo.STATUS_NOMINATED)
eq_(self.addon.get_current_version(), self.version)
def test_public_new_public_version(self):
@ -811,12 +764,12 @@ class TestStatus(test_utils.TestCase):
eq_(self.addon.get_current_version(), v)
def test_public_new_unreviewed_version(self):
v = self.new_version(amo.STATUS_UNREVIEWED)
self.new_version(amo.STATUS_UNREVIEWED)
eq_(self.addon.get_current_version(), self.version)
def test_lite_new_unreviewed_version(self):
self.addon.update(status=amo.STATUS_LITE)
v = self.new_version(amo.STATUS_UNREVIEWED)
self.new_version(amo.STATUS_UNREVIEWED)
eq_(self.addon.get_current_version(), self.version)
def test_lite_new_lan_version(self):
@ -976,18 +929,19 @@ class TestPrivacyPolicy(test_utils.TestCase):
self.assertRedirects(r, reverse('addons.detail', args=['a11730']))
def test_paypal_language_code():
def check(lc):
d = views.contribute_url_params('bz', 32, 'name', 'url')
eq_(d['lc'], lc)
check('US')
translation.activate('it')
check('IT')
translation.activate('ru-DE')
check('RU')
# When Embedded Payments support this, we can worry about it.
#def test_paypal_language_code():
# def check(lc):
# d = views.contribute_url_params('bz', 32, 'name', 'url')
# eq_(d['lc'], lc)
#
# check('US')
#
# translation.activate('it')
# check('IT')
#
# translation.activate('ru-DE')
# check('RU')
class TestAddonSharing(test_utils.TestCase):

Просмотреть файл

@ -1,6 +1,5 @@
from django.conf.urls.defaults import patterns, url, include
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from . import views
@ -21,10 +20,9 @@ detail_patterns = patterns('',
{'page': 'roadblock'}, name='addons.roadblock'),
url('^contribute/installed/', views.developers,
{'page': 'installed'}, name='addons.installed'),
url('^contribute/thanks',
csrf_exempt(lambda r, addon_id: redirect('addons.detail', addon_id)),
name='addons.thanks'),
url('^contribute/', views.contribute, name='addons.contribute'),
url('^contribute/$', views.contribute, name='addons.contribute'),
url('^contribute/(?P<status>cancel|complete)$',
views.paypal_result, name='addons.paypal'),
('^about$', lambda r, addon_id: redirect('addons.installed',
addon_id, permanent=True)),

Просмотреть файл

@ -1,4 +1,5 @@
import hashlib
import json
import random
import uuid
from operator import attrgetter
@ -10,7 +11,6 @@ from django.shortcuts import get_list_or_404, get_object_or_404, redirect
from django.utils.translation import trans_real as translation
from django.utils import http as urllib
from django.views.decorators.cache import cache_page
import caching.base as caching
import jingo
import jinja2
@ -27,6 +27,7 @@ from amo.models import manual_order
from amo import urlresolvers
from amo.urlresolvers import reverse
from bandwagon.models import Collection, CollectionFeature, CollectionPromo
import paypal
from reviews.forms import ReviewForm
from reviews.models import Review
from sharing.views import share as share_redirect
@ -39,6 +40,7 @@ from .models import Addon, MiniAddon
from .decorators import addon_view_factory
log = commonware.log.getLogger('z.addons')
paypal_log = commonware.log.getLogger('z.paypal')
addon_view = addon_view_factory(qs=Addon.objects.valid)
@ -412,19 +414,47 @@ def developers(request, addon, page):
@addon_view
def contribute(request, addon):
contrib_type = request.GET.get('type', '')
contrib_type = request.GET.get('type', 'suggested')
is_suggested = contrib_type == 'suggested'
source = request.GET.get('source', '')
comment = request.GET.get('comment', '')
amount = {
'suggested': addon.suggested_amount,
'onetime': request.GET.get('onetime-amount', ''),
'monthly': request.GET.get('monthly-amount', '')}.get(contrib_type, '')
'onetime': request.GET.get('onetime-amount', '')}.get(contrib_type, '')
if not amount:
amount = settings.DEFAULT_SUGGESTED_CONTRIBUTION
contribution_uuid = hashlib.md5(str(uuid.uuid4())).hexdigest()
uuid_qs = urllib.urlencode({'uuid': contribution_uuid})
contrib = Contribution(addon_id=addon.id,
if addon.charity:
name, paypal_id = addon.charity.name, addon.charity.paypal
else:
name, paypal_id = addon.name, addon.paypal_id
contrib_for = _(u'Contribution for {0}').format(jinja2.escape(name))
paykey = None
try:
paykey = paypal.get_paykey({
'return_url': absolutify('%s?%s' % (reverse('addons.paypal',
args=[addon.slug, 'complete']),
uuid_qs)),
'cancel_url': absolutify('%s?%s' % (reverse('addons.paypal',
args=[addon.slug, 'cancel']),
uuid_qs)),
'uuid': contribution_uuid,
'amount': str(amount),
'email': paypal_id,
'ip': request.META.get('REMOTE_ADDR'),
'memo': contrib_for})
except paypal.AuthError, error:
paypal_log.error('Authentication error: %s' % error)
except Exception, error:
paypal_log.error('Error: %s' % error)
if paykey:
contrib = Contribution(addon_id=addon.id,
charity_id=addon.charity_id,
amount=amount,
source=source,
@ -433,72 +463,35 @@ def contribute(request, addon):
uuid=str(contribution_uuid),
is_suggested=is_suggested,
suggested_amount=addon.suggested_amount,
comment=comment)
contrib.save()
comment=comment,
paykey=paykey)
contrib.save()
return_url = "%s?%s" % (reverse('addons.thanks', args=[addon.slug]),
urllib.urlencode({'uuid': contribution_uuid}))
# L10n: {0} is an add-on name.
if addon.charity:
name, paypal = addon.charity.name, addon.charity.paypal
url = '%s?paykey=%s' % (settings.PAYPAL_FLOW_URL, paykey)
if request.GET.get('result_type') == 'json' or request.is_ajax():
# If there was an error getting the paykey, then JSON will
# not have a paykey and the JS can cope appropriately.
return http.HttpResponse(json.dumps({'url': url, 'paykey': paykey}),
content_type='application/json')
elif paykey is None:
# If there was an error getting the paykey, raise this.
raise
return http.HttpResponseRedirect(url)
@addon_view
def paypal_result(request, addon, status):
uuid = request.GET.get('uuid')
if not uuid:
raise http.Http404()
if status == 'cancel':
log.info('User cancelled contribution: %s' % uuid)
else:
name, paypal = addon.name, addon.paypal_id
contrib_for = _(u'Contribution for {0}').format(jinja2.escape(name))
redirect_url_params = contribute_url_params(
paypal,
addon.id,
contrib_for,
absolutify(return_url),
amount,
contribution_uuid,
contrib_type == 'monthly',
comment)
return http.HttpResponseRedirect(settings.PAYPAL_CGI_URL
+ '?'
+ urllib.urlencode(redirect_url_params))
def contribute_url_params(business, addon_id, item_name, return_url,
amount='', item_number='',
monthly=False, comment=''):
lang = translation.get_language()
try:
paypal_lang = settings.PAYPAL_COUNTRYMAP[lang]
except KeyError:
lang = lang.split('-')[0]
paypal_lang = settings.PAYPAL_COUNTRYMAP.get(lang, 'US')
# Get all the data elements that will be URL params
# on the Paypal redirect URL.
data = {'business': business,
'item_name': item_name,
'item_number': item_number,
'bn': settings.PAYPAL_BN + '-AddonID' + str(addon_id),
'no_shipping': '1',
'return': return_url,
'charset': 'utf-8',
'lc': paypal_lang,
'notify_url': "%s%s" % (settings.SERVICES_URL,
reverse('amo.paypal'))}
if not monthly:
data['cmd'] = '_donations'
if amount:
data['amount'] = amount
else:
data.update({
'cmd': '_xclick-subscriptions',
'p3': '12', # duration: for 12 months
't3': 'M', # time unit, 'M' for month
'a3': amount, # recurring contribution amount
'no_note': '1'}) # required: no "note" text field for user
if comment:
data['custom'] = comment
return data
log.info('User completed contribution: %s' % uuid)
response = jingo.render(request, 'addons/paypal_result.html',
{'addon': addon, 'status': status})
response['x-frame-options'] = 'allow'
return response
@addon_view

Просмотреть файл

@ -13,9 +13,10 @@ from nose.tools import eq_
from pyquery import PyQuery as pq
import test_utils
from addons.models import Addon
from amo.urlresolvers import reverse
from amo.pyquery_wrapper import PyQuery
from stats.models import SubscriptionEvent
from stats.models import SubscriptionEvent, Contribution
URL_ENCODED = 'application/x-www-form-urlencoded'
@ -206,6 +207,39 @@ class TestPaypal(test_utils.TestCase):
eq_(response.content, 'Unknown error.')
class TestEmbeddedPaymentsPaypal(test_utils.TestCase):
fixtures = ['base/addon_3615']
def setUp(self):
self.url = reverse('amo.paypal')
self.addon = Addon.objects.get(pk=3615)
def urlopener(self, status):
m = Mock()
m.readline.return_value = status
return m
@patch('amo.views.urllib2.urlopen')
def test_success(self, urlopen):
uuid = 'e76059abcf747f5b4e838bf47822e6b2'
Contribution.objects.create(uuid=uuid, addon=self.addon)
data = {'tracking_id': uuid, 'payment_status': 'Completed'}
urlopen.return_value = self.urlopener('VERIFIED')
response = self.client.post(self.url, data)
eq_(response.content, 'Success!')
@patch('amo.views.urllib2.urlopen')
def test_wrong_uuid(self, urlopen):
uuid = 'e76059abcf747f5b4e838bf47822e6b2'
Contribution.objects.create(uuid=uuid, addon=self.addon)
data = {'tracking_id': 'sdf', 'payment_status': 'Completed'}
urlopen.return_value = self.urlopener('VERIFIED')
response = self.client.post(self.url, data)
eq_(response.content, 'Contribution not found')
def test_jsi18n_caching():
"""The jsi18n catalog should be cached for a long time."""
# Get the url from a real page so it includes the build id.

Просмотреть файл

@ -17,7 +17,6 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import caching.invalidation
import celery.exceptions
import commonware.log
import jingo
import phpserialize as php
@ -232,7 +231,6 @@ def paypal(request):
def _paypal(request):
def _log_error_with_data(msg, request):
"""Log a message along with some of the POST info from PayPal."""
@ -280,8 +278,16 @@ def _paypal(request):
SubscriptionEvent.objects.create(post_data=php.serialize(post))
return http.HttpResponse('Success!')
# List of (old, new) codes so we can transpose the data for
# embedded payments.
for old, new in [('payment_status', 'status'),
('item_number', 'tracking_id'),
('txn_id', 'tracking_id')]:
if old not in post and new in post:
post[old] = post[new]
# We only care about completed transactions.
if post.get('payment_status') != 'Completed':
if post.get('payment_status', '').lower() != 'completed':
return http.HttpResponse('Payment not completed')
# Make sure transaction has not yet been processed.
@ -309,7 +315,9 @@ def _paypal(request):
return http.HttpResponseServerError('Contribution not found')
c.transaction_id = post['txn_id']
c.amount = post['mc_gross']
# Embedded payments does not send an mc_gross.
if 'mc_gross' in post:
c.amount = post['mc_gross']
c.uuid = None
c.post_data = php.serialize(post)
c.save()

Просмотреть файл

@ -1,11 +1,85 @@
import contextlib
import socket
import urllib
import urllib2
import urlparse
from django.conf import settings
from django.utils.http import urlencode
import commonware.log
from amo.helpers import absolutify
from amo.urlresolvers import reverse
class PaypalError(Exception):
id = None
class AuthError(Exception):
pass
errors = {'520003': AuthError}
paypal_log = commonware.log.getLogger('z.paypal')
def get_paykey(data):
"""
Gets a paykey from Paypal. Need to pass in the following in data:
return_url and cancel_url: where user goes back to (required)
email: who the money is going to (required)
amount: the amount of money (required)
ip: ip address of end user (required)
uuid: contribution_uuid (required)
memo: any nice message
"""
request = urllib2.Request(settings.PAYPAL_PAY_URL)
for key, value in [
('security-userid', settings.PAYPAL_USER),
('security-password', settings.PAYPAL_PASSWORD),
('security-signature', settings.PAYPAL_SIGNATURE),
('application-id', settings.PAYPAL_APP_ID),
('device-ipaddress', data['ip']),
('request-data-format', 'NV'),
('response-data-format', 'NV')]:
request.add_header('X-PAYPAL-%s' % key.upper(), value)
paypal_data = {
'actionType': 'PAY',
'requestEnvelope.errorLanguage': 'US',
'currencyCode': 'USD',
'cancelUrl': data['cancel_url'],
'returnUrl': data['return_url'],
'receiverList.receiver(0).email': data['email'],
'receiverList.receiver(0).amount': data['amount'],
'receiverList.receiver(0).invoiceID': 'mozilla-%s' % data['uuid'],
'receiverList.receiver(0).primary': 'TRUE',
'receiverList.receiver(0).paymentType': 'DIGITALGOODS',
'trackingId': data['uuid'],
'ipnNotificationUrl': absolutify(reverse('amo.paypal'))}
if data.get('memo'):
paypal_data['memo'] = data['memo']
opener = urllib2.build_opener()
try:
with socket_timeout(10):
feeddata = opener.open(request, urlencode(paypal_data)).read()
except Exception, error:
paypal_log.error('HTTP Error: %s' % error)
raise
response = dict(urlparse.parse_qsl(feeddata))
if 'error(0).errorId' in response:
error = errors.get(response['error(0).errorId'], PaypalError)
paypal_log.error('Paypal Error: %s' % response['error(0).message'])
raise error(response['error(0).message'])
paypal_log.info('Paypal got key: %s' % response['payKey'])
return response['payKey']
def check_paypal_id(name):
"""

Просмотреть файл

@ -1,11 +1,51 @@
from cStringIO import StringIO
from amo.urlresolvers import reverse
from amo.helpers import absolutify
import mock
from nose.tools import eq_
import test_utils
import time
import paypal
good_response = ('responseEnvelope.timestamp='
'2011-01-28T06%3A16%3A33.259-08%3A00&responseEnvelope.ack=Success'
'&responseEnvelope.correlationId=7377e6ae1263c'
'&responseEnvelope.build=1655692'
'&payKey=AP-9GD76073HJ780401K&paymentExecStatus=CREATED')
auth_error = ('error(0).errorId=520003'
'&error(0).message=Authentication+failed.+API+'
'credentials+are+incorrect.')
class TestPayPal(test_utils.TestCase):
def setUp(self):
self.data = {'return_url': absolutify(reverse('home')),
'cancel_url': absolutify(reverse('home')),
'amount': 10,
'email': 'someone@somewhere.com',
'uuid': time.time(),
'ip': '127.0.0.1'}
@mock.patch('urllib2.OpenerDirector.open')
def test_auth_fails(self, opener):
opener.return_value = StringIO(auth_error)
self.assertRaises(paypal.AuthError, paypal.get_paykey, self.data)
@mock.patch('urllib2.OpenerDirector.open')
def test_get_key(self, opener):
opener.return_value = StringIO(good_response)
eq_(paypal.get_paykey(self.data), 'AP-9GD76073HJ780401K')
def _test_no_mock(self):
# Remove _ and run if you'd like to try unmocked.
return paypal.get_paykey(self.data)
@mock.patch('paypal.urllib.urlopen')
def test_check_paypal_id(urlopen_mock):
urlopen_mock.return_value = StringIO('ACK=Success')

Просмотреть файл

@ -151,6 +151,7 @@ class Contribution(caching.base.CachingMixin, models.Model):
nullify_invalid=True, null=True)
comment = models.CharField(max_length=255)
transaction_id = models.CharField(max_length=255, null=True)
paykey = models.CharField(max_length=255, null=True)
post_data = StatsDictField(null=True)
objects = models.Manager()

Просмотреть файл

@ -160,16 +160,40 @@ var contributions = {
counter.text(limit - Math.min(txt.length, limit));
}).keyup().end()
.find('#contrib-too-much').hide().end()
.find('#contrib-too-little').hide().end()
.find('#contrib-not-entered').hide().end()
.find('form').submit(function() {
var contrib_type = $(this).find('input:checked').val();
if (contrib_type == 'onetime' || contrib_type == 'monthly') {
if (contrib_type == 'onetime') {
var amt = $(this).find('input[name="'+contrib_type+'-amount"]').val();
$(this).find('.error').hide();
if (isNaN(parseFloat(amt))) {
$(this).find('#contrib-not-entered').show();
return false;
}
if (amt > contrib_limit) {
$(this).find('#contrib-too-much').show();
return false;
}
if (parseFloat(amt) >= 0.01) {
$(this).find('#contrib-too-little').show();
return false;
}
}
return true;
var $self = $(this);
$.ajax({type: 'GET',
url: $(this).attr('action') + '?result_type=json',
data: $(this).serialize(),
success: function(json) {
if (json.paykey) {
dgFlow.startFlow(json.url);
$self.find('span.cancel a').click()
} else {
$self.find('#paypal-error').show();
}
}
});
return false;
});
// enable overlay; make sure we have the jqm package available.
@ -183,15 +207,17 @@ var contributions = {
// avoid bleeding-through form elements
if ($.browser.opera) this.inputs = $(':input:visible').css('visibility', 'hidden');
// clean up, then show box
hash.w.find('.error').hide()
hash.w
.find('input:text').val('').end()
.find('textarea').val('').keyup().end()
.find('input:radio:first').attr('checked', 'checked').end()
.fadeIn();
},
onHide: function(hash) {
if ($.browser.opera) this.inputs.css('visibility', 'visible');
hash.w.find('#contrib-too-much').hide();
hash.w.find('.error').hide();
hash.w.fadeOut();
hash.o.remove();
},
@ -199,7 +225,12 @@ var contributions = {
toTop: true
})
.jqmAddClose(cb.find('.cancel a'));
if (window.location.hash === '#contribute-confirm') {
$('#contribute-button').click();
}
}
}
/* TODO(jbalogh): save from amo2009. */

657
media/js/paypal.js Normal file
Просмотреть файл

@ -0,0 +1,657 @@
/**
* Integration JavaScript for PayPal's inline checkout
*
* @author jeharrell
*/
if (typeof PAYPAL == 'undefined' || !PAYPAL) {
var PAYPAL = {};
}
PAYPAL.apps = PAYPAL.apps || {};
(function () {
var defaultConfig = {
// DOM element which triggers the flow
trigger: null,
// Experience for the flow; set to 'mini' to force a popup flow
expType: null,
// Merchant can control the NameOnButton feature by setting boolean values
sole: 'true',
// To set stage environment
stage: null
};
/**
* Creates an instance of the in-context UI for the Digital Goods flow
*
* @param {Object} userConfig Overrides to the default configuration
*/
PAYPAL.apps.DGFlow = function (userConfig) {
var that = this;
// storage object for UI elements
that.UI = {};
// setup
that._init(userConfig);
return {
/**
* Public method to add a trigger outside of the constructor
*
* @param {HTMLElement|String} el The element to set the click event on
*/
setTrigger: function (el) {
that._setTrigger(el);
},
/**
* Public method to start the flow without a triggering element, e.g. in a Flash movie
*
* @param {String} url The URL which starts the flow
*/
startFlow: function (url) {
var win = that._render();
if (win.location) {
win.location = url;
} else {
win.src = url;
}
},
/**
* Public method to close the flow's lightbox
*/
closeFlow: function () {
that._destroy();
},
/**
* Public method to determine if the flow is active
*/
isOpen: function () {
return that.isOpen;
}
};
};
PAYPAL.apps.DGFlow.prototype = {
/**
* Name of the iframe that's created
*/
name: 'PPDGFrame',
/**
* Boolean; true if the flow is active
*/
isOpen: false,
/**
* Boolean; true if NameOnButton feature is active
*/
NoB: true,
/**
* Initial setup
*
* @param {Object} userConfig Overrides to the default configuration: see defaultConfig.
*/
_init: function (userConfig) {
if (userConfig) {
for (var key in defaultConfig) {
if (typeof userConfig[key] !== 'undefined') {
this[key] = userConfig[key];
} else {
this[key] = defaultConfig[key];
}
}
}
this.stage = (this.stage == null) ? "www.paypal.com" : "www."+this.stage+".paypal.com:8443";
if (this.trigger) {
this._setTrigger(this.trigger);
}
this._addCSS();
// NoB is started
if(this.NoB == true && this.sole == 'true'){
var url = "https://"+ this.stage + "/webapps/checkout/nameOnButton.gif";
this._getImage(url, this._addImage);
}
},
/**
* Renders and displays the UI
*
* @return {HTMLElement} The window the flow will appear in
*/
_render: function () {
var ua = navigator.userAgent,
win;
// mobile exerience
if (ua.match(/iPhone|iPod|Android|Blackberry.*WebKit/i)) {
win = window.open('', this.name);
return win;
// popup experience
} else if (this.expType == 'mini') {
var width = 400,
height = 550,
left,
top;
if (window.outerWidth) {
left = Math.round((window.outerWidth - width) / 2) + window.screenX;
top = Math.round((window.outerHeight - height) / 2) + window.screenY;
} else if (window.screen.width) {
left = Math.round((window.screen.width - width) / 2);
top = Math.round((window.screen.height - height) / 2);
}
win = window.open('', this.name, 'top=' + top + ', left=' + left + ', width=' + width + ', height=' + height + ', location=0, status=0, toolbar=0, menubar=0, resizable=0');
return win;
// default experience
} else {
this._buildDOM();
this._createMask();
this._centerLightbox();
this._bindEvents();
this.isOpen = true;
return this.UI.iframe;
}
},
/**
* Embeds the CSS for the UI into the document head
*/
_addCSS: function () {
var css = '',
styleEl = document.createElement('style');
// write the styles into the page
css += '#' + this.name + ' { position:absolute; top:0; left:0; }';
css += '#' + this.name + ' .panel { z-index:9999; position:relative; }';
css += '#' + this.name + ' .panel iframe { width:385px; height:550px; border:0; }';
css += '#' + this.name + ' .mask { z-index:9998; position:absolute; top:0; left:0; background-color:#000; opacity:0.2; filter:alpha(opacity=20); }';
css += '.nameOnButton { display: inline-block; text-align: center; }';
css += '.nameOnButton img { border:none; }';
styleEl.type = 'text/css';
if (styleEl.styleSheet) {
styleEl.styleSheet.cssText = css;
} else {
styleEl.appendChild(document.createTextNode(css));
}
document.getElementsByTagName('head')[0].appendChild(styleEl);
},
/**
* Creates the DOM nodes and adds them to the document body
*/
_buildDOM: function () {
this.UI.wrapper = document.createElement('div');
this.UI.wrapper.id = this.name;
this.UI.panel = document.createElement('div');
this.UI.panel.className = 'panel';
// workaround: IE6 + 7 won't let you name an iframe after you create it
try {
this.UI.iframe = document.createElement('<iframe name="' + this.name + '">');
} catch (e) {
this.UI.iframe = document.createElement('iframe');
this.UI.iframe.name = this.name;
}
this.UI.iframe.frameBorder = '0';
this.UI.iframe.border = '0';
this.UI.iframe.scrolling = 'no';
this.UI.iframe.allowTransparency = 'true';
this.UI.mask = document.createElement('div');
this.UI.mask.className = 'mask';
this.UI.panel.appendChild(this.UI.iframe);
this.UI.wrapper.appendChild(this.UI.mask);
this.UI.wrapper.appendChild(this.UI.panel);
document.body.appendChild(this.UI.wrapper);
},
/**
* Creates the mask
*/
_createMask: function (e) {
var windowWidth, windowHeight, scrollWidth, scrollHeight, width, height;
// get the scroll dimensions
if (window.innerHeight && window.scrollMaxY) {
scrollWidth = window.innerWidth + window.scrollMaxX;
scrollHeight = window.innerHeight + window.scrollMaxY;
} else if (document.body.scrollHeight > document.body.offsetHeight) {
scrollWidth = document.body.scrollWidth;
scrollHeight = document.body.scrollHeight;
} else {
scrollWidth = document.body.offsetWidth;
scrollHeight = document.body.offsetHeight;
}
// get the window dimensions
// non-IE browsers
if (window.innerHeight) {
windowWidth = window.innerWidth;
windowHeight = window.innerHeight;
// IE 6+ in standards mode
} else if (document.documentElement && document.documentElement.clientHeight) {
windowWidth = document.documentElement.clientWidth;
windowHeight = document.documentElement.clientHeight;
// other IEs
} else if (document.body) {
windowWidth = document.body.clientWidth;
windowHeight = document.body.clientHeight;
}
// take the larger of each
width = (windowWidth > scrollWidth) ? windowWidth : scrollWidth;
height = (windowHeight > scrollHeight) ? windowHeight : scrollHeight;
this.UI.mask.style.width = width + 'px';
this.UI.mask.style.height = height + 'px';
},
/**
* Centers the lightbox in the middle of the window
*/
_centerLightbox: function (e) {
var width, height, scrollY;
// non-IE browsers
if (window.innerWidth) {
width = window.innerWidth;
height = window.innerHeight;
scrollY = window.pageYOffset;
// IE 6+ in standards mode
} else if (document.documentElement && (document.documentElement.clientWidth || document.documentElement.clientHeight)) {
width = document.documentElement.clientWidth;
height = document.documentElement.clientHeight;
scrollY = document.documentElement.scrollTop;
// other browsers
} else if (document.body && (document.body.clientWidth || document.body.clientHeight)) {
width = document.body.clientWidth;
height = document.body.clientHeight;
scrollY = document.body.scrollTop;
}
this.UI.panel.style.left = Math.round((width - this.UI.iframe.offsetWidth) / 2) + 'px';
var panelTop = Math.round((height - this.UI.iframe.offsetHeight) / 2) + scrollY;
if(panelTop < 5){
panelTop = 10;
}
this.UI.panel.style.top = panelTop + 'px';
},
/**
* Sets up the events for an instance
*/
_bindEvents: function () {
addEvent(window, 'resize', this._createMask, this);
addEvent(window, 'resize', this._centerLightbox, this);
addEvent(window, 'unload', this._destroy, this);
},
/**
* Adds a click event to an element which initiates the flow
*
* @param {HTMLElement[]|String[]} el The element to attach the click event to
* @return {Boolean} True if the trigger is active and false if it failed
*/
_setTrigger: function(el) {
// process an array if passed
if (el.constructor.toString().indexOf('Array') > -1) {
for (var i = 0; i < el.length; i++) {
this._setTrigger(el[i]);
}
// otherwise process a single element
} else {
el = (typeof el == 'string') ? document.getElementById(el) : el;
// forms
if (el && el.form) {
el.form.target = this.name;
// links
} else if (el && el.tagName.toLowerCase() == 'a') {
el.target = this.name;
}
addEvent(el, 'click', this._triggerClickEvent, this);
}
},
/**
* To load the NameOnButton image
*
* @param {Element} el The trigger is passed
*/
_getImage: function(url, callback){
// Can be used for addEvent case
if(typeof this.callback != 'undefined'){
url = this.url;
callback = this.callback;
}
var self = this;
var imgElement = new Image();
imgElement.src = "";
if(imgElement.readyState){
imgElement.onreadystatechange= function(){
if (imgElement.readyState == 'complete' || imgElement.readyState == 'loaded'){
callback(imgElement, self);
}
};
}
else{
imgElement.onload= function(){
callback(imgElement, self);
};
}
imgElement.src = url;
},
/**
* Place NameOnButton image in on top of the Checkout button
*
* @param {Image} img NameOnButton image is passed
* @param {Object} obj Contains config parameters and functions
* @param {Element} el The trigger element
*/
_addImage: function(img, obj){
if(checkEmptyImage(img)){
var url = "https://"+ obj.stage + "/webapps/checkout/clearNob.gif";
var wrapperObj = {};
wrapperObj.callback = obj._removeImage;
wrapperObj.url = url;
wrapperObj.outer = obj;
var el = obj.trigger;
if(el.constructor.toString().indexOf('Array') > -1){
for (var i = 0; i < el.length; i++) {
var tempImg = img.cloneNode(true);
obj._placeImage(el[i], tempImg, wrapperObj);
}
}
else{
obj._placeImage(el, img, wrapperObj);
}
}
},
/**
* Place NameOnButton image in on top of the Checkout button
*
* @param {Element} el The trigger element
* @param {Image} img NameOnButton image is passed
* @param {Object} obj Contains config parameters and logout link
*/
_placeImage: function(el, img, obj){
el = (typeof el == 'string') ? document.getElementById(el) : el;
var root = getParent(el);
var spanElement = document.createElement("span");
spanElement.className = "nameOnButton";
var lineBreak = document.createElement("br");
var link = document.createElement("a");
link.href = "javascript:";
link.appendChild(img);
root.insertBefore(spanElement,el);
spanElement.appendChild(el);
spanElement.insertBefore(link,el);
spanElement.insertBefore(lineBreak,el);
obj.span = spanElement;
obj.link = link;
obj.lbreak = lineBreak;
addEvent(link, 'click', obj.outer._getImage, obj);
},
/**
* Place NameOnButton image in on top of the Checkout button
*
* @param {Image} img NameOnButton image is passed
* @param {Object} obj Contains config parameters and logout link
* @param {Element} el The trigger element
*/
_removeImage: function(img, obj){
if(!checkEmptyImage(img)){
var el = obj.outer.trigger;
if(el.constructor.toString().indexOf('Array') > -1){
obj.outer._removeMultiImages(obj.outer.trigger);
}
else{
spanElement = obj.span;
link = obj.link;
lineBreak = obj.lbreak;
spanElement.removeChild(link);
spanElement.removeChild(lineBreak);
}
}
},
/**
* Place NameOnButton image in on top of the Checkout button
*
* @param {Image} img NameOnButton image is passed
*/
_removeMultiImages: function(obj){
for (var i = 0; i < obj.length; i++) {
obj[i] = (typeof obj[i] == 'string') ? document.getElementById(obj[i]) : obj[i];
rootNode = getParent(obj[i]);
if(rootNode.className == 'nameOnButton'){
lineBreak = getPreviousSibling(obj[i]);
linkNode = getPreviousSibling(lineBreak);
rootNode.removeChild(linkNode);
rootNode.removeChild(lineBreak);
}
}
},
/**
* Custom event which fires on click of the trigger element(s)
*
* @param {Event} e The event object
*/
_triggerClickEvent: function (e) {
this._render();
},
/**
* Custom event which does some cleanup: all UI DOM nodes, custom events,
* and intervals are removed from the current page
*
* @param {Event} e The event object
*/
_destroy: function (e) {
if (this.isOpen && this.UI.wrapper.parentNode) {
this.UI.wrapper.parentNode.removeChild(this.UI.wrapper);
}
if (this.interval) {
clearInterval(this.interval);
}
removeEvent(window, 'resize', this._createMask);
removeEvent(window, 'resize', this._centerLightbox);
removeEvent(window, 'unload', this._destroy);
removeEvent(window, 'message', this._windowMessageEvent);
this.isOpen = false;
}
};
/* Helper Methods */
/**
* Storage object for all events; used to obtain exact signature when
* removing events
*/
var eventCache = [];
/**
* Normalized method of adding an event to an element
*
* @param {HTMLElement} obj The object to attach the event to
* @param {String} type The type of event minus the "on"
* @param {Function} fn The callback function to add
* @param {Object} scope A custom scope to use in the callback (optional)
*/
function addEvent(obj, type, fn, scope) {
scope = scope || obj;
var wrappedFn;
if (obj.addEventListener) {
wrappedFn = function (e) { fn.call(scope, e); };
obj.addEventListener(type, wrappedFn, false);
} else if (obj.attachEvent) {
wrappedFn = function () {
var e = window.event;
e.target = e.target || e.srcElement;
e.preventDefault = function () {
window.event.returnValue = false;
};
fn.call(scope, e);
};
obj.attachEvent('on' + type, wrappedFn);
}
eventCache.push([obj, type, fn, wrappedFn]);
}
/**
* Normalized method of removing an event from an element
*
* @param {HTMLElement} obj The object to attach the event to
* @param {String} type The type of event minus the "on"
* @param {Function} fn The callback function to remove
*/
function removeEvent(obj, type, fn) {
var wrappedFn, item, len, i;
for (i = 0; i < eventCache.length; i++) {
item = eventCache[i];
if (item[0] == obj && item[1] == type && item[2] == fn) {
wrappedFn = item[3];
if (wrappedFn) {
if (obj.removeEventListener) {
obj.removeEventListener(type, wrappedFn, false);
} else if (obj.detachEvent) {
obj.detachEvent('on' + type, wrappedFn);
}
}
}
}
}
/**
* Normalized method of getting the corresponding parent element for an element
*
* @param {HTMLElement} obj The object to get the corresponding parent element
*/
function getParent(el) {
do {
el = el.parentNode;
} while (el && el.nodeType != 1);
return el;
}
/**
* Normalized method of getting the corresponding previous sibling element for an element
*
* @param {HTMLElement} obj The object to get the corresponding previous sibling element
*/
function getPreviousSibling(el) {
do {
el = el.previousSibling;
} while (el && el.nodeType != 1);
return el;
}
/**
* Normalized method of checking empty image
*
* @param {HTMLElement} obj The object should be an image object
*/
function checkEmptyImage(img){
return (img.width > 1 || img.height > 1);
}
}());

Просмотреть файл

@ -2,4 +2,22 @@ $(document).ready(function() {
$("#contribute-why").popup("#contribute-more-info", {
pointTo: "#contribute-more-info"
});
$('div.contribute a.suggested-amount').bind('click', function(event) {
$.ajax({type: 'GET',
url: $(this).attr('href') + '?result_type=json',
success: function(json) {
dgFlow.startFlow(json.url);
}
});
return false;
});
dgFlow = new PAYPAL.apps.DGFlow();
});
top_dgFlow = top.dgFlow || (top.opener && top.opener.top.dgFlow);
if (top_dgFlow != null) {
top_dgFlow.closeFlow();
if (top != null) {
top.close();
}
}

Просмотреть файл

@ -0,0 +1 @@
ALTER TABLE stats_contributions ADD COLUMN paykey varchar(255);

Просмотреть файл

@ -426,6 +426,9 @@ MINIFY_BUNDLES = {
# Hover delay for global header
'js/global/menu.js',
# Paypal
'js/paypal.js'
),
'zamboni/discovery-pane': (
'js/zamboni/jquery-1.4.2.min.js',
@ -573,11 +576,15 @@ CAKE_SESSION_TIMEOUT = 8640
# PayPal Settings
PAYPAL_API_URL = 'https://api-3t.paypal.com/nvp'
PAYPAL_API_VERSION = '50'
PAYPAL_APP_ID = ''
PAYPAL_BN = ''
PAYPAL_CGI_URL = 'https://www.paypal.com/cgi-bin/webscr'
PAYPAL_PAY_URL = 'https://paypal.com/adaptivepayments/pay'
PAYPAL_FLOW_URL = 'https://paypal.com/webapps/adaptivepayment/flow/pay'
PAYPAL_USER = ''
PAYPAL_PASSWORD = ''
PAYPAL_SIGNATURE = ''
PAYPAL_USER = ''
PAYPAL_EMAIL = ''
# Paypal is an awful place that doesn't understand locales. Instead they have
# country codes. This maps our locales to their codes.
@ -691,7 +698,10 @@ CSP_OBJECT_SRC = ("'none'",)
CSP_MEDIA_SRC = ("'none'",)
CSP_FRAME_SRC = ("'none'",)
CSP_FONT_SRC = ("'self'", "fonts.mozilla.com", "www.mozilla.com", )
CSP_FRAME_ANCESTORS = ("'none'",) # We also send x-frame-options:DENY
# self is needed for paypal which sends x-frame-options:allow when needed.
# x-frame-options:DENY is sent the rest of the time.
CSP_FRAME_ANCESTORS = ("'self'",)
# Should robots.txt deny everything or disallow a calculated list of URLs we
# don't want to be crawled? Default is false, disallow everything.
@ -768,3 +778,5 @@ MOBILE_COOKIE = 'mamo'
# If the users's Firefox has a version number greater than this we consider it
# a beta.
MIN_BETA_VERSION = '3.7'
DEFAULT_SUGGESTED_CONTRIBUTION = 5