diff --git a/.gitignore b/.gitignore index 46bfbf54..cfef66a1 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ secret-key .env *.mo *.log +*.sql diff --git a/Changelog b/Changelog index de602e8d..fe9a1dde 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,10 @@ +Next: + * #3764: [hosting] Show cancelled VMs' invoices + * #3736: [dcl] Refactor the place where we compute the VM price + * #3730: [dcl] Refactor price parameter passed in the DCL flow + * #3807: [dcl] Remove PricingView as it is no more used + * #3813: [hosting] JS error in create ssh key page + * #3756: [dcl] Update landing calculator and billing info page 1.2.3: 2017-09-25 * #3484: [dcl, hosting] Refactored account activation, password reset, VM order and cancellation email * #3731: [dcl, hosting] Added cdist ssh key handler diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index c0e44342..941550db 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: 2017-09-27 04:21+0530\n" +"POT-Creation-Date: 2017-09-28 18:36+0530\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -65,24 +65,6 @@ msgstr "Bitte gib einen Wert von 10 bis 200 ein." msgid "GB Storage (SSD)" msgstr "GB Storage (SSD)" -msgid "Name" -msgstr "" - -msgid "Your Name" -msgstr "Dein Name" - -msgid "Please enter your name." -msgstr "Bitte gib Deinen Namen ein." - -msgid "Email" -msgstr "E-Mail-Adresse" - -msgid "Your Email" -msgstr "Deine E-Mail" - -msgid "Please enter a valid email address." -msgstr "Bitte gib eine gültige E-Mailadresse ein." - msgid "Continue" msgstr "Weiter" @@ -95,6 +77,18 @@ msgstr "Vielen Dank für Deine Nachricht." msgid "Get in touch with us!" msgstr "Sende uns eine Nachricht." +msgid "Name" +msgstr "" + +msgid "Please enter your name." +msgstr "Bitte gib Deinen Namen ein." + +msgid "Email" +msgstr "E-Mail-Adresse" + +msgid "Please enter a valid email address." +msgstr "Bitte gib eine gültige E-Mailadresse ein." + msgid "Message" msgstr "Nachricht" @@ -270,6 +264,45 @@ msgstr "Kontaktiere uns" msgid "Switzerland " msgstr "Schweiz " +msgid "Welcome back" +msgstr "Willkommen zurück" + +msgid "" +"Review your billing address and card details and proceed to make payment." +msgstr "" +"Überprüfe die Rechnungsadresse und Kreditkartendaten und fahre mit der " +"Zahlung fort." + +msgid "Log in" +msgstr "Anmelden" + +msgid "" +"Already signed up?
By logging in you can retrieve saved billing " +"information." +msgstr "" +"Bereits eingeloggt? Nach der Anmeldung kannst Du gespeicherte " +"Rechnungsinformationen abrufen." + +msgid "LOGIN" +msgstr "ANMELDEN" + +msgid "Don't have an account yet?" +msgstr "Besitzt du kein Benutzerkonto?" + +msgid "You can sign up by filling in the information below." +msgstr "" +"Du kannst Dich anmelden, indem Du die die untenstehenden Informationen " +"ausfüllst." + +msgid "Forgot password?" +msgstr "Passwort vergessen?" + +msgid "Sign up" +msgstr "Registrieren" + +msgid "Billing Address" +msgstr "Rechnungsadresse" + msgid "Your Order" msgstr "Deine Bestellung" @@ -294,23 +327,15 @@ msgstr "inkl. Mehrwertsteuer" msgid "Month" msgstr "Monat" -msgid "Billing Address" -msgstr "Rechnungsadresse" - msgid "Credit Card" msgstr "Kreditkarte" msgid "" -"\n" -" Please fill in your credit card information " -"below. We are using Stripe for payment and do not store\n" -" your information in our database.\n" -" " +"Please fill in your credit card information below. We are using Stripe for payment and do not " +"store your information in our database." msgstr "" -"\n" -"Bitte füll Deine Kreditkarteninformationen unten aus. Wir nutzen Stripe für die Bezahlung und " "speichern keine Informationen in unserer Datenbank." @@ -336,6 +361,13 @@ msgstr "" msgid "Card Type" msgstr "Kartentyp" +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." + msgid "Processing" msgstr "Weiter" @@ -371,26 +403,20 @@ msgstr "" msgid "Place order" msgstr "Bestellen" -msgid "We are cutting down the costs significantly!" -msgstr "Wir sorgen dafür, dass die Kosten für Dich signifikant abnehmen" +msgid "Processing..." +msgstr "Abarbeitung..." -msgid "Order Now!" -msgstr "Bestelle jetzt!" +msgid "Hold tight, we are processing your request" +msgstr "Bitte warten - wir verbeiten Deine Anfrage gerade" -msgid "" -"Our VMs are hosted in Glarus, Switzerland, and our website is currently " -"running in BETA mode. If you want more information that you did not find on " -"our website, or if your order is more detailed, or if you encounter any " -"technical hiccups, please contact us at support@datacenterlight.ch, our team " -"will get in touch with you asap." +msgid "OK" msgstr "" -"Unsere VMs werden in der Schweiz im Kanton Glarus gehostet und befinden sich " -"zur Zeit noch in der BETA-Phase. Möchtest du mehr über uns erfahren und hast " -"auf unserer Website nicht genügend Informationen gefunden? Möchtest eine " -"detailliertere Bestellung aufgeben? Bist du auf technische Probleme " -"gestossen, die du uns mitteilen möchtest? Dann zögere nicht und kontaktiere " -"uns unter support@datacenterlight.ch. Unser Team wird sich umgehend um dein " -"Anliegen kümmern!" + +msgid "Close" +msgstr "" + +msgid "Some problem encountered. Please try again later." +msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal." msgid "Thank you for order! Our team will contact you via email" msgstr "" @@ -485,11 +511,59 @@ msgstr "Ungültige RAM-Grösse" msgid "Invalid storage size" msgstr "Ungültige Speicher-Grösse" -msgid "is not a proper name" -msgstr "ist kein gültiger Name" +msgid "Error." +msgstr "" -msgid "is not a proper email" -msgstr "ist keine gültige E-Mailadresse" +msgid "" +"There was a payment related error. On close of this popup, you will be " +"redirected back to the payment page." +msgstr "" + +msgid "Thank you for the order." +msgstr "Danke für Deine Bestellung." + +msgid "" +"Your VM will be up and running in a few moments. We will send you a " +"confirmation email as soon as it is ready." +msgstr "" +"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du " +"auf sie zugreifen kannst." + +#~ msgid "We are cutting down the costs significantly!" +#~ msgstr "Wir sorgen dafür, dass die Kosten für Dich signifikant abnehmen" + +#~ msgid "Order Now!" +#~ msgstr "Bestelle jetzt!" + +#~ msgid "" +#~ "Our VMs are hosted in Glarus, Switzerland, and our website is currently " +#~ "running in BETA mode. If you want more information that you did not find " +#~ "on our website, or if your order is more detailed, or if you encounter " +#~ "any technical hiccups, please contact us at support@datacenterlight.ch, " +#~ "our team will get in touch with you asap." +#~ msgstr "" +#~ "Unsere VMs werden in der Schweiz im Kanton Glarus gehostet und befinden " +#~ "sich zur Zeit noch in der BETA-Phase. Möchtest du mehr über uns erfahren " +#~ "und hast auf unserer Website nicht genügend Informationen gefunden? " +#~ "Möchtest eine detailliertere Bestellung aufgeben? Bist du auf technische " +#~ "Probleme gestossen, die du uns mitteilen möchtest? Dann zögere nicht und " +#~ "kontaktiere uns unter support@datacenterlight.ch. Unser Team wird sich " +#~ "umgehend um dein Anliegen kümmern!" + +#~ msgid "Email Address" +#~ msgstr "E-Mail-Adresse" + +#~ msgid "is not a proper name" +#~ msgstr "ist kein gültiger Name" + +#~ msgid "is not a proper email" +#~ msgstr "ist keine gültige E-Mailadresse" + +#~ msgid "Your Name" +#~ msgstr "Dein Name" + +#~ msgid "Your Email" +#~ msgstr "Deine E-Mail" #~ msgid "" #~ "\n" diff --git a/datacenterlight/static/datacenterlight/js/main.js b/datacenterlight/static/datacenterlight/js/main.js index ab37a68b..dd074397 100644 --- a/datacenterlight/static/datacenterlight/js/main.js +++ b/datacenterlight/static/datacenterlight/js/main.js @@ -155,9 +155,7 @@ function _calcPricing() { var total = (cardPricing['cpu'].value * 5) + (2 * cardPricing['ram'].value) + (0.6 * cardPricing['storage'].value); total = parseFloat(total.toFixed(2)); - $("#total").text(total); - $('input[name=total]').val(total); } function form_success() { @@ -191,4 +189,4 @@ }); }) } -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index 561bfd17..80dd2c65 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -14,7 +14,7 @@ from hosting.models import HostingOrder, HostingBill from membership.models import StripeCustomer, CustomUser from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VirtualMachineSerializer -from utils.hosting_utils import get_all_public_keys +from utils.hosting_utils import get_all_public_keys, get_or_create_vm_detail from utils.forms import UserBillingAddressForm from utils.mailer import BaseEmail from utils.models import BillingAddress @@ -53,8 +53,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, billing_address_id, charge, cc_details): - logger.debug("Running create_vm_task on {}".format( - current_task.request.hostname)) + logger.debug( + "Running create_vm_task on {}".format(current_task.request.hostname)) vm_id = None try: final_price = specs.get('price') @@ -146,8 +146,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, if 'pass' in user: lang = 'en-us' if user.get('language') is not None: - logger.debug("Language is set to {}".format( - user.get('language'))) + logger.debug( + "Language is set to {}".format(user.get('language'))) lang = user.get('language') translation.activate(lang) # Send notification to the user as soon as VM has been booked @@ -178,6 +178,7 @@ def create_vm_task(self, vm_template_id, user, specs, template, logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id)) if new_host is not None: custom_user = CustomUser.objects.get(email=user.get('email')) + get_or_create_vm_detail(custom_user, manager, vm_id) if custom_user is not None: public_keys = get_all_public_keys(custom_user) keys = [{'value': key, 'state': True} for key in diff --git a/datacenterlight/templates/datacenterlight/calculator_form.html b/datacenterlight/templates/datacenterlight/calculator_form.html index b5bac1f9..1733a719 100644 --- a/datacenterlight/templates/datacenterlight/calculator_form.html +++ b/datacenterlight/templates/datacenterlight/calculator_form.html @@ -77,47 +77,9 @@ {% endfor %} - -
-
- - -
-
- {% for message in messages %} - {% if 'name' in message.tags %} -
    -
  • - {{ message|safe }} -
  • -
