Merge branch 'feature/vm_pricing' into develop
This commit is contained in:
		
				commit
				
					
						36de71441a
					
				
			
		
					 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue