From 443619495fe26aa7334755837b090e59a77798f0 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Sun, 4 Oct 2015 18:05:13 -0700 Subject: [PATCH] Rework all payment pieces to unify implementation --- .gitignore | 1 + media/css/core.css | 39 ++-- readthedocs/core/mixins.py | 23 +- .../core/templates/core/ko_form_field.html | 22 ++ readthedocs/donate/forms.py | 41 ++-- readthedocs/donate/mixins.py | 4 +- .../donate/static-src/donate/js/donate.js | 10 +- .../donate/static/donate/img/creditcard.png | Bin 11640 -> 6793 bytes readthedocs/donate/static/donate/js/donate.js | 2 +- .../donate/templates/donate/create.html | 27 ++- readthedocs/donate/views.py | 14 +- readthedocs/gold/__init__.py | 1 + readthedocs/gold/apps.py | 16 ++ readthedocs/gold/forms.py | 59 ++++- readthedocs/gold/signals.py | 13 ++ readthedocs/gold/static-src/gold/js/gold.js | 14 +- readthedocs/gold/static/gold/js/gold.js | 2 +- readthedocs/gold/templates/gold/cancel.html | 27 --- readthedocs/gold/templates/gold/cardform.html | 20 -- readthedocs/gold/templates/gold/edit.html | 41 ---- readthedocs/gold/templates/gold/errors.html | 14 -- readthedocs/gold/templates/gold/field.html | 6 - readthedocs/gold/templates/gold/once.html | 84 ------- readthedocs/gold/templates/gold/register.html | 76 ------ .../gold/subscription_confirm_delete.html | 21 ++ .../templates/gold/subscription_detail.html | 68 ++++++ .../templates/gold/subscription_form.html | 104 +++++++++ readthedocs/gold/templates/gold/thanks.html | 18 -- readthedocs/gold/tests/__init__.py | 0 readthedocs/gold/tests/test_forms.py | 219 ++++++++++++++++++ readthedocs/gold/tests/test_signals.py | 46 ++++ readthedocs/gold/urls.py | 25 +- readthedocs/gold/views.py | 184 +++++---------- readthedocs/payments/__init__.py | 0 readthedocs/payments/forms.py | 190 +++++++++++++++ readthedocs/payments/mixins.py | 23 ++ .../static-src/payments/js/base.js} | 76 ++++-- .../payments/static/payments/css/form.css | 26 +++ readthedocs/payments/utils.py | 30 +++ readthedocs/projects/views/private.py | 10 +- readthedocs/settings/base.py | 1 + .../templates/profiles/base_profile_edit.html | 2 +- 42 files changed, 1069 insertions(+), 530 deletions(-) create mode 100644 readthedocs/core/templates/core/ko_form_field.html create mode 100644 readthedocs/gold/apps.py create mode 100644 readthedocs/gold/signals.py delete mode 100644 readthedocs/gold/templates/gold/cancel.html delete mode 100644 readthedocs/gold/templates/gold/cardform.html delete mode 100644 readthedocs/gold/templates/gold/edit.html delete mode 100644 readthedocs/gold/templates/gold/errors.html delete mode 100644 readthedocs/gold/templates/gold/field.html delete mode 100644 readthedocs/gold/templates/gold/once.html delete mode 100644 readthedocs/gold/templates/gold/register.html create mode 100644 readthedocs/gold/templates/gold/subscription_confirm_delete.html create mode 100644 readthedocs/gold/templates/gold/subscription_detail.html create mode 100644 readthedocs/gold/templates/gold/subscription_form.html delete mode 100644 readthedocs/gold/templates/gold/thanks.html create mode 100644 readthedocs/gold/tests/__init__.py create mode 100644 readthedocs/gold/tests/test_forms.py create mode 100644 readthedocs/gold/tests/test_signals.py create mode 100644 readthedocs/payments/__init__.py create mode 100644 readthedocs/payments/forms.py create mode 100644 readthedocs/payments/mixins.py rename readthedocs/{core/static-src/core/js/payment.js => payments/static-src/payments/js/base.js} (55%) create mode 100644 readthedocs/payments/static/payments/css/form.css create mode 100644 readthedocs/payments/utils.py diff --git a/.gitignore b/.gitignore index bbf9e12ab..63ba96916 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ whoosh_index xml_output public_cnames public_symlinks +.rope_project/ diff --git a/media/css/core.css b/media/css/core.css index 46640c4b7..1eae906f8 100644 --- a/media/css/core.css +++ b/media/css/core.css @@ -836,27 +836,28 @@ 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; -} -div#payment-form input#cc-number.mastercard + div.cc-type { - background-position: 0px -46px; -} -div#payment-form input#cc-number.amex + div.cc-type { - background-position: 0px -69px; -} -div#payment-form input#cc-number.discover + div.cc-type { - background-position: 0px -92px; +/* Gold */ +div.gold-subscription p.subscription-detail, +div.gold-subscription p.subscription-projects { + margin: 0em; } -div#payment-form input#cc-expiry { width: 150px; } -div#payment-form input#cc-cvv { width: 100px; } +div.gold-subscription p.subscription-detail label { + display: inline-block; +} + +div.gold-subscription p.subscription-detail-card > span { + font-family: monospace; +} + +div.gold-subscription > form { + display: inline-block; +} + +div.gold-subscription > form button { + margin: 1em .3em 1.5em 0em; + font-size: 1em; +} /* Form Wizards */ div.actions.wizard-actions button.action-primary, diff --git a/readthedocs/core/mixins.py b/readthedocs/core/mixins.py index 9c803dad7..a6ebdae96 100644 --- a/readthedocs/core/mixins.py +++ b/readthedocs/core/mixins.py @@ -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): @@ -27,3 +15,10 @@ class ListViewWithForm(ListView): 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) diff --git a/readthedocs/core/templates/core/ko_form_field.html b/readthedocs/core/templates/core/ko_form_field.html new file mode 100644 index 000000000..111545dc3 --- /dev/null +++ b/readthedocs/core/templates/core/ko_form_field.html @@ -0,0 +1,22 @@ +{% if field.is_hidden %} + {{ field }} +{% else %} + + {{ field.errors }} + {% if 'data-bind' in field.field.widget.attrs %} + + {% endif %} +

+ + {{ field }} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} +

