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 6d1dfd5d..de602e8d 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,44 @@ +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 + * #3628: [dcl] on hosting, VM is created at credit card info submit + * #3772: [dcl] Updated hosting app billing into monthly subscription and added new text and translations + * #3786: [hosting] Redesigned the hosting invoice and order-confirmation page + * #3728: [hosting] VM Termination animation added + * #3777: [hosting] Create new VM calculator added like dcl landing + * #3781: [hosting] Resend activation mail + * #3806: [hosting] Fix can not create VMs after password reset + * #3812: [hosting] Modal check icon made thin and font-size fixed + * Feature: [cms, blog] Added /cms prefix for all the django-cms generated urls + * Bugfix: [dcl, hosting] added host to celery error mails + * Bugfix: [ungleich] Fixed wrong subdomain digitalglarus.ungleich.ch +1.2.2: 2017-09-08 + * #3704: [hosting] Added my settings page + * #3771: [datacenterlight] Fixed the inconsistency in navbar style in billing page and onward + * #3769: [datacenterlight] Fixed EN dashboard url redirecting to the wrong page + * #3775: [hosting] Made the dashboard as the default start page for hosting app + * #3779: [hosting] Changed signup validation and activation page navbar transparent + * #3759: [hosting] Made the navbar style consistent to the dcl navbar and changed font weight from 300 to 400 for mobile navbar text + * #3644: [datacenterlight] Added a login button on landing + * #3659: [hosting] Changed hosting navbar design +1.2.1: 2017-09-06 + * #3757: [datacenterlight] Added /l route for linkedin +1.2: 2017-09-01 + * #3703: [hosting] Added a new dashboard + * #3717: [datacenterlight, hosting] Changed warning color for box + * #3748: [datacenterlight] Changed order msg position for mobile + * #3762: [hosting] Text fix View details to See Invoice + * #3765: [hosting] Text fix Your SSH Keys to My SSH Keys + * #3639: [datacenterlight] Added navbar menu after payment page on landing + * #3735: [hosting] Increased modal width and modal button width + * #3709: Activated Text Plugin by default for the Page Title Text, enabled tag text management + * #3768: [datacenterlight, hosting] Fixed missing DE translation + * #3678: [datacenterlight, hosting] Removed Lato font files +1.1.1: 2017-08-29 + * #3709: [datacenterlight] Added faq tos cms template + * #3657: [datacenterlight] Added a new contact section at landing + * #3740: [datacenterlight] Made contact section to send email to info when user submits a message + * #3757: [datacenterlight] Added new routes to dcl 1.1: 2017-08-24 * #3637: [datacenterlight, hosting] Added Stripe error handler * #3695: [hosting] Applied new design for VM list in hosting diff --git a/datacenterlight/forms.py b/datacenterlight/forms.py index 33d95c29..3ffe403c 100644 --- a/datacenterlight/forms.py +++ b/datacenterlight/forms.py @@ -1,6 +1,6 @@ from django import forms -from .models import BetaAccess +from .models import BetaAccess, ContactUs class BetaAccessForm(forms.ModelForm): @@ -11,6 +11,13 @@ class BetaAccessForm(forms.ModelForm): model = BetaAccess +class ContactForm(forms.ModelForm): + + class Meta: + fields = ['name', 'email', 'message'] + model = ContactUs + + # class BetaAccessVMForm(forms.ModelForm): # type = forms.CharField(widget=forms.EmailInput()) diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 3853d4e3..3c7869a1 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-08-24 11:28+0000\n" +"POT-Creation-Date: 2017-09-16 14:09+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -18,6 +18,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#, python-format +msgid "Your New VM %(vm_name)s at Data Center Light" +msgstr "Deine neue VM %(vm_name)s bei Data Center Light" + msgid "Enter name" msgstr "Name" @@ -82,8 +86,31 @@ msgstr "Bitte gib eine gültige E-Mailadresse ein." msgid "Continue" msgstr "Weiter" +msgid "Thank you for contacting us." +msgstr "Nachricht gesendet." + +msgid "Your message was successfully sent to our team." +msgstr "Vielen Dank für Deine Nachricht." + +msgid "Get in touch with us!" +msgstr "Sende uns eine Nachricht." + +msgid "Message" +msgstr "Nachricht" + +msgid "Sorry, there was an unexpected error. Kindly retry." +msgstr "" +"Bitte entschuldige, es scheint ein unerwarteter Fehler aufgetreten zu sein. " +"Versuche es doch bitte noch einmal." + +msgid "SUBMIT" +msgstr "ABSENDEN" + +msgid "Your Data Center Light Team" +msgstr "Dein Data Center Light Team" + msgid "Thank you for your request." -msgstr "Vielen Dank für Ihre Anfrage." +msgstr "Vielen Dank für Deine Anfrage." msgid "You are one step away from being our beta tester!" msgstr "" @@ -105,48 +132,39 @@ msgstr "" msgid "Thank you!" msgstr "Vielen Dank!" -msgid "account activation" -msgstr "Accountaktivierung" +msgid "Account Activation" +msgstr "Account Aktivierung" #, python-format msgid "" "\n" -" You can activate your %(dcl_text)s account by <a href=" -"\"%(base_url)s%(activation_link)s\">clicking here</a>.<br/><br/>\n" -" You can also copy and paste the following link into the " -"address bar of your browser and follow the link in order to activate your " -"datacenterlight account.<br/>\n" -" %(base_url)s%(activation_link)s\n" -" " -msgstr "" -"\n" -" <a href=\"%(base_url)s%(activation_link)s\">Klicke hier</a> " -"um deinen %(dcl_text)s zu aktivieren.<br/><br/>\n" -" Oder kopiere den folgenden Link in die Adressleiste deines " -"Browsers und folge dann dem Link um deinen %(dcl_text)s Account zu " -"aktivieren.<br/>\n" -" %(base_url)s%(activation_link)s\n" -" " - -msgid "Your" -msgstr "Dein" - -msgid "team" -msgstr "Team" - -#, python-format -msgid "" -"\n" -"Hi,\n" -"\n" -"You can activate your %(dcl_text)s account by clicking here %(base_url)s" -"%(activation_link)s\n" -msgstr "" -"\n" -"Hallo,\n" -"\n" -"Du kannst deinen %(dcl_text)s Account aktivieren, indem du hier klickst " +"You can activate your Data Center Light account by <a href=\"%(base_url)s" +"%(activation_link)s\">clicking here</a>.<br/>\n" +"You can also copy and paste the following link into the address bar of your " +"browser<br/>\n" +"to activate your Data Center Light account.<br/>\n" "%(base_url)s%(activation_link)s\n" +msgstr "" +"\n" +"<a href=\"%(base_url)s%(activation_link)s\">Klicke hier</a> um deinen Data " +"Center Light Account zu aktivieren oder kopiere den folgenden Link in die " +"Adressleiste deines Browsers.<br/>\n" +"%(base_url)s%(activation_link)s\n" + +#, python-format +msgid "" +"You can activate your Data Center Light account by clicking here.\n" +"You can also copy and paste the following link into the address bar of your " +"browser\n" +"to activate your Data Center Light account.\n" +"%(base_url)s%(activation_link)s\n" +msgstr "" +"Klicke hier, um deinen Data Center Light Account zu aktivieren oder kopiere " +"den folgenden Link in die Adressleiste deines Browsers.\n" +"%(base_url)s%(activation_link)s\n" + +msgid "Home" +msgstr "Home" msgid "Highlights" msgstr "" @@ -157,24 +175,30 @@ msgstr "Skalierung" msgid "Reliable and light" msgstr "Zuverlässig und leicht" +msgid "Pricing" +msgstr "Preise" + msgid "Order VM" msgstr "VM bestellen" msgid "Contact" msgstr "Kontakt" -msgid "Home" -msgstr "Home" - -msgid "Pricing" -msgstr "Preise" - msgid "All Rights Reserved" msgstr "Alle Rechte vorbehalten" +msgid "Toggle navigation" +msgstr "Umschalten" + msgid "Why Data Center Light?" msgstr "Warum Data Center Light?" +msgid "Login" +msgstr "Anmelden" + +msgid "Dashboard" +msgstr "" + msgid "Finally, an affordable VM hosting in Switzerland!" msgstr "Endlich: bezahlbares VM Hosting in der Schweiz" @@ -234,32 +258,14 @@ msgstr "" msgid "Affordable VM hosting based in Switzerland" msgstr "Bezahlbares VM Hosting in der Schweiz" +msgid "Contact us" +msgstr "Kontaktiere uns" + msgid "Switzerland " -msgstr "Schweiz" +msgstr "Schweiz " -msgid "Questions?" -msgstr "Fragen?" - -msgid "Contact us!" -msgstr "Kontaktiere uns!" - -msgid "Confirm Order" -msgstr "Bestellung Bestätigen" - -msgid "Date" -msgstr "Datum" - -msgid "Billed To:" -msgstr "Rechnungsadresse" - -msgid "Payment Method:" -msgstr "Bezahlmethode" - -msgid "ending" -msgstr "endend in" - -msgid "Order summary" -msgstr "Bestellungsübersicht" +msgid "Your Order" +msgstr "Deine Bestellung" msgid "Cores" msgstr "Prozessorkerne" @@ -274,13 +280,80 @@ msgid "Configuration" msgstr "Konfiguration" msgid "Total" -msgstr "" +msgstr "Gesamt" + +msgid "including VAT" +msgstr "inkl. Mehrwertsteuer" -#, fuzzy -#| msgid "month" 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" +" " +msgstr "" +"\n" +"Bitte füll 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." + +msgid "" +"You are not making any payment yet. After submitting your card information, " +"you will be taken to the Confirm Order 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 "Submit" +msgstr "Absenden" + +msgid "Card Number" +msgstr "Kreditkartennummer" + +msgid "Expiry Date" +msgstr "Ablaufdatum" + +msgid "CVC" +msgstr "" + +msgid "Card Type" +msgstr "Kartentyp" + +msgid "Processing" +msgstr "Weiter" + +msgid "Enter your credit card number" +msgstr "Deine Kreditkartennummer" + +msgid "Confirm Order" +msgstr "Bestellung Bestätigen" + +msgid "Date" +msgstr "Datum" + +msgid "Billed To:" +msgstr "Rechnungsadresse" + +msgid "Payment Method:" +msgstr "Bezahlmethode" + +msgid "ending in" +msgstr "endend in" + +msgid "Order summary" +msgstr "Bestellungsübersicht" + #, python-format msgid "" "By clicking \"Place order\" this plan will charge your credit card account " @@ -316,7 +389,7 @@ msgstr "" 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 " -"Ihnen via E-Mail in Verbindung." +"Dir via E-Mail in Verbindung." msgid "as soon as possible!" msgstr "" @@ -412,6 +485,28 @@ msgstr "ist kein gültiger Name" msgid "is not a proper email" msgstr "ist keine gültige E-Mailadresse" +#~ msgid "" +#~ "\n" +#~ "Hi,\n" +#~ "\n" +#~ "You can activate your %(dcl_text)s account by clicking here %(base_url)s" +#~ "%(activation_link)s\n" +#~ msgstr "" +#~ "\n" +#~ "Hallo,\n" +#~ "\n" +#~ "Du kannst deinen %(dcl_text)s Account aktivieren, indem du hier klickst " +#~ "%(base_url)s%(activation_link)s\n" + +#~ msgid "Your" +#~ msgstr "Dein" + +#~ msgid "team" +#~ msgstr "Team" + +#~ msgid "Questions?" +#~ msgstr "Fragen?" + #~ msgid "Please enter a value greater than or equal to 1." #~ msgstr "Bitte gib einen Wert größer oder gleich 1 ein." @@ -477,9 +572,6 @@ msgstr "ist keine gültige E-Mailadresse" #~ msgid "Buy Now!" #~ msgstr "Kaufe jetzt!" -#~ msgid "Email address" -#~ msgstr "E-Mail Adresse" - #~ msgid "Our promise" #~ msgstr "Unser Versprechen" diff --git a/datacenterlight/migrations/0007_contactus.py b/datacenterlight/migrations/0007_contactus.py new file mode 100644 index 00000000..12af594c --- /dev/null +++ b/datacenterlight/migrations/0007_contactus.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-19 21:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0006_vmtemplate'), + ] + + operations = [ + migrations.CreateModel( + name='ContactUs', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=250)), + ('email', models.CharField(max_length=250)), + ('message', models.TextField()), + ], + ), + ] diff --git a/datacenterlight/migrations/0008_contactus_field.py b/datacenterlight/migrations/0008_contactus_field.py new file mode 100644 index 00000000..ceea8f8f --- /dev/null +++ b/datacenterlight/migrations/0008_contactus_field.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-23 13:06 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0007_contactus'), + ] + + operations = [ + migrations.AddField( + model_name='contactus', + name='field', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2017, 8, 23, 13, 6, 24, 650869, tzinfo=utc)), + preserve_default=False, + ), + ] diff --git a/datacenterlight/migrations/0009_merge.py b/datacenterlight/migrations/0009_merge.py new file mode 100644 index 00000000..1f5d5bad --- /dev/null +++ b/datacenterlight/migrations/0009_merge.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-27 07:55 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0007_contactus'), + ('datacenterlight', '0008_auto_20170821_2024'), + ] + + operations = [ + ] diff --git a/datacenterlight/migrations/0010_merge.py b/datacenterlight/migrations/0010_merge.py new file mode 100644 index 00000000..72feedf5 --- /dev/null +++ b/datacenterlight/migrations/0010_merge.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-27 08:02 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0009_merge'), + ('datacenterlight', '0008_contactus_field'), + ] + + operations = [ + ] diff --git a/datacenterlight/models.py b/datacenterlight/models.py index f7b50a01..e2de41e1 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -57,7 +57,8 @@ class VMTemplate(models.Model): @classmethod def create(cls, name, opennebula_vm_template_id): - vm_template = cls(name=name, opennebula_vm_template_id=opennebula_vm_template_id) + vm_template = cls( + name=name, opennebula_vm_template_id=opennebula_vm_template_id) return vm_template @@ -71,3 +72,10 @@ class StripePlan(models.Model): def create(cls, stripe_plan_id): stripe_plan = cls(stripe_plan_id=stripe_plan_id) return stripe_plan + + +class ContactUs(models.Model): + name = models.CharField(max_length=250) + email = models.CharField(max_length=250) + message = models.TextField() + field = models.DateTimeField(auto_now_add=True) diff --git a/datacenterlight/static/datacenterlight/css/cms.css b/datacenterlight/static/datacenterlight/css/cms.css new file mode 100644 index 00000000..abf06501 --- /dev/null +++ b/datacenterlight/static/datacenterlight/css/cms.css @@ -0,0 +1,47 @@ +.dcl-cms_page-full-width { + color: #fff; + text-align: center; + background-image: -ms-linear-gradient(right, #29427A 50%, #4F6699 100%); + background-image: -moz-linear-gradient(right, #29427A 50%, #4F6699 100%); + background-image: -o-linear-gradient(right, #29427A 50%, #4F6699 100%); + background-image: -webkit-gradient(linear, right top, left top, color-stop(50, #29427A), color-stop(100, #4F6699)); + background-image: -webkit-linear-gradient(right, #29427A 50%, #4F6699 100%); + background-image: linear-gradient(to left, #29427A 50%, #4F6699 100%); +} + +.dcl-cms_page-header { + padding: 150px 0 150px 0; + text-align: center; + color: #f8f8f8; + background: url(../img/pattern.jpg) no-repeat center center; + background-size: cover; + position: relative; + background-attachment: fixed; +} + +.dcl-cms_page-header::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: rgba(90, 116, 175, 0.85); +} + +#dcl-cms_page-text { + background: #fff; +} + +#dcl-cms_page-text h3 { + font-size: 42px; + width: 70%; +} + +@media (max-width: 767px) { + #dcl-cms_page-text h3 { + font-size: 30px; + line-height: 40px; + width: 100%; + } +} \ No newline at end of file diff --git a/datacenterlight/static/datacenterlight/css/landing-page.css b/datacenterlight/static/datacenterlight/css/landing-page.css index a5562710..d50a864d 100755 --- a/datacenterlight/static/datacenterlight/css/landing-page.css +++ b/datacenterlight/static/datacenterlight/css/landing-page.css @@ -4,11 +4,6 @@ * For details, see http://www.apache.org/licenses/LICENSE-2.0. */ -/*@font-face { - font-family: 'Lato-Light'; - src: url('../fonts/Lato/Lato-Light.ttf'); -}*/ - body, html { width: 100%; @@ -127,7 +122,7 @@ button, input, optgroup, select, textarea { .navbar-default { background: #fff; - border: none; + /* border: none; */ padding: 5px; } @@ -148,15 +143,19 @@ button, input, optgroup, select, textarea { .navbar-default .navbar-nav>li>a { cursor: pointer; - /*font-family: 'Lato-Light', sans-serif;*/ - font-weight: 300; + font-weight: 400; } .navbar-transparent .navbar-nav>li>a { color: #fff; cursor: pointer; - /*font-family: 'Lato-Light', sans-serif;*/ - font-weight: 300; +} + +@media (min-width: 768px) { + .navbar-default .navbar-nav>li>a, + .navbar-transparent .navbar-nav>li>a { + font-weight: 300; + } } .navbar-transparent .navbar-nav>li>a:hover { @@ -323,9 +322,9 @@ button, input, optgroup, select, textarea { padding-top: 50px; /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */ padding-bottom: 50px; - text-align: center; +/* text-align: center; */ color: #f8f8f8; - background: url(../img/banner-bg.jpg) no-repeat center center; + background: url(../img/pattern.jpg) no-repeat center center; background-size: cover; position: relative; } @@ -654,74 +653,161 @@ button, input, optgroup, select, textarea { position: relative; } -.full-contact-section { - background-image: -ms-linear-gradient(right, #29427A 50%, #4F6699 100%); - background-image: -moz-linear-gradient(right, #29427A 50%, #4F6699 100%); - background-image: -o-linear-gradient(right, #29427A 50%, #4F6699 100%); - background-image: -webkit-gradient(linear, right top, left top, color-stop(50, #29427A), color-stop(100, #4F6699)); - background-image: -webkit-linear-gradient(right, #29427A 50%, #4F6699 100%); - background-image: linear-gradient(to left, #29427A 50%, #4F6699 100%); -} - .contact-section { - padding: 60px 0; - color: #fff; + padding: 80px 0; + color: rgba(255,255,255,0.9); background-attachment: fixed; } -.contact-section .card { - text-align: center; - width: 350px; - margin: 0 auto; - background: #fff; - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); - padding-bottom: 40px; - border-radius: 7px; - color: #4c4444; - box-sizing: border-box; - padding: 45px; - margin-top: -115px; +.contact-section .modal { + color: #333; } -.contact-section .card .social a { - color: #29427A; +.contact-details { + padding-left: 5px; +} + +.contact-section .description{ + font-size: 20px; +} + +.contact-section .social a { + color: #fff; font-size: 45px; } -.contact-section .card .subtitle h3 { - font-size: 30px; - margin-bottom: 23px; +.contact-section .social .fa-facebook { + font-size: 40px; + background: #fff; + border-radius: 100%; + color: #425d89; + width: 40px; + text-align: center; + top: -2px; + position: relative; + left: 10px; +} +.contact-section .social .fa-facebook:before { + font-size: 32px; + position: relative; + top: -1px; + left: -1px; } -.contact-section .card .social a:hover { +.contact-section .social a:hover { text-decoration: none; } -.contact-section .title { - margin-right: auto; - width: 80%; - max-width: 468px; +.contact-section .subtitle h3 { + font-size: 30px; + margin-bottom: 15px; +} + +.contact-section .contact-form-success { + font-size: 18px; + text-align: center; + background-color: rgba(0,0,0,0.2); + padding: 0 15px 35px; + margin-top: 25px; } .contact-section .title h2 { font-size: 65px; margin: 0; - color: #fff; - padding-bottom: 25px; position: relative; - text-align: right; +/* color: #eee; + padding-bottom: 25px; + text-align: right; */ } -.contact-section .title h2::before { - content: ""; +.contact-form .form-group { + border: 0; + margin-bottom: 20px; +} + +.contact-form .form-group label { + letter-spacing: 0.6px; + font-weight: 400; +} + +.contact-form .btn { + min-width: 140px; + background: rgba(23, 23, 23, 0.18); + color: #fff; + border-radius: 4px; + border-width: 2px; + box-shadow: none; + letter-spacing: 2px; + border-color: #fff; +} + +.contact-form .btn.sending { + cursor: wait; +} + +@keyframes sending { + 0% {content: '.';} + 50% {content: '..';} + 100% {content: '...';} +} + +.contact-form .btn.sending:after { + content: '.'; position: absolute; - bottom: 0; - background: #fff; - height: 7px; - width: 70px; - right: 0; + display: inline-block; + text-align: left; + margin-left: 5px; + width: 20px; + animation: sending 1s linear infinite; } +.contact-form .btn:hover, +.contact-form .btn:focus { + background: rgba(23, 23, 23, 0.28); + border-color: #fff; + box-shadow: none; + outline: 0; +} + +.contact-form .form-control { + box-shadow: none; + border-color: #ccc; +} + +.contact-form .errorlist { + list-style: none; + padding: 5px; + margin: 0; + color: rgb(255, 164, 164); + font-weight: 600; + letter-spacing: 0.4px; +} + +.contact-form .form-error { + background: rgba(255,255,255,0.9); + color: #eb4d5c; + padding: 10px; + text-align: center; + margin-bottom: 20px; + border-radius: 5px; +} + +.contact-form .has-error label { + color: #fff; +} + +.contact-form .has-error .form-control { + border: 2px solid #e8534b; + box-shadow: none; +} + +.contact-form .subtitle { + padding: 22px 0 15px; +} + +.contact-form textarea { + resize: none; +} /*Why DCL*/ @@ -842,7 +928,7 @@ tech-sub-sec h2 { border: 1px solid #fff; -webkit-box-shadow: -8px 13px 31px -8px rgba(77, 77, 77, 1); -moz-box-shadow: -8px 13px 31px -8px rgba(77, 77, 77, 1); - box-shadow: -8px 13px 31px -8px rgba(77, 77, 77, 1); + box-shadow: -8px 14px 20px -5px rgba(77, 77, 77, 0.5); display: none; text-align: center; border-radius: 4px !important; @@ -885,13 +971,21 @@ tech-sub-sec h2 { .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover { background: transparent; +} +.navbar-transparent .navbar-nav>.open>a, +.navbar-transparent .navbar-nav>.open>a:focus, +.navbar-transparent .navbar-nav>.open>a:hover { color: #fff; } .dropdown-menu>li>a { font-size: 13px; - font-weight: 300; - /*font-family: 'Lato-Light', sans-serif;*/ +} + +@media (min-width: 768px) { + .dropdown-menu>li>a { + font-weight: 300; + } } .navbar-default .navbar-nav>.active>a, @@ -1311,9 +1405,9 @@ tech-sub-sec h2 { margin: 0 auto; } .contact-section .title h2 { - font-size: 35px; + font-size: 45px; line-height: 40px; - text-align: center; +/* text-align: center; */ margin-top: 35px; } .contact-section .title h2::before { @@ -1558,4 +1652,21 @@ a.list-group-item-danger.active:focus { } .panel-danger > .panel-heading .badge { background-color: #eb4d5c; -} \ No newline at end of file +} + +.checkmark { + display: inline-block; +} +.checkmark:after { + /*Add another block-level blank space*/ + content: ''; + display: block; + /*Make it a small rectangle so the border will create an L-shape*/ + width: 25px; + height: 60px; + /*Add a white border on the bottom and left, creating that 'L' */ + border: solid #777; + border-width: 0 3px 3px 0; + /*Rotate the L 45 degrees to turn it into a checkmark*/ + transform: rotate(45deg); +} diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Black.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Black.ttf deleted file mode 100755 index 6848db0d..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Black.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-BlackItalic.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-BlackItalic.ttf deleted file mode 100755 index 5decf129..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-BlackItalic.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Bold.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Bold.ttf deleted file mode 100755 index 74343694..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Bold.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-BoldItalic.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-BoldItalic.ttf deleted file mode 100755 index 684aacf5..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-BoldItalic.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Hairline.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Hairline.ttf deleted file mode 100755 index 288be295..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Hairline.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-HairlineItalic.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-HairlineItalic.ttf deleted file mode 100755 index c2bfd335..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-HairlineItalic.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Italic.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Italic.ttf deleted file mode 100755 index 3d3b7a29..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Italic.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Light.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Light.ttf deleted file mode 100755 index a958067a..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Light.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-LightItalic.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-LightItalic.ttf deleted file mode 100755 index 5e45ad9a..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-LightItalic.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Regular.ttf b/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Regular.ttf deleted file mode 100755 index 04ea8efb..00000000 Binary files a/datacenterlight/static/datacenterlight/fonts/Lato/Lato-Regular.ttf and /dev/null differ diff --git a/datacenterlight/static/datacenterlight/fonts/Lato/OFL.txt b/datacenterlight/static/datacenterlight/fonts/Lato/OFL.txt deleted file mode 100755 index f8ca7bc3..00000000 --- a/datacenterlight/static/datacenterlight/fonts/Lato/OFL.txt +++ /dev/null @@ -1,92 +0,0 @@ -Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/datacenterlight/static/datacenterlight/img/dcl-email-bg.jpg b/datacenterlight/static/datacenterlight/img/dcl-email-bg.jpg new file mode 100644 index 00000000..5a62f3e7 Binary files /dev/null and b/datacenterlight/static/datacenterlight/img/dcl-email-bg.jpg differ diff --git a/datacenterlight/static/datacenterlight/img/facebook_logo.svg b/datacenterlight/static/datacenterlight/img/facebook_logo.svg new file mode 100644 index 00000000..c2ab1b51 --- /dev/null +++ b/datacenterlight/static/datacenterlight/img/facebook_logo.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="57px" height="66px" viewBox="0 0 57 66" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch --> + <title>Slice 20 + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/datacenterlight/static/datacenterlight/js/main.js b/datacenterlight/static/datacenterlight/js/main.js index 5594fe11..ab37a68b 100644 --- a/datacenterlight/static/datacenterlight/js/main.js +++ b/datacenterlight/static/datacenterlight/js/main.js @@ -39,7 +39,7 @@ _initScroll(); _initNavUrl(); _initPricing(); - + ajaxForms(); }); $(window).resize(function() { @@ -86,17 +86,29 @@ } function _initNavUrl() { + $('.url-init').each(function(idx, el) { + var $this = $(el); + var currentPath = window.location.pathname; + var thisPaths = $this.attr('href').split('#') + if ($this.hasClass('dropdown-toggle') && window.matchMedia("(max-width: 767px)").matches) { + $this.removeClass('url-init'); + $this.attr('href', ''); + } else if ($('#'+thisPaths[1]).length) { + $this.removeClass('url-init').addClass('url'); + $this.attr('href', '#' + thisPaths[1]); + } else { + $this.removeClass('url-init'); + } + }); $('.url').click(function(event) { event.preventDefault(); - var href = $(this).attr('data-url'); + var href = $(this).attr('href'); $('.navbar-collapse').removeClass('in'); $('.navbar-collapse').addClass('collapsing'); if ($(href).length) { $('html, body').animate({ scrollTop: $(href).offset().top }, 1000); - } else { - window.location.href = '/datacenterlight' + href; } }); } @@ -135,7 +147,6 @@ function _fetchPricing() { Object.keys(cardPricing).map(function(element) { - //$('#'+cardPricing[element].id).val(cardPricing[element].value); $('input[name=' + element + ']').val(cardPricing[element].value); }); _calcPricing(); @@ -157,4 +168,27 @@ $('#valueTotal').text(numbers * price * 31); } -})(jQuery); + function ajaxForms() { + $('body').on('submit', '.ajax-form', function(e){ + e.preventDefault(); + var $form = $(this); + $form.find('[type=submit]').addClass('sending'); + $.ajax({ + url: $form.attr('action'), + type: $form.attr('method'), + data: $form.serialize(), + + success: function(response) { + var responseContain = $($form.attr('data-response')); + responseContain.html(response); + $form.find('[type=submit]').removeClass('sending'); + }, + + error: function() { + $form.find('[type=submit]').removeClass('sending'); + $form.find('.form-error').removeClass('hide'); + } + }); + }) + } +})(jQuery); \ No newline at end of file diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index 1e3e1caa..1335869b 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -1,15 +1,22 @@ -from dynamicweb.celery import app +from datetime import datetime + +from celery.exceptions import MaxRetriesExceededError from celery.utils.log import get_task_logger +from celery import current_task from django.conf import settings +from django.core.mail import EmailMessage +from django.utils import translation +from django.utils.translation import ugettext_lazy as _ + +from dynamicweb.celery import app +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 hosting.models import HostingOrder, HostingBill +from utils.hosting_utils import get_all_public_keys from utils.forms import UserBillingAddressForm -from datetime import datetime -from membership.models import StripeCustomer -from django.core.mail import EmailMessage +from utils.mailer import BaseEmail from utils.models import BillingAddress -from celery.exceptions import MaxRetriesExceededError logger = get_task_logger(__name__) @@ -45,25 +52,36 @@ 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)) vm_id = None try: final_price = specs.get('price') billing_address = BillingAddress.objects.filter( id=billing_address_id).first() customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() - # Create OpenNebulaManager - manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME, - password=settings.OPENNEBULA_PASSWORD) - # Create a vm using oneadmin, also specify the name + if 'pass' in user: + on_user = user.get('email') + on_pass = user.get('pass') + logger.debug("Using user {user} to create VM".format(user=on_user)) + vm_name = None + else: + on_user = settings.OPENNEBULA_USERNAME + on_pass = settings.OPENNEBULA_PASSWORD + logger.debug("Using OpenNebula admin user to create VM") + vm_name = "{email}-{template_name}-{date}".format( + email=user.get('email'), + template_name=template.get('name'), + date=int(datetime.now().strftime("%s"))) + + # Create OpenNebulaManager + manager = OpenNebulaManager(email=on_user, password=on_pass) + vm_id = manager.create_vm( template_id=vm_template_id, specs=specs, ssh_key=settings.ONEADMIN_USER_SSH_PUBLIC_KEY, - vm_name="{email}-{template_name}-{date}".format( - email=user.get('email'), - template_name=template.get('name'), - date=int(datetime.now().strftime("%s"))) + vm_name=vm_name ) if vm_id is None: @@ -122,6 +140,54 @@ def create_vm_task(self, vm_template_id, user, specs, template, } email = EmailMessage(**email_data) email.send() + + if 'pass' in user: + lang = 'en-us' + if user.get('language') is not None: + 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 + context = { + 'vm': vm, + 'order': order, + 'base_url': "{0}://{1}".format(user.get('request_scheme'), + user.get('request_host')), + 'page_header': _( + 'Your New VM %(vm_name)s at Data Center Light') % { + 'vm_name': vm.get('name')} + } + email_data = { + 'subject': context.get('page_header'), + 'to': user.get('email'), + 'context': context, + 'template_name': 'new_booked_vm', + 'template_path': 'hosting/emails/', + 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS, + } + email = BaseEmail(**email_data) + email.send() + + # try to see if we have the IP and that if the ssh keys can + # be configured + new_host = manager.get_primary_ipv4(vm_id) + 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')) + if custom_user is not None: + public_keys = get_all_public_keys(custom_user) + keys = [{'value': key, 'state': True} for key in + public_keys] + if len(keys) > 0: + logger.debug( + "Calling configure on {host} for {num_keys} keys".format( + host=new_host, num_keys=len(keys))) + # Let's delay the task by 75 seconds to be sure + # that we run the cdist configure after the host + # is up + manager.manage_public_key(keys, + hosts=[new_host], + countdown=75) except Exception as e: logger.error(str(e)) try: @@ -134,8 +200,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, email_data = { 'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT, msg_text), - 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': ['info@ungleich.ch'], + 'from_email': current_task.request.hostname, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, 'body': ',\n'.join(str(i) for i in self.request.args) } email = EmailMessage(**email_data) diff --git a/datacenterlight/templates/datacenterlight/base.html b/datacenterlight/templates/datacenterlight/base.html index fc69a2d5..45b30cad 100644 --- a/datacenterlight/templates/datacenterlight/base.html +++ b/datacenterlight/templates/datacenterlight/base.html @@ -1,4 +1,4 @@ -{% load staticfiles i18n%} +{% load staticfiles i18n cms_tags sekizai_tags %} {% get_current_language as LANGUAGE_CODE %} @@ -10,7 +10,7 @@ - + {% cms_toolbar %} Data Center Light - {% block title %}VM hosting made in Switzerland{% endblock %} @@ -33,13 +33,14 @@ + {% render_block "css" postprocessor "compressor.contrib.sekizai.compress" %} + {% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %} {% include "google_analytics.html" %} - {% include "datacenterlight/includes/_navbar.html" %} diff --git a/datacenterlight/templates/datacenterlight/beta_success.html b/datacenterlight/templates/datacenterlight/beta_success.html index 2512a05c..60df607c 100644 --- a/datacenterlight/templates/datacenterlight/beta_success.html +++ b/datacenterlight/templates/datacenterlight/beta_success.html @@ -8,7 +8,7 @@ diff --git a/datacenterlight/templates/datacenterlight/calculator_form.html b/datacenterlight/templates/datacenterlight/calculator_form.html index cdba4809..b5bac1f9 100644 --- a/datacenterlight/templates/datacenterlight/calculator_form.html +++ b/datacenterlight/templates/datacenterlight/calculator_form.html @@ -26,9 +26,9 @@
{% for message in messages %} {% if 'cores' in message.tags %} - + {% endif %} {% endfor %}
diff --git a/datacenterlight/templates/datacenterlight/cms_page.html b/datacenterlight/templates/datacenterlight/cms_page.html new file mode 100644 index 00000000..3e3b038a --- /dev/null +++ b/datacenterlight/templates/datacenterlight/cms_page.html @@ -0,0 +1,35 @@ +{% extends "datacenterlight/base.html" %} +{% load staticfiles cms_tags sekizai_tags %} +{% block title %} +{% page_attribute page_title %} +{% endblock %} +{% block content %} +{% addtoblock "css" %} + +{% endaddtoblock %} +
+
+
+
+
+
+

