add pre-auth payment to Marketplace (bug 738772)
This commit is contained in:
Родитель
f38a1aaec8
Коммит
cf738eda0f
|
@ -27,8 +27,7 @@ from amo.pyquery_wrapper import PyQuery as pq
|
|||
from amo.urlresolvers import reverse
|
||||
from bandwagon.models import Collection, CollectionWatcher
|
||||
from devhub.models import ActivityLog
|
||||
from market.models import PreApprovalUser, Price
|
||||
import paypal
|
||||
from market.models import Price
|
||||
from reviews.models import Review
|
||||
from users.models import BlacklistedPassword, UserProfile, UserNotification
|
||||
import users.notifications as email
|
||||
|
@ -1252,69 +1251,3 @@ class TestReportAbuse(amo.tests.TestCase):
|
|||
|
||||
r = self.client.get(self.full_page)
|
||||
eq_(pq(r.content)('.notification-box h2').length, 1)
|
||||
|
||||
|
||||
class TestPreapproval(amo.tests.TestCase):
|
||||
fixtures = ['base/users.json']
|
||||
|
||||
def setUp(self):
|
||||
waffle.models.Flag.objects.create(name='allow-pre-auth',
|
||||
everyone=True)
|
||||
self.user = UserProfile.objects.get(pk=999)
|
||||
assert self.client.login(username=self.user.email,
|
||||
password='password')
|
||||
|
||||
def get_url(self, status=None):
|
||||
if status:
|
||||
return reverse('users.payments', args=[status])
|
||||
return reverse('users.payments')
|
||||
|
||||
def test_preapproval_denied(self):
|
||||
self.client.logout()
|
||||
eq_(self.client.get(self.get_url()).status_code, 302)
|
||||
|
||||
def test_preapproval_allowed(self):
|
||||
eq_(self.client.get(self.get_url()).status_code, 200)
|
||||
|
||||
def test_preapproval_setup(self):
|
||||
doc = pq(self.client.get(self.get_url()).content)
|
||||
eq_(doc('#preapproval').attr('action'),
|
||||
reverse('users.payments.preapproval'))
|
||||
|
||||
@patch('paypal.get_preapproval_key')
|
||||
def test_fake_preapproval(self, get_preapproval_key):
|
||||
get_preapproval_key.return_value = {'preapprovalKey': 'xyz'}
|
||||
res = self.client.post(reverse('users.payments.preapproval'))
|
||||
ssn = self.client.session['setup-preapproval']
|
||||
eq_(ssn['key'], 'xyz')
|
||||
# Checking it's in the future at least 353 just so this test will work
|
||||
# on leap years at 11:59pm.
|
||||
assert (ssn['expiry'] - datetime.today()).days > 353
|
||||
eq_(res['Location'], paypal.get_preapproval_url('xyz'))
|
||||
|
||||
def test_preapproval_complete(self):
|
||||
ssn = self.client.session
|
||||
ssn['setup-preapproval'] = {'key': 'xyz'}
|
||||
ssn.save()
|
||||
res = self.client.post(self.get_url('complete'))
|
||||
eq_(res.status_code, 200)
|
||||
eq_(self.user.preapprovaluser.paypal_key, 'xyz')
|
||||
# Check that re-loading doesn't error.
|
||||
res = self.client.post(self.get_url('complete'))
|
||||
eq_(res.status_code, 200)
|
||||
|
||||
def test_preapproval_cancel(self):
|
||||
PreApprovalUser.objects.create(user=self.user, paypal_key='xyz')
|
||||
res = self.client.post(self.get_url('cancel'))
|
||||
eq_(res.status_code, 200)
|
||||
eq_(self.user.preapprovaluser.paypal_key, 'xyz')
|
||||
eq_(pq(res.content)('#preapproval').attr('action'),
|
||||
self.get_url('remove'))
|
||||
|
||||
def test_preapproval_remove(self):
|
||||
PreApprovalUser.objects.create(user=self.user, paypal_key='xyz')
|
||||
res = self.client.post(self.get_url('remove'))
|
||||
eq_(res.status_code, 200)
|
||||
eq_(self.user.preapprovaluser.paypal_key, '')
|
||||
eq_(pq(res.content)('#preapproval').attr('action'),
|
||||
reverse('users.payments.preapproval'))
|
||||
|
|
|
@ -78,3 +78,19 @@
|
|||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#preapproval {
|
||||
p {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
}
|
||||
div + p {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.enabled b {
|
||||
color: @green;
|
||||
}
|
||||
footer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,11 +128,6 @@ p.req {
|
|||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
/* CSRF token */
|
||||
form div[style]:first-child + p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.optional {
|
||||
color: @note-gray;
|
||||
font-size: 11px;
|
||||
|
@ -199,17 +194,17 @@ form {
|
|||
margin: 5px 0 0;
|
||||
}
|
||||
}
|
||||
p,
|
||||
.form-col p,
|
||||
input + a {
|
||||
color: @medium-gray;
|
||||
}
|
||||
p,
|
||||
.form-col p,
|
||||
input + a,
|
||||
.errorlist {
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
}
|
||||
footer {
|
||||
.form-footer {
|
||||
border-top: 1px solid @light-gray;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
float: left;
|
||||
font: normal 20px @open-stack;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 1px 1px rgba(0,0,0,.8);
|
||||
a {
|
||||
color: @faint-gray;
|
||||
text-decoration: none;
|
||||
|
@ -35,7 +36,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: @3col) {
|
||||
@media (max-width: @4col) {
|
||||
#site-header {
|
||||
h1, form {
|
||||
float: none;
|
||||
|
@ -43,5 +44,78 @@
|
|||
h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Content header (breadcrumbs, heading, subnav) - used on Account Settings, etc. */
|
||||
|
||||
#page header {
|
||||
margin-bottom: 10px;
|
||||
h1 {
|
||||
float: left;
|
||||
}
|
||||
.sub-nav {
|
||||
float: right;
|
||||
}
|
||||
.sub-nav {
|
||||
.border-radius(15px);
|
||||
.box-shadow(0 1px 0 @barely-gray);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li {
|
||||
background-clip: padding-box;
|
||||
float: left;
|
||||
a {
|
||||
.gradient-two-color(@white, rgba(238,238,238,.8));
|
||||
border: 1px solid @note-gray;
|
||||
color: @medium-gray;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
padding: 5px 15px;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,.8);
|
||||
&:hover {
|
||||
.gradient-two-color(@barely-gray, darken(@barely-gray, 15%));
|
||||
color: @dark-gray;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,.8);
|
||||
}
|
||||
&:active {
|
||||
.box-shadow(0 1px 2px rgba(0,0,0,.1) inset);
|
||||
background: @faint-gray;
|
||||
border-color: @note-gray;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
a, a:hover {
|
||||
.box-shadow(0 1px 0 rgba(0,0,0,.2) inset);
|
||||
.gradient-two-color(@medium-gray, fadeOut(@medium-gray, 75%));
|
||||
border-color: @medium-gray;
|
||||
color: @white;
|
||||
text-shadow: 0 1px 0 rgba(0,0,0,.2);
|
||||
}
|
||||
}
|
||||
&:first-child a {
|
||||
.border-radius(15px 0 0 15px);
|
||||
border-right-width: 0;
|
||||
}
|
||||
&:last-child a {
|
||||
.border-radius(0 15px 15px 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.html-rtl #page header {
|
||||
h1 {
|
||||
float: right;
|
||||
}
|
||||
.sub-nav {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -187,19 +187,19 @@ body {
|
|||
}
|
||||
// Account for the wrapped stuff in the header.
|
||||
#page {
|
||||
margin-top: 50px;
|
||||
margin-top: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @4col) {
|
||||
#page {
|
||||
margin-top: 80px;
|
||||
margin-top: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @3col) {
|
||||
#page {
|
||||
margin-top: 85px;
|
||||
margin-top: 95px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
h1 + ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% set urls = [
|
||||
(url('account.settings'), _('Basic Info'),
|
||||
request.path == url('account.settings')),
|
||||
(url('account.payment'), _('Payment'),
|
||||
request.path.startswith(url('account.payment'))),
|
||||
] %}
|
||||
<ul class="sub-nav">
|
||||
{% for link, title, selected in urls %}
|
||||
<li{% if selected %} class="selected"{% endif %}>
|
||||
<a href="{{ link }}">{{ title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -0,0 +1,58 @@
|
|||
{% extends 'mkt/base.html' %}
|
||||
|
||||
{% set title = _('Payment Settings') %}
|
||||
{% block title %}{{ mkt_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="purchases" class="account form-grid">
|
||||
{{ mkt_breadcrumbs(product, [(url('account.settings'), _('Account Settings')),
|
||||
(None, _('Payment'))]) }}
|
||||
<header class="c">
|
||||
<h1>{{ title }}</h1>
|
||||
{% include 'account/includes/nav.html' %}
|
||||
</header>
|
||||
{% if preapproval.paypal_key %}
|
||||
<form id="preapproval" method="post" class="simple-field"
|
||||
action="{{ url('account.payment', 'remove') }}">
|
||||
{{ csrf() }}
|
||||
<p class="enabled">
|
||||
{% trans %}
|
||||
Your payment pre-approval is <b>enabled</b>.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans %}
|
||||
All future app purchases will be automatically charged to your
|
||||
PayPal account. To cancel this agreement at any time return to
|
||||
this page.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<footer>
|
||||
<button class="delete" type="submit">
|
||||
{{ _('Remove Pre-approval') }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
{% else %}
|
||||
<form id="preapproval" method="post" class="simple-field"
|
||||
action="{{ url('account.payment.preapproval') }}">
|
||||
{{ csrf() }}
|
||||
<p>
|
||||
{% trans %}
|
||||
Setting up PayPal pre-approval allows you to buy apps with one
|
||||
click from the Marketplace. They also allow you to use in-app
|
||||
purchases that go through this site.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans %}
|
||||
You can cancel pre-approval at any time by returning to this page.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<footer>
|
||||
<button type="submit">{{ _('Set Up Pre-approval') }}</button>
|
||||
</footer>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -5,9 +5,12 @@
|
|||
{% block title %}{{ mkt_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="account-settings">
|
||||
<section id="account-settings" class="account">
|
||||
{{ mkt_breadcrumbs(product, [(None, title)]) }}
|
||||
<h1>{{ title }}</h1>
|
||||
<header class="c">
|
||||
<h1>{{ _('Basic Info') }}</h1>
|
||||
{% include 'account/includes/nav.html' %}
|
||||
</header>
|
||||
<form class="form-grid" enctype="multipart/form-data" method="post">
|
||||
{{ csrf() }}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
from django.core.cache import cache
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
import mock
|
||||
from jingo.helpers import datetime as datetime_filter
|
||||
from nose.tools import eq_
|
||||
from pyquery import PyQuery as pq
|
||||
|
@ -13,9 +13,10 @@ import waffle
|
|||
import amo
|
||||
import amo.tests
|
||||
from amo.urlresolvers import reverse
|
||||
from addons.models import AddonPremium, AddonUser
|
||||
from market.models import Price
|
||||
from addons.models import AddonPremium
|
||||
from market.models import PreApprovalUser, Price
|
||||
from mkt.developers.models import ActivityLog
|
||||
import paypal
|
||||
from stats.models import Contribution
|
||||
from users.models import UserNotification, UserProfile
|
||||
import users.notifications as email
|
||||
|
@ -52,7 +53,7 @@ class TestAccountSettings(amo.tests.TestCase):
|
|||
eq_(doc('#id_' + field).val(), expected)
|
||||
|
||||
def test_no_password_changes(self):
|
||||
r = self.client.post(self.url, self.data)
|
||||
self.client.post(self.url, self.data)
|
||||
eq_(self.user.userlog_set
|
||||
.filter(activity_log__action=amo.LOG.CHANGE_PASSWORD.id)
|
||||
.count(), 0)
|
||||
|
@ -212,6 +213,67 @@ class TestAdminAccountSettings(amo.tests.TestCase):
|
|||
eq_(r[0].details['password'][0], u'****')
|
||||
|
||||
|
||||
class TestPreapproval(amo.tests.TestCase):
|
||||
fixtures = ['base/users']
|
||||
|
||||
def setUp(self):
|
||||
self.user = UserProfile.objects.get(pk=999)
|
||||
assert self.client.login(username=self.user.email, password='password')
|
||||
|
||||
def get_url(self, status=None):
|
||||
return reverse('account.payment', args=[status] if status else [])
|
||||
|
||||
def test_preapproval_denied(self):
|
||||
self.client.logout()
|
||||
eq_(self.client.get(self.get_url()).status_code, 302)
|
||||
|
||||
def test_preapproval_allowed(self):
|
||||
eq_(self.client.get(self.get_url()).status_code, 200)
|
||||
|
||||
def test_preapproval_setup(self):
|
||||
doc = pq(self.client.get(self.get_url()).content)
|
||||
eq_(doc('#preapproval').attr('action'),
|
||||
reverse('account.payment.preapproval'))
|
||||
|
||||
@mock.patch('paypal.get_preapproval_key')
|
||||
def test_fake_preapproval(self, get_preapproval_key):
|
||||
get_preapproval_key.return_value = {'preapprovalKey': 'xyz'}
|
||||
res = self.client.post(reverse('account.payment.preapproval'))
|
||||
ssn = self.client.session['setup-preapproval']
|
||||
eq_(ssn['key'], 'xyz')
|
||||
# Checking it's in the future at least 353 just so this test will work
|
||||
# on leap years at 11:59pm.
|
||||
assert (ssn['expiry'] - datetime.today()).days > 353
|
||||
eq_(res['Location'], paypal.get_preapproval_url('xyz'))
|
||||
|
||||
def test_preapproval_complete(self):
|
||||
ssn = self.client.session
|
||||
ssn['setup-preapproval'] = {'key': 'xyz'}
|
||||
ssn.save()
|
||||
res = self.client.post(self.get_url('complete'))
|
||||
eq_(res.status_code, 200)
|
||||
eq_(self.user.preapprovaluser.paypal_key, 'xyz')
|
||||
# Check that re-loading doesn't error.
|
||||
res = self.client.post(self.get_url('complete'))
|
||||
eq_(res.status_code, 200)
|
||||
|
||||
def test_preapproval_cancel(self):
|
||||
PreApprovalUser.objects.create(user=self.user, paypal_key='xyz')
|
||||
res = self.client.post(self.get_url('cancel'))
|
||||
eq_(res.status_code, 200)
|
||||
eq_(self.user.preapprovaluser.paypal_key, 'xyz')
|
||||
eq_(pq(res.content)('#preapproval').attr('action'),
|
||||
self.get_url('remove'))
|
||||
|
||||
def test_preapproval_remove(self):
|
||||
PreApprovalUser.objects.create(user=self.user, paypal_key='xyz')
|
||||
res = self.client.post(self.get_url('remove'))
|
||||
eq_(res.status_code, 200)
|
||||
eq_(self.user.preapprovaluser.paypal_key, '')
|
||||
eq_(pq(res.content)('#preapproval').attr('action'),
|
||||
reverse('account.payment.preapproval'))
|
||||
|
||||
|
||||
class PurchaseBase(amo.tests.TestCase):
|
||||
fixtures = ['base/users']
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from django.conf.urls.defaults import include, patterns, url
|
||||
|
||||
from lib.misc.urlconf_decorator import decorate
|
||||
|
||||
|
@ -6,14 +6,23 @@ from amo.decorators import login_required
|
|||
from . import views
|
||||
|
||||
|
||||
settings_patterns = patterns('',
|
||||
url('delete$', views.delete, name='account.delete'),
|
||||
url('delete_photo$', views.delete_photo,
|
||||
name='account.delete_photo'),
|
||||
url('payment(?:/(?P<status>cancel|complete|remove))?$', views.payment,
|
||||
name='account.payment'),
|
||||
url('payment/preapproval$', views.preapproval,
|
||||
name='account.payment.preapproval'),
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = decorate(login_required, patterns('',
|
||||
url('settings$', views.account_settings, name='account.settings'),
|
||||
('^settings/', include(settings_patterns)),
|
||||
url('purchases/$', views.purchases, name='account.purchases'),
|
||||
url(r'purchases/(?P<product_id>\d+)', views.purchases,
|
||||
name='account.purchases.receipt'),
|
||||
url('settings/$', views.account_settings, name='account.settings'),
|
||||
url('settings/delete$', views.delete, name='account.delete'),
|
||||
url('settings/delete_photo$', views.delete_photo,
|
||||
name='account.delete_photo'),
|
||||
|
||||
# Keeping the same URL pattern since admin pages already know about this.
|
||||
url(r'user/(?:/(?P<user_id>\d+)/)?edit$', views.admin_edit,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from django import http
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template import Context, loader
|
||||
|
||||
import commonware.log
|
||||
import jingo
|
||||
|
@ -10,20 +11,79 @@ from tower import ugettext as _, ugettext_lazy as _lazy
|
|||
from addons.views import BaseFilter
|
||||
import amo
|
||||
from amo.decorators import permission_required, post_required, write
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.utils import paginate, send_mail
|
||||
from amo.utils import paginate
|
||||
from market.models import PreApprovalUser
|
||||
import paypal
|
||||
from stats.models import Contribution
|
||||
from translations.query import order_by_translation
|
||||
from users.forms import AdminUserEditForm
|
||||
from users.models import UserProfile
|
||||
from users.tasks import delete_photo as delete_photo_task
|
||||
from users.utils import EmailResetCode
|
||||
from users.views import logout
|
||||
from mkt.site import messages
|
||||
from mkt.webapps.models import Webapp
|
||||
from . import forms
|
||||
|
||||
log = commonware.log.getLogger('mkt.account')
|
||||
paypal_log = commonware.log.getLogger('mkt.paypal')
|
||||
|
||||
|
||||
def payment(request, status=None):
|
||||
# Note this is not post_required because PayPal does not reply with a
|
||||
# POST but a GET; that's a sad face.
|
||||
|
||||
if status:
|
||||
pre, created = (PreApprovalUser.objects
|
||||
.safer_get_or_create(user=request.amo_user))
|
||||
|
||||
if status == 'complete':
|
||||
# The user has completed the setup at PayPal and bounced back.
|
||||
if 'setup-preapproval' in request.session:
|
||||
messages.success(request, _('Pre-approval set up'))
|
||||
paypal_log.info(u'Preapproval key created for user: %s'
|
||||
% request.amo_user)
|
||||
data = request.session.get('setup-preapproval', {})
|
||||
pre.update(paypal_key=data.get('key'),
|
||||
paypal_expiry=data.get('expiry'))
|
||||
del request.session['setup-preapproval']
|
||||
|
||||
elif status == 'cancel':
|
||||
# The user has chosen to cancel out of PayPal. Nothing really
|
||||
# to do here; PayPal just bounces to this page.
|
||||
messages.success(request, _('Pre-approval changes cancelled'))
|
||||
|
||||
elif status == 'remove':
|
||||
# The user has an pre approval key set and chooses to remove it.
|
||||
if pre.paypal_key:
|
||||
pre.update(paypal_key='')
|
||||
messages.success(request, _('Pre-approval removed'))
|
||||
paypal_log.info(u'Preapproval key removed for user: %s'
|
||||
% request.amo_user)
|
||||
|
||||
ctx = {'preapproval': pre}
|
||||
else:
|
||||
ctx = {'preapproval': request.amo_user.get_preapproval()}
|
||||
|
||||
return jingo.render(request, 'account/payment.html', ctx)
|
||||
|
||||
|
||||
@post_required
|
||||
def preapproval(request):
|
||||
today = datetime.today()
|
||||
data = {'startDate': today,
|
||||
'endDate': today + timedelta(days=365 * 2),
|
||||
'pattern': 'account.payment'}
|
||||
try:
|
||||
result = paypal.get_preapproval_key(data)
|
||||
except paypal.PaypalError, e:
|
||||
paypal_log.error(u'Preapproval key: %s' % e, exc_info=True)
|
||||
raise
|
||||
|
||||
paypal_log.info(u'Got preapproval key for user: %s' % request.amo_user)
|
||||
request.session['setup-preapproval'] = {
|
||||
'key': result['preapprovalKey'],
|
||||
'expiry': data['endDate'],
|
||||
}
|
||||
return redirect(paypal.get_preapproval_url(result['preapprovalKey']))
|
||||
|
||||
|
||||
class PurchasesFilter(BaseFilter):
|
||||
|
|
Загрузка…
Ссылка в новой задаче