+ +{% endif %} diff --git a/readthedocs/donate/forms.py b/readthedocs/donate/forms.py index ad93e804a..054b3b1b8 100644 --- a/readthedocs/donate/forms.py +++ b/readthedocs/donate/forms.py @@ -1,23 +1,24 @@ 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 (StripeSubscriptionModelForm, + StripeResourceMixin) from .models import Supporter log = logging.getLogger(__name__) -class SupporterForm(forms.ModelForm): +class SupporterForm(StripeResourceMixin, StripeSubscriptionModelForm): class Meta: model = Supporter fields = ( 'last_4_digits', - 'stripe_id', 'name', 'email', 'dollars', @@ -43,38 +44,34 @@ class SupporterForm(forms.ModelForm): }), '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): + 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 + charge = 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) diff --git a/readthedocs/donate/mixins.py b/readthedocs/donate/mixins.py index fdec627c5..d949a5899 100644 --- a/readthedocs/donate/mixins.py +++ b/readthedocs/donate/mixins.py @@ -10,8 +10,8 @@ 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() + def get_context_data(self, **kwargs): + context = super(DonateProgressMixin, self).get_context_data(**kwargs) sums = (Supporter.objects .aggregate(dollars=Sum('dollars'))) avgs = (Supporter.objects diff --git a/readthedocs/donate/static-src/donate/js/donate.js b/readthedocs/donate/static-src/donate/js/donate.js index cbe6267fb..4e1ba31be 100644 --- a/readthedocs/donate/static-src/donate/js/donate.js +++ b/readthedocs/donate/static-src/donate/js/donate.js @@ -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'); @@ -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]; diff --git a/readthedocs/donate/static/donate/img/creditcard.png b/readthedocs/donate/static/donate/img/creditcard.png index bc56007e8c79125777a3a6965f4c3f7e9606db43..0fe91a8bb178213ca2cf89329afc7bd835651202 100644 GIT binary patch literal 6793 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 k1&%)<|6l)TXmO|XnUjGU| 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 JQ7UB;^gopcH0uBW diff --git a/readthedocs/donate/static/donate/js/donate.js b/readthedocs/donate/static/donate/js/donate.js index bbf90bc53..35d5dd7fd 100644 --- a/readthedocs/donate/static/donate/js/donate.js +++ b/readthedocs/donate/static/donate/js/donate.js @@ -1 +1 @@ -require=function e(t,n,r){function o(i,u){if(!n[i]){if(!t[i]){var l="function"==typeof require&&require;if(!u&&l)return l(i,!0);if(a)return a(i,!0);var c=new Error("Cannot find module '"+i+"'");throw c.code="MODULE_NOT_FOUND",c}var s=n[i]={exports:{}};t[i][0].call(s.exports,function(e){var n=t[i][1][e];return o(n?n:e)},s,s.exports,e,t,n,r)}return n[i].exports}for(var a="function"==typeof require&&require,i=0;it;t++)if(t in this&&this[t]===e)return t;return-1};$.payment={},$.payment.fn={},$.fn.payment=function(){var e,t;return t=arguments[0],e=2<=arguments.length?C.call(arguments,1):[],$.payment.fn[t].apply(this,e)},r=/(\d{1,4})/g,$.payment.cards=n=[{type:"visaelectron",pattern:/^4(026|17500|405|508|844|91[37])/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"maestro",pattern:/^(5(018|0[23]|[68])|6(39|7))/,format:r,length:[12,13,14,15,16,17,18,19],cvcLength:[3],luhn:!0},{type:"forbrugsforeningen",pattern:/^600/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"dankort",pattern:/^5019/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"visa",pattern:/^4/,format:r,length:[13,16],cvcLength:[3],luhn:!0},{type:"mastercard",pattern:/^(5[0-5]|2[2-7])/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"amex",pattern:/^3[47]/,format:/(\d{1,4})(\d{1,6})?(\d{1,5})?/,length:[15],cvcLength:[3,4],luhn:!0},{type:"dinersclub",pattern:/^3[0689]/,format:/(\d{1,4})(\d{1,6})?(\d{1,4})?/,length:[14],cvcLength:[3],luhn:!0},{type:"discover",pattern:/^6([045]|22)/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"unionpay",pattern:/^(62|88)/,format:r,length:[16,17,18,19],cvcLength:[3],luhn:!1},{type:"jcb",pattern:/^35/,format:r,length:[16],cvcLength:[3],luhn:!0}],e=function(e){var t,r,o;for(e=(e+"").replace(/\D/g,""),r=0,o=n.length;o>r;r++)if(t=n[r],t.pattern.test(e))return t},t=function(e){var t,r,o;for(r=0,o=n.length;o>r;r++)if(t=n[r],t.type===e)return t},p=function(e){var t,n,r,o,a,i;for(r=!0,o=0,n=(e+"").split("").reverse(),a=0,i=n.length;i>a;a++)t=n[a],t=parseInt(t,10),(r=!r)&&(t*=2),t>9&&(t-=9),o+=t;return o%10===0},s=function(e){var t;return null!=e.prop("selectionStart")&&e.prop("selectionStart")!==e.prop("selectionEnd")?!0:null!=("undefined"!=typeof document&&null!==document&&null!=(t=document.selection)?t.createRange:void 0)&&document.selection.createRange().text?!0:!1},f=function(e){return setTimeout(function(){var t,n;return t=$(e.currentTarget),n=t.val(),n=n.replace(/\D/g,""),t.val(n)})},d=function(e){return setTimeout(function(){var t,n;return t=$(e.currentTarget),n=t.val(),n=$.payment.formatCardNumber(n),t.val(n)})},i=function(t){var n,r,o,a,i,u,l;return o=String.fromCharCode(t.which),!/^\d+$/.test(o)||(n=$(t.currentTarget),l=n.val(),r=e(l+o),a=(l.replace(/\D/g,"")+o).length,u=16,r&&(u=r.length[r.length.length-1]),a>=u||null!=n.prop("selectionStart")&&n.prop("selectionStart")!==l.length)?void 0:(i=r&&"amex"===r.type?/^(\d{4}|\d{4}\s\d{6})$/:/(?:^|\s)(\d{4})$/,i.test(l)?(t.preventDefault(),setTimeout(function(){return n.val(l+" "+o)})):i.test(l+o)?(t.preventDefault(),setTimeout(function(){return n.val(l+o+" ")})):void 0)},o=function(e){var t,n;return t=$(e.currentTarget),n=t.val(),8!==e.which||null!=t.prop("selectionStart")&&t.prop("selectionStart")!==n.length?void 0:/\d\s$/.test(n)?(e.preventDefault(),setTimeout(function(){return t.val(n.replace(/\d\s$/,""))})):/\s\d?$/.test(n)?(e.preventDefault(),setTimeout(function(){return t.val(n.replace(/\d$/,""))})):void 0},v=function(e){return setTimeout(function(){var t,n;return t=$(e.currentTarget),n=t.val(),n=$.payment.formatExpiry(n),t.val(n)})},u=function(e){var t,n,r;return n=String.fromCharCode(e.which),/^\d+$/.test(n)?(t=$(e.currentTarget),r=t.val()+n,/^\d$/.test(r)&&"0"!==r&&"1"!==r?(e.preventDefault(),setTimeout(function(){return t.val("0"+r+" / ")})):/^\d\d$/.test(r)?(e.preventDefault(),setTimeout(function(){return t.val(""+r+" / ")})):void 0):void 0},l=function(e){var t,n,r;return n=String.fromCharCode(e.which),/^\d+$/.test(n)?(t=$(e.currentTarget),r=t.val(),/^\d\d$/.test(r)?t.val(""+r+" / "):void 0):void 0},c=function(e){var t,n,r;return r=String.fromCharCode(e.which),"/"===r||" "===r?(t=$(e.currentTarget),n=t.val(),/^\d$/.test(n)&&"0"!==n?t.val("0"+n+" / "):void 0):void 0},a=function(e){var t,n;return t=$(e.currentTarget),n=t.val(),8!==e.which||null!=t.prop("selectionStart")&&t.prop("selectionStart")!==n.length?void 0:/\d\s\/\s$/.test(n)?(e.preventDefault(),setTimeout(function(){return t.val(n.replace(/\d\s\/\s$/,""))})):void 0},h=function(e){return setTimeout(function(){var t,n;return t=$(e.currentTarget),n=t.val(),n=n.replace(/\D/g,"").slice(0,4),t.val(n)})},_=function(e){var t;return e.metaKey||e.ctrlKey?!0:32===e.which?!1:0===e.which?!0:e.which<33?!0:(t=String.fromCharCode(e.which),!!/[\d\s]/.test(t))},g=function(t){var n,r,o,a;return n=$(t.currentTarget),o=String.fromCharCode(t.which),/^\d+$/.test(o)&&!s(n)?(a=(n.val()+o).replace(/\D/g,""),r=e(a),r?a.length<=r.length[r.length.length-1]:a.length<=16):void 0},y=function(e){var t,n,r;return t=$(e.currentTarget),n=String.fromCharCode(e.which),/^\d+$/.test(n)&&!s(t)?(r=t.val()+n,r=r.replace(/\D/g,""),r.length>6?!1:void 0):void 0},m=function(e){var t,n,r;return t=$(e.currentTarget),n=String.fromCharCode(e.which),/^\d+$/.test(n)&&!s(t)?(r=t.val()+n,r.length<=4):void 0},b=function(e){var t,r,o,a,i;return t=$(e.currentTarget),i=t.val(),a=$.payment.cardType(i)||"unknown",t.hasClass(a)?void 0:(r=function(){var e,t,r;for(r=[],e=0,t=n.length;t>e;e++)o=n[e],r.push(o.type);return r}(),t.removeClass("unknown"),t.removeClass(r.join(" ")),t.addClass(a),t.toggleClass("identified","unknown"!==a),t.trigger("payment.cardType",a))},$.payment.fn.formatCardCVC=function(){return this.on("keypress",_),this.on("keypress",m),this.on("paste",h),this.on("change",h),this.on("input",h),this},$.payment.fn.formatCardExpiry=function(){return this.on("keypress",_),this.on("keypress",y),this.on("keypress",u),this.on("keypress",c),this.on("keypress",l),this.on("keydown",a),this.on("change",v),this.on("input",v),this},$.payment.fn.formatCardNumber=function(){return this.on("keypress",_),this.on("keypress",g),this.on("keypress",i),this.on("keydown",o),this.on("keyup",b),this.on("paste",d),this.on("change",d),this.on("input",d),this.on("input",b),this},$.payment.fn.restrictNumeric=function(){return this.on("keypress",_),this.on("paste",f),this.on("change",f),this.on("input",f),this},$.payment.fn.cardExpiryVal=function(){return $.payment.cardExpiryVal($(this).val())},$.payment.cardExpiryVal=function(e){var t,n,r,o;return e=e.replace(/\s/g,""),o=e.split("/",2),t=o[0],r=o[1],2===(null!=r?r.length:void 0)&&/^\d+$/.test(r)&&(n=(new Date).getFullYear(),n=n.toString().slice(0,2),r=n+r),t=parseInt(t,10),r=parseInt(r,10),{month:t,year:r}},$.payment.validateCardNumber=function(t){var n,r;return t=(t+"").replace(/\s+|-/g,""),/^\d+$/.test(t)?(n=e(t),n?(r=t.length,w.call(n.length,r)>=0&&(n.luhn===!1||p(t))):!1):!1},$.payment.validateCardExpiry=function(e,t){var n,r,o;return"object"==typeof e&&"month"in e&&(o=e,e=o.month,t=o.year),e&&t?(e=$.trim(e),t=$.trim(t),/^\d+$/.test(e)&&/^\d+$/.test(t)&&e>=1&&12>=e?(2===t.length&&(t=70>t?"20"+t:"19"+t),4!==t.length?!1:(r=new Date(t,e),n=new Date,r.setMonth(r.getMonth()-1),r.setMonth(r.getMonth()+1,1),r>n)):!1):!1},$.payment.validateCardCVC=function(e,n){var r,o;return e=$.trim(e),/^\d+$/.test(e)?(r=t(n),null!=r?(o=e.length,w.call(r.cvcLength,o)>=0):e.length>=3&&e.length<=4):!1},$.payment.cardType=function(t){var n;return t?(null!=(n=e(t))?n.type:void 0)||null:null},$.payment.formatCardNumber=function(t){var n,r,o,a;return t=t.replace(/\D/g,""),(n=e(t))?(o=n.length[n.length.length-1],t=t.slice(0,o),n.format.global?null!=(a=t.match(n.format))?a.join(" "):void 0:(r=n.format.exec(t),null!=r?(r.shift(),r=$.grep(r,function(e){return e}),r.join(" ")):void 0)):t},$.payment.formatExpiry=function(e){var t,n,r,o;return(n=e.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/))?(t=n[1]||"",r=n[2]||"",o=n[3]||"",o.length>0?r=" / ":" /"===r?(t=t.substring(0,1),r=""):2===t.length||r.length>0?r=" / ":1===t.length&&"0"!==t&&"1"!==t&&(t="0"+t,r=" / "),t+r+o):""}}).call(this)},{}],2:[function(e,t,n){function r(e){var t=this,e=e||{};i.publishableKey=t.stripe_key=e.key,t.form=e.form,t.cc_number=o.observable(null),t.cc_expiry=o.observable(null),t.cc_cvv=o.observable(null),t.cc_error_number=o.observable(null),t.cc_error_expiry=o.observable(null),t.cc_error_cvv=o.observable(null),t.initialize_form(),t.error=o.observable(null),t.process_form=function(){var e=a.payment.cardExpiryVal(t.cc_expiry()),n={number:t.cc_number(),exp_month:e.month,exp_year:e.year,cvc:t.cc_cvv()};return t.error(null),t.cc_error_number(null),t.cc_error_expiry(null),t.cc_error_cvv(null),a.payment.validateCardNumber(n.number)?a.payment.validateCardExpiry(n.exp_month,n.exp_year)?a.payment.validateCardCVC(n.cvc)?void i.createToken(n,function(e,n){if(200===e){var r=t.form.find("#id_last_4_digits"),o=t.form.find("#id_stripe_id,#id_stripe_token");r.val(n.card.last4),o.val(n.id),t.form.submit()}else t.error(n.error.message)}):(t.cc_error_cvv("Invalid security code"),!1):(t.cc_error_expiry("Invalid expiration date"),!1):(t.cc_error_number("Invalid card number"),console.log(n),!1)}}var o=e("knockout"),a=(e("./../../../../../bower_components/jquery.payment/lib/jquery.payment.js"),e("jquery")),i=null;"undefined"!=typeof window&&"undefined"!=typeof window.Stripe&&(i=window.Stripe||{}),r.prototype.initialize_form=function(){var e=a("input#cc-number"),t=a("input#cc-cvv"),n=a("input#cc-expiry");e.payment("formatCardNumber"),n.payment("formatCardExpiry"),t.payment("formatCardCVC")},r.init=function(e,t){var n=new GoldView(e),t=t||a("#payment-form")[0];return o.applyBindings(n,t),n},t.exports.PaymentView=r},{"./../../../../../bower_components/jquery.payment/lib/jquery.payment.js":1,jquery:"jquery",knockout:"knockout"}],"donate/donate":[function(e,t,n){function r(e){var t=this,e=e||{};a.utils.extend(t,new o.PaymentView(e)),t.dollars=a.observable(),t.logo_url=a.observable(),t.site_url=a.observable(),a.computed(function(){var e=$("input#id_logo_url").closest("p"),n=$("input#id_site_url").closest("p");t.dollars()<400?(t.logo_url(null),t.site_url(null),e.hide(),n.hide()):(e.show(),n.show())}),t.urls_enabled=a.computed(function(){return t.dollars()>=400})}var o=(e("jquery"),e("../../../../core/static-src/core/js/payment")),a=e("knockout");r.init=function(e,t){var n=new r(e),t=t||$("#donate-payment")[0];return a.applyBindings(n,t),n},t.exports.DonateView=r},{"../../../../core/static-src/core/js/payment":2,jquery:"jquery",knockout:"knockout"}]},{},[]); \ No newline at end of file +require=function e(t,r,n){function o(i,u){if(!r[i]){if(!t[i]){var l="function"==typeof require&&require;if(!u&&l)return l(i,!0);if(a)return a(i,!0);var c=new Error("Cannot find module '"+i+"'");throw c.code="MODULE_NOT_FOUND",c}var s=r[i]={exports:{}};t[i][0].call(s.exports,function(e){var r=t[i][1][e];return o(r?r:e)},s,s.exports,e,t,r,n)}return r[i].exports}for(var a="function"==typeof require&&require,i=0;it;t++)if(t in this&&this[t]===e)return t;return-1};$.payment={},$.payment.fn={},$.fn.payment=function(){var e,t;return t=arguments[0],e=2<=arguments.length?C.call(arguments,1):[],$.payment.fn[t].apply(this,e)},n=/(\d{1,4})/g,$.payment.cards=r=[{type:"visaelectron",pattern:/^4(026|17500|405|508|844|91[37])/,format:n,length:[16],cvcLength:[3],luhn:!0},{type:"maestro",pattern:/^(5(018|0[23]|[68])|6(39|7))/,format:n,length:[12,13,14,15,16,17,18,19],cvcLength:[3],luhn:!0},{type:"forbrugsforeningen",pattern:/^600/,format:n,length:[16],cvcLength:[3],luhn:!0},{type:"dankort",pattern:/^5019/,format:n,length:[16],cvcLength:[3],luhn:!0},{type:"visa",pattern:/^4/,format:n,length:[13,16],cvcLength:[3],luhn:!0},{type:"mastercard",pattern:/^(5[0-5]|2[2-7])/,format:n,length:[16],cvcLength:[3],luhn:!0},{type:"amex",pattern:/^3[47]/,format:/(\d{1,4})(\d{1,6})?(\d{1,5})?/,length:[15],cvcLength:[3,4],luhn:!0},{type:"dinersclub",pattern:/^3[0689]/,format:/(\d{1,4})(\d{1,6})?(\d{1,4})?/,length:[14],cvcLength:[3],luhn:!0},{type:"discover",pattern:/^6([045]|22)/,format:n,length:[16],cvcLength:[3],luhn:!0},{type:"unionpay",pattern:/^(62|88)/,format:n,length:[16,17,18,19],cvcLength:[3],luhn:!1},{type:"jcb",pattern:/^35/,format:n,length:[16],cvcLength:[3],luhn:!0}],e=function(e){var t,n,o;for(e=(e+"").replace(/\D/g,""),n=0,o=r.length;o>n;n++)if(t=r[n],t.pattern.test(e))return t},t=function(e){var t,n,o;for(n=0,o=r.length;o>n;n++)if(t=r[n],t.type===e)return t},p=function(e){var t,r,n,o,a,i;for(n=!0,o=0,r=(e+"").split("").reverse(),a=0,i=r.length;i>a;a++)t=r[a],t=parseInt(t,10),(n=!n)&&(t*=2),t>9&&(t-=9),o+=t;return o%10===0},s=function(e){var t;return null!=e.prop("selectionStart")&&e.prop("selectionStart")!==e.prop("selectionEnd")?!0:null!=("undefined"!=typeof document&&null!==document&&null!=(t=document.selection)?t.createRange:void 0)&&document.selection.createRange().text?!0:!1},f=function(e){return setTimeout(function(){var t,r;return t=$(e.currentTarget),r=t.val(),r=r.replace(/\D/g,""),t.val(r)})},h=function(e){return setTimeout(function(){var t,r;return t=$(e.currentTarget),r=t.val(),r=$.payment.formatCardNumber(r),t.val(r)})},i=function(t){var r,n,o,a,i,u,l;return o=String.fromCharCode(t.which),!/^\d+$/.test(o)||(r=$(t.currentTarget),l=r.val(),n=e(l+o),a=(l.replace(/\D/g,"")+o).length,u=16,n&&(u=n.length[n.length.length-1]),a>=u||null!=r.prop("selectionStart")&&r.prop("selectionStart")!==l.length)?void 0:(i=n&&"amex"===n.type?/^(\d{4}|\d{4}\s\d{6})$/:/(?:^|\s)(\d{4})$/,i.test(l)?(t.preventDefault(),setTimeout(function(){return r.val(l+" "+o)})):i.test(l+o)?(t.preventDefault(),setTimeout(function(){return r.val(l+o+" ")})):void 0)},o=function(e){var t,r;return t=$(e.currentTarget),r=t.val(),8!==e.which||null!=t.prop("selectionStart")&&t.prop("selectionStart")!==r.length?void 0:/\d\s$/.test(r)?(e.preventDefault(),setTimeout(function(){return t.val(r.replace(/\d\s$/,""))})):/\s\d?$/.test(r)?(e.preventDefault(),setTimeout(function(){return t.val(r.replace(/\d$/,""))})):void 0},v=function(e){return setTimeout(function(){var t,r;return t=$(e.currentTarget),r=t.val(),r=$.payment.formatExpiry(r),t.val(r)})},u=function(e){var t,r,n;return r=String.fromCharCode(e.which),/^\d+$/.test(r)?(t=$(e.currentTarget),n=t.val()+r,/^\d$/.test(n)&&"0"!==n&&"1"!==n?(e.preventDefault(),setTimeout(function(){return t.val("0"+n+" / ")})):/^\d\d$/.test(n)?(e.preventDefault(),setTimeout(function(){return t.val(""+n+" / ")})):void 0):void 0},l=function(e){var t,r,n;return r=String.fromCharCode(e.which),/^\d+$/.test(r)?(t=$(e.currentTarget),n=t.val(),/^\d\d$/.test(n)?t.val(""+n+" / "):void 0):void 0},c=function(e){var t,r,n;return n=String.fromCharCode(e.which),"/"===n||" "===n?(t=$(e.currentTarget),r=t.val(),/^\d$/.test(r)&&"0"!==r?t.val("0"+r+" / "):void 0):void 0},a=function(e){var t,r;return t=$(e.currentTarget),r=t.val(),8!==e.which||null!=t.prop("selectionStart")&&t.prop("selectionStart")!==r.length?void 0:/\d\s\/\s$/.test(r)?(e.preventDefault(),setTimeout(function(){return t.val(r.replace(/\d\s\/\s$/,""))})):void 0},d=function(e){return setTimeout(function(){var t,r;return t=$(e.currentTarget),r=t.val(),r=r.replace(/\D/g,"").slice(0,4),t.val(r)})},_=function(e){var t;return e.metaKey||e.ctrlKey?!0:32===e.which?!1:0===e.which?!0:e.which<33?!0:(t=String.fromCharCode(e.which),!!/[\d\s]/.test(t))},g=function(t){var r,n,o,a;return r=$(t.currentTarget),o=String.fromCharCode(t.which),/^\d+$/.test(o)&&!s(r)?(a=(r.val()+o).replace(/\D/g,""),n=e(a),n?a.length<=n.length[n.length.length-1]:a.length<=16):void 0},y=function(e){var t,r,n;return t=$(e.currentTarget),r=String.fromCharCode(e.which),/^\d+$/.test(r)&&!s(t)?(n=t.val()+r,n=n.replace(/\D/g,""),n.length>6?!1:void 0):void 0},m=function(e){var t,r,n;return t=$(e.currentTarget),r=String.fromCharCode(e.which),/^\d+$/.test(r)&&!s(t)?(n=t.val()+r,n.length<=4):void 0},b=function(e){var t,n,o,a,i;return t=$(e.currentTarget),i=t.val(),a=$.payment.cardType(i)||"unknown",t.hasClass(a)?void 0:(n=function(){var e,t,n;for(n=[],e=0,t=r.length;t>e;e++)o=r[e],n.push(o.type);return n}(),t.removeClass("unknown"),t.removeClass(n.join(" ")),t.addClass(a),t.toggleClass("identified","unknown"!==a),t.trigger("payment.cardType",a))},$.payment.fn.formatCardCVC=function(){return this.on("keypress",_),this.on("keypress",m),this.on("paste",d),this.on("change",d),this.on("input",d),this},$.payment.fn.formatCardExpiry=function(){return this.on("keypress",_),this.on("keypress",y),this.on("keypress",u),this.on("keypress",c),this.on("keypress",l),this.on("keydown",a),this.on("change",v),this.on("input",v),this},$.payment.fn.formatCardNumber=function(){return this.on("keypress",_),this.on("keypress",g),this.on("keypress",i),this.on("keydown",o),this.on("keyup",b),this.on("paste",h),this.on("change",h),this.on("input",h),this.on("input",b),this},$.payment.fn.restrictNumeric=function(){return this.on("keypress",_),this.on("paste",f),this.on("change",f),this.on("input",f),this},$.payment.fn.cardExpiryVal=function(){return $.payment.cardExpiryVal($(this).val())},$.payment.cardExpiryVal=function(e){var t,r,n,o;return e=e.replace(/\s/g,""),o=e.split("/",2),t=o[0],n=o[1],2===(null!=n?n.length:void 0)&&/^\d+$/.test(n)&&(r=(new Date).getFullYear(),r=r.toString().slice(0,2),n=r+n),t=parseInt(t,10),n=parseInt(n,10),{month:t,year:n}},$.payment.validateCardNumber=function(t){var r,n;return t=(t+"").replace(/\s+|-/g,""),/^\d+$/.test(t)?(r=e(t),r?(n=t.length,w.call(r.length,n)>=0&&(r.luhn===!1||p(t))):!1):!1},$.payment.validateCardExpiry=function(e,t){var r,n,o;return"object"==typeof e&&"month"in e&&(o=e,e=o.month,t=o.year),e&&t?(e=$.trim(e),t=$.trim(t),/^\d+$/.test(e)&&/^\d+$/.test(t)&&e>=1&&12>=e?(2===t.length&&(t=70>t?"20"+t:"19"+t),4!==t.length?!1:(n=new Date(t,e),r=new Date,n.setMonth(n.getMonth()-1),n.setMonth(n.getMonth()+1,1),n>r)):!1):!1},$.payment.validateCardCVC=function(e,r){var n,o;return e=$.trim(e),/^\d+$/.test(e)?(n=t(r),null!=n?(o=e.length,w.call(n.cvcLength,o)>=0):e.length>=3&&e.length<=4):!1},$.payment.cardType=function(t){var r;return t?(null!=(r=e(t))?r.type:void 0)||null:null},$.payment.formatCardNumber=function(t){var r,n,o,a;return t=t.replace(/\D/g,""),(r=e(t))?(o=r.length[r.length.length-1],t=t.slice(0,o),r.format.global?null!=(a=t.match(r.format))?a.join(" "):void 0:(n=r.format.exec(t),null!=n?(n.shift(),n=$.grep(n,function(e){return e}),n.join(" ")):void 0)):t},$.payment.formatExpiry=function(e){var t,r,n,o;return(r=e.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/))?(t=r[1]||"",n=r[2]||"",o=r[3]||"",o.length>0?n=" / ":" /"===n?(t=t.substring(0,1),n=""):2===t.length||n.length>0?n=" / ":1===t.length&&"0"!==t&&"1"!==t&&(t="0"+t,n=" / "),t+n+o):""}}).call(this)},{}],2:[function(e,t,r){function n(e){var t=this,e=e||{};i.publishableKey=t.stripe_key=e.key,t.form=e.form,t.cc_number=o.observable(null),t.cc_expiry=o.observable(null),t.cc_cvv=o.observable(null),t.error_cc_number=o.observable(null),t.error_cc_expiry=o.observable(null),t.error_cc_cvv=o.observable(null),t.stripe_token=o.observable(null),t.card_digits=o.observable(null),t.is_editing_card=o.observable(!1),t.show_card_form=o.computed(function(){return t.is_editing_card()||!t.card_digits()||t.cc_number()||t.cc_expiry()||t.cc_cvv()}),t.initialize_form(),t.error=o.observable(null),t.process_form=function(){var e=a.payment.cardExpiryVal(t.cc_expiry()),r={number:t.cc_number(),exp_month:e.month,exp_year:e.year,cvc:t.cc_cvv()};return t.error(null),t.error_cc_number(null),t.error_cc_expiry(null),t.error_cc_cvv(null),a.payment.validateCardNumber(r.number)?a.payment.validateCardExpiry(r.exp_month,r.exp_year)?a.payment.validateCardCVC(r.cvc)?void i.createToken(r,function(e,r){200===e?t.submit_form(r.card.last4,r.id):t.error(r.error.message)}):(t.error_cc_cvv("Invalid security code"),!1):(t.error_cc_expiry("Invalid expiration date"),!1):(t.error_cc_number("Invalid card number"),!1)},t.process_full_form=function(){return t.show_card_form()?void t.process_form():!0}}var o=e("knockout"),a=(e("./../../../../../bower_components/jquery.payment/lib/jquery.payment.js"),e("jquery")),i=null;"undefined"!=typeof window&&"undefined"!=typeof window.Stripe&&(i=window.Stripe||{}),o.bindingHandlers.valueInit={init:function(e,t){var r=t();o.isWriteableObservable(r)&&r(e.value)}},n.prototype.submit_form=function(e,t){this.form.find("#id_card_digits").val(e),this.form.find("#id_stripe_token").val(t),this.form.submit()},n.prototype.initialize_form=function(){var e=a("input#id_cc_number"),t=a("input#id_cc_cvv"),r=a("input#id_cc_expiry");e.payment("formatCardNumber"),r.payment("formatCardExpiry"),t.payment("formatCardCVC"),e.trigger("keyup")},n.init=function(e,t){var r=new n(e),t=t||a("#payment-form")[0];return o.applyBindings(r,t),r},t.exports.PaymentView=n},{"./../../../../../bower_components/jquery.payment/lib/jquery.payment.js":1,jquery:"jquery",knockout:"knockout"}],"donate/donate":[function(e,t,r){function n(e){var t=this,e=e||{};t.constructor.call(t,e),t.dollars=a.observable(),t.logo_url=a.observable(),t.site_url=a.observable(),t.error_dollars=a.observable(),t.error_logo_url=a.observable(),t.error_site_url=a.observable(),a.computed(function(){var e=$("input#id_logo_url").closest("p"),r=$("input#id_site_url").closest("p");t.dollars()<400?(t.logo_url(null),t.site_url(null),e.hide(),r.hide()):(e.show(),r.show())}),t.urls_enabled=a.computed(function(){return t.dollars()>=400})}var o=(e("jquery"),e("readthedocs/payments/static-src/payments/js/base")),a=e("knockout");n.prototype=new o.PaymentView,n.init=function(e,t){var r=new n(e),t=t||$("#donate-payment")[0];return a.applyBindings(r,t),r},t.exports.DonateView=n},{jquery:"jquery",knockout:"knockout","readthedocs/payments/static-src/payments/js/base":2}]},{},[]); \ No newline at end of file diff --git a/readthedocs/donate/templates/donate/create.html b/readthedocs/donate/templates/donate/create.html index 778893eb6..31ec43001 100644 --- a/readthedocs/donate/templates/donate/create.html +++ b/readthedocs/donate/templates/donate/create.html @@ -5,6 +5,10 @@ {% block title %}{% trans "Sustainability" %}{% endblock %} +{% block extra_links %} + +{% endblock %} + {% block extra_scripts %} @@ -14,7 +18,7 @@ var donate_views = require('donate/donate'); $(document).ready(function () { var key; // var view = donate_views.DonateView.init({ @@ -36,9 +40,24 @@ $(document).ready(function () { payment is processed directly through Stripe.

- {% endblock %} diff --git a/readthedocs/donate/views.py b/readthedocs/donate/views.py index f183314c2..c339a5fc9 100644 --- a/readthedocs/donate/views.py +++ b/readthedocs/donate/views.py @@ -4,13 +4,16 @@ 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.conf import settings 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 @@ -18,7 +21,7 @@ from .mixins import DonateProgressMixin log = logging.getLogger(__name__) -class DonateCreateView(SuccessMessageMixin, StripeMixin, CreateView): +class DonateCreateView(StripeMixin, CreateView): '''Create a donation locally and in Stripe''' form_class = SupporterForm @@ -31,10 +34,9 @@ class DonateCreateView(SuccessMessageMixin, StripeMixin, CreateView): 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): diff --git a/readthedocs/gold/__init__.py b/readthedocs/gold/__init__.py index e69de29bb..a3104b1b0 100644 --- a/readthedocs/gold/__init__.py +++ b/readthedocs/gold/__init__.py @@ -0,0 +1 @@ +default_app_config = 'readthedocs.gold.apps.GoldAppConfig' diff --git a/readthedocs/gold/apps.py b/readthedocs/gold/apps.py new file mode 100644 index 000000000..d9cfa6990 --- /dev/null +++ b/readthedocs/gold/apps.py @@ -0,0 +1,16 @@ +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 diff --git a/readthedocs/gold/forms.py b/readthedocs/gold/forms.py index fa4f299b0..cc25322f8 100644 --- a/readthedocs/gold/forms.py +++ b/readthedocs/gold/forms.py @@ -1,20 +1,25 @@ from django import forms -from .models import LEVEL_CHOICES +from stripe.error import InvalidRequestError +from readthedocs.payments.forms import (StripeSubscriptionModelForm, + StripeResourceMixin) + +from .models import LEVEL_CHOICES, GoldUser -class CardForm(forms.Form): +class GoldSubscriptionForm(StripeResourceMixin, StripeSubscriptionModelForm): + + class Meta: + model = GoldUser + fields = ['last_4_digits', 'level'] last_4_digits = forms.CharField( required=True, min_length=4, max_length=4, - widget=forms.HiddenInput() - ) - - stripe_token = forms.CharField( - required=True, - widget=forms.HiddenInput() + widget=forms.HiddenInput(attrs={ + 'data-bind': 'valueInit: card_digits, value: card_digits' + }) ) level = forms.ChoiceField( @@ -22,6 +27,44 @@ class CardForm(forms.Form): choices=LEVEL_CHOICES, ) + def clean(self): + self.instance.user = self.customer + return super(GoldSubscriptionForm, self).clean() + + def validate_stripe(self): + subscription = self.get_subscription() + self.instance.stripe_id = subscription.customer + self.instance.subscribed = True + + def get_customer_kwargs(self): + return { + 'description': self.customer.get_full_name(), + 'email': self.customer.email, + 'id': self.instance.stripe_id or None + } + + def get_subscription(self): + customer = self.get_customer() + try: + # TODO get the first sub more intelligently + subscriptions = customer.subscriptions.all(limit=5) + subscription = subscriptions.data[0] + subscription.plan = self.cleaned_data['level'] + if 'stripe_token' in self.cleaned_data: + subscription.source = self.cleaned_data['stripe_token'] + subscription.save() + return subscription + except (InvalidRequestError, AttributeError, IndexError): + subscription = customer.subscriptions.create( + plan=self.cleaned_data['level'], + source=self.cleaned_data['stripe_token'] + ) + return subscription + + def clear_card_data(self): + super(GoldSubscriptionForm, self).clear_card_data() + self.data['last_4_digits'] = None + class GoldProjectForm(forms.Form): project = forms.CharField( diff --git a/readthedocs/gold/signals.py b/readthedocs/gold/signals.py new file mode 100644 index 000000000..b7d34ddd1 --- /dev/null +++ b/readthedocs/gold/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import pre_delete +from django.dispatch import receiver + +from readthedocs.payments import utils + +from .models import GoldUser + + +@receiver(pre_delete, sender=GoldUser) +def delete_customer(sender, instance, **kwargs): + """On Gold subscription deletion, remove the customer from Stripe""" + if instance.stripe_id is not None: + utils.delete_customer(instance.stripe_id) diff --git a/readthedocs/gold/static-src/gold/js/gold.js b/readthedocs/gold/static-src/gold/js/gold.js index e06f378cd..ce0902121 100644 --- a/readthedocs/gold/static-src/gold/js/gold.js +++ b/readthedocs/gold/static-src/gold/js/gold.js @@ -1,16 +1,20 @@ // Gold 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 GoldView (config) { var self = this, config = config || {}; - ko.utils.extend(self, new payment.PaymentView(config)); + self.constructor.call(self, config); + + self.last_4_digits = ko.observable(null); } +GoldView.prototype = new payment.PaymentView(); + GoldView.init = function (config, obj) { var view = new GoldView(config), obj = obj || $('#payment-form')[0]; @@ -18,4 +22,10 @@ GoldView.init = function (config, obj) { return view; } +GoldView.prototype.submit_form = function (card_digits, token) { + this.form.find('#id_last_4_digits').val(card_digits); + this.form.find('#id_stripe_token').val(token); + this.form.submit(); +}; + module.exports.GoldView = GoldView; diff --git a/readthedocs/gold/static/gold/js/gold.js b/readthedocs/gold/static/gold/js/gold.js index cbcc7ddaa..8631518d4 100644 --- a/readthedocs/gold/static/gold/js/gold.js +++ b/readthedocs/gold/static/gold/js/gold.js @@ -1 +1 @@ -require=function t(e,n,r){function a(i,u){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!u&&c)return c(i,!0);if(o)return o(i,!0);var l=new Error("Cannot find module '"+i+"'");throw l.code="MODULE_NOT_FOUND",l}var s=n[i]={exports:{}};e[i][0].call(s.exports,function(t){var n=e[i][1][t];return a(n?n:t)},s,s.exports,t,e,n,r)}return n[i].exports}for(var o="function"==typeof require&&require,i=0;ie;e++)if(e in this&&this[e]===t)return e;return-1};$.payment={},$.payment.fn={},$.fn.payment=function(){var t,e;return e=arguments[0],t=2<=arguments.length?b.call(arguments,1):[],$.payment.fn[e].apply(this,t)},r=/(\d{1,4})/g,$.payment.cards=n=[{type:"visaelectron",pattern:/^4(026|17500|405|508|844|91[37])/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"maestro",pattern:/^(5(018|0[23]|[68])|6(39|7))/,format:r,length:[12,13,14,15,16,17,18,19],cvcLength:[3],luhn:!0},{type:"forbrugsforeningen",pattern:/^600/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"dankort",pattern:/^5019/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"visa",pattern:/^4/,format:r,length:[13,16],cvcLength:[3],luhn:!0},{type:"mastercard",pattern:/^(5[0-5]|2[2-7])/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"amex",pattern:/^3[47]/,format:/(\d{1,4})(\d{1,6})?(\d{1,5})?/,length:[15],cvcLength:[3,4],luhn:!0},{type:"dinersclub",pattern:/^3[0689]/,format:/(\d{1,4})(\d{1,6})?(\d{1,4})?/,length:[14],cvcLength:[3],luhn:!0},{type:"discover",pattern:/^6([045]|22)/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"unionpay",pattern:/^(62|88)/,format:r,length:[16,17,18,19],cvcLength:[3],luhn:!1},{type:"jcb",pattern:/^35/,format:r,length:[16],cvcLength:[3],luhn:!0}],t=function(t){var e,r,a;for(t=(t+"").replace(/\D/g,""),r=0,a=n.length;a>r;r++)if(e=n[r],e.pattern.test(t))return e},e=function(t){var e,r,a;for(r=0,a=n.length;a>r;r++)if(e=n[r],e.type===t)return e},p=function(t){var e,n,r,a,o,i;for(r=!0,a=0,n=(t+"").split("").reverse(),o=0,i=n.length;i>o;o++)e=n[o],e=parseInt(e,10),(r=!r)&&(e*=2),e>9&&(e-=9),a+=e;return a%10===0},s=function(t){var e;return null!=t.prop("selectionStart")&&t.prop("selectionStart")!==t.prop("selectionEnd")?!0:null!=("undefined"!=typeof document&&null!==document&&null!=(e=document.selection)?e.createRange:void 0)&&document.selection.createRange().text?!0:!1},d=function(t){return setTimeout(function(){var e,n;return e=$(t.currentTarget),n=e.val(),n=n.replace(/\D/g,""),e.val(n)})},v=function(t){return setTimeout(function(){var e,n;return e=$(t.currentTarget),n=e.val(),n=$.payment.formatCardNumber(n),e.val(n)})},i=function(e){var n,r,a,o,i,u,c;return a=String.fromCharCode(e.which),!/^\d+$/.test(a)||(n=$(e.currentTarget),c=n.val(),r=t(c+a),o=(c.replace(/\D/g,"")+a).length,u=16,r&&(u=r.length[r.length.length-1]),o>=u||null!=n.prop("selectionStart")&&n.prop("selectionStart")!==c.length)?void 0:(i=r&&"amex"===r.type?/^(\d{4}|\d{4}\s\d{6})$/:/(?:^|\s)(\d{4})$/,i.test(c)?(e.preventDefault(),setTimeout(function(){return n.val(c+" "+a)})):i.test(c+a)?(e.preventDefault(),setTimeout(function(){return n.val(c+a+" ")})):void 0)},a=function(t){var e,n;return e=$(t.currentTarget),n=e.val(),8!==t.which||null!=e.prop("selectionStart")&&e.prop("selectionStart")!==n.length?void 0:/\d\s$/.test(n)?(t.preventDefault(),setTimeout(function(){return e.val(n.replace(/\d\s$/,""))})):/\s\d?$/.test(n)?(t.preventDefault(),setTimeout(function(){return e.val(n.replace(/\d$/,""))})):void 0},f=function(t){return setTimeout(function(){var e,n;return e=$(t.currentTarget),n=e.val(),n=$.payment.formatExpiry(n),e.val(n)})},u=function(t){var e,n,r;return n=String.fromCharCode(t.which),/^\d+$/.test(n)?(e=$(t.currentTarget),r=e.val()+n,/^\d$/.test(r)&&"0"!==r&&"1"!==r?(t.preventDefault(),setTimeout(function(){return e.val("0"+r+" / ")})):/^\d\d$/.test(r)?(t.preventDefault(),setTimeout(function(){return e.val(""+r+" / ")})):void 0):void 0},c=function(t){var e,n,r;return n=String.fromCharCode(t.which),/^\d+$/.test(n)?(e=$(t.currentTarget),r=e.val(),/^\d\d$/.test(r)?e.val(""+r+" / "):void 0):void 0},l=function(t){var e,n,r;return r=String.fromCharCode(t.which),"/"===r||" "===r?(e=$(t.currentTarget),n=e.val(),/^\d$/.test(n)&&"0"!==n?e.val("0"+n+" / "):void 0):void 0},o=function(t){var e,n;return e=$(t.currentTarget),n=e.val(),8!==t.which||null!=e.prop("selectionStart")&&e.prop("selectionStart")!==n.length?void 0:/\d\s\/\s$/.test(n)?(t.preventDefault(),setTimeout(function(){return e.val(n.replace(/\d\s\/\s$/,""))})):void 0},h=function(t){return setTimeout(function(){var e,n;return e=$(t.currentTarget),n=e.val(),n=n.replace(/\D/g,"").slice(0,4),e.val(n)})},C=function(t){var e;return t.metaKey||t.ctrlKey?!0:32===t.which?!1:0===t.which?!0:t.which<33?!0:(e=String.fromCharCode(t.which),!!/[\d\s]/.test(e))},g=function(e){var n,r,a,o;return n=$(e.currentTarget),a=String.fromCharCode(e.which),/^\d+$/.test(a)&&!s(n)?(o=(n.val()+a).replace(/\D/g,""),r=t(o),r?o.length<=r.length[r.length.length-1]:o.length<=16):void 0},y=function(t){var e,n,r;return e=$(t.currentTarget),n=String.fromCharCode(t.which),/^\d+$/.test(n)&&!s(e)?(r=e.val()+n,r=r.replace(/\D/g,""),r.length>6?!1:void 0):void 0},m=function(t){var e,n,r;return e=$(t.currentTarget),n=String.fromCharCode(t.which),/^\d+$/.test(n)&&!s(e)?(r=e.val()+n,r.length<=4):void 0},_=function(t){var e,r,a,o,i;return e=$(t.currentTarget),i=e.val(),o=$.payment.cardType(i)||"unknown",e.hasClass(o)?void 0:(r=function(){var t,e,r;for(r=[],t=0,e=n.length;e>t;t++)a=n[t],r.push(a.type);return r}(),e.removeClass("unknown"),e.removeClass(r.join(" ")),e.addClass(o),e.toggleClass("identified","unknown"!==o),e.trigger("payment.cardType",o))},$.payment.fn.formatCardCVC=function(){return this.on("keypress",C),this.on("keypress",m),this.on("paste",h),this.on("change",h),this.on("input",h),this},$.payment.fn.formatCardExpiry=function(){return this.on("keypress",C),this.on("keypress",y),this.on("keypress",u),this.on("keypress",l),this.on("keypress",c),this.on("keydown",o),this.on("change",f),this.on("input",f),this},$.payment.fn.formatCardNumber=function(){return this.on("keypress",C),this.on("keypress",g),this.on("keypress",i),this.on("keydown",a),this.on("keyup",_),this.on("paste",v),this.on("change",v),this.on("input",v),this.on("input",_),this},$.payment.fn.restrictNumeric=function(){return this.on("keypress",C),this.on("paste",d),this.on("change",d),this.on("input",d),this},$.payment.fn.cardExpiryVal=function(){return $.payment.cardExpiryVal($(this).val())},$.payment.cardExpiryVal=function(t){var e,n,r,a;return t=t.replace(/\s/g,""),a=t.split("/",2),e=a[0],r=a[1],2===(null!=r?r.length:void 0)&&/^\d+$/.test(r)&&(n=(new Date).getFullYear(),n=n.toString().slice(0,2),r=n+r),e=parseInt(e,10),r=parseInt(r,10),{month:e,year:r}},$.payment.validateCardNumber=function(e){var n,r;return e=(e+"").replace(/\s+|-/g,""),/^\d+$/.test(e)?(n=t(e),n?(r=e.length,w.call(n.length,r)>=0&&(n.luhn===!1||p(e))):!1):!1},$.payment.validateCardExpiry=function(t,e){var n,r,a;return"object"==typeof t&&"month"in t&&(a=t,t=a.month,e=a.year),t&&e?(t=$.trim(t),e=$.trim(e),/^\d+$/.test(t)&&/^\d+$/.test(e)&&t>=1&&12>=t?(2===e.length&&(e=70>e?"20"+e:"19"+e),4!==e.length?!1:(r=new Date(e,t),n=new Date,r.setMonth(r.getMonth()-1),r.setMonth(r.getMonth()+1,1),r>n)):!1):!1},$.payment.validateCardCVC=function(t,n){var r,a;return t=$.trim(t),/^\d+$/.test(t)?(r=e(n),null!=r?(a=t.length,w.call(r.cvcLength,a)>=0):t.length>=3&&t.length<=4):!1},$.payment.cardType=function(e){var n;return e?(null!=(n=t(e))?n.type:void 0)||null:null},$.payment.formatCardNumber=function(e){var n,r,a,o;return e=e.replace(/\D/g,""),(n=t(e))?(a=n.length[n.length.length-1],e=e.slice(0,a),n.format.global?null!=(o=e.match(n.format))?o.join(" "):void 0:(r=n.format.exec(e),null!=r?(r.shift(),r=$.grep(r,function(t){return t}),r.join(" ")):void 0)):e},$.payment.formatExpiry=function(t){var e,n,r,a;return(n=t.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/))?(e=n[1]||"",r=n[2]||"",a=n[3]||"",a.length>0?r=" / ":" /"===r?(e=e.substring(0,1),r=""):2===e.length||r.length>0?r=" / ":1===e.length&&"0"!==e&&"1"!==e&&(e="0"+e,r=" / "),e+r+a):""}}).call(this)},{}],2:[function(t,e,n){function r(t){var e=this,t=t||{};i.publishableKey=e.stripe_key=t.key,e.form=t.form,e.cc_number=a.observable(null),e.cc_expiry=a.observable(null),e.cc_cvv=a.observable(null),e.cc_error_number=a.observable(null),e.cc_error_expiry=a.observable(null),e.cc_error_cvv=a.observable(null),e.initialize_form(),e.error=a.observable(null),e.process_form=function(){var t=o.payment.cardExpiryVal(e.cc_expiry()),n={number:e.cc_number(),exp_month:t.month,exp_year:t.year,cvc:e.cc_cvv()};return e.error(null),e.cc_error_number(null),e.cc_error_expiry(null),e.cc_error_cvv(null),o.payment.validateCardNumber(n.number)?o.payment.validateCardExpiry(n.exp_month,n.exp_year)?o.payment.validateCardCVC(n.cvc)?void i.createToken(n,function(t,n){if(200===t){var r=e.form.find("#id_last_4_digits"),a=e.form.find("#id_stripe_id,#id_stripe_token");r.val(n.card.last4),a.val(n.id),e.form.submit()}else e.error(n.error.message)}):(e.cc_error_cvv("Invalid security code"),!1):(e.cc_error_expiry("Invalid expiration date"),!1):(e.cc_error_number("Invalid card number"),console.log(n),!1)}}var a=t("knockout"),o=(t("./../../../../../bower_components/jquery.payment/lib/jquery.payment.js"),t("jquery")),i=null;"undefined"!=typeof window&&"undefined"!=typeof window.Stripe&&(i=window.Stripe||{}),r.prototype.initialize_form=function(){var t=o("input#cc-number"),e=o("input#cc-cvv"),n=o("input#cc-expiry");t.payment("formatCardNumber"),n.payment("formatCardExpiry"),e.payment("formatCardCVC")},r.init=function(t,e){var n=new GoldView(t),e=e||o("#payment-form")[0];return a.applyBindings(n,e),n},e.exports.PaymentView=r},{"./../../../../../bower_components/jquery.payment/lib/jquery.payment.js":1,jquery:"jquery",knockout:"knockout"}],"gold/gold":[function(t,e,n){function r(t){var e=this,t=t||{};o.utils.extend(e,new a.PaymentView(t))}var a=(t("jquery"),t("../../../../core/static-src/core/js/payment")),o=t("knockout");r.init=function(t,e){var n=new r(t),e=e||$("#payment-form")[0];return o.applyBindings(n,e),n},e.exports.GoldView=r},{"../../../../core/static-src/core/js/payment":2,jquery:"jquery",knockout:"knockout"}]},{},[]); \ No newline at end of file +require=function t(e,n,r){function a(i,u){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!u&&c)return c(i,!0);if(o)return o(i,!0);var l=new Error("Cannot find module '"+i+"'");throw l.code="MODULE_NOT_FOUND",l}var s=n[i]={exports:{}};e[i][0].call(s.exports,function(t){var n=e[i][1][t];return a(n?n:t)},s,s.exports,t,e,n,r)}return n[i].exports}for(var o="function"==typeof require&&require,i=0;ie;e++)if(e in this&&this[e]===t)return e;return-1};$.payment={},$.payment.fn={},$.fn.payment=function(){var t,e;return e=arguments[0],t=2<=arguments.length?C.call(arguments,1):[],$.payment.fn[e].apply(this,t)},r=/(\d{1,4})/g,$.payment.cards=n=[{type:"visaelectron",pattern:/^4(026|17500|405|508|844|91[37])/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"maestro",pattern:/^(5(018|0[23]|[68])|6(39|7))/,format:r,length:[12,13,14,15,16,17,18,19],cvcLength:[3],luhn:!0},{type:"forbrugsforeningen",pattern:/^600/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"dankort",pattern:/^5019/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"visa",pattern:/^4/,format:r,length:[13,16],cvcLength:[3],luhn:!0},{type:"mastercard",pattern:/^(5[0-5]|2[2-7])/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"amex",pattern:/^3[47]/,format:/(\d{1,4})(\d{1,6})?(\d{1,5})?/,length:[15],cvcLength:[3,4],luhn:!0},{type:"dinersclub",pattern:/^3[0689]/,format:/(\d{1,4})(\d{1,6})?(\d{1,4})?/,length:[14],cvcLength:[3],luhn:!0},{type:"discover",pattern:/^6([045]|22)/,format:r,length:[16],cvcLength:[3],luhn:!0},{type:"unionpay",pattern:/^(62|88)/,format:r,length:[16,17,18,19],cvcLength:[3],luhn:!1},{type:"jcb",pattern:/^35/,format:r,length:[16],cvcLength:[3],luhn:!0}],t=function(t){var e,r,a;for(t=(t+"").replace(/\D/g,""),r=0,a=n.length;a>r;r++)if(e=n[r],e.pattern.test(t))return e},e=function(t){var e,r,a;for(r=0,a=n.length;a>r;r++)if(e=n[r],e.type===t)return e},p=function(t){var e,n,r,a,o,i;for(r=!0,a=0,n=(t+"").split("").reverse(),o=0,i=n.length;i>o;o++)e=n[o],e=parseInt(e,10),(r=!r)&&(e*=2),e>9&&(e-=9),a+=e;return a%10===0},s=function(t){var e;return null!=t.prop("selectionStart")&&t.prop("selectionStart")!==t.prop("selectionEnd")?!0:null!=("undefined"!=typeof document&&null!==document&&null!=(e=document.selection)?e.createRange:void 0)&&document.selection.createRange().text?!0:!1},v=function(t){return setTimeout(function(){var e,n;return e=$(t.currentTarget),n=e.val(),n=n.replace(/\D/g,""),e.val(n)})},d=function(t){return setTimeout(function(){var e,n;return e=$(t.currentTarget),n=e.val(),n=$.payment.formatCardNumber(n),e.val(n)})},i=function(e){var n,r,a,o,i,u,c;return a=String.fromCharCode(e.which),!/^\d+$/.test(a)||(n=$(e.currentTarget),c=n.val(),r=t(c+a),o=(c.replace(/\D/g,"")+a).length,u=16,r&&(u=r.length[r.length.length-1]),o>=u||null!=n.prop("selectionStart")&&n.prop("selectionStart")!==c.length)?void 0:(i=r&&"amex"===r.type?/^(\d{4}|\d{4}\s\d{6})$/:/(?:^|\s)(\d{4})$/,i.test(c)?(e.preventDefault(),setTimeout(function(){return n.val(c+" "+a)})):i.test(c+a)?(e.preventDefault(),setTimeout(function(){return n.val(c+a+" ")})):void 0)},a=function(t){var e,n;return e=$(t.currentTarget),n=e.val(),8!==t.which||null!=e.prop("selectionStart")&&e.prop("selectionStart")!==n.length?void 0:/\d\s$/.test(n)?(t.preventDefault(),setTimeout(function(){return e.val(n.replace(/\d\s$/,""))})):/\s\d?$/.test(n)?(t.preventDefault(),setTimeout(function(){return e.val(n.replace(/\d$/,""))})):void 0},f=function(t){return setTimeout(function(){var e,n;return e=$(t.currentTarget),n=e.val(),n=$.payment.formatExpiry(n),e.val(n)})},u=function(t){var e,n,r;return n=String.fromCharCode(t.which),/^\d+$/.test(n)?(e=$(t.currentTarget),r=e.val()+n,/^\d$/.test(r)&&"0"!==r&&"1"!==r?(t.preventDefault(),setTimeout(function(){return e.val("0"+r+" / ")})):/^\d\d$/.test(r)?(t.preventDefault(),setTimeout(function(){return e.val(""+r+" / ")})):void 0):void 0},c=function(t){var e,n,r;return n=String.fromCharCode(t.which),/^\d+$/.test(n)?(e=$(t.currentTarget),r=e.val(),/^\d\d$/.test(r)?e.val(""+r+" / "):void 0):void 0},l=function(t){var e,n,r;return r=String.fromCharCode(t.which),"/"===r||" "===r?(e=$(t.currentTarget),n=e.val(),/^\d$/.test(n)&&"0"!==n?e.val("0"+n+" / "):void 0):void 0},o=function(t){var e,n;return e=$(t.currentTarget),n=e.val(),8!==t.which||null!=e.prop("selectionStart")&&e.prop("selectionStart")!==n.length?void 0:/\d\s\/\s$/.test(n)?(t.preventDefault(),setTimeout(function(){return e.val(n.replace(/\d\s\/\s$/,""))})):void 0},h=function(t){return setTimeout(function(){var e,n;return e=$(t.currentTarget),n=e.val(),n=n.replace(/\D/g,"").slice(0,4),e.val(n)})},_=function(t){var e;return t.metaKey||t.ctrlKey?!0:32===t.which?!1:0===t.which?!0:t.which<33?!0:(e=String.fromCharCode(t.which),!!/[\d\s]/.test(e))},g=function(e){var n,r,a,o;return n=$(e.currentTarget),a=String.fromCharCode(e.which),/^\d+$/.test(a)&&!s(n)?(o=(n.val()+a).replace(/\D/g,""),r=t(o),r?o.length<=r.length[r.length.length-1]:o.length<=16):void 0},y=function(t){var e,n,r;return e=$(t.currentTarget),n=String.fromCharCode(t.which),/^\d+$/.test(n)&&!s(e)?(r=e.val()+n,r=r.replace(/\D/g,""),r.length>6?!1:void 0):void 0},m=function(t){var e,n,r;return e=$(t.currentTarget),n=String.fromCharCode(t.which),/^\d+$/.test(n)&&!s(e)?(r=e.val()+n,r.length<=4):void 0},b=function(t){var e,r,a,o,i;return e=$(t.currentTarget),i=e.val(),o=$.payment.cardType(i)||"unknown",e.hasClass(o)?void 0:(r=function(){var t,e,r;for(r=[],t=0,e=n.length;e>t;t++)a=n[t],r.push(a.type);return r}(),e.removeClass("unknown"),e.removeClass(r.join(" ")),e.addClass(o),e.toggleClass("identified","unknown"!==o),e.trigger("payment.cardType",o))},$.payment.fn.formatCardCVC=function(){return this.on("keypress",_),this.on("keypress",m),this.on("paste",h),this.on("change",h),this.on("input",h),this},$.payment.fn.formatCardExpiry=function(){return this.on("keypress",_),this.on("keypress",y),this.on("keypress",u),this.on("keypress",l),this.on("keypress",c),this.on("keydown",o),this.on("change",f),this.on("input",f),this},$.payment.fn.formatCardNumber=function(){return this.on("keypress",_),this.on("keypress",g),this.on("keypress",i),this.on("keydown",a),this.on("keyup",b),this.on("paste",d),this.on("change",d),this.on("input",d),this.on("input",b),this},$.payment.fn.restrictNumeric=function(){return this.on("keypress",_),this.on("paste",v),this.on("change",v),this.on("input",v),this},$.payment.fn.cardExpiryVal=function(){return $.payment.cardExpiryVal($(this).val())},$.payment.cardExpiryVal=function(t){var e,n,r,a;return t=t.replace(/\s/g,""),a=t.split("/",2),e=a[0],r=a[1],2===(null!=r?r.length:void 0)&&/^\d+$/.test(r)&&(n=(new Date).getFullYear(),n=n.toString().slice(0,2),r=n+r),e=parseInt(e,10),r=parseInt(r,10),{month:e,year:r}},$.payment.validateCardNumber=function(e){var n,r;return e=(e+"").replace(/\s+|-/g,""),/^\d+$/.test(e)?(n=t(e),n?(r=e.length,w.call(n.length,r)>=0&&(n.luhn===!1||p(e))):!1):!1},$.payment.validateCardExpiry=function(t,e){var n,r,a;return"object"==typeof t&&"month"in t&&(a=t,t=a.month,e=a.year),t&&e?(t=$.trim(t),e=$.trim(e),/^\d+$/.test(t)&&/^\d+$/.test(e)&&t>=1&&12>=t?(2===e.length&&(e=70>e?"20"+e:"19"+e),4!==e.length?!1:(r=new Date(e,t),n=new Date,r.setMonth(r.getMonth()-1),r.setMonth(r.getMonth()+1,1),r>n)):!1):!1},$.payment.validateCardCVC=function(t,n){var r,a;return t=$.trim(t),/^\d+$/.test(t)?(r=e(n),null!=r?(a=t.length,w.call(r.cvcLength,a)>=0):t.length>=3&&t.length<=4):!1},$.payment.cardType=function(e){var n;return e?(null!=(n=t(e))?n.type:void 0)||null:null},$.payment.formatCardNumber=function(e){var n,r,a,o;return e=e.replace(/\D/g,""),(n=t(e))?(a=n.length[n.length.length-1],e=e.slice(0,a),n.format.global?null!=(o=e.match(n.format))?o.join(" "):void 0:(r=n.format.exec(e),null!=r?(r.shift(),r=$.grep(r,function(t){return t}),r.join(" ")):void 0)):e},$.payment.formatExpiry=function(t){var e,n,r,a;return(n=t.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/))?(e=n[1]||"",r=n[2]||"",a=n[3]||"",a.length>0?r=" / ":" /"===r?(e=e.substring(0,1),r=""):2===e.length||r.length>0?r=" / ":1===e.length&&"0"!==e&&"1"!==e&&(e="0"+e,r=" / "),e+r+a):""}}).call(this)},{}],2:[function(t,e,n){function r(t){var e=this,t=t||{};i.publishableKey=e.stripe_key=t.key,e.form=t.form,e.cc_number=a.observable(null),e.cc_expiry=a.observable(null),e.cc_cvv=a.observable(null),e.error_cc_number=a.observable(null),e.error_cc_expiry=a.observable(null),e.error_cc_cvv=a.observable(null),e.stripe_token=a.observable(null),e.card_digits=a.observable(null),e.is_editing_card=a.observable(!1),e.show_card_form=a.computed(function(){return e.is_editing_card()||!e.card_digits()||e.cc_number()||e.cc_expiry()||e.cc_cvv()}),e.initialize_form(),e.error=a.observable(null),e.process_form=function(){var t=o.payment.cardExpiryVal(e.cc_expiry()),n={number:e.cc_number(),exp_month:t.month,exp_year:t.year,cvc:e.cc_cvv()};return e.error(null),e.error_cc_number(null),e.error_cc_expiry(null),e.error_cc_cvv(null),o.payment.validateCardNumber(n.number)?o.payment.validateCardExpiry(n.exp_month,n.exp_year)?o.payment.validateCardCVC(n.cvc)?void i.createToken(n,function(t,n){200===t?e.submit_form(n.card.last4,n.id):e.error(n.error.message)}):(e.error_cc_cvv("Invalid security code"),!1):(e.error_cc_expiry("Invalid expiration date"),!1):(e.error_cc_number("Invalid card number"),!1)},e.process_full_form=function(){return e.show_card_form()?void e.process_form():!0}}var a=t("knockout"),o=(t("./../../../../../bower_components/jquery.payment/lib/jquery.payment.js"),t("jquery")),i=null;"undefined"!=typeof window&&"undefined"!=typeof window.Stripe&&(i=window.Stripe||{}),a.bindingHandlers.valueInit={init:function(t,e){var n=e();a.isWriteableObservable(n)&&n(t.value)}},r.prototype.submit_form=function(t,e){this.form.find("#id_card_digits").val(t),this.form.find("#id_stripe_token").val(e),this.form.submit()},r.prototype.initialize_form=function(){var t=o("input#id_cc_number"),e=o("input#id_cc_cvv"),n=o("input#id_cc_expiry");t.payment("formatCardNumber"),n.payment("formatCardExpiry"),e.payment("formatCardCVC"),t.trigger("keyup")},r.init=function(t,e){var n=new r(t),e=e||o("#payment-form")[0];return a.applyBindings(n,e),n},e.exports.PaymentView=r},{"./../../../../../bower_components/jquery.payment/lib/jquery.payment.js":1,jquery:"jquery",knockout:"knockout"}],"gold/gold":[function(t,e,n){function r(t){var e=this,t=t||{};e.constructor.call(e,t),e.last_4_digits=o.observable(null)}var a=(t("jquery"),t("readthedocs/payments/static-src/payments/js/base")),o=t("knockout");r.prototype=new a.PaymentView,r.init=function(t,e){var n=new r(t),e=e||$("#payment-form")[0];return o.applyBindings(n,e),n},r.prototype.submit_form=function(t,e){this.form.find("#id_last_4_digits").val(t),this.form.find("#id_stripe_token").val(e),this.form.submit()},e.exports.GoldView=r},{jquery:"jquery",knockout:"knockout","readthedocs/payments/static-src/payments/js/base":2}]},{},[]); \ No newline at end of file diff --git a/readthedocs/gold/templates/gold/cancel.html b/readthedocs/gold/templates/gold/cancel.html deleted file mode 100644 index 21c39c6b3..000000000 --- a/readthedocs/gold/templates/gold/cancel.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "profiles/base_profile_edit.html" %} - -{% block profile-admin-gold-edit %}active{% endblock %} - -{% block title %} -Cancel Gold -{% endblock %} - -{% block edit_content %} -
-
-
-
- - Are you sure you want to cancel? -
-
-
- {% csrf_token %} - - -
-
-
-{% endblock %} diff --git a/readthedocs/gold/templates/gold/cardform.html b/readthedocs/gold/templates/gold/cardform.html deleted file mode 100644 index 874c00231..000000000 --- a/readthedocs/gold/templates/gold/cardform.html +++ /dev/null @@ -1,20 +0,0 @@ -{% load i18n %} - -
- - -
-

- - - -

- - - -

- -
- - -
diff --git a/readthedocs/gold/templates/gold/edit.html b/readthedocs/gold/templates/gold/edit.html deleted file mode 100644 index 36356809d..000000000 --- a/readthedocs/gold/templates/gold/edit.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "profiles/base_profile_edit.html" %} -{% load static %} - -{% block profile-admin-gold-edit %}active{% endblock %} - -{% block title %} -Edit Gold -{% endblock %} - -{% block extra_scripts %} - - - - -{% endblock %} - -{% block edit_content %} -
-