- {% endif %} - {% endfor %} -
-
-
-
- - -
-
- {% for message in messages %} - {% if 'email' in message.tags %} -
    -
  • - {{ message|safe }} -
  • -
- {% endif %} - {% endfor %} -
-
diff --git a/datacenterlight/templates/datacenterlight/landing_payment.html b/datacenterlight/templates/datacenterlight/landing_payment.html index f4974a56..c7ada779 100644 --- a/datacenterlight/templates/datacenterlight/landing_payment.html +++ b/datacenterlight/templates/datacenterlight/landing_payment.html @@ -1,190 +1,182 @@ {% extends "hosting/base_short.html" %} {% load staticfiles bootstrap3 i18n %} +{% block css_extra %} + +{% endblock css_extra %} + {% block navbar %} {% include "datacenterlight/includes/_navbar.html" %} {% endblock navbar %} {% block content %} -
-
-
-

{%trans "Your Order" %}

-
-
- {%trans "Cores" %} -
-
- {%trans "Memory" %} -
-
- {%trans "Disk space" %} -
-
- {%trans "Configuration" %} -
-
-
-
- {{request.session.specs.cpu|floatformat}} -
-
- {{request.session.specs.memory|floatformat}} GB -
-
- {{request.session.specs.disk_size|floatformat}} GB -
-
- {{request.session.template.name}} -
-
-
-
- {%trans "Total" %} {%trans "including VAT" %} -
-
-
-
{{request.session.specs.price}} - CHF/{% trans "Month" %} +
+
+
+ {% if request.user.is_authenticated %} +
+

{% trans "Welcome back" %} {{request.user.name}}!

+

{% trans "Review your billing address and card details and proceed to make payment." %}

-
+ {% else %} +

{%trans "Log in" %}

+
+

{% blocktrans %}Already signed up?
By logging in you can retrieve saved billing information.{% endblocktrans %}

+
+ {% for field in login_form %} + {% csrf_token %} + {% bootstrap_field field show_label=False type='fields'%} + {% endfor %} + +
+ +
+
+

+ {% trans "Don't have an account yet?" %}
+ {% trans "You can sign up by filling in the information below." %}
+ {% trans "Forgot password?" %} +

+ {% endif %}
-
-
-
-
-

{%trans "Billing Address"%}

-
+
+
+ {% if not request.user.is_authenticated %} +

{%trans "Sign up"%}

+ {% else %} +

{%trans "Billing Address"%}

+ {% endif %} +
+ {% for message in messages %} + {% if 'duplicate_email' in message.tags %} +

{{message}}

+ {% endif %} + {% endfor %}
- {% for field in form %} {% csrf_token %} + {% for field in form %} {% bootstrap_field field show_label=False type='fields'%} {% endfor %}
-
+
+
+
+

{%trans "Your Order" %}

+
+
+

{% trans "Cores"%} {{request.session.specs.cpu|floatformat}}

+
+

{% trans "Memory"%} {{request.session.specs.memory|floatformat}} GB

+
+

{% trans "Disk space"%} {{request.session.specs.disk_size|floatformat}} GB

+
+

{% trans "Configuration"%} {{request.session.template.name}}

+
+

{%trans "Total" %}  ({%trans "including VAT" %}) {{request.session.specs.price}} CHF/{% trans "Month" %}

+
+
+
+
+

{%trans "Credit Card"%}

-
+
+

+ {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} +

-
-

- {% blocktrans %} - Please fill in your credit card information below. We are using Stripe for payment and do not store - your information in our database. - {% endblocktrans %} -

-
-
-
- {% if credit_card_data.last4 %} + {% if credit_card_data.last4 %}
Credit Card
Last 4: *****{{credit_card_data.last4}}
Type: {{credit_card_data.cc_brand}}
-
-
- {% if not messages and not form.non_field_errors %} -

- {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} -

- {% endif %} -
- {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
  • -

    {{ message|safe }}

    -
- {% endif %} - {% endfor %} - {% for error in form.non_field_errors %} -

- {{ error|escape }} -

- {% endfor %} -
-
-
-
- -
-
+ {% if not messages and not form.non_field_errors %} +

+ {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} +

+ {% endif %} +
+ {% for message in messages %} + {% if 'failed_payment' or 'make_charge_error' in message.tags %} +
  • +

    {{ message|safe }}

    +
+ {% endif %} + {% endfor %} + {% for error in form.non_field_errors %} +

+ {{ error|escape }} +

+ {% endfor %}
- - {% else %} +
+ +
+ {% else %}
-
-
+
+
-
- -
+
+
+ +
+
+
+ +
+
-
-
- -
-
-
+
- -
-
- {% if not messages and not form.non_field_errors %} +
+ {% if not messages and not form.non_field_errors %} +

+ {% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %} +

+ {% endif %} +
+ {% for message in messages %} + {% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags %} +
    +
  • {{ message|safe }}

  • +
+ {% elif not form.non_field_errors %}

- {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} + {% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %}

{% endif %} -
- {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
  • -

    {{ message|safe }}

    -
- {% endif %} - {% endfor %} + {% endfor %} - {% for error in form.non_field_errors %} -

- {{ error|escape }} -

- {% endfor %} -
-
-
-
- -
-
+ {% for error in form.non_field_errors %} +

+ {{ error|escape }} +

+ {% endfor %} +
+
+
- + {% endif %}
diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 085b0c37..5e8af4ea 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -9,98 +9,135 @@ {% block content %} -
- {% if messages %} -
-
-
-
- {% for message in messages %} - {{ message }} - {% endfor %} -
+
+ {% if messages %} +
+
+
+
+ {% for message in messages %} + {{ message }} + {% endfor %}
- {% endif %} - {% if not error %} -
-
-
-

{% trans "Confirm Order"%}

-
-
-
-
-
- {% trans "Date"%}:
- {% now "Y-m-d H:i" %}

-
-
-
-
-

{% trans "Billed To:"%}

- {% with request.session.billing_address_data as billing_address %} - {{billing_address|get_value_from_dict:'cardholder_name'}}
{{billing_address|get_value_from_dict:'street_address'}}, {{billing_address|get_value_from_dict:'postal_code'}}
- {{billing_address|get_value_from_dict:'city'}}, {{billing_address|get_value_from_dict:'country'}}. - {% endwith %} -
-
-
-
-
-
- {% trans "Payment Method:"%}
- {{cc_brand}} {% trans "ending in" %} **** {{cc_last4}}
- {{request.session.user.email}} -
-
-
+
+ {% endif %} + {% if not error %} +
+
+
+

{% trans "Confirm Order"%}

-
- -
-
-

{% trans "Order summary"%}

-
-
- {% with request.session.specs as vm %} -

{% trans "Cores"%} {{vm.cpu}}

-
-

{% trans "Memory"%} {{vm.memory}} GB

-
-

{% trans "Disk space"%} {{vm.disk_size}} GB

-
-

{% trans "Configuration"%} {{request.session.template.name}}

-
-

{% trans "Total"%}

{{vm.price}} CHF /{% trans "Month" %}

+
+
+
+
+ {% trans "Date"%}:
+ {% now "Y-m-d H:i" %}

+
+
+
+
+

{% trans "Billed To:"%}

+ {% with billing_address_data as billing_address %} + {{billing_address.cardholder_name}}
{{billing_address.street_address}}, {{billing_address.postal_code}}
+ {{billing_address.city}}, {{billing_address.country}}. {% endwith %} +
+
+
+
+
+
+ {% trans "Payment Method:"%}
+ {{cc_brand}} {% trans "ending in" %} **** {{cc_last4}}
+ {{request.session.user.email}} +
-
-
- {% csrf_token %} -
-
-

{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.

-
- -
-
- {% endif %} +
+ +
+
+

{% trans "Order summary"%}

+
+
+ {% with request.session.specs as vm %} +

{% trans "Cores"%} {{vm.cpu}}

+
+

{% trans "Memory"%} {{vm.memory}} GB

+
+

{% trans "Disk space"%} {{vm.disk_size}} GB

+
+

{% trans "Configuration"%} {{request.session.template.name}}

+
+

{% trans "Total"%}

{{vm.price}} CHF /{% trans "Month" %}

+ {% endwith %} +
+
+
+ {% csrf_token %} +
+
+

{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.

+
+
+ +
+
+
+
+
+ {% endif %} +
+ + + - + {%endblock%} diff --git a/datacenterlight/templates/datacenterlight/pricing.html b/datacenterlight/templates/datacenterlight/pricing.html deleted file mode 100644 index 0724a6ce..00000000 --- a/datacenterlight/templates/datacenterlight/pricing.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "datacenterlight/base.html" %} -{% load staticfiles i18n%} -{% get_current_language as LANGUAGE_CODE %} - -{% block content %} -
- -
-

{% trans "We are cutting down the costs significantly!" %}

-
- -
- -
-
- -
-
- {% csrf_token %} - -
-

{% trans "VM hosting" %}

-
-
- 15 - CHF -
-

{% trans "VAT included" %}

-
-
-
-
-

{% trans "Hosted in Switzerland" %}

-
-
- - - Core - -
-
- - - GB RAM - -
-
- - - {% trans "GB Storage (SSD)" %} - -
- - - -
- - -
- - - - - -
- - -
-
-
- -
-

{% trans "Simple and affordable: Try our virtual machine with featherlight price." %}

- -
-

{% trans "Our VMs are hosted in Glarus, Switzerland, and our website is currently running in BETA mode. If you want more information that you did not find on our website, or if your order is more detailed, or if you encounter any technical hiccups, please contact us at support@datacenterlight.ch, our team will get in touch with you asap." %}

-
-
-
-{% endblock %} - - - diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index c34c56ba..edde2db8 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -12,6 +12,7 @@ from datacenterlight.models import VMTemplate from datacenterlight.tasks import create_vm_task from membership.models import StripeCustomer from opennebula_api.serializers import VMTemplateSerializer +from utils.hosting_utils import get_vm_price from utils.models import BillingAddress from utils.stripe_utils import StripeUtils @@ -94,12 +95,11 @@ class CeleryTaskTestCase(TestCase): cpu = specs.get('cpu') memory = specs.get('memory') disk_size = specs.get('disk_size') - amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) - plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( - cpu=cpu, - memory=memory, - disk_size=disk_size) - + amount_to_be_charged = get_vm_price(cpu=cpu, memory=memory, + disk_size=disk_size) + 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, diff --git a/datacenterlight/urls.py b/datacenterlight/urls.py index 0ac2c306..0c225817 100644 --- a/datacenterlight/urls.py +++ b/datacenterlight/urls.py @@ -2,7 +2,7 @@ from django.conf.urls import url from .views import ( IndexView, BetaProgramView, LandingProgramView, BetaAccessView, - PricingView, SuccessView, PaymentOrderView, OrderConfirmationView, + SuccessView, PaymentOrderView, OrderConfirmationView, WhyDataCenterLightView, ContactUsView ) @@ -17,7 +17,6 @@ urlpatterns = [ name='whydatacenterlight'), url(r'^beta-program/?$', BetaProgramView.as_view(), name='beta'), url(r'^landing/?$', LandingProgramView.as_view(), name='landing'), - url(r'^pricing/?$', PricingView.as_view(), name='pricing'), url(r'^payment/?$', PaymentOrderView.as_view(), name='payment'), url(r'^order-confirmation/?$', OrderConfirmationView.as_view(), name='order_confirmation'), diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 0521ffef..308913b0 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -1,28 +1,35 @@ +import logging +import json + from django import forms from django.conf import settings 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 -from django.shortcuts import redirect +from django.http import HttpResponseRedirect, HttpResponse from django.shortcuts import render -from django.utils.translation import ugettext_lazy as _ +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, TemplateView, DetailView from datacenterlight.tasks import create_vm_task from hosting.models import HostingOrder +from hosting.forms import HostingUserLoginForm from membership.models import CustomUser, StripeCustomer -from opennebula_api.models import OpenNebulaManager -from opennebula_api.serializers import VirtualMachineTemplateSerializer, \ - VMTemplateSerializer -from utils.forms import BillingAddressForm +from opennebula_api.serializers import VMTemplateSerializer +from utils.forms import ( + BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm +) +from utils.hosting_utils import get_vm_price from utils.mailer import BaseEmail from utils.stripe_utils import StripeUtils from utils.tasks import send_plain_email_task from .forms import BetaAccessForm, ContactForm from .models import BetaAccess, BetaAccessVMType, BetaAccessVM, VMTemplate +logger = logging.getLogger(__name__) + class ContactUsView(FormView): template_name = "datacenterlight/contact_form.html" @@ -88,56 +95,6 @@ class SuccessView(TemplateView): return render(request, self.template_name) -class PricingView(TemplateView): - template_name = "datacenterlight/pricing.html" - - def get(self, request, *args, **kwargs): - try: - manager = OpenNebulaManager() - templates = manager.get_templates() - - context = { - 'templates': VirtualMachineTemplateSerializer(templates, - many=True).data, - } - except: - messages.error(request, - 'We have a temporary problem to connect to our backend. \ - Please try again in a few minutes' - ) - context = { - 'error': 'connection' - } - - return render(request, self.template_name, context) - - def post(self, request): - - cores = request.POST.get('cpu') - memory = request.POST.get('ram') - storage = request.POST.get('storage') - price = request.POST.get('total') - - template_id = int(request.POST.get('config')) - manager = OpenNebulaManager() - template = manager.get_template(template_id) - - request.session['template'] = VirtualMachineTemplateSerializer( - template).data - - if not request.user.is_authenticated(): - request.session['next'] = reverse('hosting:payment') - - request.session['specs'] = { - 'cpu': cores, - 'memory': memory, - 'disk_size': storage, - 'price': price, - } - - return redirect(reverse('hosting:payment')) - - class BetaAccessView(FormView): template_name = "datacenterlight/beta_access.html" form_class = BetaAccessForm @@ -274,17 +231,11 @@ class IndexView(CreateView): memory_field = forms.IntegerField(validators=[self.validate_memory]) storage = request.POST.get('storage') storage_field = forms.IntegerField(validators=[self.validate_storage]) - price = request.POST.get('total') template_id = int(request.POST.get('config')) template = VMTemplate.objects.filter( opennebula_vm_template_id=template_id).first() template_data = VMTemplateSerializer(template).data - name = request.POST.get('name') - email = request.POST.get('email') - name_field = forms.CharField() - email_field = forms.EmailField() - try: cores = cores_field.clean(cores) except ValidationError as err: @@ -311,40 +262,16 @@ class IndexView(CreateView): extra_tags='storage') return HttpResponseRedirect( reverse('datacenterlight:index') + "#order_form") - - try: - name = name_field.clean(name) - except ValidationError as err: - msg = '{} {}.'.format(name, _('is not a proper name')) - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='name') - return HttpResponseRedirect( - reverse('datacenterlight:index') + "#order_form") - - try: - email = email_field.clean(email) - except ValidationError as err: - msg = '{} {}.'.format(email, _('is not a proper email')) - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='email') - return HttpResponseRedirect( - reverse('datacenterlight:index') + "#order_form") - + amount_to_be_charged = get_vm_price(cpu=cores, memory=memory, + disk_size=storage) specs = { 'cpu': cores, 'memory': memory, 'disk_size': storage, - 'price': price + 'price': amount_to_be_charged } - - this_user = { - 'name': name, - 'email': email - } - request.session['specs'] = specs request.session['template'] = template_data - request.session['user'] = this_user return HttpResponseRedirect(reverse('datacenterlight:payment')) def get_success_url(self): @@ -407,20 +334,19 @@ class WhyDataCenterLightView(IndexView): class PaymentOrderView(FormView): template_name = 'datacenterlight/landing_payment.html' - form_class = BillingAddressForm + + def get_form_class(self): + if self.request.user.is_authenticated(): + return BillingAddressForm + else: + return BillingAddressFormSignup def get_form_kwargs(self): form_kwargs = super(PaymentOrderView, self).get_form_kwargs() - billing_address_data = self.request.session.get('billing_address_data') - if billing_address_data: + # if user is signed in, get billing address + if self.request.user.is_authenticated(): form_kwargs.update({ - 'initial': { - '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'], - } + 'instance': self.request.user.billing_addresses.first() }) return form_kwargs @@ -428,48 +354,49 @@ class PaymentOrderView(FormView): context = super(PaymentOrderView, self).get_context_data(**kwargs) context.update({ 'stripe_key': settings.STRIPE_API_PUBLIC_KEY, - 'site_url': reverse('datacenterlight:index') + 'site_url': reverse('datacenterlight:index'), + 'login_form': HostingUserLoginForm() }) 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 or 'user' not in request.session: + # user is no longer added to session on the index page + if '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): form = self.get_form() if form.is_valid(): - # Get billing address data - billing_address_data = form.cleaned_data token = form.cleaned_data.get('token') - user = request.session.get('user') - try: - CustomUser.objects.get(email=user.get('email')) - except CustomUser.DoesNotExist: - password = CustomUser.get_random_password() - # Register the user, and do not send emails - CustomUser.register(user.get('name'), - password, - user.get('email'), - app='dcl', - base_url=None, send_email=False) - + if request.user.is_authenticated(): + this_user = { + 'email': request.user.email, + 'name': request.user.name + } + customer = StripeCustomer.get_or_create( + email=this_user.get('email'), + token=token) + else: + this_user = { + 'email': form.cleaned_data.get('email'), + 'name': form.cleaned_data.get('name') + } + customer = StripeCustomer.create_stripe_api_customer( + email=this_user.get('email'), + token=token, + customer_name=form.cleaned_data.get('name')) + request.session['billing_address_data'] = form.cleaned_data + request.session['user'] = this_user # Get or create stripe customer - customer = StripeCustomer.get_or_create(email=user.get('email'), - token=token) if not customer: form.add_error("__all__", "Invalid credit card") return self.render_to_response( self.get_context_data(form=form)) - - # Create Billing Address - billing_address = form.save() - request.session['billing_address_data'] = billing_address_data - request.session['billing_address'] = billing_address.id request.session['token'] = token - request.session['customer'] = customer.id + request.session[ + 'customer'] = customer.id if request.user.is_authenticated() else customer return HttpResponseRedirect( reverse('datacenterlight:order_confirmation')) else: @@ -489,9 +416,15 @@ class OrderConfirmationView(DetailView): if 'token' not in request.session: return HttpResponseRedirect(reverse('datacenterlight:payment')) stripe_customer_id = request.session.get('customer') - customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() + if request.user.is_authenticated(): + customer = StripeCustomer.objects.filter( + id=stripe_customer_id).first() + stripe_api_cus_id = customer.stripe_id + else: + stripe_api_cus_id = stripe_customer_id + stripe_utils = StripeUtils() - card_details = stripe_utils.get_card_details(customer.stripe_id, + card_details = stripe_utils.get_card_details(stripe_api_cus_id, request.session.get( 'token')) if not card_details.get('response_object'): @@ -503,7 +436,8 @@ class OrderConfirmationView(DetailView): context = { 'site_url': reverse('datacenterlight:index'), 'cc_last4': card_details.get('response_object').get('last4'), - 'cc_brand': card_details.get('response_object').get('brand') + 'cc_brand': card_details.get('response_object').get('brand'), + 'billing_address_data': request.session.get('billing_address_data') } return render(request, self.template_name, context) @@ -512,32 +446,43 @@ class OrderConfirmationView(DetailView): specs = request.session.get('specs') user = request.session.get('user') stripe_customer_id = request.session.get('customer') - customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() - billing_address_data = request.session.get('billing_address_data') - billing_address_id = request.session.get('billing_address') + if request.user.is_authenticated(): + customer = StripeCustomer.objects.filter( + id=stripe_customer_id).first() + stripe_api_cus_id = customer.stripe_id + else: + stripe_api_cus_id = stripe_customer_id vm_template_id = template.get('id', 1) - # Make stripe charge to a customer stripe_utils = StripeUtils() - card_details = stripe_utils.get_card_details(customer.stripe_id, + card_details = stripe_utils.get_card_details(stripe_api_cus_id, request.session.get( 'token')) if not card_details.get('response_object'): msg = card_details.get('error') messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') - return HttpResponseRedirect( - reverse('datacenterlight:payment') + '#payment_error') + 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 HttpResponse(json.dumps(response), + content_type="application/json") card_details_dict = card_details.get('response_object') cpu = specs.get('cpu') memory = specs.get('memory') disk_size = specs.get('disk_size') - amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) - plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( - cpu=cpu, - memory=memory, - disk_size=disk_size) - + amount_to_be_charged = specs.get('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, @@ -548,7 +493,7 @@ class OrderConfirmationView(DetailView): name=plan_name, stripe_plan_id=stripe_plan_id) subscription_result = stripe_utils.subscribe_customer_to_plan( - customer.stripe_id, + stripe_api_cus_id, [{"plan": stripe_plan.get( 'response_object').stripe_plan_id}]) stripe_subscription_obj = subscription_result.get('response_object') @@ -558,11 +503,96 @@ class OrderConfirmationView(DetailView): msg = subscription_result.get('error') messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') - return HttpResponseRedirect( - reverse('datacenterlight:payment') + '#payment_error') + 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 HttpResponse(json.dumps(response), + content_type="application/json") + + # Create user if the user is not logged in and if he is not already + # registered + if not request.user.is_authenticated(): + try: + custom_user = CustomUser.objects.get( + email=user.get('email')) + customer = StripeCustomer.objects.filter( + user_id=custom_user.id).first() + stripe_customer_id = customer.id + except CustomUser.DoesNotExist: + logger.debug( + "Customer {} does not exist.".format(user.get('email'))) + password = CustomUser.get_random_password() + base_url = "{0}://{1}".format(self.request.scheme, + self.request.get_host()) + custom_user = CustomUser.register( + user.get('name'), password, + user.get('email'), + app='dcl', base_url=base_url, send_email=True, + account_details=password + ) + logger.debug("Created user {}.".format(user.get('email'))) + stripe_customer = StripeCustomer.objects. \ + create(user=custom_user, stripe_id=stripe_api_cus_id) + stripe_customer_id = stripe_customer.id + new_user = authenticate(username=custom_user.email, + password=password) + login(request, new_user) + else: + customer = StripeCustomer.objects.filter( + id=stripe_customer_id).first() + custom_user = customer.user + stripe_customer_id = customer.id + + # Save billing address + billing_address_data = request.session.get('billing_address_data') + logger.debug('billing_address_data is {}'.format(billing_address_data)) + billing_address_data.update({ + 'user': custom_user.id + }) + billing_address_user_form = UserBillingAddressForm( + instance=custom_user.billing_addresses.first(), + data=billing_address_data) + billing_address = billing_address_user_form.save() + billing_address_id = billing_address.id + logger.debug("billing address id = {}".format(billing_address_id)) + user = { + 'name': custom_user.name, + 'email': custom_user.email, + 'pass': custom_user.password, + 'request_scheme': request.scheme, + 'request_host': request.get_host(), + 'language': get_language(), + } + create_vm_task.delay(vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, billing_address_id, stripe_subscription_obj, card_details_dict) - request.session['order_confirmation'] = True - return HttpResponseRedirect(reverse('datacenterlight:order_success')) + for session_var in ['specs', 'template', 'billing_address', + 'billing_address_data', + 'token', 'customer']: + if session_var in request.session: + del request.session[session_var] + + response = { + 'status': True, + 'redirect': reverse( + 'hosting:virtual_machines') if request.user.is_authenticated() else reverse( + 'datacenterlight:index'), + 'msg_title': str(_('Thank you for the order.')), + 'msg_body': str( + _('Your VM will be up and running in a few moments.' + ' We will send you a confirmation email as soon as' + ' it is ready.')) + } + + return HttpResponse(json.dumps(response), + content_type="application/json") diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 8c9c5509..b7a3e344 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: 2017-09-27 04:21+0530\n" +"POT-Creation-Date: 2017-09-28 18:36+0530\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -198,6 +198,7 @@ msgstr "Deine Neue VM %(vm_name)s" msgid "You have ordered a new virtual machine!" msgstr "Du hast eine neue virtuelle Maschine bestellt!" +#, python-format msgid "Your order of [ %(vm_name)s ] has been charged." msgstr "Deine Bestellung von [ %(vm_name)s ] wurde erhoben." @@ -237,13 +238,13 @@ msgstr "Dankeschön!" msgid "Virtual Machine Cancellation" msgstr "VM Kündigung" +#, python-format msgid "" "You are receiving this email because your virutal machine [ " "%(vm_name)s ] has been cancelled." msgstr "" -"Du erhälst diese E-Mail, da deine virtuelle Maschine [ " -"%(vm_name)s ] gekündigt " -"wurde." +"Du erhälst diese E-Mail, da deine virtuelle Maschine [ %(vm_name)s ] gekündigt wurde." msgid "You can always order a new VM by clicking the button below." msgstr "" @@ -313,6 +314,9 @@ msgstr "Rechnung Datum" msgid "Status" msgstr "" +msgid "Terminated" +msgstr "Beendet" + msgid "Approved" msgstr "Akzeptiert" @@ -334,6 +338,9 @@ msgstr "Bestellungsübersicht" msgid "Product" msgstr "Produkt" +msgid "Period" +msgstr "" + msgid "Cores" msgstr "Prozessorkerne" @@ -595,8 +602,13 @@ msgstr "Virtuelle Maschinen" msgid "To create a new virtual machine, click \"Create VM\"" msgstr "Um eine neue VM zu erzeugen, klicke \"Neue VM erzeugen\"" +#, python-format +msgid "" +"To access your VM, add your SSH key here" +msgstr "" + msgid "login" -msgstr "Einloggen" +msgstr "anmelden" msgid "" "Thank you for signing up. We have sent an email to you. Please follow the " @@ -673,12 +685,17 @@ msgid "" "contact Data Center Light Support." msgstr "Kontaktiere den Data Center Light Support." -msgid "Terminated" -msgstr "Beendet" - msgid "Error terminating VM" msgstr "Fehler beenden VM" +#, fuzzy, python-format +#| msgid "Virtual Machine Cancellation" +msgid "Virtual Machine %(vm_name)s Cancelled" +msgstr "VM Kündigung" + +#~ msgid "My VM page" +#~ msgstr "Meine VM page" + #~ msgid "" #~ "\n" #~ "You're receiving this email because you requested a password reset for " diff --git a/hosting/migrations/0043_vmdetail.py b/hosting/migrations/0043_vmdetail.py new file mode 100644 index 00000000..66966233 --- /dev/null +++ b/hosting/migrations/0043_vmdetail.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-09-24 18:12 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('hosting', '0042_hostingorder_subscription_id'), + ] + + operations = [ + migrations.CreateModel( + name='VMDetail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('vm_id', models.IntegerField(default=0)), + ('disk_size', models.FloatField(default=0.0)), + ('cores', models.FloatField(default=0.0)), + ('memory', models.FloatField(default=0.0)), + ('configuration', models.CharField(default='', max_length=25)), + ('ipv4', models.TextField(default='')), + ('ipv6', models.TextField(default='')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('terminated_at', models.DateTimeField(null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/hosting/models.py b/hosting/models.py index 478ed745..73c082bb 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -159,3 +159,16 @@ class HostingBill(AssignPermissionsMixin, models.Model): instance = cls.objects.create(customer=customer, billing_address=billing_address) return instance + + +class VMDetail(models.Model): + user = models.ForeignKey(CustomUser) + vm_id = models.IntegerField(default=0) + disk_size = models.FloatField(default=0.0) + cores = models.FloatField(default=0.0) + memory = models.FloatField(default=0.0) + configuration = models.CharField(default='', max_length=25) + ipv4 = models.TextField(default='') + ipv6 = models.TextField(default='') + created_at = models.DateTimeField(auto_now_add=True) + terminated_at = models.DateTimeField(null=True) diff --git a/hosting/static/hosting/css/landing-page.css b/hosting/static/hosting/css/landing-page.css index 5275dd97..48a0c1a1 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -857,6 +857,8 @@ a.list-group-item-danger:focus, background: rgba(235, 204, 209, 0.2); } .has-error .form-control, +.has-error .form-control:focus, +.has-error .form-control:active, .has-error .input-group-addon { color: #eb4d5c; border-color: #eb4d5c; @@ -871,6 +873,13 @@ a.list-group-item-danger.active:focus { background-color: #eb4d5c; } +/* bootstrap input box-shadom disable */ +.has-error .form-control:focus, +.has-error .form-control:active, +.has-success .form-control:focus, +.has-success .form-control:active { + box-shadow: inset 0 0 1px rgba(0,0,0,0.25); +} .checkmark { display: inline-block; } diff --git a/hosting/static/hosting/css/payment.css b/hosting/static/hosting/css/payment.css index 68a36837..b1b0460d 100644 --- a/hosting/static/hosting/css/payment.css +++ b/hosting/static/hosting/css/payment.css @@ -22,7 +22,100 @@ } .summary-box .content { - padding-top: 15px; +} +/* landing page payment new style */ +.last-p { + margin-bottom: 0; +} +.dcl-payment-section { + max-width: 391px; + margin: 0 auto 30px; + padding: 0 10px 30px; + border-bottom: 1px solid #edebeb; + height: 100%; +} +.dcl-payment-section hr{ + margin-top: 15px; + margin-bottom: 15px; +} +.dcl-payment-section .top-hr { + margin-left: -10px; +} +.dcl-payment-section h3 { + font-weight: 600; +} +.dcl-payment-section p { + padding: 0 5px; + font-weight: 400; +} +.dcl-payment-section .card-warning-content { + padding: 8px 10px; + font-weight: 300; +} +.dcl-payment-order strong{ + font-size: 17px; +} +.dcl-payment-order p { + font-weight: 300; +} +.dcl-payment-section .form-group { + margin-bottom: 10px; +} +.dcl-payment-section .form-control { + box-shadow: none; + padding: 6px 12px; + height: 32px; +} +.dcl-payment-user { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; +} + +.dcl-payment-user h4 { + font-weight: 600; + padding-left: 5px; + font-size: 17px; +} + +@media (min-width: 768px) { + .dcl-payment-grid { + display: flex; + align-items: stretch; + flex-wrap: wrap; + } + .dcl-payment-box { + width: 50%; + position: relative; + padding: 0 30px; + } + .dcl-payment-box:nth-child(2) { + order: 1; + } + .dcl-payment-box:nth-child(4) { + order: 2; + } + .dcl-payment-section { + padding: 15px 10px; + margin-bottom: 0; + border-bottom-width: 5px; + } + .dcl-payment-box:nth-child(2n) .dcl-payment-section { + border-bottom: none; + } + .dcl-payment-box:nth-child(1):after, + .dcl-payment-box:nth-child(2):after { + content: ' '; + display: block; + background: #eee; + width: 1px; + position: absolute; + right: 0; + z-index: 2; + top: 20px; + bottom: 20px; + } } \ No newline at end of file diff --git a/hosting/templates/hosting/choice_ssh_keys.html b/hosting/templates/hosting/choice_ssh_keys.html index 3a377388..87224156 100644 --- a/hosting/templates/hosting/choice_ssh_keys.html +++ b/hosting/templates/hosting/choice_ssh_keys.html @@ -47,20 +47,5 @@ window.location.href = '{{next_url}}'; {% endif %} - - - - - {%endblock%} diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html index 699a2d37..dc8de901 100644 --- a/hosting/templates/hosting/order_detail.html +++ b/hosting/templates/hosting/order_detail.html @@ -42,7 +42,9 @@

{% trans "Status" %}: - {% if order.status == 'Approved' %} + {% if vm.terminated_at %} + {% trans "Terminated" %} + {% elif order.status == 'Approved' %} {% trans "Approved" %} {% else %} {% trans "Declined" %} @@ -96,14 +98,14 @@

- {% comment %} + {% if vm.created_at %} +

+ {% trans "Period" %}: + {{ vm.created_at|date:'Y/m/d' }} - {% if vm.terminated_at %}{{ vm.terminated_at|date:'Y/m/d' }}{% else %}{% now 'Y/m/d' %}{% endif %} +

+ {% endif %}

- {% trans "Period" %} - {{}} -

- {% endcomment %} -

- {% trans "Cores" %} + {% trans "Cores" %}: {% if vm.cores %} {{vm.cores|floatformat}} {% else %} @@ -111,11 +113,11 @@ {% endif %}

- {% trans "Memory" %} + {% trans "Memory" %}: {{vm.memory}} GB

- {% trans "Disk space" %} + {% trans "Disk space" %}: {{vm.disk_size}} GB

@@ -171,7 +173,7 @@ {% trans "Hold tight, we are processing your request" %}

@@ -199,4 +201,4 @@ -{% endblock js_extra %} \ No newline at end of file +{% endblock js_extra %} diff --git a/hosting/templates/hosting/user_keys.html b/hosting/templates/hosting/user_keys.html index 1cfb880c..6810efdf 100644 --- a/hosting/templates/hosting/user_keys.html +++ b/hosting/templates/hosting/user_keys.html @@ -101,21 +101,5 @@ window.location.href = '{{next_url}}'; {% endif %} - - - - - {%endblock%} diff --git a/hosting/templates/hosting/virtual_machines.html b/hosting/templates/hosting/virtual_machines.html index 622e7a55..aac241e3 100644 --- a/hosting/templates/hosting/virtual_machines.html +++ b/hosting/templates/hosting/virtual_machines.html @@ -13,7 +13,12 @@ {% endif %} {% if not error %}
-