{% page_attribute page_title %}

+
+
+
+
+
+
+ +
+
+
+
+
+ {% placeholder 'datacenterlight_cms_page_text' %} +
+
+
+
+
+{% endblock %} diff --git a/datacenterlight/templates/datacenterlight/contact_form.html b/datacenterlight/templates/datacenterlight/contact_form.html new file mode 100644 index 00000000..458d6168 --- /dev/null +++ b/datacenterlight/templates/datacenterlight/contact_form.html @@ -0,0 +1,50 @@ +{% load i18n %} + +{% if success %} +
+
+

{% trans "Thank you for contacting us." %}

+
+

+ {% trans "Your message was successfully sent to our team." %} +

+
+{% else %} +
+
+
+

{% trans "Get in touch with us!" %}

+
+
+
+
+ {% csrf_token %} +
+ +
+ + {{contact_form.name.errors}} +
+
+
+ +
+ + {{contact_form.email.errors}} +
+
+
+ +
+ + {{contact_form.message.errors}} +
+
+
+
+
{% trans "Sorry, there was an unexpected error. Kindly retry." %}
+ +
+
+
+{% endif %} \ No newline at end of file diff --git a/datacenterlight/templates/datacenterlight/emails/base_email_datacenterlight.html b/datacenterlight/templates/datacenterlight/emails/base_email_datacenterlight.html new file mode 100644 index 00000000..be8479d9 --- /dev/null +++ b/datacenterlight/templates/datacenterlight/emails/base_email_datacenterlight.html @@ -0,0 +1,176 @@ +{% load static from staticfiles %} +{% load i18n %} + + + + + + + {{dcl_text}} + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ + + + + +
+ logo + +
+
+ +
+
+
+
+ + + + + + + + + + +
+ {% block email_head %} + {% endblock %} +
+

+ {% block email_body %} + {% endblock %} +

  +
+
+
+
+ + + + +
{% trans 'Your Data Center Light Team' %} +
+
+
+ + + diff --git a/datacenterlight/templates/datacenterlight/emails/base_email_datacenterlight.txt b/datacenterlight/templates/datacenterlight/emails/base_email_datacenterlight.txt new file mode 100644 index 00000000..46fd0730 --- /dev/null +++ b/datacenterlight/templates/datacenterlight/emails/base_email_datacenterlight.txt @@ -0,0 +1,7 @@ +{% load static from staticfiles %} +{% load i18n %} +{% block email_head %} +{% endblock %} +{% block email_body %} +{% endblock %} +{% trans 'Your Data Center Light Team' %} diff --git a/datacenterlight/templates/datacenterlight/emails/request_access_confirmation.html b/datacenterlight/templates/datacenterlight/emails/request_access_confirmation.html index e96a8ef1..2f71944f 100644 --- a/datacenterlight/templates/datacenterlight/emails/request_access_confirmation.html +++ b/datacenterlight/templates/datacenterlight/emails/request_access_confirmation.html @@ -74,7 +74,7 @@
-
+ logo @@ -95,12 +95,12 @@
- -
+ {% trans "Thank you for your request." %}
+

{% trans "You are one step away from being our beta tester!" %}

{% trans "Currently we are running our tests to make sure everything runs perfectly." %}
{% trans "In the meantime, we would like to ask you a little patience
until our team contacts you with beta access." %}
@@ -117,7 +117,7 @@

-
Your data center light team
+
Your data center light team
diff --git a/datacenterlight/templates/datacenterlight/emails/request_access_notification.html b/datacenterlight/templates/datacenterlight/emails/request_access_notification.html index 09531ba6..095c917e 100644 --- a/datacenterlight/templates/datacenterlight/emails/request_access_notification.html +++ b/datacenterlight/templates/datacenterlight/emails/request_access_notification.html @@ -73,7 +73,7 @@
-
+ logo @@ -94,12 +94,12 @@
- - @@ -113,7 +113,7 @@
+ An user requested a beta access
+

User {{email}} requested beta access

-
Your data center light team
+
Your data center light team
diff --git a/datacenterlight/templates/datacenterlight/emails/request_beta_access_notification.html b/datacenterlight/templates/datacenterlight/emails/request_beta_access_notification.html index 7991eb69..c6f3033e 100644 --- a/datacenterlight/templates/datacenterlight/emails/request_beta_access_notification.html +++ b/datacenterlight/templates/datacenterlight/emails/request_beta_access_notification.html @@ -73,7 +73,7 @@
-
+ logo @@ -94,14 +94,14 @@
- - {% endfor %} diff --git a/hosting/templates/hosting/payment.html b/hosting/templates/hosting/payment.html index 499511f8..9b1c9e45 100644 --- a/hosting/templates/hosting/payment.html +++ b/hosting/templates/hosting/payment.html @@ -1,8 +1,12 @@ {% extends "hosting/base_short.html" %} {% load staticfiles bootstrap3 i18n %} + +{% block css_extra %} + +{% endblock css_extra %} + {% block content %} -
@@ -87,7 +91,7 @@
{% if not messages and not form.non_field_errors %}

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

{% endif %}
@@ -144,7 +148,7 @@
{% if not messages and not form.non_field_errors %}

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

{% endif %}
@@ -207,7 +211,6 @@ window.hasCreditcard = true; })(); - {%endif%} {%endblock%} diff --git a/hosting/templates/hosting/resend_activation_link.html b/hosting/templates/hosting/resend_activation_link.html new file mode 100644 index 00000000..fffb6e59 --- /dev/null +++ b/hosting/templates/hosting/resend_activation_link.html @@ -0,0 +1,36 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3%} +{% load i18n %} + +{% block navbar %} + {% include 'hosting/includes/_navbar_transparent.html' %} +{% endblock navbar %} + + +{% block content %} +
+
+
+
+

{% trans "Your VM hosted in Switzerland"%}

+
+
+ +
+
+
+{% endblock %} diff --git a/hosting/templates/hosting/settings.html b/hosting/templates/hosting/settings.html new file mode 100644 index 00000000..0bafe8e5 --- /dev/null +++ b/hosting/templates/hosting/settings.html @@ -0,0 +1,149 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3 i18n %} + +{% block css_extra %} + +{% endblock css_extra %} + +{% block content %} +
+
+

{% trans "My Settings" %}

+
+ +
+
+
+

{%trans "Billing Address"%}

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

{%trans "Credit Card"%}

+
+
+ {% if credit_card_data.last4 %} +
+
{% trans "Credit Card" %}
+
{% trans "Last" %} 4: *****{{credit_card_data.last4}}
+
{% trans "Type" %}: {{credit_card_data.cc_brand}}
+ {% comment %} + + {% endcomment %} +
+ {% else %} +
+

{% trans "No Credit Cards Added" %}

+

{% blocktrans %}We are using Stripe for payment and do not store your information in our database.{% endblocktrans %}

+
+ + {% comment %} +