Change Credit Card

-
- {% csrf_token %} - {% for field in form %} - {{ field }} - {% endfor %} - {% include "gold/cardform.html" %} -
-
-{% endblock %} diff --git a/readthedocs/gold/templates/gold/errors.html b/readthedocs/gold/templates/gold/errors.html deleted file mode 100644 index dbb9e3df8..000000000 --- a/readthedocs/gold/templates/gold/errors.html +++ /dev/null @@ -1,14 +0,0 @@ - {% if form.is_bound and not form.is_valid %} -
-
- {% for field in form.visible_fields %} - {% for error in field.errors %} -

{{ field.label }}: {{ error }}

- {% endfor %} - {% endfor %} - {% for error in form.non_field_errors %} -

{{ error }}

- {% endfor %} -
-
- {% endif %} diff --git a/readthedocs/gold/templates/gold/field.html b/readthedocs/gold/templates/gold/field.html deleted file mode 100644 index 1e7c2581e..000000000 --- a/readthedocs/gold/templates/gold/field.html +++ /dev/null @@ -1,6 +0,0 @@ -
- {{ field.label_tag }} -
- {{ field }} -
-
diff --git a/readthedocs/gold/templates/gold/once.html b/readthedocs/gold/templates/gold/once.html deleted file mode 100644 index c3bb49a5c..000000000 --- a/readthedocs/gold/templates/gold/once.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends "profiles/base_profile_edit.html" %} - -{% block profile-admin-gold-edit %}active{% endblock %} - -{% block title %} -One-time Payment -{% endblock %} - -{% block extra_scripts %} - - - - -{% endblock %} - - -{% block edit_content %} -
-
-
-
- -
-
-
- {% csrf_token %} - {% include "gold/errors.html" %} - {% for field in form.visible_fields %} - {% include "gold/field.html" %} - {% endfor %} - {% include "gold/cardform.html" %} -
-
-
-{% endblock %} diff --git a/readthedocs/gold/templates/gold/register.html b/readthedocs/gold/templates/gold/register.html deleted file mode 100644 index 1e0f4e221..000000000 --- a/readthedocs/gold/templates/gold/register.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "profiles/base_profile_edit.html" %} -{% load static %} -{% load i18n %} - -{% block profile-admin-gold-edit %}active{% endblock %} - -{% block title %} -Gold Status -{% endblock %} - -{% block extra_scripts %} - - - - -{% endblock %} - -{% block edit_content %} -
-

