forked from GithubBackups/healthchecks
Merge branch 'master' into pushover
This commit is contained in:
commit
6c9e3bb24d
@ -11,7 +11,6 @@ urlpatterns = [
|
|||||||
url(r'^checks/([\w-]+)/email/$', views.email_preview),
|
url(r'^checks/([\w-]+)/email/$', views.email_preview),
|
||||||
url(r'^checks/([\w-]+)/remove/$', views.remove_check, name="hc-remove-check"),
|
url(r'^checks/([\w-]+)/remove/$', views.remove_check, name="hc-remove-check"),
|
||||||
url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"),
|
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'^docs/$', views.docs, name="hc-docs"),
|
||||||
url(r'^about/$', views.about, name="hc-about"),
|
url(r'^about/$', views.about, name="hc-about"),
|
||||||
url(r'^integrations/$', views.channels, name="hc-channels"),
|
url(r'^integrations/$', views.channels, name="hc-channels"),
|
||||||
|
@ -50,10 +50,6 @@ def index(request):
|
|||||||
return render(request, "front/welcome.html", ctx)
|
return render(request, "front/welcome.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
def pricing(request):
|
|
||||||
return render(request, "front/pricing.html", {"page": "pricing"})
|
|
||||||
|
|
||||||
|
|
||||||
def docs(request):
|
def docs(request):
|
||||||
if "welcome_code" in request.session:
|
if "welcome_code" in request.session:
|
||||||
code = request.session["welcome_code"]
|
code = request.session["welcome_code"]
|
||||||
|
0
hc/payments/__init__.py
Normal file
0
hc/payments/__init__.py
Normal file
8
hc/payments/admin.py
Normal file
8
hc/payments/admin.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Subscription
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Subscription)
|
||||||
|
class SubsAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = ("id", "user")
|
5
hc/payments/context_processors.py
Normal file
5
hc/payments/context_processors.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def payments(request):
|
||||||
|
return {'USE_PAYMENTS': settings.USE_PAYMENTS}
|
25
hc/payments/migrations/0001_initial.py
Normal file
25
hc/payments/migrations/0001_initial.py
Normal file
@ -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
hc/payments/migrations/__init__.py
Normal file
0
hc/payments/migrations/__init__.py
Normal file
32
hc/payments/models.py
Normal file
32
hc/payments/models.py
Normal file
@ -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
|
3
hc/payments/tests.py
Normal file
3
hc/payments/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
22
hc/payments/urls.py
Normal file
22
hc/payments/urls.py
Normal file
@ -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"),
|
||||||
|
|
||||||
|
]
|
101
hc/payments/views.py
Normal file
101
hc/payments/views.py
Normal file
@ -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
|
DEBUG = True
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
DEFAULT_FROM_EMAIL = 'healthchecks@example.org'
|
DEFAULT_FROM_EMAIL = 'healthchecks@example.org'
|
||||||
|
USE_PAYMENTS = False
|
||||||
|
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
@ -62,6 +63,7 @@ TEMPLATES = [
|
|||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'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_RETRY_DELAY = 300
|
||||||
PUSHOVER_EMERGENCY_EXPIRATION = 86400
|
PUSHOVER_EMERGENCY_EXPIRATION = 86400
|
||||||
|
|
||||||
try:
|
if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")):
|
||||||
from .local_settings import *
|
from .local_settings import *
|
||||||
except ImportError as e:
|
else:
|
||||||
warnings.warn("local_settings.py not found, using defaults")
|
warnings.warn("local_settings.py not found, using defaults")
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
@ -6,4 +7,8 @@ urlpatterns = [
|
|||||||
url(r'^accounts/', include('hc.accounts.urls')),
|
url(r'^accounts/', include('hc.accounts.urls')),
|
||||||
url(r'^', include('hc.api.urls')),
|
url(r'^', include('hc.api.urls')),
|
||||||
url(r'^', include('hc.front.urls')),
|
url(r'^', include('hc.front.urls')),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.USE_PAYMENTS:
|
||||||
|
urlpatterns.append(url(r'^', include('hc.payments.urls')))
|
||||||
|
184
static/css/bootstrap.css
vendored
184
static/css/bootstrap.css
vendored
@ -3466,19 +3466,22 @@ fieldset[disabled] .btn-link:focus {
|
|||||||
color: #777777;
|
color: #777777;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.btn-lg {
|
.btn-lg,
|
||||||
|
.btn-group-lg > .btn {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 1.3333333;
|
line-height: 1.3333333;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.btn-sm {
|
.btn-sm,
|
||||||
|
.btn-group-sm > .btn {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
.btn-xs {
|
.btn-xs,
|
||||||
|
.btn-group-xs > .btn {
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@ -3670,6 +3673,175 @@ tbody.collapse.in {
|
|||||||
right: auto;
|
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 {
|
.input-group {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: table;
|
display: table;
|
||||||
@ -5743,6 +5915,10 @@ button.close {
|
|||||||
.row:after,
|
.row:after,
|
||||||
.form-horizontal .form-group:before,
|
.form-horizontal .form-group:before,
|
||||||
.form-horizontal .form-group:after,
|
.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:before,
|
||||||
.nav:after,
|
.nav:after,
|
||||||
.navbar:before,
|
.navbar:before,
|
||||||
@ -5764,6 +5940,8 @@ button.close {
|
|||||||
.container-fluid:after,
|
.container-fluid:after,
|
||||||
.row:after,
|
.row:after,
|
||||||
.form-horizontal .form-group:after,
|
.form-horizontal .form-group:after,
|
||||||
|
.btn-toolbar:after,
|
||||||
|
.btn-group-vertical > .btn-group:after,
|
||||||
.nav:after,
|
.nav:after,
|
||||||
.navbar:after,
|
.navbar:after,
|
||||||
.navbar-header:after,
|
.navbar-header:after,
|
||||||
|
@ -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 {
|
.panel-pricing .panel-heading {
|
||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
}
|
}
|
||||||
@ -26,9 +18,93 @@
|
|||||||
border-top-left-radius: 0px;
|
border-top-left-radius: 0px;
|
||||||
}
|
}
|
||||||
.panel-pricing .panel-body {
|
.panel-pricing .panel-body {
|
||||||
background-color: #f0f0f0;
|
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
color: #777777;
|
color: #777777;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 0px;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pww-switch-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subscription-status form {
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
57
static/js/pricing.js
Normal file
57
static/js/pricing.js
Normal file
@ -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");
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
2
stuff/bootstrap/bootstrap.less
vendored
2
stuff/bootstrap/bootstrap.less
vendored
@ -25,7 +25,7 @@
|
|||||||
// Components
|
// Components
|
||||||
@import "component-animations.less";
|
@import "component-animations.less";
|
||||||
@import "dropdowns.less";
|
@import "dropdowns.less";
|
||||||
// @import "button-groups.less";
|
@import "button-groups.less";
|
||||||
@import "input-groups.less";
|
@import "input-groups.less";
|
||||||
@import "navs.less";
|
@import "navs.less";
|
||||||
@import "navbar.less";
|
@import "navbar.less";
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
<link rel="stylesheet" href="{% static 'css/my_checks.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/my_checks.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/my_checks_mobile.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/my_checks_mobile.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/my_checks_desktop.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/my_checks_desktop.css' %}" type="text/css">
|
||||||
|
{% if USE_PAYMENTS %}
|
||||||
<link rel="stylesheet" href="{% static 'css/pricing.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/pricing.css' %}" type="text/css">
|
||||||
|
{% endif %}
|
||||||
<link rel="stylesheet" href="{% static 'css/syntax.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/syntax.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/channels.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/channels.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
||||||
@ -80,9 +82,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if USE_PAYMENTS %}
|
||||||
<li {% if page == 'pricing' %} class="active" {% endif %}>
|
<li {% if page == 'pricing' %} class="active" {% endif %}>
|
||||||
<a href="{% url 'hc-pricing' %}">Pricing</a>
|
<a href="{% url 'hc-pricing' %}">Pricing</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<li {% if page == 'docs' %} class="active" {% endif %}>
|
<li {% if page == 'docs' %} class="active" {% endif %}>
|
||||||
<a href="{% url 'hc-docs' %}">Docs</a>
|
<a href="{% url 'hc-docs' %}">Docs</a>
|
||||||
|
@ -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 %}
|
|
190
templates/payments/pricing.html
Normal file
190
templates/payments/pricing.html
Normal file
@ -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 %}
|
Loading…
x
Reference in New Issue
Block a user