Merge branch 'master' into 5151/gdpr_modal

This commit is contained in:
PCoder 2018-10-25 22:05:24 +02:00
commit 1feacc1770
39 changed files with 1391 additions and 390 deletions

View File

@ -1,4 +1,25 @@
Next: Next:
* bugfix: Use correct version of django-multisite (MR #676)
2.4.1: 2018-10-18
* bugfix: Update pycryptodome module from 3.4 to 3.6.6 (PR #674)
2.4: 2018-10-18
* #5681: [hosting,dcl] Allow admin to lower minimum RAM to 512 MB (PR #672)
2.3.1: 2018-10-17
* bugfix: [hosting, dcl] Show VAT percent rounded to 2 decimal places in the order confirmation page (PR #673)
2.3: 2018-10-08
* #5690: Generic payment page - allow admin to add a onetime/monthly product and the frontend for user to pay for this product (PR #666)
2.2.2: 2018-09-28
* #5721: Set calculator OS list in alphabetical order and set `Devuan Ascii` as the default (PR #668)
* bugfix: Fix some typos and correct DE translations (PR #667)
2.2.1: 2018-09-25
* feature: Change DCLNavbarPlugin to show login option only if set (PR #665)
* bugfix: Log opennebula errors and send proper message when vm terminate is not completed in the stipulated time (PR #648)
2.2: 2018-09-06
* bugfix: Include price in the Stripe plan name to make it distinct and to correct pricing since version 1.9
2.1.2: 2018-08-30
* bugfix: [blog, comic] Set blog rss feed for all blog templates
2.1.1: 2018-08-24
* #5487: [hosting] Add explicit warning message for teminating VM (PR #656)
* bugfix: [dg] Send email to admin on dg subscription and increase cc_brand field to 128 characters (PR #652) * bugfix: [dg] Send email to admin on dg subscription and increase cc_brand field to 128 characters (PR #652)
* #5458: [admin] Make hostingorder more readable (PR #657) * #5458: [admin] Make hostingorder more readable (PR #657)
* bugfix: [CMS templates] Set description meta field of ungleich template (was missing before) and set ungleich glarus ag uniformly as author of various CMS pages (PR #653) * bugfix: [CMS templates] Set description meta field of ungleich template (was missing before) and set ungleich glarus ag uniformly as author of various CMS pages (PR #653)

View File

@ -180,6 +180,10 @@ class DCLNavbarPluginModel(CMSPlugin):
default=True, default=True,
help_text='Select to include the language selection dropdown.' help_text='Select to include the language selection dropdown.'
) )
show_login_option = models.BooleanField(
default=True,
help_text='Uncheck this if you do not want to show login/dashboard.'
)
def get_logo_dark(self): def get_logo_dark(self):
# used only if atleast one logo exists # used only if atleast one logo exists
@ -350,3 +354,11 @@ class DCLCalculatorPluginModel(CMSPlugin):
"in the backend to be automatically listed in this " "in the backend to be automatically listed in this "
"calculator instance." "calculator instance."
) )
default_selected_template = models.CharField(
default="Devuan Ascii",
null=True,
max_length=128,
help_text="Write the name of the template that you need selected as"
" default when the calculator loads"
)
enable_512mb_ram = models.BooleanField(default=False)

View File

@ -9,6 +9,7 @@ from .cms_models import (
DCLSectionPromoPluginModel, DCLCalculatorPluginModel DCLSectionPromoPluginModel, DCLCalculatorPluginModel
) )
from .models import VMTemplate from .models import VMTemplate
from datacenterlight.utils import clear_all_session_vars
@plugin_pool.register_plugin @plugin_pool.register_plugin
@ -85,6 +86,7 @@ class DCLCalculatorPlugin(CMSPluginBase):
require_parent = True require_parent = True
def render(self, context, instance, placeholder): def render(self, context, instance, placeholder):
clear_all_session_vars(context['request'])
context = super(DCLCalculatorPlugin, self).render( context = super(DCLCalculatorPlugin, self).render(
context, instance, placeholder context, instance, placeholder
) )
@ -92,11 +94,13 @@ class DCLCalculatorPlugin(CMSPluginBase):
if ids: if ids:
context['templates'] = VMTemplate.objects.filter( context['templates'] = VMTemplate.objects.filter(
vm_type=instance.vm_type vm_type=instance.vm_type
).filter(opennebula_vm_template_id__in=ids) ).filter(opennebula_vm_template_id__in=ids).order_by('name')
else: else:
context['templates'] = VMTemplate.objects.filter( context['templates'] = VMTemplate.objects.filter(
vm_type=instance.vm_type vm_type=instance.vm_type
) ).order_by('name')
context['instance'] = instance
context['min_ram'] = 0.5 if instance.enable_512mb_ram else 1
return context return context

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-05 23:11+0000\n" "POT-Creation-Date: 2018-09-26 20:44+0000\n"
"PO-Revision-Date: 2018-03-30 23:22+0000\n" "PO-Revision-Date: 2018-03-30 23:22+0000\n"
"Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n" "Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -293,6 +293,9 @@ msgstr "Registrieren"
msgid "Billing Address" msgid "Billing Address"
msgstr "Rechnungsadresse" msgstr "Rechnungsadresse"
msgid "Make a payment"
msgstr ""
msgid "Your Order" msgid "Your Order"
msgstr "Deine Bestellung" msgstr "Deine Bestellung"
@ -336,9 +339,9 @@ msgid ""
"database." "database."
msgstr "" msgstr ""
"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine " "Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine "
"Kreditkartendetails unten an. Die Bezahlung wird über " "Kreditkartendetails unten an. Die Bezahlung wird über <a href=\"https://"
"<a href=\"https://stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. " "stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. Wir speichern Deine "
"Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank." "Kreditkartendetails nicht in unserer Datenbank."
msgid "" msgid ""
"Please fill in your credit card information below. We are using <a href=" "Please fill in your credit card information below. We are using <a href="
@ -395,12 +398,35 @@ msgstr "Bestellungsübersicht"
msgid "Product" msgid "Product"
msgstr "Produkt" msgstr "Produkt"
msgid "Amount"
msgstr ""
msgid "Description"
msgstr ""
msgid "Recurring"
msgstr ""
msgid "Subtotal" msgid "Subtotal"
msgstr "Zwischensumme" msgstr "Zwischensumme"
msgid "VAT" msgid "VAT"
msgstr "Mehrwertsteuer" msgstr "Mehrwertsteuer"
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(total_price)s CHF/month"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(vm_total_price)s CHF pro Monat belastet"
msgid ""
"By clicking \"Place order\" this payment will charge your credit card "
"account with a one time amount of %(total_price)s CHF"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(vm_total_price)s CHF pro Monat belastet"
#, python-format #, python-format
msgid "" msgid ""
"By clicking \"Place order\" this plan will charge your credit card account " "By clicking \"Place order\" this plan will charge your credit card account "
@ -541,8 +567,33 @@ msgstr ""
#, python-brace-format #, python-brace-format
msgid "An error occurred while associating the card. Details: {details}" msgid "An error occurred while associating the card. Details: {details}"
msgstr "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: " msgstr ""
"{details}" "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
msgid "Confirmation of your payment"
msgstr ""
msgid " This is a monthly recurring plan."
msgstr ""
#, python-brace-format
msgid ""
"Hi {name},\n"
"\n"
"thank you for your order!\n"
"We have just received a payment of CHF {amount:.2f} from you.{recurring}\n"
"\n"
"Cheers,\n"
"Your Data Center Light team"
msgstr ""
msgid "Thank you for the payment."
msgstr "Danke für Deine Bestellung."
msgid ""
"You will soon receive a confirmation email of the payment. You can always "
"contact us at info@ungleich.ch for any question that you may have."
msgstr ""
msgid "Thank you for the order." msgid "Thank you for the order."
msgstr "Danke für Deine Bestellung." msgstr "Danke für Deine Bestellung."

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2018-09-25 20:27
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datacenterlight', '0024_dclcalculatorpluginmodel_vm_templates_to_show'),
]
operations = [
migrations.AddField(
model_name='dclnavbarpluginmodel',
name='show_login_option',
field=models.BooleanField(default=True, help_text='Uncheck this if you do not want to show login/dashboard.'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2018-09-27 20:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datacenterlight', '0025_dclnavbarpluginmodel_show_login_option'),
]
operations = [
migrations.AddField(
model_name='dclcalculatorpluginmodel',
name='default_selected_template',
field=models.CharField(default='Devuan Ascii', help_text='Write the name of the template that you need selected as default when the calculator loads', max_length=128, null=True),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2018-09-29 05:36
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datacenterlight', '0026_dclcalculatorpluginmodel_default_selected_template'),
]
operations = [
migrations.AddField(
model_name='dclcalculatorpluginmodel',
name='enable_512mb_ram',
field=models.BooleanField(default=False),
),
]

View File

@ -179,4 +179,10 @@ footer .dcl-link-separator::before {
.new-card-button-margin button{ .new-card-button-margin button{
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
} }
.input-no-border {
border: none !important;
background: transparent !important;
resize: none;
}

View File

@ -5,6 +5,10 @@
/* --------------------------------------------- /* ---------------------------------------------
Scripts initialization Scripts initialization
--------------------------------------------- */ --------------------------------------------- */
var minRam = 1;
if(window.minRam){
minRam = window.minRam;
}
var cardPricing = { var cardPricing = {
'cpu': { 'cpu': {
'id': 'coreValue', 'id': 'coreValue',
@ -16,7 +20,7 @@
'ram': { 'ram': {
'id': 'ramValue', 'id': 'ramValue',
'value': 2, 'value': 2,
'min': 1, 'min': minRam,
'max': 200, 'max': 200,
'interval': 1 'interval': 1
}, },
@ -40,6 +44,7 @@
_initNavUrl(); _initNavUrl();
_initPricing(); _initPricing();
ajaxForms(); ajaxForms();
$('#ramValue').data('old-value', $('#ramValue').val());
}); });
$(window).resize(function() { $(window).resize(function() {
@ -144,21 +149,54 @@
var data = $(this).data('minus'); var data = $(this).data('minus');
if (cardPricing[data].value > cardPricing[data].min) { if (cardPricing[data].value > cardPricing[data].min) {
cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval; if(data === 'ram' && String(cardPricing[data].value) === "1" && minRam === 0.5){
cardPricing[data].value = 0.5;
$('#ramValue').val('0.5');
$("#ramValue").attr('step', 0.5);
} else {
cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval;
}
} }
_fetchPricing(); _fetchPricing();
$('#ramValue').data('old-value', $('#ramValue').val());
}); });
$('.fa-plus-circle.right').click(function(event) { $('.fa-plus-circle.right').click(function(event) {
var data = $(this).data('plus'); var data = $(this).data('plus');
if (cardPricing[data].value < cardPricing[data].max) { if (cardPricing[data].value < cardPricing[data].max) {
cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval; if(data === 'ram' && String(cardPricing[data].value) === "0.5" && minRam === 0.5){
cardPricing[data].value = 1;
$('#ramValue').val('1');
$("#ramValue").attr('step', 1);
} else {
cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval;
}
} }
_fetchPricing(); _fetchPricing();
$('#ramValue').data('old-value', $('#ramValue').val());
}); });
$('.input-price').change(function() { $('.input-price').change(function() {
var data = $(this).attr("name"); var data = $(this).attr("name");
cardPricing[data].value = $('input[name=' + data + ']').val(); var input = $('input[name=' + data + ']');
var inputValue = input.val();
if(data === 'ram') {
var ramInput = $('#ramValue');
if ($('#ramValue').data('old-value') < $('#ramValue').val()) {
if($('#ramValue').val() === '1' && minRam === 0.5) {
$("#ramValue").attr('step', 1);
$('#ramValue').val('1');
}
} else {
if($('#ramValue').val() === '0' && minRam === 0.5) {
$("#ramValue").attr('step', 0.5);
$('#ramValue').val('0.5');
}
}
inputValue = $('#ramValue').val();
$('#ramValue').data('old-value', $('#ramValue').val());
}
cardPricing[data].value = inputValue;
_fetchPricing(); _fetchPricing();
}); });
} }

View File

@ -35,14 +35,16 @@
{% endif %} {% endif %}
</li> </li>
{% endif %} {% endif %}
{% if not request.user.is_authenticated %} {% if instance.show_login_option %}
<li> {% if not request.user.is_authenticated %}
<a href="{% url 'hosting:login' %}">{% trans "Login" %}&nbsp;&nbsp;<span class="fa fa-sign-in"></span></a> <li>
</li> <a href="{% url 'hosting:login' %}">{% trans "Login" %}&nbsp;&nbsp;<span class="fa fa-sign-in"></span></a>
{% else %} </li>
<li> {% else %}
<a href="{% url 'hosting:dashboard' %}">{% trans "Dashboard" %}</a> <li>
</li> <a href="{% url 'hosting:dashboard' %}">{% trans "Dashboard" %}</a>
</li>
{% endif %}
{% endif %} {% endif %}
{% comment %} {% comment %}
<!-- to be used when more than one option for language --> <!-- to be used when more than one option for language -->

View File

@ -9,11 +9,14 @@
window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}}; window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}};
window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}}; window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}};
window.discountAmount = {{vm_pricing.discount_amount|default:0}}; window.discountAmount = {{vm_pricing.discount_amount|default:0}};
window.minRam = {{min_ram}};
window.minRamErr = '{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}';
</script> </script>
{% endif %} {% endif %}
<form id="order_form" method="POST" action="{{calculator_form_url}}" data-toggle="validator" role="form"> <form id="order_form" method="POST" action="{{calculator_form_url}}" data-toggle="validator" role="form">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="pid" value="{{instance.id}}">
<div class="title"> <div class="title">
<h3>{% trans "VM hosting" %} </h3> <h3>{% trans "VM hosting" %} </h3>
</div> </div>
@ -54,8 +57,8 @@
<div class="form-group"> <div class="form-group">
<div class="description input"> <div class="description input">
<i class="fa fa-minus-circle left" data-minus="ram" aria-hidden="true"></i> <i class="fa fa-minus-circle left" data-minus="ram" aria-hidden="true"></i>
<input id="ramValue" class="input-price select-number" type="number" min="1" max="200" name="ram" <input id="ramValue" class="input-price select-number" type="number" min="{% if min_ram == 0.5 %}0{% else %}1{% endif %}" max="200" name="ram"
data-error="{% trans 'Please enter a value in range 1 - 200.' %}" required> data-error="{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}" required step="1">
<span> GB RAM</span> <span> GB RAM</span>
<i class="fa fa-plus-circle right" data-plus="ram" aria-hidden="true"></i> <i class="fa fa-plus-circle right" data-plus="ram" aria-hidden="true"></i>
</div> </div>
@ -91,11 +94,12 @@
<label for="config">OS</label> <label for="config">OS</label>
<select name="config"> <select name="config">
{% for template in templates %} {% for template in templates %}
<option value="{{template.opennebula_vm_template_id}}">{{template.name}}</option>
<option value="{{template.opennebula_vm_template_id}}" {% if template.name|lower == instance.default_selected_template|lower %}selected="selected"{% endif %}>{{template.name}}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div>
<input type="hidden" name="pricing_name" value="{% if vm_pricing.name %}{{vm_pricing.name}}{% else %}unknown{% endif%}"></input> <input type="hidden" name="pricing_name" value="{% if vm_pricing.name %}{{vm_pricing.name}}{% else %}unknown{% endif%}"></input>
<input type="submit" class="btn btn-primary disabled" value="{% trans 'Continue' %}"></input> <input type="submit" class="btn btn-primary disabled" value="{% trans 'Continue' %}"></input>
</form> </form>