{% trans 'To create a new virtual machine, click "Create VM"' %}

+

{% trans 'To create a new virtual machine, click "Create VM"' %} + {% if show_create_ssh_key_msg %} + {% url 'hosting:create_ssh_key' as create_ssh_url %} +
{% blocktrans %}To access your VM, add your SSH key here{% endblocktrans %} + {% endif %} +

diff --git a/hosting/views.py b/hosting/views.py index 39f986a1..1f625c3a 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1,6 +1,7 @@ import json import logging import uuid +from datetime import datetime from time import sleep from django import forms @@ -37,6 +38,7 @@ from utils.forms import ( BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm, ResendActivationEmailForm ) +from utils.hosting_utils import get_vm_price from utils.mailer import BaseEmail from utils.stripe_utils import StripeUtils from utils.views import ( @@ -46,10 +48,11 @@ from utils.views import ( from .forms import HostingUserSignupForm, HostingUserLoginForm, \ UserHostingKeyForm, generate_ssh_key_name from .mixins import ProcessVMSelectionMixin -from .models import HostingOrder, HostingBill, HostingPlan, UserHostingKey +from .models import ( + HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail +) from datacenterlight.models import VMTemplate - logger = logging.getLogger(__name__) CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a \ @@ -627,13 +630,6 @@ class PaymentVMView(LoginRequiredMixin, FormView): return context def get(self, request, *args, **kwargs): - if not UserHostingKey.objects.filter(user=self.request.user).exists(): - messages.success( - request, - 'In order to create a VM, you create/upload your SSH KEY first.' - ) - return HttpResponseRedirect(reverse('hosting:ssh_keys')) - if 'next' in request.session: del request.session['next'] @@ -706,25 +702,30 @@ class OrdersHostingDetailView(LoginRequiredMixin, if obj is not None: # invoice for previous order try: - manager = OpenNebulaManager( - email=owner.email, password=owner.password - ) - vm = manager.get_vm(obj.vm_id) - context['vm'] = VirtualMachineSerializer(vm).data - 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.') - ) + 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']) + except VMDetail.DoesNotExist: + try: + manager = OpenNebulaManager( + email=owner.email, password=owner.password + ) + vm = manager.get_vm(obj.vm_id) + context['vm'] = VirtualMachineSerializer(vm).data + 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.') + ) elif not card_details.get('response_object'): # new order, failed to get card details context['failed_payment'] = True @@ -784,12 +785,11 @@ class OrdersHostingDetailView(LoginRequiredMixin, cpu = specs.get('cpu') memory = specs.get('memory') disk_size = specs.get('disk_size') - amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) - plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( - cpu=cpu, - memory=memory, - disk_size=disk_size) - + amount_to_be_charged = get_vm_price(cpu=cpu, memory=memory, + disk_size=disk_size) + 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, @@ -834,9 +834,10 @@ class OrdersHostingDetailView(LoginRequiredMixin, 'status': True, 'redirect': reverse('hosting:virtual_machines'), 'msg_title': str(_('Thank you for the order.')), - 'msg_body': str(_('Your VM will be up and running in a few moments.' - ' We will send you a confirmation email as soon as' - ' it is ready.')) + 'msg_body': str( + _('Your VM will be up and running in a few moments.' + ' We will send you a confirmation email as soon as' + ' it is ready.')) } return HttpResponse(json.dumps(response), @@ -894,6 +895,10 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView): context = {'error': 'connection'} else: context = super(ListView, self).get_context_data(**kwargs) + if UserHostingKey.objects.filter(user=self.request.user).exists(): + context['show_create_ssh_key_msg'] = False + else: + context['show_create_ssh_key_msg'] = True return context @@ -914,15 +919,6 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View): raise ValidationError(_('Invalid storage size')) def get(self, request, *args, **kwargs): - if not UserHostingKey.objects.filter(user=self.request.user).exists(): - messages.success( - request, - _( - 'In order to create a VM, you need to ' - 'create/upload your SSH KEY first.' - ) - ) - return HttpResponseRedirect(reverse('hosting:ssh_keys')) context = {'templates': VMTemplate.objects.all()} return render(request, self.template_name, context) @@ -933,7 +929,6 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View): memory_field = forms.IntegerField(validators=[self.validate_memory]) storage = request.POST.get('storage') storage_field = forms.IntegerField(validators=[self.validate_storage]) - price = request.POST.get('total') template_id = int(request.POST.get('config')) template = VMTemplate.objects.filter( opennebula_vm_template_id=template_id).first() @@ -965,7 +960,8 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View): extra_tags='storage') return HttpResponseRedirect( reverse('datacenterlight:index') + "#order_form") - + price = get_vm_price(cpu=cores, memory=memory, + disk_size=storage) specs = { 'cpu': cores, 'memory': memory, @@ -1072,6 +1068,10 @@ class VirtualMachineView(LoginRequiredMixin, View): except WrongIdError: response['status'] = True response['text'] = ugettext('Terminated') + vm_detail_obj = VMDetail.objects.filter( + vm_id=opennebula_vm_id).first() + vm_detail_obj.terminated_at = datetime.utcnow() + vm_detail_obj.save() break except BaseException: break diff --git a/membership/models.py b/membership/models.py index 5d7c7b11..d3f6372a 100644 --- a/membership/models.py +++ b/membership/models.py @@ -82,7 +82,7 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): @classmethod def register(cls, name, password, email, app='digital_glarus', - base_url=None, send_email=True): + base_url=None, send_email=True, account_details=None): user = cls.objects.filter(email=email).first() if not user: user = cls.objects.create_user(name=name, email=email, @@ -112,6 +112,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): 'template_name': 'user_activation', 'template_path': 'datacenterlight/emails/' } + if account_details: + email_data['context'][ + 'account_details'] = account_details email = BaseEmail(**email_data) email.send() return user @@ -176,6 +179,25 @@ class StripeCustomer(models.Model): def __str__(self): return "%s - %s" % (self.stripe_id, self.user.email) + @classmethod + def create_stripe_api_customer(cls, email=None, token=None, + customer_name=None): + """ + This method creates a Stripe API customer with the given + email, token and customer_name. This is different from + get_or_create method below in that it does not create a + CustomUser and associate the customer created in stripe + with it, while get_or_create does that before creating the + stripe user. + """ + stripe_utils = StripeUtils() + stripe_data = stripe_utils.create_customer(token, email, customer_name) + if stripe_data.get('response_object'): + stripe_cus_id = stripe_data.get('response_object').get('id') + return stripe_cus_id + else: + return None + @classmethod def get_or_create(cls, email=None, token=None): """ @@ -195,7 +217,6 @@ class StripeCustomer(models.Model): except StripeCustomer.DoesNotExist: user = CustomUser.objects.get(email=email) - stripe_utils = StripeUtils() stripe_data = stripe_utils.create_customer(token, email, user.name) if stripe_data.get('response_object'): diff --git a/opennebula_api/models.py b/opennebula_api/models.py index 057139c0..33812c52 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -150,9 +150,10 @@ class OpenNebulaManager(): oca.User.METHODS['allocate'], email, password, 'core') logger.debug( - "User {0} does not exist. Created the user. User id = {1}", - email, - opennebula_user + "User {} does not exist. Created the user. User id = {}".format( + email, + opennebula_user + ) ) return opennebula_user except ConnectionRefusedError: diff --git a/utils/forms.py b/utils/forms.py index a12034dd..f8a6d103 100644 --- a/utils/forms.py +++ b/utils/forms.py @@ -131,6 +131,12 @@ class BillingAddressForm(forms.ModelForm): } +class BillingAddressFormSignup(BillingAddressForm): + name = forms.CharField(label=_('Name')) + email = forms.EmailField(label=_('Email Address')) + field_order = ['name', 'email'] + + class UserBillingAddressForm(forms.ModelForm): user = forms.ModelChoiceField(queryset=CustomUser.objects.all(), widget=forms.HiddenInput()) diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py index 7c1a83ad..3c193ad7 100644 --- a/utils/hosting_utils.py +++ b/utils/hosting_utils.py @@ -1,4 +1,10 @@ -from hosting.models import UserHostingKey +import logging +from oca.pool import WrongIdError + +from hosting.models import UserHostingKey, VMDetail +from opennebula_api.serializers import VirtualMachineSerializer + +logger = logging.getLogger(__name__) def get_all_public_keys(customer): @@ -9,3 +15,48 @@ def get_all_public_keys(customer): """ return UserHostingKey.objects.filter(user_id=customer.id).values_list( "public_key", flat=True) + + +def get_or_create_vm_detail(user, manager, vm_id): + """ + Returns VMDetail object related to given vm_id. Creates the object + if it does not exist + + :param vm_id: The ID of the VM which should be greater than 0. + :param user: The CustomUser object that owns this VM + :param manager: The OpenNebulaManager object + :return: The VMDetail object. None if vm_id is less than or equal to 0. + Also, for the cases where the VMDetail does not exist and we can not + fetch data about the VM from OpenNebula, the function returns None + """ + if vm_id <= 0: + return None + try: + vm_detail_obj = VMDetail.objects.get(vm_id=vm_id) + except VMDetail.DoesNotExist: + try: + vm_obj = manager.get_vm(vm_id) + except (WrongIdError, ConnectionRefusedError) as e: + logger.error(str(e)) + return None + vm = VirtualMachineSerializer(vm_obj).data + vm_detail_obj = VMDetail.objects.create( + user=user, vm_id=vm_id, disk_size=vm['disk_size'], + cores=vm['cores'], memory=vm['memory'], + configuration=vm['configuration'], ipv4=vm['ipv4'], + ipv6=vm['ipv6'] + ) + return vm_detail_obj + + +def get_vm_price(cpu, memory, disk_size): + """ + A helper function that computes price of a VM from given cpu, ram and + ssd parameters + + :param cpu: Number of cores of the VM + :param memory: RAM of the VM + :param disk_size: Disk space of the VM + :return: The price of the VM + """ + return (cpu * 5) + (memory * 2) + (disk_size * 0.6) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index f35a6b9c..98f85d62 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -238,7 +238,7 @@ class StripeUtils(object): @staticmethod def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None): """ - Returns the stripe plan id string of the form + Returns the Stripe plan id string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters :param cpu: The number of cores @@ -256,7 +256,19 @@ class StripeUtils(object): if hdd is not None: dcl_plan_string = '{dcl_plan_string}-hdd-{hdd}gb'.format( dcl_plan_string=dcl_plan_string, hdd=hdd) - stripe_plan_id_string = '{app}-v{version}-{plan}'.format(app=app, - version=version, - plan=dcl_plan_string) + stripe_plan_id_string = '{app}-v{version}-{plan}'.format( + app=app, + version=version, + plan=dcl_plan_string) return stripe_plan_id_string + + @staticmethod + def get_stripe_plan_name(cpu, memory, disk_size): + """ + 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)