Gold Status

- - {% if gold_user.subscribed %} -

- Thanks for supporting Read the Docs! It really means a lot to us. -

-

- Level: {{ gold_user.get_level_display }} -

-

- Card: Ends with {{ gold_user.last_4_digits }} -

- -

Projects

-

- You can adopt {{ gold_user.num_supported_projects }} projects with your subscription. Select Projects -

- -

Changes

-

- Cancel or Change your subscription. -

- - {% else %} -

- Supporting Read the Docs lets us work more on features that people - love. Your money will go directly to maintenance and development of - the product. -

-

- You can make one-time donations on our sustainability page. -

- -

Become a Gold Member

-
- {% csrf_token %} - {% for field in form %} - {{ field }} - {% endfor %} - {% include "gold/cardform.html" %} - All information is submitted directly to Stripe. -
- {% endif %} -{% endblock %} diff --git a/readthedocs/gold/templates/gold/subscription_confirm_delete.html b/readthedocs/gold/templates/gold/subscription_confirm_delete.html new file mode 100644 index 000000000..b341566c4 --- /dev/null +++ b/readthedocs/gold/templates/gold/subscription_confirm_delete.html @@ -0,0 +1,21 @@ +{% extends "profiles/base_profile_edit.html" %} +{% load i18n %} + +{% block profile-admin-gold-edit %}active{% endblock %} + +{% block title %}Cancel Gold{% endblock %} + +{% block edit_content %} +

