@ -0,0 +1,8 @@ | |||
from django.contrib import admin | |||
from .models import Subscription | |||
@admin.register(Subscription) | |||
class SubsAdmin(admin.ModelAdmin): | |||
list_display = ("id", "user") |
@ -0,0 +1,5 @@ | |||
from django.conf import settings | |||
def payments(request): | |||
return {'USE_PAYMENTS': settings.USE_PAYMENTS} |
@ -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)), | |||
], | |||
), | |||
] |
@ -0,0 +1,32 @@ | |||
import braintree | |||
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) | |||
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 |
@ -0,0 +1,3 @@ | |||
from django.test import TestCase | |||
# Create your tests here. |
@ -0,0 +1,22 @@ | |||
from django.conf.urls import url | |||
from . import views | |||
urlpatterns = [ | |||
url(r'^pricing/$', | |||
views.pricing, | |||
name="hc-pricing"), | |||
url(r'^pricing/create_plan/$', | |||
views.create_plan, | |||
name="hc-create-plan"), | |||
url(r'^pricing/update_plan/$', | |||
views.update_plan, | |||
name="hc-update-plan"), | |||
url(r'^pricing/cancel_plan/$', | |||
views.cancel_plan, | |||
name="hc-cancel-plan"), | |||
] |
@ -0,0 +1,101 @@ | |||
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 | |||
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): | |||
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 | |||
@require_POST | |||
def create_plan(request): | |||
setup_braintree() | |||
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 "payment_method_nonce" in request.POST: | |||
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() | |||
price = int(request.POST["price"]) | |||
assert price in (2, 5, 10, 15, 20, 25, 50, 100) | |||
result = braintree.Subscription.create({ | |||
"payment_method_token": sub.payment_method_token, | |||
"plan_id": "P%d" % price, | |||
"price": price | |||
}) | |||
sub.subscription_id = result.subscription.id | |||
sub.save() | |||
return redirect("hc-pricing") | |||
@login_required | |||
@require_POST | |||
def update_plan(request): | |||
setup_braintree() | |||
sub = Subscription.objects.get(user=request.user) | |||
price = int(request.POST["price"]) | |||
assert price in (2, 5, 10, 15, 20, 25, 50, 100) | |||
fields = { | |||
"plan_id": "P%s" % price, | |||
"price": price | |||
} | |||
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") |
@ -20,6 +20,7 @@ SECRET_KEY = "---" | |||
DEBUG = True | |||
ALLOWED_HOSTS = [] | |||
DEFAULT_FROM_EMAIL = '[email protected]' | |||
USE_PAYMENTS = False | |||
INSTALLED_APPS = ( | |||
@ -62,6 +63,7 @@ TEMPLATES = [ | |||
'django.template.context_processors.request', | |||
'django.contrib.auth.context_processors.auth', | |||
'django.contrib.messages.context_processors.messages', | |||
'hc.payments.context_processors.payments' | |||
], | |||
}, | |||
}, | |||
@ -131,7 +133,7 @@ PUSHOVER_SUBSCRIPTION_URL = None | |||
PUSHOVER_EMERGENCY_RETRY_DELAY = 300 | |||
PUSHOVER_EMERGENCY_EXPIRATION = 86400 | |||
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") |
@ -0,0 +1,57 @@ | |||
$(function () { | |||
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 > 6) | |||
return; | |||
priceIdx += 1; | |||
updateDisplayPrice(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; | |||
updateDisplayPrice(prices[priceIdx]); | |||
$("#piggy").removeClass().addClass("tadaIn animated").one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){ | |||
$(this).removeClass(); | |||
});; | |||
}); | |||
$("#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"); | |||
}); | |||
}); |
@ -1,41 +0,0 @@ | |||
{% extends "base.html" %} | |||
{% load staticfiles %} | |||
{% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} | |||
{% block content %} | |||
<!-- Plans --> | |||
<section id="plans"> | |||
<div class="container"> | |||
<div class="row"> | |||
<!-- item --> | |||
<div class="col-md-4 text-center col-md-offset-4"> | |||
<div class="panel panel-success panel-pricing"> | |||
<div class="panel-heading"> | |||
<i class="glyphicon glyphicon-heart-empty"></i> | |||
<h3>Free Plan</h3> | |||
</div> | |||
<div class="panel-body text-center"> | |||
<p><strong>€0 / Month</strong></p> | |||
</div> | |||
<ul class="list-group text-center"> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Personal or Commercial use</li> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Checks</li> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Alerts</li> | |||
</ul> | |||
<div class="panel-footer"> | |||
<a class="btn btn-lg btn-block btn-success" href="{% url 'hc-login' %}">Get Started</a> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- /item --> | |||
</div> | |||
</div> | |||
</section> | |||
<!-- /Plans --> | |||
{% endblock %} |
@ -0,0 +1,190 @@ | |||
{% extends "base.html" %} | |||
{% load staticfiles compress %} | |||
{% block title %}Pricing - It's Free! - healthchecks.io{% endblock %} | |||
{% block content %} | |||
<!-- Plans --> | |||
<section id="plans"> | |||
<div class="container"> | |||
{% if sub.is_active %} | |||
<div class="row"> | |||
<div class="col-md-12"> | |||
<div id="subscription-status" class="jumbotron"> | |||
<p> | |||
You are currently paying <strong>${{ sub.price }}/month</strong>. | |||
Next billing date will be {{ sub.next_billing_date }}.</p> | |||
<p> | |||
Thank you for supporting healthchecks.io! | |||
</p> | |||
<a class="btn btn-default" href="#">Update Payment Method</a> | |||
<form method="post" action="{% url 'hc-cancel-plan' %}"> | |||
{% csrf_token %} | |||
<button type="submit" class="btn btn-default"> | |||
Cancel Subscription | |||
</button> | |||
</form> | |||
</div> | |||
</div> | |||
</div> | |||
{% endif %} | |||
<div class="row"> | |||
<!-- item --> | |||
<div class="col-sm-4 text-center"> | |||
<div class="panel panel-success panel-pricing"> | |||
<div class="panel-heading"> | |||
<i class="glyphicon glyphicon-heart"></i> | |||
<h3>Free Plan</h3> | |||
</div> | |||
<div class="panel-body text-center"> | |||
<p><strong>$0 / Month</strong></p> | |||
</div> | |||
<ul class="list-group text-center"> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Personal or Commercial use</li> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Checks</li> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Alerts</li> | |||
<li class="list-group-item"> </li> | |||
</ul> | |||
<div class="panel-footer"> | |||
{% if request.user.is_authenticated %} | |||
{% if sub.subscription_id %} | |||
<form method="post" action="{% url 'hc-cancel-plan' %}"> | |||
{% csrf_token %} | |||
<button type="submit" class="btn btn-lg btn-default"> | |||
Switch To Free | |||
</button> | |||
</form> | |||
{% else %} | |||
<a class="btn btn-lg btn-success disabled" href="#">Selected</a> | |||
{% endif %} | |||
{% else %} | |||
<a class="btn btn-lg btn-success" href="{% url 'hc-login' %}">Get Started</a> | |||
{% endif %} | |||
</div> | |||
</div> | |||
</div> | |||
<!-- /item --> | |||
<!-- item --> | |||
<div class="col-sm-8 text-center"> | |||
<div class="panel panel-success panel-pricing"> | |||
<div class="panel-heading"> | |||
<div id="piggy"> | |||
<i class="glyphicon glyphicon-piggy-bank"></i> | |||
</div> | |||
<h3>Pay What You Want Plan</h3> | |||
</div> | |||
<div class="panel-body text-center"> | |||
<p> | |||
<strong> | |||
{% if sub.is_active %} | |||
$<span id="pricing-value">{{ sub.price }}</span> / Month | |||
{% else %} | |||
$<span id="pricing-value">10</span> / Month | |||
{% endif %} | |||
</strong> | |||
<span class="btn-group" role="group"> | |||
<button id="pay-minus" type="button" class="btn btn-default"> | |||
<i class="glyphicon glyphicon-chevron-down"></i> | |||
</button> | |||
<button id="pay-plus" type="button" class="btn btn-default"> | |||
<i class="glyphicon glyphicon-chevron-up"></i> | |||
</button> | |||
</span> | |||
</p> | |||
</div> | |||
<ul class="list-group text-center"> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Personal or Commercial use</li> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Checks</li> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Alerts</li> | |||
<li class="list-group-item"><i class="fa fa-check"></i> Priority Support</li> | |||
</ul> | |||
<div class="panel-footer"> | |||
{% if request.user.is_authenticated %} | |||
{% if sub.is_active %} | |||
<button id="pww-selected-btn" | |||
class="btn btn-lg btn-success disabled">Selected</button> | |||
<form method="post" action="{% url 'hc-update-plan' %}"> | |||
{% csrf_token %} | |||
<input class="selected-price" type="hidden" name="price" /> | |||
<button | |||
id="pww-switch-btn" | |||
type="submit" | |||
class="btn btn-lg btn-default"> | |||
Switch To | |||
</button> | |||
</form> | |||
{% else %} | |||
{% if sub.payment_method_token %} | |||
<form method="post" action="{% url 'hc-create-plan' %}"> | |||
{% csrf_token %} | |||
<input class="selected-price" type="hidden" name="price" value="10" /> | |||
<button | |||
type="submit" | |||
class="btn btn-lg btn-default"> | |||
Select (direct) | |||
</button> | |||
</form> | |||
{% else %} | |||
<button | |||
id="pww-create-payment-method" | |||
class="btn btn-lg btn-default">Select (form)</button> | |||
{% endif %} | |||
{% endif %} | |||
{% else %} | |||
<a class="btn btn-lg btn-primary" href="{% url 'hc-login' %}">Get Started</a> | |||
{% endif %} | |||
</div> | |||
</div> | |||
</div> | |||
<!-- /item --> | |||
</div> | |||
</div> | |||
</section> | |||
<!-- /Plans --> | |||
<div id="payment-method-modal" class="modal" data-client-token="{{ client_token }}"> | |||
<div class="modal-dialog"> | |||
<form method="post" action="{% url 'hc-create-plan' %}"> | |||
{% csrf_token %} | |||
<input class="selected-price" type="hidden" name="price" value="10" /> | |||
<div class="modal-content"> | |||
<div class="modal-header"> | |||
<button type="button" class="close" data-dismiss="modal">×</span></button> | |||
<h4>Set Up Subscription</h4> | |||
</div> | |||
<div class="modal-body" id="payment-method-body"> | |||
<div id="payment-form"></div> | |||
</div> | |||
<div class="modal-footer"> | |||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> | |||
<button type="submit" class="btn btn-primary">Set Up Subscription</button> | |||
</div> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
{% endblock %} | |||
{% block scripts %} | |||
<script src="https://js.braintreegateway.com/v2/braintree.js"></script> | |||
{% compress js %} | |||
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script> | |||
<script src="{% static 'js/bootstrap.min.js' %}"></script> | |||
<script src="{% static 'js/pricing.js' %}"></script> | |||
{% endcompress %} | |||
{% endblock %} |