move to new payment flow (bug 627077)
This commit is contained in:
Родитель
5bf877b415
Коммит
f8f7eac524
|
@ -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. */
|
||||
|
|
|
@ -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);
|
16
settings.py
16
settings.py
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче