diff --git a/Changelog b/Changelog index 538a862d..b85dd4ff 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,25 @@ 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) * #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) diff --git a/datacenterlight/cms_models.py b/datacenterlight/cms_models.py index 62a7b312..2d1a98b5 100644 --- a/datacenterlight/cms_models.py +++ b/datacenterlight/cms_models.py @@ -180,6 +180,10 @@ class DCLNavbarPluginModel(CMSPlugin): default=True, 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): # used only if atleast one logo exists @@ -350,3 +354,11 @@ class DCLCalculatorPluginModel(CMSPlugin): "in the backend to be automatically listed in this " "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) diff --git a/datacenterlight/cms_plugins.py b/datacenterlight/cms_plugins.py index 95a496d8..c3ec974f 100644 --- a/datacenterlight/cms_plugins.py +++ b/datacenterlight/cms_plugins.py @@ -9,6 +9,7 @@ from .cms_models import ( DCLSectionPromoPluginModel, DCLCalculatorPluginModel ) from .models import VMTemplate +from datacenterlight.utils import clear_all_session_vars @plugin_pool.register_plugin @@ -85,6 +86,7 @@ class DCLCalculatorPlugin(CMSPluginBase): require_parent = True def render(self, context, instance, placeholder): + clear_all_session_vars(context['request']) context = super(DCLCalculatorPlugin, self).render( context, instance, placeholder ) @@ -92,11 +94,13 @@ class DCLCalculatorPlugin(CMSPluginBase): if ids: context['templates'] = VMTemplate.objects.filter( vm_type=instance.vm_type - ).filter(opennebula_vm_template_id__in=ids) + ).filter(opennebula_vm_template_id__in=ids).order_by('name') else: context['templates'] = VMTemplate.objects.filter( 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 diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 1b66b640..d43e91ea 100644 --- a/datacenterlight/locale/de/LC_MESSAGES/django.po +++ b/datacenterlight/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\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" "Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -293,6 +293,9 @@ msgstr "Registrieren" msgid "Billing Address" msgstr "Rechnungsadresse" +msgid "Make a payment" +msgstr "" + msgid "Your Order" msgstr "Deine Bestellung" @@ -336,9 +339,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=" @@ -395,12 +398,35 @@ msgstr "Bestellungsübersicht" msgid "Product" msgstr "Produkt" +msgid "Amount" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Recurring" +msgstr "" + msgid "Subtotal" msgstr "Zwischensumme" msgid "VAT" 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 msgid "" "By clicking \"Place order\" this plan will charge your credit card account " @@ -541,8 +567,33 @@ msgstr "" #, 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 "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." msgstr "Danke für Deine Bestellung." diff --git a/datacenterlight/migrations/0025_dclnavbarpluginmodel_show_login_option.py b/datacenterlight/migrations/0025_dclnavbarpluginmodel_show_login_option.py new file mode 100644 index 00000000..e9ec57ba --- /dev/null +++ b/datacenterlight/migrations/0025_dclnavbarpluginmodel_show_login_option.py @@ -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.'), + ), + ] diff --git a/datacenterlight/migrations/0026_dclcalculatorpluginmodel_default_selected_template.py b/datacenterlight/migrations/0026_dclcalculatorpluginmodel_default_selected_template.py new file mode 100644 index 00000000..047d4096 --- /dev/null +++ b/datacenterlight/migrations/0026_dclcalculatorpluginmodel_default_selected_template.py @@ -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), + ), + ] diff --git a/datacenterlight/migrations/0027_dclcalculatorpluginmodel_enable_512mb_ram.py b/datacenterlight/migrations/0027_dclcalculatorpluginmodel_enable_512mb_ram.py new file mode 100644 index 00000000..bd639c9d --- /dev/null +++ b/datacenterlight/migrations/0027_dclcalculatorpluginmodel_enable_512mb_ram.py @@ -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), + ), + ] diff --git a/datacenterlight/static/datacenterlight/css/common.css b/datacenterlight/static/datacenterlight/css/common.css index 28674b30..00ee52cc 100644 --- a/datacenterlight/static/datacenterlight/css/common.css +++ b/datacenterlight/static/datacenterlight/css/common.css @@ -179,4 +179,10 @@ footer .dcl-link-separator::before { .new-card-button-margin button{ margin-top: 5px; margin-bottom: 5px; -} \ No newline at end of file +} + +.input-no-border { + border: none !important; + background: transparent !important; + resize: none; +} diff --git a/datacenterlight/static/datacenterlight/js/main.js b/datacenterlight/static/datacenterlight/js/main.js index 292e8c16..65db1d6b 100644 --- a/datacenterlight/static/datacenterlight/js/main.js +++ b/datacenterlight/static/datacenterlight/js/main.js @@ -5,6 +5,10 @@ /* --------------------------------------------- Scripts initialization --------------------------------------------- */ + var minRam = 1; + if(window.minRam){ + minRam = window.minRam; + } var cardPricing = { 'cpu': { 'id': 'coreValue', @@ -16,7 +20,7 @@ 'ram': { 'id': 'ramValue', 'value': 2, - 'min': 1, + 'min': minRam, 'max': 200, 'interval': 1 }, @@ -40,6 +44,7 @@ _initNavUrl(); _initPricing(); ajaxForms(); + $('#ramValue').data('old-value', $('#ramValue').val()); }); $(window).resize(function() { @@ -144,21 +149,54 @@ 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(); }); } diff --git a/datacenterlight/templates/datacenterlight/cms/navbar.html b/datacenterlight/templates/datacenterlight/cms/navbar.html index ae6643aa..886a5009 100644 --- a/datacenterlight/templates/datacenterlight/cms/navbar.html +++ b/datacenterlight/templates/datacenterlight/cms/navbar.html @@ -35,14 +35,16 @@ {% endif %} </li> {% endif %} - {% if not request.user.is_authenticated %} - <li> - <a href="{% url 'hosting:login' %}">{% trans "Login" %} <span class="fa fa-sign-in"></span></a> - </li> - {% else %} - <li> - <a href="{% url 'hosting:dashboard' %}">{% trans "Dashboard" %}</a> - </li> + {% if instance.show_login_option %} + {% if not request.user.is_authenticated %} + <li> + <a href="{% url 'hosting:login' %}">{% trans "Login" %} <span class="fa fa-sign-in"></span></a> + </li> + {% else %} + <li> + <a href="{% url 'hosting:dashboard' %}">{% trans "Dashboard" %}</a> + </li> + {% endif %} {% endif %} {% comment %} <!-- to be used when more than one option for language --> diff --git a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html index 72ca5a05..f9896f17 100644 --- a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html +++ b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html @@ -9,11 +9,14 @@ window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}}; window.hddUnitPrice = {{vm_pricing.hdd_unit_price|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> {% endif %} <form id="order_form" method="POST" action="{{calculator_form_url}}" data-toggle="validator" role="form"> {% csrf_token %} + <input type="hidden" name="pid" value="{{instance.id}}"> <div class="title"> <h3>{% trans "VM hosting" %} </h3> </div> @@ -54,8 +57,8 @@ <div class="form-group"> <div class="description input"> <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" - data-error="{% trans 'Please enter a value in range 1 - 200.' %}" required> + <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="{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}" required step="1"> <span> GB RAM</span> <i class="fa fa-plus-circle right" data-plus="ram" aria-hidden="true"></i> </div> @@ -91,11 +94,12 @@ <label for="config">OS</label> <select name="config"> {% 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 %} </select> </div> </div> <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> -</form> \ No newline at end of file +</form> diff --git a/datacenterlight/templates/datacenterlight/landing_payment.html b/datacenterlight/templates/datacenterlight/landing_payment.html index 4c43f41c..fb6d51b0 100644 --- a/datacenterlight/templates/datacenterlight/landing_payment.html +++ b/datacenterlight/templates/datacenterlight/landing_payment.html @@ -67,36 +67,49 @@ </div> <div class="dcl-payment-box"> <div class="dcl-payment-section"> - <h3>{%trans "Your Order" %}</h3> - <hr class="top-hr"> - <div class="dcl-payment-order"> - <p>{% trans "Cores"%} <strong class="pull-right">{{request.session.specs.cpu|floatformat}}</strong></p> - <hr> - <p>{% trans "Memory"%} <strong class="pull-right">{{request.session.specs.memory|floatformat}} GB</strong></p> - <hr> - <p>{% trans "Disk space"%} <strong class="pull-right">{{request.session.specs.disk_size|floatformat}} GB</strong></p> - <hr> - <p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p> - <hr> - <p> - <strong>{%trans "Total" %}</strong> - <small> - ({% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %}) - </small> - <strong class="pull-right">{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}</strong> - </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> - <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> + {% if generic_payment_form %} + <h3>{%trans "Make a payment" %}</h3> + <hr class="top-hr"> + <form role="form" id="generic-payment-form" method="post" action="" novalidate> + {% csrf_token %} + <input type="hidden" name="product" value="1" /> + {% for field in generic_payment_form %} + {% bootstrap_field field type='fields'%} + {% endfor %} + <p class="text-danger">{{generic_payment_form.non_field_errors|striptags}}</p> + </form> + {% else %} + <h3>{%trans "Your Order" %}</h3> + <hr class="top-hr"> + <div class="dcl-payment-order"> + <p>{% trans "Cores"%} <strong class="pull-right">{{request.session.specs.cpu|floatformat}}</strong></p> + <hr> + <p>{% trans "Memory"%} <strong class="pull-right">{{request.session.specs.memory|floatformat}} GB</strong></p> + <hr> + <p>{% trans "Disk space"%} <strong class="pull-right">{{request.session.specs.disk_size|floatformat}} GB</strong></p> + <hr> + <p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p> + <hr> + <p> + <strong>{%trans "Total" %}</strong> + <small> + ({% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %}) + </small> + <strong class="pull-right">{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}</strong> + </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> + <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 class="dcl-payment-box"> diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 49347ba2..31933e12 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -47,61 +47,88 @@ <hr> <div> <h4>{% trans "Order summary" %}</h4> - <p> - <strong>{% trans "Product" %}:</strong> - {{ request.session.template.name }} - </p> - <div class="row"> - <div class="col-sm-6"> + {% if generic_payment_details %} <p> - <span>{% trans "Cores" %}: </span> - <strong class="pull-right">{{vm.cpu|floatformat}}</strong> + <strong>{% trans "Product" %}:</strong> + {{ generic_payment_details.product_name }} </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 %} + <div class="row"> + <div class="col-sm-6"> + <p> + <span>{% trans "Amount" %}: </span> + <strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong> + </p> + {% if generic_payment_details.description %} <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> + <span>{% trans "Description" %}: </span> + <strong class="pull-right">{{generic_payment_details.description}}</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> + {% if generic_payment_details.recurring %} + <p> + <span>{% trans "Recurring" %}: </span> + <strong class="pull-right">Yes</strong> </p> {% endif %} </div> </div> - <div class="col-sm-12"> - <hr class="thin-hr"> + {% else %} + <p> + <strong>{% trans "Product" %}:</strong> + {{ 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> {% 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> <hr class="thin-hr"> </div> @@ -109,7 +136,15 @@ {% csrf_token %} <div class="row"> <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 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"> @@ -151,16 +186,5 @@ <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%} \ No newline at end of file diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index c755cc6f..ca1bb930 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -105,7 +105,8 @@ class CeleryTaskTestCase(TestCase): disk_size=disk_size) plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu, 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, ram=memory, ssd=disk_size, diff --git a/datacenterlight/utils.py b/datacenterlight/utils.py index 8da408a0..bbcb16ab 100644 --- a/datacenterlight/utils.py +++ b/datacenterlight/utils.py @@ -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) - for session_var in ['specs', 'template', 'billing_address', - 'billing_address_data', 'card_id', - 'token', 'customer']: - if session_var in request.session: - del request.session[session_var] + clear_all_session_vars(request) + + +def clear_all_session_vars(request): + 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] diff --git a/datacenterlight/views.py b/datacenterlight/views.py index bf87d9b9..445ff7cf 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -6,23 +6,31 @@ from django.contrib import messages from django.contrib.auth import login, authenticate from django.core.exceptions import ValidationError 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.utils.translation import get_language, ugettext_lazy as _ from django.views.decorators.cache import cache_control from django.views.generic import FormView, CreateView, DetailView -from hosting.forms import HostingUserLoginForm -from hosting.models import HostingOrder, UserCardDetail +from hosting.forms import ( + HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm +) +from hosting.models import ( + HostingBill, HostingOrder, UserCardDetail, GenericProduct +) from membership.models import CustomUser, StripeCustomer 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.stripe_utils import StripeUtils from utils.tasks import send_plain_email_task +from .cms_models import DCLCalculatorPluginModel from .forms import ContactForm 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__) @@ -82,7 +90,29 @@ class IndexView(CreateView): 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): @@ -91,17 +121,14 @@ class IndexView(CreateView): @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): - for session_var in ['specs', 'user', 'billing_address_data', - 'pricing_name']: - if session_var in request.session: - del request.session[session_var] + clear_all_session_vars(request) return HttpResponseRedirect(reverse('datacenterlight:cms_index')) def post(self, request): 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')) @@ -170,7 +197,7 @@ class IndexView(CreateView): 'vat': vat, 'vat_percent': vat_percent, 'discount': discount, - 'total_price': price + vat - discount['amount'], + 'total_price': round(price + vat - discount['amount'], 2), 'pricing_name': vm_pricing_name } request.session['specs'] = specs @@ -242,19 +269,93 @@ class PaymentOrderView(FormView): 'login_form': HostingUserLoginForm(prefix='login_form'), 'billing_address_form': billing_address_form, '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 @cache_control(no_cache=True, must_revalidate=True, no_store=True) 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 self.render_to_response(self.get_context_data()) 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: login_form = HostingUserLoginForm( data=request.POST, prefix='login_form' @@ -265,6 +366,13 @@ class PaymentOrderView(FormView): auth_user = authenticate(email=email, password=password) if 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( reverse('datacenterlight:payment') ) @@ -281,6 +389,50 @@ class PaymentOrderView(FormView): data=request.POST, ) 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') if token is '': card_id = address_form.cleaned_data.get('card') @@ -296,8 +448,8 @@ class PaymentOrderView(FormView): except UserCardDetail.DoesNotExist as e: ex = str(e) 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)) messages.add_message( @@ -386,7 +538,8 @@ class OrderConfirmationView(DetailView): @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): 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')) if 'token' in self.request.session: token = self.request.session['token'] @@ -404,9 +557,19 @@ class OrderConfirmationView(DetailView): card_detail = UserCardDetail.objects.get(id=card_id) context['cc_last4'] = card_detail.last4 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({ 'site_url': reverse('datacenterlight:index'), - 'vm': request.session.get('specs'), 'page_header_text': _('Confirm Order'), 'billing_address_data': ( request.session.get('billing_address_data') @@ -416,11 +579,8 @@ class OrderConfirmationView(DetailView): return render(request, self.template_name, context) def post(self, request, *args, **kwargs): - template = request.session.get('template') - specs = request.session.get('specs') user = request.session.get('user') stripe_api_cus_id = request.session.get('customer') - vm_template_id = template.get('id', 1) stripe_utils = StripeUtils() if 'token' in request.session: @@ -434,7 +594,14 @@ class OrderConfirmationView(DetailView): response = { 'status': False, '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'), 'msg_title': str(_('Error.')), 'msg_body': str( @@ -450,7 +617,8 @@ class OrderConfirmationView(DetailView): 'brand': card_details_response['brand'], '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: ucd = UserCardDetail.get_user_card_details( stripe_customer_obj, card_details_response @@ -472,7 +640,16 @@ class OrderConfirmationView(DetailView): response = { 'status': False, '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'), 'msg_title': str(_('Error.')), 'msg_body': str( @@ -504,51 +681,122 @@ class OrderConfirmationView(DetailView): } return JsonResponse(response) - 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) - stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, - ram=memory, - ssd=disk_size, - version=1, - app='dcl') - 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. - # ... + if ('generic_payment_type' in request.session and + self.request.session['generic_payment_type'] == 'generic'): + gp_details = self.request.session['generic_payment_details'] + if gp_details['recurring']: + # generic recurring payment + logger.debug("Commencing a generic recurring payment") + else: + # generic one time payment + logger.debug("Commencing a one time payment") + charge_response = stripe_utils.make_charge( + amount=gp_details['amount'], + customer=stripe_api_cus_id + ) + stripe_onetime_charge = charge_response.get('response_object') - 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('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) + # Check if the payment was approved + if not stripe_onetime_charge: + msg = charge_response.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': gp_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) + + 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 # registered @@ -618,6 +866,118 @@ class OrderConfirmationView(DetailView): '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 = { 'name': custom_user.name, 'email': custom_user.email, diff --git a/dynamicweb/urls.py b/dynamicweb/urls.py index 7e2d58a1..37bb69a4 100644 --- a/dynamicweb/urls.py +++ b/dynamicweb/urls.py @@ -10,6 +10,7 @@ from django.conf import settings from hosting.views import ( RailsHostingView, DjangoHostingView, NodeJSHostingView ) +from datacenterlight.views import PaymentOrderView from membership import urls as membership_urls from ungleich_page.views import LandingView from django.views.generic import RedirectView @@ -29,6 +30,9 @@ urlpatterns = [ url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")), url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')), 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) urlpatterns += i18n_patterns( diff --git a/hosting/admin.py b/hosting/admin.py index 6ebe461d..dc476b43 100644 --- a/hosting/admin.py +++ b/hosting/admin.py @@ -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) diff --git a/hosting/forms.py b/hosting/forms.py index 7beab60f..16b06fe0 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -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: diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 95515355..0e337cfb 100644 --- a/hosting/locale/de/LC_MESSAGES/django.po +++ b/hosting/locale/de/LC_MESSAGES/django.po @@ -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" diff --git a/hosting/migrations/0048_auto_20181003_0757.py b/hosting/migrations/0048_auto_20181003_0757.py new file mode 100644 index 00000000..7b80958a --- /dev/null +++ b/hosting/migrations/0048_auto_20181003_0757.py @@ -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'), + ), + ] diff --git a/hosting/migrations/0049_auto_20181005_0736.py b/hosting/migrations/0049_auto_20181005_0736.py new file mode 100644 index 00000000..b091ef45 --- /dev/null +++ b/hosting/migrations/0049_auto_20181005_0736.py @@ -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), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index abc4c428..707b072d 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -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): diff --git a/hosting/static/hosting/css/virtual-machine.css b/hosting/static/hosting/css/virtual-machine.css index 1c50776d..726b0f35 100644 --- a/hosting/static/hosting/css/virtual-machine.css +++ b/hosting/static/hosting/css/virtual-machine.css @@ -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; */ diff --git a/hosting/static/hosting/js/initial.js b/hosting/static/hosting/js/initial.js index 9c1c226e..6b6d744d 100644 --- a/hosting/static/hosting/js/initial.js +++ b/hosting/static/hosting/js/initial.js @@ -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(); -}); \ No newline at end of file + $('#ramValue').data('old-value', $('#ramValue').val()); +}); diff --git a/hosting/static/hosting/js/payment.js b/hosting/static/hosting/js/payment.js index 4934fdd3..fa89f218 100644 --- a/hosting/static/hosting/js/payment.js +++ b/hosting/static/hosting/js/payment.js @@ -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(); }); }); diff --git a/hosting/static/hosting/js/virtual_machine_detail.js b/hosting/static/hosting/js/virtual_machine_detail.js index 43a5a01d..8f90933b 100644 --- a/hosting/static/hosting/js/virtual_machine_detail.js +++ b/hosting/static/hosting/js/virtual_machine_detail.js @@ -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'; + } +}; \ No newline at end of file diff --git a/hosting/templates/hosting/emails/vm_canceled.html b/hosting/templates/hosting/emails/vm_canceled.html index 9c2ec4c2..78781d5c 100644 --- a/hosting/templates/hosting/emails/vm_canceled.html +++ b/hosting/templates/hosting/emails/vm_canceled.html @@ -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 %} diff --git a/hosting/templates/hosting/emails/vm_canceled.txt b/hosting/templates/hosting/emails/vm_canceled.txt index 9149a554..43263c40 100644 --- a/hosting/templates/hosting/emails/vm_canceled.txt +++ b/hosting/templates/hosting/emails/vm_canceled.txt @@ -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' %} diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html index e2e38c35..4a62e9fa 100644 --- a/hosting/templates/hosting/order_detail.html +++ b/hosting/templates/hosting/order_detail.html @@ -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> - {% 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> + {% 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> + {{ 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%} diff --git a/hosting/templates/hosting/orders.html b/hosting/templates/hosting/orders.html index 140cc4c6..96d9e9e3 100644 --- a/hosting/templates/hosting/orders.html +++ b/hosting/templates/hosting/orders.html @@ -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> diff --git a/hosting/templates/hosting/settings.html b/hosting/templates/hosting/settings.html index 62dcc947..56818cbf 100644 --- a/hosting/templates/hosting/settings.html +++ b/hosting/templates/hosting/settings.html @@ -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"> diff --git a/hosting/templates/hosting/virtual_machine_detail.html b/hosting/templates/hosting/virtual_machine_detail.html index 68894851..ce02036f 100644 --- a/hosting/templates/hosting/virtual_machine_detail.html +++ b/hosting/templates/hosting/virtual_machine_detail.html @@ -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"> diff --git a/hosting/views.py b/hosting/views.py index 6af1885b..32de4e54 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -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( diff --git a/opennebula_api/models.py b/opennebula_api/models.py index 3682c5da..adc39bf0 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -110,7 +110,7 @@ class OpenNebulaManager(): raise UserExistsError() except OpenNebulaException as 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() except WrongNameError: @@ -148,7 +148,7 @@ class OpenNebulaManager(): ) return opennebula_user except ConnectionRefusedError: - logger.info( + logger.error( 'Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) @@ -160,7 +160,7 @@ class OpenNebulaManager(): user_pool = oca.UserPool(self.oneadmin_client) user_pool.info() except ConnectionRefusedError: - logger.info( + logger.error( 'Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) @@ -174,7 +174,7 @@ class OpenNebulaManager(): vm_pool.info() return vm_pool except AttributeError: - logger.info('Could not connect via client, using oneadmin instead') + logger.error('Could not connect via client, using oneadmin instead') try: vm_pool = oca.VirtualMachinePool(self.oneadmin_client) vm_pool.info(filter=-2) @@ -183,7 +183,7 @@ class OpenNebulaManager(): raise ConnectionRefusedError except ConnectionRefusedError: - logger.info( + logger.error( 'Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) @@ -249,8 +249,8 @@ class OpenNebulaManager(): vm_specs = vm_specs_formatter.format( vcpu=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> <TYPE>fs</TYPE> @@ -269,8 +269,8 @@ class OpenNebulaManager(): vm_specs = vm_specs_formatter.format( vcpu=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> <TYPE>fs</TYPE> @@ -325,14 +325,14 @@ class OpenNebulaManager(): ) vm_terminated = True 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: - logger.info( + logger.error( "OpenNebulaException error: {0}".format(opennebula_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: - logger.info("ValueError : {0}".format(value_err)) + logger.error("ValueError : {0}".format(value_err)) return vm_terminated @@ -342,7 +342,7 @@ class OpenNebulaManager(): template_pool.info() return template_pool except ConnectionRefusedError: - logger.info( + logger.error( """Could not connect to host: {host} via protocol {protocol}""".format( host=settings.OPENNEBULA_DOMAIN, diff --git a/requirements.txt b/requirements.txt index 85a41841..a14617a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,6 @@ django-meta==1.2 django-meta-mixin==0.3.0 django-model-utils==2.5 django-mptt==0.8.4 -django-multisite==1.4.1 django-parler==1.6.3 django-phonenumber-field==1.1.0 django-polymorphic==0.9.2 @@ -69,7 +68,7 @@ model-mommy==1.2.6 phonenumbers==7.4.0 phonenumberslite==7.4.0 psycopg2==2.7.3.2 -pycryptodome==3.4 +pycryptodome==3.6.6 pylibmc==1.5.1 python-dateutil==2.5.3 python-slugify==1.2.0 diff --git a/ungleich/templates/cms/ungleichch/_footer.html b/ungleich/templates/cms/ungleichch/_footer.html index 94832ed4..21c96b54 100644 --- a/ungleich/templates/cms/ungleichch/_footer.html +++ b/ungleich/templates/cms/ungleichch/_footer.html @@ -31,7 +31,7 @@ </a> </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"> <i class="fa fa-circle fa-stack-2x"></i> <i class="fa fa-rss fa-stack-1x fa-inverse"></i> diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py index 1859a82c..ec97a320 100644 --- a/utils/hosting_utils.py +++ b/utils/hosting_utils.py @@ -1,6 +1,7 @@ import decimal import logging import subprocess + from oca.pool import WrongIdError 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)) cents = decimal.Decimal('.01') 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, @@ -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) discount = { '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): diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 2045df8e..a3224a0e 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -291,7 +291,8 @@ class StripeUtils(object): return charge @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 `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 app: The application to which the stripe plan belongs 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` """ 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( app=app, version=version, - plan=dcl_plan_string) - return stripe_plan_id_string + plan=dcl_plan_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 - def get_stripe_plan_name(cpu, memory, disk_size): + def get_stripe_plan_name(cpu, memory, disk_size, price): """ Returns the Stripe plan name :return: """ - return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( - cpu=cpu, - memory=memory, - disk_size=disk_size) + return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \ + "{price} CHF".format( + cpu=cpu, + memory=memory, + disk_size=disk_size, + price=round(price, 2) + ) @handleStripeError def set_subscription_meta_data(self, subscription_id, meta_data):