View File

@ -67,36 +67,49 @@
</div> </div>
<div class="dcl-payment-box"> <div class="dcl-payment-box">
<div class="dcl-payment-section"> <div class="dcl-payment-section">
<h3>{%trans "Your Order" %}</h3> {% if generic_payment_form %}
<hr class="top-hr"> <h3>{%trans "Make a payment" %}</h3>
<div class="dcl-payment-order"> <hr class="top-hr">
<p>{% trans "Cores"%} <strong class="pull-right">{{request.session.specs.cpu|floatformat}}</strong></p> <form role="form" id="generic-payment-form" method="post" action="" novalidate>
<hr> {% csrf_token %}
<p>{% trans "Memory"%} <strong class="pull-right">{{request.session.specs.memory|floatformat}} GB</strong></p> <input type="hidden" name="product" value="1" />
<hr> {% for field in generic_payment_form %}
<p>{% trans "Disk space"%} <strong class="pull-right">{{request.session.specs.disk_size|floatformat}} GB</strong></p> {% bootstrap_field field type='fields'%}
<hr> {% endfor %}
<p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p> <p class="text-danger">{{generic_payment_form.non_field_errors|striptags}}</p>
<hr> </form>
<p> {% else %}
<strong>{%trans "Total" %}</strong>&nbsp;&nbsp; <h3>{%trans "Your Order" %}</h3>
<small> <hr class="top-hr">
({% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %}) <div class="dcl-payment-order">
</small> <p>{% trans "Cores"%} <strong class="pull-right">{{request.session.specs.cpu|floatformat}}</strong></p>
<strong class="pull-right">{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}</strong> <hr>
</p> <p>{% trans "Memory"%} <strong class="pull-right">{{request.session.specs.memory|floatformat}} GB</strong></p>
<hr> <hr>
{% if vm_pricing.discount_amount %} <p>{% trans "Disk space"%} <strong class="pull-right">{{request.session.specs.disk_size|floatformat}} GB</strong></p>
<p class="mb-0"> <hr>
{%trans "Discount" as discount_name %} <p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p>
<strong>{{ vm_pricing.discount_name|default:discount_name }}</strong>&nbsp;&nbsp; <hr>
<strong class="pull-right text-primary">- {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %}</strong> <p>
</p> <strong>{%trans "Total" %}</strong>&nbsp;&nbsp;
<p> <small>
({% trans "Will be applied at checkout" %}) ({% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %})
</p> </small>
{% endif %} <strong class="pull-right">{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}</strong>
</div> </p>
<hr>
{% if vm_pricing.discount_amount %}
<p class="mb-0">
{%trans "Discount" as discount_name %}
<strong>{{ vm_pricing.discount_name|default:discount_name }}</strong>&nbsp;&nbsp;
<strong class="pull-right text-primary">- {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %}</strong>
</p>
<p>
({% trans "Will be applied at checkout" %})
</p>
{% endif %}
</div>
{% endif %}
</div> </div>
</div> </div>
<div class="dcl-payment-box"> <div class="dcl-payment-box">

View File

@ -47,61 +47,88 @@
<hr> <hr>
<div> <div>
<h4>{% trans "Order summary" %}</h4> <h4>{% trans "Order summary" %}</h4>
<p> {% if generic_payment_details %}
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ request.session.template.name }}
</p>
<div class="row">
<div class="col-sm-6">
<p> <p>
<span>{% trans "Cores" %}: </span> <strong>{% trans "Product" %}:</strong>&nbsp;
<strong class="pull-right">{{vm.cpu|floatformat}}</strong> {{ generic_payment_details.product_name }}
</p> </p>
<p> <div class="row">
<span>{% trans "Memory" %}: </span> <div class="col-sm-6">
<strong class="pull-right">{{vm.memory|intcomma}} GB</strong> <p>
</p> <span>{% trans "Amount" %}: </span>
<p> <strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong>
<span>{% trans "Disk space" %}: </span> </p>
<strong class="pull-right">{{vm.disk_size|intcomma}} GB</strong> {% if generic_payment_details.description %}
</p>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
{% if vm.vat > 0 or vm.discount.amount > 0 %}
<div class="col-sm-6">
<div class="subtotal-price">
{% if vm.vat > 0 %}
<p> <p>
<strong class="text-lg">{% trans "Subtotal" %} </strong> <span>{% trans "Description" %}: </span>
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong> <strong class="pull-right">{{generic_payment_details.description}}</strong>
</p>
<p>
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p> </p>
{% endif %} {% endif %}
{% if vm.discount.amount > 0 %} {% if generic_payment_details.recurring %}
<p class="text-primary"> <p>
{%trans "Discount" as discount_name %} <span>{% trans "Recurring" %}: </span>
<strong>{{ vm.discount.name|default:discount_name }} </strong> <strong class="pull-right">Yes</strong>
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
</p> </p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-sm-12"> {% else %}
<hr class="thin-hr"> <p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ request.session.template.name }}
</p>
<div class="row">
<div class="col-sm-6">
<p>
<span>{% trans "Cores" %}: </span>
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
</p>
<p>
<span>{% trans "Memory" %}: </span>
<strong class="pull-right">{{vm.memory|intcomma}} GB</strong>
</p>
<p>
<span>{% trans "Disk space" %}: </span>
<strong class="pull-right">{{vm.disk_size|intcomma}} GB</strong>
</p>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
{% if vm.vat > 0 or vm.discount.amount > 0 %}
<div class="col-sm-6">
<div class="subtotal-price">
{% if vm.vat > 0 %}
<p>
<strong class="text-lg">{% trans "Subtotal" %} </strong>
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
</p>
<p>
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p>
{% endif %}
{% if vm.discount.amount > 0 %}
<p class="text-primary">
{%trans "Discount" as discount_name %}
<strong>{{ vm.discount.name|default:discount_name }} </strong>
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
</p>
{% endif %}
</div>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
{% endif %}
<div class="col-sm-6">
<p class="total-price">
<strong>{% trans "Total" %} </strong>
<strong class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong>
</p>
</div>
</div> </div>
{% endif %} {% endif %}
<div class="col-sm-6">
<p class="total-price">
<strong>{% trans "Total" %} </strong>
<strong class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong>
</p>
</div>
</div>
</div> </div>
<hr class="thin-hr"> <hr class="thin-hr">
</div> </div>
@ -109,7 +136,15 @@
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
<div class="col-sm-8"> <div class="col-sm-8">
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.</div> {% if generic_payment_details %}
{% if generic_payment_details.recurring %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.</div>
{% else %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.</div>
{% endif %}
{% else %}
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.</div>
{% endif %}
</div> </div>
<div class="col-sm-4 order-confirm-btn text-right"> <div class="col-sm-4 order-confirm-btn text-right">
<button class="btn choice-btn" id="btn-create-vm" data-toggle="modal" data-target="#createvm-modal"> <button class="btn choice-btn" id="btn-create-vm" data-toggle="modal" data-target="#createvm-modal">
@ -151,16 +186,5 @@
<script type="text/javascript"> <script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %} {% trans "Some problem encountered. Please try again later." as err_msg %}
var create_vm_error_message = '{{err_msg|safe}}'; var create_vm_error_message = '{{err_msg|safe}}';
window.onload = function () {
var locale_dates = document.getElementsByClassName("locale_date");
var formats = ['YYYY-MM-DD hh:mm a']
var i;
for (i = 0; i < locale_dates.length; i++) {
var oldDate = moment.utc(locale_dates[i].textContent, formats);
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
locale_dates[i].className += ' done';
}
};
</script> </script>
{%endblock%} {%endblock%}

View File

@ -105,7 +105,8 @@ class CeleryTaskTestCase(TestCase):
disk_size=disk_size) disk_size=disk_size)
plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu, plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu,
memory=memory, memory=memory,
disk_size=disk_size) disk_size=disk_size,
price=amount_to_be_charged)
stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu,
ram=memory, ram=memory,
ssd=disk_size, ssd=disk_size,

View File

