add pre-auth payment to Marketplace (bug 738772)

This commit is contained in:
Chris Van 2012-03-29 22:06:31 -07:00
Родитель f38a1aaec8
Коммит cf738eda0f
12 изменённых файлов: 318 добавлений и 97 удалений

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

@ -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):