Cancel Gold Subscription

+ +

+ {% blocktrans %} + Are you sure you want to cancel your subscription? + {% endblocktrans %} +

+ +
+ {% csrf_token %} + +
+{% endblock %} diff --git a/readthedocs/gold/templates/gold/subscription_detail.html b/readthedocs/gold/templates/gold/subscription_detail.html new file mode 100644 index 000000000..a25d3391a --- /dev/null +++ b/readthedocs/gold/templates/gold/subscription_detail.html @@ -0,0 +1,68 @@ +{% extends "profiles/base_profile_edit.html" %} +{% load static %} +{% load i18n %} + +{% block profile-admin-gold-edit %}active{% endblock %} + +{% block title %}{% trans "Gold Subscription" %}{% endblock %} + +{% block extra_scripts %} + + + + +{% endblock %} + +{% block edit_content %} +
+

{% trans "Gold Subscription" %}

+ +

+ {% blocktrans %} + Thanks for supporting Read the Docs! It really means a lot to us. + {% endblocktrans %} +

+ +

+ + {{ golduser.get_level_display }} +

+ +

+ + ****-{{ golduser.last_4_digits }} +

+ +
+ +
+ +
+ +
+ +

{% trans "Projects" %}

+

+ {% blocktrans with projects=golduser.num_supported_projects %} + You can adopt {{ projects }} projects with your subscription. + {% endblocktrans %} +

