Merge branch 'feature/vm_pricing' into develop
This commit is contained in:
commit
9415041f64
21 changed files with 661 additions and 170 deletions
|
@ -446,8 +446,8 @@ AUTH_USER_MODEL = 'membership.CustomUser'
|
|||
|
||||
# PAYMENT
|
||||
|
||||
STRIPE_API_PUBLIC_KEY = 'pk_test_ZRg6P8g5ybiHE6l2RW5pSaYV' # used in frontend to call from user browser
|
||||
STRIPE_API_PRIVATE_KEY = 'sk_test_uIPMdgXoRGydrcD7fkwcn7dj' # used in backend payment
|
||||
STRIPE_API_PUBLIC_KEY = 'pk_test_QqBZ50Am8KOxaAlOxbcm9Psl' # used in frontend to call from user browser
|
||||
STRIPE_API_PRIVATE_KEY = 'sk_test_dqAmbKAij12QCGfkYZ3poGt2' # used in backend payment
|
||||
STRIPE_DESCRIPTION_ON_PAYMENT = "Payment for ungleich GmbH services"
|
||||
|
||||
# EMAIL MESSAGES
|
||||
|
|
9
hosting/managers.py
Normal file
9
hosting/managers.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class VMPlansManager(models.Manager):
|
||||
|
||||
def active(self, user, **kwargs):
|
||||
return self.prefetch_related('hosting_orders__customer__user').\
|
||||
filter(hosting_orders__customer__user=user, hosting_orders__approved=True, **kwargs)\
|
||||
.distinct()
|
27
hosting/migrations/0012_auto_20160501_1850.py
Normal file
27
hosting/migrations/0012_auto_20160501_1850.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2016-05-01 18:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0011_auto_20160426_0555'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hostingorder',
|
||||
name='cc_brand',
|
||||
field=models.CharField(default='Visa', max_length=10),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hostingorder',
|
||||
name='last4',
|
||||
field=models.CharField(default=1111, max_length=4),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
21
hosting/migrations/0013_auto_20160505_0302.py
Normal file
21
hosting/migrations/0013_auto_20160505_0302.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2016-05-05 03:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0012_auto_20160501_1850'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='hostingorder',
|
||||
name='VMPlan',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hosting.VirtualMachinePlan'),
|
||||
),
|
||||
]
|
21
hosting/migrations/0014_auto_20160505_0541.py
Normal file
21
hosting/migrations/0014_auto_20160505_0541.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2016-05-05 05:41
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0013_auto_20160505_0302'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='hostingorder',
|
||||
name='VMPlan',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hosting_orders', to='hosting.VirtualMachinePlan'),
|
||||
),
|
||||
]
|
|
@ -2,10 +2,13 @@ import json
|
|||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.functional import cached_property
|
||||
from django.core import serializers
|
||||
from membership.models import StripeCustomer
|
||||
from utils.models import BillingAddress
|
||||
|
||||
from .managers import VMPlansManager
|
||||
|
||||
|
||||
class RailsBetaUser(models.Model):
|
||||
email = models.EmailField(unique=True)
|
||||
|
@ -81,6 +84,17 @@ class VirtualMachinePlan(models.Model):
|
|||
vm_type = models.ForeignKey(VirtualMachineType)
|
||||
price = models.FloatField()
|
||||
|
||||
objects = VMPlansManager()
|
||||
|
||||
@cached_property
|
||||
def hosting_company_name(self):
|
||||
return self.vm_type.get_hosting_company_display()
|
||||
|
||||
@cached_property
|
||||
def name(self):
|
||||
name = 'vm-%s' % self.id
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def create(cls, data, user):
|
||||
instance = cls.objects.create(**data)
|
||||
|
@ -88,13 +102,23 @@ class VirtualMachinePlan(models.Model):
|
|||
|
||||
|
||||
class HostingOrder(models.Model):
|
||||
VMPlan = models.OneToOneField(VirtualMachinePlan)
|
||||
|
||||
ORDER_APPROVED_STATUS = 'Approved'
|
||||
ORDER_DECLINED_STATUS = 'Declined'
|
||||
|
||||
VMPlan = models.ForeignKey(VirtualMachinePlan, related_name='hosting_orders')
|
||||
customer = models.ForeignKey(StripeCustomer)
|
||||
billing_address = models.ForeignKey(BillingAddress)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
approved = models.BooleanField(default=False)
|
||||
last4 = models.CharField(max_length=4)
|
||||
cc_brand = models.CharField(max_length=10)
|
||||
stripe_charge_id = models.CharField(max_length=100, null=True)
|
||||
|
||||
@cached_property
|
||||
def status(self):
|
||||
return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS
|
||||
|
||||
@classmethod
|
||||
def create(cls, VMPlan=None, customer=None, billing_address=None):
|
||||
instance = cls.objects.create(VMPlan=VMPlan, customer=customer,
|
||||
|
@ -107,6 +131,8 @@ class HostingOrder(models.Model):
|
|||
|
||||
def set_stripe_charge(self, stripe_charge):
|
||||
self.stripe_charge_id = stripe_charge.id
|
||||
self.last4 = stripe_charge.source.last4
|
||||
self.cc_brand = stripe_charge.source.brand
|
||||
self.save()
|
||||
|
||||
|
||||
|
|
20
hosting/static/hosting/css/commons.css
Normal file
20
hosting/static/hosting/css/commons.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.dashboard-container {
|
||||
padding-top:5%; padding-bottom: 11%;
|
||||
}
|
||||
|
||||
.borderless td {
|
||||
border: none !important;
|
||||
}
|
||||
.borderless thead {
|
||||
}
|
||||
|
||||
.borderless tbody:before {
|
||||
content: "-";
|
||||
display: block;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.inline-headers h3, .inline-headers h4 {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
}
|
17
hosting/static/hosting/css/order.css
Normal file
17
hosting/static/hosting/css/order.css
Normal file
|
@ -0,0 +1,17 @@
|
|||
.order-detail-container {padding-top:5%; padding-bottom: 11%;}
|
||||
|
||||
.order-detail-container .invoice-title h2, .invoice-title h3 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.order-detail-container .table > tbody > tr > .no-line {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.order-detail-container .table > thead > tr > .no-line {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.order-detail-container .table > tbody > tr > .thick-line {
|
||||
border-top: 2px solid;
|
||||
}
|
5
hosting/static/hosting/css/orders.css
Normal file
5
hosting/static/hosting/css/orders.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.orders-container {padding-top:5%; padding-bottom: 11%;}
|
||||
|
||||
.orders-container .table > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
42
hosting/static/hosting/css/virtual-machine.css
Normal file
42
hosting/static/hosting/css/virtual-machine.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
.virtual-machine-container .tabs-left, .virtual-machine-container .tabs-right {
|
||||
border-bottom: none;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.virtual-machine-container .tabs-left {
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
.virtual-machine-container .tabs-right {
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
.virtual-machine-container .tabs-left>li, .virtual-machine-container .tabs-right>li {
|
||||
float: none;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.virtual-machine-container .tabs-left>li {
|
||||
margin-right: -1px;
|
||||
}
|
||||
.virtual-machine-container .tabs-right>li {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.virtual-machine-container .tabs-left>li.active>a,
|
||||
.virtual-machine-container .tabs-left>li.active>a:hover,
|
||||
.virtual-machine-container .tabs-left>li.active>a:focus {
|
||||
border-bottom-color: #ddd;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.virtual-machine-container .tabs-right>li.active>a,
|
||||
.virtual-machine-container .tabs-right>li.active>a:hover,
|
||||
.virtual-machine-container .tabs-right>li.active>a:focus {
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
.virtual-machine-container .tabs-left>li>a {
|
||||
border-radius: 4px 0 0 4px;
|
||||
margin-right: 0;
|
||||
display:block;
|
||||
}
|
||||
.virtual-machine-container .tabs-right>li>a {
|
||||
border-radius: 0 4px 4px 0;
|
||||
margin-right: 0;
|
||||
}
|
|
@ -18,6 +18,10 @@
|
|||
<!-- Custom CSS -->
|
||||
<link href="{% static 'hosting/css/landing-page.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'hosting/css/payment.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'hosting/css/order.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'hosting/css/orders.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'hosting/css/commons.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'hosting/css/virtual-machine.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href='http://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
|
||||
|
@ -49,11 +53,33 @@
|
|||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand topnav" href="#"><img src="img/logo_black.svg"></a>
|
||||
<a class="navbar-brand topnav" href="#"><img src="{% static 'hosting/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">
|
||||
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url 'hosting:virtual_machines' %}">
|
||||
<i class="fa fa-server" aria-hidden="true"></i> My Virtual Machines
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'hosting:orders' %}">
|
||||
<i class="fa fa-credit-card"></i> My Orders
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" role="button" data-toggle="dropdown" href="#">
|
||||
<i class="glyphicon glyphicon-user"></i> {{request.user.name}} <span class="caret"></span></a>
|
||||
<ul id="g-account-menu" class="dropdown-menu" role="menu">
|
||||
<li><a href="{% url 'hosting:logout' %}"><i class="glyphicon glyphicon-lock"></i> Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a href="{{ request.META.HTTP_REFERER }}#how">How it works</a>
|
||||
</li>
|
||||
|
@ -69,9 +95,8 @@
|
|||
<li>
|
||||
<a href="{{ request.META.HTTP_REFERER }}#contact">Contact</a>
|
||||
</li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url 'hosting:logout' %}"> Logout</a>
|
||||
<a href="{% url 'hosting:login' %}?next={{request.current_path}}">Login</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 %}
|
||||
{% block content %}
|
||||
|
||||
<style type="text/css">
|
||||
.invoice-title h2, .invoice-title h3 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.table > tbody > tr > .no-line {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.table > thead > tr > .no-line {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table > tbody > tr > .thick-line {
|
||||
border-top: 2px solid;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<div class="container payment-container">
|
||||
<div class="row">
|
||||
<div class="col-xs-8 col-xs-offset-2">
|
||||
<div class="invoice-title">
|
||||
<h2>Invoice</h2><h3 class="pull-right">Order # {{order.id}}</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<address>
|
||||
<h3><b>Billed To:</b></h3>
|
||||
John Smith<br>
|
||||
1234 Main<br>
|
||||
Apt. 4B<br>
|
||||
Springfield, ST 54321
|
||||
</address>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<address>
|
||||
<strong>Order Date:</strong><br>
|
||||
{{order.created_at}}<br><br>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<address>
|
||||
<strong>Payment Method:</strong><br>
|
||||
{{brand}} ending **** {{last4}}<br>
|
||||
{{user.email}}
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<h3><b>Order summary</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.disk_size}} GiB</span></p>
|
||||
<hr>
|
||||
<h4>Total<p class="pull-right"><b>{{request.session.vm_specs.final_price}} CHF</b></p></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%endblock%}
|
64
hosting/templates/hosting/order_detail.html
Normal file
64
hosting/templates/hosting/order_detail.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container order-detail-container">
|
||||
<div class="row">
|
||||
<div class="col-xs-8 col-xs-offset-2">
|
||||
<div class="invoice-title">
|
||||
<h2>Invoice</h2><h3 class="pull-right">Order # {{object.id}}</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<address>
|
||||
<h3><b>Billed To:</b></h3>
|
||||
{{user.name}}<br>
|
||||
{{object.billing_address.street_address}},{{order.billing_address.postal_code}}<br>
|
||||
{{object.billing_address.city}}, {{object.billing_address.country}}.
|
||||
</address>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<address>
|
||||
<strong>Order Date:</strong><br>
|
||||
{{object.created_at}}<br><br>
|
||||
<strong>Status:</strong><br>
|
||||
<strong class="{% if object.status == 'Approved' %}text-success
|
||||
{%else%} text-danger
|
||||
{% endif %}">{{object.status}}</strong>
|
||||
<br><br>
|
||||
</address>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<address>
|
||||
<strong>Payment Method:</strong><br>
|
||||
{{object.cc_brand}} ending **** {{object.last4}}<br>
|
||||
{{user.email}}
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<h3><b>Order summary</b></h3>
|
||||
<hr>
|
||||
<div class="content">
|
||||
<p><b>Type</b> <span class="pull-right">{{object.VMPlan.hosting_company_name}}</span></p>
|
||||
<hr>
|
||||
<p><b>Cores</b> <span class="pull-right">{{object.VMPlan.cores}}</span></p>
|
||||
<hr>
|
||||
<p><b>Memory</b> <span class="pull-right">{{object.VMPlan.memory}} GiB</span></p>
|
||||
<hr>
|
||||
<p><b>Disk space</b> <span class="pull-right">{{object.VMPlan.disk_size}} GiB</span></p>
|
||||
<hr>
|
||||
<h4>Total<p class="pull-right"><b>{{object.VMPlan.price}} CHF</b></p></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%endblock%}
|
|
@ -1,31 +1,59 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 %}
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
<div class="container payment-container">
|
||||
<div class="container orders-container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<table class="table">
|
||||
<caption>My orders.</caption>
|
||||
<table class="table borderless table-hover">
|
||||
<h3><i class="fa fa-credit-card"></i> My Orders</h3>
|
||||
<br/>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<th scope="row">{{order.id}}</th>
|
||||
<td scope="row">{{order.id}}</td>
|
||||
<td>{{order.created_at}}</td>
|
||||
<td>{{order.VMPlan.price}}</td>
|
||||
<td>{{order.approved}}</td>
|
||||
<td>{{order.VMPlan.price}} CHF</td>
|
||||
<td>{% if order.approved %}
|
||||
<span class="text-success strong">Approved</span>
|
||||
{% else%}
|
||||
<span class="text-danger strong">Declined</span>
|
||||
{% endif%}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-default"><a href="{% url 'hosting:orders' order.id %}">View Detail</a></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="pagination">
|
||||
<span class="page-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
<span class="page-current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
{% if page_obj.has_next %}
|
||||
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">next</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
155
hosting/templates/hosting/virtual_machine_detail.html
Normal file
155
hosting/templates/hosting/virtual_machine_detail.html
Normal file
|
@ -0,0 +1,155 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 %}
|
||||
{% block content %}
|
||||
<div>
|
||||
<div class="container virtual-machine-container dashboard-container ">
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-md-offset-2">
|
||||
<div class="col-sm-12">
|
||||
<h3><i class="fa fa-cloud" aria-hidden="true"></i> {{virtual_machine.name}}</h3>
|
||||
<hr/>
|
||||
<div class="col-md-3"> <!-- required for floating -->
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs tabs-left sideways">
|
||||
<li class="active">
|
||||
<a href="#settings-v" data-toggle="tab">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#billing-v" data-toggle="tab">
|
||||
<i class="fa fa-money" aria-hidden="true"></i>
|
||||
Billing
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#orders-v" data-toggle="tab">
|
||||
<i class="fa fa-credit-card"></i>
|
||||
Orders
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#status-v" data-toggle="tab">
|
||||
<i class="fa fa-signal" aria-hidden="true"></i> Status
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="settings-v">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{{virtual_machine.hosting_company_name}}</h3>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="well text-center">
|
||||
<i class="fa fa-cubes" aria-hidden="true"></i> Cores <br/>
|
||||
<span class="label label-success">{{virtual_machine.cores}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="well text-center">
|
||||
<i class="fa fa-tachometer" aria-hidden="true"></i> Memory <br/>
|
||||
<span class="label label-success">{{virtual_machine.memory}} GiB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="well text-center">
|
||||
<i class="fa fa-hdd-o" aria-hidden="true"></i> Disk <br/>
|
||||
<span class="label label-success">{{virtual_machine.disk_size}} GiB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div><!--/row-->
|
||||
</div><!--/col-12-->
|
||||
</div><!--/row-->
|
||||
|
||||
|
||||
</div>
|
||||
<div class="tab-pane" id="billing-v">
|
||||
|
||||
<div class="row ">
|
||||
<div class="col-md-12 inline-headers">
|
||||
<h3>Current pricing</h3>
|
||||
<span class="h3 pull-right"><strong>{{virtual_machine.price|floatformat}} CHF</strong>/mo</span>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="orders-v">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table borderless table-hover">
|
||||
<h3>Orders</h3>
|
||||
<br/>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in virtual_machine.hosting_orders.all %}
|
||||
<tr>
|
||||
<td scope="row">{{order.id}}</td>
|
||||
<td>{{order.created_at}}</td>
|
||||
<td>{{order.VMPlan.price}} CHF</td>
|
||||
<td>{% if order.approved %}
|
||||
<span class="text-success strong">Approved</span>
|
||||
{% else%}
|
||||
<span class="text-danger strong">Declined</span>
|
||||
{% endif%}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-default"><a href="{% url 'hosting:orders' order.id %}">View Detail</a></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!--/col-12-->
|
||||
</div><!--/row-->
|
||||
</div>
|
||||
<div class="tab-pane" id="status-v">
|
||||
|
||||
<div class="row ">
|
||||
<div class="col-md-12 inline-headers">
|
||||
<h3>Current status</h3>
|
||||
<span class="h3 pull-right label label-success"><strong>Online</strong></span>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{%endblock%}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
56
hosting/templates/hosting/virtual_machines.html
Normal file
56
hosting/templates/hosting/virtual_machines.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 %}
|
||||
{% block content %}
|
||||
<div>
|
||||
<div class="container dashboard-container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<table class="table borderless table-hover">
|
||||
<h3><i class="fa fa-server" aria-hidden="true"></i> Virtual Machines</h3>
|
||||
<br/>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Type</th>
|
||||
<th>Amount</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vm in vms %}
|
||||
<tr>
|
||||
<td scope="row">{{vm.name}}</td>
|
||||
<td>{{vm.hosting_company_name}}</td>
|
||||
<td>{{vm.price}} CHF</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-default"><a href="{% url 'hosting:virtual_machines' vm.id %}">View Detail</a></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="pagination">
|
||||
<span class="page-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">previous</a>
|
||||
{% endif %}
|
||||
<span class="page-current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
{% if page_obj.has_next %}
|
||||
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">next</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{%endblock%}
|
72
hosting/test_views.py
Normal file
72
hosting/test_views.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import resolve
|
||||
from .models import VirtualMachineType
|
||||
from .views import DjangoHostingView, RailsHostingView, NodeJSHostingView
|
||||
|
||||
|
||||
class ProcessVMSelectionTestMixin(object):
|
||||
|
||||
def url_resolve_to_view_correctly(self):
|
||||
found = resolve(self.url)
|
||||
self.assertEqual(found.func.__name__, self.view.__name__)
|
||||
|
||||
def test_get(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.view.get_context_data(), self.expected_context)
|
||||
self.assertEqual(response.context['hosting'], self.expected_context['hosting'])
|
||||
self.assertTemplateUsed(response, self.expected_template)
|
||||
|
||||
def test_anonymous_post(self):
|
||||
response = self.client.post(self.url)
|
||||
self.assertRedirects(response, expected_url=reverse('hosting:login'),
|
||||
status_code=302, target_status_code=200)
|
||||
|
||||
|
||||
class DjangoHostingViewTest(TestCase, ProcessVMSelectionTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse('django.hosting')
|
||||
self.view = DjangoHostingView()
|
||||
self.expected_template = 'hosting/django.html'
|
||||
self.expected_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(),
|
||||
}
|
||||
|
||||
|
||||
class RailsHostingViewTest(TestCase, ProcessVMSelectionTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse('rails.hosting')
|
||||
self.view = RailsHostingView()
|
||||
self.expected_template = 'hosting/rails.html'
|
||||
self.expected_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(),
|
||||
}
|
||||
|
||||
|
||||
class NodeJSHostingViewTest(TestCase, ProcessVMSelectionTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse('node.hosting')
|
||||
self.view = NodeJSHostingView()
|
||||
self.expected_template = 'hosting/nodejs.html'
|
||||
self.expected_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(),
|
||||
}
|
|
@ -2,7 +2,8 @@ from django.conf.urls import url
|
|||
|
||||
from .views import DjangoHostingView, RailsHostingView, PaymentVMView, \
|
||||
NodeJSHostingView, LoginView, SignupView, IndexView, \
|
||||
InvoiceVMView, OrdersHostingView
|
||||
OrdersHostingListView, OrdersHostingDetailView, VirtualMachinesPlanListView,\
|
||||
VirtualMachineDetailListView
|
||||
|
||||
urlpatterns = [
|
||||
# url(r'pricing/?$', VMPricingView.as_view(), name='pricing'),
|
||||
|
@ -11,8 +12,11 @@ urlpatterns = [
|
|||
url(r'nodejs/?$', NodeJSHostingView.as_view(), name='nodejshosting'),
|
||||
url(r'rails/?$', RailsHostingView.as_view(), name='railshosting'),
|
||||
url(r'payment/?$', PaymentVMView.as_view(), name='payment'),
|
||||
url(r'invoice/?$', InvoiceVMView.as_view(), name='invoice'),
|
||||
url(r'orders/?$', OrdersHostingView.as_view(), name='orders'),
|
||||
url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'),
|
||||
url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'),
|
||||
url(r'my-virtual-machines/?$', VirtualMachinesPlanListView.as_view(), name='virtual_machines'),
|
||||
url(r'my-virtual-machines/(?P<pk>\d+)/?$', VirtualMachineDetailListView.as_view(),
|
||||
name='virtual_machines'),
|
||||
url(r'login/?$', LoginView.as_view(), name='login'),
|
||||
url(r'signup/?$', SignupView.as_view(), name='signup'),
|
||||
url(r'^logout/?$', 'django.contrib.auth.views.logout',
|
||||
|
|
|
@ -2,16 +2,12 @@
|
|||
from django.shortcuts import get_object_or_404, render
|
||||
from django.core.urlresolvers import reverse_lazy, reverse
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from django.views.generic import View, CreateView, FormView
|
||||
from django.shortcuts import redirect
|
||||
from django.views.generic import View, CreateView, FormView, ListView, DetailView
|
||||
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, StripeCustomer
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from utils.forms import BillingAddressForm
|
||||
|
@ -21,7 +17,6 @@ from .forms import HostingUserSignupForm, HostingUserLoginForm
|
|||
from .mixins import ProcessVMSelectionMixin
|
||||
|
||||
|
||||
|
||||
class DjangoHostingView(ProcessVMSelectionMixin, View):
|
||||
template_name = "hosting/django.html"
|
||||
|
||||
|
@ -106,7 +101,7 @@ class IndexView(View):
|
|||
|
||||
class LoginView(FormView):
|
||||
template_name = 'hosting/login.html'
|
||||
success_url = reverse_lazy('hosting:login')
|
||||
success_url = reverse_lazy('hosting:orders')
|
||||
form_class = HostingUserLoginForm
|
||||
moodel = CustomUser
|
||||
|
||||
|
@ -217,60 +212,45 @@ class PaymentVMView(FormView):
|
|||
'order': order.id,
|
||||
'billing_address': billing_address.id
|
||||
})
|
||||
return HttpResponseRedirect(reverse('hosting:invoice'))
|
||||
return HttpResponseRedirect(reverse('hosting:orders', kwargs={'pk': order.id}))
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class InvoiceVMView(LoginRequiredMixin, View):
|
||||
template_name = "hosting/invoice.html"
|
||||
class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
||||
template_name = "hosting/order_detail.html"
|
||||
login_url = reverse_lazy('hosting:login')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
charge = self.request.session.get('charge')
|
||||
order_id = self.request.session.get('order')
|
||||
billing_address_id = self.request.session.get('billing_address')
|
||||
last4 = charge.get('source').get('last4')
|
||||
brand = charge.get('source').get('brand')
|
||||
|
||||
order = get_object_or_404(HostingOrder, pk=order_id)
|
||||
billing_address = get_object_or_404(BillingAddress, pk=billing_address_id)
|
||||
|
||||
if not charge:
|
||||
return
|
||||
|
||||
context = {
|
||||
'last4': last4,
|
||||
'brand': brand,
|
||||
'order': order,
|
||||
'billing_address': billing_address,
|
||||
}
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
context = self.get_context_data()
|
||||
|
||||
return render(request, self.template_name, context)
|
||||
model = HostingOrder
|
||||
|
||||
|
||||
class OrdersHostingView(LoginRequiredMixin, View):
|
||||
class OrdersHostingListView(LoginRequiredMixin, ListView):
|
||||
template_name = "hosting/orders.html"
|
||||
login_url = reverse_lazy('hosting:login')
|
||||
context_object_name = "orders"
|
||||
model = HostingOrder
|
||||
paginate_by = 10
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
orders = HostingOrder.objects.filter(customer__user=user)
|
||||
context = {
|
||||
'orders':orders
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
context = self.get_context_data()
|
||||
|
||||
return render(request, self.template_name, context)
|
||||
self.queryset = HostingOrder.objects.filter(customer__user=user)
|
||||
return super(OrdersHostingListView, self).get_queryset()
|
||||
|
||||
|
||||
class VirtualMachinesPlanListView(LoginRequiredMixin, ListView):
|
||||
template_name = "hosting/virtual_machines.html"
|
||||
login_url = reverse_lazy('hosting:login')
|
||||
context_object_name = "vms"
|
||||
model = VirtualMachinePlan
|
||||
paginate_by = 10
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
self.queryset = VirtualMachinePlan.objects.active(user)
|
||||
return super(VirtualMachinesPlanListView, self).get_queryset()
|
||||
|
||||
|
||||
class VirtualMachineDetailListView(LoginRequiredMixin, DetailView):
|
||||
template_name = "hosting/virtual_machine_detail.html"
|
||||
login_url = reverse_lazy('hosting:login')
|
||||
model = VirtualMachinePlan
|
||||
context_object_name = "virtual_machine"
|
||||
|
|
|
@ -131,7 +131,7 @@ class StripeCustomer(models.Model):
|
|||
Check if there is a registered stripe customer with that email
|
||||
or create a new one
|
||||
"""
|
||||
|
||||
stripe_customer = None
|
||||
try:
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_customer = cls.objects.get(user__email=email)
|
||||
|
|
|
@ -52,8 +52,6 @@ def handleStripeError(f):
|
|||
return handleProblems
|
||||
|
||||
|
||||
|
||||
|
||||
class StripeUtils(object):
|
||||
CURRENCY = 'chf'
|
||||
INTERVAL = 'month'
|
||||
|
@ -71,7 +69,7 @@ class StripeUtils(object):
|
|||
customer = stripe.Customer.retrieve(id)
|
||||
except stripe.InvalidRequestError:
|
||||
customer = self.create_customer(token, user.email)
|
||||
user.stripecustomer.stripe_id = customer.get('id')
|
||||
user.stripecustomer.stripe_id = customer.get('response_object').get('id')
|
||||
user.stripecustomer.save()
|
||||
return customer
|
||||
|
||||
|
|
Loading…
Reference in a new issue