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,8 +1,8 @@
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(HostingBill)
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 utils.hosting_utils import get_all_public_keys
from .models import UserHostingKey
from .models import UserHostingKey, GenericProduct
logger = logging.getLogger(__name__)
@ -52,6 +52,93 @@ class HostingUserLoginForm(forms.Form):
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):
confirm_password = forms.CharField(label=_("Confirm Password"),
widget=forms.PasswordInput())
@ -111,7 +198,7 @@ class UserHostingKeyForm(forms.ModelForm):
public_key=openssh_pubkey_str).first().name
KEY_EXISTS_MESSAGE = _(
"This key exists already with the name \"%(name)s\"") % {
'name': key_name}
'name': key_name}
raise forms.ValidationError(KEY_EXISTS_MESSAGE)
with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file:

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -209,7 +209,7 @@ msgstr "Du hast eine neue virtuelle Maschine bestellt!"
#, python-format
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."
msgstr "Um die Rechnung zu sehen, klicke auf den Button unten."
@ -222,7 +222,7 @@ msgstr "Dein Data Center Light Team"
#, python-format
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."
msgstr "Um die Rechnung zu sehen, klicke auf den Link unten."
@ -249,7 +249,7 @@ msgstr "VM Kündigung"
#, python-format
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."
msgstr ""
"Du erhälst diese E-Mail, da deine virtuelle Maschine <strong>%(vm_name)s</"
@ -265,7 +265,7 @@ msgstr "NEUE VM"
#, python-format
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."
msgstr ""
"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 "
"taken to the Submit Payment Page."
msgstr ""
"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst "
"ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt "
"hast."
"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, "
"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast."
msgid "SUBMIT"
msgstr "ABSENDEN"
@ -469,9 +468,9 @@ msgid ""
"database."
msgstr ""
"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine "
"Kreditkartendetails unten an. Die Bezahlung wird über "
"<a href=\"https://stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. "
"Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank."
"Kreditkartendetails unten an. Die Bezahlung wird über <a href=\"https://"
"stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. Wir speichern Deine "
"Kreditkartendetails nicht in unserer Datenbank."
msgid ""
"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. "
"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?"
msgstr "Etwas funktioniert nicht?"
@ -643,8 +648,12 @@ msgstr "KONTAKT"
msgid "Terminate your Virtual Machine"
msgstr "Deine Virtuelle Maschine beenden"
msgid "Do you want to cancel your Virtual Machine"
msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst"
msgid ""
"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
msgid ""
@ -723,8 +732,8 @@ msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
#, python-brace-format
msgid "An error occurred while associating the card. Details: {details}"
msgstr "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: "
"{details}"
msgstr ""
"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
msgid "Successfully associated the card with your account"
msgstr "Die Karte wurde erfolgreich mit deinem Konto verbunden"
@ -798,6 +807,11 @@ msgstr ""
msgid "Error terminating VM"
msgstr "Fehler beenden VM"
msgid ""
"VM terminate action timed out. Please contact support@datacenterlight.ch for "
"further information."
msgstr ""
#, python-format
msgid "Virtual Machine %(vm_name)s Cancelled"
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 "
"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"
#~ 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 membership.models import StripeCustomer, CustomUser
from utils.models import BillingAddress
from utils.mixins import AssignPermissionsMixin
from utils.models import BillingAddress
from utils.stripe_utils import StripeUtils
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):
ORDER_APPROVED_STATUS = 'Approved'
ORDER_DECLINED_STATUS = 'Declined'
@ -80,7 +104,13 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
OrderDetail, null=True, blank=True, default=None,
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',)
class Meta:
@ -89,11 +119,18 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
)
def __str__(self):
return ("Order Nr: #{} - VM_ID: {} - {} - {} - "
"Specs: {} - Price: {}").format(
hosting_order_str = ("Order Nr: #{} - VM_ID: {} - {} - {} - "
"Specs: {} - Price: {}").format(
self.id, self.vm_id, self.customer.user.email, self.created_at,
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
def status(self):

View file

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

View file

@ -157,6 +157,10 @@ $( document ).ready(function() {
/* ---------------------------------------------
Scripts initialization
--------------------------------------------- */
var minRam = 1;
if(window.minRam){
minRam = window.minRam;
}
var cardPricing = {
'cpu': {
'id': 'coreValue',
@ -168,7 +172,7 @@ $( document ).ready(function() {
'ram': {
'id': 'ramValue',
'value': 2,
'min': 1,
'min': minRam,
'max': 200,
'interval': 1
},
@ -188,21 +192,54 @@ $( document ).ready(function() {
var data = $(this).data('minus');
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();
$('#ramValue').data('old-value', $('#ramValue').val());
});
$('.fa-plus-circle.right').click(function(event) {
var data = $(this).data('plus');
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();
$('#ramValue').data('old-value', $('#ramValue').val());
});
$('.input-price').change(function() {
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();
});
}
@ -236,4 +273,5 @@ $( document ).ready(function() {
}
_initPricing();
});
$('#ramValue').data('old-value', $('#ramValue').val());
});

View file

@ -22,6 +22,39 @@ function setBrandIcon(brand) {
$(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({
beforeSend: function (xhr, settings) {
function getCookie(name) {
@ -124,17 +157,35 @@ $(document).ready(function () {
$('#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');
$form_new.submit(payWithStripe_new);
function payWithStripe_new(e) {
e.preventDefault();
function stripeTokenHandler(token) {
// Insert the token ID into the form so it gets submitted to the server
$('#id_token').val(token.id);
$('#billing-form').submit();
submitBillingForm();
}
@ -196,10 +247,10 @@ $(document).ready(function () {
}
});
$('.credit-card-info .btn.choice-btn').click(function(){
var id = this.dataset['id_card'];
$('#id_card').val(id);
$('#billing-form').submit();
$('.credit-card-info .btn.choice-btn').click(function () {
var id = this.dataset['id_card'];
$('#id_card').val(id);
submitBillingForm();
});
});

View file

@ -134,3 +134,15 @@ $(document).ready(function() {
$(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>
<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;">
{% 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 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 %}

View file

@ -2,7 +2,7 @@
{% 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 %}
{{ base_url }}{% url 'hosting:create_virtual_machine' %}

View file

@ -39,7 +39,7 @@
{% endif %}
</span>
</p>
{% if order %}
{% if order and vm %}
<p>
<strong>{% trans "Status" %}: </strong>
<strong>
@ -93,77 +93,104 @@
<hr>
<div>
<h4>{% trans "Order summary" %}</h4>
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{% if vm.name %}
{{ vm.name }}
{% else %}
{{ request.session.template.name }}
{% endif %}
</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>
{% if vm %}
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{% if vm.name %}
{{ vm.name }}
{% else %}
{{ request.session.template.name }}
{% endif %}
<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 %}
</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 %}
</p>
<div class="row">
<div class="col-sm-6">
<div class="subtotal-price">
{% if vm.vat > 0 %}
<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>
{% 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 %}
<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 %}
{% 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>
</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>
{% 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>
{% if vm.vat > 0 or vm.discount.amount > 0 %}
<div class="col-sm-6">
<div class="subtotal-price">
{% if vm.vat > 0 %}
<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>
{% 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>
<hr class="thin-hr">
</div>
@ -229,17 +256,6 @@
<script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %}
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>
{%endblock%}

View file

@ -28,7 +28,7 @@
{% for order in orders %}
<tr>
<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="text-right last-td">
<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>
<hr>
<form role="form" id="billing-form" method="post" action="" novalidate>
{% csrf_token %}
{% for field in form %}
{% csrf_token %}
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
{% endfor %}
<div class="form-group text-right">

View file

@ -51,7 +51,7 @@
</div>
<div class="vm-detail-item">
<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 id="terminate-VM" data-alt="{% trans 'Terminating' %}">
{% if virtual_machine.state == 'PENDING' %}
@ -74,6 +74,10 @@
{% endif %}
</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 class="vm-contact-us">
@ -105,7 +109,7 @@
<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>
<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>
</div>
<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.settings import stored_messages_settings
from datacenterlight.cms_models import DCLCalculatorPluginModel
from datacenterlight.models import VMTemplate, VMPricing
from datacenterlight.utils import create_vm, get_cms_integration
from hosting.models import UserCardDetail
@ -59,7 +60,8 @@ from .forms import (
)
from .mixins import ProcessVMSelectionMixin, HostingContextMixin
from .models import (
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
GenericProduct
)
logger = logging.getLogger(__name__)
@ -862,32 +864,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
raise Http404
if obj is not None:
# invoice for previous order
try:
vm_detail = VMDetail.objects.get(vm_id=obj.vm_id)
context['vm'] = vm_detail.__dict__
context['vm']['name'] = '{}-{}'.format(
context['vm']['configuration'], context['vm']['vm_id'])
price, vat, vat_percent, discount = get_vm_price_with_vat(
cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'],
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:
if obj.generic_product_id is not None:
# generic payment case
logger.debug("Generic payment case")
context['product_name'] = GenericProduct.objects.get(
id=obj.generic_product_id
).product_name
else:
# invoice for previous order
logger.debug("Invoice of VM order")
try:
manager = OpenNebulaManager(
email=owner.email, password=owner.password
)
vm = manager.get_vm(obj.vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
vm_detail = VMDetail.objects.get(vm_id=obj.vm_id)
context['vm'] = vm_detail.__dict__
context['vm']['name'] = '{}-{}'.format(
context['vm']['configuration'], context['vm']['vm_id'])
price, vat, vat_percent, discount = get_vm_price_with_vat(
cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'],
@ -899,23 +889,43 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
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.')
)
context['vm']['total_price'] = price + vat - discount['amount']
context['subscription_end_date'] = vm_detail.end_date()
except VMDetail.DoesNotExist:
try:
manager = OpenNebulaManager(
email=owner.email, password=owner.password
)
vm = manager.get_vm(obj.vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
price, vat, vat_percent, discount = get_vm_price_with_vat(
cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'],
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']
)
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:
# new order, confirm payment
if 'token' in self.request.session:
@ -1032,14 +1042,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
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)
stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu,
ram=memory,
ssd=disk_size,
version=1,
app='dcl')
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,
@ -1183,7 +1199,29 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
raise ValidationError(_('Invalid number of cores'))
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'))
def validate_storage(self, value):
@ -1203,7 +1241,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
cores = request.POST.get('cpu')
cores_field = forms.IntegerField(validators=[self.validate_cores])
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_field = forms.IntegerField(validators=[self.validate_storage])
template_id = int(request.POST.get('config'))
@ -1267,7 +1305,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
'price': price,
'vat': vat,
'vat_percent': vat_percent,
'total_price': price + vat - discount['amount'],
'total_price': round(price + vat - discount['amount'], 2),
'pricing_name': vm_pricing_name
}
@ -1394,7 +1432,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
terminated = manager.delete_vm(vm.id)
if not terminated:
logger.debug(
logger.error(
"manager.delete_vm returned False. Hence, error making "
"xml-rpc call to delete vm failed."
)
@ -1404,6 +1442,9 @@ class VirtualMachineView(LoginRequiredMixin, View):
try:
manager.get_vm(vm.id)
except WrongIdError:
logger.error(
"VM {} not found. So, its terminated.".format(vm.id)
)
response['status'] = True
response['text'] = ugettext('Terminated')
vm_detail_obj = VMDetail.objects.filter(
@ -1421,6 +1462,10 @@ class VirtualMachineView(LoginRequiredMixin, View):
break
else:
sleep(2)
if not response['status']:
response['text'] = _("VM terminate action timed out. Please "
"contact support@datacenterlight.ch for "
"further information.")
context = {
'vm_name': vm_name,
'base_url': "{0}://{1}".format(
@ -1441,11 +1486,13 @@ class VirtualMachineView(LoginRequiredMixin, View):
email = BaseEmail(**email_data)
email.send()
admin_email_body.update(response)
admin_msg_sub = "VM and Subscription for VM {} and user: {}".format(
vm.id,
owner.email
)
email_to_admin_data = {
'subject': "Deleted VM and Subscription for VM {vm_id} and "
"user: {user}".format(
vm_id=vm.id, user=owner.email
),
'subject': ("Deleted " if response['status']
else "ERROR deleting ") + admin_msg_sub,
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'],
'body': "\n".join(