@ -89,8 +89,14 @@ def create_vm(billing_address_data, stripe_customer_id, specs,
create_vm_task.delay(vm_template_id, user, specs, template, order.id) create_vm_task.delay(vm_template_id, user, specs, template, order.id)
for session_var in ['specs', 'template', 'billing_address', clear_all_session_vars(request)
'billing_address_data', 'card_id',
'token', 'customer']:
if session_var in request.session: def clear_all_session_vars(request):
del request.session[session_var] if request.session is not None:
for session_var in ['specs', 'template', 'billing_address',
'billing_address_data', 'card_id',
'token', 'customer', 'generic_payment_type',
'generic_payment_details', 'product_id']:
if session_var in request.session:
del request.session[session_var]

View File

@ -6,23 +6,31 @@ from django.contrib import messages
from django.contrib.auth import login, authenticate from django.contrib.auth import login, authenticate
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, JsonResponse from django.http import HttpResponseRedirect, JsonResponse, Http404
from django.shortcuts import render from django.shortcuts import render
from django.utils.translation import get_language, ugettext_lazy as _ from django.utils.translation import get_language, ugettext_lazy as _
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from django.views.generic import FormView, CreateView, DetailView from django.views.generic import FormView, CreateView, DetailView
from hosting.forms import HostingUserLoginForm from hosting.forms import (
from hosting.models import HostingOrder, UserCardDetail HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm
)
from hosting.models import (
HostingBill, HostingOrder, UserCardDetail, GenericProduct
)
from membership.models import CustomUser, StripeCustomer from membership.models import CustomUser, StripeCustomer
from opennebula_api.serializers import VMTemplateSerializer from opennebula_api.serializers import VMTemplateSerializer
from utils.forms import BillingAddressForm, BillingAddressFormSignup from utils.forms import (
BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm,
BillingAddress
)
from utils.hosting_utils import get_vm_price_with_vat from utils.hosting_utils import get_vm_price_with_vat
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task from utils.tasks import send_plain_email_task
from .cms_models import DCLCalculatorPluginModel
from .forms import ContactForm from .forms import ContactForm
from .models import VMTemplate, VMPricing from .models import VMTemplate, VMPricing
from .utils import get_cms_integration, create_vm from .utils import get_cms_integration, create_vm, clear_all_session_vars
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -82,7 +90,29 @@ class IndexView(CreateView):
raise ValidationError(_('Invalid number of cores')) raise ValidationError(_('Invalid number of cores'))
def validate_memory(self, value): def validate_memory(self, value):
if (value > 200) or (value < 1): if 'pid' in self.request.POST:
try:
plugin = DCLCalculatorPluginModel.objects.get(
id=self.request.POST['pid']
)
except DCLCalculatorPluginModel.DoesNotExist as dne:
logger.error(
str(dne) + " plugin_id: " + self.request.POST['pid']
)
raise ValidationError(_('Invalid calculator properties'))
if plugin.enable_512mb_ram:
if value % 1 == 0 or value == 0.5:
logger.debug(
"Given ram {value} is either 0.5 or a"
" whole number".format(value=value)
)
if (value > 200) or (value < 0.5):
raise ValidationError(_('Invalid RAM size'))
else:
raise ValidationError(_('Invalid RAM size'))
elif (value > 200) or (value < 1) or (value % 1 != 0):
raise ValidationError(_('Invalid RAM size'))
else:
raise ValidationError(_('Invalid RAM size')) raise ValidationError(_('Invalid RAM size'))
def validate_storage(self, value): def validate_storage(self, value):
@ -91,17 +121,14 @@ class IndexView(CreateView):
@cache_control(no_cache=True, must_revalidate=True, no_store=True) @cache_control(no_cache=True, must_revalidate=True, no_store=True)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
for session_var in ['specs', 'user', 'billing_address_data', clear_all_session_vars(request)
'pricing_name']:
if session_var in request.session:
del request.session[session_var]
return HttpResponseRedirect(reverse('datacenterlight:cms_index')) return HttpResponseRedirect(reverse('datacenterlight:cms_index'))
def post(self, request): def post(self, request):
cores = request.POST.get('cpu') cores = request.POST.get('cpu')
cores_field = forms.IntegerField(validators=[self.validate_cores]) cores_field = forms.IntegerField(validators=[self.validate_cores])
memory = request.POST.get('ram') memory = request.POST.get('ram')
memory_field = forms.IntegerField(validators=[self.validate_memory]) memory_field = forms.FloatField(validators=[self.validate_memory])
storage = request.POST.get('storage') storage = request.POST.get('storage')
storage_field = forms.IntegerField(validators=[self.validate_storage]) storage_field = forms.IntegerField(validators=[self.validate_storage])
template_id = int(request.POST.get('config')) template_id = int(request.POST.get('config'))
@ -170,7 +197,7 @@ class IndexView(CreateView):
'vat': vat, 'vat': vat,
'vat_percent': vat_percent, 'vat_percent': vat_percent,
'discount': discount, 'discount': discount,
'total_price': price + vat - discount['amount'], 'total_price': round(price + vat - discount['amount'], 2),
'pricing_name': vm_pricing_name 'pricing_name': vm_pricing_name
} }
request.session['specs'] = specs request.session['specs'] = specs
@ -242,19 +269,93 @@ class PaymentOrderView(FormView):
'login_form': HostingUserLoginForm(prefix='login_form'), 'login_form': HostingUserLoginForm(prefix='login_form'),
'billing_address_form': billing_address_form, 'billing_address_form': billing_address_form,
'cms_integration': get_cms_integration('default'), 'cms_integration': get_cms_integration('default'),
'vm_pricing': VMPricing.get_vm_pricing_by_name(
self.request.session['specs']['pricing_name']
)
}) })
if ('generic_payment_type' in self.request.session and
self.request.session['generic_payment_type'] == 'generic'):
if 'product_id' in self.request.session:
product = GenericProduct.objects.get(
id=self.request.session['product_id']
)
context.update({'generic_payment_form': ProductPaymentForm(
prefix='generic_payment_form',
initial={'product_name': product.product_name,
'amount': float(product.get_actual_price()),
'recurring': product.product_is_subscription,
'description': product.product_description,
},
product_id=product.id
), })
else:
context.update({'generic_payment_form': GenericPaymentForm(
prefix='generic_payment_form',
), })
else:
context.update({
'vm_pricing': VMPricing.get_vm_pricing_by_name(
self.request.session['specs']['pricing_name']
)
})
return context return context
@cache_control(no_cache=True, must_revalidate=True, no_store=True) @cache_control(no_cache=True, must_revalidate=True, no_store=True)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'specs' not in request.session: if (('type' in request.GET and request.GET['type'] == 'generic')
or 'product_slug' in kwargs):
request.session['generic_payment_type'] = 'generic'
if 'generic_payment_details' in request.session:
request.session.pop('generic_payment_details')
request.session.pop('product_id')
if 'product_slug' in kwargs:
logger.debug("Product slug is " + kwargs['product_slug'])
try:
product = GenericProduct.objects.get(
product_slug=kwargs['product_slug']
)
except GenericProduct.DoesNotExist as dne:
logger.error(
"Product '{}' does "
"not exist".format(kwargs['product_slug'])
)
raise Http404()
request.session['product_id'] = product.id
elif 'specs' not in request.session:
return HttpResponseRedirect(reverse('datacenterlight:index')) return HttpResponseRedirect(reverse('datacenterlight:index'))
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if 'product' in request.POST:
# query for the supplied product
product = None
try:
product = GenericProduct.objects.get(
id=request.POST['generic_payment_form-product_name']
)
except GenericProduct.DoesNotExist as dne:
logger.error(
"The requested product '{}' does not exist".format(
request.POST['generic_payment_form-product_name']
)
)
except GenericProduct.MultipleObjectsReturned as mpe:
logger.error(
"There seem to be more than one product with "
"the name {}".format(
request.POST['generic_payment_form-product_name']
)
)
product = GenericProduct.objects.all(
product_name=request.
POST['generic_payment_form-product_name']
).first()
if product is None:
return JsonResponse({})
else:
return JsonResponse({
'amount': product.get_actual_price(),
'isSubscription': product.product_is_subscription
})
if 'login_form' in request.POST: if 'login_form' in request.POST:
login_form = HostingUserLoginForm( login_form = HostingUserLoginForm(
data=request.POST, prefix='login_form' data=request.POST, prefix='login_form'
@ -265,6 +366,13 @@ class PaymentOrderView(FormView):
auth_user = authenticate(email=email, password=password) auth_user = authenticate(email=email, password=password)
if auth_user: if auth_user:
login(self.request, auth_user) login(self.request, auth_user)
if 'product_slug' in kwargs:
return HttpResponseRedirect(
reverse('show_product',
kwargs={
'product_slug': kwargs['product_slug']}
)
)
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('datacenterlight:payment') reverse('datacenterlight:payment')
) )
@ -281,6 +389,50 @@ class PaymentOrderView(FormView):
data=request.POST, data=request.POST,
) )
if address_form.is_valid(): if address_form.is_valid():
# Check if we are in a generic payment case and handle the generic
# payment details form before we go on to verify payment
if ('generic_payment_type' in request.session and
self.request.session['generic_payment_type'] == 'generic'):
if 'product_id' in request.session:
generic_payment_form = ProductPaymentForm(
data=request.POST, prefix='generic_payment_form',
product_id=request.session['product_id']
)
else:
generic_payment_form = GenericPaymentForm(
data=request.POST, prefix='generic_payment_form'
)
if generic_payment_form.is_valid():
logger.debug("Generic payment form is valid.")
if 'product_id' in request.session:
product = generic_payment_form.product
else:
product = generic_payment_form.cleaned_data.get(
'product_name'
)
gp_details = {
"product_name": product.product_name,
"amount": generic_payment_form.cleaned_data.get(
'amount'
),
"recurring": generic_payment_form.cleaned_data.get(
'recurring'
),
"description": generic_payment_form.cleaned_data.get(
'description'
),
"product_id": product.id,
"product_slug": product.product_slug
}
request.session["generic_payment_details"] = (
gp_details
)
else:
logger.debug("Generic payment form invalid")
context = self.get_context_data()
context['generic_payment_form'] = generic_payment_form
context['billing_address_form'] = address_form
return self.render_to_response(context)
token = address_form.cleaned_data.get('token') token = address_form.cleaned_data.get('token')
if token is '': if token is '':
card_id = address_form.cleaned_data.get('card') card_id = address_form.cleaned_data.get('card')
@ -296,8 +448,8 @@ class PaymentOrderView(FormView):
except UserCardDetail.DoesNotExist as e: except UserCardDetail.DoesNotExist as e:
ex = str(e) ex = str(e)
logger.error("Card Id: {card_id}, Exception: {ex}".format( logger.error("Card Id: {card_id}, Exception: {ex}".format(
card_id=card_id, ex=ex card_id=card_id, ex=ex
) )
) )
msg = _("An error occurred. Details: {}".format(ex)) msg = _("An error occurred. Details: {}".format(ex))
messages.add_message( messages.add_message(
@ -386,7 +538,8 @@ class OrderConfirmationView(DetailView):
@cache_control(no_cache=True, must_revalidate=True, no_store=True) @cache_control(no_cache=True, must_revalidate=True, no_store=True)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
context = {} context = {}
if 'specs' not in request.session or 'user' not in request.session: if (('specs' not in request.session or 'user' not in request.session)
and 'generic_payment_type' not in request.session):
return HttpResponseRedirect(reverse('datacenterlight:index')) return HttpResponseRedirect(reverse('datacenterlight:index'))
if 'token' in self.request.session: if 'token' in self.request.session:
token = self.request.session['token'] token = self.request.session['token']
@ -404,9 +557,19 @@ class OrderConfirmationView(DetailView):
card_detail = UserCardDetail.objects.get(id=card_id) card_detail = UserCardDetail.objects.get(id=card_id)
context['cc_last4'] = card_detail.last4 context['cc_last4'] = card_detail.last4
context['cc_brand'] = card_detail.brand context['cc_brand'] = card_detail.brand
if ('generic_payment_type' in request.session and
self.request.session['generic_payment_type'] == 'generic'):
context.update({
'generic_payment_details':
request.session['generic_payment_details'],
})
else:
context.update({
'vm': request.session.get('specs'),
})
context.update({ context.update({
'site_url': reverse('datacenterlight:index'), 'site_url': reverse('datacenterlight:index'),
'vm': request.session.get('specs'),
'page_header_text': _('Confirm Order'), 'page_header_text': _('Confirm Order'),
'billing_address_data': ( 'billing_address_data': (
request.session.get('billing_address_data') request.session.get('billing_address_data')
@ -416,11 +579,8 @@ class OrderConfirmationView(DetailView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
template = request.session.get('template')
specs = request.session.get('specs')
user = request.session.get('user') user = request.session.get('user')
stripe_api_cus_id = request.session.get('customer') stripe_api_cus_id = request.session.get('customer')
vm_template_id = template.get('id', 1)
stripe_utils = StripeUtils() stripe_utils = StripeUtils()
if 'token' in request.session: if 'token' in request.session:
@ -434,7 +594,14 @@ class OrderConfirmationView(DetailView):
response = { response = {
'status': False, 'status': False,
'redirect': "{url}#{section}".format( 'redirect': "{url}#{section}".format(
url=reverse('datacenterlight:payment'), url=(reverse(
'show_product',
kwargs={'product_slug':
request.session['generic_payment_details']
['product_slug']}
) if 'generic_payment_details' in request.session else
reverse('datacenterlight:payment')
),
section='payment_error'), section='payment_error'),
'msg_title': str(_('Error.')), 'msg_title': str(_('Error.')),
'msg_body': str( 'msg_body': str(
@ -450,7 +617,8 @@ class OrderConfirmationView(DetailView):
'brand': card_details_response['brand'], 'brand': card_details_response['brand'],
'card_id': card_details_response['card_id'] 'card_id': card_details_response['card_id']
} }
stripe_customer_obj = StripeCustomer.objects.filter(stripe_id=stripe_api_cus_id).first() stripe_customer_obj = StripeCustomer.objects.filter(
stripe_id=stripe_api_cus_id).first()
if stripe_customer_obj: if stripe_customer_obj:
ucd = UserCardDetail.get_user_card_details( ucd = UserCardDetail.get_user_card_details(
stripe_customer_obj, card_details_response stripe_customer_obj, card_details_response
@ -472,7 +640,16 @@ class OrderConfirmationView(DetailView):
response = { response = {
'status': False, 'status': False,
'redirect': "{url}#{section}".format( 'redirect': "{url}#{section}".format(
url=reverse('hosting:payment'), url=(reverse(
'show_product',
kwargs={'product_slug':
request.session
['generic_payment_details']
['product_slug']}
) if 'generic_payment_details' in
request.session else
reverse('datacenterlight:payment')
),
section='payment_error'), section='payment_error'),
'msg_title': str(_('Error.')), 'msg_title': str(_('Error.')),
'msg_body': str( 'msg_body': str(
@ -504,51 +681,122 @@ class OrderConfirmationView(DetailView):
} }
return JsonResponse(response) return JsonResponse(response)
cpu = specs.get('cpu') if ('generic_payment_type' in request.session and
memory = specs.get('memory') self.request.session['generic_payment_type'] == 'generic'):
disk_size = specs.get('disk_size') gp_details = self.request.session['generic_payment_details']
amount_to_be_charged = specs.get('total_price') if gp_details['recurring']:
plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu, # generic recurring payment
memory=memory, logger.debug("Commencing a generic recurring payment")
disk_size=disk_size) else:
stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, # generic one time payment
ram=memory, logger.debug("Commencing a one time payment")
ssd=disk_size, charge_response = stripe_utils.make_charge(
version=1, amount=gp_details['amount'],
app='dcl') customer=stripe_api_cus_id
stripe_plan = stripe_utils.get_or_create_stripe_plan( )
amount=amount_to_be_charged, stripe_onetime_charge = charge_response.get('response_object')
name=plan_name,
stripe_plan_id=stripe_plan_id)
subscription_result = stripe_utils.subscribe_customer_to_plan(
stripe_api_cus_id,
[{"plan": stripe_plan.get(
'response_object').stripe_plan_id}])
stripe_subscription_obj = subscription_result.get('response_object')
# Check if the subscription was approved and is active
if (stripe_subscription_obj is None
or stripe_subscription_obj.status != 'active'):
# At this point, we have created a Stripe API card and
# associated it with the customer; but the transaction failed
# due to some reason. So, we would want to dissociate this card
# here.
# ...
msg = subscription_result.get('error') # Check if the payment was approved
messages.add_message(self.request, messages.ERROR, msg, if not stripe_onetime_charge:
extra_tags='failed_payment') msg = charge_response.get('error')
response = { messages.add_message(self.request, messages.ERROR, msg,
'status': False, extra_tags='failed_payment')
'redirect': "{url}#{section}".format( response = {
url=reverse('datacenterlight:payment'), 'status': False,
section='payment_error'), 'redirect': "{url}#{section}".format(
'msg_title': str(_('Error.')), url=(reverse('show_product', kwargs={
'msg_body': str( 'product_slug': gp_details['product_slug']}
_('There was a payment related error.' ) if 'generic_payment_details' in
' On close of this popup, you will be redirected back to' request.session else
' the payment page.')) reverse('datacenterlight:payment')
} ),
return JsonResponse(response) section='payment_error'),
'msg_title': str(_('Error.')),
'msg_body': str(
_('There was a payment related error.'
' On close of this popup, you will be redirected'
' back to the payment page.'))
}
return JsonResponse(response)
if ('generic_payment_type' not in request.session or
(request.session['generic_payment_details']['recurring'])):
if 'generic_payment_details' in request.session:
amount_to_be_charged = (
round(
request.session['generic_payment_details']['amount'],
2
)
)
plan_name = "generic-{0}-{1:.2f}".format(
request.session['generic_payment_details']['product_id'],
amount_to_be_charged
)
stripe_plan_id = plan_name
else:
template = request.session.get('template')
specs = request.session.get('specs')
vm_template_id = template.get('id', 1)
cpu = specs.get('cpu')
memory = specs.get('memory')
disk_size = specs.get('disk_size')
amount_to_be_charged = specs.get('total_price')
plan_name = StripeUtils.get_stripe_plan_name(
cpu=cpu,
memory=memory,
disk_size=disk_size,
price=amount_to_be_charged
)
stripe_plan_id = StripeUtils.get_stripe_plan_id(
cpu=cpu,
ram=memory,
ssd=disk_size,
version=1,
app='dcl',
price=amount_to_be_charged
)
stripe_plan = stripe_utils.get_or_create_stripe_plan(
amount=amount_to_be_charged,
name=plan_name,
stripe_plan_id=stripe_plan_id)
subscription_result = stripe_utils.subscribe_customer_to_plan(
stripe_api_cus_id,
[{"plan": stripe_plan.get(
'response_object').stripe_plan_id}])
stripe_subscription_obj = subscription_result.get('response_object')
# Check if the subscription was approved and is active
if (stripe_subscription_obj is None
or stripe_subscription_obj.status != 'active'):
# At this point, we have created a Stripe API card and
# associated it with the customer; but the transaction failed
# due to some reason. So, we would want to dissociate this card
# here.
# ...
msg = subscription_result.get('error')
messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment')
response = {
'status': False,
'redirect': "{url}#{section}".format(
url=(reverse(
'show_product',
kwargs={'product_slug':
request.session['generic_payment_details']
['product_slug']}
) if 'generic_payment_details' in request.session else
reverse('datacenterlight:payment')
),
section='payment_error'
),
'msg_title': str(_('Error.')),
'msg_body': str(
_('There was a payment related error.'
' On close of this popup, you will be redirected back to'
' the payment page.'))
}
return JsonResponse(response)
# Create user if the user is not logged in and if he is not already # Create user if the user is not logged in and if he is not already
# registered # registered
@ -618,6 +866,118 @@ class OrderConfirmationView(DetailView):
'user': custom_user.id 'user': custom_user.id
}) })
if 'generic_payment_type' in request.session:
stripe_cus = StripeCustomer.objects.filter(
stripe_id=stripe_api_cus_id
).first()
billing_address = BillingAddress(
cardholder_name=billing_address_data['cardholder_name'],
street_address=billing_address_data['street_address'],
city=billing_address_data['city'],
postal_code=billing_address_data['postal_code'],
country=billing_address_data['country']
)
billing_address.save()
order = HostingOrder.create(
price=self.request
.session['generic_payment_details']['amount'],
customer=stripe_cus,
billing_address=billing_address,
vm_pricing=VMPricing.get_default_pricing()
)
# Create a Hosting Bill
HostingBill.create(customer=stripe_cus,
billing_address=billing_address)
# Create Billing Address for User if he does not have one
if not stripe_cus.user.billing_addresses.count():
billing_address_data.update({
'user': stripe_cus.user.id
})
billing_address_user_form = UserBillingAddressForm(
billing_address_data
)
billing_address_user_form.is_valid()
billing_address_user_form.save()
if self.request.session['generic_payment_details']['recurring']:
# Associate the given stripe subscription with the order
order.set_subscription_id(
stripe_subscription_obj.id, card_details_dict
)
else:
# Associate the given stripe charge id with the order
order.set_stripe_charge(stripe_onetime_charge)
# Set order status approved
order.set_approved()
order.generic_payment_description = gp_details["description"]
order.generic_product_id = gp_details["product_id"]
order.save()
# send emails
context = {
'name': user.get('name'),
'email': user.get('email'),
'amount': gp_details['amount'],
'description': gp_details['description'],
'recurring': gp_details['recurring'],
'product_name': gp_details['product_name'],
'product_id': gp_details['product_id'],
'order_id': order.id
}
email_data = {
'subject': (settings.DCL_TEXT +
" Payment received from %s" % context['email']),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in context.items()]),
'reply_to': [context['email']],
}
send_plain_email_task.delay(email_data)
email_data = {
'subject': _("Confirmation of your payment"),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': [user.get('email')],
'body': _("Hi {name},\n\n"
"thank you for your order!\n"
"We have just received a payment of CHF {amount:.2f}"
" from you.{recurring}\n\n"
"Cheers,\nYour Data Center Light team".format(
name=user.get('name'),
amount=gp_details['amount'],
recurring=(
_(' This is a monthly recurring plan.')
if gp_details['recurring'] else ''
)
)
),
'reply_to': ['info@ungleich.ch'],
}
send_plain_email_task.delay(email_data)
response = {
'status': True,
'redirect': (
reverse('hosting:orders')
if request.user.is_authenticated()
else reverse('datacenterlight:index')
),
'msg_title': str(_('Thank you for the payment.')),
'msg_body': str(
_('You will soon receive a confirmation email of the '
'payment. You can always contact us at '
'info@ungleich.ch for any question that you may have.')
)
}
clear_all_session_vars(request)
return JsonResponse(response)
user = { user = {
'name': custom_user.name, 'name': custom_user.name,
'email': custom_user.email, 'email': custom_user.email,

View File

@ -10,6 +10,7 @@ from django.conf import settings
from hosting.views import ( from hosting.views import (
RailsHostingView, DjangoHostingView, NodeJSHostingView RailsHostingView, DjangoHostingView, NodeJSHostingView
) )
from datacenterlight.views import PaymentOrderView
from membership import urls as membership_urls from membership import urls as membership_urls
from ungleich_page.views import LandingView from ungleich_page.views import LandingView
from django.views.generic import RedirectView from django.views.generic import RedirectView
@ -29,6 +30,9 @@ urlpatterns = [
url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")), url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")),
url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')), url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')),
url(r'^jsi18n/(?P<packages>\S+?)/$', i18n.javascript_catalog), url(r'^jsi18n/(?P<packages>\S+?)/$', i18n.javascript_catalog),
url(r'^product/(?P<product_slug>[\w-]+)/$',
PaymentOrderView.as_view(),
name='show_product'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(

View File

@ -1,8 +1,8 @@
from django.contrib import admin from django.contrib import admin
from .models import HostingOrder, HostingBill, HostingPlan from .models import HostingOrder, HostingBill, HostingPlan, GenericProduct
admin.site.register(HostingOrder) admin.site.register(HostingOrder)
admin.site.register(HostingBill) admin.site.register(HostingBill)
admin.site.register(HostingPlan) admin.site.register(HostingPlan)
admin.site.register(GenericProduct)

View File

@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from membership.models import CustomUser from membership.models import CustomUser
from utils.hosting_utils import get_all_public_keys from utils.hosting_utils import get_all_public_keys
from .models import UserHostingKey from .models import UserHostingKey, GenericProduct
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,6 +52,93 @@ class HostingUserLoginForm(forms.Form):
raise forms.ValidationError(_("User does not exist")) raise forms.ValidationError(_("User does not exist"))
class ProductModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.product_name
class GenericPaymentForm(forms.Form):
product_name = ProductModelChoiceField(
queryset=GenericProduct.objects.all().order_by('product_name'),
empty_label=_("Choose a product"),
)
amount = forms.FloatField(
widget=forms.TextInput(
attrs={'placeholder': _('Amount in CHF'),
'readonly': 'readonly', }
),
max_value=999999,
min_value=1,
label=_('Amount in CHF')
)
recurring = forms.BooleanField(required=False,
label=_("Recurring monthly"), )
description = forms.CharField(
widget=forms.Textarea(attrs={'style': "height: 60px;"}),
required=False
)
class Meta:
model = GenericProduct
fields = ['product_name', 'amount', 'recurring', 'description']
def clean_amount(self):
amount = self.cleaned_data.get('amount')
if (float(self.cleaned_data.get('product_name').get_actual_price()) !=
amount):
raise forms.ValidationError(_("Amount field does not match"))
return amount
def clean_recurring(self):
recurring = self.cleaned_data.get('recurring')
if (self.cleaned_data.get('product_name').product_is_subscription !=
(True if recurring else False)):
raise forms.ValidationError(_("Recurring field does not match"))
return recurring
class ProductPaymentForm(GenericPaymentForm):
def __init__(self, *args, **kwargs):
product_id = kwargs.pop('product_id', None)
if product_id is not None:
self.product = GenericProduct.objects.get(id=product_id)
super(ProductPaymentForm, self).__init__(*args, **kwargs)
self.fields['product_name'] = forms.CharField(
widget=forms.TextInput(
attrs={'placeholder': _('Product name'),
'readonly': 'readonly'}
)
)
if self.product.product_is_subscription:
self.fields['amount'].label = "{amt} ({payment_type})".format(
amt=_('Amount in CHF'),
payment_type=_('Monthly subscription')
)
else:
self.fields['amount'].label = "{amt} ({payment_type})".format(
amt=_('Amount in CHF'),
payment_type=_('One time payment')
)
self.fields['recurring'].widget = forms.HiddenInput()
self.fields['product_name'].widget.attrs['class'] = 'input-no-border'
self.fields['amount'].widget.attrs['class'] = 'input-no-border'
self.fields['description'].widget.attrs['class'] = 'input-no-border'
def clean_amount(self):
amount = self.cleaned_data.get('amount')
if (self.product is None or
float(self.product.get_actual_price()) != amount):
raise forms.ValidationError(_("Amount field does not match"))
return amount
def clean_recurring(self):
recurring = self.cleaned_data.get('recurring')
if (self.product.product_is_subscription !=
(True if recurring else False)):
raise forms.ValidationError(_("Recurring field does not match"))
return recurring
class HostingUserSignupForm(forms.ModelForm): class HostingUserSignupForm(forms.ModelForm):
confirm_password = forms.CharField(label=_("Confirm Password"), confirm_password = forms.CharField(label=_("Confirm Password"),
widget=forms.PasswordInput()) widget=forms.PasswordInput())
@ -111,7 +198,7 @@ class UserHostingKeyForm(forms.ModelForm):
public_key=openssh_pubkey_str).first().name public_key=openssh_pubkey_str).first().name
KEY_EXISTS_MESSAGE = _( KEY_EXISTS_MESSAGE = _(
"This key exists already with the name \"%(name)s\"") % { "This key exists already with the name \"%(name)s\"") % {
'name': key_name} 'name': key_name}
raise forms.ValidationError(KEY_EXISTS_MESSAGE) raise forms.ValidationError(KEY_EXISTS_MESSAGE)
with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file: with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file:

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-05 23:15+0000\n" "POT-Creation-Date: 2018-09-08 08:45+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -209,7 +209,7 @@ msgstr "Du hast eine neue virtuelle Maschine bestellt!"
#, python-format #, python-format
msgid "Your order of <strong>%(vm_name)s</strong> has been charged." msgid "Your order of <strong>%(vm_name)s</strong> has been charged."
msgstr "Deine Bestellung von <strong>%(vm_name)s</strong> wurde erhoben." msgstr "Deine Bestellung von <strong>%(vm_name)s</strong> wurde entgegengenommen."
msgid "You can view your VM detail by clicking the button below." msgid "You can view your VM detail by clicking the button below."
msgstr "Um die Rechnung zu sehen, klicke auf den Button unten." msgstr "Um die Rechnung zu sehen, klicke auf den Button unten."
@ -222,7 +222,7 @@ msgstr "Dein Data Center Light Team"
#, python-format #, python-format
msgid "Your order of %(vm_name)s has been charged." msgid "Your order of %(vm_name)s has been charged."
msgstr "Deine Bestellung von %(vm_name)s wurde erhoben." msgstr "Deine Bestellung von %(vm_name)s wurde entgegengenommen."
msgid "You can view your VM detail by following the link below." msgid "You can view your VM detail by following the link below."
msgstr "Um die Rechnung zu sehen, klicke auf den Link unten." msgstr "Um die Rechnung zu sehen, klicke auf den Link unten."
@ -249,7 +249,7 @@ msgstr "VM Kündigung"
#, python-format #, python-format
msgid "" msgid ""
"You are receiving this email because your virutal machine <strong>" "You are receiving this email because your virtual machine <strong>"
"%(vm_name)s</strong> has been cancelled." "%(vm_name)s</strong> has been cancelled."
msgstr "" msgstr ""
"Du erhälst diese E-Mail, da deine virtuelle Maschine <strong>%(vm_name)s</" "Du erhälst diese E-Mail, da deine virtuelle Maschine <strong>%(vm_name)s</"
@ -265,7 +265,7 @@ msgstr "NEUE VM"
#, python-format #, python-format
msgid "" msgid ""
"You are receiving this email because your virutal machine %(vm_name)s has " "You are receiving this email because your virtual machine %(vm_name)s has "
"been cancelled." "been cancelled."
msgstr "" msgstr ""
"Du erhälst diese E-Mail, da deine virtuelle Maschine %(vm_name)s gekündigt " "Du erhälst diese E-Mail, da deine virtuelle Maschine %(vm_name)s gekündigt "
@ -290,9 +290,8 @@ msgid ""
"You are not making any payment yet. After placing your order, you will be " "You are not making any payment yet. After placing your order, you will be "
"taken to the Submit Payment Page." "taken to the Submit Payment Page."
msgstr "" msgstr ""
"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst " "Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, "
"ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt " "nachdem Du die Bestellung auf der nächsten Seite bestätigt hast."
"hast."
msgid "SUBMIT" msgid "SUBMIT"
msgstr "ABSENDEN" msgstr "ABSENDEN"
@ -469,9 +468,9 @@ msgid ""
"database." "database."
msgstr "" msgstr ""
"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine " "Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine "
"Kreditkartendetails unten an. Die Bezahlung wird über " "Kreditkartendetails unten an. Die Bezahlung wird über <a href=\"https://"
"<a href=\"https://stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. " "stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. Wir speichern Deine "
"Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank." "Kreditkartendetails nicht in unserer Datenbank."
msgid "" msgid ""
"Please fill in your credit card information below. We are using <a href=" "Please fill in your credit card information below. We are using <a href="
@ -631,6 +630,12 @@ msgstr ""
"Bitte entschuldige, es scheint ein unerwarteter Fehler aufgetreten zu sein. " "Bitte entschuldige, es scheint ein unerwarteter Fehler aufgetreten zu sein. "
"Versuche es doch bitte noch einmal." "Versuche es doch bitte noch einmal."
msgid "Attention:"
msgstr "Achtung:"
msgid "terminating VM can not be reverted."
msgstr "Das Beenden kann nicht rückgängig gemacht werden."
msgid "Something doesn't work?" msgid "Something doesn't work?"
msgstr "Etwas funktioniert nicht?" msgstr "Etwas funktioniert nicht?"
@ -643,8 +648,12 @@ msgstr "KONTAKT"
msgid "Terminate your Virtual Machine" msgid "Terminate your Virtual Machine"
msgstr "Deine Virtuelle Maschine beenden" msgstr "Deine Virtuelle Maschine beenden"
msgid "Do you want to cancel your Virtual Machine" msgid ""
msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst" "Terminated VMs can not be revived and will not be refunded. Do you want to "
"terminate your VM?"
msgstr ""
"Beendete VMs können nicht wiederhergestellt oder erstattet werden. Möchtest "
"du die VM beenden?"
#, python-format #, python-format
msgid "" msgid ""
@ -723,8 +732,8 @@ msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
#, python-brace-format #, python-brace-format
msgid "An error occurred while associating the card. Details: {details}" msgid "An error occurred while associating the card. Details: {details}"
msgstr "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: " msgstr ""
"{details}" "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
msgid "Successfully associated the card with your account" msgid "Successfully associated the card with your account"
msgstr "Die Karte wurde erfolgreich mit deinem Konto verbunden" msgstr "Die Karte wurde erfolgreich mit deinem Konto verbunden"
@ -798,6 +807,11 @@ msgstr ""
msgid "Error terminating VM" msgid "Error terminating VM"
msgstr "Fehler beenden VM" msgstr "Fehler beenden VM"
msgid ""
"VM terminate action timed out. Please contact support@datacenterlight.ch for "
"further information."
msgstr ""
#, python-format #, python-format
msgid "Virtual Machine %(vm_name)s Cancelled" msgid "Virtual Machine %(vm_name)s Cancelled"
msgstr "Virtuelle Maschine %(vm_name)s Kündigung" msgstr "Virtuelle Maschine %(vm_name)s Kündigung"
@ -807,6 +821,9 @@ msgstr ""
"Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es " "Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es "
"noch einmal." "noch einmal."
#~ msgid "Do you want to cancel your Virtual Machine"
#~ msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst"
#~ msgid "Reset your password" #~ msgid "Reset your password"
#~ msgstr "Passwort zurücksetzen" #~ msgstr "Passwort zurücksetzen"

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2018-10-03 07:57
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0047_auto_20180821_1240'),
]
operations = [
migrations.CreateModel(
name='GenericProduct',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_name', models.CharField(default='', max_length=128)),
('product_slug', models.SlugField(help_text='An optional html id for the Section. Required to set as target of a link on page', unique=True)),
('product_description', models.CharField(default='', max_length=500)),
('created_at', models.DateTimeField(auto_now_add=True)),
('product_price', models.DecimalField(decimal_places=2, max_digits=6)),
('product_vat', models.DecimalField(decimal_places=4, default=0, max_digits=6)),
('product_is_subscription', models.BooleanField(default=True)),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
migrations.AddField(
model_name='hostingorder',
name='generic_payment_description',
field=models.CharField(max_length=500, null=True),
),
migrations.AddField(
model_name='hostingorder',
name='generic_product',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hosting.GenericProduct'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2018-10-05 07:36
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0048_auto_20181003_0757'),
]
operations = [
migrations.AlterField(
model_name='genericproduct',
name='product_slug',
field=models.SlugField(help_text='An mandatory unique slug for the product', unique=True),
),
]

View File

@ -9,8 +9,8 @@ from django.utils.functional import cached_property
from datacenterlight.models import VMPricing, VMTemplate from datacenterlight.models import VMPricing, VMTemplate
from membership.models import StripeCustomer, CustomUser from membership.models import StripeCustomer, CustomUser
from utils.models import BillingAddress
from utils.mixins import AssignPermissionsMixin from utils.mixins import AssignPermissionsMixin
from utils.models import BillingAddress
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -61,6 +61,30 @@ class OrderDetail(AssignPermissionsMixin, models.Model):
) )
class GenericProduct(AssignPermissionsMixin, models.Model):
permissions = ('view_genericproduct',)
product_name = models.CharField(max_length=128, default="")
product_slug = models.SlugField(
unique=True,
help_text=(
'An mandatory unique slug for the product'
)
)
product_description = models.CharField(max_length=500, default="")
created_at = models.DateTimeField(auto_now_add=True)
product_price = models.DecimalField(max_digits=6, decimal_places=2)
product_vat = models.DecimalField(max_digits=6, decimal_places=4, default=0)
product_is_subscription = models.BooleanField(default=True)
def __str__(self):
return self.product_name
def get_actual_price(self):
return round(
self.product_price + (self.product_price * self.product_vat), 2
)
class HostingOrder(AssignPermissionsMixin, models.Model): class HostingOrder(AssignPermissionsMixin, models.Model):
ORDER_APPROVED_STATUS = 'Approved' ORDER_APPROVED_STATUS = 'Approved'
ORDER_DECLINED_STATUS = 'Declined' ORDER_DECLINED_STATUS = 'Declined'
@ -80,7 +104,13 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
OrderDetail, null=True, blank=True, default=None, OrderDetail, null=True, blank=True, default=None,
on_delete=models.SET_NULL on_delete=models.SET_NULL
) )
generic_product = models.ForeignKey(
GenericProduct, null=True, blank=True, default=None,
on_delete=models.SET_NULL
)
generic_payment_description = models.CharField(
max_length=500, null=True
)
permissions = ('view_hostingorder',) permissions = ('view_hostingorder',)
class Meta: class Meta:
@ -89,11 +119,18 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
) )
def __str__(self): def __str__(self):
return ("Order Nr: #{} - VM_ID: {} - {} - {} - " hosting_order_str = ("Order Nr: #{} - VM_ID: {} - {} - {} - "
"Specs: {} - Price: {}").format( "Specs: {} - Price: {}").format(
self.id, self.vm_id, self.customer.user.email, self.created_at, self.id, self.vm_id, self.customer.user.email, self.created_at,
self.order_detail, self.price self.order_detail, self.price
) )
if self.generic_product_id is not None:
hosting_order_str += " - Generic Payment"
if self.stripe_charge_id is not None:
hosting_order_str += " - One time charge"
else:
hosting_order_str += " - Recurring"
return hosting_order_str
@cached_property @cached_property
def status(self): def status(self):

View File

@ -146,9 +146,13 @@
text-align: center; text-align: center;
} }
.vm-vmid-with-warning {
padding: 50px 0 33px !important;
}
.vm-vmid .alert { .vm-vmid .alert {
margin-top: 15px; margin-top: 15px;
margin-bottom: -60px; margin-bottom: -25px;
} }
.vm-item-lg { .vm-item-lg {
@ -183,6 +187,13 @@
margin-top: 25px; margin-top: 25px;
} }
.vm-terminate-warning {
letter-spacing: 0.6px;
font-size: 12px;
font-weight: 400;
color: #373636;
}
.vm-contact-us { .vm-contact-us {
margin: 25px 0 30px; margin: 25px 0 30px;
/* text-align: center; */ /* text-align: center; */

View File

@ -157,6 +157,10 @@ $( document ).ready(function() {
/* --------------------------------------------- /* ---------------------------------------------
Scripts initialization Scripts initialization
--------------------------------------------- */ --------------------------------------------- */
var minRam = 1;
if(window.minRam){
minRam = window.minRam;
}
var cardPricing = { var cardPricing = {
'cpu': { 'cpu': {
'id': 'coreValue', 'id': 'coreValue',
@ -168,7 +172,7 @@ $( document ).ready(function() {
'ram': { 'ram': {
'id': 'ramValue', 'id': 'ramValue',
'value': 2, 'value': 2,
'min': 1, 'min': minRam,
'max': 200, 'max': 200,
'interval': 1 'interval': 1
}, },
@ -188,21 +192,54 @@ $( document ).ready(function() {
var data = $(this).data('minus'); var data = $(this).data('minus');
if (cardPricing[data].value > cardPricing[data].min) { if (cardPricing[data].value > cardPricing[data].min) {
cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval; if(data === 'ram' && String(cardPricing[data].value) === "1" && minRam === 0.5){
cardPricing[data].value = 0.5;
$('#ramValue').val('0.5');
$("#ramValue").attr('step', 0.5);
} else {
cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval;
}
} }
_fetchPricing(); _fetchPricing();
$('#ramValue').data('old-value', $('#ramValue').val());
}); });
$('.fa-plus-circle.right').click(function(event) { $('.fa-plus-circle.right').click(function(event) {
var data = $(this).data('plus'); var data = $(this).data('plus');
if (cardPricing[data].value < cardPricing[data].max) { if (cardPricing[data].value < cardPricing[data].max) {
cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval; if(data === 'ram' && String(cardPricing[data].value) === "0.5" && minRam === 0.5){
cardPricing[data].value = 1;
$('#ramValue').val('1');
$("#ramValue").attr('step', 1);
} else {
cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval;
}
} }
_fetchPricing(); _fetchPricing();
$('#ramValue').data('old-value', $('#ramValue').val());
}); });
$('.input-price').change(function() { $('.input-price').change(function() {
var data = $(this).attr("name"); var data = $(this).attr("name");
cardPricing[data].value = $('input[name=' + data + ']').val(); var input = $('input[name=' + data + ']');
var inputValue = input.val();
if(data === 'ram') {
var ramInput = $('#ramValue');
if ($('#ramValue').data('old-value') < $('#ramValue').val()) {
if($('#ramValue').val() === '1' && minRam === 0.5) {
$("#ramValue").attr('step', 1);
$('#ramValue').val('1');
}
} else {
if($('#ramValue').val() === '0' && minRam === 0.5) {
$("#ramValue").attr('step', 0.5);
$('#ramValue').val('0.5');
}
}
inputValue = $('#ramValue').val();
$('#ramValue').data('old-value', $('#ramValue').val());
}
cardPricing[data].value = inputValue;
_fetchPricing(); _fetchPricing();
}); });
} }
@ -236,4 +273,5 @@ $( document ).ready(function() {
} }
_initPricing(); _initPricing();
}); $('#ramValue').data('old-value', $('#ramValue').val());
});

View File

@ -22,6 +22,39 @@ function setBrandIcon(brand) {
$(document).ready(function () { $(document).ready(function () {
$(function () {
$("select#id_generic_payment_form-product_name").change(function () {
var gp_form = $('#generic-payment-form');
$.ajax({
url: gp_form.attr('action'),
type: 'POST',
data: gp_form.serialize(),
init: function () {
console.log("init")
},
success: function (data) {
if (data.amount !== undefined) {
$("#id_generic_payment_form-amount").val(data.amount);
if (data.isSubscription) {
$('#id_generic_payment_form-recurring').prop('checked', true);
} else {
$('#id_generic_payment_form-recurring').prop('checked', false);
}
} else {
$("#id_generic_payment_form-amount").val('');
$('#id_generic_payment_form-recurring').prop('checked', false);
console.log("No product found")
}
},
error: function (xmlhttprequest, textstatus, message) {
$("#id_generic_payment_form-amount").val('');
$('#id_generic_payment_form-recurring').prop('checked', false);
console.log("Error fetching product")
}
});
})
});
$.ajaxSetup({ $.ajaxSetup({
beforeSend: function (xhr, settings) { beforeSend: function (xhr, settings) {
function getCookie(name) { function getCookie(name) {
@ -124,17 +157,35 @@ $(document).ready(function () {
$('#billing-form').submit(); $('#billing-form').submit();
} }
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length === 2) return parts.pop().split(";").shift();
}
function submitBillingForm() {
var billing_form = $('#billing-form');
var recurring_input = $('#id_generic_payment_form-recurring');
billing_form.append('<input type="hidden" name="generic_payment_form-product_name" value="' + $('#id_generic_payment_form-product_name').val() + '" />');
billing_form.append('<input type="hidden" name="generic_payment_form-amount" value="' + $('#id_generic_payment_form-amount').val() + '" />');
if (recurring_input.attr('type') === 'hidden') {
billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.val() === 'True' ? 'on' : '') + '" />');
} else {
billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.prop('checked') ? 'on' : '') + '" />');
}
billing_form.append('<input type="hidden" name="generic_payment_form-description" value="' + $('#id_generic_payment_form-description').val() + '" />');
billing_form.submit();
}
var $form_new = $('#payment-form-new'); var $form_new = $('#payment-form-new');
$form_new.submit(payWithStripe_new); $form_new.submit(payWithStripe_new);
function payWithStripe_new(e) { function payWithStripe_new(e) {
e.preventDefault(); e.preventDefault();
function stripeTokenHandler(token) { function stripeTokenHandler(token) {
// Insert the token ID into the form so it gets submitted to the server // Insert the token ID into the form so it gets submitted to the server
$('#id_token').val(token.id); $('#id_token').val(token.id);
$('#billing-form').submit(); submitBillingForm();
} }
@ -196,10 +247,10 @@ $(document).ready(function () {
} }
}); });
$('.credit-card-info .btn.choice-btn').click(function(){ $('.credit-card-info .btn.choice-btn').click(function () {
var id = this.dataset['id_card']; var id = this.dataset['id_card'];
$('#id_card').val(id); $('#id_card').val(id);
$('#billing-form').submit(); submitBillingForm();
}); });
}); });

View File

@ -134,3 +134,15 @@ $(document).ready(function() {
$(this).find('.modal-footer .btn').addClass('hide'); $(this).find('.modal-footer .btn').addClass('hide');
}) })
}); });
window.onload = function () {
var locale_dates = document.getElementsByClassName("locale_date");
var formats = ['YYYY-MM-DD hh:mm a'];
var i;
for (i = 0; i < locale_dates.length; i++) {
var oldDate = moment.utc(locale_dates[i].textContent, formats);
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
locale_dates[i].className += ' done';
}
};

