Skip to content

Coalesce implementations of Stripe payment forms #1761

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 22, 2015
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ whoosh_index
xml_output
public_cnames
public_symlinks
.rope_project/
32 changes: 16 additions & 16 deletions media/css/core.css
Original file line number Diff line number Diff line change
Expand Up @@ -836,27 +836,27 @@ ul.donate-supporters.donate-supporters-large div.supporter-name {
font-size: .9em;
}

div#payment-form div.cc-type {
height: 23px;
margin: 3px 0px 10px;
background: url('/static/donate/img/creditcard.png');
background-repeat: no-repeat;
}
div#payment-form input#cc-number.visa + div.cc-type {
background-position: 0px -23px;
/* Gold */
div.gold-subscription p.subscription-detail,
div.gold-subscription p.subscription-projects {
margin: 0em;
}
div#payment-form input#cc-number.mastercard + div.cc-type {
background-position: 0px -46px;

div.gold-subscription p.subscription-detail label {
display: inline-block;
}
div#payment-form input#cc-number.amex + div.cc-type {
background-position: 0px -69px;

div.gold-subscription p.subscription-detail-card > span {
font-family: monospace;
}
div#payment-form input#cc-number.discover + div.cc-type {
background-position: 0px -92px;

div.gold-subscription > form {
display: inline-block;
}

div#payment-form input#cc-expiry { width: 150px; }
div#payment-form input#cc-cvv { width: 100px; }
div.gold-subscription > form button {
margin: 1em .3em 1.5em 0em;
}

/* Form Wizards */
div.actions.wizard-actions button.action-primary,
Expand Down
23 changes: 9 additions & 14 deletions readthedocs/core/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,9 @@
Common mixin classes for views
"""

from django.conf import settings

from vanilla import ListView


class StripeMixin(object):

"""Adds Stripe publishable key to the context data"""

def get_context_data(self, **kwargs):
context = super(StripeMixin, self).get_context_data(**kwargs)
context.update({
'publishable': settings.STRIPE_PUBLISHABLE,
})
return context
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator


class ListViewWithForm(ListView):
Expand All @@ -27,3 +15,10 @@ def get_context_data(self, **kwargs):
context = super(ListViewWithForm, self).get_context_data(**kwargs)
context['form'] = self.get_form(data=None, files=None)
return context


class LoginRequiredMixin(object):

@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(LoginRequiredMixin, self).dispatch(*args, **kwargs)
22 changes: 22 additions & 0 deletions readthedocs/core/templates/core/ko_form_field.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% if field.is_hidden %}
{{ field }}
{% else %}
<!-- Begin: {{ field.name }} -->
{{ field.errors }}
{% if 'data-bind' in field.field.widget.attrs %}
<ul
class="errorlist"
data-bind="visible: error_{{ field.name }}"
style="display: none;">
<li data-bind="text: error_{{ field.name }}"></li>
</ul>
{% endif %}
<p>
<label for="{{ field.id_for_label }}">{{ field.label }}:</label>
{{ field }}
{% if field.help_text %}
<span class="helptext">{{ field.help_text }}</span>
{% endif %}
</p>
<!-- End: {{ field.name }} -->
{% endif %}
51 changes: 28 additions & 23 deletions readthedocs/donate/forms.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
"""Forms for RTD donations"""

import logging

import stripe
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _

import stripe
from readthedocs.payments.forms import StripeModelForm, StripeResourceMixin

from .models import Supporter

log = logging.getLogger(__name__)


class SupporterForm(forms.ModelForm):
class SupporterForm(StripeResourceMixin, StripeModelForm):

"""Donation support sign up form

