diff --git a/.gitignore b/.gitignore index 2bfa8cdf..46bfbf54 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ secret-key .env *.mo +*.log diff --git a/Changelog b/Changelog index 5ab95416..1f5b70e6 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,43 @@ +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 + * #3565: [datacenterlight, hosting] Changed warning text color + * #3622: [datacenterlight] Moved the create vm xml-rpc call made in the DCL VM purchase flow into a celery asynchronous task + [datacenterlight] Added test for create vm celery task + * #3711: [hosting] Displayed all IPv4s and IPv6s in the VM list + * #3697: [hosting] Applied new design for VM detail page + * #3645: [hosting] Fixed navbar movement on modal popup + * #3698: [hosting] Applied new design for My Orders page + * #3737: [all] Corrected/added missing google analytics and reformated code, fixed broken head tag + * #3701: [datacenterlight] Enabled monthly Stripe subscriptions +1.0.24: 2017-08-15 + * #3699: [datacenterlight] Added oneadmin ssh key by default to the created VM via DCL landing + * #3687: [datacenterlight] Added the name of the customer as description field of the stripe metadata + [all] Added CustomUser as a parameter in get_anonymous_user function to resolve issues with tests +1.0.23: 2017-08-11 + * #3629: [datacentlight] Fixed navbar changing language from DE to EN between menus bug + * #3623: [hosting] Fixed “Confirm Order” text appearing in “Invoice” place + * #3633: [datacenterlight, hosting] Translated “All Rights Reserved” for German pages + * #3627: [datacenterlight, hosting] Added border for payment warning message when the user has already submitted card information + * #3620: [hosting] Updated SSH Key page with new style: new key choice page, upload key page, added icons for downloading and deleting key on mobile + * [hosting] bug fix: added modal icon and translation back for delete SSH Key + * #3660: [datacenterlight] Rearranged desktop and mobile view for “Why Data Centre Light?” IPv6/SSD section + * #3646: Added file with VM Template hosting migration + * #3617: [hosting] Fixed Password reset confirmation page style bug + * #3408: [hosting] Changed background image of signup/login background into smaller size + * #3621: [hosting] Fixed signup/login/password reset page navbar logo overlapping with form + * #3354: [hosting] Restyled modal + * #3638: [hosting] Added “download” btn on generated key list for generated keys from upload your key page + * #3655: [hosting] Disabled deleting SSH keys from other users + * #3619: [datacenterlight, hosting] Replaced 'Lato-Light' and 'Lato-Regular' with only ‘Lato’ with appropriate font-weights + * #3677: [hosting] Added wrapping for show SSH key modal text + * #3683: [hosting] Fixed footer floating bug on VM creating page + * #3676: [datacenterlight, hosting] Added missing card holder's name field migration 1.0.22: 2017-07-30 * #3593: [datacenterlight] Removed underbars between social icons in index * #3509: [datacenterlight, hosting] Made navbar transparent and removed mobile navbar bug in login/signup/reset-password 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 6511367f..c0dc55ca 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-03 03:10+0530\n" +"POT-Creation-Date: 2017-08-24 11:28+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -82,6 +82,24 @@ 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 "Thank you for your request." msgstr "Vielen Dank für Ihre Anfrage." @@ -234,15 +252,12 @@ 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" -msgid "Questions?" -msgstr "Fragen?" - -msgid "Contact us!" -msgstr "Kontaktiere uns!" - msgid "Confirm Order" msgstr "Bestellung Bestätigen" @@ -276,6 +291,19 @@ msgstr "Konfiguration" msgid "Total" msgstr "" +#, fuzzy +#| msgid "month" +msgid "Month" +msgstr "Monat" + +#, 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" @@ -399,6 +427,9 @@ msgstr "ist kein gültiger Name" msgid "is not a proper email" msgstr "ist keine gültige E-Mailadresse" +#~ 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." 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/0007_stripeplan.py b/datacenterlight/migrations/0007_stripeplan.py new file mode 100644 index 00000000..95892205 --- /dev/null +++ b/datacenterlight/migrations/0007_stripeplan.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-16 19:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0006_vmtemplate'), + ] + + operations = [ + migrations.CreateModel( + name='StripePlan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_plan_id', models.CharField(max_length=100, null=True)), + ], + ), + ] diff --git a/datacenterlight/migrations/0008_auto_20170821_2024.py b/datacenterlight/migrations/0008_auto_20170821_2024.py new file mode 100644 index 00000000..5357a404 --- /dev/null +++ b/datacenterlight/migrations/0008_auto_20170821_2024.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-21 20:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0007_stripeplan'), + ] + + operations = [ + migrations.AlterField( + model_name='stripeplan', + name='stripe_plan_id', + field=models.CharField(max_length=256, null=True), + ), + ] 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 fdfebc96..e2de41e1 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -57,5 +57,25 @@ 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 + + +class StripePlan(models.Model): + """ + A model to store Data Center Light's created Stripe plans + """ + stripe_plan_id = models.CharField(max_length=256, null=True) + + @classmethod + 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 6c813661..9d83a88f 100755 --- a/datacenterlight/static/datacenterlight/css/landing-page.css +++ b/datacenterlight/static/datacenterlight/css/landing-page.css @@ -4,10 +4,10 @@ * For details, see http://www.apache.org/licenses/LICENSE-2.0. */ -@font-face { +/*@font-face { font-family: 'Lato-Light'; src: url('../fonts/Lato/Lato-Light.ttf'); -} +}*/ body, html { @@ -22,7 +22,12 @@ h3, h4, h5, h6 { - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-family: 'Lato', sans-serif; + font-weight: 300; +} + +button, input, optgroup, select, textarea { font-weight: 300; } @@ -143,13 +148,15 @@ h6 { .navbar-default .navbar-nav>li>a { cursor: pointer; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .navbar-transparent .navbar-nav>li>a { color: #fff; cursor: pointer; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .navbar-transparent .navbar-nav>li>a:hover { @@ -202,13 +209,15 @@ h6 { .navbar-transparent .nav-language .select-language { color: #fff; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .nav-language .select-language span { margin-left: 5px; margin-right: 5px; - font-family: 'Lato', sans-serif; + /*font-family: 'Lato', sans-serif;*/ + font-weight: normal; } .nav-language .drop-language{ /*position: absolute;*/ @@ -237,7 +246,8 @@ h6 { .nav-language .drop-language a{ cursor: pointer; padding: 5px 10px !important; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } /* Show the dropdown menu on hover */ @@ -260,7 +270,8 @@ h6 { .navbar-transparent .nav-language .drop-language a { color: #fff; padding: 5px 10px !important; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } /* .nav-language:hover .drop-language{ display: block; @@ -312,9 +323,9 @@ h6 { 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; } @@ -343,7 +354,7 @@ h6 { .intro-message>h1 { margin: 0; - font-weight: 400; + font-weight: 300; font-size: 6em; } @@ -643,74 +654,161 @@ h6 { 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*/ @@ -792,7 +890,8 @@ tech-sub-sec h2 { } .percent-text { - font-family: 'Lato', sans-serif; + /*font-family: 'Lato', sans-serif;*/ +/* font-weight: normal; */ font-size: 50px; color: #999; } @@ -879,7 +978,7 @@ tech-sub-sec h2 { .dropdown-menu>li>a { font-size: 13px; font-weight: 300; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ } .navbar-default .navbar-nav>.active>a, @@ -898,7 +997,8 @@ tech-sub-sec h2 { background: -webkit-linear-gradient(top, #f0f4f7, #fff) no-repeat; background: linear-gradient(to bottom, #f0f4f7, #fff) no-repeat; display: flex; - font-family: 'Lato', sans-serif; + /*font-family: 'Lato', sans-serif;*/ +/* font-weight: normal; */ } .price-calc-section .text { @@ -963,7 +1063,8 @@ tech-sub-sec h2 { } .price-calc-section .card .title h3 { - font-family: 'Lato', sans-serif; + /*font-family: 'Lato', sans-serif;*/ + font-weight: normal; } .price-calc-section .card .price { @@ -1050,8 +1151,9 @@ tech-sub-sec h2 { .price-calc-section .card .description.input label { font-size: 15px; - font-weight: 800; - font-family: 'Lato'; + font-weight: 700; + /*font-weight: 800;*/ + /*font-family: 'Lato';*/ margin-bottom: 0; width: 40px; } @@ -1081,19 +1183,6 @@ tech-sub-sec h2 { padding: 0; } -.has-error .checkbox, -.has-error .checkbox-inline, -.has-error .control-label, -.has-error .help-block, -.has-error .radio, -.has-error .radio-inline, -.has-error.checkbox label, -.has-error.checkbox-inline label, -.has-error.radio label, -.has-error.radio-inline label { - color: #eb4d5c; -} - .form-group { margin: 0; border-bottom: 1px solid rgba(128, 128, 128, 0.3); @@ -1309,9 +1398,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 { @@ -1364,7 +1453,8 @@ tech-sub-sec h2 { padding: 30px; } .percent-text { - font-family: 'Lato'; + /*font-family: 'Lato';*/ + font-weight: normal; font-size: 37px; /* text-align: center; */ } @@ -1402,7 +1492,7 @@ tech-sub-sec h2 { .network-name { text-transform: uppercase; font-size: 14px; - font-weight: 400; + font-weight: 300; letter-spacing: 2px; } @@ -1519,4 +1609,40 @@ a#forgotpassword { .w380 { max-width: 380px !important; -} \ No newline at end of file +} + +/* bootstrap danger color override from #a94442 */ +.text-danger, +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.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, +a.list-group-item-danger, +a.list-group-item-danger:hover, +a.list-group-item-danger:focus, +.panel-danger > .panel-heading { + color: #eb4d5c; +} +.has-error .input-group-addon { + color: #eb4d5c; + border-color: #eb4d5c; +} +a.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus { + background-color: #eb4d5c; + border-color: #eb4d5c; +} +.panel-danger > .panel-heading .badge { + background-color: #eb4d5c; +} 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 @@ + + + + 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 2c5333ae..79422844 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() { @@ -157,4 +157,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 new file mode 100644 index 00000000..1e3e1caa --- /dev/null +++ b/datacenterlight/tasks.py @@ -0,0 +1,174 @@ +from dynamicweb.celery import app +from celery.utils.log import get_task_logger +from django.conf import settings +from opennebula_api.models import OpenNebulaManager +from opennebula_api.serializers import VirtualMachineSerializer +from hosting.models import HostingOrder, HostingBill +from utils.forms import UserBillingAddressForm +from datetime import datetime +from membership.models import StripeCustomer +from django.core.mail import EmailMessage +from utils.models import BillingAddress +from celery.exceptions import MaxRetriesExceededError + +logger = get_task_logger(__name__) + + +def retry_task(task, exception=None): + """Retries the specified task using a "backing off countdown", + meaning that the interval between retries grows exponentially + with every retry. + + Arguments: + task: + The task to retry. + + exception: + Optionally, the exception that caused the retry. + """ + + def backoff(attempts): + return 2 ** attempts + + kwargs = { + 'countdown': backoff(task.request.retries), + } + + if exception: + kwargs['exc'] = exception + + raise task.retry(**kwargs) + + +@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES) +def create_vm_task(self, vm_template_id, user, specs, template, + stripe_customer_id, billing_address_data, + billing_address_id, + charge, cc_details): + 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 + 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"))) + ) + + if vm_id is None: + raise Exception("Could not create VM") + + # 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 subscription + charge_object = DictDotLookup(charge) + order.set_subscription_id(charge_object, cc_details) + + # If the Stripe payment succeeds, set order status approved + order.set_approved() + + vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data + + context = { + 'name': user.get('name'), + 'email': user.get('email'), + 'cores': specs.get('cpu'), + 'memory': specs.get('memory'), + 'storage': specs.get('disk_size'), + 'price': specs.get('price'), + 'template': template.get('name'), + 'vm.name': vm['name'], + 'vm.id': vm['vm_id'], + 'order.id': order.id + } + email_data = { + 'subject': settings.DCL_TEXT + " Order from %s" % context['email'], + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': ['info@ungleich.ch'], + 'body': "\n".join( + ["%s=%s" % (k, v) for (k, v) in context.items()]), + 'reply_to': [context['email']], + } + email = EmailMessage(**email_data) + email.send() + except Exception as e: + logger.error(str(e)) + try: + retry_task(self) + except MaxRetriesExceededError: + msg_text = 'Finished {} retries for create_vm_task'.format( + self.request.retries) + logger.error(msg_text) + # Try sending email and stop + email_data = { + 'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT, + msg_text), + 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, + 'to': ['info@ungleich.ch'], + 'body': ',\n'.join(str(i) for i in self.request.args) + } + email = EmailMessage(**email_data) + email.send() + return + + return vm_id + + +class DictDotLookup(object): + """ + Creates objects that behave much like a dictionaries, but allow nested + key access using object '.' (dot) lookups. + """ + + def __init__(self, d): + for k in d: + if isinstance(d[k], dict): + self.__dict__[k] = DictDotLookup(d[k]) + elif isinstance(d[k], (list, tuple)): + l = [] + for v in d[k]: + if isinstance(v, dict): + l.append(DictDotLookup(v)) + else: + l.append(v) + self.__dict__[k] = l + else: + self.__dict__[k] = d[k] + + def __getitem__(self, name): + if name in self.__dict__: + return self.__dict__[name] + + def __iter__(self): + return iter(self.__dict__.keys()) diff --git a/datacenterlight/templates/datacenterlight/base.html b/datacenterlight/templates/datacenterlight/base.html index fc69a2d5..671d894e 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 %} @@ -33,13 +33,15 @@ + {% render_block "css" postprocessor "compressor.contrib.sekizai.compress" %} + {% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %} {% include "google_analytics.html" %} - + {% cms_toolbar %} {% include "datacenterlight/includes/_navbar.html" %} diff --git a/datacenterlight/templates/datacenterlight/cms_page.html b/datacenterlight/templates/datacenterlight/cms_page.html new file mode 100644 index 00000000..f42528e4 --- /dev/null +++ b/datacenterlight/templates/datacenterlight/cms_page.html @@ -0,0 +1,33 @@ +{% extends "datacenterlight/base.html" %} +{% load staticfiles cms_tags sekizai_tags %} +{% block content %} +{% addtoblock "css" %} + +{% endaddtoblock %} + +
+
+
+
+
+
+

