Added View to render payment page, Added Payment and summary forms, Added Payment.js library to request stripe token , Added jQuery validator for handling payment form errors

This commit is contained in:
Levi 2016-04-22 08:36:38 -05:00
parent 4e23adcea6
commit 38801abed7
13 changed files with 480 additions and 71 deletions

View file

@ -30,6 +30,7 @@ dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR))
SITE_ID = 1
APP_ROOT_ENDPOINT = "/"
APPEND_SLASH=True
LOGIN_URL = None
LOGOUT_URL = None

20
hosting/mixins.py Normal file
View file

@ -0,0 +1,20 @@
from django.shortcuts import redirect
from django.core.urlresolvers import reverse
class ProcessVMSelectionMixin(object):
def post(self, request, *args, **kwargs):
vm_specs = {
'cores': request.POST.get('cores'),
'memory': request.POST.get('memory'),
'disk_size': request.POST.get('disk_space'),
'hosting_company': request.POST.get('hosting_company'),
'hosting_company_name': request.POST.get('hosting_company_name'),
'final_price': request.POST.get('final_price')
}
request.session['vm_specs'] = vm_specs
if not request.user.is_authenticated():
request.session['vm_specs'] = vm_specs
return redirect(reverse('hosting:login'))
return redirect(reverse('hosting:payment'))

View file