This extends the basic payment form, giving fields for credit card number,
expiry, and CVV. The proper Knockout data bindings are established on
:py:cls:`StripeModelForm`
"""

class Meta:
model = Supporter
fields = (
'last_4_digits',
'stripe_id',
'name',
'email',
'dollars',
Expand All @@ -43,38 +52,34 @@ class Meta:
}),
'site_url': forms.TextInput(attrs={
'data-bind': 'value: site_url, enable: urls_enabled'
})
}),
'last_4_digits': forms.TextInput(attrs={
'data-bind': 'valueInit: card_digits, value: card_digits'
}),
}

last_4_digits = forms.CharField(widget=forms.HiddenInput(), required=True)
stripe_id = forms.CharField(widget=forms.HiddenInput(), required=True)
name = forms.CharField(required=True)
email = forms.CharField(required=True)

def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(SupporterForm, self).__init__(*args, **kwargs)

def clean(self):
'''Call stripe for payment (not ideal here) and clean up logo < $200'''
def validate_stripe(self):
"""Call stripe for payment (not ideal here) and clean up logo < $200"""
dollars = self.cleaned_data['dollars']
if dollars < 200:
self.cleaned_data['logo_url'] = None
self.cleaned_data['site_url'] = None
try:
stripe.api_key = settings.STRIPE_SECRET
stripe.Charge.create(
amount=int(self.cleaned_data['dollars']) * 100,
currency='usd',
source=self.cleaned_data['stripe_id'],
description='Read the Docs Sustained Engineering',
receipt_email=self.cleaned_data['email']
)
except stripe.error.CardError, e:
stripe_error = e.json_body['error']
log.error('Credit card error: %s', stripe_error['message'])
raise forms.ValidationError(
_('There was a problem processing your card: %(message)s'),
params=stripe_error)
return self.cleaned_data
stripe.api_key = settings.STRIPE_SECRET
stripe.Charge.create(
amount=int(self.cleaned_data['dollars']) * 100,
currency='usd',
source=self.cleaned_data['stripe_token'],
description='Read the Docs Sustained Engineering',
receipt_email=self.cleaned_data['email']
)

def save(self, commit=True):
supporter = super(SupporterForm, self).save(commit)
Expand Down
11 changes: 5 additions & 6 deletions readthedocs/donate/mixins.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
'''
Mixin classes for donation views
'''
"""Mixin classes for donation views"""

from django.db.models import Avg, Sum

from .models import Supporter


class DonateProgressMixin(object):
'''Add donation progress to context data'''

def get_context_data(self):
context = super(DonateProgressMixin, self).get_context_data()
"""Add donation progress to context data"""

def get_context_data(self, **kwargs):
context = super(DonateProgressMixin, self).get_context_data(**kwargs)
sums = (Supporter.objects
.aggregate(dollars=Sum('dollars')))
avgs = (Supporter.objects
Expand Down
10 changes: 8 additions & 2 deletions readthedocs/donate/static-src/donate/js/donate.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
// Donate payment views

var jquery = require('jquery'),
payment = require('../../../../core/static-src/core/js/payment'),
payment = require('readthedocs/payments/static-src/payments/js/base'),
ko = require('knockout');

function DonateView (config) {
var self = this,
config = config || {};

ko.utils.extend(self, new payment.PaymentView(config));
self.constructor.call(self, config);

self.dollars = ko.observable();
self.logo_url = ko.observable();
self.site_url = ko.observable();
self.error_dollars = ko.observable();
self.error_logo_url = ko.observable();
self.error_site_url = ko.observable();

ko.computed(function () {
var input_logo = $('input#id_logo_url').closest('p'),
input_site = $('input#id_site_url').closest('p');
Expand All @@ -32,6 +36,8 @@ function DonateView (config) {
});
}

DonateView.prototype = new payment.PaymentView();

DonateView.init = function (config, obj) {
var view = new DonateView(config),
obj = obj || $('#donate-payment')[0];
Expand Down
Binary file modified readthedocs/donate/static/donate/img/creditcard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion readthedocs/donate/static/donate/js/donate.js

Large diffs are not rendered by default.

27 changes: 23 additions & 4 deletions readthedocs/donate/templates/donate/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

{% block title %}{% trans "Sustainability" %}{% endblock %}

{% block extra_links %}
<link rel="stylesheet" href="{% static 'payments/css/form.css' %}" />
{% endblock %}

{% block extra_scripts %}
<script src="https://js.stripe.com/v2/" type="text/javascript"></script>
<script type="text/javascript" src="{% static 'vendor/knockout.js' %}"></script>
Expand All @@ -14,7 +18,7 @@
$(document).ready(function () {
var key;
//<![CDATA[
key = '{{ publishable }}';
key = '{{ stripe_publishable }}';
//]]>

var view = donate_views.DonateView.init({
Expand All @@ -36,9 +40,24 @@ <h2>Donate to Read the Docs</h2>
payment is processed directly through <a href="https://stripe.com">Stripe</a>.
</p>

<form accept-charset="UTF-8" action="" method="post" id="donate-payment">
<form action="" method="post" id="donate-payment" class="payment">
{% csrf_token %}
{{ form.as_p }}
{% include "gold/cardform.html" %}

{{ form.non_field_errors }}

{% for field in form.fields_with_cc_group %}
{% if field.is_cc_group %}
<div class="subscription-card">
{% for groupfield in field.fields %}
{% include 'core/ko_form_field.html' with field=groupfield %}
{% endfor %}
</div>
{% else %}
{% include 'core/ko_form_field.html' with field=field %}
{% endif %}
{% endfor %}

{% trans "Donate" as form_submit_text %}
<input type="submit" value="{{ form_submit_text }}" data-bind="click: process_form" />
</form>
{% endblock %}
25 changes: 12 additions & 13 deletions readthedocs/donate/views.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
'''
Donation views
'''
"""Donation views"""

import logging

from django.views.generic import CreateView, ListView, TemplateView
from django.views.generic import TemplateView
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.contrib.messages.views import SuccessMessageMixin
from django.utils.translation import ugettext_lazy as _
from vanilla import CreateView, ListView

from readthedocs.payments.mixins import StripeMixin

from readthedocs.core.mixins import StripeMixin
from .models import Supporter
from .forms import SupporterForm
from .mixins import DonateProgressMixin

log = logging.getLogger(__name__)


class DonateCreateView(SuccessMessageMixin, StripeMixin, CreateView):
'''Create a donation locally and in Stripe'''
class DonateCreateView(StripeMixin, CreateView):

"""Create a donation locally and in Stripe"""

form_class = SupporterForm
success_message = _('Your contribution has been received')
Expand All @@ -31,18 +30,18 @@ def get_success_url(self):
def get_initial(self):
return {'dollars': self.request.GET.get('dollars', 50)}

def get_form_kwargs(self):
kwargs = super(DonateCreateView, self).get_form_kwargs()
def get_form(self, data=None, files=None, **kwargs):
kwargs['user'] = self.request.user
return kwargs
return super(DonateCreateView, self).get_form(data, files, **kwargs)


class DonateSuccessView(TemplateView):
template_name = 'donate/success.html'


class DonateListView(DonateProgressMixin, ListView):
'''Donation list and detail view'''

"""Donation list and detail view"""

template_name = 'donate/list.html'
model = Supporter
Expand Down
1 change: 1 addition & 0 deletions readthedocs/gold/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'readthedocs.gold.apps.GoldAppConfig'
18 changes: 18 additions & 0 deletions readthedocs/gold/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Gold application config for establishing signals"""

import logging

from django.apps import AppConfig

log = logging.getLogger(__name__)


class GoldAppConfig(AppConfig):
name = 'readthedocs.gold'
verbose_name = 'Read the Docs Gold'

def ready(self):
if hasattr(self, 'already_run'):
return
self.already_run = True
import readthedocs.gold.signals # noqa
Loading