diff --git a/Changelog b/Changelog index 58835832..fe9a1dde 100644 --- a/Changelog +++ b/Changelog @@ -4,7 +4,7 @@ Next: * #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 b60a5e67..c0906022 100644 --- a/datacenterlight/locale/de/LC_MESSAGES/django.po +++ b/datacenterlight/locale/de/LC_MESSAGES/django.po @@ -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" @@ -151,6 +145,15 @@ msgstr "" "Adressleiste deines Browsers.<br/>\n" "%(base_url)s%(activation_link)s\n" +#, python-format +msgid "" +"Your account details are as follows:<br/><br/>\n" +"Username : Your email address<br/>\n" +"Password : %(account_details)s<br/><br/>\n" +"You can reset your password here:\n" +"%(base_url)s%(reset_password_url)s\n" +msgstr "" + #, python-format msgid "" "You can activate your Data Center Light account by clicking here.\n" @@ -163,6 +166,17 @@ msgstr "" "den folgenden Link in die Adressleiste deines Browsers.\n" "%(base_url)s%(activation_link)s\n" +#, python-format +msgid "" +"Your account details are as follows:\n" +"\n" +"Username : Your email address\n" +"Password : %(account_details)s\n" +"\n" +"You can reset your password here:\n" +"%(base_url)s%(reset_password_url)s\n" +msgstr "" + msgid "Home" msgstr "Home" @@ -264,6 +278,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?<br>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" @@ -288,23 +341,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 <a\n" -" href=\"https://stripe.com\" target=" -"\"_blank\">Stripe</a> for payment and do not store\n" -" your information in our database.\n" -" " +"Please fill in your credit card information below. We are using <a href=" +"\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not " +"store your information in our database." msgstr "" -"\n" -"Bitte füll Deine Kreditkarteninformationen unten aus. Wir nutzen <a href=" +"Bitte fülle Deine Kreditkarteninformationen unten aus. Wir nutzen <a href=" "\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und " "speichern keine Informationen in unserer Datenbank." @@ -330,6 +375,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" @@ -347,6 +399,36 @@ msgstr "" msgid "Place order" msgstr "Bestellen" +msgid "Processing..." +msgstr "Abarbeitung..." + +msgid "Hold tight, we are processing your request" +msgstr "Bitte warten - wir verbeiten Deine Anfrage gerade" + +msgid "Some problem encountered. Please try again later." +msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal." + +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 "Thank you for order! Our team will contact you via email" msgstr "" "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich mit " @@ -440,11 +522,38 @@ 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 "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 "Confirm Order" msgstr "Bestellung Bestätigen" diff --git a/datacenterlight/templates/datacenterlight/calculator_form.html b/datacenterlight/templates/datacenterlight/calculator_form.html index 9a0fcaa3..1733a719 100644 --- a/datacenterlight/templates/datacenterlight/calculator_form.html +++ b/datacenterlight/templates/datacenterlight/calculator_form.html @@ -80,43 +80,6 @@ <!--<div class="description check-ip"> <input type="checkbox" name="ipv6"> Ipv6 Only<br> </div>--> - <div class="form-group"> - <div class="description input justify-center"> - <label for="name" class="control-label">{% trans "Name"%}</label> - <input type="text" name="name" class="form-control" placeholder="{% trans 'Your Name'%}" - data-minlength="3" data-error="{% trans 'Please enter your name.' %}" required> - </div> - <div class="help-block with-errors"> - {% for message in messages %} - {% if 'name' in message.tags %} - <ul class="list-unstyled"> - <li> - {{ message|safe }} - </li> - </ul> - {% endif %} - {% endfor %} - </div> - </div> - <div class="form-group"> - <div class="description input justify-center"> - <label for="email" class="control-label">{% trans "Email"%}</label> - <input name="email" type="email" pattern="^[^@\s]+@([^@\s]+\.)+[^@\s]+$" class="form-control" - placeholder="{% trans 'Your Email' %}" - data-error="{% trans 'Please enter a valid email address.' %}" required> - </div> - <div class="help-block with-errors"> - {% for message in messages %} - {% if 'email' in message.tags %} - <ul class="list-unstyled"> - <li> - {{ message|safe }} - </li> - </ul> - {% endif %} - {% endfor %} - </div> - </div> </div> <input type="submit" class="btn btn-primary disabled" value="{% trans 'Continue' %}"></input> </form> diff --git a/datacenterlight/templates/datacenterlight/emails/user_activation.html b/datacenterlight/templates/datacenterlight/emails/user_activation.html index 955eed18..3c7fae74 100644 --- a/datacenterlight/templates/datacenterlight/emails/user_activation.html +++ b/datacenterlight/templates/datacenterlight/emails/user_activation.html @@ -11,4 +11,14 @@ You can also copy and paste the following link into the address bar of your brow to activate your Data Center Light account.<br/> {{base_url}}{{activation_link}} {% endblocktrans %} +{% if account_details %} +{% url 'hosting:reset_password' as reset_password_url %} +<br/><br/> +{% blocktrans %}Your account details are as follows:<br/><br/> +Username : Your email address<br/> +Password : {{account_details}}<br/><br/> +You can reset your password here: +{{base_url}}{{reset_password_url}} +{% endblocktrans %} +{% endif %} {% endblock %} diff --git a/datacenterlight/templates/datacenterlight/emails/user_activation.txt b/datacenterlight/templates/datacenterlight/emails/user_activation.txt index 84ec50a9..7c833256 100644 --- a/datacenterlight/templates/datacenterlight/emails/user_activation.txt +++ b/datacenterlight/templates/datacenterlight/emails/user_activation.txt @@ -7,4 +7,15 @@ You can also copy and paste the following link into the address bar of your brow to activate your Data Center Light account. {{base_url}}{{activation_link}} {% endblocktrans %} +{% if account_details %} +{% url 'hosting:reset_password' as reset_password_url %} +{% blocktrans %}Your account details are as follows: + +Username : Your email address +Password : {{account_details}} + +You can reset your password here: +{{base_url}}{{reset_password_url}} +{% endblocktrans %} +{% endif %} {% endblock %} 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 %} + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/paymentfont/1.1.2/css/paymentfont.min.css"/> +{% endblock css_extra %} + {% block navbar %} {% include "datacenterlight/includes/_navbar.html" %} {% endblock navbar %} {% block content %} <!-- Credit card form --> -<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/paymentfont/1.1.2/css/paymentfont.min.css"/> <div class="dcl-order-container"> <div class="payment-container"> - <div class="row"> - <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-order-sec"> - <h3><strong>{%trans "Your Order" %}</strong></h3> - <div class="col-xs-6 col-sm-12 col-md-12 col-lg-12 dcl-order-table-header"> - <div class="col-xs-12 col-sm-2 col-md-1 col-lg-1 tbl-header"> - {%trans "Cores" %} - </div> - <div class="col-xs-12 col-sm-3 col-md-4 col-lg-4 tbl-header"> - {%trans "Memory" %} - </div> - <div class="col-xs-12 col-sm-3 col-md-3 col-lg-3 tbl-header"> - {%trans "Disk space" %} - </div> - <div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-header"> - {%trans "Configuration" %} - </div> - </div> - <div class="col-xs-6 col-sm-12 col-md-12 col-lg-12 dcl-order-table-content"> - <div class="col-xs-12 col-sm-2 col-md-1 col-lg-1 tbl-content"> - {{request.session.specs.cpu|floatformat}} - </div> - <div class="col-xs-12 col-sm-3 col-md-4 col-lg-4 tbl-content"> - {{request.session.specs.memory|floatformat}} GB - </div> - <div class="col-xs-12 col-sm-3 col-md-3 col-lg-3 tbl-content"> - {{request.session.specs.disk_size|floatformat}} GB - </div> - <div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-content"> - {{request.session.template.name}} - </div> - </div> - <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-order-table-total"> - <div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-tot tbl-no-padding"> - {%trans "Total" %} <span>{%trans "including VAT" %}</span> - </div> - <div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-no-padding"> - <div class="col-xs-12 col-sm-4 col-md-4 col-lg-4"></div> - <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 tbl-total">{{request.session.specs.price}} - CHF<span class="dcl-price-month">/{% trans "Month" %}</span> + <div class="dcl-payment-grid"> + <div class="dcl-payment-box"> + <div class="dcl-payment-section"> + {% if request.user.is_authenticated %} + <div class="dcl-payment-user"> + <h4>{% trans "Welcome back" %} {{request.user.name}}!</h4> + <p>{% trans "Review your billing address and card details and proceed to make payment." %}</p> </div> - </div> + {% else %} + <h3>{%trans "Log in" %}</h3> + <hr class="top-hr"> + <p style="margin-bottom: 20px;">{% blocktrans %}Already signed up?<br>By logging in you can retrieve saved billing information.{% endblocktrans %}</p> + <form role="form" id="login-form" method="post" action="{% url 'hosting:login' %}" novalidate> + {% for field in login_form %} + {% csrf_token %} + {% bootstrap_field field show_label=False type='fields'%} + {% endfor %} + <input type='hidden' name='next' value='{{request.path}}'/> + <div class="form-group text-right"> + <button type="submit" class="btn btn-wide btn-vm-contact">{% trans "LOGIN" %}</button> + </div> + </form> + <p> + {% trans "Don't have an account yet?" %}<br> + {% trans "You can sign up by filling in the information below." %}<br> + <a href="{% url 'hosting:reset_password' %}">{% trans "Forgot password?" %}</a> + </p> + {% endif %} </div> </div> - </div> - <div class="row"> - <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-billing-sec"> - <div class="col-xs-12 col-sm-5 col-md-6 billing dcl-billing"> - <h3><b>{%trans "Billing Address"%}</b></h3> - <hr> + <div class="dcl-payment-box"> + <div class="dcl-payment-section"> + {% if not request.user.is_authenticated %} + <h3><b>{%trans "Sign up"%}</b></h3> + {% else %} + <h3><b>{%trans "Billing Address"%}</b></h3> + {% endif %} + <hr class="top-hr"> + {% for message in messages %} + {% if 'duplicate_email' in message.tags %} + <p class="text-danger">{{message}}</p> + {% endif %} + {% endfor %} <form role="form" id="billing-form" method="post" action="" novalidate> - {% for field in form %} {% csrf_token %} + {% for field in form %} {% bootstrap_field field show_label=False type='fields'%} {% endfor %} </form> </div> - <div class="col-xs-12 col-sm-7 col-md-6 creditcard-box dcl-creditcard"> + </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 class="last-p"><strong>{%trans "Total" %}</strong> <small>({%trans "including VAT" %})</small> <strong class="pull-right">{{request.session.specs.price}} CHF/{% trans "Month" %}</strong></p> + </div> + </div> + </div> + <div class="dcl-payment-box"> + <div class="dcl-payment-section"> <h3><b>{%trans "Credit Card"%}</b></h3> - <hr> + <hr class="top-hr"> + <p> + {% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %} + </p> <div> - <div> - <p> - {% blocktrans %} - Please fill in your credit card information below. We are using <a - href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store - your information in our database. - {% endblocktrans %} - </p> - </div> - <br> - <div> - {% if credit_card_data.last4 %} + {% if credit_card_data.last4 %} <form role="form" id="payment-form-with-creditcard" novalidate> <h5 class="billing-head">Credit Card</h5> <h5 class="membership-lead">Last 4: *****{{credit_card_data.last4}}</h5> <h5 class="membership-lead">Type: {{credit_card_data.cc_brand}}</h5> <input type="hidden" name="credit_card_needed" value="false"/> </form> - <div class="row"> - <div class="col-xs-12"> - {% if not messages and not form.non_field_errors %} - <p class="card-warning-content card-warning-addtional-margin"> - {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} - </p> - {% endif %} - <div id='payment_error'> - {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} - <ul class="list-unstyled"><li> - <p class="card-warning-content card-warning-error">{{ message|safe }}</p> - </li></ul> - {% endif %} - {% endfor %} - {% for error in form.non_field_errors %} - <p class="card-warning-content card-warning-error"> - {{ error|escape }} - </p> - {% endfor %} - </div> - </div> - <div class="col-xs-12"> - <div class="col-xs-6 pull-right"> - <button id="payment_button_with_creditcard" class="btn btn-success stripe-payment-btn" - type="submit"> - {%trans "Submit" %} - </button> - </div> - </div> + {% if not messages and not form.non_field_errors %} + <p class="card-warning-content card-warning-addtional-margin"> + {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} + </p> + {% endif %} + <div id='payment_error'> + {% for message in messages %} + {% if 'failed_payment' or 'make_charge_error' in message.tags %} + <ul class="list-unstyled"><li> + <p class="card-warning-content card-warning-error">{{ message|safe }}</p> + </li></ul> + {% endif %} + {% endfor %} + {% for error in form.non_field_errors %} + <p class="card-warning-content card-warning-error"> + {{ error|escape }} + </p> + {% endfor %} </div> - - {% else %} + <div class="text-right"> + <button id="payment_button_with_creditcard" class="btn btn-success btn-vm-contact" type="submit">{%trans "Submit" %}</button> + </div> + {% else %} <form action="" id="payment-form-new" method="POST"> <input type="hidden" name="token"/> <div class="group"> - <div class="col-xs-12 col-sm-12 col-md-10 col-lg-9 credit-card-goup"> - <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 card-element card-number-element"> + <div class="credit-card-goup"> + <div class="card-element card-number-element"> <label>{%trans "Card Number" %}</label> <div id="card-number-element" class="field my-input"></div> </div> - <div class="col-xs-5 col-sm-3 col-md-3 col-lg-3 card-element card-expiry-element"> - <label>{%trans "Expiry Date" %}</label> - <div id="card-expiry-element" class="field my-input"></div> + <div class="row"> + <div class="col-xs-5 card-element card-expiry-element"> + <label>{%trans "Expiry Date" %}</label> + <div id="card-expiry-element" class="field my-input"></div> + </div> + <div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element"> + <label>{%trans "CVC" %}</label> + <div id="card-cvc-element" class="field my-input"></div> + </div> </div> - <div class="col-xs-12 col-sm-2 col-md-6 col-lg-7 hide-mobile"></div> - <div class="col-xs-3 col-sm-3 col-md-3 col-lg-2 card-element card-cvc-element"> - <label>{%trans "CVC" %}</label> - <div id="card-cvc-element" class="field my-input"></div> - </div> - <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 card-element brand"> + <div class="card-element brand"> <label>{%trans "Card Type" %}</label> <i class="pf pf-credit-card" id="brand-icon"></i> </div> </div> </div> - <div id="card-errors" role="alert"></div> - <div class="row"> - <div class="col-xs-12"> - {% if not messages and not form.non_field_errors %} + <div id="card-errors"></div> + {% if not messages and not form.non_field_errors %} + <p class="card-warning-content"> + {% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %} + </p> + {% endif %} + <div id='payment_error'> + {% for message in messages %} + {% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags %} + <ul class="list-unstyled"> + <li><p class="card-warning-content card-warning-error">{{ message|safe }}</p></li> + </ul> + {% elif not form.non_field_errors %} <p class="card-warning-content"> - {% 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." %} </p> {% endif %} - <div id='payment_error'> - {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} - <ul class="list-unstyled"><li> - <p class="card-warning-content card-warning-error">{{ message|safe }}</p> - </li></ul> - {% endif %} - {% endfor %} + {% endfor %} - {% for error in form.non_field_errors %} - <p class="card-warning-content card-warning-error"> - {{ error|escape }} - </p> - {% endfor %} - </div> - </div> - <div class="col-xs-12"> - <div class="col-xs-6 pull-right"> - <button class="btn btn-success stripe-payment-btn" type="submit">{%trans "Submit" %} - </button> - </div> - </div> + {% for error in form.non_field_errors %} + <p class="card-warning-content card-warning-error"> + {{ error|escape }} + </p> + {% endfor %} + </div> + <div class="text-right"> + <button class="btn btn-vm-contact btn-wide" type="submit">{%trans "SUBMIT" %}</button> </div> - <div class="row" style="display:none;"> - <div class="col-xs-12"> - <p class="payment-errors"></p> - </div> + <div style="display:none;"> + <p class="payment-errors"></p> </div> </form> - - {% endif %} - </div> + {% endif %} </div> </div> </div> diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index dbb5ebbf..ad53360e 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -17,4 +17,5 @@ </div> </div> </form> -{% endblock submit_btn %} \ No newline at end of file +{% endblock submit_btn %} + diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 7d3559dd..ec87616f 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -1,19 +1,26 @@ +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.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.serializers import VMTemplateSerializer -from utils.forms import BillingAddressForm +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 @@ -21,6 +28,8 @@ 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" @@ -227,11 +236,6 @@ class IndexView(CreateView): 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: @@ -258,39 +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': 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): @@ -353,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 @@ -374,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: @@ -435,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'): @@ -452,6 +439,7 @@ class OrderConfirmationView(DetailView): 'cc_brand': card_details.get('response_object').get('brand'), 'vm': request.session.get('specs'), 'page_header_text': _('Confirm Order') + 'billing_address_data': request.session.get('billing_address_data') } return render(request, self.template_name, context) @@ -460,29 +448,40 @@ 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 = get_vm_price(cpu=cpu, memory=memory, - disk_size=disk_size) - specs['price'] = amount_to_be_charged + amount_to_be_charged = specs.get('price') plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu, memory=memory, disk_size=disk_size) @@ -496,7 +495,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') @@ -506,11 +505,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 74391147..6a50d7be 100644 --- a/hosting/locale/de/LC_MESSAGES/django.po +++ b/hosting/locale/de/LC_MESSAGES/django.po @@ -268,7 +268,7 @@ msgstr "" "Link klickst</a>.<br/>\n" msgid "My VM page" -msgstr "" +msgstr "Meine VM page" #, python-format msgid "" @@ -629,7 +629,7 @@ msgid "View Detail" msgstr "Details anzeigen" msgid "login" -msgstr "Einloggen" +msgstr "anmelden" msgid "" "Thank you for signing up. We have sent an email to you. Please follow the " 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/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 %} <div class="dashboard-subtitle"> - <p>{% trans 'To create a new virtual machine, click "Create VM"' %}</p> + <p>{% 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 %} + <br/>{% blocktrans %}To access your VM, add your SSH key <a href="{{create_ssh_url}}">here</a>{% endblocktrans %} + {% endif %} + </p> <div class="text-right"> <a class="btn btn-vm" href="{% url 'hosting:create_virtual_machine' %}"><span class="css-plus"></span> <span>{% trans "CREATE VM" %}</span></a> </div> diff --git a/hosting/views.py b/hosting/views.py index 4d736aa8..facc8c01 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -613,13 +613,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'] @@ -885,6 +878,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 @@ -905,15 +902,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) 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())