{% trans "Add a new 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 %} +

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

+ {% blocktrans %}You are not making any payment here.{% endblocktrans %} +

+ {% 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 %} +
+
+
+ +
+
+
+ +
+

+
+ + {% endcomment %} + {% endif %} +
+
+
+
+
+ + {% comment %} + + {% if stripe_key %} + {% get_current_language as LANGUAGE_CODE %} + + {%endif%} + + {% if credit_card_data.last4 and credit_card_data.cc_brand %} + + {%endif%} + {% endcomment %} +{%endblock%} diff --git a/hosting/templates/hosting/signup_validate.html b/hosting/templates/hosting/signup_validate.html index 3ccbe15b..449064dc 100644 --- a/hosting/templates/hosting/signup_validate.html +++ b/hosting/templates/hosting/signup_validate.html @@ -1,6 +1,10 @@ {% extends "hosting/base_short.html" %} {% load staticfiles bootstrap3 i18n %} +{% block navbar %} + {% include 'hosting/includes/_navbar_transparent.html' %} +{% endblock navbar %} + {% block content %}
diff --git a/hosting/templates/hosting/user_keys.html b/hosting/templates/hosting/user_keys.html index fd93b66f..1cfb880c 100644 --- a/hosting/templates/hosting/user_keys.html +++ b/hosting/templates/hosting/user_keys.html @@ -3,7 +3,7 @@ {% block content %}
-

 {% trans "Your SSH Keys" %}

+

 {% trans "My SSH Keys" %}

{% if messages %}
{% for message in messages %} @@ -33,8 +33,7 @@
{% endif %}
+ An user requested a beta access
+

User {{email}} requested beta access

{% for vm in vms %} Type: {{vm.type}} - Amount: {{vm.amount}} @@ -119,7 +119,7 @@
-
Your data center light team
+
Your data center light team
diff --git a/datacenterlight/templates/datacenterlight/emails/user_activation.html b/datacenterlight/templates/datacenterlight/emails/user_activation.html index bf688127..955eed18 100644 --- a/datacenterlight/templates/datacenterlight/emails/user_activation.html +++ b/datacenterlight/templates/datacenterlight/emails/user_activation.html @@ -1,132 +1,14 @@ +{% extends "datacenterlight/emails/base_email_datacenterlight.html" %} {% load static from staticfiles %} {% load i18n %} - - - - - - -{{dcl_text}} - - - - - - - - - - - - - -
-
- - -
- -
- - - -
- logo - -
-
- -
-
-
-
- - - - - - - - - - -
- {{dcl_text}} {% trans 'account activation' %} -
-

- {% blocktrans %} - You can activate your {{dcl_text}} account by clicking here.

- You can also copy and paste the following link into the address bar of your browser and follow the link in order to activate your datacenterlight account.
- {{base_url}}{{activation_link}} - {% endblocktrans %} -

 
-
-
-
- - -
{% trans 'Your' %} {{dcl_text}} {% trans 'team' %}
-
-
-
- - - +{% block email_head %} +{{dcl_text}} {% trans 'Account Activation' %} +{% endblock %} +{% block email_body %} +{% blocktrans %} +You can activate your Data Center Light account by clicking here.
+You can also copy and paste the following link into the address bar of your browser
+to activate your Data Center Light account.
+{{base_url}}{{activation_link}} +{% endblocktrans %} +{% endblock %} diff --git a/datacenterlight/templates/datacenterlight/emails/user_activation.txt b/datacenterlight/templates/datacenterlight/emails/user_activation.txt index e40ef956..84ec50a9 100644 --- a/datacenterlight/templates/datacenterlight/emails/user_activation.txt +++ b/datacenterlight/templates/datacenterlight/emails/user_activation.txt @@ -1,11 +1,10 @@ -{% load static from staticfiles %} +{% extends "datacenterlight/emails/base_email_datacenterlight.txt" %} {% load i18n %} -{{dcl_text}} {% trans 'account activation' %} - -{% blocktrans %} -Hi, - -You can activate your {{dcl_text}} account by clicking here {{base_url}}{{activation_link}} +{% block email_head %}{{dcl_text}} {% trans 'Account Activation' %}{% endblock %} +{% block email_body %} +{% blocktrans %}You can activate your Data Center Light account by clicking here. +You can also copy and paste the following link into the address bar of your browser +to activate your Data Center Light account. +{{base_url}}{{activation_link}} {% endblocktrans %} - -{% trans 'Your' %} {{dcl_text}} {% trans 'team' %} +{% endblock %} diff --git a/datacenterlight/templates/datacenterlight/includes/_footer.html b/datacenterlight/templates/datacenterlight/includes/_footer.html index f77c3385..76c2c16e 100644 --- a/datacenterlight/templates/datacenterlight/includes/_footer.html +++ b/datacenterlight/templates/datacenterlight/includes/_footer.html @@ -2,52 +2,37 @@ {% get_current_language as LANGUAGE_CODE %} diff --git a/datacenterlight/templates/datacenterlight/includes/_navbar.html b/datacenterlight/templates/datacenterlight/includes/_navbar.html index 99f6a4a8..e2f1edc0 100644 --- a/datacenterlight/templates/datacenterlight/includes/_navbar.html +++ b/datacenterlight/templates/datacenterlight/includes/_navbar.html @@ -1,74 +1,71 @@ -{% load staticfiles i18n%} {% get_current_language as LANGUAGE_CODE %} {% load custom_tags %} +{% load staticfiles i18n%} +{% load custom_tags %} +{% get_current_language as LANGUAGE_CODE %} \ No newline at end of file + + + diff --git a/datacenterlight/templates/datacenterlight/index.html b/datacenterlight/templates/datacenterlight/index.html index 3ddb516d..cde420dd 100755 --- a/datacenterlight/templates/datacenterlight/index.html +++ b/datacenterlight/templates/datacenterlight/index.html @@ -1,5 +1,5 @@ {% extends "datacenterlight/base.html" %} -{% load staticfiles i18n%} +{% load staticfiles i18n %} {% block content %} @@ -16,10 +16,10 @@
@@ -149,32 +149,34 @@ - +
- -
-
+
+
+

{% trans "Contact us" %}

+
+
-

ungleich GmbH

+

ungleich GmbH

-

info@datacenterlight.ch

+

info@datacenterlight.ch

In der Au 7, Schwanden 8762

{% trans "Switzerland " %}

- +
+
-
-
-

{% trans "Questions?" %} {% trans "Contact us!" %}

+
+
+ {% include "datacenterlight/contact_form.html" %}
diff --git a/datacenterlight/templates/datacenterlight/landing_payment.html b/datacenterlight/templates/datacenterlight/landing_payment.html new file mode 100644 index 00000000..f4974a56 --- /dev/null +++ b/datacenterlight/templates/datacenterlight/landing_payment.html @@ -0,0 +1,217 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3 i18n %} + +{% 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" %} +
+
+
+
+
+
+
+
+

{%trans "Billing Address"%}

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

{%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 %} +

+
+
+
+ {% 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 %} +
+
+
+
+ +
+
+
+ + {% else %} +
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ + +
+
+
+ +
+
+ {% 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 %} +
+
+
+
+ +
+
+
+ + +
+ + {% endif %} +
+
+
+
+
+
+
+ + +{% if stripe_key %} +{% get_current_language as LANGUAGE_CODE %} + +{%endif%} + +{% if credit_card_data.last4 and credit_card_data.cc_brand %} + +{%endif%} + +{%endblock%} diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 7a882236..085b0c37 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -2,6 +2,11 @@ {% load staticfiles bootstrap3 %} {% load i18n %} {% load custom_tags %} + +{% block navbar %} + {% include "datacenterlight/includes/_navbar.html" %} +{% endblock navbar %} + {% block content %}
@@ -45,7 +50,7 @@
{% trans "Payment Method:"%}
- {{cc_brand}} {% trans "ending" %} **** {{cc_last4}}
+ {{cc_brand}} {% trans "ending in" %} **** {{cc_last4}}
{{request.session.user.email}}
@@ -72,13 +77,15 @@

- {% 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 %}.

-
- + {% 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 %}.

+
+ +
diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index 602fb403..c34c56ba 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -6,10 +6,8 @@ import stripe from celery.result import AsyncResult from django.conf import settings from django.core.management import call_command -# Create your tests here. from django.test import TestCase, override_settings from model_mommy import mommy - from datacenterlight.models import VMTemplate from datacenterlight.tasks import create_vm_task from membership.models import StripeCustomer @@ -117,8 +115,8 @@ class CeleryTaskTestCase(TestCase): 'response_object').stripe_plan_id}]) stripe_subscription_obj = subscription_result.get('response_object') # Check if the subscription was approved and is active - if stripe_subscription_obj is None or \ - stripe_subscription_obj.status != 'active': + if stripe_subscription_obj is None \ + or stripe_subscription_obj.status != 'active': msg = subscription_result.get('error') raise Exception("Creating subscription failed: {}".format(msg)) diff --git a/datacenterlight/urls.py b/datacenterlight/urls.py index a3aed7a6..772e691d 100644 --- a/datacenterlight/urls.py +++ b/datacenterlight/urls.py @@ -1,17 +1,25 @@ from django.conf.urls import url -from .views import IndexView, BetaProgramView, LandingProgramView, BetaAccessView, PricingView, SuccessView, \ - PaymentOrderView, OrderConfirmationView, WhyDataCenterLightView - +from .views import IndexView, BetaProgramView, LandingProgramView, \ + BetaAccessView, PricingView, SuccessView, \ + PaymentOrderView, OrderConfirmationView, \ + WhyDataCenterLightView, ContactUsView urlpatterns = [ url(r'^$', IndexView.as_view(), name='index'), - url(r'^whydatacenterlight/?$', WhyDataCenterLightView.as_view(), name='whydatacenterlight'), + url(r'^t/$', IndexView.as_view(), name='index_t'), + url(r'^g/$', IndexView.as_view(), name='index_g'), + url(r'^f/$', IndexView.as_view(), name='index_f'), + url(r'^l/$', IndexView.as_view(), name='index_l'), + url(r'^whydatacenterlight/?$', WhyDataCenterLightView.as_view(), + 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'), + url(r'^order-confirmation/?$', OrderConfirmationView.as_view(), + name='order_confirmation'), url(r'^order-success/?$', SuccessView.as_view(), name='order_success'), url(r'^beta_access?$', BetaAccessView.as_view(), name='beta_access'), + url(r'^contact/?$', ContactUsView.as_view(), name='contact_us'), ] diff --git a/datacenterlight/views.py b/datacenterlight/views.py index fd1435a1..0521ffef 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -1,26 +1,67 @@ -from django.views.generic import FormView, CreateView, TemplateView, DetailView -from django.http import HttpResponseRedirect -from .forms import BetaAccessForm -from .models import BetaAccess, BetaAccessVMType, BetaAccessVM, VMTemplate -from django.contrib import messages -from django.core.urlresolvers import reverse -from utils.mailer import BaseEmail -from django.shortcuts import render -from django.shortcuts import redirect from django import forms -from django.core.exceptions import ValidationError -from django.views.decorators.cache import cache_control from django.conf import settings +from django.contrib import messages +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect +from django.shortcuts import redirect +from django.shortcuts import render from django.utils.translation import ugettext_lazy as _ -from utils.forms import BillingAddressForm -from utils.models import BillingAddress +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 utils.stripe_utils import StripeUtils from membership.models import CustomUser, StripeCustomer from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VirtualMachineTemplateSerializer, \ VMTemplateSerializer -from datacenterlight.tasks import create_vm_task +from utils.forms import BillingAddressForm +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 + + +class ContactUsView(FormView): + template_name = "datacenterlight/contact_form.html" + form_class = ContactForm + + def get(self, request, *args, **kwargs): + return HttpResponseRedirect(reverse('datacenterlight:index')) + + def form_invalid(self, form): + if self.request.is_ajax(): + return self.render_to_response( + self.get_context_data(contact_form=form)) + else: + return render(self.request, + 'datacenterlight/index.html', + self.get_context_data(contact_form=form)) + + def form_valid(self, form): + form.save() + email_data = { + 'subject': "{dcl_text} Message from {sender}".format( + dcl_text=settings.DCL_TEXT, + sender=form.cleaned_data.get('email') + ), + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': ['info@ungleich.ch'], + 'body': "\n".join( + ["%s=%s" % (k, v) for (k, v) in form.cleaned_data.items()]), + 'reply_to': [form.cleaned_data.get('email')], + } + send_plain_email_task.delay(email_data) + if self.request.is_ajax(): + return self.render_to_response( + self.get_context_data(success=True, contact_form=form)) + else: + return render(self.request, + 'datacenterlight/index.html', + self.get_context_data(success=True, + contact_form=form)) class LandingProgramView(TemplateView): @@ -315,7 +356,8 @@ class IndexView(CreateView): context = super(IndexView, self).get_context_data(**kwargs) context.update({ 'base_url': "{0}://{1}".format(self.request.scheme, - self.request.get_host()) + self.request.get_host()), + 'contact_form': ContactForm }) return context @@ -364,7 +406,7 @@ class WhyDataCenterLightView(IndexView): class PaymentOrderView(FormView): - template_name = 'hosting/payment.html' + template_name = 'datacenterlight/landing_payment.html' form_class = BillingAddressForm def get_form_kwargs(self): @@ -419,7 +461,8 @@ class PaymentOrderView(FormView): token=token) if not customer: form.add_error("__all__", "Invalid credit card") - return self.render_to_response(self.get_context_data(form=form)) + return self.render_to_response( + self.get_context_data(form=form)) # Create Billing Address billing_address = form.save() @@ -435,7 +478,7 @@ class PaymentOrderView(FormView): class OrderConfirmationView(DetailView): template_name = "datacenterlight/order_detail.html" - payment_template_name = 'hosting/payment.html' + payment_template_name = 'datacenterlight/landing_payment.html' context_object_name = "order" model = HostingOrder @@ -511,7 +554,7 @@ class OrderConfirmationView(DetailView): stripe_subscription_obj = subscription_result.get('response_object') # Check if the subscription was approved and is active if stripe_subscription_obj is None or \ - stripe_subscription_obj.status != 'active': + stripe_subscription_obj.status != 'active': msg = subscription_result.get('error') messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index f3d00e47..dc2917ea 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -173,7 +173,8 @@ TEMPLATES = [ os.path.join(PROJECT_DIR, 'nosystemd/templates/'), os.path.join(PROJECT_DIR, 'ungleich/templates/djangocms_blog/'), - os.path.join(PROJECT_DIR, 'ungleich/templates/cms/ungleichch'), + os.path.join(PROJECT_DIR, + 'ungleich/templates/cms/ungleichch'), os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich'), os.path.join(PROJECT_DIR, 'ungleich_page/templates/ungleich_page'), @@ -213,6 +214,8 @@ CMS_TEMPLATES = ( # ungleich ('blog_ungleich.html', gettext('Blog')), ('page.html', gettext('Page')), + # dcl + ('datacenterlight/cms_page.html', gettext('Data Center Light')), ) DATABASES = { @@ -557,9 +560,21 @@ CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND') CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' -CELERY_TIMEZONE = 'Europe/Zurich' +# CELERY_TIMEZONE = 'Europe/Zurich' CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5) +DCL_ERROR_EMAILS_TO = env('DCL_ERROR_EMAILS_TO') + +DCL_ERROR_EMAILS_TO_LIST = [] +if DCL_ERROR_EMAILS_TO is not None: + DCL_ERROR_EMAILS_TO_LIST = [x.strip() for x in + DCL_ERROR_EMAILS_TO.split( + ',')] \ + if "," in DCL_ERROR_EMAILS_TO else [DCL_ERROR_EMAILS_TO.strip()] + +if 'info@ungleich.ch' not in DCL_ERROR_EMAILS_TO_LIST: + DCL_ERROR_EMAILS_TO_LIST.append('info@ungleich.ch') + ENABLE_DEBUG_LOGGING = bool_env('ENABLE_DEBUG_LOGGING') if ENABLE_DEBUG_LOGGING: @@ -583,6 +598,9 @@ if ENABLE_DEBUG_LOGGING: }, } +TEST_MANAGE_SSH_KEY_PUBKEY = env('TEST_MANAGE_SSH_KEY_PUBKEY') +TEST_MANAGE_SSH_KEY_HOST = env('TEST_MANAGE_SSH_KEY_HOST') + DEBUG = bool_env('DEBUG') if DEBUG: diff --git a/dynamicweb/settings/prod.py b/dynamicweb/settings/prod.py index a3d6bbeb..e8cfc64c 100644 --- a/dynamicweb/settings/prod.py +++ b/dynamicweb/settings/prod.py @@ -16,7 +16,7 @@ CACHES = { # MANAGERS = ADMINS -REGISTRATION_MESSAGE['message'] = REGISTRATION_MESSAGE['message'].format(host='digitalglarus.ungleich.ch', +REGISTRATION_MESSAGE['message'] = REGISTRATION_MESSAGE['message'].format(host='digitalglarus.ch', slug='{slug}') # flake8: noqa ALLOWED_HOSTS = [ diff --git a/dynamicweb/urls.py b/dynamicweb/urls.py index b5cccff3..699ce50b 100644 --- a/dynamicweb/urls.py +++ b/dynamicweb/urls.py @@ -12,42 +12,59 @@ from django.views.generic import RedirectView from django.core.urlresolvers import reverse_lazy import debug_toolbar -urlpatterns = [url(r'^index.html$', LandingView.as_view()), - url(r'^hosting/', include('hosting.urls', namespace="hosting")), - url(r'^open_api/', include('opennebula_api.urls', - namespace='opennebula_api')), - url(r'^railshosting/', RailsHostingView.as_view(), name="rails.hosting"), - url(r'^nodehosting/', NodeJSHostingView.as_view(), name="node.hosting"), - url(r'^djangohosting/', DjangoHostingView.as_view(), name="django.hosting"), - url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")), - url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')), - url(r'^jsi18n/(?P\S+?)/$', - i18n.javascript_catalog), - ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +urlpatterns = [ + url(r'^index.html$', LandingView.as_view()), + url(r'^open_api/', include('opennebula_api.urls', + namespace='opennebula_api')), + url(r'^railshosting/', RailsHostingView.as_view(), + name="rails.hosting"), + url(r'^nodehosting/', NodeJSHostingView.as_view(), + name="node.hosting"), + url(r'^djangohosting/', DjangoHostingView.as_view(), + name="django.hosting"), + url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")), + url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')), + url(r'^jsi18n/(?P\S+?)/$', + i18n.javascript_catalog), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +urlpatterns += i18n_patterns( + url(r'^hosting/', include('hosting.urls', namespace="hosting")), +) # note the django CMS URLs included via i18n_patterns urlpatterns += i18n_patterns( - url(r'^$', LandingView.as_view()), - url(r'^admin/', include(admin.site.urls)), - url(r'^datacenterlight/', include('datacenterlight.urls', namespace="datacenterlight")), - url(r'^hosting/', RedirectView.as_view( - url=reverse_lazy('hosting:login')), name='redirect_hosting_login'), - url(r'^alplora/', include('alplora.urls', namespace="alplora")), - url(r'^membership/', include(membership_urls)), - url(r'^digitalglarus/', include('digitalglarus.urls', - namespace="digitalglarus")), - # url(r'^blog/', include('ungleich.urls', namespace='ungleich')), - url(r'^', - include('ungleich_page.urls', namespace='ungleich_page'), - name='ungleich_page'), - url(r'^blog/', include('ungleich.urls', namespace='ungleich')), - url(r'^', include('cms.urls')) - ) + url(r'^$', LandingView.as_view()), + url(r'^admin/', include(admin.site.urls)), + url(r'^datacenterlight/', + include('datacenterlight.urls', namespace="datacenterlight")), + url(r'^hosting/', RedirectView.as_view( + url=reverse_lazy('hosting:login')), name='redirect_hosting_login'), + url(r'^alplora/', include('alplora.urls', namespace="alplora")), + url(r'^membership/', include(membership_urls)), + url(r'^digitalglarus/', include('digitalglarus.urls', + namespace="digitalglarus")), + # url(r'^blog/', include('ungleich.urls', namespace='ungleich')), + url(r'^', + include('ungleich_page.urls', + namespace='ungleich_page'), + name='ungleich_page'), + url(r'^cms/blog/', + include('ungleich.urls', namespace='ungleich')), + url( + r'^blog/(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/(?P\w[-\w]*)/$', + RedirectView.as_view(pattern_name='ungleich:post-detail')), + url(r'^blog/|cms/$', RedirectView.as_view( + url=reverse_lazy('ungleich:post-list')), name='blog_list_view'), + url(r'^cms/', include('cms.urls')), +) + urlpatterns += [ - url(r'^media/(?P.*)$', - static_view.serve, { - 'document_root': settings.MEDIA_ROOT, - }), - ] + url(r'^media/(?P.*)$', + static_view.serve, { + 'document_root': settings.MEDIA_ROOT, + }), +] + if settings.DEBUG: urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls))] diff --git a/hosting/forms.py b/hosting/forms.py index 288a8caf..056d0004 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -1,16 +1,22 @@ import datetime +import logging +import subprocess +import tempfile from django import forms -from membership.models import CustomUser from django.contrib.auth import authenticate - from django.utils.translation import ugettext_lazy as _ +from membership.models import CustomUser +from utils.hosting_utils import get_all_public_keys from .models import UserHostingKey +logger = logging.getLogger(__name__) + def generate_ssh_key_name(): - return 'dcl-generated-key-' + datetime.datetime.now().strftime('%m%d%y%H%M') + return 'dcl-generated-key-' + datetime.datetime.now().strftime( + '%m%d%y%H%M') class HostingUserLoginForm(forms.Form): @@ -38,9 +44,7 @@ class HostingUserLoginForm(forms.Form): CustomUser.objects.get(email=email) return email except CustomUser.DoesNotExist: - raise forms.ValidationError("User does not exist") - else: - return email + raise forms.ValidationError(_("User does not exist")) class HostingUserSignupForm(forms.ModelForm): @@ -51,7 +55,8 @@ class HostingUserSignupForm(forms.ModelForm): model = CustomUser fields = ['name', 'email', 'password'] widgets = { - 'name': forms.TextInput(attrs={'placeholder': 'Enter your name or company name'}), + 'name': forms.TextInput( + attrs={'placeholder': 'Enter your name or company name'}), } def clean_confirm_password(self): @@ -65,19 +70,55 @@ class HostingUserSignupForm(forms.ModelForm): class UserHostingKeyForm(forms.ModelForm): private_key = forms.CharField(widget=forms.HiddenInput(), required=False) public_key = forms.CharField(widget=forms.Textarea( - attrs={'class': 'form_public_key', 'placeholder': _('Paste here your public key')}), + attrs={'class': 'form_public_key', + 'placeholder': _('Paste here your public key')}), required=False, ) user = forms.models.ModelChoiceField(queryset=CustomUser.objects.all(), - required=False, widget=forms.HiddenInput()) + required=False, + widget=forms.HiddenInput()) name = forms.CharField(required=False, widget=forms.TextInput( - attrs={'class': 'form_key_name', 'placeholder': _('Give a name to your key')})) + attrs={'class': 'form_key_name', + 'placeholder': _('Give a name to your key')})) def __init__(self, *args, **kwargs): self.request = kwargs.pop("request") super(UserHostingKeyForm, self).__init__(*args, **kwargs) self.fields['name'].label = _('Key name') + def clean_public_key(self): + """ + Validates a public ssh key using `ssh-keygen -lf key.pub` + Also checks if a given key already exists in the database and + alerts the user of it. + :return: + """ + if 'generate' in self.request.POST: + return self.data.get('public_key') + KEY_ERROR_MESSAGE = _("Please input a proper SSH key") + openssh_pubkey_str = self.data.get('public_key').strip() + + if openssh_pubkey_str in get_all_public_keys(self.request.user): + key_name = UserHostingKey.objects.filter( + user_id=self.request.user.id, + public_key=openssh_pubkey_str).first().name + KEY_EXISTS_MESSAGE = _( + "This key exists already with the name \"%(name)s\"") % { + 'name': key_name} + raise forms.ValidationError(KEY_EXISTS_MESSAGE) + + with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file: + tmp_public_key_file.write(openssh_pubkey_str.encode('utf-8')) + tmp_public_key_file.flush() + try: + subprocess.check_output( + ['ssh-keygen', '-lf', tmp_public_key_file.name]) + except subprocess.CalledProcessError as cpe: + logger.debug( + "Not a correct ssh format {error}".format(error=str(cpe))) + raise forms.ValidationError(KEY_ERROR_MESSAGE) + return openssh_pubkey_str + def clean_name(self): return self.data.get('name') diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 2887095f..0cf50fd7 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-08-24 11:12+0000\n" +"POT-Creation-Date: 2017-09-24 12:34+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -24,29 +24,24 @@ msgstr "Dein Benutzername und/oder Dein Passwort ist falsch." msgid "Your account is not activated yet." msgstr "Dein Account wurde noch nicht aktiviert." +msgid "User does not exist" +msgstr "Der Benutzer existiert nicht" + msgid "Paste here your public key" -msgstr "Füge deinen Public Key ein" +msgstr "Füge Deinen Public Key ein" msgid "Give a name to your key" -msgstr "Gebe deinem SSH-Key einen Name" +msgstr "Gebe Deinem SSH-Key einen Name" msgid "Key name" msgstr "Key-Name" -msgid "My Virtual Machines" -msgstr "Meine virtuellen Maschinen" +msgid "Please input a proper SSH key" +msgstr "Bitte verwende einen gültigen SSH-Key" -msgid "My Orders" -msgstr "Meine Bestellungen" - -msgid "SSH Keys" -msgstr "SSH Key" - -msgid "Notifications " -msgstr "Benachrichtigungen" - -msgid "Logout" -msgstr "Abmelden" +#, python-format +msgid "This key exists already with the name \"%(name)s\"" +msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits" msgid "All Rights Reserved" msgstr "Alle Rechte vorbehalten" @@ -73,7 +68,7 @@ msgid "Mwst-Nummer: CHE-109.549.333 MWST" msgstr "" msgid "Total:" -msgstr "" +msgstr "Gesamt:" #, python-format msgid "Alles Preise in CHF mit 8%% Mehrwertsteuer." @@ -115,6 +110,27 @@ msgstr "vorherige" msgid "next" msgstr "nächste" +msgid "Month" +msgstr "Monat" + +msgid "VAT included" +msgstr "MwSt. inklusive" + +msgid "Please enter a value in range 1 - 48." +msgstr "Bitte gib einen Wert von 1 bis 48 ein." + +msgid "Please enter a value in range 2 - 200." +msgstr "Bitte gib einen Wert von 2 bis 200 ein." + +msgid "Please enter a value in range 10 - 2000." +msgstr "Bitte gib einen Wert von 10 bis 200 ein." + +msgid "GB Storage (SSD)" +msgstr "GB Storage (SSD)" + +msgid "Continue" +msgstr "Weiter" + msgid "SSH Key" msgstr "SSH Key" @@ -140,10 +156,10 @@ msgid "Upload" msgstr "Hochladen" msgid "Your VM hosted in Switzerland" -msgstr "deine VM in der Schweiz" +msgstr "Deine VM in der Schweiz" msgid "Set your new password" -msgstr "Setze dein neues Passwort" +msgstr "Setze Dein neues Passwort" msgid "Reset" msgstr "Zurücksetzen" @@ -154,39 +170,132 @@ msgstr "Hast Du bereits ein Benutzerkonto?" msgid "Login" msgstr "Anmelden" -msgid "New Virtual Machine" -msgstr "Neue virtuelle Maschine" +msgid "Create VM" +msgstr "VM erstellen" -msgid "Step 1. Select VM Template:" -msgstr "Wähle eine Vorlage" +msgid "My Dashboard" +msgstr "Mein Dashboard" -msgid "Step2. Select VM Configuration" -msgstr "Wähle eine Konfiguration" +msgid "My VMs" +msgstr "Meine VMs" -msgid "Price " -msgstr "Preis" +msgid "My SSH Keys" +msgstr "Meine SSH-Keys" -msgid "CHF/Month" -msgstr "CHF/Monat" +msgid "My Bills" +msgstr "Meine Rechnungen" -msgid "Start VM" -msgstr "VM jetzt starten" +msgid "My Settings" +msgstr "Meine Einstellungen" + +msgid "Support / Contact" +msgstr "Support / Kontakt" + +#, python-format +msgid "" +"You have ordered a new virtual machine!\n" +"
\n" +"Your order of [%(vm_name)s] has been charged.

\n" +"You can view your invoice by clicking the button below.

\n" +msgstr "" +"Du hast eine neue virtuelle Maschine bestellt!
\n" +"Deine Bestellung von [%(vm_name)s] wurde erhoben.

\n" +"Um die Rechnung zu sehen, klicke auf den Button unten.

\n" + +msgid "View Invoice" +msgstr "Zur Rechnung" + +#, python-format +msgid "" +"You have ordered a new virtual machine!\n" +"Your order of [%(vm_name)s] has been charged.\n" +"You can view your invoice here.\n" +msgstr "" +"Du hast eine neue virtuelle Maschine bestellt!\n" +"Deine Bestellung von [%(vm_name)s] wurde erhoben.\n" +"Um die Rechnung zu sehen, klicke hier.\n" + +msgid "Password Reset" +msgstr "Passwort zurücksetzen" + +#, python-format +msgid "" +"\n" +"You're receiving this email because you requested a password reset for your " +"user account at %(site_name)s.
\n" +"Please go to the following page and choose a new password: %(base_url)s" +"%(password_reset_url)s
\n" +"If you didn't request a new password, ignore this e-mail.
\n" +"Thank you!\n" +msgstr "" +"\n" +"Du erhälst diese E-Mail da Du Dein Passwort für Deinen Account bei " +"%(site_name)s zurücksetzen möchtest.
\n" +"Bitte folge diesem Link und wähle ein neues Passwort: %(base_url)s" +"%(password_reset_url)s Solltest Du kein neues Passwort angefordert haben, " +"dann ignoriere diese E-Mail.
\n" +"Dankeschön!\n" #, python-format msgid "" "You're receiving this email because you requested a password reset for your " -"user account at %(site_name)s." +"user account at %(site_name)s.\n" +"Please go to the following page and choose a new password: %(base_url)s" +"%(password_reset_url)s\n" +"If you didn't request a new password, ignore this e-mail.\n" +"Thank you!\n" msgstr "" +"Du erhälst diese E-Mail da Du Dein Passwort für Deinen Account bei " +"%(site_name)s zurücksetzen möchtest.\n" +"Bitte folge diesem Link und wähle ein neues Passwort: %(base_url)s" +"%(password_reset_url)s Solltest Du kein neues Passwort angefordert haben, " +"dann ignoriere diese E-Mail.\n" +"Dankeschön!\n" -msgid "Please go to the following page and choose a new password:" +#, python-format +msgid "" +"You're receiving this mail because your virtual machine [%(vm_name)s] has " +"been cancelled.
\n" +"You can see your order status by clicking [my VM page] below.
\n" +"If you want to order a new virtual machine, you can do it by clicking this link.
\n" msgstr "" +"Du erhälst diese E-Mail, Da Deine virtuelle Maschine [%(vm_name)s] gekündigt " +"wurde.
\n" +"Um Deinen Auftragsstatus zu sehen, klicke auf die [my VM page] unten.
\n" +"Falls Du eine neue virtuelle Maschine bestellen möchtest, kannst Du dies " +"tun, indem Du diesen " +"Link klickst.
\n" -msgid "Thanks for using our site!" +msgid "My VM page" msgstr "" #, python-format -msgid "The %(site_name)s team" +msgid "" +"You're receiving this mail because your virtual machine [%(vm_name)s] has " +"been cancelled.\n" +"You can see your order status by clicking here\n" +"%(base_url)s%(vm_order_url)s\n" +"If you want to order a new virtual machine, you can do it by clicking this " +"link.\n" +"%(base_url)s%(my_virtual_machines_url)s\n" msgstr "" +"Du erhälst diese E-Mail, da Deine virtuelle Maschine [%(vm_name)s] gekündigt " +"wurde.\n" +"Um Deinen Auftragsstatus zu sehen, klicke hier.\n" +"%(base_url)s%(vm_order_url)s\n" +"Falls Du eine neue virtuelle Maschine bestellen möchtest, kannst Du dies " +"tun, indem Du diesen Link klickst.\n" +"%(base_url)s%(my_virtual_machines_url)s\n" + +msgid "Toggle navigation" +msgstr "Umschalten" + +msgid "Dashboard" +msgstr "Dashboard" + +msgid "Logout" +msgstr "Abmelden" msgid "Don't have an account yet ? " msgstr "Besitzt du kein Benutzerkonto?" @@ -197,6 +306,9 @@ msgstr "Registrieren" msgid "Forgot your password ? " msgstr "Passwort vergessen?" +msgid "Resend activation link" +msgstr "Aktivierungslink noch einmal senden" + msgid "Notifications" msgstr "Benachrichtigungen" @@ -215,21 +327,37 @@ msgstr "Als gelesen markieren" msgid "All notifications" msgstr "Alle Benachrichtigungen" -msgid "Date" -msgstr "Datum" - -msgid "Status:" +#, python-format +msgid "%(page_header_text)s" msgstr "" -msgid "Billed To:" +msgid "Invoice Date" +msgstr "Rechnung Datum" + +msgid "Status" +msgstr "" + +msgid "Approved" +msgstr "Akzeptiert" + +msgid "Declined" +msgstr "Abgelehnt" + +msgid "Billed to" msgstr "Rechnungsadresse" -msgid "Payment Method:" +msgid "Payment method" msgstr "Bezahlmethode" +msgid "ending in" +msgstr "endend in" + msgid "Order summary" msgstr "Bestellungsübersicht" +msgid "Product" +msgstr "Produkt" + msgid "Cores" msgstr "Prozessorkerne" @@ -242,23 +370,40 @@ msgstr "Festplattenkapazität" msgid "Total" msgstr "Gesamt" -msgid "Finish Configuration" -msgstr "Konfiguration beenden" +#, python-format +msgid "" +"By clicking \"Place order\" this plan will charge your credit card account " +"with the fee of %(vm_price)sCHF/month" +msgstr "" +"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF " +"pro Monat belastet" + +msgid "Place order" +msgstr "Bestellen" + +msgid "BACK TO LIST" +msgstr "ZURÜCK ZUR LISTE" + +msgid "Processing..." +msgstr "Abarbeitung..." + +msgid "Hold tight, we are processing your request" +msgstr "Bitte warten - wir bearbeiten Deine Anfrage gerade" + +msgid "Some problem encountered. Please try again later." +msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal." msgid "Order Nr." msgstr "Bestellung Nr." +msgid "Date" +msgstr "Datum" + msgid "Amount" msgstr "Betrag" -msgid "Status" -msgstr "" - msgid "See Invoice" -msgstr "Rechnung" - -msgid "View Detail" -msgstr "Details anzeigen" +msgstr "Siehe Rechnung" msgid "Page" msgstr "" @@ -275,9 +420,6 @@ msgstr "Konfiguration" msgid "including VAT" msgstr "inkl. Mehrwertsteuer" -msgid "Month" -msgstr "Monat" - msgid "Billing Address" msgstr "Rechnungsadresse" @@ -302,9 +444,8 @@ msgid "" "You are not making any payment yet. After submitting your card information, " "you will be taken to the Confirm Order Page." msgstr "" -"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst " -"ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt " -"hast." +"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, " +"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast." msgid "Submit" msgstr "Absenden" @@ -330,6 +471,25 @@ msgstr "Deine Kreditkartennummer" msgid "Reset your password" msgstr "Passwort zurücksetzen" +msgid "UPDATE" +msgstr "" + +msgid "Last" +msgstr "" + +msgid "Type" +msgstr "Kartentyp" + +msgid "No Credit Cards Added" +msgstr "Es wurde keine Kreditkarte hinzugefügt" + +msgid "" +"We are using Stripe for payment and do " +"not store your information in our database." +msgstr "" +"Wir nutzen Stripe für " +"die Bezahlung und speichern keine Informationen in unserer Datenbank." + msgid "Add your public SSH key" msgstr "Füge deinen öffentlichen SSH-Key hinzu" @@ -345,16 +505,11 @@ msgstr "Erstelle dein neues Keypaar" msgid "Warning!" msgstr "Achtung!" -#, fuzzy -#| msgid "You can download your SSH private key once. Don't lost your key" msgid "You can download your SSH private key once. Don't loose your key" msgstr "" "Du kannst deinen privaten SSH Schlüssel nur einmal herunterladen. Beware ihn " "sicher auf." -msgid "Your SSH Keys" -msgstr "Deine SSH Keys" - msgid "" "To generate a new key pair or to upload your existing key, click 'Add Key'" msgstr "" @@ -382,8 +537,6 @@ msgstr "Möchtest Du den Schlüssel löschen?" msgid "Show" msgstr "Anzeigen" -#, fuzzy -#| msgid "Public SSH Key" msgid "Public SSH Key" msgstr "Public SSH Key" @@ -411,6 +564,9 @@ msgstr "Aktueller Preis" msgid "Your VM is" msgstr "Deine VM ist" +msgid "Terminating" +msgstr "Beenden" + msgid "Pending" msgstr "In Vorbereitung" @@ -423,8 +579,10 @@ msgstr "Fehlgeschlagen" msgid "Terminate VM" msgstr "VM Beenden" -msgid "Support / Contact" -msgstr "Support / Kontakt" +msgid "Sorry, there was an unexpected error. Kindly retry." +msgstr "" +"Bitte entschuldige, es scheint ein unerwarteter Fehler aufgetreten zu sein. " +"Versuche es doch bitte noch einmal." msgid "Something doesn't work?" msgstr "Etwas funktioniert nicht?" @@ -433,10 +591,7 @@ msgid "We are here to help you!" msgstr "Wir sind hier, um Dir zu helfen!" msgid "CONTACT" -msgstr "KONTACT" - -msgid "BACK TO LIST" -msgstr "ZURÜCK ZUR LISTE" +msgstr "KONTAKT" msgid "Terminate your Virtual Machine" msgstr "Deine Virtuelle Maschine beenden" @@ -447,15 +602,26 @@ msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst" msgid "OK" msgstr "" +#, python-format +msgid "" +"Your Virtual Machine %(machine_name)s is successfully " +"terminated!" +msgstr "" +"Deine Virtuelle Machine (VM) %(machine_name)s wurde erfolgreich " +"beendet!" + msgid "Virtual Machines" msgstr "Virtuelle Maschinen" msgid "To create a new virtual machine, click \"Create VM\"" -msgstr "" +msgstr "Um eine neue VM zu erzeugen, klicke \"Neue VM erzeugen\"" msgid "CREATE VM" msgstr "NEUE VM" +msgid "View Detail" +msgstr "Details anzeigen" + msgid "login" msgstr "Einloggen" @@ -483,57 +649,129 @@ msgstr "Du kannst dich nun" msgid "Sorry. Your request is invalid." msgstr "Entschuldigung, deine Anfrage ist ungültig." +msgid "Password has been reset." +msgstr "Dein Passwort wurde erfolgreich zurückgesetzt." + +msgid "Password reset has not been successful." +msgstr "Dein Passwort konnte nicht zurückgesetzt werden." + +msgid "The reset password link is no longer valid." +msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig." + msgid "Invalid credit card" msgstr "Ungültige Kreditkarte" msgid "Confirm Order" msgstr "Bestellung Bestätigen" +msgid "" +"The VM you are looking for is unavailable at the moment. Please contact Data " +"Center Light support." +msgstr "Kontaktiere den Data Center Light Support." + +msgid "In order to create a VM, you need to create/upload your SSH KEY first." +msgstr "" +"Um eine VM zu erstellen musst du zuerst einen SSH-Key erstellen / hochladen." + +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 "Invalid number of cores" +msgstr "Ungültige Anzahle CPU-Kerne" + +msgid "Invalid RAM size" +msgstr "Ungültige RAM-Grösse" + +msgid "Invalid storage size" +msgstr "Ungültige Speicher-Grösse" + msgid "" "We could not find the requested VM. Please " "contact Data Center Light Support." -msgstr "" +msgstr "Kontaktiere den Data Center Light Support." -#~ msgid "" -#~ "\n" -#~ " You are not making any " -#~ "payment yet. After submitting your card\n" -#~ " information, you will be " -#~ "taken to the Confirm Order Page.\n" -#~ " " -#~ msgstr "" -#~ "\n" -#~ "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner " -#~ "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " -#~ "weitergeleitet." +msgid "Terminated" +msgstr "Beendet" -#~ msgid "Approved" -#~ msgstr "Akzeptiert" +msgid "Error terminating VM" +msgstr "Fehler beenden VM" -#~ msgid "Declined" -#~ msgstr "Abgelehnt" +msgid "Virtual Machine Cancellation" +msgstr "VM Kündigung" + +#~ msgid "Close" +#~ msgstr "Schliessen" + +#~ msgid "VM %(VM_ID)s terminated successfully" +#~ msgstr "VM %(VM_ID)s erfolgreich beendet" + +#~ msgid "days" +#~ msgstr "Tage" + +#~ msgid "New Virtual Machine" +#~ msgstr "Neue virtuelle Maschine" + +#~ msgid "Step 1. Select VM Template:" +#~ msgstr "Wähle eine Vorlage" + +#~ msgid "Step2. Select VM Configuration" +#~ msgstr "Wähle eine Konfiguration" + +#~ msgid "Price " +#~ msgstr "Preis" + +#~ msgid "CHF/Month" +#~ msgstr "CHF/Monat" + +#~ msgid "Start VM" +#~ msgstr "VM jetzt starten" + +#~ msgid "Finish Configuration" +#~ msgstr "Konfiguration beenden" + +#~ msgid "Your New VM %(vm_name)s at Data Center Light" +#~ msgstr "Deine neue VM %(vm_name)s bei Data Center Light" + +#~ msgid "My Virtual Machines" +#~ msgstr "Meine virtuellen Maschinen" + +#~ msgid "My Orders" +#~ msgstr "Meine Bestellungen" + +#~ msgid "SSH Keys" +#~ msgstr "SSH Keys" + +#~ msgid "Notifications " +#~ msgstr "Benachrichtigungen" + +#~ msgid "REMOVE CARD" +#~ msgstr "KARTE ENTFERNEN" + +#~ msgid "EDIT CARD" +#~ msgstr "BEARBEITEN" + +#~ msgid "Add a new Card." +#~ msgstr "Neue Kreditkarte hinzufügen." + +#~ msgid "You are not making any payment here." +#~ msgstr "Es wird noch keine Bezahlung vorgenommen" + +#~ msgid "Your SSH Keys" +#~ msgstr "Deine SSH Keys" #~ msgid "Cancel Order" #~ msgstr "Bestellung stornieren" -#, fuzzy -#~| msgid "Do You want to delete your order?" #~ msgid "Do you want to delete your order?" #~ msgstr "Willst du deine Bestellung löschen?" -#~ msgid "" -#~ "\n" -#~ " You are not making any payment " -#~ "yet. After submitting your card\n" -#~ " information, you will be taken to " -#~ "the Confirm Order Page.\n" -#~ " " -#~ msgstr "" -#~ "\n" -#~ "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner " -#~ "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " -#~ "weitergeleitet." - #~ msgid "Ip not assigned yet" #~ msgstr "Ip nicht zugewiesen" @@ -549,9 +787,6 @@ msgstr "" #~ msgid "Ipv6" #~ msgstr "IPv6" -#~ msgid "Close" -#~ msgstr "Schliessen" - #~ msgid "Cancel" #~ msgstr "Beenden" @@ -559,89 +794,19 @@ msgstr "" #~ msgstr "Hinzufügen" #~ msgid "Keys" -#~ msgstr "Schlüssel" - -#, fuzzy -#~| msgid "Contact" -#~ msgid "Content" -#~ msgstr "Kontakt" - -#, fuzzy -#~| msgid "Contact" -#~ msgid "DG.Contact" -#~ msgstr "Kontakt" - -#, fuzzy -#~| msgid "Home" -#~ msgid "DG.Home" -#~ msgstr "Home" - -#, fuzzy -#~| msgid "Amount" -#~ msgid "Country" -#~ msgstr "Betrag" +#~ msgstr "Keys" #~ msgid "Log in" #~ msgstr "Anmelden" -#, fuzzy -#~| msgid "Configuration" -#~ msgid "Donation #" -#~ msgstr "Konfiguration" - -#, fuzzy -#~| msgid "Billing Address" -#~ msgid "Billing Address:" -#~ msgstr "Rechnungsadresse" - -#, fuzzy -#~| msgid "Date" -#~ msgid "Date:" -#~ msgstr "Datum" - -#, fuzzy -#~| msgid "Configuration" -#~ msgid "Donation" -#~ msgstr "Konfiguration" - -#, fuzzy -#~| msgid "View Detail" -#~ msgid "View Donations" -#~ msgstr "Details anzeigen" - #~ msgid "You haven been logged out" -#~ msgstr "Sie wurden abgmeldet" - -#, fuzzy -#~| msgid "Log in" -#~ msgid "Log in " -#~ msgstr "Anmelden" - -#, fuzzy -#~| msgid "View Detail" -#~ msgid "DG.Detail" -#~ msgstr "Details anzeigen" - -#, fuzzy -#~| msgid "Cancel" -#~ msgid "France" -#~ msgstr "Beenden" - -#, fuzzy -#~| msgid "Enter your credit card number" -#~ msgid "Enter your name or company name" -#~ msgstr "Deine Kreditkartennummer" - -#, fuzzy -#~| msgid "Card Number" -#~ msgid "Cardholder Name" -#~ msgstr "Kreditkartennummer" +#~ msgstr "Du wurdest abgemeldet" #~ msgid "How it works" #~ msgstr "So funktioniert es" #~ msgid "Your infrastructure" -#~ msgstr "deine Infrastruktur" +#~ msgstr "Deine Infrastruktur" #~ msgid "Our inftrastructure" #~ msgstr "Unsere Infrastruktur" @@ -650,26 +815,20 @@ msgstr "" #~ msgstr "Preise" #~ msgid "Access Key" -#~ msgstr "Zugriffsschlüssel" +#~ msgstr "SSH Key" #~ msgid "Upload your own key. " -#~ msgstr "Lade deinen Key hoch" +#~ msgstr "Lade Deinen Key hoch" #~ msgid "Generate Key Pair" #~ msgstr "Schlüsselpaar generieren" -#~ msgid "Created at" -#~ msgstr "Erstellt am" - #~ msgid "Billing Amount" #~ msgstr "Rechnungsbetrag" #~ msgid "Payment Details" #~ msgstr "Rechnungsdetails" -#~ msgid "Place Order" -#~ msgstr "Bestelle" - #~ msgid "CARD NUMBER" #~ msgstr "Kreditkartennummer" @@ -683,5 +842,5 @@ msgstr "" #~ "Your SSH private key was already generated and downloaded, if you lost " #~ "it, contact us. " #~ msgstr "" -#~ "Dein privater SSH Schlüssel wurde bereits generiert und heruntergeladen. " -#~ "Falls du ihn verloren hast, kontaktiere uns." +#~ "Dein privater SSH Key wurde bereits generiert und heruntergeladen. " +#~ "Falls Du ihn verloren hast, kontaktiere uns." diff --git a/hosting/static/hosting/css/commons.css b/hosting/static/hosting/css/commons.css index 1ebae4b4..317caabc 100644 --- a/hosting/static/hosting/css/commons.css +++ b/hosting/static/hosting/css/commons.css @@ -1,11 +1,22 @@ +@media (min-width: 768px) { + .navbar-right { + margin-right: 10px; + } +} + .dashboard-container { - padding-top:70px; + padding-top: 80px; padding-bottom: 70px; width: 90%; margin: 0 auto; max-width: 768px; } +.dashboard-container.wide { + padding-top: 90px; + max-width: 980px; +} + .content-dashboard{ min-height: calc(100vh - 70px); width: 80%; @@ -103,7 +114,7 @@ text-align: center; width: 100%; float: left; - padding: 0px 40px 15px 30px; + padding: 0px 30px 15px 30px; } .modal-body .modal-icon i { font-size: 80px; @@ -111,7 +122,7 @@ color: #999; } .modal-body .modal-icon { - margin-bottom: 10px; + margin-bottom: 15px; } .modal-title { margin: 0; @@ -122,9 +133,16 @@ font-weight: 300; } .modal-text { - padding-top: 15px; + padding-top: 5px; font-size: 16px; } +.modal-text p:not(:last-of-type){ + margin-bottom: 5px; +} + +.modal-title + .modal-footer { + margin-top: 5px; +} .modal-footer { border-top: 0px solid #e5e5e5; width: 100%; @@ -132,11 +150,8 @@ text-align: center; padding: 15px 15px; } -.modal-footer button[type="submit"] { - min-width: 80px; -} @media (min-width: 1300px) { - .modal-dialog {/* top: 30%; */width: 35%;} + .modal-dialog {/* top: 30%; */width: 40%;} } @media (max-width: 1299px) { .modal-dialog { @@ -233,4 +248,131 @@ -webkit-transform: translate(-50%,-50%); -ms-transform: translate(-50%,-50%); transform: translate(-50%,-50%); +} + +.settings-container { + padding: 8px; +} + +.settings-container h4 { + margin-bottom: 15px; + color: #333; + font-size: 14px; +} + +.settings-container .card-expiry-element, +.settings-container .card-cvc-element { + padding: 0 15px; +} +.settings-container .card-cvc-element .my-input, +.settings-container .card-cvc-element label { + padding-left: 0; +} + +.settings-container .stripe-payment-btn { + float: none; + position: static; +} + +.settings-container h3 { + font-weight: bold; +} + +.settings-container hr { + margin-top: 15px; +} + +.settings-container .credit-card-details { + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.settings-container .credit-card-details h5 { + font-weight: bold; + font-size: 16px; +} + +.credit-card-form { + max-width: 360px; +} + +.btn-wide { + min-width: 100px; +} + +.caps-link { + font-weight: 600; + color: #8da4c0; + fill: #8da4c0; + padding: 8px 0; + display: block; +} +.caps-link:hover, +.caps-link:focus, +.caps-link:active { + color: #627388; + fill: #627388; + text-decoration: none; +} + +.settings-container .credit-card-details-opt { + padding-top: 15px; +} + +.caps-link .svg-img { + margin-right: 5px; + height: 13px; + position: relative; + top: 1px; + vertical-align: baseline; +} + +.settings-container .caps-link { + font-size: 13px; + letter-spacing: 1.1px; +} + +.settings-container .btn-vm-contact { + font-weight: 600; + font-size: 13px; + /* padding: 4px 15px; */ +} + +.btn-wide { + min-width: 100px; +} + +.no-cards { + text-align: center; + color: #999; + padding: 15px; + background: rgba(0,0,0,0.02); + display: flex; + flex-direction: column; + height: 230px; + justify-content: center; +} + +.no-cards h4 { + font-size: 24px; +} + +.no-cards a { + color: #7ca3d0; +} + +.btn-plain { + background: transparent; + border: none; + fill: #595959; + color: #595959; + outline: none; +} +.btn-plain:hover, +.btn-plain:focus, +.btn-plain:active, +.btn-plain:active:focus { + outline: none; + color: #999; + fill: #999; } \ No newline at end of file diff --git a/hosting/static/hosting/css/dashboard.css b/hosting/static/hosting/css/dashboard.css new file mode 100644 index 00000000..c7bbecd9 --- /dev/null +++ b/hosting/static/hosting/css/dashboard.css @@ -0,0 +1,85 @@ +.hosting-dashboard:after { + content: ''; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(91, 116, 173, 0.7); + z-index: -1; +} +.hosting-dashboard:before { + content: ''; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: url(../../datacenterlight/img/pattern.jpg) no-repeat center center; + background-size: cover; + z-index: -2; + height: 100%; +} + +.hosting-dashboard .dashboard-container-head { + color: #fff; + margin-bottom: 60px; +} + +.hosting-dashboard-item { + background: #e9ebee; + box-shadow: 1px 3px 3px rgba(0,0,0,0.4); + padding: 25px; + color: rgba(124, 139, 175, 1); + font-size: 19px; + display: block; + margin-bottom: 20px; +} +.hosting-dashboard-item:hover, +.hosting-dashboard-item:focus, +.hosting-dashboard-item:active { + text-decoration: none; + color: #7c8baf; + background: #fff; +} + +.hosting-dashboard-item h2 { + margin: 0; + font-size: 18px; + padding-bottom: 15px; + border-bottom: 2px solid #acb5cf; + margin-bottom: 10px; +} + +.hosting-dashboard-image { + height: 120px; + fill: #8b9bb7; + display: flex; + align-items: center; +} +.hosting-dashboard-item:hover .hosting-dashboard-image, +.hosting-dashboard-item:focus .hosting-dashboard-image, +.hosting-dashboard-item:active .hosting-dashboard-image { + fill: #6D84AC; + color: #6D84AC; +} +.hosting-dashboard-image img, +.hosting-dashboard-image svg { + width: 100%; + height: 100%; + max-height: 79px; +} +.hosting-dashboard-image img { + opacity: 0.2; +} + +@media (min-width: 768px) { + .hosting-dashboard-content { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + } + .hosting-dashboard-item { + width: 31.5%; + } +} \ No newline at end of file diff --git a/hosting/static/hosting/css/landing-page.css b/hosting/static/hosting/css/landing-page.css index d1dc657a..5275dd97 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -4,20 +4,6 @@ * For details, see http://www.apache.org/licenses/LICENSE-2.0. */ -/*@font-face { - font-family: 'Lato-Regular'; - src: url('../fonts/Lato/Lato-Regular.ttf'); -} - -@font-face { - src: url('../fonts/Lato/Lato-Black.ttf'); -} - -@font-face { - font-family: 'Lato-Light'; - src: url('../fonts/Lato/Lato-Light.ttf'); -}*/ - body, html { width: 100%; @@ -39,11 +25,29 @@ h6 { .topnav { font-size: 14px; } +.topnav .navbar-fixed-top .navbar-collapse { + max-height: 740px; +} +.navbar-brand { + padding: 10px 15px; +} + +.navbar-default { + background: #fff; + /* box-shadow: 0 3px 3px -2px hsla(0,0%,78%,.72); */ + padding: 5px; +} + +.navbar-default .navbar-header { + position: relative; + z-index: 1; +} .navbar-transparent { background: transparent; border: none; padding: 20px; + box-shadow: none; } .navbar-transparent.topnav { @@ -71,6 +75,48 @@ h6 { /* color: #fff; */ } +.navbar-right .highlights-dropdown .dropdown-menu { + left: 0 !important; + min-width: 155px; + margin-left: 15px; + padding: 0 5px 8px !important; +} +@media(min-width: 768px) { + .navbar-default .navbar-nav>li>a, + .navbar-right .highlights-dropdown .dropdown-menu > li > a { + font-weight: 300; + } + .navbar-right .highlights-dropdown .dropdown-menu { + box-shadow: 0 2px 5px 0 rgba(0,0,0,0.02); + border-width: 0 0 1px 0; + border-color: #e7e7e7; + box-shadow: -8px 14px 20px -5px rgba(77, 77, 77, 0.5); + } +/* .navbar-right .highlights-dropdown .dropdown-menu:before { + content: ''; + display: block; + height: 1px; + background: #e7e7e7; + position: absolute; + top: -1px; + left: -1px; + right: -1px; + } */ +} +.navbar-right .highlights-dropdown .dropdown-menu > li > a{ + font-size: 13px; + font-family: 'Lato', sans-serif; + padding: 1px 10px 1px 18px !important; + background: transparent; + color: #333; +} +.navbar-right .highlights-dropdown .dropdown-menu > li > a:hover, +.navbar-right .highlights-dropdown .dropdown-menu > li > a:focus, +.navbar-right .highlights-dropdown .dropdown-menu > li > a:active { + background: transparent; + text-decoration: underline !important; +} + .lead { font-size: 18px; font-weight: 400; @@ -534,8 +580,9 @@ a.unlink:hover { } .dcl-place-order-text{ - font-size: 13px; + /* font-size: 13px; */ color: #808080; + /* margin-bottom: 15px; */ } .dcl-order-table-total .tbl-total { @@ -568,6 +615,7 @@ a.unlink:hover { border: 1px solid #a1a1a1; border-radius: 3px; padding: 5px; + margin-bottom: 15px; } .card-warning-error { border: 1px solid #EB4D5C; @@ -725,12 +773,12 @@ a.unlink:hover { @media (min-width: 768px) { .dcl-billing { - padding-right: 50px; + padding-right: 65px; border-right: 1px solid #eee; } .dcl-creditcard { - padding-left: 50px; + padding-left: 65px; } .tbl-tot { @@ -760,6 +808,30 @@ a.unlink:hover { color: #ddd; } +.visible-mobile { + display: none !important; +} + +@media(max-width:767px) { + .visible-mobile { + display: block !important; + } + .visible-desktop { + display: none !important; + } +} + +.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover, +.navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:focus, .navbar-default .navbar-nav>.active>a:hover { + background-color: transparent; +} + +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu>.active>a, .navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus, .navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover { + background-color: transparent; + } +} + /* bootstrap danger color override from #a94442 */ .text-danger, .has-error .help-block, @@ -772,7 +844,6 @@ a.unlink:hover { .has-error.checkbox label, .has-error.radio-inline label, .has-error.checkbox-inline label, -.has-error .form-control, .has-error .form-control-feedback, .alert-danger, .list-group-item-danger, @@ -782,6 +853,10 @@ a.list-group-item-danger:focus, .panel-danger > .panel-heading { color: #eb4d5c; } +.alert-danger{ + background: rgba(235, 204, 209, 0.2); +} +.has-error .form-control, .has-error .input-group-addon { color: #eb4d5c; border-color: #eb4d5c; @@ -795,3 +870,41 @@ a.list-group-item-danger.active:focus { .panel-danger > .panel-heading .badge { background-color: #eb4d5c; } + +.checkmark { + display: inline-block; +} +.checkmark:after { + /*Add another block-level blank space*/ + content: ''; + display: block; + /*Make it a small rectangle so the border will create an L-shape*/ + width: 25px; + height: 60px; + /*Add a white border on the bottom and left, creating that 'L' */ + border: solid #777; + border-width: 0 3px 3px 0; + /*Rotate the L 45 degrees to turn it into a checkmark*/ + transform: rotate(45deg); +} + +.closemark { + display: inline-block; + width: 50px; + height: 50px; + position: relative; +} +.closemark:before, .closemark:after { + position: absolute; + left: 25px; + content: ' '; + height: 50px; + width: 2px; + background-color: #777; +} +.closemark:before { + transform: rotate(45deg); +} +.closemark:after { + transform: rotate(-45deg); +} diff --git a/hosting/static/hosting/css/order.css b/hosting/static/hosting/css/order.css index e8bc0328..8e9226e2 100644 --- a/hosting/static/hosting/css/order.css +++ b/hosting/static/hosting/css/order.css @@ -1,4 +1,15 @@ -.order-detail-container {padding-top: 70px; padding-bottom: 70px; margin-bottom: 70px;} +.order-detail-container { + max-width: 600px; + margin: 100px auto 40px; + border: 1px solid #ccc; + padding: 15px; +} + +@media(min-width: 768px) { + .order-detail-container { + padding: 30px; + } +} .order-detail-container .invoice-title h2, .invoice-title h3 { display: inline-block; @@ -15,3 +26,67 @@ .order-detail-container .table > tbody > tr > .thick-line { border-top: 2px solid; } + +.order-detail-container .dashboard-title-thin { + margin-top: 0; + margin-left: -3px; +} + +.order-detail-container .dashboard-title-thin .un-icon { + margin-top: -6px; +} + +.order-detail-container .dashboard-container-head { + position: relative; + padding: 0; + margin-bottom: 38px; +} + +.order-detail-container .dashboard-container-options { + position: absolute; + top: 10px; + right: 0; +} +.order-detail-container .dashboard-container-options .svg-img { + height: 22px; + width: 22px; +} + +.order-detail-container .order-details { + margin-bottom: 30px; +} + +.order-detail-container .order-details strong { + color: #595959; +} + +.order-detail-container h4 { + font-size: 16px; + font-weight: bold; + margin-bottom: 10px; +} + +.order-detail-container p { + margin-bottom: 5px; + color: #595959; +} + +.order-detail-container hr { + margin: 15px 0; +} + +@media (max-width: 767px) { + .order-confirm-btn { + text-align: center; + margin-top: 10px; + } + .order-detail-container .dashboard-container-options { + position: absolute; + top: 4px; + right: -4px; + } + .order-detail-container .dashboard-container-options .svg-img { + height: 16px; + width: 16px; + } +} diff --git a/hosting/static/hosting/css/price_calculator.css b/hosting/static/hosting/css/price_calculator.css new file mode 100644 index 00000000..24624f10 --- /dev/null +++ b/hosting/static/hosting/css/price_calculator.css @@ -0,0 +1,237 @@ +/* Create VM calculator */ + +.price-calc-section { + padding: 80px 40px !important; +} + +@media (max-width: 768px) { + .price-calc-section { + margin-top: 40px; + } +} + +.price-calc-section .text { + width: 50%; +} + +.price-calc-section .text .section-heading { + font-size: 48px; + line-height: 48px; + padding-bottom: 27px; + color: #3a3a3a; + letter-spacing: 1px; + position: relative; + text-align: right; +} + +.price-calc-section .text .description { + font-size: 20px; + text-align: right; +} + +.price-calc-section .text .section-heading::before { + content: ""; + position: absolute; + bottom: 0; + background: #29427A; + height: 7px; + width: 70px; + right: 0; +} + +.price-calc-section .card { + width: 50%; + margin: 0 auto; + background: #fff; + box-shadow: 1px 3px 6px 2px rgba(0, 0, 0, 0.2); + padding-bottom: 30px; + text-align: center; + max-width: 320px; + position: relative; +} + +@media (min-width: 768px) { + .price-calc-section .card { + margin-left: 0; + } +} + +.price-calc-section .landing { + width: 100% !important; +} + +.no-padding { + padding: 0 !important; +} + +.price-calc-section .card .img-beta { + position: absolute; + top: 5px; + width: 60px; + left: 3px; +} + +.price-calc-section .card .title { + padding: 15px 40px; +} + +.price-calc-section .card .title h3 { + /*font-family: 'Lato', sans-serif;*/ + font-weight: normal; +} + +.price-calc-section .card .price { + background: #5A74AF; + padding: 22px; + color: #fff; + font-size: 32px; +} + +.price-calc-section .card .price .price-text { + font-size: 14px; +} + +.price-calc-section .card .description { + padding: 7px 8px 2px; + position: relative; + display: flex; + justify-content: space-around !important; + align-items: center !important; +} + +.price-calc-section .card .description span { + font-size: 14px; + margin-left: 5px; + /* margin-left: 0px; */ + /* justify-self: start; */ + width: 29%; + text-align: left; + line-height: 16px; + /* font-weight: normal; */ +} + +.price-calc-section .card .description .select-number{ + font-size: 16px; + text-align: center; + width: 85px; +} + +.price-calc-section .card .description i { + color: #29427a; + cursor: pointer; + font-size: 20px; + border: 1px solid #ccc; + padding: 5px 6px 3px; + border-radius: 5px; +} + +.price-calc-section .card .description .left { + margin-right: 7px; +} + +.price-calc-section .card .description .right { + margin-left: 7px; +} + +.price-calc-section .card .descriptions { + padding: 10px; +} + +.price-calc-section .card .description p { + margin: 0; +} + +.price-calc-section .card .btn { + margin-top: 15px; + font-size: 20px; + width: 150px; + border: none; +} + +.price-calc-section .card .select-configuration select { + outline: none; + background: #fff; + border-color: #d0d0d0; + height: 32px; + width: 150px; + text-align: center; + font-size: 14px; + margin-left: 10px; + padding: 6px; + border-radius: 4px; +} + +.price-calc-section .card .check-ip { + font-size: 18px; +} + +.price-calc-section .card .justify-center { + justify-content: center !important; +} + +.price-calc-section .card .description.input label { + font-size: 15px; + font-weight: 700; + /*font-weight: 800;*/ + /*font-family: 'Lato';*/ + margin-bottom: 0; + width: 40px; +} + + +/*Changed class****.price-calc-section .card .description.input input*/ + +.price-calc-section .card .description input { + width: 200px; + font-size: 14px; + text-align: left; + padding: 4px 10px; + border-radius: 4px; + border: 1px solid #d0d0d0; + background: #fff; + margin-left: 10px; +} + +.price-calc-section .card .check-ip input[type=checkbox] { + font-size: 17px; + margin: 0 8px; +} + +.price-calc-section .help-block.with-errors { + text-align: center; + margin: 0 0; + padding: 0 0 5px; +} +.price-calc-section .help-block.with-errors ul { + margin-bottom: 0; +} + +.price-calc-section .form-group { + margin: 0; + position: relative; +} + +.price-calc-section .form-group:after { + content: ' '; + display: block; + position: absolute; + bottom: 0; + left: 18%; + z-index: 20; + height: 1px; + width: 65%; + background: rgba(128, 128, 128, 0.2); +} + +.price-calc-section .btn-primary { + background: #29427A; + border-color: #29427A; + color: #fff; + width: auto; +} + +@media(min-width: 768px) { + .create-vm-container { + padding-top: 120px; + } +} diff --git a/hosting/static/hosting/css/user_keys.css b/hosting/static/hosting/css/user_keys.css index 7935344f..6039ae0e 100644 --- a/hosting/static/hosting/css/user_keys.css +++ b/hosting/static/hosting/css/user_keys.css @@ -163,8 +163,8 @@ /* margin-left: 0; */ } .col-md-12, .col-sm-12{ - padding-left: 5px; - padding-right: 5px; + /*padding-left: 5px;*/ + /*padding-right: 5px;*/ } } @media (max-width: 360px){ @@ -172,8 +172,8 @@ /* width: 100% !important; */ } .container { - padding-right: 5px; - padding-left: 5px; + /*padding-right: 5px; + padding-left: 5px;*/ } } .dashboard-choice-container { @@ -299,6 +299,7 @@ } .key_contain { word-break: break-all; + margin-bottom: 15px; } .custom_form_button{ border-radius: 0; diff --git a/hosting/static/hosting/css/virtual-machine.css b/hosting/static/hosting/css/virtual-machine.css index 45aa68ff..a14abc31 100644 --- a/hosting/static/hosting/css/virtual-machine.css +++ b/hosting/static/hosting/css/virtual-machine.css @@ -290,6 +290,11 @@ text-align: center; } +.vm-vmid .alert { + margin-top: 15px; + margin-bottom: -60px; +} + .vm-item-lg { font-size: 22px; margin-top: 5px; @@ -305,6 +310,10 @@ color: #e47f2f; } +.vm-color-failed { + color: #eb4d5c; +} + .vm-detail-item .value{ font-weight: 400; } @@ -440,10 +449,16 @@ } .dashboard-title-thin .un-icon { - height: 30px; + height: 34px; margin-right: 5px; - margin-top: -1px; - width: 30px; + margin-top: -2px; + width: 34px; + vertical-align: middle; +} +.dashboard-title-thin .un-icon.wide { + height: 38px; + width: 38px; + margin-top: -6px; } .dashboard-subtitle { @@ -506,7 +521,7 @@ color: #87B6EA; } -.vm-status, .vm-status-active, .vm-status-failed { +.vm-status, .vm-status-active, .vm-status-failed, .vm-status-pending { font-weight: 600; } .vm-status-active { @@ -515,6 +530,9 @@ .vm-status-failed { color: #eb4d5c; } +.vm-status-pending { + color: #e47f2f; +} @media (min-width:768px) { .dashboard-subtitle { @@ -528,10 +546,15 @@ font-size: 22px; } .dashboard-title-thin .un-icon { - height: 20px; - width: 18px; + height: 22px; + width: 22px; margin-top: -3px; } + .dashboard-title-thin .un-icon.wide { + height: 25px; + width: 25px; + margin-top: -5px; + } .dashboard-subtitle p { width: 200px; } @@ -616,4 +639,28 @@ left: auto; right: 8px; } +} + +.processing > .btn { + position: relative; + border-color: #eee; +} +.processing > .btn:hover, +.processing > .btn:focus, +.processing > .btn:active { + border-color: #eee; +} + +.processing > .btn:after { + content: ' '; + display: block; + position: absolute; + background-image: url('/static/hosting/img/ajax-loader.gif'); + background-repeat: no-repeat; + background-position: center; + background-color: #eee; + width: 100%; + top: 0; + height: 100%; + left: 0; } \ No newline at end of file diff --git a/hosting/static/hosting/fonts/Lato/Lato-Black.ttf b/hosting/static/hosting/fonts/Lato/Lato-Black.ttf deleted file mode 100755 index 6848db0d..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-Black.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-BlackItalic.ttf b/hosting/static/hosting/fonts/Lato/Lato-BlackItalic.ttf deleted file mode 100755 index 5decf129..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-BlackItalic.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-Bold.ttf b/hosting/static/hosting/fonts/Lato/Lato-Bold.ttf deleted file mode 100755 index 74343694..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-Bold.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-BoldItalic.ttf b/hosting/static/hosting/fonts/Lato/Lato-BoldItalic.ttf deleted file mode 100755 index 684aacf5..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-BoldItalic.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-Hairline.ttf b/hosting/static/hosting/fonts/Lato/Lato-Hairline.ttf deleted file mode 100755 index 288be295..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-Hairline.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-HairlineItalic.ttf b/hosting/static/hosting/fonts/Lato/Lato-HairlineItalic.ttf deleted file mode 100755 index c2bfd335..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-HairlineItalic.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-Italic.ttf b/hosting/static/hosting/fonts/Lato/Lato-Italic.ttf deleted file mode 100755 index 3d3b7a29..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-Italic.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-Light.ttf b/hosting/static/hosting/fonts/Lato/Lato-Light.ttf deleted file mode 100755 index a958067a..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-Light.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-LightItalic.ttf b/hosting/static/hosting/fonts/Lato/Lato-LightItalic.ttf deleted file mode 100755 index 5e45ad9a..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-LightItalic.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/Lato-Regular.ttf b/hosting/static/hosting/fonts/Lato/Lato-Regular.ttf deleted file mode 100755 index 04ea8efb..00000000 Binary files a/hosting/static/hosting/fonts/Lato/Lato-Regular.ttf and /dev/null differ diff --git a/hosting/static/hosting/fonts/Lato/OFL.txt b/hosting/static/hosting/fonts/Lato/OFL.txt deleted file mode 100755 index f8ca7bc3..00000000 --- a/hosting/static/hosting/fonts/Lato/OFL.txt +++ /dev/null @@ -1,92 +0,0 @@ -Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/hosting/static/hosting/img/24-hours-support.svg b/hosting/static/hosting/img/24-hours-support.svg index 4db05be3..473828a2 100644 --- a/hosting/static/hosting/img/24-hours-support.svg +++ b/hosting/static/hosting/img/24-hours-support.svg @@ -1,61 +1,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Slice 23 + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/img/ajax-loader.gif b/hosting/static/hosting/img/ajax-loader.gif new file mode 100644 index 00000000..a7c3f2ba Binary files /dev/null and b/hosting/static/hosting/img/ajax-loader.gif differ diff --git a/hosting/static/hosting/img/billing.svg b/hosting/static/hosting/img/billing.svg index d002fa6c..c382cffa 100644 --- a/hosting/static/hosting/img/billing.svg +++ b/hosting/static/hosting/img/billing.svg @@ -1 +1,13 @@ -billing icon \ No newline at end of file + + + + Slice 23 + Created with Sketch. + + + + + + + + diff --git a/hosting/static/hosting/img/dashboard_settings.svg b/hosting/static/hosting/img/dashboard_settings.svg new file mode 100644 index 00000000..f8d60bf5 --- /dev/null +++ b/hosting/static/hosting/img/dashboard_settings.svg @@ -0,0 +1,14 @@ + + + + Slice 23 + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/img/delete.svg b/hosting/static/hosting/img/delete.svg new file mode 100644 index 00000000..31991c4a --- /dev/null +++ b/hosting/static/hosting/img/delete.svg @@ -0,0 +1,7 @@ + + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/hosting/static/hosting/img/icon-pdf.svg b/hosting/static/hosting/img/icon-pdf.svg new file mode 100644 index 00000000..3743bbb8 --- /dev/null +++ b/hosting/static/hosting/img/icon-pdf.svg @@ -0,0 +1,18 @@ + + + + icon-pdf + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/img/icon-print.svg b/hosting/static/hosting/img/icon-print.svg new file mode 100644 index 00000000..75eb175f --- /dev/null +++ b/hosting/static/hosting/img/icon-print.svg @@ -0,0 +1,17 @@ + + + + 54471 + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/img/key.svg b/hosting/static/hosting/img/key.svg new file mode 100644 index 00000000..42b1e539 --- /dev/null +++ b/hosting/static/hosting/img/key.svg @@ -0,0 +1,12 @@ + + + + Slice 23 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/img/plusVM.svg b/hosting/static/hosting/img/plusVM.svg new file mode 100644 index 00000000..2bd59e2d --- /dev/null +++ b/hosting/static/hosting/img/plusVM.svg @@ -0,0 +1,14 @@ + + + + Slice 23 + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/img/vm.svg b/hosting/static/hosting/img/vm.svg index 376e7d0a..061d80ce 100644 --- a/hosting/static/hosting/img/vm.svg +++ b/hosting/static/hosting/img/vm.svg @@ -1,7 +1,12 @@ - - - - - Svg Vector Icons : http://www.onlinewebfonts.com/icon - + + + + Slice 23 + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/js/createvm.js b/hosting/static/hosting/js/createvm.js index 53646b40..726513ad 100644 --- a/hosting/static/hosting/js/createvm.js +++ b/hosting/static/hosting/js/createvm.js @@ -1,73 +1,75 @@ (function($){ "use strict"; // Start of use strict - - - $(window).load(function(){ - - - }); - - $(document).ready(function(){ - _initOs(); - - }); - - $(window).resize(function(){ - - - }); - - - function _initOs(){ - - - $('.os-circle').click(function(event){ - $('.os-circle').removeClass('active'); - $(this).addClass('active'); - - var idTemplate = $(this).data('id'); - $('input[name=vm_template_id]').val(idTemplate); - }); - $('.config-box').click(function(event){ - $('.config-box').removeClass('active'); - $(this).addClass('active'); - var idConfig = $(this).data('id'); - var price = $(this).data('price'); - $('input[name=configuration]').val(idConfig); - $('.container-button').fadeIn(); - $('#priceValue').text(price); - }); - - $('.owl-carousel').owlCarousel({ - items:4, - nav: true, - margin:30, - responsiveClass:true, - navText: ['', ''], - responsive:{ - 0:{ - items:1, - nav:true - }, - 600:{ - items:2, - nav:true - }, - 768:{ - items:3, - nav:true - }, - 990:{ - items:4, - nav:true - } + var cardPricing = { + 'cpu': { + 'id': 'coreValue', + 'value': 1, + 'min': 1, + 'max': 48, + 'interval': 1 + }, + 'ram': { + 'id': 'ramValue', + 'value': 2, + 'min': 2, + 'max': 200, + 'interval': 1 + }, + 'storage': { + 'id': 'storageValue', + 'value': 10, + 'min': 10, + 'max': 2000, + 'interval': 10 } - }); - } - - - -})(jQuery); + }; + + function _initPricing() { + _fetchPricing(); + + $('.fa-minus.left').click(function(event) { + var data = $(this).data('minus'); + + if (cardPricing[data].value > cardPricing[data].min) { + cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval; + } + _fetchPricing(); + }); + $('.fa-plus.right').click(function(event) { + var data = $(this).data('plus'); + if (cardPricing[data].value < cardPricing[data].max) { + cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval; + } + _fetchPricing(); + }); + + $('.input-price').change(function() { + var data = $(this).attr("name"); + cardPricing[data].value = $('input[name=' + data + ']').val(); + _fetchPricing(); + }); + } + + function _fetchPricing() { + Object.keys(cardPricing).map(function(element) { + $('input[name=' + element + ']').val(cardPricing[element].value); + }); + _calcPricing(); + } + + 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); + } + + $(document).ready(function() { + _initPricing(); + }); + +})(jQuery); diff --git a/hosting/static/hosting/js/createvm_old.js b/hosting/static/hosting/js/createvm_old.js new file mode 100644 index 00000000..53646b40 --- /dev/null +++ b/hosting/static/hosting/js/createvm_old.js @@ -0,0 +1,73 @@ +(function($){ + "use strict"; // Start of use strict + + + $(window).load(function(){ + + + }); + + $(document).ready(function(){ + _initOs(); + + }); + + $(window).resize(function(){ + + + }); + + + + function _initOs(){ + + + $('.os-circle').click(function(event){ + $('.os-circle').removeClass('active'); + $(this).addClass('active'); + + var idTemplate = $(this).data('id'); + $('input[name=vm_template_id]').val(idTemplate); + }); + $('.config-box').click(function(event){ + $('.config-box').removeClass('active'); + $(this).addClass('active'); + var idConfig = $(this).data('id'); + var price = $(this).data('price'); + $('input[name=configuration]').val(idConfig); + $('.container-button').fadeIn(); + $('#priceValue').text(price); + }); + + $('.owl-carousel').owlCarousel({ + items:4, + nav: true, + margin:30, + responsiveClass:true, + navText: ['', ''], + responsive:{ + 0:{ + items:1, + nav:true + }, + 600:{ + items:2, + nav:true + }, + 768:{ + items:3, + nav:true + }, + 990:{ + items:4, + nav:true + } + } + }); + } + + + +})(jQuery); + + diff --git a/hosting/static/hosting/js/html2pdf.js b/hosting/static/hosting/js/html2pdf.js new file mode 100644 index 00000000..45ae5b0c --- /dev/null +++ b/hosting/static/hosting/js/html2pdf.js @@ -0,0 +1,387 @@ +/** + * @license + * + * MIT License + * + * Copyright (c) 2017 Erik Koopmans + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Generate a PDF from an HTML element or string using html2canvas and jsPDF. + * + * @param {Element|string} source The source element or HTML string. + * @param {Object=} opt An object of optional settings: 'margin', 'filename', + * 'image' ('type' and 'quality'), and 'html2canvas' / 'jspdf', which are + * sent as settings to their corresponding functions. + */ +var html2pdf = (function(html2canvas, jsPDF) { + + /* ---------- MAIN FUNCTION ---------- */ + + var html2pdf = function(source, opt) { + // Handle input. + opt = objType(opt) === 'object' ? opt : {}; + var source = html2pdf.parseInput(source, opt); + + // Determine the PDF page size. + var pageSize = jsPDF.getPageSize(opt.jsPDF); + pageSize.inner = { + width: pageSize.width - opt.margin[1] - opt.margin[3], + height: pageSize.height - opt.margin[0] - opt.margin[2] + }; + pageSize.inner.ratio = pageSize.inner.height / pageSize.inner.width; + + // Copy the source element into a PDF-styled container div. + var container = html2pdf.makeContainer(source, pageSize); + var overlay = container.parentElement; + + // Get the locations of all hyperlinks. + if (opt.enableLinks) { + // Find all anchor tags and get the container's bounds for reference. + opt.links = []; + var links = container.querySelectorAll('a'); + var containerRect = unitConvert(container.getBoundingClientRect(), pageSize.k); + + // Treat each client rect as a separate link (for text-wrapping). + Array.prototype.forEach.call(links, function(link) { + var clientRects = link.getClientRects(); + for (var i=0; i
{{ order.price }} {% if order.approved %} - Approved + {% trans "Approved" %} {% else %} - Declined + {% trans "Declined" %} {% endif %} - {% trans "View Detail" %} + {% trans 'See Invoice' %}
{{user_key.name}} - @@ -42,20 +41,18 @@ -

+

{% trans "Show" %}

{% if user_key.private_key %}
-
{% endif %} diff --git a/hosting/templates/hosting/virtual_machine_detail.html b/hosting/templates/hosting/virtual_machine_detail.html index 59a36bdc..0b882055 100644 --- a/hosting/templates/hosting/virtual_machine_detail.html +++ b/hosting/templates/hosting/virtual_machine_detail.html @@ -53,19 +53,26 @@

{% trans "Status" %}

{% trans "Your VM is" %}
- {% if virtual_machine.state == 'PENDING' %} -
{% trans "Pending" %}
- {% elif virtual_machine.state == 'ACTIVE' %} -
{% trans "Online" %}
- {% elif virtual_machine.state == 'FAILED'%} -
{% trans "Failed" %}
- {% endif %} - {% if not virtual_machine.status == 'canceled' %} -
- {% csrf_token %} -
- - {% endif %} +
+ {% if virtual_machine.state == 'PENDING' %} +
{% trans "Pending" %}
+ {% elif virtual_machine.state == 'ACTIVE' %} +
{% trans "Online" %}
+ {% elif virtual_machine.state == 'FAILED'%} +
{% trans "Failed" %}
+ {% else %} +
+ {% endif %} + {% if not virtual_machine.status == 'canceled' %} +
+ {% csrf_token %} +
+ +
+ {% trans "Sorry, there was an unexpected error. Kindly retry." %} +
+ {% endif %} +
@@ -89,24 +96,42 @@ + + + {%endblock%} diff --git a/hosting/templates/hosting/virtual_machines.html b/hosting/templates/hosting/virtual_machines.html index f7461abe..622e7a55 100644 --- a/hosting/templates/hosting/virtual_machines.html +++ b/hosting/templates/hosting/virtual_machines.html @@ -41,13 +41,15 @@
{{vm.ipv6}} - {% if vm.state == 'ACTIVE' %} - {{vm.state|title}} - {% elif vm.state == 'FAILED' %} - {{vm.state|title}} - {% else %} - {{vm.state|title}} - {% endif %} + + {% if vm.state == 'ACTIVE' %} + {{vm.state|title}} + {% elif vm.state == 'FAILED' %} + {{vm.state|title}} + {% else %} + {{vm.state|title}} + {% endif %} + {% trans "View Detail" %} diff --git a/hosting/urls.py b/hosting/urls.py index 23709904..f40e803a 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -1,25 +1,32 @@ from django.conf.urls import url from django.contrib.auth import views as auth_views - -from .views import DjangoHostingView, RailsHostingView, PaymentVMView,\ - NodeJSHostingView, LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView, \ - OrdersHostingListView, OrdersHostingDetailView, VirtualMachinesPlanListView,\ - VirtualMachineView, OrdersHostingDeleteView, NotificationsView, \ - MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView, HostingPricingView,\ - CreateVirtualMachinesView, HostingBillListView, HostingBillDetailView, \ - SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView, SSHKeyChoiceView +from .views import ( + DjangoHostingView, RailsHostingView, PaymentVMView, NodeJSHostingView, + LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView, + NotificationsView, OrdersHostingListView, OrdersHostingDetailView, + VirtualMachinesPlanListView, VirtualMachineView, OrdersHostingDeleteView, + MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView, + HostingPricingView, CreateVirtualMachinesView, HostingBillListView, + HostingBillDetailView, SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView, + SSHKeyChoiceView, DashboardView, SettingsView, ResendActivationEmailView) urlpatterns = [ url(r'index/?$', IndexView.as_view(), name='index'), url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'), + url(r'dashboard/?$', DashboardView.as_view(), name='dashboard'), url(r'nodejs/?$', NodeJSHostingView.as_view(), name='nodejshosting'), url(r'rails/?$', RailsHostingView.as_view(), name='railshosting'), url(r'pricing/?$', HostingPricingView.as_view(), name='pricing'), url(r'payment/?$', PaymentVMView.as_view(), name='payment'), + url(r'settings/?$', SettingsView.as_view(), name='settings'), url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'), - url(r'orders/(?P\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'), + url(r'order-confirmation/?$', OrdersHostingDetailView.as_view(), + name='order-confirmation'), + url(r'orders/(?P\d+)/?$', OrdersHostingDetailView.as_view(), + name='orders'), url(r'bills/?$', HostingBillListView.as_view(), name='bills'), - url(r'bills/(?P\d+)/?$', HostingBillDetailView.as_view(), name='bills'), + url(r'bills/(?P\d+)/?$', HostingBillDetailView.as_view(), + name='bills'), url(r'cancel_order/(?P\d+)/?$', OrdersHostingDeleteView.as_view(), name='delete_order'), url(r'create_virtual_machine/?$', CreateVirtualMachinesView.as_view(), @@ -36,13 +43,18 @@ urlpatterns = [ name='delete_ssh_key'), url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(), name='create_ssh_key'), - url(r'^notifications/$', NotificationsView.as_view(), name='notifications'), + url(r'^notifications/$', NotificationsView.as_view(), + name='notifications'), url(r'^notifications/(?P\d+)/?$', MarkAsReadNotificationView.as_view(), name='read_notification'), url(r'login/?$', LoginView.as_view(), name='login'), url(r'signup/?$', SignupView.as_view(), name='signup'), - url(r'signup-validate/?$', SignupValidateView.as_view(), name='signup-validate'), - url(r'reset-password/?$', PasswordResetView.as_view(), name='reset_password'), + url(r'signup-validate/?$', SignupValidateView.as_view(), + name='signup-validate'), + url(r'resend-activation-link/?$', ResendActivationEmailView.as_view(), + name='resend_activation_link'), + url(r'reset-password/?$', PasswordResetView.as_view(), + name='reset_password'), url(r'reset-password-confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', PasswordResetConfirmView.as_view(), name='reset_password_confirm'), url(r'^logout/?$', auth_views.logout, diff --git a/hosting/views.py b/hosting/views.py index 0747f134..8d1d51db 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1,43 +1,72 @@ +import json +import logging import uuid +from time import sleep -from django.core.files.base import ContentFile - -from oca.pool import WrongNameError, WrongIdError -from django.shortcuts import render -from django.http import Http404 -from django.core.urlresolvers import reverse_lazy, reverse -from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic import View, CreateView, FormView, ListView, DetailView, \ - DeleteView, TemplateView, UpdateView -from django.http import HttpResponseRedirect -from django.contrib import messages +from django import forms from django.conf import settings -from django.shortcuts import redirect -from django.utils.http import urlsafe_base64_decode +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.tokens import default_token_generator +from django.core.exceptions import ValidationError +from django.core.files.base import ContentFile +from django.core.urlresolvers import reverse_lazy, reverse -from guardian.mixins import PermissionRequiredMixin -from stored_messages.settings import stored_messages_settings -from stored_messages.models import Message -from stored_messages.api import mark_read +from django.http import Http404, HttpResponseRedirect, HttpResponse +from django.shortcuts import redirect, render +from django.utils.http import urlsafe_base64_decode from django.utils.safestring import mark_safe +from django.utils.translation import get_language, ugettext_lazy as _ +from django.utils.translation import ugettext +from django.views.generic import ( + View, CreateView, FormView, ListView, DetailView, DeleteView, + TemplateView, UpdateView +) +from guardian.mixins import PermissionRequiredMixin +from oca.pool import WrongIdError +from stored_messages.api import mark_read +from stored_messages.models import Message +from stored_messages.settings import stored_messages_settings +from datacenterlight.tasks import create_vm_task from membership.models import CustomUser, StripeCustomer -from utils.stripe_utils import StripeUtils -from utils.forms import BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm -from utils.views import PasswordResetViewMixin, PasswordResetConfirmViewMixin, LoginViewMixin -from utils.mailer import BaseEmail -from .models import HostingOrder, HostingBill, HostingPlan, UserHostingKey -from .forms import HostingUserSignupForm, HostingUserLoginForm, UserHostingKeyForm, generate_ssh_key_name -from .mixins import ProcessVMSelectionMixin - from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VirtualMachineSerializer, \ - VirtualMachineTemplateSerializer -from django.utils.translation import ugettext_lazy as _ + VirtualMachineTemplateSerializer, VMTemplateSerializer +from utils.forms import ( + BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm, + ResendActivationEmailForm +) +from utils.mailer import BaseEmail +from utils.stripe_utils import StripeUtils +from utils.views import ( + PasswordResetViewMixin, PasswordResetConfirmViewMixin, LoginViewMixin, + ResendActivationLinkViewMixin +) +from .forms import HostingUserSignupForm, HostingUserLoginForm, \ + UserHostingKeyForm, generate_ssh_key_name +from .mixins import ProcessVMSelectionMixin +from .models import HostingOrder, HostingBill, HostingPlan, UserHostingKey +from datacenterlight.models import VMTemplate -CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a backend \ - connection error. please try again in a few minutes." + +logger = logging.getLogger(__name__) + +CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a \ + backend connection error. please try again in a few \ + minutes." + + +class DashboardView(View): + template_name = "hosting/dashboard.html" + + def get_context_data(self, **kwargs): + context = {} + return context + + def get(self, request, *args, **kwargs): + context = self.get_context_data() + return render(request, self.template_name, context) class DjangoHostingView(ProcessVMSelectionMixin, View): @@ -178,7 +207,7 @@ class IndexView(View): class LoginView(LoginViewMixin): template_name = "hosting/login.html" form_class = HostingUserLoginForm - success_url = reverse_lazy('hosting:virtual_machines') + success_url = reverse_lazy('hosting:dashboard') class SignupView(CreateView): @@ -256,6 +285,14 @@ class SignupValidatedView(SignupValidateView): return context +class ResendActivationEmailView(ResendActivationLinkViewMixin): + template_name = 'hosting/resend_activation_link.html' + form_class = ResendActivationEmailForm + success_url = reverse_lazy('hosting:login') + email_template_path = 'datacenterlight/emails/' + email_template_name = 'user_activation' + + class PasswordResetView(PasswordResetViewMixin): site = 'dcl' template_name = 'hosting/reset_password.html' @@ -284,27 +321,29 @@ class PasswordResetConfirmView(PasswordResetConfirmViewMixin): form = self.form_class(request.POST) - if user is not None and default_token_generator.check_token(user, token): + if user is not None and default_token_generator.check_token(user, + token): if form.is_valid(): new_password = form.cleaned_data['new_password2'] user.set_password(new_password) user.save() - messages.success(request, 'Password has been reset.') + messages.success(request, _('Password has been reset.')) # Change opennebula password - opennebula_client.change_user_password(new_password) + opennebula_client.change_user_password(user.password) return self.form_valid(form) else: messages.error( - request, 'Password reset has not been successful.') - form.add_error(None, 'Password reset has not been successful.') + request, _('Password reset has not been successful.')) + form.add_error(None, + _('Password reset has not been successful.')) return self.form_invalid(form) else: - messages.error( - request, 'The reset password link is no longer valid.') - form.add_error(None, 'The reset password link is no longer valid.') + error_msg = _('The reset password link is no longer valid.') + messages.error(request, _(error_msg)) + form.add_error(None, error_msg) return self.form_invalid(form) @@ -354,17 +393,14 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView): def delete(self, request, *args, **kwargs): owner = self.request.user - manager = OpenNebulaManager() + manager = OpenNebulaManager( + email=owner.email, + password=owner.password + ) pk = self.kwargs.get('pk') # Get user ssh key public_key = UserHostingKey.objects.get(pk=pk).public_key - # Add ssh key to user - try: - manager.remove_public_key(user=owner, public_key=public_key) - except ConnectionError: - pass - except WrongNameError: - pass + manager.manage_public_key([{'value': public_key, 'state': False}]) return super(SSHKeyDeleteView, self).delete(request, *args, **kwargs) @@ -385,7 +421,8 @@ class SSHKeyListView(LoginRequiredMixin, ListView): def render_to_response(self, context, **response_kwargs): if not self.queryset: return HttpResponseRedirect(reverse('hosting:choice_ssh_keys')) - return super(SSHKeyListView, self).render_to_response(context, **response_kwargs) + return super(SSHKeyListView, self).render_to_response(context, + **response_kwargs) class SSHKeyChoiceView(LoginRequiredMixin, View): @@ -404,6 +441,13 @@ class SSHKeyChoiceView(LoginRequiredMixin, View): user=request.user, public_key=public_key, name=name) filename = name + '_' + str(uuid.uuid4())[:8] + '_private.pem' ssh_key.private_key.save(filename, content) + owner = self.request.user + manager = OpenNebulaManager( + email=owner.email, + password=owner.password + ) + public_key_str = public_key.decode() + manager.manage_public_key([{'value': public_key_str, 'state': True}]) return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar') @@ -448,23 +492,17 @@ class SSHKeyCreateView(LoginRequiredMixin, FormView): }) owner = self.request.user - manager = OpenNebulaManager() - - # Get user ssh key - public_key = str(form.cleaned_data.get('public_key', '')) - # Add ssh key to user - try: - manager.add_public_key( - user=owner, public_key=public_key, merge=True) - except ConnectionError: - pass - except WrongNameError: - pass - + manager = OpenNebulaManager( + email=owner.email, + password=owner.password + ) + public_key = form.cleaned_data['public_key'] + if type(public_key) is bytes: + public_key = public_key.decode() + manager.manage_public_key([{'value': public_key, 'state': True}]) return HttpResponseRedirect(self.success_url) def post(self, request, *args, **kwargs): - print(self.request.POST.dict()) form = self.get_form() required = 'add_ssh' in self.request.POST form.fields['name'].required = required @@ -475,6 +513,57 @@ class SSHKeyCreateView(LoginRequiredMixin, FormView): return self.form_invalid(form) +class SettingsView(LoginRequiredMixin, FormView): + template_name = "hosting/settings.html" + login_url = reverse_lazy('hosting:login') + form_class = BillingAddressForm + + def get_form(self, form_class): + """ + Check if the user already saved contact details. If so, then show + the form populated with those details, to let user change them. + """ + return form_class( + instance=self.request.user.billing_addresses.first(), + **self.get_form_kwargs()) + + def get_context_data(self, **kwargs): + context = super(SettingsView, self).get_context_data(**kwargs) + # Get user + user = self.request.user + # Get user last order + last_hosting_order = HostingOrder.objects.filter( + customer__user=user).last() + # If user has already an hosting order, get the credit card data from + # it + if last_hosting_order: + credit_card_data = last_hosting_order.get_cc_data() + context.update({ + 'credit_card_data': credit_card_data if credit_card_data else None, + }) + context.update({ + 'stripe_key': settings.STRIPE_API_PUBLIC_KEY + }) + + return context + + def post(self, request, *args, **kwargs): + form = self.get_form() + if form.is_valid(): + billing_address_data = form.cleaned_data + billing_address_data.update({ + 'user': self.request.user.id + }) + billing_address_user_form = UserBillingAddressForm( + instance=self.request.user.billing_addresses.first(), + data=billing_address_data) + billing_address_user_form.save() + return self.render_to_response(self.get_context_data()) + else: + billing_address_data = form.cleaned_data + return self.form_invalid(form) + + class PaymentVMView(LoginRequiredMixin, FormView): template_name = 'hosting/payment.html' login_url = reverse_lazy('hosting:login') @@ -536,157 +625,206 @@ class PaymentVMView(LoginRequiredMixin, FormView): def post(self, request, *args, **kwargs): form = self.get_form() if form.is_valid(): - # Get billing address data billing_address_data = form.cleaned_data - - context = self.get_context_data() - - template = request.session.get('template') - specs = request.session.get('specs') - - vm_template_id = template.get('id', 1) - - final_price = specs.get('price') - token = form.cleaned_data.get('token') - owner = self.request.user - # Get or create stripe customer customer = StripeCustomer.get_or_create(email=owner.email, token=token) if not customer: msg = _("Invalid credit card") - messages.add_message(self.request, messages.ERROR, msg, extra_tags='make_charge_error') - return HttpResponseRedirect(reverse('hosting:payment') + '#payment_error') + messages.add_message( + self.request, messages.ERROR, msg, + extra_tags='make_charge_error') + return HttpResponseRedirect( + reverse('hosting:payment') + '#payment_error') # Create Billing Address billing_address = form.save() - - # Make stripe charge to a customer - stripe_utils = StripeUtils() - charge_response = stripe_utils.make_charge(amount=final_price, - customer=customer.stripe_id) - - # Check if the payment was approved - if not charge_response.get('response_object'): - msg = charge_response.get('error') - messages.add_message(self.request, messages.ERROR, msg, extra_tags='make_charge_error') - return HttpResponseRedirect(reverse('hosting:payment') + '#payment_error') - - charge = charge_response.get('response_object') - - # Create OpenNebulaManager - manager = OpenNebulaManager(email=owner.email, - password=owner.password) - # Get user ssh key - if not UserHostingKey.objects.filter(user=self.request.user).exists(): - context.update({ - 'sshError': 'error', - 'form': form - }) - return render(request, self.template_name, context) - # For now just get first one - user_key = UserHostingKey.objects.filter( - user=self.request.user).first() - - # Create a vm using logged user - vm_id = manager.create_vm( - template_id=vm_template_id, - # XXX: Confi - specs=specs, - ssh_key=user_key.public_key, - ) - - # Create a Hosting Order - order = HostingOrder.create( - price=final_price, - vm_id=vm_id, - customer=customer, - billing_address=billing_address - ) - - # Create a Hosting Bill - HostingBill.create( - customer=customer, billing_address=billing_address) - - # Create Billing Address for User if he does not have one - if not customer.user.billing_addresses.count(): - billing_address_data.update({ - 'user': customer.user.id - }) - billing_address_user_form = UserBillingAddressForm( - billing_address_data) - billing_address_user_form.is_valid() - billing_address_user_form.save() - - # Associate an order with a stripe payment - order.set_stripe_charge(charge) - - # If the Stripe payment was successed, set order status approved - order.set_approved() - - vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data - - # Send notification to ungleich as soon as VM has been booked - context = { - 'vm': vm, - 'order': order, - 'base_url': "{0}://{1}".format(request.scheme, request.get_host()) - - } - email_data = { - 'subject': 'New VM request', - 'to': request.user.email, - 'context': context, - 'template_name': 'new_booked_vm', - 'template_path': 'hosting/emails/' - } - email = BaseEmail(**email_data) - email.send() - - return HttpResponseRedirect( - "{url}?{query_params}".format(url=reverse('hosting:orders', kwargs={'pk': order.id}), - query_params='page=payment')) + request.session['billing_address_data'] = billing_address_data + request.session['billing_address'] = billing_address.id + request.session['token'] = token + request.session['customer'] = customer.id + return HttpResponseRedirect("{url}?{query_params}".format( + url=reverse('hosting:order-confirmation'), + query_params='page=payment')) else: return self.form_invalid(form) -class OrdersHostingDetailView(PermissionRequiredMixin, LoginRequiredMixin, DetailView): +class OrdersHostingDetailView(LoginRequiredMixin, + DetailView): template_name = "hosting/order_detail.html" context_object_name = "order" login_url = reverse_lazy('hosting:login') permission_required = ['view_hostingorder'] model = HostingOrder + def get_object(self): + return HostingOrder.objects.get( + pk=self.kwargs.get('pk')) if self.kwargs.get('pk') else None + def get_context_data(self, **kwargs): # Get context context = super(DetailView, self).get_context_data(**kwargs) obj = self.get_object() owner = self.request.user - manager = OpenNebulaManager(email=owner.email, - password=owner.password) - if self.request.GET.get('page', '') == 'payment': + stripe_customer_id = self.request.session.get('customer') + customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() + stripe_utils = StripeUtils() + if customer: + card_details = stripe_utils.get_card_details( + customer.stripe_id, + self.request.session.get('token') + ) + else: + card_details = {} + + if self.request.GET.get('page') == 'payment': context['page_header_text'] = _('Confirm Order') else: context['page_header_text'] = _('Invoice') - try: - 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.' - ) + + 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.') + ) + elif not card_details.get('response_object'): + # new order, failed to get card details + context['failed_payment'] = True + context['card_details'] = card_details + else: + # new order, confirm payment + context['site_url'] = reverse('hosting:create_virtual_machine') + context['cc_last4'] = card_details.get('response_object').get( + 'last4') + context['cc_brand'] = card_details.get('response_object').get( + 'cc_brand') + context['vm'] = self.request.session.get('specs') return context + def get(self, request, *args, **kwargs): + if not self.kwargs.get('pk'): + if 'specs' not in self.request.session: + return HttpResponseRedirect( + reverse('hosting:create_virtual_machine') + ) + if 'token' not in self.request.session: + return HttpResponseRedirect(reverse('hosting:payment')) + self.object = self.get_object() + context = self.get_context_data(object=self.object) + if 'failed_payment' in context: + msg = context['card_details'].get('error') + messages.add_message( + self.request, messages.ERROR, msg, + extra_tags='failed_payment' + ) + return HttpResponseRedirect( + reverse('hosting:payment') + '#payment_error' + ) + return self.render_to_response(context) + + def post(self, request): + template = request.session.get('template') + specs = request.session.get('specs') + 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') + 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, + 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') + 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) + + stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, + ram=memory, + ssd=disk_size, + version=1, + app='dcl') + stripe_plan = stripe_utils.get_or_create_stripe_plan( + amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id) + subscription_result = stripe_utils.subscribe_customer_to_plan( + customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + stripe_subscription_obj = subscription_result.get('response_object') + # Check if the subscription was approved and is active + if stripe_subscription_obj is None or stripe_subscription_obj.status != 'active': + msg = subscription_result.get('error') + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='failed_payment') + return HttpResponseRedirect( + reverse('hosting:payment') + '#payment_error') + user = { + 'name': self.request.user.name, + 'email': self.request.user.email, + 'pass': self.request.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) + + 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'), + '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") + class OrdersHostingListView(LoginRequiredMixin, ListView): template_name = "hosting/orders.html" @@ -746,46 +884,80 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View): template_name = "hosting/create_virtual_machine.html" login_url = reverse_lazy('hosting:login') - def get(self, request, *args, **kwargs): + def validate_cores(self, value): + if (value > 48) or (value < 1): + raise ValidationError(_('Invalid number of cores')) + def validate_memory(self, value): + if (value > 200) or (value < 2): + raise ValidationError(_('Invalid RAM size')) + + def validate_storage(self, value): + if (value > 2000) or (value < 10): + 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.' + _( + 'In order to create a VM, you need to ' + 'create/upload your SSH KEY first.' + ) ) return HttpResponseRedirect(reverse('hosting:ssh_keys')) - - try: - manager = OpenNebulaManager() - templates = manager.get_templates() - configuration_options = HostingPlan.get_serialized_configs() - - context = { - 'templates': VirtualMachineTemplateSerializer(templates, many=True).data, - 'configuration_options': configuration_options, - } - except: - messages.error( - request, - 'We could not load the VM templates due to a backend connection \ - error. Please try again in a few minutes' - ) - context = { - 'error': 'connection' - } - + context = {'templates': VMTemplate.objects.all()} return render(request, self.template_name, context) def post(self, request): - manager = OpenNebulaManager() - template_id = request.POST.get('vm_template_id') - template = manager.get_template(template_id) - configuration_id = int(request.POST.get('configuration')) - configuration = HostingPlan.objects.get(id=configuration_id) - request.session['template'] = VirtualMachineTemplateSerializer( - template).data + cores = request.POST.get('cpu') + cores_field = forms.IntegerField(validators=[self.validate_cores]) + memory = request.POST.get('ram') + memory_field = forms.IntegerField(validators=[self.validate_memory]) + 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 - request.session['specs'] = configuration.serialize() + try: + cores = cores_field.clean(cores) + except ValidationError as err: + msg = '{} : {}.'.format(cores, str(err)) + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='cores') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") + + try: + memory = memory_field.clean(memory) + except ValidationError as err: + msg = '{} : {}.'.format(memory, str(err)) + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='memory') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") + + try: + storage = storage_field.clean(storage) + except ValidationError as err: + msg = '{} : {}.'.format(storage, str(err)) + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='storage') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") + + specs = { + 'cpu': cores, + 'memory': memory, + 'disk_size': storage, + 'price': price + } + + request.session['specs'] = specs + request.session['template'] = template_data return redirect(reverse('hosting:payment')) @@ -827,19 +999,34 @@ class VirtualMachineView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): vm = self.get_object() if vm is None: - return redirect(reverse('hosting:virtual_machines')) + if self.request.is_ajax(): + storage = messages.get_messages(request) + for m in storage: + pass + storage.used = True + return HttpResponse( + json.dumps({'text': ugettext('Terminated')}), + content_type="application/json" + ) + else: + return redirect(reverse('hosting:virtual_machines')) + elif self.request.is_ajax(): + return HttpResponse() try: serializer = VirtualMachineSerializer(vm) context = { 'virtual_machine': serializer.data, - 'order': HostingOrder.objects.get(vm_id=serializer.data['vm_id']) + 'order': HostingOrder.objects.get( + vm_id=serializer.data['vm_id']) } - except: + except Exception as ex: + logger.debug("Exception generated {}".format(str(ex))) pass return render(request, self.template_name, context) def post(self, request, *args, **kwargs): + response = {'status': False} owner = self.request.user vm = self.get_object() @@ -850,40 +1037,52 @@ class VirtualMachineView(LoginRequiredMixin, View): password=owner.password ) - terminated = manager.delete_vm( - vm.id - ) + try: + vm_data = VirtualMachineSerializer(manager.get_vm(vm.id)).data + except WrongIdError: + return redirect(reverse('hosting:virtual_machines')) + + terminated = manager.delete_vm(vm.id) if not terminated: - messages.error( - request, - 'Error terminating VM %s' % (opennebula_vm_id) - ) - return HttpResponseRedirect(self.get_success_url()) - - context = { - 'vm': vm, - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) - } - email_data = { - 'subject': 'Virtual machine plan canceled', - 'to': self.request.user.email, - 'context': context, - 'template_name': 'vm_status_changed', - 'template_path': 'hosting/emails/' - } - email = BaseEmail(**email_data) - email.send() - - messages.error( - request, - 'VM %s terminated successfully' % (opennebula_vm_id) + response['text'] = ugettext( + 'Error terminating VM') + opennebula_vm_id + else: + for t in range(15): + try: + manager.get_vm(opennebula_vm_id) + except WrongIdError: + response['status'] = True + response['text'] = ugettext('Terminated') + break + except BaseException: + break + else: + sleep(2) + context = { + 'vm': vm_data, + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()), + 'page_header': _('Virtual Machine Cancellation') + } + email_data = { + 'subject': context['page_header'], + 'to': self.request.user.email, + 'context': context, + 'template_name': 'vm_canceled', + 'template_path': 'hosting/emails/', + 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS, + } + email = BaseEmail(**email_data) + email.send() + return HttpResponse( + json.dumps(response), + content_type="application/json" ) - return HttpResponseRedirect(self.get_success_url()) - -class HostingBillListView(PermissionRequiredMixin, LoginRequiredMixin, ListView): +class HostingBillListView(PermissionRequiredMixin, LoginRequiredMixin, + ListView): template_name = "hosting/bills.html" login_url = reverse_lazy('hosting:login') permission_required = ['view_hostingview'] @@ -893,7 +1092,8 @@ class HostingBillListView(PermissionRequiredMixin, LoginRequiredMixin, ListView) ordering = '-id' -class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin, DetailView): +class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin, + DetailView): template_name = "hosting/bill_detail.html" login_url = reverse_lazy('hosting:login') permission_required = ['view_hostingview'] diff --git a/membership/models.py b/membership/models.py index 72b8073e..5d7c7b11 100644 --- a/membership/models.py +++ b/membership/models.py @@ -1,17 +1,19 @@ from datetime import datetime -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin -from django.contrib.auth.hashers import make_password -from django.core.validators import RegexValidator -from django.contrib.sites.models import Site -from django.conf import settings -from django.utils.crypto import get_random_string -from utils.stripe_utils import StripeUtils -from utils.mailer import DigitalGlarusRegistrationMailer +from django.conf import settings +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \ + PermissionsMixin +from django.contrib.sites.models import Site from django.core.urlresolvers import reverse +from django.core.validators import RegexValidator +from django.db import models +from django.utils.crypto import get_random_string +from django.utils.translation import ugettext_lazy as _ + from utils.mailer import BaseEmail +from utils.mailer import DigitalGlarusRegistrationMailer +from utils.stripe_utils import StripeUtils REGISTRATION_MESSAGE = {'subject': "Validation mail", 'message': 'Please validate Your account under this link ' @@ -64,11 +66,13 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): email = models.EmailField(unique=True) validated = models.IntegerField(choices=VALIDATED_CHOICES, default=0) - validation_slug = models.CharField(db_index=True, unique=True, max_length=50) + validation_slug = models.CharField(db_index=True, unique=True, + max_length=50) is_admin = models.BooleanField( _('staff status'), default=False, - help_text=_('Designates whether the user can log into this admin site.'), + help_text=_( + 'Designates whether the user can log into this admin site.'), ) objects = MyUserManager() @@ -77,28 +81,32 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = ['name', 'password'] @classmethod - def register(cls, name, password, email, app='digital_glarus', base_url=None, send_email=True): + def register(cls, name, password, email, app='digital_glarus', + base_url=None, send_email=True): user = cls.objects.filter(email=email).first() if not user: - user = cls.objects.create_user(name=name, email=email, password=password) + user = cls.objects.create_user(name=name, email=email, + password=password) if user: if app == 'digital_glarus': dg = DigitalGlarusRegistrationMailer(user.validation_slug) dg.send_mail(to=user.email) elif app == 'dcl': dcl_text = settings.DCL_TEXT - # not used - # dcl_from_address = settings.DCL_SUPPORT_FROM_ADDRESS user.is_active = False - if send_email is True: email_data = { - 'subject': str(_('Activate your ')) + dcl_text + str(_(' account')), + 'subject': '{dcl_text} {account_activation}'.format( + dcl_text=dcl_text, + account_activation=_('Account Activation') + ), 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS, 'to': user.email, 'context': {'base_url': base_url, - 'activation_link': reverse('hosting:validate', - kwargs={'validate_slug': user.validation_slug}), + 'activation_link': reverse( + 'hosting:validate', + kwargs={ + 'validate_slug': user.validation_slug}), 'dcl_text': dcl_text }, 'template_name': 'user_activation', @@ -114,7 +122,8 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): @classmethod def get_all_members(cls): - return cls.objects.filter(stripecustomer__membershiporder__isnull=False) + return cls.objects.filter( + stripecustomer__membershiporder__isnull=False) @classmethod def validate_url(cls, validation_slug): @@ -204,9 +213,11 @@ class CreditCards(models.Model): name = models.CharField(max_length=50) user_id = models.ForeignKey(CustomUser, on_delete=models.CASCADE) card_number = models.CharField(max_length=50) - expiry_date = models.CharField(max_length=50, validators=[RegexValidator(r'\d{2}\/\d{4}', _( - 'Use this pattern(MM/YYYY).'))]) - ccv = models.CharField(max_length=4, validators=[RegexValidator(r'\d{3,4}', _('Wrong CCV number.'))]) + expiry_date = models.CharField(max_length=50, validators=[ + RegexValidator(r'\d{2}\/\d{4}', _( + 'Use this pattern(MM/YYYY).'))]) + ccv = models.CharField(max_length=4, validators=[ + RegexValidator(r'\d{3,4}', _('Wrong CCV number.'))]) payment_type = models.CharField(max_length=5, default='N') def save(self, *args, **kwargs): @@ -221,7 +232,8 @@ class Calendar(models.Model): def __init__(self, *args, **kwargs): if kwargs.get('datebooked'): user = kwargs.get('user') - kwargs['datebooked'] = datetime.strptime(kwargs.get('datebooked', ''), '%d,%m,%Y') + kwargs['datebooked'] = datetime.strptime( + kwargs.get('datebooked', ''), '%d,%m,%Y') self.user_id = user.id super(Calendar, self).__init__(*args, **kwargs) diff --git a/opennebula_api/models.py b/opennebula_api/models.py index 60f3159c..057139c0 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -1,13 +1,14 @@ -import oca -import socket import logging +import socket -from oca.pool import WrongNameError, WrongIdError -from oca.exceptions import OpenNebulaException - +import oca from django.conf import settings +from oca.exceptions import OpenNebulaException +from oca.pool import WrongNameError, WrongIdError +from hosting.models import HostingOrder from utils.models import CustomUser +from utils.tasks import save_ssh_key, save_ssh_key_error_handler from .exceptions import KeyExistsError, UserExistsError, UserCredentialError logger = logging.getLogger(__name__) @@ -17,7 +18,8 @@ class OpenNebulaManager(): """This class represents an opennebula manager.""" def __init__(self, email=None, password=None): - + self.email = email + self.password = password # Get oneadmin client self.oneadmin_client = self._get_opennebula_client( settings.OPENNEBULA_USERNAME, @@ -122,16 +124,19 @@ class OpenNebulaManager(): except WrongNameError: user_id = self.oneadmin_client.call(oca.User.METHODS['allocate'], - user.email, user.password, 'core') - logger.debug('Created a user for CustomObject: {user} with user id = {u_id}', - user=user, - u_id=user_id - ) + user.email, user.password, + 'core') + logger.debug( + 'Created a user for CustomObject: {user} with user id = {u_id}', + user=user, + u_id=user_id + ) return user_id except ConnectionRefusedError: - logger.error('Could not connect to host: {host} via protocol {protocol}'.format( - host=settings.OPENNEBULA_DOMAIN, - protocol=settings.OPENNEBULA_PROTOCOL) + logger.error( + 'Could not connect to host: {host} via protocol {protocol}'.format( + host=settings.OPENNEBULA_DOMAIN, + protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError @@ -141,8 +146,9 @@ class OpenNebulaManager(): opennebula_user = user_pool.get_by_name(email) return opennebula_user except WrongNameError as wrong_name_err: - opennebula_user = self.oneadmin_client.call(oca.User.METHODS['allocate'], email, - password, 'core') + opennebula_user = self.oneadmin_client.call( + oca.User.METHODS['allocate'], email, + password, 'core') logger.debug( "User {0} does not exist. Created the user. User id = {1}", email, @@ -150,9 +156,10 @@ class OpenNebulaManager(): ) return opennebula_user except ConnectionRefusedError: - logger.info('Could not connect to host: {host} via protocol {protocol}'.format( - host=settings.OPENNEBULA_DOMAIN, - protocol=settings.OPENNEBULA_PROTOCOL) + logger.info( + 'Could not connect to host: {host} via protocol {protocol}'.format( + host=settings.OPENNEBULA_DOMAIN, + protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError @@ -161,9 +168,10 @@ class OpenNebulaManager(): user_pool = oca.UserPool(self.oneadmin_client) user_pool.info() except ConnectionRefusedError: - logger.info('Could not connect to host: {host} via protocol {protocol}'.format( - host=settings.OPENNEBULA_DOMAIN, - protocol=settings.OPENNEBULA_PROTOCOL) + logger.info( + 'Could not connect to host: {host} via protocol {protocol}'.format( + host=settings.OPENNEBULA_DOMAIN, + protocol=settings.OPENNEBULA_PROTOCOL) ) raise return user_pool @@ -183,9 +191,10 @@ class OpenNebulaManager(): raise ConnectionRefusedError except ConnectionRefusedError: - logger.info('Could not connect to host: {host} via protocol {protocol}'.format( - host=settings.OPENNEBULA_DOMAIN, - protocol=settings.OPENNEBULA_PROTOCOL) + logger.info( + 'Could not connect to host: {host} via protocol {protocol}'.format( + host=settings.OPENNEBULA_DOMAIN, + protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError # For now we'll just handle all other errors as connection errors @@ -208,6 +217,33 @@ class OpenNebulaManager(): except: raise ConnectionRefusedError + def get_primary_ipv4(self, vm_id): + """ + Returns the primary IPv4 of the given vm. + To be changed later. + + :return: An IP address string, if it exists else returns None + """ + all_ipv4s = self.get_vm_ipv4_addresses(vm_id) + if len(all_ipv4s) > 0: + return all_ipv4s[0] + else: + return None + + def get_vm_ipv4_addresses(self, vm_id): + """ + Returns a list of IPv4 addresses of the given vm + + :param vm_id: The ID of the vm + :return: + """ + ipv4s = [] + vm = self.get_vm(vm_id) + for nic in vm.template.nics: + if hasattr(nic, 'ip'): + ipv4s.append(nic.ip) + return ipv4s + def create_vm(self, template_id, specs, ssh_key=None, vm_name=None): template = self.get_template(template_id) @@ -258,7 +294,8 @@ class OpenNebulaManager(): vm_specs += "" if ssh_key: - vm_specs += "{ssh}".format(ssh=ssh_key) + vm_specs += "{ssh}".format( + ssh=ssh_key) vm_specs += """YES @@ -312,9 +349,11 @@ class OpenNebulaManager(): template_pool.info() return template_pool except ConnectionRefusedError: - logger.info('Could not connect to host: {host} via protocol {protocol}'.format( - host=settings.OPENNEBULA_DOMAIN, - protocol=settings.OPENNEBULA_PROTOCOL) + logger.info( + """Could not connect to host: {host} via protocol + {protocol}""".format( + host=settings.OPENNEBULA_DOMAIN, + protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError except: @@ -347,7 +386,8 @@ class OpenNebulaManager(): except: raise ConnectionRefusedError - def create_template(self, name, cores, memory, disk_size, core_price, memory_price, + def create_template(self, name, cores, memory, disk_size, core_price, + memory_price, disk_size_price, ssh=''): """Create and add a new template to opennebula. :param name: A string representation describing the template. @@ -398,11 +438,11 @@ class OpenNebulaManager(): self.oneadmin_client.call(oca.VmTemplate.METHODS[ 'delete'], template_id, False) - def change_user_password(self, new_password): + def change_user_password(self, passwd_hash): self.oneadmin_client.call( oca.User.METHODS['passwd'], self.opennebula_user.id, - new_password + passwd_hash ) def add_public_key(self, user, public_key='', merge=False): @@ -490,3 +530,57 @@ class OpenNebulaManager(): except ConnectionError: raise + + def manage_public_key(self, keys, hosts=None, countdown=0): + """ + A function that manages the supplied keys in the + authorized_keys file of the given list of hosts. If hosts + parameter is not supplied, all hosts of this customer + will be configured with the supplied keys + + :param keys: A list of ssh keys that are to be added/removed + A key should be a dict of the form + { + 'value': 'sha-.....', # public key as string + 'state': True # whether key is to be added or + } # removed + :param hosts: A list of hosts IP addresses + :param countdown: Parameter to be passed to celery apply_async + Allows to delay a task by `countdown` number of seconds + :return: + """ + if hosts is None: + hosts = self.get_all_hosts() + + if len(hosts) > 0 and len(keys) > 0: + save_ssh_key.apply_async((hosts, keys), countdown=countdown, + link_error=save_ssh_key_error_handler.s()) + else: + logger.debug( + "Keys and/or hosts are empty, so not managing any keys") + + def get_all_hosts(self): + """ + A utility function to obtain all hosts of this owner + :return: A list of hosts IP addresses, empty if none exist + """ + owner = CustomUser.objects.filter( + email=self.email).first() + all_orders = HostingOrder.objects.filter(customer__user=owner) + hosts = [] + if len(all_orders) > 0: + logger.debug("The user {} has 1 or more VMs. We need to configure " + "the ssh keys.".format(self.email)) + for order in all_orders: + try: + vm = self.get_vm(order.vm_id) + for nic in vm.template.nics: + if hasattr(nic, 'ip'): + hosts.append(nic.ip) + except WrongIdError: + logger.debug( + "VM with ID {} does not exist".format(order.vm_id)) + else: + logger.debug("The user {} has no VMs. We don't need to configure " + "the ssh keys.".format(self.email)) + return hosts diff --git a/requirements.txt b/requirements.txt index 8d9c68c5..89285c83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -95,4 +95,5 @@ pycodestyle==2.3.1 pyflakes==1.5.0 billiard==3.5.0.3 amqp==2.2.1 -vine==1.1.4 \ No newline at end of file +vine==1.1.4 +cdist==4.7.0 diff --git a/ungleich_page/templates/ungleich_page/includes/_about.html b/ungleich_page/templates/ungleich_page/includes/_about.html index ebcbbdcb..e1abb545 100644 --- a/ungleich_page/templates/ungleich_page/includes/_about.html +++ b/ungleich_page/templates/ungleich_page/includes/_about.html @@ -64,7 +64,7 @@

{% trans "ungleich introduces HA-Hosting" %}

{% trans "and introduces affordable 24X7 support." %}

-

{% trans "ungleich launches" %} +

{% trans "ungleich launches" %} {% trans "Digital Glarus project" %}

@@ -79,4 +79,4 @@ - \ No newline at end of file + diff --git a/utils/forms.py b/utils/forms.py index c521e3ba..a12034dd 100644 --- a/utils/forms.py +++ b/utils/forms.py @@ -18,7 +18,8 @@ class SignupFormMixin(forms.ModelForm): model = CustomUser fields = ['name', 'email', 'password'] widgets = { - 'name': forms.TextInput(attrs={'placeholder': _('Enter your name or company name')}), + 'name': forms.TextInput( + attrs={'placeholder': _('Enter your name or company name')}), } def clean_confirm_password(self): @@ -41,7 +42,8 @@ class LoginFormMixin(forms.Form): password = self.cleaned_data.get('password') is_auth = authenticate(email=email, password=password) if not is_auth: - raise forms.ValidationError("Your username and/or password were incorrect.") + raise forms.ValidationError( + _("Your username and/or password were incorrect.")) return self.cleaned_data def clean_email(self): @@ -50,7 +52,24 @@ class LoginFormMixin(forms.Form): CustomUser.objects.get(email=email) return email except CustomUser.DoesNotExist: - raise forms.ValidationError("User does not exist") + raise forms.ValidationError(_("User does not exist")) + + +class ResendActivationEmailForm(forms.Form): + email = forms.CharField(widget=forms.EmailInput()) + + class Meta: + fields = ['email'] + + def clean_email(self): + email = self.cleaned_data.get('email') + try: + c = CustomUser.objects.get(email=email) + if c.validated == 1: + raise forms.ValidationError(_("The account is already active.")) + return email + except CustomUser.DoesNotExist: + raise forms.ValidationError(_("User does not exist")) class PasswordResetRequestForm(forms.Form): @@ -65,7 +84,7 @@ class PasswordResetRequestForm(forms.Form): CustomUser.objects.get(email=email) return email except CustomUser.DoesNotExist: - raise forms.ValidationError("User does not exist") + raise forms.ValidationError(_("User does not exist")) class SetPasswordForm(forms.Form): @@ -74,11 +93,11 @@ class SetPasswordForm(forms.Form): password """ error_messages = { - 'password_mismatch': ("The two password fields didn't match."), + 'password_mismatch': _("The two password fields didn't match."), } - new_password1 = forms.CharField(label=("New password"), + new_password1 = forms.CharField(label=_("New password"), widget=forms.PasswordInput) - new_password2 = forms.CharField(label=("New password confirmation"), + new_password2 = forms.CharField(label=_("New password confirmation"), widget=forms.PasswordInput) def clean_new_password2(self): @@ -101,7 +120,8 @@ class BillingAddressForm(forms.ModelForm): class Meta: model = BillingAddress - fields = ['cardholder_name', 'street_address', 'city', 'postal_code', 'country'] + fields = ['cardholder_name', 'street_address', + 'city', 'postal_code', 'country'] labels = { 'cardholder_name': _('Cardholder Name'), 'street_address': _('Street Address'), @@ -117,8 +137,10 @@ class UserBillingAddressForm(forms.ModelForm): class Meta: model = UserBillingAddress - fields = ['street_address', 'city', 'postal_code', 'country', 'user'] + fields = ['cardholder_name', 'street_address', + 'city', 'postal_code', 'country', 'user'] labels = { + 'cardholder_name': _('Cardholder Name'), 'street_address': _('Street Building'), 'city': _('City'), 'postal_code': _('Postal Code'), @@ -146,8 +168,10 @@ class ContactUsForm(forms.ModelForm): } def send_email(self, email_to='info@digitalglarus.ch'): - text_content = render_to_string('emails/contact.txt', {'data': self.cleaned_data}) - html_content = render_to_string('emails/contact.html', {'data': self.cleaned_data}) + text_content = render_to_string( + 'emails/contact.txt', {'data': self.cleaned_data}) + html_content = render_to_string( + 'emails/contact.html', {'data': self.cleaned_data}) email = EmailMultiAlternatives('Subject', text_content) email.attach_alternative(html_content, "text/html") email.to = [email_to] diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py new file mode 100644 index 00000000..7c1a83ad --- /dev/null +++ b/utils/hosting_utils.py @@ -0,0 +1,11 @@ +from hosting.models import UserHostingKey + + +def get_all_public_keys(customer): + """ + Returns all the public keys of the user + :param customer: The customer whose public keys are needed + :return: A list of public keys + """ + return UserHostingKey.objects.filter(user_id=customer.id).values_list( + "public_key", flat=True) diff --git a/utils/locale/de/LC_MESSAGES/django.po b/utils/locale/de/LC_MESSAGES/django.po new file mode 100644 index 00000000..f8374e4e --- /dev/null +++ b/utils/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,810 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-09-25 20:11+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Andorra" +msgstr "" + +msgid "United Arab Emirates" +msgstr "" + +msgid "Afghanistan" +msgstr "" + +msgid "Antigua & Barbuda" +msgstr "" + +msgid "Anguilla" +msgstr "" + +msgid "Albania" +msgstr "" + +msgid "Armenia" +msgstr "" + +msgid "Netherlands Antilles" +msgstr "" + +msgid "Angola" +msgstr "" + +msgid "Antarctica" +msgstr "" + +msgid "Argentina" +msgstr "" + +msgid "American Samoa" +msgstr "" + +msgid "Austria" +msgstr "" + +msgid "Australia" +msgstr "" + +msgid "Aruba" +msgstr "" + +msgid "Azerbaijan" +msgstr "" + +msgid "Bosnia and Herzegovina" +msgstr "" + +msgid "Barbados" +msgstr "" + +msgid "Bangladesh" +msgstr "" + +msgid "Belgium" +msgstr "" + +msgid "Burkina Faso" +msgstr "" + +msgid "Bulgaria" +msgstr "" + +msgid "Bahrain" +msgstr "" + +msgid "Burundi" +msgstr "" + +msgid "Benin" +msgstr "" + +msgid "Bermuda" +msgstr "" + +msgid "Brunei Darussalam" +msgstr "" + +msgid "Bolivia" +msgstr "" + +msgid "Brazil" +msgstr "" + +msgid "Bahama" +msgstr "" + +msgid "Bhutan" +msgstr "" + +msgid "Bouvet Island" +msgstr "" + +msgid "Botswana" +msgstr "" + +msgid "Belarus" +msgstr "" + +msgid "Belize" +msgstr "" + +msgid "Canada" +msgstr "" + +msgid "Cocos (Keeling) Islands" +msgstr "" + +msgid "Central African Republic" +msgstr "" + +msgid "Congo" +msgstr "" + +msgid "Switzerland" +msgstr "" + +msgid "Ivory Coast" +msgstr "" + +msgid "Cook Iislands" +msgstr "" + +msgid "Chile" +msgstr "" + +msgid "Cameroon" +msgstr "" + +msgid "China" +msgstr "" + +msgid "Colombia" +msgstr "" + +msgid "Costa Rica" +msgstr "" + +msgid "Cuba" +msgstr "" + +msgid "Cape Verde" +msgstr "" + +msgid "Christmas Island" +msgstr "" + +msgid "Cyprus" +msgstr "" + +msgid "Czech Republic" +msgstr "" + +msgid "Germany" +msgstr "" + +msgid "Djibouti" +msgstr "" + +msgid "Denmark" +msgstr "" + +msgid "Dominica" +msgstr "" + +msgid "Dominican Republic" +msgstr "" + +msgid "Algeria" +msgstr "" + +msgid "Ecuador" +msgstr "" + +msgid "Estonia" +msgstr "" + +msgid "Egypt" +msgstr "" + +msgid "Western Sahara" +msgstr "" + +msgid "Eritrea" +msgstr "" + +msgid "Spain" +msgstr "" + +msgid "Ethiopia" +msgstr "" + +msgid "Finland" +msgstr "" + +msgid "Fiji" +msgstr "" + +msgid "Falkland Islands (Malvinas)" +msgstr "" + +msgid "Micronesia" +msgstr "" + +msgid "Faroe Islands" +msgstr "" + +msgid "France" +msgstr "" + +msgid "France, Metropolitan" +msgstr "" + +msgid "Gabon" +msgstr "" + +msgid "United Kingdom (Great Britain)" +msgstr "" + +msgid "Grenada" +msgstr "" + +msgid "Georgia" +msgstr "" + +msgid "French Guiana" +msgstr "" + +msgid "Ghana" +msgstr "" + +msgid "Gibraltar" +msgstr "" + +msgid "Greenland" +msgstr "" + +msgid "Gambia" +msgstr "" + +msgid "Guinea" +msgstr "" + +msgid "Guadeloupe" +msgstr "" + +msgid "Equatorial Guinea" +msgstr "" + +msgid "Greece" +msgstr "" + +msgid "South Georgia and the South Sandwich Islands" +msgstr "" + +msgid "Guatemala" +msgstr "" + +msgid "Guam" +msgstr "" + +msgid "Guinea-Bissau" +msgstr "" + +msgid "Guyana" +msgstr "" + +msgid "Hong Kong" +msgstr "" + +msgid "Heard & McDonald Islands" +msgstr "" + +msgid "Honduras" +msgstr "" + +msgid "Croatia" +msgstr "" + +msgid "Haiti" +msgstr "" + +msgid "Hungary" +msgstr "" + +msgid "Indonesia" +msgstr "" + +msgid "Ireland" +msgstr "" + +msgid "Israel" +msgstr "" + +msgid "India" +msgstr "" + +msgid "British Indian Ocean Territory" +msgstr "" + +msgid "Iraq" +msgstr "" + +msgid "Islamic Republic of Iran" +msgstr "" + +msgid "Iceland" +msgstr "" + +msgid "Italy" +msgstr "" + +msgid "Jamaica" +msgstr "" + +msgid "Jordan" +msgstr "" + +msgid "Japan" +msgstr "" + +msgid "Kenya" +msgstr "" + +msgid "Kyrgyzstan" +msgstr "" + +msgid "Cambodia" +msgstr "" + +msgid "Kiribati" +msgstr "" + +msgid "Comoros" +msgstr "" + +msgid "St. Kitts and Nevis" +msgstr "" + +msgid "Korea, Democratic People's Republic of" +msgstr "" + +msgid "Korea, Republic of" +msgstr "" + +msgid "Kuwait" +msgstr "" + +msgid "Cayman Islands" +msgstr "" + +msgid "Kazakhstan" +msgstr "" + +msgid "Lao People's Democratic Republic" +msgstr "" + +msgid "Lebanon" +msgstr "" + +msgid "Saint Lucia" +msgstr "" + +msgid "Liechtenstein" +msgstr "" + +msgid "Sri Lanka" +msgstr "" + +msgid "Liberia" +msgstr "" + +msgid "Lesotho" +msgstr "" + +msgid "Lithuania" +msgstr "" + +msgid "Luxembourg" +msgstr "" + +msgid "Latvia" +msgstr "" + +msgid "Libyan Arab Jamahiriya" +msgstr "" + +msgid "Morocco" +msgstr "" + +msgid "Monaco" +msgstr "" + +msgid "Moldova, Republic of" +msgstr "" + +msgid "Madagascar" +msgstr "" + +msgid "Marshall Islands" +msgstr "" + +msgid "Mali" +msgstr "" + +msgid "Mongolia" +msgstr "" + +msgid "Myanmar" +msgstr "" + +msgid "Macau" +msgstr "" + +msgid "Northern Mariana Islands" +msgstr "" + +msgid "Martinique" +msgstr "" + +msgid "Mauritania" +msgstr "" + +msgid "Monserrat" +msgstr "" + +msgid "Malta" +msgstr "" + +msgid "Mauritius" +msgstr "" + +msgid "Maldives" +msgstr "" + +msgid "Malawi" +msgstr "" + +msgid "Mexico" +msgstr "" + +msgid "Malaysia" +msgstr "" + +msgid "Mozambique" +msgstr "" + +msgid "Namibia" +msgstr "" + +msgid "New Caledonia" +msgstr "" + +msgid "Niger" +msgstr "" + +msgid "Norfolk Island" +msgstr "" + +msgid "Nigeria" +msgstr "" + +msgid "Nicaragua" +msgstr "" + +msgid "Netherlands" +msgstr "" + +msgid "Norway" +msgstr "" + +msgid "Nepal" +msgstr "" + +msgid "Nauru" +msgstr "" + +msgid "Niue" +msgstr "" + +msgid "New Zealand" +msgstr "" + +msgid "Oman" +msgstr "" + +msgid "Panama" +msgstr "" + +msgid "Peru" +msgstr "" + +msgid "French Polynesia" +msgstr "" + +msgid "Papua New Guinea" +msgstr "" + +msgid "Philippines" +msgstr "" + +msgid "Pakistan" +msgstr "" + +msgid "Poland" +msgstr "" + +msgid "St. Pierre & Miquelon" +msgstr "" + +msgid "Pitcairn" +msgstr "" + +msgid "Puerto Rico" +msgstr "" + +msgid "Portugal" +msgstr "" + +msgid "Palau" +msgstr "" + +msgid "Paraguay" +msgstr "" + +msgid "Qatar" +msgstr "" + +msgid "Reunion" +msgstr "" + +msgid "Romania" +msgstr "" + +msgid "Russian Federation" +msgstr "" + +msgid "Rwanda" +msgstr "" + +msgid "Saudi Arabia" +msgstr "" + +msgid "Solomon Islands" +msgstr "" + +msgid "Seychelles" +msgstr "" + +msgid "Sudan" +msgstr "" + +msgid "Sweden" +msgstr "" + +msgid "Singapore" +msgstr "" + +msgid "St. Helena" +msgstr "" + +msgid "Slovenia" +msgstr "" + +msgid "Svalbard & Jan Mayen Islands" +msgstr "" + +msgid "Slovakia" +msgstr "" + +msgid "Sierra Leone" +msgstr "" + +msgid "San Marino" +msgstr "" + +msgid "Senegal" +msgstr "" + +msgid "Somalia" +msgstr "" + +msgid "Suriname" +msgstr "" + +msgid "Sao Tome & Principe" +msgstr "" + +msgid "El Salvador" +msgstr "" + +msgid "Syrian Arab Republic" +msgstr "" + +msgid "Swaziland" +msgstr "" + +msgid "Turks & Caicos Islands" +msgstr "" + +msgid "Chad" +msgstr "" + +msgid "French Southern Territories" +msgstr "" + +msgid "Togo" +msgstr "" + +msgid "Thailand" +msgstr "" + +msgid "Tajikistan" +msgstr "" + +msgid "Tokelau" +msgstr "" + +msgid "Turkmenistan" +msgstr "" + +msgid "Tunisia" +msgstr "" + +msgid "Tonga" +msgstr "" + +msgid "East Timor" +msgstr "" + +msgid "Turkey" +msgstr "" + +msgid "Trinidad & Tobago" +msgstr "" + +msgid "Tuvalu" +msgstr "" + +msgid "Taiwan, Province of China" +msgstr "" + +msgid "Tanzania, United Republic of" +msgstr "" + +msgid "Ukraine" +msgstr "" + +msgid "Uganda" +msgstr "" + +msgid "United States Minor Outlying Islands" +msgstr "" + +msgid "United States of America" +msgstr "" + +msgid "Uruguay" +msgstr "" + +msgid "Uzbekistan" +msgstr "" + +msgid "Vatican City State (Holy See)" +msgstr "" + +msgid "St. Vincent & the Grenadines" +msgstr "" + +msgid "Venezuela" +msgstr "" + +msgid "British Virgin Islands" +msgstr "" + +msgid "United States Virgin Islands" +msgstr "" + +msgid "Viet Nam" +msgstr "" + +msgid "Vanuatu" +msgstr "" + +msgid "Wallis & Futuna Islands" +msgstr "" + +msgid "Samoa" +msgstr "" + +msgid "Yemen" +msgstr "" + +msgid "Mayotte" +msgstr "" + +msgid "Yugoslavia" +msgstr "" + +msgid "South Africa" +msgstr "" + +msgid "Zambia" +msgstr "" + +msgid "Zaire" +msgstr "" + +msgid "Zimbabwe" +msgstr "" + +msgid "Unknown or unspecified country" +msgstr "" + +msgid "Enter your name or company name" +msgstr "Geben Sie Ihren Namen oder der Ihrer Firma ein" + +msgid "Your username and/or password were incorrect." +msgstr "Dein Benutzername und/oder Dein Passwort ist falsch." + +msgid "User does not exist" +msgstr "Der Benutzer existiert nicht" + +msgid "The account is already active." +msgstr "Das Benutzerkonto ist bereits aktiv." + +msgid "The two password fields didn't match." +msgstr "Die beiden Passwörter stimmen nicht überein." + +msgid "New password" +msgstr "Neues Passwort" + +msgid "New password confirmation" +msgstr "Neues Passwort Bestätigung" + +msgid "Cardholder Name" +msgstr "Name des Kartenbesitzer" + +msgid "Street Address" +msgstr "" + +msgid "City" +msgstr "" + +msgid "Postal Code" +msgstr "" + +msgid "Country" +msgstr "" + +msgid "Street Building" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Phone number" +msgstr "Telefon" + +msgid "Message" +msgstr "Nachricht" + +msgid "An email with the activation link has been sent to your email" +msgstr "" +"Der Link zum Zurücksetzen deines Passwortes wurde an deine E-Mail gesendet" + +msgid "Account Activation" +msgstr "Accountaktivierung" + +msgid "The link to reset your email has been sent to your email" +msgstr "" +"Der Link zum Zurücksetzen deines Passwortes wurde an deine E-Mail gesendet" + +msgid "Password Reset" +msgstr "Passwort zurücksetzen" + +msgid "Password has been reset." +msgstr "Das Passwort wurde zurückgesetzt." + +msgid "Password reset has not been successful." +msgstr "Das Zurücksetzen war nicht erfolgreich." + +msgid "The reset password link is no longer valid." +msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht länger gültig." diff --git a/utils/tasks.py b/utils/tasks.py new file mode 100644 index 00000000..23deac51 --- /dev/null +++ b/utils/tasks.py @@ -0,0 +1,98 @@ +import tempfile + +import cdist +from cdist.integration import configure_hosts_simple +from celery.result import AsyncResult +from celery import current_task +from celery.utils.log import get_task_logger +from django.conf import settings +from django.core.mail import EmailMessage + +from dynamicweb.celery import app + +logger = get_task_logger(__name__) + + +@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES) +def send_plain_email_task(self, email_data): + """ + This is a generic celery task to be used for sending emails. + A celery wrapper task for EmailMessage + + :param self: + :param email_data: A dict of all needed email headers + :return: + """ + email = EmailMessage(**email_data) + email.send() + + +@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES) +def save_ssh_key(self, hosts, keys): + """ + Saves ssh key into the VMs of a user using cdist + + :param hosts: A list of hosts to be configured + :param keys: A list of keys to be added. A key should be dict of the + form { + 'value': 'sha-.....', # public key as string + 'state': True # whether key is to be added or + } # removed + """ + logger.debug( + "Running save_ssh_key on {}".format(current_task.request.hostname)) + logger.debug("""Running save_ssh_key task for + Hosts: {hosts_str} + Keys: {keys_str}""".format(hosts_str=", ".join(hosts), + keys_str=", ".join([ + "{value}->{state}".format( + value=key.get('value'), + state=str( + key.get('state'))) + for key in keys])) + ) + return_value = True + with tempfile.NamedTemporaryFile(delete=True) as tmp_manifest: + # Generate manifest to be used for configuring the hosts + lines_list = [ + ' --key "{key}" --state {state} \\\n'.format( + key=key['value'], + state='present' if key['state'] else 'absent' + ).encode('utf-8') + for key in keys] + lines_list.insert(0, b'__ssh_authorized_keys root \\\n') + tmp_manifest.writelines(lines_list) + tmp_manifest.flush() + try: + configure_hosts_simple(hosts, + tmp_manifest.name, + verbose=cdist.argparse.VERBOSE_TRACE) + except Exception as cdist_exception: + logger.error(cdist_exception) + return_value = False + email_data = { + 'subject': "celery save_ssh_key error - task id {0}".format( + self.request.id.__str__()), + 'from_email': current_task.request.hostname, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, + 'body': "Task Id: {0}\nResult: {1}\nTraceback: {2}".format( + self.request.id.__str__(), False, str(cdist_exception)), + } + send_plain_email_task(email_data) + return return_value + + +@app.task +def save_ssh_key_error_handler(uuid): + result = AsyncResult(uuid) + exc = result.get(propagate=False) + logger.error('Task {0} raised exception: {1!r}\n{2!r}'.format( + uuid, exc, result.traceback)) + email_data = { + 'subject': "[celery error] Save SSH key error {0}".format(uuid), + 'from_email': current_task.request.hostname, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, + 'body': "Task Id: {0}\nResult: {1}\nTraceback: {2}".format( + uuid, exc, result.traceback), + } + send_plain_email_task(email_data) diff --git a/utils/tests.py b/utils/tests.py index c4608e73..d5c2d726 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -1,16 +1,20 @@ import uuid +from time import sleep from unittest.mock import patch import stripe +from celery.result import AsyncResult +from django.conf import settings from django.http.request import HttpRequest from django.test import Client -from django.test import TestCase +from django.test import TestCase, override_settings +from unittest import skipIf from model_mommy import mommy from datacenterlight.models import StripePlan from membership.models import StripeCustomer from utils.stripe_utils import StripeUtils -from django.conf import settings +from .tasks import save_ssh_key class BaseTestCase(TestCase): @@ -235,3 +239,57 @@ class StripePlanTestCase(TestStripeCustomerDescription): 'response_object').stripe_plan_id}]) self.assertIsNone(result.get('response_object'), None) self.assertIsNotNone(result.get('error')) + + +class SaveSSHKeyTestCase(TestCase): + """ + A test case to test the celery save_ssh_key task + """ + + @override_settings( + task_eager_propagates=True, + task_always_eager=True, + ) + def setUp(self): + self.public_key = settings.TEST_MANAGE_SSH_KEY_PUBKEY + self.hosts = settings.TEST_MANAGE_SSH_KEY_HOST + + @skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY is None or + settings.TEST_MANAGE_SSH_KEY_PUBKEY == "" or + settings.TEST_MANAGE_SSH_KEY_HOST is None or + settings.TEST_MANAGE_SSH_KEY_HOST is "", + """Skipping test_save_ssh_key_add because either host + or public key were not specified or were empty""") + def test_save_ssh_key_add(self): + async_task = save_ssh_key.delay([self.hosts], + [{'value': self.public_key, + 'state': True}]) + save_ssh_key_result = None + for i in range(0, 10): + sleep(5) + res = AsyncResult(async_task.task_id) + if type(res.result) is bool: + save_ssh_key_result = res.result + break + self.assertIsNotNone(save_ssh_key, "save_ssh_key_result is None") + self.assertTrue(save_ssh_key_result, "save_ssh_key_result is False") + + @skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY is None or + settings.TEST_MANAGE_SSH_KEY_PUBKEY == "" or + settings.TEST_MANAGE_SSH_KEY_HOST is None or + settings.TEST_MANAGE_SSH_KEY_HOST is "", + """Skipping test_save_ssh_key_add because either host + or public key were not specified or were empty""") + def test_save_ssh_key_remove(self): + async_task = save_ssh_key.delay([self.hosts], + [{'value': self.public_key, + 'state': False}]) + save_ssh_key_result = None + for i in range(0, 10): + sleep(5) + res = AsyncResult(async_task.task_id) + if type(res.result) is bool: + save_ssh_key_result = res.result + break + self.assertIsNotNone(save_ssh_key, "save_ssh_key_result is None") + self.assertTrue(save_ssh_key_result, "save_ssh_key_result is False") diff --git a/utils/views.py b/utils/views.py index 039e08d8..4ec39bce 100644 --- a/utils/views.py +++ b/utils/views.py @@ -1,15 +1,17 @@ -from django.views.generic import FormView, CreateView -from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.conf import settings from django.contrib import messages -from django.contrib.auth.tokens import default_token_generator -from django.utils.encoding import force_bytes -from django.http import HttpResponseRedirect from django.contrib.auth import authenticate, login +from django.contrib.auth.tokens import default_token_generator +from django.core.urlresolvers import reverse_lazy +from django.http import HttpResponseRedirect +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import FormView, CreateView from membership.models import CustomUser - -from .mailer import BaseEmail from .forms import SetPasswordForm +from .mailer import BaseEmail class SignupViewMixin(CreateView): @@ -17,8 +19,8 @@ class SignupViewMixin(CreateView): success_url = None def get_success_url(self): - - next_url = self.request.POST.get('next') if self.request.POST.get('next')\ + next_url = self.request.POST.get('next') if self.request.POST.get( + 'next') \ else self.success_url return next_url @@ -62,42 +64,75 @@ class LoginViewMixin(FormView): return super(LoginViewMixin, self).get(request, *args, **kwargs) +class ResendActivationLinkViewMixin(FormView): + success_message = _( + "An email with the activation link has been sent to your email") + + def generate_email_context(self, user): + context = { + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()), + 'activation_link': reverse_lazy( + 'hosting:validate', + kwargs={'validate_slug': user.validation_slug} + ), + 'dcl_text': settings.DCL_TEXT, + } + return context + + def form_valid(self, form): + email = form.cleaned_data.get('email') + user = CustomUser.objects.get(email=email) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) + context = self.generate_email_context(user) + email_data = { + 'subject': '{dcl_text} {account_activation}'.format( + dcl_text=settings.DCL_TEXT, + account_activation=_('Account Activation') + ), + 'to': email, + 'context': context, + 'template_name': self.email_template_name, + 'template_path': self.email_template_path, + 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS + } + email = BaseEmail(**email_data) + email.send() + return HttpResponseRedirect(self.get_success_url()) + + class PasswordResetViewMixin(FormView): - # template_name = 'hosting/reset_password.html' - # form_class = PasswordResetRequestForm - success_message = "The link to reset your email has been sent to your email" + success_message = _( + "The link to reset your email has been sent to your email") site = '' - success_message = "Thank you! You will shortly receive a password reset mail from us" - # success_url = reverse_lazy('hosting:login') def test_generate_email_context(self, user): context = { 'user': user, 'token': default_token_generator.make_token(user), 'uid': urlsafe_base64_encode(force_bytes(user.pk)), - 'site_name': 'ungleich', - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) - + 'site_name': 'ungleich' if self.site != 'dcl' else settings.DCL_TEXT, + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()) } return context def form_valid(self, form): - email = form.cleaned_data.get('email') user = CustomUser.objects.get(email=email) - - messages.add_message(self.request, messages.SUCCESS, self.success_message) - + messages.add_message(self.request, messages.SUCCESS, + self.success_message) context = self.test_generate_email_context(user) email_data = { - 'subject': 'Password Reset', + 'subject': _('Password Reset'), 'to': email, 'context': context, 'template_name': 'password_reset_email', 'template_path': self.template_email_path } if self.site == 'dcl': - email_data['from_address'] = '(Data Center Light) Data Center Light Support ' + email_data['from_address'] = settings.DCL_SUPPORT_FROM_ADDRESS email = BaseEmail(**email_data) email.send() @@ -105,9 +140,7 @@ class PasswordResetViewMixin(FormView): class PasswordResetConfirmViewMixin(FormView): - # template_name = 'hosting/confirm_reset_password.html' form_class = SetPasswordForm - # success_url = reverse_lazy('hosting:login') def post(self, request, uidb64=None, token=None, *arg, **kwargs): try: @@ -118,19 +151,24 @@ class PasswordResetConfirmViewMixin(FormView): form = self.form_class(request.POST) - if user is not None and default_token_generator.check_token(user, token): + if user is not None and default_token_generator.check_token(user, + token): if form.is_valid(): new_password = form.cleaned_data['new_password2'] user.set_password(new_password) user.save() - messages.success(request, 'Password has been reset.') + messages.success(request, _('Password has been reset.')) return self.form_valid(form) else: - messages.error(request, 'Password reset has not been successful.') - form.add_error(None, 'Password reset has not been successful.') + messages.error(request, + _('Password reset has not been successful.')) + form.add_error(None, + _('Password reset has not been successful.')) return self.form_invalid(form) else: - messages.error(request, 'The reset password link is no longer valid.') - form.add_error(None, 'The reset password link is no longer valid.') + messages.error(request, + _('The reset password link is no longer valid.')) + form.add_error(None, + _('The reset password link is no longer valid.')) return self.form_invalid(form)