View File

@ -25,7 +25,7 @@
<tr> <tr>
<td style="padding-top: 25px; font-size: 16px;"> <td style="padding-top: 25px; font-size: 16px;">
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;"> <p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
{% blocktrans %}You are receiving this email because your virutal machine <strong>{{ vm_name }}</strong> has been cancelled.{% endblocktrans %} {% blocktrans %}You are receiving this email because your virtual machine <strong>{{ vm_name }}</strong> has been cancelled.{% endblocktrans %}
</p> </p>
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;"> <p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
{% blocktrans %}You can always order a new VM by clicking the button below.{% endblocktrans %} {% blocktrans %}You can always order a new VM by clicking the button below.{% endblocktrans %}

View File

@ -2,7 +2,7 @@
{% trans "Virtual Machine Cancellation" %} {% trans "Virtual Machine Cancellation" %}
{% blocktrans %}You are receiving this email because your virutal machine {{vm_name}} has been cancelled.{% endblocktrans %} {% blocktrans %}You are receiving this email because your virtual machine {{vm_name}} has been cancelled.{% endblocktrans %}
{% blocktrans %}You can always order a new VM by following the link below.{% endblocktrans %} {% blocktrans %}You can always order a new VM by following the link below.{% endblocktrans %}
{{ base_url }}{% url 'hosting:create_virtual_machine' %} {{ base_url }}{% url 'hosting:create_virtual_machine' %}

