From 70391884827c80e4aa39348a562a1b01d95c73ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Mon, 16 Nov 2015 21:28:33 +0200 Subject: [PATCH 1/5] Payments with Braintree, WIP --- hc/front/urls.py | 1 - hc/front/views.py | 4 - hc/payments/__init__.py | 0 hc/payments/admin.py | 8 ++ hc/payments/context_processors.py | 5 ++ hc/payments/migrations/0001_initial.py | 25 +++++++ hc/payments/migrations/__init__.py | 0 hc/payments/models.py | 9 +++ hc/payments/tests.py | 3 + hc/payments/urls.py | 18 +++++ hc/payments/views.py | 81 +++++++++++++++++++++ hc/settings.py | 5 +- hc/urls.py | 1 + templates/base.html | 4 + templates/payments/create_subscription.html | 23 ++++++ templates/{front => payments}/pricing.html | 1 - templates/payments/status.html | 39 ++++++++++ 17 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 hc/payments/__init__.py create mode 100644 hc/payments/admin.py create mode 100644 hc/payments/context_processors.py create mode 100644 hc/payments/migrations/0001_initial.py create mode 100644 hc/payments/migrations/__init__.py create mode 100644 hc/payments/models.py create mode 100644 hc/payments/tests.py create mode 100644 hc/payments/urls.py create mode 100644 hc/payments/views.py create mode 100644 templates/payments/create_subscription.html rename templates/{front => payments}/pricing.html (98%) create mode 100644 templates/payments/status.html diff --git a/hc/front/urls.py b/hc/front/urls.py index 2c5289b3..b9a957b9 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -11,7 +11,6 @@ urlpatterns = [ url(r'^checks/([\w-]+)/email/$', views.email_preview), url(r'^checks/([\w-]+)/remove/$', views.remove_check, name="hc-remove-check"), url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"), - url(r'^pricing/$', views.pricing, name="hc-pricing"), url(r'^docs/$', views.docs, name="hc-docs"), url(r'^about/$', views.about, name="hc-about"), url(r'^integrations/$', views.channels, name="hc-channels"), diff --git a/hc/front/views.py b/hc/front/views.py index afe89586..ecd35135 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -47,10 +47,6 @@ def index(request): return render(request, "front/welcome.html", ctx) -def pricing(request): - return render(request, "front/pricing.html", {"page": "pricing"}) - - def docs(request): if "welcome_code" in request.session: code = request.session["welcome_code"] diff --git a/hc/payments/__init__.py b/hc/payments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hc/payments/admin.py b/hc/payments/admin.py new file mode 100644 index 00000000..67599c92 --- /dev/null +++ b/hc/payments/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from .models import Subscription + + +@admin.register(Subscription) +class SubsAdmin(admin.ModelAdmin): + + list_display = ("id", "user") diff --git a/hc/payments/context_processors.py b/hc/payments/context_processors.py new file mode 100644 index 00000000..a6a428eb --- /dev/null +++ b/hc/payments/context_processors.py @@ -0,0 +1,5 @@ +from django.conf import settings + + +def payments(request): + return {'USE_PAYMENTS': settings.USE_PAYMENTS} diff --git a/hc/payments/migrations/0001_initial.py b/hc/payments/migrations/0001_initial.py new file mode 100644 index 00000000..0f87d3de --- /dev/null +++ b/hc/payments/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Subscription', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('customer_id', models.CharField(blank=True, max_length=36)), + ('payment_method_token', models.CharField(blank=True, max_length=35)), + ('subscription_id', models.CharField(blank=True, max_length=10)), + ('user', models.OneToOneField(blank=True, null=True, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/hc/payments/migrations/__init__.py b/hc/payments/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hc/payments/models.py b/hc/payments/models.py new file mode 100644 index 00000000..30045204 --- /dev/null +++ b/hc/payments/models.py @@ -0,0 +1,9 @@ +from django.contrib.auth.models import User +from django.db import models + + +class Subscription(models.Model): + user = models.OneToOneField(User, blank=True, null=True) + customer_id = models.CharField(max_length=36, blank=True) + payment_method_token = models.CharField(max_length=35, blank=True) + subscription_id = models.CharField(max_length=10, blank=True) diff --git a/hc/payments/tests.py b/hc/payments/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/hc/payments/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/hc/payments/urls.py b/hc/payments/urls.py new file mode 100644 index 00000000..500291b3 --- /dev/null +++ b/hc/payments/urls.py @@ -0,0 +1,18 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^pricing/$', + views.pricing, + name="hc-pricing"), + + url(r'^create_subscription/$', + views.create, + name="hc-create-subscription"), + + url(r'^subscription_status/$', + views.status, + name="hc-subscription-status"), + +] diff --git a/hc/payments/views.py b/hc/payments/views.py new file mode 100644 index 00000000..e56f36f2 --- /dev/null +++ b/hc/payments/views.py @@ -0,0 +1,81 @@ +import braintree +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect, render + +from .models import Subscription + + +def setup_braintree(): + kw = { + "merchant_id": settings.BRAINTREE_MERCHANT_ID, + "public_key": settings.BRAINTREE_PUBLIC_KEY, + "private_key": settings.BRAINTREE_PRIVATE_KEY + } + + braintree.Configuration.configure(settings.BRAINTREE_ENV, **kw) + + +def pricing(request): + ctx = { + "page": "pricing", + } + return render(request, "payments/pricing.html", ctx) + + +@login_required +def create(request): + setup_braintree() + + try: + sub = Subscription.objects.get(user=request.user) + except Subscription.DoesNotExist: + sub = Subscription(user=request.user) + sub.save() + + if request.method == "POST": + if not sub.customer_id: + result = braintree.Customer.create({}) + assert result.is_success + sub.customer_id = result.customer.id + sub.save() + + result = braintree.PaymentMethod.create({ + "customer_id": sub.customer_id, + "payment_method_nonce": request.POST["payment_method_nonce"] + }) + assert result.is_success + sub.payment_method_token = result.payment_method.token + sub.save() + + result = braintree.Subscription.create({ + "payment_method_token": sub.payment_method_token, + "plan_id": "pww", + "price": 5 + }) + + sub.subscription_id = result.subscription.id + sub.save() + + return redirect("hc-subscription-status") + + ctx = { + "page": "pricing", + "client_token": braintree.ClientToken.generate() + } + return render(request, "payments/create_subscription.html", ctx) + + +@login_required +def status(request): + setup_braintree() + + sub = Subscription.objects.get(user=request.user) + subscription = braintree.Subscription.find(sub.subscription_id) + + ctx = { + "page": "pricing", + "subscription": subscription + } + + return render(request, "payments/status.html", ctx) diff --git a/hc/settings.py b/hc/settings.py index 30d359b6..83abc219 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -20,6 +20,7 @@ SECRET_KEY = "---" DEBUG = True ALLOWED_HOSTS = [] DEFAULT_FROM_EMAIL = 'healthchecks@example.org' +USE_PAYMENTS = False INSTALLED_APPS = ( @@ -35,7 +36,8 @@ INSTALLED_APPS = ( 'hc.accounts', 'hc.api', - 'hc.front' + 'hc.front', + 'hc.payments' ) MIDDLEWARE_CLASSES = ( @@ -62,6 +64,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'hc.payments.context_processors.payments' ], }, }, diff --git a/hc/urls.py b/hc/urls.py index 5351cbea..a890f794 100644 --- a/hc/urls.py +++ b/hc/urls.py @@ -6,4 +6,5 @@ urlpatterns = [ url(r'^accounts/', include('hc.accounts.urls')), url(r'^', include('hc.api.urls')), url(r'^', include('hc.front.urls')), + url(r'^', include('hc.payments.urls')), ] diff --git a/templates/base.html b/templates/base.html index dec36838..3ce911f4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -20,7 +20,9 @@ + {% if USE_PAYMENTS %} + {% endif %} @@ -80,9 +82,11 @@ {% endif %} + {% if USE_PAYMENTS %}
  • Pricing
  • + {% endif %}
  • Docs diff --git a/templates/payments/create_subscription.html b/templates/payments/create_subscription.html new file mode 100644 index 00000000..0e6065fc --- /dev/null +++ b/templates/payments/create_subscription.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} + +{% block content %} + +
    + {% csrf_token %} +
    + +
    + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/front/pricing.html b/templates/payments/pricing.html similarity index 98% rename from templates/front/pricing.html rename to templates/payments/pricing.html index 03062b26..852d3d4c 100644 --- a/templates/front/pricing.html +++ b/templates/payments/pricing.html @@ -37,5 +37,4 @@ - {% endblock %} \ No newline at end of file diff --git a/templates/payments/status.html b/templates/payments/status.html new file mode 100644 index 00000000..20171e2f --- /dev/null +++ b/templates/payments/status.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} + +{% block content %} + +

    Subscription Status

    + +
    +
    Status
    +
    {{ subscription.status }}
    + +
    Next Billing Date:
    +
    {{ subscription.next_billing_date }}
    + + +
    Amount
    +
    {{ subscription.price }} / Month
    + + +
    + +

    Transactions

    + + + + + + {% for tx in subscription.transactions %} + + + + + {% endfor %} +
    DateAmount
    ???{{ tx.amount }}
    + + +{% endblock %} \ No newline at end of file From 7a207a8c49642afa80ded5e5dc68c28df8a26268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Mon, 16 Nov 2015 23:16:38 +0200 Subject: [PATCH 2/5] Pricing table, WIP --- static/css/bootstrap.css | 184 +++++++++++++++++++++++++++++++- static/css/pricing.css | 94 +++++++++++++--- static/js/pricing.js | 32 ++++++ stuff/bootstrap/bootstrap.less | 2 +- templates/payments/pricing.html | 59 +++++++++- 5 files changed, 349 insertions(+), 22 deletions(-) create mode 100644 static/js/pricing.js diff --git a/static/css/bootstrap.css b/static/css/bootstrap.css index 668e7384..add848f8 100644 --- a/static/css/bootstrap.css +++ b/static/css/bootstrap.css @@ -3466,19 +3466,22 @@ fieldset[disabled] .btn-link:focus { color: #777777; text-decoration: none; } -.btn-lg { +.btn-lg, +.btn-group-lg > .btn { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } -.btn-sm { +.btn-sm, +.btn-group-sm > .btn { padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } -.btn-xs { +.btn-xs, +.btn-group-xs > .btn { padding: 1px 5px; font-size: 12px; line-height: 1.5; @@ -3670,6 +3673,175 @@ tbody.collapse.in { right: auto; } } +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn, +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-bottom-left-radius: 4px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + float: none; + display: table-cell; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} .input-group { position: relative; display: table; @@ -5743,6 +5915,10 @@ button.close { .row:after, .form-horizontal .form-group:before, .form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, .nav:before, .nav:after, .navbar:before, @@ -5764,6 +5940,8 @@ button.close { .container-fluid:after, .row:after, .form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, .nav:after, .navbar:after, .navbar-header:after, diff --git a/static/css/pricing.css b/static/css/pricing.css index 6f126402..5bcd2eab 100644 --- a/static/css/pricing.css +++ b/static/css/pricing.css @@ -1,11 +1,3 @@ -.panel-pricing { - -moz-transition: all .3s ease; - -o-transition: all .3s ease; - -webkit-transition: all .3s ease; -} -.panel-pricing:hover { - box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.2); -} .panel-pricing .panel-heading { padding: 20px 10px; } @@ -26,9 +18,85 @@ border-top-left-radius: 0px; } .panel-pricing .panel-body { - background-color: #f0f0f0; - font-size: 40px; - color: #777777; - padding: 20px; - margin: 0px; + font-size: 40px; + color: #777777; + padding: 20px; + margin: 0px; + background-color: #f0f0f0; +} + +.panel-slider { + padding: 20px 20px 40px 20px; + height: 78px; + background-color: #f0f0f0; +} + +.animated { + animation-duration: 1s; + animation-fill-mode: both; +} + + +@keyframes tada { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 10%, 20% { + -webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg); + } + + 30%, 50%, 70%, 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, 60%, 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.tada { + -webkit-animation-name: tada; + animation-name: tada; +} + +@keyframes tadaIn { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 10%, 20% { + -webkit-transform: scale3d(1.05, 1.05, 1.05) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.05, 1.05, 1.05) rotate3d(0, 0, 1, -3deg); + } + + 30%, 50%, 70%, 90% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, 3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, 3deg); + } + + 40%, 60%, 80% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.tadaIn { + -webkit-animation-name: tadaIn; + animation-name: tadaIn; } diff --git a/static/js/pricing.js b/static/js/pricing.js new file mode 100644 index 00000000..22ceb3a1 --- /dev/null +++ b/static/js/pricing.js @@ -0,0 +1,32 @@ +$(function () { + var prices = [2, 5, 10, 15, 20, 25, 50, 75, 100, 125, 150, 175, 200]; + var priceIdx = 2; + + $("#pay-plus").click(function() { + if (priceIdx >= 12) + return; + + priceIdx += 1; + $("#pricing-value").text(prices[priceIdx]); + + $("#piggy").removeClass().addClass("tada animated").one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){ + $(this).removeClass(); + });; + + }); + + $("#pay-minus").click(function() { + if (priceIdx <= 0) + return; + + priceIdx -= 1; + $("#pricing-value").text(prices[priceIdx]); + + $("#piggy").removeClass().addClass("tadaIn animated").one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){ + $(this).removeClass(); + });; + + }); + + +}); \ No newline at end of file diff --git a/stuff/bootstrap/bootstrap.less b/stuff/bootstrap/bootstrap.less index a4333692..ac59f666 100755 --- a/stuff/bootstrap/bootstrap.less +++ b/stuff/bootstrap/bootstrap.less @@ -25,7 +25,7 @@ // Components @import "component-animations.less"; @import "dropdowns.less"; -// @import "button-groups.less"; +@import "button-groups.less"; @import "input-groups.less"; @import "navs.less"; @import "navbar.less"; diff --git a/templates/payments/pricing.html b/templates/payments/pricing.html index 852d3d4c..920f4917 100644 --- a/templates/payments/pricing.html +++ b/templates/payments/pricing.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load staticfiles %} +{% load staticfiles compress %} {% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} @@ -11,10 +11,10 @@
    -
    +
    - +

    Free Plan

    @@ -24,9 +24,50 @@
  • Personal or Commercial use
  • Unlimited Checks
  • Unlimited Alerts
  • +
  •  
  • + + + + + +
    +
    +
    +
    + +
    +

    Pay What You Want Plan

    +
    +
    +

    + 10 / Month + + + + + + + +

    +
    + +
      +
    • Personal or Commercial use
    • +
    • Unlimited Checks
    • +
    • Unlimited Alerts
    • +
    • Priority Support
    • +
    +
    @@ -37,4 +78,12 @@ -{% endblock %} \ No newline at end of file +{% endblock %} + +{% block scripts %} +{% compress js %} + + + +{% endcompress %} +{% endblock %} From 3a93ab77a9ab9f9add6c8e3f048bc2e905a2f7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Mon, 16 Nov 2015 23:23:24 +0200 Subject: [PATCH 3/5] Don't load payment views if USE_PAYMENTS=False --- hc/urls.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hc/urls.py b/hc/urls.py index a890f794..8038c1d9 100644 --- a/hc/urls.py +++ b/hc/urls.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.conf.urls import include, url from django.contrib import admin @@ -6,5 +7,8 @@ urlpatterns = [ url(r'^accounts/', include('hc.accounts.urls')), url(r'^', include('hc.api.urls')), url(r'^', include('hc.front.urls')), - url(r'^', include('hc.payments.urls')), + ] + +if settings.USE_PAYMENTS: + urlpatterns.append(url(r'^', include('hc.payments.urls'))) From 20edec4c943823ee0bbddd0d4d90b53ed6d9c2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Sun, 22 Nov 2015 12:20:36 +0200 Subject: [PATCH 4/5] Payments WIP --- hc/payments/models.py | 23 ++++ hc/payments/urls.py | 16 +-- hc/payments/views.py | 86 +++++++++------ static/css/pricing.css | 8 ++ static/js/pricing.js | 35 +++++- templates/payments/create_subscription.html | 23 ---- templates/payments/pricing.html | 113 ++++++++++++++++++-- templates/payments/status.html | 39 ------- 8 files changed, 231 insertions(+), 112 deletions(-) delete mode 100644 templates/payments/create_subscription.html delete mode 100644 templates/payments/status.html diff --git a/hc/payments/models.py b/hc/payments/models.py index 30045204..4a8df4bd 100644 --- a/hc/payments/models.py +++ b/hc/payments/models.py @@ -1,3 +1,4 @@ +import braintree from django.contrib.auth.models import User from django.db import models @@ -7,3 +8,25 @@ class Subscription(models.Model): customer_id = models.CharField(max_length=36, blank=True) payment_method_token = models.CharField(max_length=35, blank=True) subscription_id = models.CharField(max_length=10, blank=True) + + def _get_braintree_sub(self): + if not hasattr(self, "_sub"): + print("getting subscription over network") + self._sub = braintree.Subscription.find(self.subscription_id) + + return self._sub + + def is_active(self): + if not self.subscription_id: + return False + + o = self._get_braintree_sub() + return o.status == "Active" + + def price(self): + o = self._get_braintree_sub() + return int(o.price) + + def next_billing_date(self): + o = self._get_braintree_sub() + return o.next_billing_date diff --git a/hc/payments/urls.py b/hc/payments/urls.py index 500291b3..053acb64 100644 --- a/hc/payments/urls.py +++ b/hc/payments/urls.py @@ -7,12 +7,16 @@ urlpatterns = [ views.pricing, name="hc-pricing"), - url(r'^create_subscription/$', - views.create, - name="hc-create-subscription"), + url(r'^pricing/create_plan/$', + views.create_plan, + name="hc-create-plan"), - url(r'^subscription_status/$', - views.status, - name="hc-subscription-status"), + url(r'^pricing/update_plan/$', + views.update_plan, + name="hc-update-plan"), + + url(r'^pricing/cancel_plan/$', + views.cancel_plan, + name="hc-cancel-plan"), ] diff --git a/hc/payments/views.py b/hc/payments/views.py index e56f36f2..9bf9bde4 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -2,6 +2,7 @@ import braintree from django.conf import settings from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, render +from django.views.decorators.http import require_POST from .models import Subscription @@ -17,29 +18,35 @@ def setup_braintree(): def pricing(request): + setup_braintree() + + try: + sub = Subscription.objects.get(user=request.user) + except Subscription.DoesNotExist: + sub = Subscription(user=request.user) + sub.save() + ctx = { "page": "pricing", + "sub": sub, + "client_token": braintree.ClientToken.generate() } + return render(request, "payments/pricing.html", ctx) @login_required -def create(request): +@require_POST +def create_plan(request): setup_braintree() - - try: - sub = Subscription.objects.get(user=request.user) - except Subscription.DoesNotExist: - sub = Subscription(user=request.user) + sub = Subscription.objects.get(user=request.user) + if not sub.customer_id: + result = braintree.Customer.create({}) + assert result.is_success + sub.customer_id = result.customer.id sub.save() - if request.method == "POST": - if not sub.customer_id: - result = braintree.Customer.create({}) - assert result.is_success - sub.customer_id = result.customer.id - sub.save() - + if "payment_method_nonce" in request.POST: result = braintree.PaymentMethod.create({ "customer_id": sub.customer_id, "payment_method_nonce": request.POST["payment_method_nonce"] @@ -48,34 +55,47 @@ def create(request): sub.payment_method_token = result.payment_method.token sub.save() - result = braintree.Subscription.create({ - "payment_method_token": sub.payment_method_token, - "plan_id": "pww", - "price": 5 - }) + price = int(request.POST["price"]) + assert price in (2, 5, 10, 15, 20, 25, 50, 100) - sub.subscription_id = result.subscription.id - sub.save() + result = braintree.Subscription.create({ + "payment_method_token": sub.payment_method_token, + "plan_id": "P%d" % price, + "price": price + }) - return redirect("hc-subscription-status") + sub.subscription_id = result.subscription.id + sub.save() - ctx = { - "page": "pricing", - "client_token": braintree.ClientToken.generate() - } - return render(request, "payments/create_subscription.html", ctx) + return redirect("hc-pricing") @login_required -def status(request): +@require_POST +def update_plan(request): setup_braintree() - sub = Subscription.objects.get(user=request.user) - subscription = braintree.Subscription.find(sub.subscription_id) - ctx = { - "page": "pricing", - "subscription": subscription + price = int(request.POST["price"]) + assert price in (2, 5, 10, 15, 20, 25, 50, 100) + + fields = { + "plan_id": "P%s" % price, + "price": price } - return render(request, "payments/status.html", ctx) + braintree.Subscription.update(sub.subscription_id, fields) + return redirect("hc-pricing") + + +@login_required +@require_POST +def cancel_plan(request): + setup_braintree() + sub = Subscription.objects.get(user=request.user) + + braintree.Subscription.cancel(sub.subscription_id) + sub.subscription_id = "" + sub.save() + + return redirect("hc-pricing") diff --git a/static/css/pricing.css b/static/css/pricing.css index 5bcd2eab..648a4737 100644 --- a/static/css/pricing.css +++ b/static/css/pricing.css @@ -100,3 +100,11 @@ -webkit-animation-name: tadaIn; animation-name: tadaIn; } + +#pww-switch-btn { + display: none; +} + +#subscription-status form { + display: inline-block; +} \ No newline at end of file diff --git a/static/js/pricing.js b/static/js/pricing.js index 22ceb3a1..b20d7511 100644 --- a/static/js/pricing.js +++ b/static/js/pricing.js @@ -1,13 +1,28 @@ $(function () { - var prices = [2, 5, 10, 15, 20, 25, 50, 75, 100, 125, 150, 175, 200]; - var priceIdx = 2; + var prices = [2, 5, 10, 15, 20, 25, 50, 100]; + var initialPrice = parseInt($("#pricing-value").text()); + var priceIdx = prices.indexOf(initialPrice); + + function updateDisplayPrice(price) { + $("#pricing-value").text(price); + $(".selected-price").val(price); + $("#pww-switch-btn").text("Switch to $" + price + " / mo"); + + if (price == initialPrice) { + $("#pww-selected-btn").show(); + $("#pww-switch-btn").hide(); + } else { + $("#pww-selected-btn").hide(); + $("#pww-switch-btn").show(); + } + } $("#pay-plus").click(function() { - if (priceIdx >= 12) + if (priceIdx > 6) return; priceIdx += 1; - $("#pricing-value").text(prices[priceIdx]); + updateDisplayPrice(prices[priceIdx]); $("#piggy").removeClass().addClass("tada animated").one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){ $(this).removeClass(); @@ -20,7 +35,7 @@ $(function () { return; priceIdx -= 1; - $("#pricing-value").text(prices[priceIdx]); + updateDisplayPrice(prices[priceIdx]); $("#piggy").removeClass().addClass("tadaIn animated").one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){ $(this).removeClass(); @@ -28,5 +43,15 @@ $(function () { }); + $("#pww-create-payment-method").click(function(){ + var $modal = $("#payment-method-modal"); + var clientToken = $modal.attr("data-client-token"); + + braintree.setup(clientToken, "dropin", { + container: "payment-form" + }); + + $modal.modal("show"); + }); }); \ No newline at end of file diff --git a/templates/payments/create_subscription.html b/templates/payments/create_subscription.html deleted file mode 100644 index 0e6065fc..00000000 --- a/templates/payments/create_subscription.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} -{% load staticfiles %} - -{% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} - -{% block content %} - -
    - {% csrf_token %} -
    - -
    - - - - -{% endblock %} \ No newline at end of file diff --git a/templates/payments/pricing.html b/templates/payments/pricing.html index 920f4917..b355f6a4 100644 --- a/templates/payments/pricing.html +++ b/templates/payments/pricing.html @@ -8,17 +8,40 @@
    + {% if sub.is_active %} +
    +
    +
    +

    + You are currently paying ${{ sub.price }}/month. + Next billing date will be {{ sub.next_billing_date }}.

    +

    + Thank you for supporting healthchecks.io! +

    + Update Payment Method +
    + {% csrf_token %} + +
    +
    +
    +
    + {% endif %} + +
    -
    +

    Free Plan

    -

    €0 / Month

    +

    $0 / Month

    • Personal or Commercial use
    • @@ -27,14 +50,27 @@
    •  
    -
    +
    @@ -44,7 +80,13 @@

    - 10 / Month + + {% if sub.is_active %} + ${{ sub.price }} / Month + {% else %} + $10 / Month + {% endif %} + + +

    + {% csrf_token %} + + +
    + {% else %} + {% if sub.payment_method_token %} +
    + {% csrf_token %} + + +
    + {% else %} + + {% endif %} + + {% endif %} + {% else %} + Get Started + {% endif %}
    -
    +
    + + {% endblock %} {% block scripts %} + {% compress js %} diff --git a/templates/payments/status.html b/templates/payments/status.html deleted file mode 100644 index 20171e2f..00000000 --- a/templates/payments/status.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "base.html" %} -{% load staticfiles %} - -{% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} - -{% block content %} - -

    Subscription Status

    - -
    -
    Status
    -
    {{ subscription.status }}
    - -
    Next Billing Date:
    -
    {{ subscription.next_billing_date }}
    - - -
    Amount
    -
    {{ subscription.price }} / Month
    - - -
    - -

    Transactions

    - - - - - - {% for tx in subscription.transactions %} - - - - - {% endfor %} -
    DateAmount
    ???{{ tx.amount }}
    - - -{% endblock %} \ No newline at end of file From 81116431dd749c2d5bba96ed99685ad72dae9d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Mon, 23 Nov 2015 09:06:43 +0200 Subject: [PATCH 5/5] braintree requirement is optional --- hc/settings.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hc/settings.py b/hc/settings.py index 83abc219..be6d12be 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -36,8 +36,7 @@ INSTALLED_APPS = ( 'hc.accounts', 'hc.api', - 'hc.front', - 'hc.payments' + 'hc.front' ) MIDDLEWARE_CLASSES = ( @@ -128,7 +127,7 @@ COMPRESS_OFFLINE = True EMAIL_BACKEND = "djmail.backends.default.EmailBackend" -try: +if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): from .local_settings import * -except ImportError as e: +else: warnings.warn("local_settings.py not found, using defaults")