{% placeholder 'datacenterlight_cms_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/includes/_navbar.html b/datacenterlight/templates/datacenterlight/includes/_navbar.html index e1b65fba..0cf4908e 100644 --- a/datacenterlight/templates/datacenterlight/includes/_navbar.html +++ b/datacenterlight/templates/datacenterlight/includes/_navbar.html @@ -58,4 +58,4 @@ - \ No newline at end of file + diff --git a/datacenterlight/templates/datacenterlight/index.html b/datacenterlight/templates/datacenterlight/index.html index 3ddb516d..3f89055c 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 %} @@ -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/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index f3c946a1..b55953bf 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -72,15 +72,20 @@

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


-

{% trans "Total"%}

{{vm.price}} CHF

+

{% trans "Total"%}

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

{% endwith %}

- {% csrf_token %} - + {% 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 a79ca8be..7c2f7353 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -1,3 +1,143 @@ # from django.test import TestCase -# Create your tests here. +from time import sleep + +import stripe +from celery.result import AsyncResult +from django.conf import settings +from django.core.management import call_command +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 +from opennebula_api.serializers import VMTemplateSerializer +from utils.models import BillingAddress +from utils.stripe_utils import StripeUtils + + +class CeleryTaskTestCase(TestCase): + @override_settings( + task_eager_propagates=True, + task_always_eager=True, + ) + def setUp(self): + self.customer_password = 'test_password' + self.customer_email = 'celery-createvm-task-test@ungleich.ch' + self.customer_name = "Monty Python" + self.user = { + 'email': self.customer_email, + 'name': self.customer_name + } + self.customer = mommy.make('membership.CustomUser') + self.customer.set_password(self.customer_password) + self.customer.email = self.customer_email + self.customer.save() + self.stripe_utils = StripeUtils() + stripe.api_key = settings.STRIPE_API_PRIVATE_KEY_TEST + self.token = stripe.Token.create( + card={ + "number": '4111111111111111', + "exp_month": 12, + "exp_year": 2022, + "cvc": '123' + }, + ) + # Run fetchvmtemplates so that we have the VM templates from + # OpenNebula + call_command('fetchvmtemplates') + + def test_create_vm_task(self): + """Tests the create vm task for monthly subscription + + This test is supposed to validate the proper execution + of celery create_vm_task on production, as we have no + other way to do this. + """ + + # We create a VM from the first template available to DCL + vm_template = VMTemplate.objects.all().first() + template_data = VMTemplateSerializer(vm_template).data + + # The specs of VM that we want to create + specs = { + 'cpu': 1, + 'memory': 2, + 'disk_size': 10, + 'price': 15 + } + + stripe_customer = StripeCustomer.get_or_create( + email=self.customer_email, + token=self.token) + card_details = self.stripe_utils.get_card_details( + stripe_customer.stripe_id, + self.token) + card_details_dict = card_details.get('response_object') + billing_address = BillingAddress( + cardholder_name=self.customer_name, + postal_code='1232', + country='CH', + street_address='Monty\'s Street', + city='Hollywood') + billing_address.save() + billing_address_data = {'cardholder_name': self.customer_name, + 'postal_code': '1231', + 'country': 'CH', + 'token': self.token, + 'street_address': 'Monty\'s Street', + 'city': 'Hollywood'} + + billing_address_id = billing_address.id + vm_template_id = template_data.get('id', 1) + + 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 = self.stripe_utils.get_or_create_stripe_plan( + amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id) + subscription_result = self.stripe_utils.subscribe_customer_to_plan( + stripe_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') + raise Exception("Creating subscription failed: {}".format(msg)) + + async_task = create_vm_task.delay(vm_template_id, self.user, + specs, + template_data, + stripe_customer.id, + billing_address_data, + billing_address_id, + stripe_subscription_obj, + card_details_dict) + new_vm_id = 0 + res = None + for i in range(0, 10): + sleep(5) + res = AsyncResult(async_task.task_id) + if res.result is not None and res.result > 0: + new_vm_id = res.result + break + + # We expect a VM to be created within 50 seconds + self.assertGreater(new_vm_id, 0, + "VM could not be created. res._get_task_meta() = {}" + .format(res._get_task_meta())) diff --git a/datacenterlight/urls.py b/datacenterlight/urls.py index a3aed7a6..5c9ffcd7 100644 --- a/datacenterlight/urls.py +++ b/datacenterlight/urls.py @@ -1,17 +1,24 @@ 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'^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 1be73eda..76da4b4e 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 django.core.mail import EmailMessage -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, UserBillingAddressForm -from utils.models import BillingAddress -from hosting.models import HostingOrder, HostingBill -from utils.stripe_utils import StripeUtils -from datetime import datetime +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 membership.models import CustomUser, StripeCustomer from opennebula_api.models import OpenNebulaManager -from opennebula_api.serializers import VirtualMachineTemplateSerializer, VirtualMachineSerializer, VMTemplateSerializer +from opennebula_api.serializers import VirtualMachineTemplateSerializer, \ + VMTemplateSerializer +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): @@ -33,13 +74,14 @@ class SuccessView(TemplateView): def get(self, request, *args, **kwargs): if 'specs' not in request.session or 'user' not in request.session: return HttpResponseRedirect(reverse('datacenterlight:index')) - elif 'token' not in request.session: return HttpResponseRedirect(reverse('datacenterlight:payment')) elif 'order_confirmation' not in request.session: - return HttpResponseRedirect(reverse('datacenterlight:order_confirmation')) + return HttpResponseRedirect( + reverse('datacenterlight:order_confirmation')) else: - for session_var in ['specs', 'user', 'template', 'billing_address', 'billing_address_data', + for session_var in ['specs', 'user', 'template', 'billing_address', + 'billing_address_data', 'token', 'customer']: if session_var in request.session: del request.session[session_var] @@ -55,7 +97,8 @@ class PricingView(TemplateView): templates = manager.get_templates() context = { - 'templates': VirtualMachineTemplateSerializer(templates, many=True).data, + 'templates': VirtualMachineTemplateSerializer(templates, + many=True).data, } except: messages.error(request, @@ -102,7 +145,8 @@ class BetaAccessView(FormView): def form_valid(self, form): context = { - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()) } email_data = { @@ -132,8 +176,8 @@ class BetaAccessView(FormView): email = BaseEmail(**email_data) email.send() - messages.add_message( - self.request, messages.SUCCESS, self.success_message) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) return render(self.request, 'datacenterlight/beta_success.html', {}) @@ -158,7 +202,8 @@ class BetaProgramView(CreateView): # data = VirtualMachineTemplateSerializer(templates, many=True).data context.update({ - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()), + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()), 'vms': vms }) return context @@ -168,7 +213,8 @@ class BetaProgramView(CreateView): vms = BetaAccessVM.create(data) context = { - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()), + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()), 'email': data.get('email'), 'name': data.get('name'), 'vms': vms @@ -185,8 +231,8 @@ class BetaProgramView(CreateView): email = BaseEmail(**email_data) email.send() - messages.add_message( - self.request, messages.SUCCESS, self.success_message) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) return HttpResponseRedirect(self.get_success_url()) @@ -243,41 +289,46 @@ class IndexView(CreateView): 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") + 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") + 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") + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='storage') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") try: name = name_field.clean(name) except ValidationError as err: msg = '{} {}.'.format(name, _('is not a proper name')) - messages.add_message( - self.request, messages.ERROR, msg, extra_tags='name') - return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='name') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") try: email = email_field.clean(email) except ValidationError as err: msg = '{} {}.'.format(email, _('is not a proper email')) - messages.add_message( - self.request, messages.ERROR, msg, extra_tags='email') - return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='email') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") specs = { 'cpu': cores, @@ -304,14 +355,17 @@ class IndexView(CreateView): def get_context_data(self, **kwargs): context = super(IndexView, self).get_context_data(**kwargs) context.update({ - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()), + 'contact_form': ContactForm }) return context def form_valid(self, form): context = { - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()) } email_data = { @@ -341,8 +395,8 @@ class IndexView(CreateView): email = BaseEmail(**email_data) email.send() - messages.add_message( - self.request, messages.SUCCESS, self.success_message) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) return super(IndexView, self).form_valid(form) @@ -407,16 +461,17 @@ 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() - 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(reverse('datacenterlight:order_confirmation')) + return HttpResponseRedirect( + reverse('datacenterlight:order_confirmation')) else: return self.form_invalid(form) @@ -436,8 +491,15 @@ class OrderConfirmationView(DetailView): stripe_customer_id = request.session.get('customer') customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() stripe_utils = StripeUtils() - card_details = stripe_utils.get_card_details( - customer.stripe_id, request.session.get('token')) + 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') context = { 'site_url': reverse('datacenterlight:index'), 'cc_last4': card_details.get('response_object').get('last4'), @@ -453,91 +515,54 @@ class OrderConfirmationView(DetailView): 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') - billing_address = BillingAddress.objects.filter( - id=billing_address_id).first() vm_template_id = template.get('id', 1) - final_price = specs.get('price') # Make stripe charge to a customer stripe_utils = StripeUtils() - charge_response = stripe_utils.make_charge(amount=final_price, - customer=customer.stripe_id) - charge = charge_response.get('response_object') + 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) - # Check if the payment was approved - if not charge: - context = {} - context.update({ - 'paymentError': charge_response.get('error') - }) - return render(request, self.payment_template_name, context) - - charge = charge_response.get('response_object') - - # Create OpenNebulaManager - manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME, - password=settings.OPENNEBULA_PASSWORD) - - # Create a vm using oneadmin, also specify the name - vm_id = manager.create_vm( - template_id=vm_template_id, - specs=specs, - vm_name="{email}-{template_name}-{date}".format( - email=user.get('email'), - template_name=template.get('name'), - date=int(datetime.now().strftime("%s"))) - ) - - # 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 - - context = { - 'name': user.get('name'), - 'email': user.get('email'), - 'cores': specs.get('cpu'), - 'memory': specs.get('memory'), - 'storage': specs.get('disk_size'), - 'price': specs.get('price'), - 'template': template.get('name'), - 'vm.name': vm['name'], - 'vm.id': vm['vm_id'], - 'order.id': order.id - } - email_data = { - 'subject': settings.DCL_TEXT + " Order from %s" % context['email'], - 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, - 'to': ['info@ungleich.ch'], - 'body': "\n".join(["%s=%s" % (k, v) for (k, v) in context.items()]), - 'reply_to': [context['email']], - } - email = EmailMessage(**email_data) - email.send() + 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('datacenterlight:payment') + '#payment_error') + create_vm_task.delay(vm_template_id, user, specs, template, + stripe_customer_id, billing_address_data, + billing_address_id, + stripe_subscription_obj, card_details_dict) request.session['order_confirmation'] = True return HttpResponseRedirect(reverse('datacenterlight:order_success')) diff --git a/deploy.sh b/deploy.sh index f2a1d59e..04a7b04c 100755 --- a/deploy.sh +++ b/deploy.sh @@ -13,6 +13,7 @@ while true; do case "$1" in -h | --help ) HELP=true; shift ;; -v | --verbose ) VERBOSE=true; shift ;; + -D | --dbmakemigrations ) DB_MAKE_MIGRATIONS=true; shift ;; -d | --dbmigrate ) DB_MIGRATE=true; shift ;; -n | --nogit ) NO_GIT=true; shift ;; -b | --branch ) BRANCH="$2"; shift 2 ;; @@ -31,13 +32,15 @@ if [ "$HELP" == "true" ]; then echo "options are : " echo " -h, --help: Print this help message" echo " -v, --verbose: Show verbose output to stdout. Without this a deploy.log is written to ~/app folder" - echo " -d, --dbmigrate: Do DB migrate" - echo " -n, --nogit: Don't execute git commands. With this --branch has no effect." + echo " -D, --dbmakemigrations: Do DB makemigrations" + echo " -d, --dbmigrate: Do DB migrate. To do both makemigrations and migrate, supply both switches -D and -d" + echo " -n, --nogit: Don't execute git commands. This is used to deploy the current code in the project repo. With this --branch has no effect." echo " -b, --branch: The branch to pull from origin repo." exit fi echo "BRANCH="$BRANCH +echo "DB_MAKE_MIGRATIONS="$DB_MAKE_MIGRATIONS echo "DB_MIGRATE="$DB_MIGRATE echo "NO_GIT="$NO_GIT echo "VERBOSE="$VERBOSE @@ -45,7 +48,7 @@ echo "VERBOSE="$VERBOSE # The project directory exists, we pull the specified branch cd $APP_HOME_DIR if [ -z "$NO_GIT" ]; then - echo 'We are executing default git commands. Please -no_git to not use this.' + echo 'We are executing default git commands. Please add --nogit to not do this.' # Save any modified changes before git pulling git stash # Fetch all branches/tags @@ -59,16 +62,23 @@ fi source ~/pyvenv/bin/activate pip install -r requirements.txt > deploy.log 2>&1 echo "###" >> deploy.log -if [ -z "$DB_MIGRATE" ]; then - echo 'We are not doing DB migration' +if [ -z "$DB_MAKE_MIGRATIONS" ]; then + echo 'We are not doing DB makemigrations' else + echo 'Doing DB makemigrations' ./manage.py makemigrations >> deploy.log 2>&1 echo "###" >> deploy.log +fi +if [ -z "$DB_MIGRATE" ]; then + echo 'We are not doing DB migrate' +else + echo 'Doing DB migrate' ./manage.py migrate >> deploy.log 2>&1 echo "###" >> deploy.log fi printf 'yes' | ./manage.py collectstatic >> deploy.log 2>&1 echo "###" >> deploy.log django-admin compilemessages +sudo systemctl restart celery.service sudo systemctl restart uwsgi diff --git a/dynamicweb/__init__.py b/dynamicweb/__init__.py index e69de29b..1a6c551d 100644 --- a/dynamicweb/__init__.py +++ b/dynamicweb/__init__.py @@ -0,0 +1,5 @@ +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ['celery_app'] diff --git a/dynamicweb/celery.py b/dynamicweb/celery.py new file mode 100644 index 00000000..609ef5c4 --- /dev/null +++ b/dynamicweb/celery.py @@ -0,0 +1,21 @@ +import os +from celery import Celery + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dynamicweb.settings') + +app = Celery('dynamicweb') + +# Using a string here means the worker don't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks() + + +@app.task(bind=True) +def debug_task(self): + print('Request: {0!r}'.format(self.request)) diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index 40187f84..08ce457d 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -10,6 +10,9 @@ from django.utils.translation import ugettext_lazy as _ # dotenv import dotenv +import logging + +logger = logging.getLogger(__name__) def gettext(s): @@ -25,6 +28,23 @@ def bool_env(val): return True if os.environ.get(val, False) == 'True' else False +def int_env(val, default_value=0): + """Replaces string based environment values with Python integers + Return default_value if val is not set or cannot be parsed, otherwise + returns the python integer equal to the passed val + """ + return_value = default_value + try: + return_value = int(os.environ.get(val)) + except Exception as e: + logger.error( + ("Encountered exception trying to get env value for {}\nException " + "details: {}").format( + val, str(e))) + + return return_value + + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.abspath( @@ -120,7 +140,8 @@ INSTALLED_APPS = ( 'datacenterlight.templatetags', 'alplora', 'rest_framework', - 'opennebula_api' + 'opennebula_api', + 'django_celery_results', ) MIDDLEWARE_CLASSES = ( @@ -150,10 +171,12 @@ TEMPLATES = [ os.path.join(PROJECT_DIR, 'membership'), os.path.join(PROJECT_DIR, 'hosting/templates/'), os.path.join(PROJECT_DIR, 'nosystemd/templates/'), - os.path.join(PROJECT_DIR, 'ungleich/templates/djangocms_blog/'), + 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/ungleich'), - os.path.join(PROJECT_DIR, 'ungleich_page/templates/ungleich_page'), + os.path.join(PROJECT_DIR, + 'ungleich_page/templates/ungleich_page'), os.path.join(PROJECT_DIR, 'templates/analytics'), ], 'APP_DIRS': True, @@ -190,6 +213,8 @@ CMS_TEMPLATES = ( # ungleich ('blog_ungleich.html', gettext('Blog')), ('page.html', gettext('Page')), + # dcl + ('datacenterlight/cms_page.html', gettext('Data Center Light')), ) DATABASES = { @@ -474,6 +499,7 @@ REGISTRATION_MESSAGE = {'subject': "Validation mail", } STRIPE_API_PRIVATE_KEY = env('STRIPE_API_PRIVATE_KEY') STRIPE_API_PUBLIC_KEY = env('STRIPE_API_PUBLIC_KEY') +STRIPE_API_PRIVATE_KEY_TEST = env('STRIPE_API_PRIVATE_KEY_TEST') ANONYMOUS_USER_NAME = 'anonymous@ungleich.ch' GUARDIAN_GET_INIT_ANONYMOUS_USER = 'membership.models.get_anonymous_user_instance' @@ -504,6 +530,9 @@ OPENNEBULA_PORT = env('OPENNEBULA_PORT') # default value is /RPC2 OPENNEBULA_ENDPOINT = env('OPENNEBULA_ENDPOINT') +# The public ssh key of the oneadmin user +ONEADMIN_USER_SSH_PUBLIC_KEY = env('ONEADMIN_USER_SSH_PUBLIC_KEY') + # dcl email configurations DCL_TEXT = env('DCL_TEXT') DCL_SUPPORT_FROM_ADDRESS = env('DCL_SUPPORT_FROM_ADDRESS') @@ -513,14 +542,26 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = { 'ungleich.ch': 'UA-62285904-1', 'digitalglarus.ch': 'UA-62285904-2', 'blog.ungleich.ch': 'UA-62285904-4', - 'hosting': 'UA-62285904-5', - 'datacenterlight.ch': 'UA-62285904-9', - + 'rails-hosting.ch': 'UA-62285904-5', + 'django-hosting.ch': 'UA-62285904-6', + 'node-hosting.ch': 'UA-62285904-7', + 'datacenterlight.ch': 'UA-62285904-8', + 'devuanhosting.ch': 'UA-62285904-9', + 'ipv6onlyhosting.ch': 'UA-62285904-10', '127.0.0.1:8000': 'localhost', 'dynamicweb-development.ungleich.ch': 'development', 'dynamicweb-staging.ungleich.ch': 'staging' } +# CELERY Settings +CELERY_BROKER_URL = env('CELERY_BROKER_URL') +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_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5) + ENABLE_DEBUG_LOGGING = bool_env('ENABLE_DEBUG_LOGGING') if ENABLE_DEBUG_LOGGING: @@ -531,7 +572,8 @@ if ENABLE_DEBUG_LOGGING: 'file': { 'level': 'DEBUG', 'class': 'logging.FileHandler', - 'filename': "{PROJECT_DIR}/debug.log".format(PROJECT_DIR=PROJECT_DIR), + 'filename': "{PROJECT_DIR}/debug.log".format( + PROJECT_DIR=PROJECT_DIR), }, }, 'loggers': { diff --git a/hosting/forms.py b/hosting/forms.py index 5435ff8f..288a8caf 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -14,7 +14,6 @@ def generate_ssh_key_name(): class HostingUserLoginForm(forms.Form): - email = forms.CharField(widget=forms.EmailInput()) password = forms.CharField(widget=forms.PasswordInput()) @@ -45,7 +44,6 @@ class HostingUserLoginForm(forms.Form): class HostingUserSignupForm(forms.ModelForm): - confirm_password = forms.CharField(widget=forms.PasswordInput()) password = forms.CharField(widget=forms.PasswordInput()) @@ -88,9 +86,8 @@ class UserHostingKeyForm(forms.ModelForm): def clean(self): cleaned_data = self.cleaned_data - if not self.cleaned_data.get('name', ''): + if 'generate' in self.request.POST: self.cleaned_data['name'] = generate_ssh_key_name() - if not cleaned_data.get('public_key'): private_key, public_key = UserHostingKey.generate_keys() cleaned_data.update({ 'private_key': private_key, diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 0e777a19..fac8cc5a 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-04 18:25+0000\n" +"POT-Creation-Date: 2017-08-31 23:46+0530\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -172,6 +172,27 @@ msgstr "CHF/Monat" msgid "Start VM" msgstr "VM jetzt starten" +msgid "My Dashboard" +msgstr "Mein Dashboard" + +msgid "Create VM" +msgstr "VM erstellen" + +msgid "My VMs" +msgstr "Meine VMs" + +msgid "My SSH Keys" +msgstr "Meine SSH Keys" + +msgid "My Bills" +msgstr "Meine Rechnungen" + +msgid "My Settings" +msgstr "Meine Einstellungen" + +msgid "Support / Contact" +msgstr "Support / Kontakt" + #, python-format msgid "" "You're receiving this email because you requested a password reset for your " @@ -245,31 +266,23 @@ msgstr "Gesamt" msgid "Finish Configuration" msgstr "Konfiguration beenden" +msgid "Order Nr." +msgstr "Bestellung Nr." + msgid "Amount" msgstr "Betrag" msgid "Status" msgstr "" -msgid "Approved" -msgstr "Akzeptiert" +msgid "See Invoice" +msgstr "Rechnung" -msgid "Declined" -msgstr "Abgelehnt" +msgid "Page" +msgstr "" -msgid "View Detail" -msgstr "Details anzeigen" - -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 "Delete" -msgstr "Löschen" +msgid "of" +msgstr "" msgid "Your Order" msgstr "Deine Bestellung" @@ -280,6 +293,9 @@ msgstr "Konfiguration" msgid "including VAT" msgstr "inkl. Mehrwertsteuer" +msgid "Month" +msgstr "Monat" + msgid "Billing Address" msgstr "Rechnungsadresse" @@ -301,17 +317,11 @@ msgstr "" "speichern keine Informationen in unserer Datenbank." 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" -" " +"You are not making any payment yet. After submitting your card information, " +"you will be taken to the Confirm Order Page." msgstr "" -"\n" -"Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner " -"Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " -"weitergeleitet." +"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" @@ -328,19 +338,6 @@ msgstr "" msgid "Card Type" msgstr "Kartentyp" -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 "Processing" msgstr "Weiter" @@ -365,16 +362,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 "" @@ -390,65 +382,92 @@ msgstr "" msgid "Private Key" msgstr "" +msgid "Delete" +msgstr "Löschen" + msgid "Delete SSH Key" msgstr "SSH Key löschen" -msgid "Do You want to delete this key?" +msgid "Do you want to delete this key?" msgstr "Möchtest Du den Schlüssel löschen?" msgid "Show" msgstr "Anzeigen" -msgid "Public ssh key" -msgstr "" +msgid "Public SSH Key" +msgstr "Public SSH Key" msgid "Download" msgstr "" -msgid "Settings" -msgstr "Einstellungen" +msgid "Your Virtual Machine Detail" +msgstr "Virtuelle Maschinen Detail" -msgid "Billing" -msgstr "Abrechnungen" +msgid "VM Settings" +msgstr "VM Einstellungen" -msgid "Ip not assigned yet" -msgstr "Ip nicht zugewiesen" +msgid "Copied" +msgstr "Kopiert" msgid "Disk" msgstr "Festplatte" -msgid "Current pricing" +msgid "Billing" +msgstr "Abrechnungen" + +msgid "Current Pricing" msgstr "Aktueller Preis" -msgid "Current status" -msgstr "Aktueller Status" +msgid "Your VM is" +msgstr "Deine VM ist" -msgid "Terminate Virtual Machine" -msgstr "Virtuelle Maschine beenden" +msgid "Pending" +msgstr "In Vorbereitung" + +msgid "Online" +msgstr "" + +msgid "Failed" +msgstr "Fehlgeschlagen" + +msgid "Terminate VM" +msgstr "VM Beenden" + +msgid "Something doesn't work?" +msgstr "Etwas funktioniert nicht?" + +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" msgid "Terminate your Virtual Machine" -msgstr "Ihre virtuelle Maschine beenden" +msgstr "Deine Virtuelle Maschine beenden" -msgid "Are you sure do you want to cancel your Virtual Machine " -msgstr "Sind Sie sicher, dass Sie ihre virtuelle Maschine beenden wollen " +msgid "Do you want to cancel your Virtual Machine" +msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst" + +msgid "OK" +msgstr "" msgid "Virtual Machines" msgstr "Virtuelle Maschinen" -msgid "Create VM" -msgstr "Neue VM" - -msgid "ID" +msgid "To create a new virtual machine, click \"Create VM\"" msgstr "" -msgid "Ipv4" -msgstr "IPv4" +msgid "CREATE VM" +msgstr "NEUE VM" -msgid "Ipv6" -msgstr "IPv6" +msgid "View Detail" +msgstr "Details anzeigen" msgid "login" -msgstr "einloggen" +msgstr "Einloggen" msgid "" "Thank you for signing up. We have sent an email to you. Please follow the " @@ -474,13 +493,46 @@ msgstr "Du kannst dich nun" msgid "Sorry. Your request is invalid." msgstr "Entschuldigung, deine Anfrage ist ungültig." +msgid "Invalid credit card" +msgstr "Ungültige Kreditkarte" + msgid "Confirm Order" msgstr "Bestellung Bestätigen" msgid "" "We could not find the requested VM. Please " "contact Data Center Light Support." -msgstr "" +msgstr "Kontaktiere den Data Center Light Support." + +#~ msgid "Your SSH Keys" +#~ msgstr "Deine SSH Keys" + +#~ msgid "Approved" +#~ msgstr "Akzeptiert" + +#~ msgid "Declined" +#~ msgstr "Abgelehnt" + +#~ msgid "Cancel Order" +#~ msgstr "Bestellung stornieren" + +#~ msgid "Do you want to delete your order?" +#~ msgstr "Willst du deine Bestellung löschen?" + +#~ msgid "Ip not assigned yet" +#~ msgstr "Ip nicht zugewiesen" + +#~ msgid "Current status" +#~ msgstr "Aktueller Status" + +#~ msgid "Terminate Virtual Machine" +#~ msgstr "Virtuelle Maschine beenden" + +#~ msgid "Ipv4" +#~ msgstr "IPv4" + +#~ msgid "Ipv6" +#~ msgstr "IPv6" #~ msgid "Close" #~ msgstr "Schliessen" @@ -494,82 +546,12 @@ msgstr "" #~ 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" - #~ 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" - #~ msgid "How it works" #~ msgstr "So funktioniert es" @@ -591,9 +573,6 @@ msgstr "" #~ msgid "Generate Key Pair" #~ msgstr "Schlüsselpaar generieren" -#~ msgid "Created at" -#~ msgstr "Erstellt am" - #~ msgid "Billing Amount" #~ msgstr "Rechnungsbetrag" @@ -603,14 +582,6 @@ msgstr "" #~ msgid "Place Order" #~ msgstr "Bestelle" -#~ msgid "" -#~ "You are not making any payment yet. After placing your order, you will be " -#~ "taken to the Submit Payment Page." -#~ msgstr "" -#~ "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe deiner " -#~ "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " -#~ "weitergeleitet." - #~ msgid "CARD NUMBER" #~ msgstr "Kreditkartennummer" diff --git a/hosting/migrations/0042_hostingorder_subscription_id.py b/hosting/migrations/0042_hostingorder_subscription_id.py new file mode 100644 index 00000000..2aa634a8 --- /dev/null +++ b/hosting/migrations/0042_hostingorder_subscription_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-17 16:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0041_userhostingkey_private_key'), + ] + + operations = [ + migrations.AddField( + model_name='hostingorder', + name='subscription_id', + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index 88386913..478ed745 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,13 +1,9 @@ import os import logging - from django.db import models from django.utils.functional import cached_property - - from Crypto.PublicKey import RSA - from membership.models import StripeCustomer, CustomUser from utils.models import BillingAddress from utils.mixins import AssignPermissionsMixin @@ -42,7 +38,6 @@ class HostingPlan(models.Model): class HostingOrder(AssignPermissionsMixin, models.Model): - ORDER_APPROVED_STATUS = 'Approved' ORDER_DECLINED_STATUS = 'Declined' @@ -55,6 +50,7 @@ class HostingOrder(AssignPermissionsMixin, models.Model): cc_brand = models.CharField(max_length=10) stripe_charge_id = models.CharField(max_length=100, null=True) price = models.FloatField() + subscription_id = models.CharField(max_length=100, null=True) permissions = ('view_hostingorder',) @@ -71,7 +67,8 @@ class HostingOrder(AssignPermissionsMixin, models.Model): return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS @classmethod - def create(cls, price=None, vm_id=None, customer=None, billing_address=None): + def create(cls, price=None, vm_id=None, customer=None, + billing_address=None): instance = cls.objects.create( price=price, vm_id=vm_id, @@ -91,6 +88,23 @@ class HostingOrder(AssignPermissionsMixin, models.Model): self.cc_brand = stripe_charge.source.brand self.save() + def set_subscription_id(self, subscription_object, cc_details): + """ + When creating a Stripe subscription, we have subscription id. + We store this in the subscription_id field. + This method sets the subscription id from subscription_object + and also the last4 and credit card brands used for this order. + + :param subscription_object: Stripe's subscription object + :param cc_details: A dict containing card details + {last4, brand} + :return: + """ + self.subscription_id = subscription_object.id + self.last4 = cc_details.get('last4') + self.cc_brand = cc_details.get('brand') + self.save() + def get_cc_data(self): return { 'last4': self.last4, @@ -101,7 +115,7 @@ class HostingOrder(AssignPermissionsMixin, models.Model): class UserHostingKey(models.Model): user = models.ForeignKey(CustomUser) public_key = models.TextField() - private_key = models.FileField(upload_to='private_keys', blank=True) + private_key = models.FileField(upload_to='private_keys', blank=True) created_at = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=100) @@ -142,5 +156,6 @@ class HostingBill(AssignPermissionsMixin, models.Model): @classmethod def create(cls, customer=None, billing_address=None): - instance = cls.objects.create(customer=customer, billing_address=billing_address) + instance = cls.objects.create(customer=customer, + billing_address=billing_address) return instance diff --git a/hosting/static/hosting/css/commons.css b/hosting/static/hosting/css/commons.css index 05e6f8e5..cb7cdd8e 100644 --- a/hosting/static/hosting/css/commons.css +++ b/hosting/static/hosting/css/commons.css @@ -13,7 +13,7 @@ } .content-dashboard{ - min-height: calc(100vh - 120px); + min-height: calc(100vh - 70px); width: 80%; margin: 0 auto; max-width: 1120px; @@ -116,12 +116,16 @@ font-weight: 100; color: #999; } +.modal-body .modal-icon { + margin-bottom: 10px; +} .modal-title { margin: 0; line-height: 1.42857143; font-size: 25px; padding: 0; - font-family: 'Lato', sans-serif; + /*font-family: 'Lato', sans-serif;*/ + font-weight: 300; } .modal-text { padding-top: 15px; @@ -164,29 +168,75 @@ /* ========= */ @media(min-width: 320px) { - .modal:before { - content: ''; - display: inline-block; - height: 100%; - vertical-align: middle; - margin-right: -4px; - } - } + .modal:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + margin-right: -4px; + } +} - @media (min-width: 768px) { - .modal-dialog { +@media (min-width: 768px) { + .modal-dialog { /* width: 520px; */ - margin: 15px auto; - } - } + margin: 15px auto; + } +} - .modal { - text-align: center; - } +.modal { + text-align: center; +} - .modal-dialog { - display: inline-block; - text-align: left; - vertical-align: middle; - } +.modal-dialog { + display: inline-block; + text-align: left; + vertical-align: middle; +} + +.un-icon { + width: 15px; + height: 15px; + opacity: 0.5; + margin-top: -1px; +} + +.css-plus { + position: relative; + width: 16px; + height: 20px; + display: inline-block; + vertical-align: middle; + /* top: -1px; */ +} + +.css-plus + span { + vertical-align: middle; +} + +.css-plus:before { + content: ''; + width: 10px; + height: 2px; + background: #f6f7f9; + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); +} + +.css-plus:after { + content: ''; + width: 2px; + height: 10px; + background: #f6f7f9; + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); +} \ 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 dca4b560..c0629480 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -4,7 +4,7 @@ * For details, see http://www.apache.org/licenses/LICENSE-2.0. */ -@font-face { +/*@font-face { font-family: 'Lato-Regular'; src: url('../fonts/Lato/Lato-Regular.ttf'); } @@ -16,7 +16,7 @@ @font-face { font-family: 'Lato-Light'; src: url('../fonts/Lato/Lato-Light.ttf'); -} +}*/ body, html { @@ -31,8 +31,9 @@ h3, h4, h5, h6 { - font-family: 'Lato-Regular', sans-serif; - font-weight: 300; + /*font-family: 'Lato-Regular', sans-serif;*/ + font-family: 'Lato', sans-serif; + /*font-weight: 300;*/ } .topnav { @@ -65,7 +66,8 @@ h6 { .navbar-transparent .navbar-nav>li>a { color: #fff; cursor: pointer; - font-family: 'Lato-Regular', sans-serif; + /*font-family: 'Lato-Regular', sans-serif;*/ + font-weight: normal; } .navbar-transparent .navbar-nav>li>a:hover { color: #fff; @@ -379,7 +381,7 @@ h6 { } .auth-box .form .red { - color: #ea3a3a; + color: #eb4d5c; } .auth-box .form .btn { @@ -424,7 +426,8 @@ h6 { text-align: center; font-size: 18px; line-height: 30px; - font-family: 'Lato' !important; + /*font-family: 'Lato' !important;*/ + font-weight: 300 !important; } .sign-up-message a { @@ -502,16 +505,16 @@ h6 { } footer { - padding: 2%; + padding: 20px; background-color: #f8f8f8; - #position: absolute; +/* position: absolute */ right: 0; bottom: 0; left: 0; } p.copyright { - margin: 15px 0 0; + margin: 14px 0 0; } a#forgotpassword { @@ -536,7 +539,8 @@ a.unlink:hover { /***** DCL payment page **********/ .dcl-order-container { - font-family: Lato; + /*font-family: Lato;*/ + font-weight: 300; } .dcl-order-table-header { @@ -577,9 +581,22 @@ a.unlink:hover { padding-left: 5px; } +.dcl-place-order-text{ + font-size: 13px; + color: #808080; + margin-bottom: 15px; +} + .dcl-order-table-total .tbl-total { text-align: center; color: #000; + padding-left: 44px; +} + +.tbl-total .dcl-price-month { + font-size: 16px; + text-transform: capitalize; + color: #000; } .tbl-no-padding { @@ -595,11 +612,16 @@ a.unlink:hover { } .card-warning-content { - font-family: Lato; + /*font-family: Lato;*/ + font-weight: 300; border: 1px solid #a1a1a1; border-radius: 3px; padding: 5px; } +.card-warning-error { + border: 1px solid #EB4D5C; + color: #EB4D5C; +} .card-warning-addtional-margin { margin-top: 15px; @@ -810,3 +832,39 @@ a.unlink:hover { background-color: transparent; } } + +/* bootstrap danger color override from #a94442 */ +.text-danger, +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label, +.has-error .form-control-feedback, +.alert-danger, +.list-group-item-danger, +a.list-group-item-danger, +a.list-group-item-danger:hover, +a.list-group-item-danger:focus, +.panel-danger > .panel-heading { + color: #eb4d5c; +} +.has-error .form-control, +.has-error .input-group-addon { + color: #eb4d5c; + border-color: #eb4d5c; +} +a.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus { + background-color: #eb4d5c; + border-color: #eb4d5c; +} +.panel-danger > .panel-heading .badge { + background-color: #eb4d5c; +} diff --git a/hosting/static/hosting/css/pricing.css b/hosting/static/hosting/css/pricing.css index 8eba7ffa..3c952209 100644 --- a/hosting/static/hosting/css/pricing.css +++ b/hosting/static/hosting/css/pricing.css @@ -9,7 +9,7 @@ font-size: 14px; padding-left: 0; margin-bottom: 30px; - font-family: 'Lato'; + font-family: Lato, sans-serif; } diff --git a/hosting/static/hosting/css/user_keys.css b/hosting/static/hosting/css/user_keys.css index 77a12155..7935344f 100644 --- a/hosting/static/hosting/css/user_keys.css +++ b/hosting/static/hosting/css/user_keys.css @@ -1,6 +1,6 @@ /* ssh_keys_choice */ .h1-thin { - font-family: Lato, sans-serif; + /*font-family: Lato, sans-serif;*/ font-weight: 300; font-size: 32px; } @@ -10,12 +10,12 @@ } .dashboard-choice-container .page-header p { font-size: 16px; - font-family: Lato, sans-serif; + /*font-family: Lato, sans-serif;*/ font-weight: 300; } .dashboard-choice-container h2 { - font-family: Lato, sans-serif; - font-weight: 400; + /*font-family: Lato, sans-serif; + font-weight: 400;*/ font-size: 22px; margin-top: 0; } @@ -26,7 +26,7 @@ } .choice-container p{ font-size: 18px; - font-family: Lato, sans-serif; + /*font-family: Lato, sans-serif;*/ font-weight: 300; } .choice-container-top { @@ -119,7 +119,7 @@ color: #717274; font-size: 16px; font-weight: 300; - font-family: 'Lato'; + /*font-family: 'Lato';*/ } .borderless tbody:before { @@ -195,7 +195,8 @@ border-bottom: 1px solid grey; box-shadow: none; border-radius: 0; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; font-size: 20px; padding-left: 0; } @@ -203,57 +204,58 @@ .form_key_name::-webkit-input-placeholder{ font-size: 20px; font-weight:100; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .form_key_name::-moz-input-placeholder{ font-size: 20px; - font-weight:200; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .form_key_name:-moz-input-placeholder{ - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; font-size: 20px; - font-weight:200; } .form_key_name:-ms-input-placeholder { font-size: 20px; - font-family: 'Lato-Light', sans-serif; - font-weight:200; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .form_public_key::-webkit-input-placeholder{ position: relative; top: 110px; font-size: 20px; - font-weight: 200; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .form_public_key::-moz-input-placeholder{ position: relative; top: 110px; font-size: 20px; - font-family: 'Lato-Light', sans-serif; - font-weight:200; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .form_public_key:-moz-input-placeholder{ position: relative; top: 110px; font-size: 20px; - font-weight:200; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .form_public_key:-ms-input-placeholder { position: relative; top: 110px; font-size: 20px; - font-weight:200; - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .underform-contaner{ margin-bottom: 20px; @@ -273,7 +275,8 @@ } } .underform-contaner h4{ - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; } .underform-contaner button{ /* font-family: Lato; */ @@ -287,13 +290,16 @@ color: #fff; } .control-label{ - font-family: 'Lato-Light', sans-serif; + /*font-family: 'Lato-Light', sans-serif;*/ + font-weight: 300; font-size: 20px; - font-weight:200; } .form-ssh h3{ margin-bottom: 40px; } +.key_contain { + word-break: break-all; +} .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 e043879d..806d40c6 100644 --- a/hosting/static/hosting/css/virtual-machine.css +++ b/hosting/static/hosting/css/virtual-machine.css @@ -1,3 +1,6 @@ +.virtual-machine-container { + max-width: 900px; +} .virtual-machine-container .tabs-left, .virtual-machine-container .tabs-right { border-bottom: none; padding-top: 2px; @@ -66,8 +69,8 @@ overflow-x: hidden; overflow-y: hidden; } -.parent-container ::-webkit-scrollbar { - display: none; +.parent-container ::-webkit-scrollbar { + display: none; } .container-os{ overflow: auto; @@ -225,6 +228,393 @@ } @media (max-width: 420px) { .btn-create-vm { - float: left !important; + float: left !important; } +} + +/* Vm Details */ + +.vm-detail-item, .vm-contact-us { + overflow: hidden; + border: 1px solid #ccc; + padding: 15px; + color: #555; + font-weight: 300; + margin-bottom: 15px; +} + +.vm-detail-title { + margin-top: 0; + font-size: 20px; + font-weight: 300; +} + +.vm-detail-title .un-icon { + float: right; + height: 24px; + width: 21px; + margin-top: 0; +} + +.vm-detail-item .vm-name { + font-size: 16px; + margin-bottom: 15px; +} + +.vm-detail-item p { + margin-bottom: 5px; + position: relative; +} + +.vm-detail-ip { + padding-bottom: 5px; + border-bottom: 1px solid #ddd; + margin-bottom: 10px; +} + +.vm-detail-ip .un-icon { + height: 14px; + width: 14px; +} + +.vm-detail-ip .to_copy { + position: absolute; + right: 0; + top: 1px; + padding: 0; + line-height: 1; +} + +.vm-vmid { + padding: 50px 0 70px; + text-align: center; +} + +.vm-item-lg { + font-size: 22px; + margin-top: 5px; + margin-bottom: 15px; + letter-spacing: 0.6px; +} + +.vm-color-online { + color: #37B07B; +} + +.vm-color-pending { + color: #e47f2f; +} + +.vm-detail-item .value{ + font-weight: 400; +} + +.vm-detail-config .value { + float: right; + font-weight: 600; +} + +.vm-detail-contain { + margin-top: 25px; +} + +.vm-contact-us { + margin: 25px 0 30px; + /* text-align: center; */ +} + +@media(min-width: 768px) { + .vm-detail-contain { + display: flex; + margin-left: -15px; + margin-right: -15px; + } + .vm-detail-item { + width: 33.333333%; + margin: 0 15px; + } + .vm-contact-us { + display: flex; + align-items: center; + justify-content: space-between; + } + .vm-contact-us .vm-detail-title { + margin-bottom: 0; + } + .vm-contact-us .un-icon { + width: 22px; + height: 22px; + margin-right: 5px; + } + .vm-contact-us div { + padding: 0 15px; + position: relative; + } + .vm-contact-us-text { + display: flex; + align-items: center; + } +} + +.value-sm-block { + display: block; + padding-top: 2px; +} + +@media(max-width: 767px) { + .vm-contact-us div { + margin-bottom: 30px; + } + .vm-contact-us div span { + display: block; + margin-bottom: 3px; + } + .dashboard-title-thin { + font-size: 22px; + } +} + +.btn-vm-invoice { + color: #87B6EA; + border: 2px solid #87B6EA; + padding: 4px 18px; + letter-spacing: 0.6px; +} +.btn-vm-invoice:hover, .btn-vm-invoice:focus { + color : #fff; + background: #87B6EA; +} + + +.btn-vm-term { + color: #aaa; + border: 2px solid #ccc; + background: #fff; + padding: 4px 18px; + letter-spacing: 0.6px; +} +.btn-vm-term:hover, .btn-vm-term:focus, .btn-vm-term:active { + color: #eb4d5c; + border-color: #eb4d5c; +} + +.btn-vm-contact { + color: #fff; + background: #A3C0E2; + border: 2px solid #A3C0E2; + padding: 5px 25px; + font-size: 12px; + letter-spacing: 1.3px; +} +.btn-vm-contact:hover, .btn-vm-contact:focus { + background: #fff; + color: #a3c0e2; +} + +.btn-vm-back { + color: #fff; + background: #C4CEDA; + border: 2px solid #C4CEDA; + padding: 5px 25px; + font-size: 12px; + letter-spacing: 1.3px; +} +.btn-vm-back:hover, .btn-vm-back:focus { + color: #fff; + background: #8da4c0; + border-color: #8da4c0; +} + +.vm-contact-us-text { + letter-spacing: 0.4px; +} + + +/* New styles */ +.dashboard-container-head { + padding: 0 8px; +} +.dashboard-title-thin { + font-weight: 300; + font-size: 32px; +} + +.dashboard-title-thin .un-icon { + height: 34px; + margin-right: 5px; + margin-top: -2px; + width: 34px; + vertical-align: middle; +} + +.dashboard-subtitle { + font-weight: 300; + margin-bottom: 25px; +} + +.btn-vm { + background: #1596DA; + color: #fff; + font-weight: 400; + letter-spacing: 0.8px; + border-radius: 3px; + padding-bottom: 7px; + border: 2px solid #1596DA; +} + +.btn-vm:hover, .btn-vm:focus { + color: #1596DA; + background: #fff; +} +.btn-vm:hover .css-plus:after, +.btn-vm:focus .css-plus:after, +.btn-vm:hover .css-plus:before, +.btn-vm:focus .css-plus:before { + background: #1596DA; +} +.btn-vm-detail { + background: #3770CC; + color: #fff; + font-weight: 400; + letter-spacing: 0.6px; + font-size: 14px; + border-radius: 3px; + border: 2px solid #3770CC; + padding: 4px 20px; + /* padding-bottom: 7px; */ +} + +.btn-vm-detail:hover, .btn-vm-detail:focus { + background: #fff; + color: #3770CC; +} + +.btn-order-detail { + background: #87B6EA; + color: #fff; + font-weight: 400; + letter-spacing: 0.6px; + font-size: 14px; + border-radius: 3px; + border: 2px solid #87B6EA; + padding: 4px 20px; + min-width: 155px; + /* padding-bottom: 7px; */ +} + +.btn-order-detail:hover, .btn-order-detail:focus, .btn-order-detail:active { + background: #fff; + color: #87B6EA; +} + +.vm-status, .vm-status-active, .vm-status-failed { + font-weight: 600; +} +.vm-status-active { + color: #4A90E2; +} +.vm-status-failed { + color: #eb4d5c; +} + +@media (min-width:768px) { + .dashboard-subtitle { + display: flex; + justify-content: space-between; + font-size: 16px; + } +} +@media (max-width:767px) { + .dashboard-title-thin { + font-size: 22px; + } + .dashboard-title-thin .un-icon { + height: 22px; + width: 22px; + margin-top: -3px; + } + .dashboard-subtitle p { + width: 200px; + } +} + +.table-switch { + color: #555; +} + +.table-switch > tbody > tr > td { + padding: 12px 8px; +} + +@media (min-width: 768px) { + .table-switch > tbody > tr > td:nth-child(1) { + padding-right: 45px; + } + .table-switch > tbody > tr:last-child > td { + border-bottom: 1px solid #ddd; + } +} + +.table-switch .un-icon { + margin-left: 5px; +} + +@media (max-width:767px) { + .dashboard-subtitle { + margin-bottom: 15px; + } + .table-switch .un-icon { + float: right; + margin-top: 0; + } + .table-switch thead { + display: none; + } + .table-switch tbody tr { + display: block; + position: relative; + border-top: 1px solid #ddd; + /* margin-top: 15px; */ + padding-top: 10px; + padding-bottom: 13px; + } + .table-switch tbody tr:last-child { + border-bottom: 1px solid #ddd; + } + .table-switch tbody tr td { + display: block; + padding-top: 28px; + padding-bottom: 6px; + position: relative; + border: 0; + } + .table-switch td:before { + content: attr(data-header); + font-weight: 600; + position: absolute; + top: 5px; + left: 8px; + } + .table-switch .last-td { + position: absolute; + bottom: 13px; + right: 0; + } + .table-switch tbody tr .xs-td-inline { + text-align: right; + padding-top: 6px; + } + .table-switch tbody tr .xs-td-bighalf { + width: 52%; + display: inline-block; + } + .table-switch tbody tr .xs-td-smallhalf { + width: 47%; + text-align: right; + display: inline-block; + } + .table-switch tbody tr .xs-td-smallhalf:before { + left: auto; + right: 8px; + } } \ No newline at end of file diff --git a/hosting/static/hosting/img/24-hours-support.svg b/hosting/static/hosting/img/24-hours-support.svg new file mode 100644 index 00000000..473828a2 --- /dev/null +++ b/hosting/static/hosting/img/24-hours-support.svg @@ -0,0 +1,15 @@ + + + + Slice 23 + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/img/billing.svg b/hosting/static/hosting/img/billing.svg new file mode 100644 index 00000000..c382cffa --- /dev/null +++ b/hosting/static/hosting/img/billing.svg @@ -0,0 +1,13 @@ + + + + Slice 23 + Created with Sketch. + + + + + + + + diff --git a/hosting/static/hosting/img/connected.svg b/hosting/static/hosting/img/connected.svg new file mode 100644 index 00000000..fa3875dc --- /dev/null +++ b/hosting/static/hosting/img/connected.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosting/static/hosting/img/copy.svg b/hosting/static/hosting/img/copy.svg new file mode 100644 index 00000000..c30b5922 --- /dev/null +++ b/hosting/static/hosting/img/copy.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/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/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/settings.svg b/hosting/static/hosting/img/settings.svg new file mode 100644 index 00000000..61dc8613 --- /dev/null +++ b/hosting/static/hosting/img/settings.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosting/static/hosting/img/shopping-cart.svg b/hosting/static/hosting/img/shopping-cart.svg new file mode 100644 index 00000000..19e70e1d --- /dev/null +++ b/hosting/static/hosting/img/shopping-cart.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/hosting/static/hosting/img/vm.svg b/hosting/static/hosting/img/vm.svg new file mode 100644 index 00000000..061d80ce --- /dev/null +++ b/hosting/static/hosting/img/vm.svg @@ -0,0 +1,12 @@ + + + + Slice 23 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/hosting/static/hosting/js/initial.js b/hosting/static/hosting/js/initial.js index 9d1cabe8..f5a60bbe 100644 --- a/hosting/static/hosting/js/initial.js +++ b/hosting/static/hosting/js/initial.js @@ -44,4 +44,99 @@ $( document ).ready(function() { _initNavUrl(); + /* + * Replace all SVG images with inline SVG + */ + $('.svg-img').each(function() { + console.log('asa') + var $img = $(this); + var imgID = $img.attr('id'); + var imgClass = $img.attr('class'); + var imgURL = $img.attr('src'); + + jQuery.get(imgURL, function(data) { + // Get the SVG tag, ignore the rest + var $svg = jQuery(data).find('svg'); + + // Add replaced image's ID to the new SVG + if(typeof imgID !== 'undefined') { + $svg = $svg.attr('id', imgID); + } + // Add replaced image's classes to the new SVG + if(typeof imgClass !== 'undefined') { + $svg = $svg.attr('class', imgClass+' replaced-svg'); + } + + // Remove any invalid XML tags as per http://validator.w3.org + $svg = $svg.removeAttr('xmlns:a'); + + // Check if the viewport is set, if the viewport is not set the SVG wont't scale. + if(!$svg.attr('viewBox') && $svg.attr('height') && $svg.attr('width')) { + $svg.attr('viewBox', '0 0 ' + $svg.attr('height') + ' ' + $svg.attr('width')) + } + + // Replace image with new SVG + $img.replaceWith($svg); + + }, 'xml'); + }); + + $('.alt-text').on('mouseenter mouseleave', function(e){ + var $this = $(this); + var txt = $this.text(); + var alt = $this.attr('data-alt'); + $this.text(alt); + $this.attr('data-alt', txt); + }); + +}); + +function getScrollbarWidth() { + var outer = document.createElement("div"); + outer.style.visibility = "hidden"; + outer.style.width = "100px"; + outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps + + document.body.appendChild(outer); + + var widthNoScroll = outer.offsetWidth; + // force scrollbars + outer.style.overflow = "scroll"; + + // add innerdiv + var inner = document.createElement("div"); + inner.style.width = "100%"; + outer.appendChild(inner); + + var widthWithScroll = inner.offsetWidth; + + // remove divs + outer.parentNode.removeChild(outer); + + return widthNoScroll - widthWithScroll; +} + +// globally stores the width of scrollbar +var scrollbarWidth = getScrollbarWidth(); +var paddingAdjusted = false; + +$( document ).ready(function() { + // add proper padding to fixed topnav on modal show + $('body').on('click', '[data-toggle=modal]', function(){ + var $body = $('body'); + if ($body[0].scrollHeight > $body.height()) { + scrollbarWidth = getScrollbarWidth(); + var topnavPadding = parseInt($('.navbar-fixed-top.topnav').css('padding-right')); + $('.navbar-fixed-top.topnav').css('padding-right', topnavPadding+scrollbarWidth); + paddingAdjusted = true; + } + }); + + // remove added padding on modal hide + $('body').on('hidden.bs.modal', function(){ + if (paddingAdjusted) { + var topnavPadding = parseInt($('.navbar-fixed-top.topnav').css('padding-right')); + $('.navbar-fixed-top.topnav').css('padding-right', topnavPadding-scrollbarWidth); + } + }); }); \ No newline at end of file diff --git a/hosting/templates/hosting/base.html b/hosting/templates/hosting/base.html index b485451f..ec57475d 100644 --- a/hosting/templates/hosting/base.html +++ b/hosting/templates/hosting/base.html @@ -38,7 +38,6 @@ {% with 'hosting/img/'|add:hosting|add:'-intro-bg.png' as image_static %} - alt="">