+ +
+ +
+
+{% endblock %} diff --git a/readthedocs/gold/templates/gold/subscription_form.html b/readthedocs/gold/templates/gold/subscription_form.html new file mode 100644 index 000000000..e04ac036c --- /dev/null +++ b/readthedocs/gold/templates/gold/subscription_form.html @@ -0,0 +1,104 @@ +{% extends "profiles/base_profile_edit.html" %} +{% load static %} +{% load i18n %} + +{% block profile-admin-gold-edit %}active{% endblock %} + +{% block title %} +Gold Status +{% endblock %} + +{% block extra_links %} + +{% endblock %} + +{% block extra_scripts %} + + + + +{% endblock %} + +{% block edit_content %} +
+

Gold Status

+ +

+ {% blocktrans %} + Supporting Read the Docs lets us work more on features that people love. + Your money will go directly to maintenance and development of the + product. + {% endblocktrans %} +

+

+ {% blocktrans %} + You can make one-time donations on our sustainability page. + {% endblocktrans %} +

+ + {% trans "Become a Gold Member" as subscription_title %} + {% if golduser %} + {% trans "Update Your Subscription" as subscription_title %} + {% endif %} +

{{ subscription_title }}

+ +
+ {% csrf_token %} + + {{ form.non_field_errors }} + + {% for field in form.fields_with_cc_group %} + {% if field.is_cc_group %} + + + + + {% else %} + {% include 'core/ko_form_field.html' with field=field %} + {% endif %} + {% endfor %} + + {% trans "Sign Up" as form_submit_text %} + {% if golduser %} + {% trans "Update Subscription" as form_submit_text %} + {% endif %} + + + {% trans "All information is submitted directly to Stripe." %} +
+{% endblock %} diff --git a/readthedocs/gold/templates/gold/thanks.html b/readthedocs/gold/templates/gold/thanks.html deleted file mode 100644 index cd5b41614..000000000 --- a/readthedocs/gold/templates/gold/thanks.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "profiles/base_profile_edit.html" %} - -{% block profile-admin-gold-edit %}active{% endblock %} - -{% block title %} -Thanks! -{% endblock %} - -{% block edit_content %} - - -

-Thanks for contributing to Read the Docs! -