View File

@ -39,7 +39,7 @@
{% endif %} {% endif %}
</span> </span>
</p> </p>
{% if order %} {% if order and vm %}
<p> <p>
<strong>{% trans "Status" %}: </strong> <strong>{% trans "Status" %}: </strong>
<strong> <strong>
@ -93,77 +93,104 @@
<hr> <hr>
<div> <div>
<h4>{% trans "Order summary" %}</h4> <h4>{% trans "Order summary" %}</h4>
<p> {% if vm %}
<strong>{% trans "Product" %}:</strong>&nbsp; <p>
{% if vm.name %} <strong>{% trans "Product" %}:</strong>&nbsp;
{{ vm.name }} {% if vm.name %}
{% else %} {{ vm.name }}
{{ request.session.template.name }} {% else %}
{% endif %} {{ request.session.template.name }}
</p>
<div class="row">
<div class="col-sm-6">
{% if vm.created_at %}
<p>
<span>{% trans "Period" %}: </span>
<span>
<span class="locale_date" data-format="YYYY/MM/DD">{{ vm.created_at|date:'Y-m-d h:i a' }}</span> - <span class="locale_date" data-format="YYYY/MM/DD">{{ subscription_end_date|date:'Y-m-d h:i a' }}</span>
</span>
</p>
{% endif %} {% endif %}
<p> </p>
<span>{% trans "Cores" %}: </span> <div class="row">
{% if vm.cores %}
<strong class="pull-right">{{vm.cores|floatformat}}</strong>
{% else %}
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
{% endif %}
</p>
<p>
<span>{% trans "Memory" %}: </span>
<strong class="pull-right">{{vm.memory}} GB</strong>
</p>
<p>
<span>{% trans "Disk space" %}: </span>
<strong class="pull-right">{{vm.disk_size}} GB</strong>
</p>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
{% if vm.vat > 0 or vm.discount.amount > 0 %}
<div class="col-sm-6"> <div class="col-sm-6">
<div class="subtotal-price"> {% if vm.created_at %}
{% if vm.vat > 0 %} <p>
<p> <span>{% trans "Period" %}: </span>
<strong>{% trans "Subtotal" %} </strong> <span>
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong> <span class="locale_date" data-format="YYYY/MM/DD">{{ vm.created_at|date:'Y-m-d h:i a' }}</span> - <span class="locale_date" data-format="YYYY/MM/DD">{{ subscription_end_date|date:'Y-m-d h:i a' }}</span>
</p> </span>
<p> </p>
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small> {% endif %}
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong> <p>
</p> <span>{% trans "Cores" %}: </span>
{% if vm.cores %}
<strong class="pull-right">{{vm.cores|floatformat}}</strong>
{% else %}
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
{% endif %} {% endif %}
{% if vm.discount.amount > 0 %} </p>
<p class="text-primary"> <p>
{%trans "Discount" as discount_name %} <span>{% trans "Memory" %}: </span>
<strong>{{ vm.discount.name|default:discount_name }} </strong> <strong class="pull-right">{{vm.memory}} GB</strong>
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong> </p>
</p> <p>
{% endif %} <span>{% trans "Disk space" %}: </span>
</div> <strong class="pull-right">{{vm.disk_size}} GB</strong>
</p>
</div> </div>
<div class="col-sm-12"> <div class="col-sm-12">
<hr class="thin-hr"> <hr class="thin-hr">
</div> </div>
{% endif %} {% if vm.vat > 0 or vm.discount.amount > 0 %}
<div class="col-sm-6"> <div class="col-sm-6">
<p class="total-price"> <div class="subtotal-price">
<strong>{% trans "Total" %} </strong> {% if vm.vat > 0 %}
<strong class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF</strong> <p>
</p> <strong>{% trans "Subtotal" %} </strong>
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
</p>
<p>
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p>
{% endif %}
{% if vm.discount.amount > 0 %}
<p class="text-primary">
{%trans "Discount" as discount_name %}
<strong>{{ vm.discount.name|default:discount_name }} </strong>
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
</p>
{% endif %}
</div>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
{% endif %}
<div class="col-sm-6">
<p class="total-price">
<strong>{% trans "Total" %} </strong>
<strong class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF</strong>
</p>
</div>
</div> </div>
</div> {% else %}
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ product_name }}
</p>
<div class="row">
<div class="col-sm-6">
<p>
<span>{% trans "Amount" %}: </span>
<strong class="pull-right">{{order.price|floatformat:2|intcomma}} CHF</strong>
</p>
{% if order.generic_payment_description %}
<p>
<span>{% trans "Description" %}: </span>
<strong class="pull-right">{{order.generic_payment_description}}</strong>
</p>
{% endif %}
{% if order.subscription_id %}
<p>
<span>{% trans "Recurring" %}: </span>
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}} {% trans "of every month" %}</strong>
</p>
{% endif %}
</div>
</div>
{% endif %}
</div> </div>
<hr class="thin-hr"> <hr class="thin-hr">
</div> </div>
@ -229,17 +256,6 @@
<script type="text/javascript"> <script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %} {% trans "Some problem encountered. Please try again later." as err_msg %}
var create_vm_error_message = '{{err_msg|safe}}'; var create_vm_error_message = '{{err_msg|safe}}';
window.onload = function () {
var locale_dates = document.getElementsByClassName("locale_date");
var formats = ['YYYY-MM-DD hh:mm a']
var i;
for (i = 0; i < locale_dates.length; i++) {
var oldDate = moment.utc(locale_dates[i].textContent, formats);
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
locale_dates[i].className += ' done';
}
};
</script> </script>
{%endblock%} {%endblock%}