@ -31,8 +31,7 @@ h6 {
}
.intro-header {
height: 85%;
padding-top: 10%; /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */
padding-top: 50px; /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */
padding-bottom: 50px;
text-align: center;
color: #f8f8f8;
@ -48,8 +47,7 @@ h6 {
background-size: cover;
}
.intro-header-2 {
height: 85%;
padding-top: 100px; /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */
padding-top: 50px; /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */
padding-bottom: 50px;
text-align: center;
color: #f8f8f8;

View file

@ -0,0 +1,33 @@
.creditcard-box {padding-top:17%; padding-bottom: 11%;}
.creditcard-box .panel-title {display: inline;font-weight: bold; font-size:17px;}
.creditcard-box .checkbox.pull-right { margin: 0; }
.creditcard-box .pl-ziro { padding-left: 0px; }
.creditcard-box .form-control.error {
border-color: red;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(255,0,0,0.6);
}
.creditcard-box label.error {
font-weight: bold;
color: red;
padding: 2px 8px;
margin-top: 2px;
}
.creditcard-box .payment-errors {
font-weight: bold;
color: red;
padding: 2px 8px;
margin-top: 2px;
}
.summary-box {
padding-top:17%;
padding-bottom: 11%;
}
.summary-box .content {
padding-top: 15px;
}

View file

@ -0,0 +1,110 @@
$( document ).ready(function() {
var $form = $('#payment-form');
$form.submit(payWithStripe);
/* If you're using Stripe for payments */
function payWithStripe(e) {
e.preventDefault();
/* Visual feedback */
$form.find('[type=submit]').html('Validating <i class="fa fa-spinner fa-pulse"></i>');
var PublishableKey = 'pk_test_6pRNASCoBOKtIshFeQd4XMUh'; // Replace with your API publishable key
Stripe.setPublishableKey(PublishableKey);
Stripe.card.createToken($form, function stripeResponseHandler(status, response) {
console.log
if (response.error) {
/* Visual feedback */
$form.find('[type=submit]').html('Try again');
/* Show Stripe errors on the form */
$form.find('.payment-errors').text(response.error.message);
$form.find('.payment-errors').closest('.row').show();
} else {
/* Visual feedback */
$form.find('[type=submit]').html('Processing <i class="fa fa-spinner fa-pulse"></i>');
/* Hide Stripe errors on the form */
$form.find('.payment-errors').closest('.row').hide();
$form.find('.payment-errors').text("");
// response contains id and card, which contains additional card details
var token = response.id;
console.log(token);
// AJAX
$.post('/account/stripe_card_token', {
token: token
})
// Assign handlers immediately after making the request,
.done(function(data, textStatus, jqXHR) {
$form.find('[type=submit]').html('Payment successful <i class="fa fa-check"></i>').prop('disabled', true);
})
.fail(function(jqXHR, textStatus, errorThrown) {
$form.find('[type=submit]').html('There was a problem').removeClass('success').addClass('error');
/* Show Stripe errors on the form */
$form.find('.payment-errors').text('Try refreshing the page and trying again.');
$form.find('.payment-errors').closest('.row').show();
});
}
});
}
/* Form validation */
$.validator.addMethod("month", function(value, element) {
return this.optional(element) || /^(01|02|03|04|05|06|07|08|09|10|11|12)$/.test(value);
}, "Please specify a valid 2-digit month.");
$.validator.addMethod("year", function(value, element) {
return this.optional(element) || /^[0-9]{2}$/.test(value);
}, "Please specify a valid 2-digit year.");
validator = $form.validate({
rules: {
cardNumber: {
required: true,
creditcard: true,
digits: true
},
expMonth: {
required: true,
month: true
},
expYear: {
required: true,
year: true
},
cvCode: {
required: true,
digits: true
}
},
highlight: function(element) {
$(element).closest('.form-control').removeClass('success').addClass('error');
},
unhighlight: function(element) {
$(element).closest('.form-control').removeClass('error').addClass('success');
},
errorPlacement: function(error, element) {
$(element).closest('.form-group').append(error);
}
});
paymentFormReady = function() {
if ($form.find('[name=cardNumber]').hasClass("success") &&
$form.find('[name=expMonth]').hasClass("success") &&
$form.find('[name=expYear]').hasClass("success") &&
$form.find('[name=cvCode]').val().length > 1) {
return true;
} else {
return false;
}
}
$form.find('[type=submit]').prop('disabled', true);
var readyInterval = setInterval(function() {
if (paymentFormReady()) {
$form.find('[type=submit]').prop('disabled', false);
clearInterval(readyInterval);
}
}, 250);
});

View file

@ -8,7 +8,7 @@ $( document ).ready(function() {
function calculate_price(vm_type){
var ID_SELECTOR = "#";
var CURRENCY = "$";
var CURRENCY = "CHF";
var final_price_selector = ID_SELECTOR.concat(vm_type.concat('-final-price'));
var core_selector = ID_SELECTOR.concat(vm_type.concat('-cores'));
var memory_selector = ID_SELECTOR.concat(vm_type.concat('-memory'));

View file

@ -0,0 +1,135 @@
{% load staticfiles bootstrap3%}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>Payment</title>
<!-- Bootstrap Core CSS -->
<link href="{% static 'hosting/css/bootstrap.min.css' %}" rel="stylesheet">
<!-- Custom CSS -->
<link href="{% static 'hosting/css/landing-page.css' %}" rel="stylesheet">
<link href="{% static 'hosting/css/payment.css' %}" rel="stylesheet">
<!-- Custom Fonts -->
<link href='http://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link href="http://fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic,700italic" rel="stylesheet" type="text/css">
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon" />
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-default navbar-fixed-top topnav" role="navigation">
<div class="container topnav">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand topnav" href="#"><img src="img/logo_black.svg"></a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="#how">How it works</a>
</li>
<li>
<a href="#your">Your infrastructure</a>
</li>
<li>
<a href="#our">Our inftrastructure</a>
</li>
<li>
<a href="#price">Pricing</a>
</li>
<li>
<a href="#contact">Contact</a>
</li>
</ul>
</div>
<!-- /.navbar-collapse -->
</div>
<!-- /.container -->
</nav>
<!-- Header -->
<a name="about"></a>
{% block content %}
{% endblock %}
<!-- Footer -->
<footer>
<div class="container">
<div class="row">
<div class="col-lg-12">
<ul class="list-inline">
<li>
<a href="#">Home</a>
</li>
<li class="footer-menu-divider">&sdot;</li>
<li>
<a href="#about">How it works</a></li>
<li class="footer-menu-divider">&sdot;</li>
<li>
<a href="#about">Your infrastructure</a></li>
<li>&sdot;</li>
<li>
<a href="#about">Our infrastructure</a></li>
<li class="footer-menu-divider">&sdot;</li>
<li>
<a href="#services">Pricing</a>
</li>
<li class="footer-menu-divider">&sdot;</li>
<li>
<a href="#contact">Contact</a>
</li>
</ul>
<p class="copyright text-muted small">Copyright &copy; ungleich GmbH {% now "Y" %}. All Rights Reserved</p>
</div>
</div>
</div>
</footer>
<!-- jQuery -->
<script src="{% static 'hosting/js/jquery.js' %}"></script>
<script type="text/javascript" src="http://cdn.jsdelivr.net/jquery.validation/1.13.1/jquery.validate.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script src="{% static 'hosting/js/bootstrap.min.js' %}"></script>
<!-- Stripe Lib -->
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<!-- Proccess payment lib -->
<script type="text/javascript" src="{% static 'hosting/js/payment.js' %}"></script>
</body>
</html>

View file

@ -1,5 +1,4 @@
{% load staticfiles %}
<a name="about"></a>
<div class="intro-header">
<div class="container">

View file

@ -80,7 +80,10 @@
{% endfor %}
{% for vm in vm_types %}
<div class="col-xs-12 col-sm-6 col-md-3">
<form class="form-inline">
<form class="form-inline" method="POST" action="{{request.path}}">
{% csrf_token %}
<input type="hidden" name="hosting_company" value="{{vm.hosting_company}}">
<input type="hidden" name="hosting_company_name" value="{{vm.hosting_company_name}}">
<ul class="pricing {% cycle 'p-green' 'p-yel' 'p-red' 'p-blue' %}">
<li style="height:200px;">
<!-- <img src="http://bread.pp.ua/n/settings_g.svg" alt=""> -->
@ -91,7 +94,7 @@
<div class="btn-group">
<div class="form-group">
<label for="cores">Cores: </label>
<select class="form-control cores-selector" id="{{vm.hosting_company}}-cores" data-vm-type="{{vm.hosting_company}}">
<select class="form-control cores-selector" name="cores" id="{{vm.hosting_company}}-cores" data-vm-type="{{vm.hosting_company}}">
{% with ''|center:10 as range %}
{% for _ in range %}
<option>{{ forloop.counter }}</option>
@ -106,7 +109,7 @@
<div class="btn-group">
<div class="form-group">
<label for="memory">Memory: </label>
<select class="form-control memory-selector" id="{{vm.hosting_company}}-memory" data-vm-type="{{vm.hosting_company}}">
<select class="form-control memory-selector" name="memory" id="{{vm.hosting_company}}-memory" data-vm-type="{{vm.hosting_company}}">
{% with ''|center:50 as range %}
{% for _ in range %}
<option>{{ forloop.counter }}</option>
@ -120,12 +123,13 @@
<li class="row">
<div class="form-group">
<label for="Disk Size">Disk Size: </label>
<input class="form-control short-input disk-space-selector" type="number" id="{{vm.hosting_company}}-disk_space" min="10" value="10" data-vm-type="{{vm.hosting_company}}"/>
<input class="form-control short-input disk-space-selector" name="disk_space" type="number" id="{{vm.hosting_company}}-disk_space" min="10" value="10" data-vm-type="{{vm.hosting_company}}"/>
<span>GiB</span>
</div>
</li>
<li>
<h3 id="{{vm.hosting_company}}-final-price">{{vm.default_price|floatformat}}$</h3>
<input id="{{vm.hosting_company}}-final-price" type="hidden" name="final_price" value="{{vm.default_price|floatformat}}">
<h3 id="{{vm.hosting_company}}-final-price">{{vm.default_price|floatformat}}CHF</h3>
<span>per month</span>
</li>
<li>

View file

@ -36,7 +36,7 @@
<body>
<!-- Navigation -->
<nav class="navbar navbar-default navbar-fixed-top topnav" role="navigation">
<nav class="navbar navbar-default topnav" role="navigation">
<div class="container topnav">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
@ -78,25 +78,25 @@
<a name="about"></a>
<div class="intro-header">
<div class="container">
<div class="col-md-4">&nbsp;</div><div class="col-md-4">
<div class="intro-message">
<h2>Login</h2>
<form action="{% url 'hosting:login' %}" method="post" class="form" novalidate>
{% csrf_token %}
{% for field in form %}
{% bootstrap_field field show_label=False type='fields'%}
{% endfor %}
{% bootstrap_form_errors form type='non_fields'%}
{% buttons %}
<button type="submit" class="btn btn-default">
Login
</button>
{% endbuttons %}
</form>
<ul class="list-inline intro-social-buttons">
<div class="col-md-4 col-md-offset-4">
<div class="intro-message">
<h2>Login</h2>
<form action="{% url 'hosting:login' %}" method="post" class="form" novalidate>
{% csrf_token %}
{% for field in form %}
{% bootstrap_field field show_label=False type='fields'%}
{% endfor %}
{% bootstrap_form_errors form type='non_fields'%}
{% buttons %}
<button type="submit" class="btn btn-default">
Login
</button>
{% endbuttons %}
</form>
<ul class="list-inline intro-social-buttons">
</ul>
</div>
</ul>
</div>
</div>
</div>
<!-- /.container -->

View file

@ -0,0 +1,90 @@
{% extends "hosting/base_short.html" %}
{% load staticfiles bootstrap3 %}
{%block content %}
<!-- Credit card form -->
<div>
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-4 col-md-offset-2 creditcard-box">
<h3><b>Payment Details</b></h3>
<hr>
<div>
<div class="panel-body">
<form role="form" id="payment-form" novalidate>
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<label class="control-label" for="cardNumber">CARD NUMBER</label>
<div class="input-group">
<input type="text" class="form-control" name="cardNumber" placeholder="Valid Card Number" required autofocus data-stripe="number" />
<span class="input-group-addon"><i class="fa fa-credit-card"></i></span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-7 col-md-7">
<div class="form-group">
<label for="expMonth">EXPIRATION DATE</label>
<div class="col-xs-6 col-lg-6 pl-ziro">
<input type="text" class="form-control" name="expMonth" placeholder="MM" required data-stripe="exp_month" />
</div>
<div class="col-xs-6 col-lg-6 pl-ziro">
<input type="text" class="form-control" name="expYear" placeholder="YY" required data-stripe="exp_year" />
</div>
</div>
</div>
<div class="col-xs-5 col-md-5 pull-right">
<div class="form-group">
<label for="cvCode">CV CODE</label>
<input type="password" class="form-control" name="cvCode" placeholder="CV" required data-stripe="cvc" />
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button class="btn btn-success btn-lg btn-block" type="submit">Submit Payment</button>
</div>
</div>
<div class="row" style="display:none;">
<div class="col-xs-12">
<p class="payment-errors"></p>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-xs-12 col-md-3 col-md-offset-1 summary-box">
<form role="form" novalidate>
<div class="row">
<div class="col-xs-12">
<h3><b>Billing Amount</b></h3>
<hr>
<div class="content">
<p><b>Type</b> <span class="pull-right">{{request.session.vm_specs.hosting_company_name}}</span></p>
<hr>
<p><b>Cores</b> <span class="pull-right">{{request.session.vm_specs.cores}}</span></p>
<hr>
<p><b>Memory</b> <span class="pull-right">{{request.session.vm_specs.memory}} GiB</span></p>
<hr>
<p><b>Disk space</b> <span class="pull-right">{{request.session.vm_specs.memory}} GiB</span></p>
<hr>
<h4>Total<p class="pull-right"><b>{{request.session.vm_specs.final_price}} CHF</b></p></h4>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{%endblock%}

View file

@ -1,14 +1,14 @@
from django.conf.urls import url
from .views import VMPricingView, DjangoHostingView, RailsHostingView, \
from .views import DjangoHostingView, RailsHostingView, PaymentVMView, \
NodeJSHostingView, LoginView, SignupView, IndexView
urlpatterns = [
url(r'index/?$', IndexView.as_view(), name='index'),
url(r'pricing/?$', VMPricingView.as_view(), name='pricing'),
url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'),
url(r'nodejs/?$', NodeJSHostingView.as_view(), name='nodejshosting'),
url(r'rails/?$', RailsHostingView.as_view(), name='railshosting'),
url(r'login/?$', LoginView.as_view(), name='login'),
url(r'signup/?$', SignupView.as_view(), name='signup'),
url(r'payment/?$', PaymentVMView.as_view(), name='payment'),
]

View file

@ -3,32 +3,32 @@ from django.shortcuts import get_object_or_404, render
from django.core.urlresolvers import reverse_lazy, reverse
from django.views.generic import View, CreateView, FormView
from django.shortcuts import redirect
from django.http import HttpResponseRedirect
from django.contrib.auth import authenticate, login
from django.conf import settings
from membership.forms import PaymentForm
from membership.models import CustomUser
from .models import RailsBetaUser, VirtualMachineType
from .forms import HostingUserSignupForm, HostingUserLoginForm
from .mixins import ProcessVMSelectionMixin
class VMPricingView(View):
template_name = "hosting/pricing.html"
def get(self, request, *args, **kwargs):
return render(request, self.template_name, request)
class DjangoHostingView(View):
class DjangoHostingView(ProcessVMSelectionMixin, View):
template_name = "hosting/django.html"
def get_context_data(self, **kwargs):
context = {}
context["hosting"] = "django"
context["hosting_long"] = "Django"
context["domain"] = "django-hosting.ch"
context["google_analytics"] = "UA-62285904-6"
context["email"] = "info@django-hosting.ch"
context["vm_types"] = VirtualMachineType.get_serialized_vm_types()
context = {
'hosting': "django",
'hosting_long': "Django",
'domain': "django-hosting.ch",
'google_analytics': "UA-62285904-6",
'email': "info@django-hosting.ch",
'vm_types': VirtualMachineType.get_serialized_vm_types(),
}
return context
def get(self, request, *args, **kwargs):
@ -36,17 +36,18 @@ class DjangoHostingView(View):
return render(request, self.template_name, context)
class RailsHostingView(View):
class RailsHostingView(ProcessVMSelectionMixin, View):
template_name = "hosting/rails.html"
def get_context_data(self, **kwargs):
context = {}
context["hosting"] = "rails"
context["hosting_long"] = "Ruby On Rails"
context["domain"] = "rails-hosting.ch"
context["google_analytics"] = "UA-62285904-5"
context["email"] = "info@rails-hosting.ch"
context["vm_types"] = VirtualMachineType.get_serialized_vm_types()
context = {
'hosting': "rails",
'hosting_long': "Ruby On Rails",
'domain': "rails-hosting.ch",
'google_analytics': "UA-62285904-5",
'email': "info@rails-hosting.ch",
'vm_types': VirtualMachineType.get_serialized_vm_types(),
}
return context
def get(self, request, *args, **kwargs):
@ -54,17 +55,18 @@ class RailsHostingView(View):
return render(request, self.template_name, context)
class NodeJSHostingView(View):
class NodeJSHostingView(ProcessVMSelectionMixin, View):
template_name = "hosting/nodejs.html"
def get_context_data(self, **kwargs):
context = {}
context["hosting"] = "nodejs"
context["hosting_long"] = "NodeJS"
context["domain"] = "node-hosting.ch"
context["google_analytics"] = "UA-62285904-7"
context["email"] = "info@node-hosting.ch"
context["vm_types"] = VirtualMachineType.get_serialized_vm_types()
context = {
'hosting': "nodejs",
'hosting_long': "NodeJS",
'domain': "node-hosting.ch",
'google_analytics': "UA-62285904-7",
'email': "info@node-hosting.ch",
'vm_types': VirtualMachineType.get_serialized_vm_types(),
}
return context
def get(self, request, *args, **kwargs):
@ -76,13 +78,14 @@ class IndexView(View):
template_name = "hosting/index.html"
def get_context_data(self, **kwargs):
context = {}
context["hosting"] = "nodejs"
context["hosting_long"] = "NodeJS"
context["domain"] = "node-hosting.ch"
context["google_analytics"] = "UA-62285904-7"
context["email"] = "info@node-hosting.ch"
context["vm_types"] = VirtualMachineType.get_serialized_vm_types()
context = {
'hosting': "nodejs",
'hosting_long': "NodeJS",
'domain': "node-hosting.ch",
'google_analytics': "UA-62285904-7",
'email': "info@node-hosting.ch",
'vm_types': VirtualMachineType.get_serialized_vm_types(),
}
return context
def get(self, request, *args, **kwargs):
@ -124,6 +127,22 @@ class SignupView(CreateView):
return HttpResponseRedirect(self.get_success_url())
class PaymentVMView(FormView):
template_name = 'hosting/payment.html'
form_class = PaymentForm
def get_context_data(self, **kwargs):
context = super(PaymentVMView, self).get_context_data(**kwargs)
context.update({
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
})
return context
# moodel = CustomUser
# def get(self, request, *args, **kwargs):
# return render(request, self.template_name, self.context)
# class RailsBetaUserForm(ModelForm):
# required_css_class = 'form-control'