- -{% endblock %} diff --git a/readthedocs/gold/tests/__init__.py b/readthedocs/gold/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/readthedocs/gold/tests/test_forms.py b/readthedocs/gold/tests/test_forms.py new file mode 100644 index 000000000..fe768a264 --- /dev/null +++ b/readthedocs/gold/tests/test_forms.py @@ -0,0 +1,219 @@ +import mock +import django_dynamic_fixture as fixture +from django.test import TestCase +from django.contrib.auth.models import User + +from readthedocs.projects.models import Project + +from ..models import GoldUser +from ..forms import GoldSubscriptionForm + + +class GoldSubscriptionFormTests(TestCase): + + def setUp(self): + self.owner = fixture.get(User) + self.user = fixture.get(User) + self.project = fixture.get(Project, users=[self.user]) + + # Mocking + self.patches = {} + self.mocks = {} + self.patches['requestor'] = mock.patch('stripe.api_requestor.APIRequestor') + + for patch in self.patches: + self.mocks[patch] = self.patches[patch].start() + + self.mocks['request'] = self.mocks['requestor'].return_value + self.mock_request([({}, 'reskey')]) + + def mock_request(self, resp): + self.mocks['request'].request = mock.Mock(side_effect=resp) + + def test_add_subscription(self): + """Valid subscription form""" + subscription_list = { + 'object': 'list', + 'data': [], + 'has_more': False, + 'total_count': 1, + 'url': '/v1/customers/cus_12345/subscriptions', + } + customer_obj = { + 'id': 'cus_12345', + 'description': self.user.get_full_name(), + 'email': self.user.email, + 'subscriptions': subscription_list + } + subscription_obj = { + 'id': 'sub_12345', + 'object': 'subscription', + 'customer': 'cus_12345', + 'plan': { + 'id': 'v1-org-5', + 'object': 'plan', + 'amount': 1000, + 'currency': 'usd', + 'name': 'Test', + } + } + self.mock_request([ + (customer_obj, ''), + (subscription_list, ''), + (subscription_obj, ''), + ]) + + # Create user and subscription + subscription_form = GoldSubscriptionForm( + {'level': 'v1-org-5', + 'last_4_digits': '0000', + 'stripe_token': 'GARYBUSEY'}, + customer=self.user + ) + self.assertTrue(subscription_form.is_valid()) + subscription = subscription_form.save() + + self.assertEqual(subscription.level, 'v1-org-5') + self.assertEqual(subscription.stripe_id, 'cus_12345') + self.assertIsNotNone(self.user.gold) + self.assertEqual(self.user.gold.first().level, 'v1-org-5') + + self.mocks['request'].request.assert_has_calls([ + mock.call('post', + '/v1/customers', + {'description': mock.ANY, 'email': mock.ANY}, + mock.ANY), + mock.call('get', + '/v1/customers/cus_12345/subscriptions', + mock.ANY, + mock.ANY), + mock.call('post', + '/v1/customers/cus_12345/subscriptions', + {'source': mock.ANY, 'plan': 'v1-org-5'}, + mock.ANY), + ]) + + def test_add_subscription_update_user(self): + """Valid subscription form""" + subscription_list = { + 'object': 'list', + 'data': [], + 'has_more': False, + 'total_count': 1, + 'url': '/v1/customers/cus_12345/subscriptions', + } + customer_obj = { + 'id': 'cus_12345', + 'description': self.user.get_full_name(), + 'email': self.user.email, + 'subscriptions': subscription_list + } + subscription_obj = { + 'id': 'sub_12345', + 'object': 'subscription', + 'customer': 'cus_12345', + 'plan': { + 'id': 'v1-org-5', + 'object': 'plan', + 'amount': 1000, + 'currency': 'usd', + 'name': 'Test', + } + } + self.mock_request([ + (customer_obj, ''), + (subscription_list, ''), + (subscription_obj, ''), + ]) + + # Create user and update the current gold subscription + golduser = fixture.get(GoldUser, user=self.user, stripe_id='cus_12345') + subscription_form = GoldSubscriptionForm( + {'level': 'v1-org-5', + 'last_4_digits': '0000', + 'stripe_token': 'GARYBUSEY'}, + customer=self.user, + instance=golduser + ) + self.assertTrue(subscription_form.is_valid()) + subscription = subscription_form.save() + + self.assertEqual(subscription.level, 'v1-org-5') + self.assertEqual(subscription.stripe_id, 'cus_12345') + self.assertIsNotNone(self.user.gold) + self.assertEqual(self.user.gold.first().level, 'v1-org-5') + + self.mocks['request'].request.assert_has_calls([ + mock.call('get', + '/v1/customers/cus_12345', + {}, + mock.ANY), + mock.call('get', + '/v1/customers/cus_12345/subscriptions', + mock.ANY, + mock.ANY), + mock.call('post', + '/v1/customers/cus_12345/subscriptions', + {'source': mock.ANY, 'plan': 'v1-org-5'}, + mock.ANY), + ]) + + def test_update_subscription_plan(self): + """Update subcription plan""" + subscription_obj = { + 'id': 'sub_12345', + 'object': 'subscription', + 'customer': 'cus_12345', + 'plan': { + 'id': 'v1-org-5', + 'object': 'plan', + 'amount': 1000, + 'currency': 'usd', + 'name': 'Test', + } + } + subscription_list = { + 'object': 'list', + 'data': [subscription_obj], + 'has_more': False, + 'total_count': 1, + 'url': '/v1/customers/cus_12345/subscriptions', + } + customer_obj = { + 'id': 'cus_12345', + 'description': self.user.get_full_name(), + 'email': self.user.email, + 'subscriptions': subscription_list + } + self.mock_request([ + (customer_obj, ''), + (subscription_list, ''), + (subscription_obj, ''), + ]) + subscription_form = GoldSubscriptionForm( + {'level': 'v1-org-5', + 'last_4_digits': '0000', + 'stripe_token': 'GARYBUSEY'}, + customer=self.user + ) + self.assertTrue(subscription_form.is_valid()) + subscription = subscription_form.save() + + self.assertEqual(subscription.level, 'v1-org-5') + self.assertIsNotNone(self.user.gold) + self.assertEqual(self.user.gold.first().level, 'v1-org-5') + + self.mocks['request'].request.assert_has_calls([ + mock.call('post', + '/v1/customers', + {'description': mock.ANY, 'email': mock.ANY}, + mock.ANY), + mock.call('get', + '/v1/customers/cus_12345/subscriptions', + mock.ANY, + mock.ANY), + mock.call('post', + '/v1/customers/cus_12345/subscriptions/sub_12345', + {'source': mock.ANY, 'plan': 'v1-org-5'}, + mock.ANY), + ]) diff --git a/readthedocs/gold/tests/test_signals.py b/readthedocs/gold/tests/test_signals.py new file mode 100644 index 000000000..f6b10a0d4 --- /dev/null +++ b/readthedocs/gold/tests/test_signals.py @@ -0,0 +1,46 @@ +import mock +import django_dynamic_fixture as fixture +from django.test import TestCase +from django.contrib.auth.models import User +from django.db.models.signals import pre_delete + +from readthedocs.projects.models import Project + +from ..models import GoldUser +from ..signals import delete_customer + + +class GoldSignalTests(TestCase): + + def setUp(self): + self.user = fixture.get(User) + + # Mocking + self.patches = {} + self.mocks = {} + self.patches['requestor'] = mock.patch('stripe.api_requestor.APIRequestor') + + for patch in self.patches: + self.mocks[patch] = self.patches[patch].start() + + self.mocks['request'] = self.mocks['requestor'].return_value + + def mock_request(self, resp=None): + if resp is None: + resp = ({}, '') + self.mocks['request'].request = mock.Mock(side_effect=resp) + + def test_delete_subscription(self): + subscription = fixture.get(GoldUser, user=self.user, stripe_id='cus_123') + self.assertIsNotNone(subscription) + self.mock_request([ + ({'id': 'cus_123', 'object': 'customer'}, ''), + ({'deleted': True, 'customer': 'cus_123'}, ''), + ]) + + subscription.delete() + + self.mocks['request'].request.assert_has_calls([ + mock.call('get', '/v1/customers/cus_123', {}, mock.ANY), + mock.call('delete', '/v1/customers/cus_123', {}, mock.ANY), + ]) diff --git a/readthedocs/gold/urls.py b/readthedocs/gold/urls.py index ea87bd3b6..c78482fc7 100644 --- a/readthedocs/gold/urls.py +++ b/readthedocs/gold/urls.py @@ -4,15 +4,16 @@ from readthedocs.gold import views from readthedocs.projects.constants import PROJECT_SLUG_REGEX -urlpatterns = patterns('', - url(r'^register/$', views.register, name='gold_register'), - url(r'^edit/$', views.edit, name='gold_edit'), - url(r'^cancel/$', views.cancel, name='gold_cancel'), - url(r'^thanks/$', views.thanks, name='gold_thanks'), - url(r'^projects/$', views.projects, name='gold_projects'), - url(r'^projects/remove/(?P{project_slug})/$'.format( - project_slug=PROJECT_SLUG_REGEX - ), - views.projects_remove, - name='gold_projects_remove'), - ) +urlpatterns = patterns( + '', + url(r'^$', views.DetailGoldSubscription.as_view(), name='gold_detail'), + url(r'^subscription/$', views.UpdateGoldSubscription.as_view(), + name='gold_subscription'), + url(r'^cancel/$', views.DeleteGoldSubscription.as_view(), name='gold_cancel'), + url(r'^projects/$', views.projects, name='gold_projects'), + url(r'^projects/remove/(?P{project_slug})/$'.format( + project_slug=PROJECT_SLUG_REGEX + ), + views.projects_remove, + name='gold_projects_remove'), +) diff --git a/readthedocs/gold/views.py b/readthedocs/gold/views.py index c39b16c27..09cd21b90 100644 --- a/readthedocs/gold/views.py +++ b/readthedocs/gold/views.py @@ -1,146 +1,90 @@ import datetime -from django.core.urlresolvers import reverse +import stripe +from django.core.urlresolvers import reverse, reverse_lazy from django.conf import settings +from django.contrib.messages.views import SuccessMessageMixin +from django.contrib import messages from django.db import IntegrityError from django.http import HttpResponseRedirect, Http404 from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext_lazy as _ +from vanilla import CreateView, DeleteView, UpdateView, DetailView -import stripe - -from .forms import CardForm, GoldProjectForm -from .models import GoldUser +from readthedocs.core.mixins import LoginRequiredMixin from readthedocs.projects.models import Project +from readthedocs.payments.mixins import StripeMixin + +from .forms import GoldSubscriptionForm, GoldProjectForm +from .models import GoldUser stripe.api_key = settings.STRIPE_SECRET -def soon(): - soon = datetime.date.today() + datetime.timedelta(days=30) - return {'month': soon.month, 'year': soon.year} +class GoldSubscriptionView(SuccessMessageMixin, StripeMixin, LoginRequiredMixin): + + model = GoldUser + form_class = GoldSubscriptionForm + + def get_object(self): + try: + return self.get_queryset().get(user=self.request.user) + except self.model.DoesNotExist: + return None + + def get_context_data(self, **kwargs): + context = (super(GoldSubscriptionView, self) + .get_context_data(**kwargs)) + context['stripe_publishable'] = settings.STRIPE_PUBLISHABLE + return context + + def get_form(self, data=None, files=None, **kwargs): + """Pass in copy of POST data to avoid read only QueryDicts""" + kwargs['customer'] = self.request.user + return super(GoldSubscriptionView, self).get_form(data, files, **kwargs) + + def get_success_url(self, **kwargs): + return reverse_lazy('gold_detail') + + def get_template_names(self): + return ('gold/subscription{0}.html' + .format(self.template_name_suffix)) -@login_required -def register(request): - user = request.user - try: - gold_user = GoldUser.objects.get(user=request.user) - except GoldUser.DoesNotExist: - gold_user = None - if request.method == 'POST': - form = CardForm(request.POST) - if form.is_valid(): +# Subscription Views +class DetailGoldSubscription(GoldSubscriptionView, DetailView): - customer = stripe.Customer.create( - description=user.username, - email=user.email, - card=form.cleaned_data['stripe_token'], - plan=form.cleaned_data['level'], - ) - - try: - user = GoldUser.objects.get(user=user) - except GoldUser.DoesNotExist: - user = GoldUser( - user=request.user, - ) - - user.level = form.cleaned_data['level'] - user.last_4_digits = form.cleaned_data['last_4_digits'] - user.stripe_id = customer.id - user.subscribed = True - - try: - user.save() - except IntegrityError: - form.add_error(None, user.user.username + ' is already a member') - else: - return HttpResponseRedirect(reverse('gold_thanks')) - - else: - form = CardForm() - - return render_to_response( - 'gold/register.html', - { - 'form': form, - 'gold_user': gold_user, - 'publishable': settings.STRIPE_PUBLISHABLE, - 'soon': soon(), - 'user': user, - }, - context_instance=RequestContext(request) - ) + def get(self, request, *args, **kwargs): + resp = super(DetailGoldSubscription, self).get(request, *args, **kwargs) + if self.object is None: + return HttpResponseRedirect(reverse('gold_subscription')) + return resp -@login_required -def edit(request): - user = get_object_or_404(GoldUser, user=request.user) - if request.method == 'POST': - form = CardForm(request.POST) - if form.is_valid(): - - customer = stripe.Customer.retrieve(user.stripe_id) - customer.card = form.cleaned_data['stripe_token'] - customer.plan = form.cleaned_data['level'] - customer.save() - - user.last_4_digits = form.cleaned_data['last_4_digits'] - user.stripe_id = customer.id - user.level = form.cleaned_data['level'] - user.subscribed = True - user.save() - - return HttpResponseRedirect(reverse('gold_thanks')) - - else: - form = CardForm(initial={'level': user.level}) - - return render_to_response( - 'gold/edit.html', - { - 'form': form, - 'publishable': settings.STRIPE_PUBLISHABLE, - 'soon': soon(), - }, - context_instance=RequestContext(request) - ) +class UpdateGoldSubscription(GoldSubscriptionView, UpdateView): + success_message = _('Your subscription has been updated') -@login_required -def cancel(request): - user = get_object_or_404(GoldUser, user=request.user) - if request.method == 'POST': - customer = stripe.Customer.retrieve(user.stripe_id) - customer.delete() - user.subscribed = False - user.save() - return HttpResponseRedirect(reverse('gold_register')) - return render_to_response( - 'gold/cancel.html', - { - 'publishable': settings.STRIPE_PUBLISHABLE, - 'soon': soon(), - 'months': range(1, 13), - 'years': range(2011, 2036) - }, - context_instance=RequestContext(request) - ) +class DeleteGoldSubscription(GoldSubscriptionView, DeleteView): + """Delete Gold subscription view -def thanks(request): - return render_to_response( - 'gold/thanks.html', - { - 'publishable': settings.STRIPE_PUBLISHABLE, - 'soon': soon(), - 'months': range(1, 13), - 'years': range(2011, 2036) - }, - context_instance=RequestContext(request) - ) + On object deletion, the corresponding Stripe customer is deleted as well. + Deletion is triggered on subscription deletion, to ensure the subscription + is synced with Stripe. + """ + + success_message = _('Your subscription has been cancelled') + + def post(self, request, *args, **kwargs): + """Add success message to delete post""" + resp = super(SuccessMessageMixin, self).post(request, *args, **kwargs) + success_message = self.get_success_message({}) + if success_message: + messages.success(self.request, success_message) + return resp @login_required diff --git a/readthedocs/payments/__init__.py b/readthedocs/payments/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/readthedocs/payments/forms.py b/readthedocs/payments/forms.py new file mode 100644 index 000000000..2d73a31c5 --- /dev/null +++ b/readthedocs/payments/forms.py @@ -0,0 +1,190 @@ +"""Payment forms""" + +import logging + +import stripe +from stripe.resource import Customer, Charge +from stripe.error import InvalidRequestError +from django import forms +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +log = logging.getLogger(__name__) +stripe.api_key = getattr(settings, 'STRIPE_SECRET') + + +class StripeResourceMixin(object): + + """Stripe actions for resources, available as a Form mixin class""" + + def ensure_stripe_resource(self, resource, attrs): + try: + return resource.retrieve(attrs['id']) + except (KeyError, InvalidRequestError): + try: + del attrs['id'] + except KeyError: + pass + try: + return resource.create(**attrs) + except InvalidRequestError: + return None + + def get_customer_kwargs(self): + raise NotImplementedError + + def get_customer(self): + return self.ensure_stripe_resource(resource=Customer, + attrs=self.get_customer_kwargs()) + + def get_subscription_kwargs(self): + raise NotImplementedError + + def get_subscription(self): + customer = self.get_customer() + return self.ensure_stripe_resource(resource=customer.subscriptions, + attrs=self.get_subscription_kwargs()) + + def get_charge_kwargs(self): + raise NotImplementedError + + def get_charge(self): + customer = self.get_customer() + return self.ensure_stripe_resource(resource=Charge, + attrs=self.get_charge_kwargs()) + + +class StripeSubscriptionModelForm(forms.ModelForm): + + """Payment form base for Stripe interaction + + Use this as a base class for payment forms. It includes the necessary fields + for card input and manipulates the Knockout field data bindings correctly. + + :cvar stripe_token: Stripe token passed from Stripe.js + :cvar cc_number: Credit card number field, used only by Stripe.js + :cvar cc_expiry: Credit card expiry field, used only by Stripe.js + :cvar cc_cvv: Credit card security code field, used only by Stripe.js + """ + + # Stripe token input from Stripe.js + stripe_token = forms.CharField( + required=False, + widget=forms.HiddenInput(attrs={ + 'data-bind': 'valueInit: stripe_token', + }) + ) + + # Fields used for fetching token with javascript, listed as form fields so + # that data can survive validation errors + cc_number = forms.CharField( + label=_('Card number'), + widget=forms.TextInput(attrs={ + 'data-bind': ('valueInit: cc_number, ' + 'value: cc_number, ' + '''css: {'field-error': error_cc_number() != null}''') + }), + max_length=25, + required=False) + cc_expiry = forms.CharField( + label=_('Card expiration'), + widget=forms.TextInput(attrs={ + 'data-bind': ('valueInit: cc_expiry, ' + 'value: cc_expiry, ' + '''css: {'field-error': error_cc_expiry() != null}''') + }), + max_length=10, + required=False) + cc_cvv = forms.CharField( + label=_('Card CVV'), + widget=forms.TextInput(attrs={ + 'data-bind': ('valueInit: cc_cvv, ' + 'value: cc_cvv, ' + '''css: {'field-error': error_cc_cvv() != null}'''), + 'autocomplete': 'off', + }), + max_length=8, + required=False) + + def __init__(self, *args, **kwargs): + self.customer = kwargs.pop('customer', None) + super(StripeSubscriptionModelForm, self).__init__(*args, **kwargs) + + def validate_stripe(self): + """Run validation against Stripe + + This is what will create several objects using the Stripe API. We need + to actually create the objects, as that is what will provide us with + validation errors to throw back at the form. + + Form fields can be accessed here via ``self.cleaned_data`` as this + method is triggered from the :py:meth:`clean` method. Cleaned form data + should already exist on the form at this point. + """ + raise NotImplemented('Stripe validation is not implemented') + + def clean_stripe_token(self): + data = self.cleaned_data['stripe_token'] + if not data: + data = None + return data + + def clean(self): + """Clean form to add Stripe objects via API during validation phase + + This will handle ensuring a customer and subscription exist and will + raise any issues as validation errors. This is required because part + of Stripe's validation happens on the API call to establish a + subscription. + """ + cleaned_data = super(StripeSubscriptionModelForm, self).clean() + + # Form isn't valid, no need to try to associate a card now + if not self.is_valid(): + self.clear_card_data() + return + + try: + self.validate_stripe() + except stripe.error.CardError as e: + self.clear_card_data() + field_lookup = { + 'cvc': 'cc_cvv', + 'number': 'cc_number', + 'expiry': 'cc_expiry', + 'exp_month': 'cc_expiry', + 'exp_year': 'cc_expiry', + } + error_field = field_lookup.get(e.param, None) + self.add_error( + error_field, + forms.ValidationError(e.message), + ) + except stripe.error.StripeError as e: + log.error('There was a problem communicating with Stripe: %s', + str(e), exc_info=True) + raise forms.ValidationError( + _('There was a problem communicating with Stripe')) + + def clear_card_data(self): + """Clear card data on validation errors + + This requires the form was created by passing in a mutable QueryDict + instance, see :py:cls:`readthedocs.payments.mixin.StripeMixin` + """ + try: + self.data['stripe_token'] = None + except AttributeError: + raise AttributeError('Form was passed immutable QueryDict POST data') + + def fields_with_cc_group(self): + group = { + 'is_cc_group': True, + 'fields': [] + } + for field in self: + if field.name in ['cc_number', 'cc_expiry', 'cc_cvv']: + group['fields'].append(field) + else: + yield field + yield group diff --git a/readthedocs/payments/mixins.py b/readthedocs/payments/mixins.py new file mode 100644 index 000000000..102a3b361 --- /dev/null +++ b/readthedocs/payments/mixins.py @@ -0,0 +1,23 @@ +from django.conf import settings + + +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['stripe_publishable'] = settings.STRIPE_PUBLISHABLE + return context + + def get_form(self, data=None, files=None, **kwargs): + """Pass in copy of POST data to avoid read only QueryDicts on form + + This is used to be able to reset some important credit card fields if + card validation fails. In this case, the Stripe token was valid, but the + card was rejected during the charge or subscription instantiation. + """ + if self.request.method == 'POST': + data = self.request.POST.copy() + cls = self.get_form_class() + return cls(data=data, files=files, **kwargs) diff --git a/readthedocs/core/static-src/core/js/payment.js b/readthedocs/payments/static-src/payments/js/base.js similarity index 55% rename from readthedocs/core/static-src/core/js/payment.js rename to readthedocs/payments/static-src/payments/js/base.js index b102bff30..b933ac8ad 100644 --- a/readthedocs/core/static-src/core/js/payment.js +++ b/readthedocs/payments/static-src/payments/js/base.js @@ -5,12 +5,21 @@ var ko = require('knockout'), $ = require('jquery'), stripe = null; - // TODO stripe doesn't support loading locally very well, do they? if (typeof(window) != 'undefined' && typeof(window.Stripe) != 'undefined') { stripe = window.Stripe || {}; } +/* Knockout binding to set initial observable values from HTML */ +ko.bindingHandlers.valueInit = { + init: function(element, accessor) { + var value = accessor(); + if (ko.isWriteableObservable(value)) { + value(element.value); + } + } +}; + function PaymentView (config) { var self = this, config = config || {}; @@ -23,9 +32,22 @@ function PaymentView (config) { self.cc_number = ko.observable(null); self.cc_expiry = ko.observable(null); self.cc_cvv = ko.observable(null); - self.cc_error_number = ko.observable(null); - self.cc_error_expiry = ko.observable(null); - self.cc_error_cvv = ko.observable(null); + self.error_cc_number = ko.observable(null); + self.error_cc_expiry = ko.observable(null); + self.error_cc_cvv = ko.observable(null); + + self.stripe_token = ko.observable(null); + self.card_digits = ko.observable(null); + + // Form editing + self.is_editing_card = ko.observable(false); + self.show_card_form = ko.computed(function () { + return (self.is_editing_card() || + !self.card_digits() || + self.cc_number() || + self.cc_expiry() || + self.cc_cvv()); + }); // Credit card validation self.initialize_form(); @@ -45,52 +67,64 @@ function PaymentView (config) { }; self.error(null); - self.cc_error_number(null); - self.cc_error_expiry(null); - self.cc_error_cvv(null); + self.error_cc_number(null); + self.error_cc_expiry(null); + self.error_cc_cvv(null); if (!$.payment.validateCardNumber(card.number)) { - self.cc_error_number('Invalid card number'); - console.log(card); + self.error_cc_number('Invalid card number'); return false; } if (!$.payment.validateCardExpiry(card.exp_month, card.exp_year)) { - self.cc_error_expiry('Invalid expiration date'); + self.error_cc_expiry('Invalid expiration date'); return false; } if (!$.payment.validateCardCVC(card.cvc)) { - self.cc_error_cvv('Invalid security code'); + self.error_cc_cvv('Invalid security code'); return false; } stripe.createToken(card, function(status, response) { if (status === 200) { - // Update form fields that are actually sent to - var cc_last_digits = self.form.find('#id_last_4_digits'), - token = self.form.find('#id_stripe_id,#id_stripe_token'); - cc_last_digits.val(response.card.last4); - token.val(response.id); - self.form.submit(); + self.submit_form(response.card.last4, response.id); } else { self.error(response.error.message); } }); }; + + self.process_full_form = function () { + if (self.show_card_form()) { + self.process_form() + } + else { + return true; + } + }; + } +PaymentView.prototype.submit_form = function (card_digits, token) { + this.form.find('#id_card_digits').val(card_digits); + this.form.find('#id_stripe_token').val(token); + this.form.submit(); +}; + PaymentView.prototype.initialize_form = function () { - var cc_number = $('input#cc-number'), - cc_cvv = $('input#cc-cvv'), - cc_expiry = $('input#cc-expiry'); + var cc_number = $('input#id_cc_number'), + cc_cvv = $('input#id_cc_cvv'), + cc_expiry = $('input#id_cc_expiry'); cc_number.payment('formatCardNumber'); cc_expiry.payment('formatCardExpiry'); cc_cvv.payment('formatCardCVC'); + + cc_number.trigger('keyup'); }; PaymentView.init = function (config, obj) { - var view = new GoldView(config), + var view = new PaymentView(config), obj = obj || $('#payment-form')[0]; ko.applyBindings(view, obj); return view; diff --git a/readthedocs/payments/static/payments/css/form.css b/readthedocs/payments/static/payments/css/form.css new file mode 100644 index 000000000..44c7f0828 --- /dev/null +++ b/readthedocs/payments/static/payments/css/form.css @@ -0,0 +1,26 @@ +/* Payment form CSS */ + +form.payment input#id_cc_number { + height: 23px; + margin: 3px 0px 10px; + background: url('/static/donate/img/creditcard.png'); + background-repeat: no-repeat; + background-position: -0px -128px; + padding-left: 40px; +} + +form.payment input#id_cc_number.visa { + background-position: -40px -96px; +} +form.payment input#id_cc_number.mastercard { + background-position: -80px -64px; +} +form.payment input#id_cc_number.amex { + background-position: -120px -32px; +} +form.payment input#id_cc_number.discover { + background-position: -160px -0px; +} + +form.payment input#id_cc_expiry { width: 150px; } +form.payment input#id_cc_cvv { width: 100px; } diff --git a/readthedocs/payments/utils.py b/readthedocs/payments/utils.py new file mode 100644 index 000000000..a18c7e052 --- /dev/null +++ b/readthedocs/payments/utils.py @@ -0,0 +1,30 @@ +"""Payment utility functions + +These are mostly one-off functions. Define the bulk of Stripe operations on +:py:cls:`readthedocs.payments.forms.StripeResourceMixin`. +""" + +import stripe +from django.conf import settings + +stripe.api_key = getattr(settings, 'STRIPE_SECRET', None) + + +def delete_customer(customer_id): + """Delete customer from Stripe, cancelling subscriptions""" + try: + customer = stripe.Customer.retrieve(customer_id) + customer.delete() + except stripe.error.InvalidRequestError: + pass + + +def cancel_subscription(customer_id, subscription_id): + """Cancel Stripe subscription, if it exists""" + try: + customer = stripe.Customer.retrieve(customer_id) + if hasattr(customer, 'subscriptions'): + subscription = customer.subscriptions.retrieve(subscription_id) + subscription.delete() + except stripe.error.StripeError as e: + pass diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 207de770e..5ebfdb2a7 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -12,7 +12,6 @@ from django.db.models import Q from django.shortcuts import get_object_or_404, render_to_response, render from django.template import RequestContext from django.views.generic import View, TemplateView, ListView -from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from formtools.wizard.views import SessionWizardView from allauth.socialaccount.models import SocialAccount @@ -37,19 +36,12 @@ from readthedocs.projects.views.base import ProjectAdminMixin from readthedocs.projects import constants, tasks from readthedocs.projects.tasks import remove_path_from_web - +from readthedocs.core.mixins import LoginRequiredMixin from readthedocs.projects.signals import project_import log = logging.getLogger(__name__) -class LoginRequiredMixin(object): - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super(LoginRequiredMixin, self).dispatch(*args, **kwargs) - - class PrivateViewMixin(LoginRequiredMixin): pass diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 5bcc2251f..e740d813c 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -214,6 +214,7 @@ INSTALLED_APPS = [ 'readthedocs.privacy', 'readthedocs.gold', 'readthedocs.donate', + 'readthedocs.payments', # allauth 'allauth', diff --git a/readthedocs/templates/profiles/base_profile_edit.html b/readthedocs/templates/profiles/base_profile_edit.html index 634cedfc5..8cb11a086 100644 --- a/readthedocs/templates/profiles/base_profile_edit.html +++ b/readthedocs/templates/profiles/base_profile_edit.html @@ -48,7 +48,7 @@
  • {% trans "Social Accounts" %}
  • {% trans "Change Password" %}
  • {% trans "Change Email" %}
  • -
  • {% trans "Gold" %}
  • +
  • {% trans "Gold" %}
  • {% block edit_content_header %}{% endblock %}