View File

@ -28,7 +28,7 @@
{% for order in orders %} {% for order in orders %}
<tr> <tr>
<td class="xs-td-inline" data-header="{% trans 'Order Nr.' %}">{{ order.id }}</td> <td class="xs-td-inline" data-header="{% trans 'Order Nr.' %}">{{ order.id }}</td>
<td class="xs-td-bighalf" data-header="{% trans 'Date' %}">{{ order.created_at | date:"M d, Y H:i" }}</td> <td class="xs-td-bighalf locale_date" data-header="{% trans 'Date' %}">{{ order.created_at | date:'Y-m-d h:i a' }}</td>
<td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|floatformat:2|intcomma }}</td> <td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|floatformat:2|intcomma }}</td>
<td class="text-right last-td"> <td class="text-right last-td">
<a class="btn btn-order-detail" href="{% url 'hosting:orders' order.pk %}">{% trans 'See Invoice' %}</a> <a class="btn btn-order-detail" href="{% url 'hosting:orders' order.pk %}">{% trans 'See Invoice' %}</a>

View File

@ -18,8 +18,8 @@
<h3>{%trans "Billing Address" %}</h3> <h3>{%trans "Billing Address" %}</h3>
<hr> <hr>
<form role="form" id="billing-form" method="post" action="" novalidate> <form role="form" id="billing-form" method="post" action="" novalidate>
{% csrf_token %}
{% for field in form %} {% for field in form %}
{% csrf_token %}
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %} {% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
{% endfor %} {% endfor %}
<div class="form-group text-right"> <div class="form-group text-right">

View File

@ -51,7 +51,7 @@
</div> </div>
<div class="vm-detail-item"> <div class="vm-detail-item">
<h2 class="vm-detail-title">{% trans "Status" %} <img src="{% static 'hosting/img/connected.svg' %}" class="un-icon"></h2> <h2 class="vm-detail-title">{% trans "Status" %} <img src="{% static 'hosting/img/connected.svg' %}" class="un-icon"></h2>
<div class="vm-vmid"> <div class="vm-vmid vm-vmid-with-warning">
<div class="vm-item-subtitle">{% trans "Your VM is" %}</div> <div class="vm-item-subtitle">{% trans "Your VM is" %}</div>
<div id="terminate-VM" data-alt="{% trans 'Terminating' %}"> <div id="terminate-VM" data-alt="{% trans 'Terminating' %}">
{% if virtual_machine.state == 'PENDING' %} {% if virtual_machine.state == 'PENDING' %}
@ -74,6 +74,10 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="vm-terminate-warning text-center">
<p>{% trans "Attention:" %}</p>
<p>{% trans "terminating VM can not be reverted." %}</p>
</div>
</div> </div>
</div> </div>
<div class="vm-contact-us"> <div class="vm-contact-us">
@ -105,7 +109,7 @@
<div class="modal-icon"><i class="fa fa-ban" aria-hidden="true"></i></div> <div class="modal-icon"><i class="fa fa-ban" aria-hidden="true"></i></div>
<h4 class="modal-title" id="ModalLabel">{% trans "Terminate your Virtual Machine" %}</h4> <h4 class="modal-title" id="ModalLabel">{% trans "Terminate your Virtual Machine" %}</h4>
<div class="modal-text"> <div class="modal-text">
<p>{% trans "Do you want to cancel your Virtual Machine" %} ?</p> <p>{% trans "Terminated VMs can not be revived and will not be refunded. Do you want to terminate your VM?" %}</p>
<p><strong>{{virtual_machine.name}}</strong></p> <p><strong>{{virtual_machine.name}}</strong></p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -32,6 +32,7 @@ from stored_messages.api import mark_read
from stored_messages.models import Message from stored_messages.models import Message
from stored_messages.settings import stored_messages_settings from stored_messages.settings import stored_messages_settings
from datacenterlight.cms_models import DCLCalculatorPluginModel
from datacenterlight.models import VMTemplate, VMPricing from datacenterlight.models import VMTemplate, VMPricing
from datacenterlight.utils import create_vm, get_cms_integration from datacenterlight.utils import create_vm, get_cms_integration
from hosting.models import UserCardDetail from hosting.models import UserCardDetail
@ -59,7 +60,8 @@ from .forms import (
) )
from .mixins import ProcessVMSelectionMixin, HostingContextMixin from .mixins import ProcessVMSelectionMixin, HostingContextMixin
from .models import ( from .models import (
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
GenericProduct
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -862,32 +864,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
raise Http404 raise Http404
if obj is not None: if obj is not None:
# invoice for previous order if obj.generic_product_id is not None:
try: # generic payment case
vm_detail = VMDetail.objects.get(vm_id=obj.vm_id) logger.debug("Generic payment case")
context['vm'] = vm_detail.__dict__ context['product_name'] = GenericProduct.objects.get(
context['vm']['name'] = '{}-{}'.format( id=obj.generic_product_id
context['vm']['configuration'], context['vm']['vm_id']) ).product_name
price, vat, vat_percent, discount = get_vm_price_with_vat( else:
cpu=context['vm']['cores'], # invoice for previous order
ssd_size=context['vm']['disk_size'], logger.debug("Invoice of VM order")
memory=context['vm']['memory'],
pricing_name=(obj.vm_pricing.name
if obj.vm_pricing else 'default')
)
context['vm']['vat'] = vat
context['vm']['price'] = price
context['vm']['discount'] = discount
context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = price + vat - discount['amount']
context['subscription_end_date'] = vm_detail.end_date()
except VMDetail.DoesNotExist:
try: try:
manager = OpenNebulaManager( vm_detail = VMDetail.objects.get(vm_id=obj.vm_id)
email=owner.email, password=owner.password context['vm'] = vm_detail.__dict__
) context['vm']['name'] = '{}-{}'.format(
vm = manager.get_vm(obj.vm_id) context['vm']['configuration'], context['vm']['vm_id'])
context['vm'] = VirtualMachineSerializer(vm).data
price, vat, vat_percent, discount = get_vm_price_with_vat( price, vat, vat_percent, discount = get_vm_price_with_vat(
cpu=context['vm']['cores'], cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'], ssd_size=context['vm']['disk_size'],
@ -899,23 +889,43 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
context['vm']['price'] = price context['vm']['price'] = price
context['vm']['discount'] = discount context['vm']['discount'] = discount
context['vm']['vat_percent'] = vat_percent context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = ( context['vm']['total_price'] = price + vat - discount['amount']
price + vat - discount['amount'] context['subscription_end_date'] = vm_detail.end_date()
) except VMDetail.DoesNotExist:
except WrongIdError: try:
messages.error( manager = OpenNebulaManager(
self.request, email=owner.email, password=owner.password
_('The VM you are looking for is unavailable at the ' )
'moment. Please contact Data Center Light support.') vm = manager.get_vm(obj.vm_id)
) context['vm'] = VirtualMachineSerializer(vm).data
self.kwargs['error'] = 'WrongIdError' price, vat, vat_percent, discount = get_vm_price_with_vat(
context['error'] = 'WrongIdError' cpu=context['vm']['cores'],
except ConnectionRefusedError: ssd_size=context['vm']['disk_size'],
messages.error( memory=context['vm']['memory'],
self.request, pricing_name=(obj.vm_pricing.name
_('In order to create a VM, you need to create/upload ' if obj.vm_pricing else 'default')
'your SSH KEY first.') )
) context['vm']['vat'] = vat
context['vm']['price'] = price
context['vm']['discount'] = discount
context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = (
price + vat - discount['amount']
)
except WrongIdError:
messages.error(
self.request,
_('The VM you are looking for is unavailable at the '
'moment. Please contact Data Center Light support.')
)
self.kwargs['error'] = 'WrongIdError'
context['error'] = 'WrongIdError'
except ConnectionRefusedError:
messages.error(
self.request,
_('In order to create a VM, you need to create/upload '
'your SSH KEY first.')
)
else: else:
# new order, confirm payment # new order, confirm payment
if 'token' in self.request.session: if 'token' in self.request.session:
@ -1032,14 +1042,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
memory = specs.get('memory') memory = specs.get('memory')
disk_size = specs.get('disk_size') disk_size = specs.get('disk_size')
amount_to_be_charged = specs.get('total_price') amount_to_be_charged = specs.get('total_price')
plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu, plan_name = StripeUtils.get_stripe_plan_name(
memory=memory, cpu=cpu,
disk_size=disk_size) memory=memory,
stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, disk_size=disk_size,
ram=memory, price=amount_to_be_charged
ssd=disk_size, )
version=1, stripe_plan_id = StripeUtils.get_stripe_plan_id(
app='dcl') cpu=cpu,
ram=memory,
ssd=disk_size,
version=1,
app='dcl',
price=amount_to_be_charged
)
stripe_plan = stripe_utils.get_or_create_stripe_plan( stripe_plan = stripe_utils.get_or_create_stripe_plan(
amount=amount_to_be_charged, amount=amount_to_be_charged,
name=plan_name, name=plan_name,
@ -1183,7 +1199,29 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
raise ValidationError(_('Invalid number of cores')) raise ValidationError(_('Invalid number of cores'))
def validate_memory(self, value): def validate_memory(self, value):
if (value > 200) or (value < 1): if 'pid' in self.request.POST:
try:
plugin = DCLCalculatorPluginModel.objects.get(
id=self.request.POST['pid']
)
except DCLCalculatorPluginModel.DoesNotExist as dne:
logger.error(
str(dne) + " plugin_id: " + self.request.POST['pid']
)
raise ValidationError(_('Invalid calculator properties'))
if plugin.enable_512mb_ram:
if value % 1 == 0 or value == 0.5:
logger.debug(
"Given ram {value} is either 0.5 or a"
" whole number".format(value=value)
)
if (value > 200) or (value < 0.5):
raise ValidationError(_('Invalid RAM size'))
else:
raise ValidationError(_('Invalid RAM size'))
elif (value > 200) or (value < 1) or (value % 1 != 0):
raise ValidationError(_('Invalid RAM size'))
else:
raise ValidationError(_('Invalid RAM size')) raise ValidationError(_('Invalid RAM size'))
def validate_storage(self, value): def validate_storage(self, value):
@ -1203,7 +1241,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
cores = request.POST.get('cpu') cores = request.POST.get('cpu')
cores_field = forms.IntegerField(validators=[self.validate_cores]) cores_field = forms.IntegerField(validators=[self.validate_cores])
memory = request.POST.get('ram') memory = request.POST.get('ram')
memory_field = forms.IntegerField(validators=[self.validate_memory]) memory_field = forms.FloatField(validators=[self.validate_memory])
storage = request.POST.get('storage') storage = request.POST.get('storage')
storage_field = forms.IntegerField(validators=[self.validate_storage]) storage_field = forms.IntegerField(validators=[self.validate_storage])
template_id = int(request.POST.get('config')) template_id = int(request.POST.get('config'))
@ -1267,7 +1305,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
'price': price, 'price': price,
'vat': vat, 'vat': vat,
'vat_percent': vat_percent, 'vat_percent': vat_percent,
'total_price': price + vat - discount['amount'], 'total_price': round(price + vat - discount['amount'], 2),
'pricing_name': vm_pricing_name 'pricing_name': vm_pricing_name
} }
@ -1394,7 +1432,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
terminated = manager.delete_vm(vm.id) terminated = manager.delete_vm(vm.id)
if not terminated: if not terminated:
logger.debug( logger.error(
"manager.delete_vm returned False. Hence, error making " "manager.delete_vm returned False. Hence, error making "
"xml-rpc call to delete vm failed." "xml-rpc call to delete vm failed."
) )
@ -1404,6 +1442,9 @@ class VirtualMachineView(LoginRequiredMixin, View):
try: try:
manager.get_vm(vm.id) manager.get_vm(vm.id)
except WrongIdError: except WrongIdError:
logger.error(
"VM {} not found. So, its terminated.".format(vm.id)
)
response['status'] = True response['status'] = True
response['text'] = ugettext('Terminated') response['text'] = ugettext('Terminated')
vm_detail_obj = VMDetail.objects.filter( vm_detail_obj = VMDetail.objects.filter(
@ -1421,6 +1462,10 @@ class VirtualMachineView(LoginRequiredMixin, View):
break break
else: else:
sleep(2) sleep(2)
if not response['status']:
response['text'] = _("VM terminate action timed out. Please "
"contact support@datacenterlight.ch for "
"further information.")
context = { context = {
'vm_name': vm_name, 'vm_name': vm_name,
'base_url': "{0}://{1}".format( 'base_url': "{0}://{1}".format(
@ -1441,11 +1486,13 @@ class VirtualMachineView(LoginRequiredMixin, View):
email = BaseEmail(**email_data) email = BaseEmail(**email_data)
email.send() email.send()
admin_email_body.update(response) admin_email_body.update(response)
admin_msg_sub = "VM and Subscription for VM {} and user: {}".format(
vm.id,
owner.email
)
email_to_admin_data = { email_to_admin_data = {
'subject': "Deleted VM and Subscription for VM {vm_id} and " 'subject': ("Deleted " if response['status']
"user: {user}".format( else "ERROR deleting ") + admin_msg_sub,
vm_id=vm.id, user=owner.email
),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'], 'to': ['info@ungleich.ch'],
'body': "\n".join( 'body': "\n".join(

View File

@ -110,7 +110,7 @@ class OpenNebulaManager():
raise UserExistsError() raise UserExistsError()
except OpenNebulaException as err: except OpenNebulaException as err:
logger.error('OpenNebulaException error: {0}'.format(err)) logger.error('OpenNebulaException error: {0}'.format(err))
logger.debug('User exists but password is wrong') logger.error('User exists but password is wrong')
raise UserCredentialError() raise UserCredentialError()
except WrongNameError: except WrongNameError:
@ -148,7 +148,7 @@ class OpenNebulaManager():
) )
return opennebula_user return opennebula_user
except ConnectionRefusedError: except ConnectionRefusedError:
logger.info( logger.error(
'Could not connect to host: {host} via protocol {protocol}'.format( 'Could not connect to host: {host} via protocol {protocol}'.format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,
protocol=settings.OPENNEBULA_PROTOCOL) protocol=settings.OPENNEBULA_PROTOCOL)
@ -160,7 +160,7 @@ class OpenNebulaManager():
user_pool = oca.UserPool(self.oneadmin_client) user_pool = oca.UserPool(self.oneadmin_client)
user_pool.info() user_pool.info()
except ConnectionRefusedError: except ConnectionRefusedError:
logger.info( logger.error(
'Could not connect to host: {host} via protocol {protocol}'.format( 'Could not connect to host: {host} via protocol {protocol}'.format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,
protocol=settings.OPENNEBULA_PROTOCOL) protocol=settings.OPENNEBULA_PROTOCOL)
@ -174,7 +174,7 @@ class OpenNebulaManager():
vm_pool.info() vm_pool.info()
return vm_pool return vm_pool
except AttributeError: except AttributeError:
logger.info('Could not connect via client, using oneadmin instead') logger.error('Could not connect via client, using oneadmin instead')
try: try:
vm_pool = oca.VirtualMachinePool(self.oneadmin_client) vm_pool = oca.VirtualMachinePool(self.oneadmin_client)
vm_pool.info(filter=-2) vm_pool.info(filter=-2)
@ -183,7 +183,7 @@ class OpenNebulaManager():
raise ConnectionRefusedError raise ConnectionRefusedError
except ConnectionRefusedError: except ConnectionRefusedError:
logger.info( logger.error(
'Could not connect to host: {host} via protocol {protocol}'.format( 'Could not connect to host: {host} via protocol {protocol}'.format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,
protocol=settings.OPENNEBULA_PROTOCOL) protocol=settings.OPENNEBULA_PROTOCOL)
@ -249,8 +249,8 @@ class OpenNebulaManager():
vm_specs = vm_specs_formatter.format( vm_specs = vm_specs_formatter.format(
vcpu=int(specs['cpu']), vcpu=int(specs['cpu']),
cpu=0.1 * int(specs['cpu']), cpu=0.1 * int(specs['cpu']),
memory=1024 * int(specs['memory']), memory=(512 if specs['memory'] == 0.5 else
1024 * int(specs['memory'])),
) )
vm_specs += """<DISK> vm_specs += """<DISK>
<TYPE>fs</TYPE> <TYPE>fs</TYPE>
@ -269,8 +269,8 @@ class OpenNebulaManager():
vm_specs = vm_specs_formatter.format( vm_specs = vm_specs_formatter.format(
vcpu=int(specs['cpu']), vcpu=int(specs['cpu']),
cpu=0.1 * int(specs['cpu']), cpu=0.1 * int(specs['cpu']),
memory=1024 * int(specs['memory']), memory=(512 if specs['memory'] == 0.5 else
1024 * int(specs['memory'])),
) )
vm_specs += """<DISK> vm_specs += """<DISK>
<TYPE>fs</TYPE> <TYPE>fs</TYPE>
@ -325,14 +325,14 @@ class OpenNebulaManager():
) )
vm_terminated = True vm_terminated = True
except socket.timeout as socket_err: except socket.timeout as socket_err:
logger.info("Socket timeout error: {0}".format(socket_err)) logger.error("Socket timeout error: {0}".format(socket_err))
except OpenNebulaException as opennebula_err: except OpenNebulaException as opennebula_err:
logger.info( logger.error(
"OpenNebulaException error: {0}".format(opennebula_err)) "OpenNebulaException error: {0}".format(opennebula_err))
except OSError as os_err: except OSError as os_err:
logger.info("OSError : {0}".format(os_err)) logger.error("OSError : {0}".format(os_err))
except ValueError as value_err: except ValueError as value_err:
logger.info("ValueError : {0}".format(value_err)) logger.error("ValueError : {0}".format(value_err))
return vm_terminated return vm_terminated
@ -342,7 +342,7 @@ class OpenNebulaManager():
template_pool.info() template_pool.info()
return template_pool return template_pool
except ConnectionRefusedError: except ConnectionRefusedError:
logger.info( logger.error(
"""Could not connect to host: {host} via protocol """Could not connect to host: {host} via protocol
{protocol}""".format( {protocol}""".format(
host=settings.OPENNEBULA_DOMAIN, host=settings.OPENNEBULA_DOMAIN,

View File

@ -34,7 +34,6 @@ django-meta==1.2
django-meta-mixin==0.3.0 django-meta-mixin==0.3.0
django-model-utils==2.5 django-model-utils==2.5
django-mptt==0.8.4 django-mptt==0.8.4
django-multisite==1.4.1
django-parler==1.6.3 django-parler==1.6.3
django-phonenumber-field==1.1.0 django-phonenumber-field==1.1.0
django-polymorphic==0.9.2 django-polymorphic==0.9.2
@ -69,7 +68,7 @@ model-mommy==1.2.6
phonenumbers==7.4.0 phonenumbers==7.4.0
phonenumberslite==7.4.0 phonenumberslite==7.4.0
psycopg2==2.7.3.2 psycopg2==2.7.3.2
pycryptodome==3.4 pycryptodome==3.6.6
pylibmc==1.5.1 pylibmc==1.5.1
python-dateutil==2.5.3 python-dateutil==2.5.3
python-slugify==1.2.0 python-slugify==1.2.0

View File

@ -31,7 +31,7 @@
</a> </a>
</li> </li>
<li> <li>
<a href="{% url 'djangocms_blog:posts-latest-feed' %}"> <a href="https://blog.ungleich.ch/en-us/cms/blog/feed/">
<span class="fa-stack fa-lg"> <span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i> <i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-rss fa-stack-1x fa-inverse"></i> <i class="fa fa-rss fa-stack-1x fa-inverse"></i>

View File

@ -1,6 +1,7 @@
import decimal import decimal
import logging import logging
import subprocess import subprocess
from oca.pool import WrongIdError from oca.pool import WrongIdError
from datacenterlight.models import VMPricing from datacenterlight.models import VMPricing
@ -80,7 +81,7 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'):
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price)) (decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
cents = decimal.Decimal('.01') cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP) price = price.quantize(cents, decimal.ROUND_HALF_UP)
return float(price) return round(float(price), 2)
def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
@ -126,9 +127,10 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
vat = vat.quantize(cents, decimal.ROUND_HALF_UP) vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
discount = { discount = {
'name': pricing.discount_name, 'name': pricing.discount_name,
'amount': float(pricing.discount_amount), 'amount': round(float(pricing.discount_amount), 2)
} }
return float(price), float(vat), float(vat_percent), discount return (round(float(price), 2), round(float(vat), 2),
round(float(vat_percent), 2), discount)
def ping_ok(host_ipv6): def ping_ok(host_ipv6):

View File

@ -291,7 +291,8 @@ class StripeUtils(object):
return charge return charge
@staticmethod @staticmethod
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None): def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None,
price=None):
""" """
Returns the Stripe plan id string of the form Returns the Stripe plan id string of the form
`dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters
@ -303,6 +304,7 @@ class StripeUtils(object):
:param version: The version of the Stripe plans :param version: The version of the Stripe plans
:param app: The application to which the stripe plan belongs :param app: The application to which the stripe plan belongs
to. By default it is 'dcl' to. By default it is 'dcl'
:param price: The price for this plan
:return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` :return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb`
""" """
dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu,
@ -314,19 +316,30 @@ class StripeUtils(object):
stripe_plan_id_string = '{app}-v{version}-{plan}'.format( stripe_plan_id_string = '{app}-v{version}-{plan}'.format(
app=app, app=app,
version=version, version=version,
plan=dcl_plan_string) plan=dcl_plan_string
return stripe_plan_id_string )
if price is not None:
stripe_plan_id_string_with_price = '{}-{}chf'.format(
stripe_plan_id_string,
round(price, 2)
)
return stripe_plan_id_string_with_price
else:
return stripe_plan_id_string
@staticmethod @staticmethod
def get_stripe_plan_name(cpu, memory, disk_size): def get_stripe_plan_name(cpu, memory, disk_size, price):
""" """
Returns the Stripe plan name Returns the Stripe plan name
:return: :return:
""" """
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
cpu=cpu, "{price} CHF".format(
memory=memory, cpu=cpu,
disk_size=disk_size) memory=memory,
disk_size=disk_size,
price=round(price, 2)
)
@handleStripeError @handleStripeError
def set_subscription_meta_data(self, subscription_id, meta_data): def set_subscription_meta_data(self, subscription_id, meta_data):