merge master
1
.gitignore
vendored
|
@ -35,3 +35,4 @@ secret-key
|
|||
|
||||
.env
|
||||
*.mo
|
||||
*.log
|
||||
|
|
40
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
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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."
|
||||
|
||||
|
|
24
datacenterlight/migrations/0007_contactus.py
Normal file
|
@ -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()),
|
||||
],
|
||||
),
|
||||
]
|
22
datacenterlight/migrations/0007_stripeplan.py
Normal file
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
20
datacenterlight/migrations/0008_auto_20170821_2024.py
Normal file
|
@ -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),
|
||||
),
|
||||
]
|
23
datacenterlight/migrations/0008_contactus_field.py
Normal file
|
@ -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,
|
||||
),
|
||||
]
|
16
datacenterlight/migrations/0009_merge.py
Normal file
|
@ -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 = [
|
||||
]
|
16
datacenterlight/migrations/0010_merge.py
Normal file
|
@ -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 = [
|
||||
]
|
|
@ -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)
|
||||
|
|
47
datacenterlight/static/datacenterlight/css/cms.css
Normal file
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -1520,3 +1610,39 @@ a#forgotpassword {
|
|||
.w380 {
|
||||
max-width: 380px !important;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
|
11
datacenterlight/static/datacenterlight/img/facebook_logo.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="57px" height="66px" viewBox="0 0 57 66" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 20</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="contact-us" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<ellipse id="Oval-2" fill="#FFFFFF" cx="28.7865939" cy="33.4691264" rx="19.7865939" ry="19.4691264"></ellipse>
|
||||
<path d="M35.3784886,34.6387051 L30.2336176,34.6387051 L30.2336176,50.2467762 L22.6226844,50.2467762 L22.6226844,34.6387051 L19,34.6387051 L19,29.1194625 L22.6226844,29.1194625 L22.6226844,25.5403791 C22.6226844,22.9849851 24.0459888,19 30.3115248,19 L35.9567996,19.0178699 L35.9567996,24.3762836 L31.8546864,24.3762836 C31.1894789,24.3762836 30.2426069,24.6596489 30.2426069,25.8824599 L30.2426069,29.1194625 L36.0436961,29.1194625 L35.3784886,34.6387051 Z" id="Shape" fill="#5E79AD" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -39,7 +39,7 @@
|
|||
_initScroll();
|
||||
_initNavUrl();
|
||||
_initPricing();
|
||||
|
||||
ajaxForms();
|
||||
});
|
||||
|
||||
$(window).resize(function() {
|
||||
|
@ -157,4 +157,27 @@
|
|||
$('#valueTotal').text(numbers * price * 31);
|
||||
}
|
||||
|
||||
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);
|
174
datacenterlight/tasks.py
Normal file
|
@ -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())
|
|
@ -1,4 +1,4 @@
|
|||
{% load staticfiles i18n%}
|
||||
{% load staticfiles i18n cms_tags sekizai_tags %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{LANGUAGE_CODE}}">
|
||||
|
@ -33,13 +33,15 @@
|
|||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
{% render_block "css" postprocessor "compressor.contrib.sekizai.compress" %}
|
||||
{% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %}
|
||||
<!-- Google analytics -->
|
||||
{% include "google_analytics.html" %}
|
||||
<!-- End Google Analytics -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% cms_toolbar %}
|
||||
<!-- Navigation -->
|
||||
{% include "datacenterlight/includes/_navbar.html" %}
|
||||
|
||||
|
|
33
datacenterlight/templates/datacenterlight/cms_page.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
{% extends "datacenterlight/base.html" %}
|
||||
{% load staticfiles cms_tags sekizai_tags %}
|
||||
{% block content %}
|
||||
{% addtoblock "css" %}
|
||||
<link href="{% static 'datacenterlight/css/cms.css' %}" media="screen" rel="stylesheet" type="text/css"/>
|
||||
{% endaddtoblock %}
|
||||
|
||||
<div class="dcl-cms_page-full-width">
|
||||
<div class="dcl-cms_page-header">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<div class="single-heading">
|
||||
<h2>{% placeholder 'datacenterlight_cms_page_title' %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="split-section left" id="dcl-cms_page-text">
|
||||
<div class="space">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% placeholder 'datacenterlight_cms_page_text' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
50
datacenterlight/templates/datacenterlight/contact_form.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% if success %}
|
||||
<div class="contact-form-success">
|
||||
<div class="subtitle text-center">
|
||||
<h3>{% trans "Thank you for contacting us." %}</h3>
|
||||
</div>
|
||||
<p>
|
||||
{% trans "Your message was successfully sent to our team." %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="subtitle">
|
||||
<h3>{% trans "Get in touch with us!" %}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form class="form-horizontal ajax-form" method="POST" action="{% url 'datacenterlight:contact_us' %}" data-toggle="validator" data-response="#contact-form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="name">{% trans "Name" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" name="name" class="form-control" data-minlength="3" data-error="{% trans 'Please enter your name.' %}" required>
|
||||
{{contact_form.name.errors}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="email">{% trans "Email" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="email" type="email" pattern="^[^@\s]+@([^@\s]+\.)+[^@\s]+$" class="form-control" data-error="{% trans 'Please enter a valid email address.' %}" required>
|
||||
{{contact_form.email.errors}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="message">{% trans "Message" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" name="message" id="message" rows="6" required></textarea>
|
||||
{{contact_form.message.errors}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10 text-right">
|
||||
<div class="form-error hide">{% trans "Sorry, there was an unexpected error. Kindly retry." %}</div>
|
||||
<button type="submit" class="btn btn-default">{% trans "SUBMIT" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "datacenterlight/base.html" %}
|
||||
{% load staticfiles i18n%}
|
||||
{% load staticfiles i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
@ -154,27 +154,29 @@
|
|||
<div class="intro-header-2 contact-section" id="contact">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-6 col-md-6">
|
||||
<div class="card">
|
||||
<div class="col-sm-6">
|
||||
<div class="title">
|
||||
<h2>{% trans "Contact us" %}</h2>
|
||||
</div>
|
||||
<div class="contact-details">
|
||||
<div class="subtitle">
|
||||
<h3>ungleich GmbH </h3>
|
||||
<h3>ungleich GmbH</h3>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p><i class="fa fa-envelope-o"></i> info@datacenterlight.ch</p>
|
||||
<p>info@datacenterlight.ch</p>
|
||||
<p>In der Au 7, Schwanden 8762</p>
|
||||
<p>{% trans "Switzerland " %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="social">
|
||||
<a target="_blank" class="" href="https://twitter.com/datacenterlight"><i class="fa fa-twitter fa-fw"></i></a>
|
||||
<a target="_blank" class="" href="https://github.com/ungleich"><i class="fa fa-github fa-fw"></i></a>
|
||||
<a target="_blank" class="" href="https://www.facebook.com/ungleich.ch/"><i class="fa fa-facebook fa-fw"></i></a>
|
||||
<a target="_blank" class="" href="https://www.facebook.com/ungleich.ch/"><i class="fa fa-facebook"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6">
|
||||
<div class="title">
|
||||
<h2>{% trans "Questions?" %} {% trans "Contact us!" %}</h2>
|
||||
<div class="col-sm-6">
|
||||
<div id="contact-form" class="contact-form">
|
||||
{% include "datacenterlight/contact_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -72,14 +72,19 @@
|
|||
<hr>
|
||||
<p><b>{% trans "Configuration"%}</b> <span class="pull-right">{{request.session.template.name}}</span></p>
|
||||
<hr>
|
||||
<h4>{% trans "Total"%}<p class="pull-right"><b>{{vm.price}} CHF</b></p></h4>
|
||||
<h4>{% trans "Total"%}<p class="pull-right"><b>{{vm.price}} CHF</b><span class="dcl-price-month"> /{% trans "Month" %}</span></p></h4>
|
||||
{% endwith %}
|
||||
</div>
|
||||
<br/>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class=" content pull-right">
|
||||
<a href="{{next_url}}" ><button class="btn btn-info">{% trans "Place order"%}</button></a>
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<p class="dcl-place-order-text">{% 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 %}.</p>
|
||||
</div>
|
||||
<div class="col-sm-4 content">
|
||||
<a href="{{next_url}}" ><button class="btn btn-info pull-right">{% trans "Place order"%}</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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'))
|
||||
|
|
20
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
|
||||
|
||||
|
|
|
@ -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']
|
21
dynamicweb/celery.py
Normal file
|
@ -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))
|
|
@ -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': {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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"
|
||||
|
||||
|
|
20
hosting/migrations/0042_hostingorder_subscription_id.py
Normal file
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
@ -171,22 +175,68 @@
|
|||
vertical-align: middle;
|
||||
margin-right: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (min-width: 768px) {
|
||||
.modal-dialog {
|
||||
/* width: 520px; */
|
||||
margin: 15px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
.modal {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
.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%);
|
||||
}
|
85
hosting/static/hosting/css/dashboard.css
Normal file
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
font-size: 14px;
|
||||
padding-left: 0;
|
||||
margin-bottom: 30px;
|
||||
font-family: 'Lato';
|
||||
font-family: Lato, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -228,3 +231,390 @@
|
|||
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;
|
||||
}
|
||||
}
|
15
hosting/static/hosting/img/24-hours-support.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="90px" height="90px" viewBox="0 0 90 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 23</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="dashboard" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="24-hours-support" transform="translate(5.000000, 7.000000)" fill-rule="nonzero">
|
||||
<path d="M46.5951681,0.152636732 C38.3819652,0.152636732 30.8805993,3.25307034 25.1957228,8.34311252 C26.4566594,10.3481531 27.1729858,12.6809138 27.2403255,15.0888704 C27.2695061,16.187911 27.3343206,17.2732031 27.4271933,18.3568116 C31.7391809,12.4317567 38.7228726,8.57010354 46.5951681,8.57010354 C59.662163,8.57010354 70.2928624,19.200803 70.2928624,32.2677979 C70.2928624,45.1970269 59.883823,55.7340118 47.0062211,55.9551106 C48.1821412,56.9377096 49.3962205,57.8742931 50.6470561,58.7536378 C52.3616941,59.9632278 53.7488926,61.5384164 54.7222323,63.3400349 C68.5080789,59.7314668 78.7100487,47.1692394 78.7100487,32.2677979 C78.7103293,14.5594118 64.3038348,0.152636732 46.5951681,0.152636732 Z" id="Shape"></path>
|
||||
<path d="M45.8067321,65.6399674 C42.1378389,62.7112501 39.8488491,60.760923 36.5730516,57.7264262 C35.6092516,56.8336136 34.4313674,56.2774996 33.2217774,56.2774996 C32.2961367,56.2774996 31.3819998,56.5723915 30.6294782,57.189953 C29.8528266,57.8437096 29.076175,58.4974662 28.2992428,59.1509422 C21.9864233,51.6487346 17.298736,42.9066342 14.543138,33.4970286 C15.5173195,33.2119571 16.4917816,32.9266049 17.4659631,32.6415334 C19.6129783,31.9841292 20.6230743,29.692053 20.3079804,27.3943652 C19.6225181,22.3932675 19.2501854,19.9087119 18.8259451,15.3242789 C18.6084939,12.9744028 17.0445285,10.9331671 14.8029571,10.8111138 C14.4533517,10.7771633 14.1040268,10.7603284 13.7575078,10.7603284 C5.42140979,10.7603284 -1.83669126,20.422458 0.848200072,32.242826 C4.18909265,46.881362 11.4216607,60.3692302 21.7658857,71.2524537 C26.0032385,75.7013655 31.1942903,77.7159458 35.8825387,77.7159458 C40.8283616,77.7159458 45.2149841,75.4757773 47.3395528,71.4884233 C48.478436,69.5538089 47.6512796,67.1124629 45.8067321,65.6399674 Z" id="Shape"></path>
|
||||
<path d="M36.9372473,32.862071 C34.7172807,34.9218251 33.2647065,36.5413458 32.4975947,37.8123833 C31.956071,38.7088435 31.5610112,39.6566502 31.3227969,40.6294288 C31.2108446,41.087339 31.3138183,41.5626453 31.6053432,41.9338556 C31.8965876,42.3045047 32.3337347,42.517186 32.8051128,42.517186 L43.3993366,42.517186 C44.6973099,42.517186 45.7534214,41.4610745 45.7534214,40.1631011 C45.7534214,38.8651278 44.6973099,37.8090163 43.3993366,37.8090163 L38.615129,37.8090163 C38.6541299,37.7630008 38.6942532,37.7169853 38.7352182,37.6706892 C39.0477868,37.3188391 39.8132151,36.5837137 41.0098983,35.4863566 C42.2649426,34.3345665 43.1123009,33.4720568 43.6002334,32.8494448 C44.3356394,31.9134225 44.8827748,30.9995662 45.2259269,30.1336894 C45.5758129,29.2512583 45.7534214,28.3104661 45.7534214,27.3371264 C45.7534214,25.5907826 45.1209891,24.1084667 43.8738011,22.9305825 C42.63475,21.7622381 40.9290906,21.1702096 38.8033997,21.1702096 C36.8718716,21.1702096 35.2313073,21.6758188 33.9277223,22.673008 C33.1412503,23.2751375 32.5357539,24.1093084 32.1277873,25.1530743 C31.8522556,25.8581775 31.9235235,26.6648514 32.3185832,27.3104711 C32.713643,27.9563714 33.399386,28.3870651 34.1527493,28.4622611 C34.2366433,28.4706786 34.3202569,28.4748873 34.402748,28.4748873 C35.4630683,28.4748873 36.4086304,27.8006482 36.7559912,26.7972862 C36.8648571,26.4827535 37.0087958,26.2327547 37.1844402,26.0537433 C37.5542476,25.6769213 38.0522811,25.4934206 38.7074406,25.4934206 C39.3732622,25.4934206 39.8707345,25.6668204 40.2276351,26.0234404 C40.5836939,26.3794992 40.7568131,26.8988569 40.7568131,27.6115358 C40.7568131,28.2734293 40.5202823,28.9636616 40.0539547,29.6637142 C39.8039559,30.0301546 39.0567654,30.8873333 36.9372473,32.862071 Z" id="Shape"></path>
|
||||
<path d="M58.1464578,21.1702096 L56.3768257,21.1702096 C55.8726195,21.1702096 55.4018025,21.4188055 55.1172921,21.8351895 L46.9332697,33.8154894 C46.6944942,34.1650949 46.5682322,34.5739032 46.5682322,34.9973018 L46.5682322,36.9386502 C46.5682322,37.7795551 47.2525723,38.4638952 48.0934772,38.4638952 L54.8120186,38.4638952 L54.8120186,40.0876245 C54.8120186,41.4274047 55.9020806,42.5174666 57.2418607,42.5174666 C58.5816409,42.5174666 59.6717028,41.4274047 59.6717028,40.0876245 L59.6717028,38.4638952 L59.9144064,38.4638952 C61.1593498,38.4638952 62.1722516,37.4509933 62.1722516,36.20605 C62.1722516,34.9611067 61.1593498,33.9482048 59.9144064,33.9482048 L59.6717028,33.9482048 L59.6717028,22.6954546 C59.6717028,21.8545497 58.9873628,21.1702096 58.1464578,21.1702096 Z M54.8122992,33.9482048 L51.7966014,33.9482048 L54.8122992,29.465062 L54.8122992,33.9482048 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
13
hosting/static/hosting/img/billing.svg
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="90px" height="90px" viewBox="0 0 90 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 23</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="dashboard" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="billing.9cb6c82b" transform="translate(10.000000, 8.000000)" fill-rule="nonzero">
|
||||
<path d="M0.432900478,0.106254164 L0.432900478,73.8096859 L13.2526963,67.4020978 L26.0724922,73.8096859 L35.6815644,67.4020978 L48.5013603,73.8096859 L58.1150522,67.4020978 L70.9348481,73.8096859 L70.9348481,0.106254164 L0.432900478,0.106254164 Z M64.5226403,63.4383556 L57.6530776,60.0012643 L48.0393856,66.4088524 L35.2195898,60.0058841 L25.6105176,66.4134722 L13.2526963,60.2368714 L6.84510828,63.4429753 L6.84510828,6.51384222 L64.5226403,6.51384222 L64.5226403,63.4383556 Z" id="Shape"></path>
|
||||
<path d="M18.1542471,16.7096222 L53.9157029,16.7096222 L53.9157029,22.9370402 L18.1542471,22.9370402 L18.1542471,16.7096222 Z M18.1542471,29.1690778 L53.9157029,29.1690778 L53.9157029,35.4011155 L18.1542471,35.4011155 L18.1542471,29.1690778 Z M18.1542471,41.6285335 L53.9157029,41.6285335 L53.9157029,47.8605712 L18.1542471,47.8605712 L18.1542471,41.6285335 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
45
hosting/static/hosting/img/connected.svg
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 278.898 278.898" style="enable-background:new 0 0 278.898 278.898;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M269.898,175.773h-20.373V64.751c0-4.971-4.029-9-9-9h-62.702V35.377c0-4.971-4.029-9-9-9h-58.748c-4.971,0-9,4.029-9,9
|
||||
v20.374H38.373c-4.971,0-9,4.029-9,9v111.022H9c-4.971,0-9,4.029-9,9v58.748c0,4.971,4.029,9,9,9h58.747c4.971,0,9-4.029,9-9
|
||||
v-58.748c0-4.971-4.029-9-9-9H47.373V73.751h53.702v20.374c0,4.971,4.029,9,9,9h20.374v72.648h-20.374c-4.971,0-9,4.029-9,9v58.748
|
||||
c0,4.971,4.029,9,9,9h58.748c4.971,0,9-4.029,9-9v-58.748c0-4.971-4.029-9-9-9h-20.374v-72.648h20.374c4.971,0,9-4.029,9-9V73.751
|
||||
h53.702v102.022h-20.374c-4.971,0-9,4.029-9,9v58.748c0,4.971,4.029,9,9,9h58.747c4.971,0,9-4.029,9-9v-58.748
|
||||
C278.898,179.803,274.869,175.773,269.898,175.773z M58.747,234.521H18v-40.748h40.747V234.521z M159.823,234.521h-40.748v-40.748
|
||||
h40.748V234.521z M159.823,85.125h-40.748V44.377h40.748V85.125z M260.898,234.521h-40.747v-40.748h40.747V234.521z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
7
hosting/static/hosting/img/copy.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
|
||||
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
|
||||
<g><path d="M691,160.8V10H269.5C206.3,72.6,143.1,135.2,80,197.8v641.4h227.9V990H920V160.8H691z M269.5,64.4v134.4H133.1C178.5,154,224,109.2,269.5,64.4z M307.9,801.2H117.5V236.8h190.5V47.9h344.5v112.9h-154c-63.5,62.9-127,125.9-190.5,188.8V801.2z M499.5,215.2v134.5H363.1v-1c45.1-44.5,90.2-89,135.3-133.5L499.5,215.2z M881.5,952h-535V386.6H538V198.8h343.5V952z"/></g>
|
||||
</svg>
|
After Width: | Height: | Size: 846 B |
14
hosting/static/hosting/img/dashboard_settings.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="90px" height="90px" viewBox="0 0 90 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 23</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="dashboard" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="62780" transform="translate(1.000000, 16.000000)" fill-rule="nonzero">
|
||||
<path d="M77.3788837,0.0166335071 L10.5054936,0.0166335071 C4.71263581,0.0166335071 0,4.72907813 0,10.5221271 L0,48.3010279 C0,54.0936945 4.71263581,58.8065215 10.5054936,58.8065215 L77.3786925,58.8065215 C83.1713591,58.8065215 87.883995,54.0936945 87.883995,48.3010279 L87.883995,10.5221271 C87.8841861,4.72907813 83.1715503,0.0166335071 77.3788837,0.0166335071 Z M82.0714446,48.3008367 C82.0714446,50.8883986 79.9662544,52.9933976 77.3788837,52.9933976 L10.5054936,52.9933976 C7.91793174,52.9933976 5.81274156,50.8883986 5.81274156,48.3008367 L5.81274156,22.983683 L82.0712534,22.983683 L82.0714446,48.3008367 L82.0714446,48.3008367 Z M82.0714446,14.6908282 L5.81293275,14.6963727 L5.81293275,10.521936 C5.81293275,7.93437406 7.91812293,5.82918387 10.5056848,5.82918387 L77.3788837,5.82918387 C79.9662544,5.82918387 82.0714446,7.93437406 82.0714446,10.521936 L82.0714446,14.6908282 Z" id="Shape"></path>
|
||||
<path d="M13.0269039,47.6024206 L28.9396256,47.6024206 C29.5093711,47.6024206 29.9716678,47.1405062 29.9716678,46.5707608 L29.9716678,43.4312341 C29.9716678,42.8612975 29.5093711,42.3993831 28.9396256,42.3993831 L13.0269039,42.3993831 C12.4569673,42.3993831 11.9948617,42.8612975 11.9948617,43.4312341 L11.9948617,46.5707608 C11.9946705,47.1405062 12.4567761,47.6024206 13.0269039,47.6024206 Z" id="Shape"></path>
|
||||
<path d="M64.6150569,47.6024206 L69.34815,47.6024206 C72.079869,47.6024206 74.2942285,45.388061 74.2942285,42.6565333 L74.2942285,37.9234402 C74.2942285,35.1919124 72.079869,32.9775529 69.34815,32.9775529 L64.6150569,32.9775529 C61.8835291,32.9775529 59.6691696,35.1919124 59.6691696,37.9234402 L59.6691696,42.6565333 C59.6691696,45.388061 61.8835291,47.6024206 64.6150569,47.6024206 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
12
hosting/static/hosting/img/key.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="90px" height="90px" viewBox="0 0 90 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 23</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="dashboard" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="img_16097" transform="translate(12.000000, 14.000000)" fill-rule="nonzero">
|
||||
<path d="M63.5394739,34.6343332 C59.133452,30.2283113 51.9992431,30.2283113 47.5932212,34.6343332 C47.0271224,35.200432 46.5428692,35.8142741 46.1268207,36.455398 L10.6262881,0.954865429 C9.37132214,-0.300100564 7.32518194,-0.300100564 6.07021594,0.954865429 L3.28746526,3.73761611 C2.0256788,4.9925821 2.03249927,7.03872231 3.28746526,8.2936883 L6.56811006,11.5743331 L0.920763093,17.2216801 C-0.306921031,18.4493642 -0.306921031,20.4341202 0.920763093,21.6618043 C2.14844722,22.8894884 4.13320322,22.8894884 5.36088734,21.6618043 L11.0082343,16.0144573 L12.2563798,17.2626029 L6.60903287,22.9167703 C5.38134874,24.1444544 5.38134874,26.1292104 6.60903287,27.3568946 C7.83671699,28.5845787 9.82147299,28.5845787 11.0491571,27.3568946 L16.6965041,21.7095476 L38.7811774,43.8010413 C38.1400534,44.2170899 37.5262114,44.701343 36.9601126,45.2674418 C32.5540907,49.6734637 32.5540907,56.8076726 36.9601126,61.2136945 C40.5613194,64.8149013 45.9835909,65.4628457 50.246383,63.1848096 C54.5091751,65.4696661 59.9382671,64.8149013 63.5326534,61.2136945 C67.1338602,57.6124877 67.7818046,52.1902162 65.5037685,47.9274241 C67.7954455,43.6578115 67.1406806,38.2287195 63.5394739,34.6343332 Z M58.7037625,56.3711627 C56.6780837,58.3968415 53.3906184,58.3968415 51.3649396,56.3711627 C50.9216093,55.9278323 50.5942268,55.4231177 50.34869,54.8843008 C48.1729609,56.9031591 44.7763682,56.8690568 42.6620233,54.7547119 C40.4931147,52.5858033 40.4931147,49.0732626 42.6620233,46.9111745 C44.0465782,45.5266196 45.96995,45.0355459 47.7500919,45.4174921 C47.3681458,43.6373501 47.8592194,41.7139783 49.2437743,40.3294235 C51.4126829,38.1605148 54.9252236,38.1605148 57.0873117,40.3294235 C59.2016566,42.4437683 59.235759,45.8403611 57.2169006,48.0160902 C57.7557175,48.2684475 58.2604321,48.5958299 58.7037625,49.0323398 C60.7294413,51.0580186 60.7294413,54.3454839 58.7037625,56.3711627 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
14
hosting/static/hosting/img/plusVM.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="90px" height="90px" viewBox="0 0 90 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 23</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="dashboard" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="img_194174" transform="translate(36.000000, 36.000000)" fill-rule="nonzero">
|
||||
<path d="M37.8716732,11.3625619 C37.8716732,12.857078 39.0853051,14.0654101 40.5745214,14.0654101 C42.0637378,14.0654101 43.2773696,12.8517783 43.2773696,11.3625619 C43.2773696,9.86804583 42.0637378,8.65971369 40.5745214,8.65971369 C39.0853051,8.65971369 37.8716732,9.86804583 37.8716732,11.3625619 Z M37.8716732,28.6713899 C37.8716732,30.165906 39.0853051,31.3742381 40.5745214,31.3742381 C42.0637378,31.3742381 43.2773696,30.1606063 43.2773696,28.6713899 C43.2773696,27.1768738 42.0637378,25.9685417 40.5745214,25.9685417 C39.0853051,25.9685417 37.8716732,27.1821735 37.8716732,28.6713899 L37.8716732,28.6713899 Z M0,35.7040949 C0,38.0942607 1.93439137,40.0339518 4.32985685,40.0339518 L22.7251238,40.0339518 L22.7251238,43.0229839 C22.1951536,43.378064 21.7393792,43.8338384 21.3842991,44.3638086 L8.11914405,44.3638086 C6.62462798,44.3638086 5.41629583,45.5774405 5.41629583,47.0666568 C5.41629583,48.5558732 6.62992768,49.7695051 8.11914405,49.7695051 L21.3842991,49.7695051 C22.25875,51.0785315 23.7426667,51.9317836 25.4332717,51.9317836 C27.1185771,51.9317836 28.6024938,51.0785315 29.4822443,49.7695051 L43.8285387,49.7695051 C45.3230548,49.7695051 46.5313869,48.5558732 46.5313869,47.0666568 C46.5313869,45.5774405 45.3177551,44.3638086 43.8285387,44.3638086 L29.4769446,44.3638086 C29.1218646,43.8338384 28.6660902,43.378064 28.1361199,43.0229839 L28.1361199,40.0392515 L47.6125262,40.0392515 C50.002692,40.0392515 51.942383,38.1048601 51.942383,35.7093946 L51.942383,4.32985685 C51.942383,1.93969107 50.0079917,0 47.6125262,0 L4.32985685,0 C1.93439137,0 0,1.93439137 0,4.32985685 L0,35.7040949 L0,35.7040949 Z M7.7905625,5.41099613 L44.1465208,5.41099613 C45.4661467,5.41099613 46.5260872,6.47093661 46.5260872,7.7905625 L46.5260872,14.9292616 C46.5260872,16.2488875 45.4661467,17.308828 44.1465208,17.308828 L7.7905625,17.308828 C6.47093661,17.308828 5.41099613,16.2488875 5.41099613,14.9292616 L5.41099613,7.7905625 C5.41099613,6.47093661 6.47093661,5.41099613 7.7905625,5.41099613 L7.7905625,5.41099613 L7.7905625,5.41099613 Z M7.7905625,22.7198241 L44.1465208,22.7198241 C45.4661467,22.7198241 46.5260872,23.7797646 46.5260872,25.0993905 L46.5260872,32.2380896 C46.5260872,33.5577155 45.4661467,34.617656 44.1465208,34.617656 L7.7905625,34.617656 C6.47093661,34.617656 5.41099613,33.5577155 5.41099613,32.2380896 L5.41099613,25.1046902 C5.41099613,23.7850643 6.47093661,22.7198241 7.7905625,22.7198241 L7.7905625,22.7198241 L7.7905625,22.7198241 Z" id="Shape"></path>
|
||||
</g>
|
||||
<rect id="Rectangle-3" x="23" y="3" width="10" height="50"></rect>
|
||||
<rect id="Rectangle-4" x="3" y="23" width="50" height="10"></rect>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
53
hosting/static/hosting/img/settings.svg
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="340.274px" height="340.274px" viewBox="0 0 340.274 340.274" style="enable-background:new 0 0 340.274 340.274;"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M293.629,127.806l-5.795-13.739c19.846-44.856,18.53-46.189,14.676-50.08l-25.353-24.77l-2.516-2.12h-2.937
|
||||
c-1.549,0-6.173,0-44.712,17.48l-14.184-5.719c-18.332-45.444-20.212-45.444-25.58-45.444h-35.765
|
||||
c-5.362,0-7.446-0.006-24.448,45.606l-14.123,5.734C86.848,43.757,71.574,38.19,67.452,38.19l-3.381,0.105L36.801,65.032
|
||||
c-4.138,3.891-5.582,5.263,15.402,49.425l-5.774,13.691C0,146.097,0,147.838,0,153.33v35.068c0,5.501,0,7.44,46.585,24.127
|
||||
l5.773,13.667c-19.843,44.832-18.51,46.178-14.655,50.032l25.353,24.8l2.522,2.168h2.951c1.525,0,6.092,0,44.685-17.516
|
||||
l14.159,5.758c18.335,45.438,20.218,45.427,25.598,45.427h35.771c5.47,0,7.41,0,24.463-45.589l14.195-5.74
|
||||
c26.014,11,41.253,16.585,45.349,16.585l3.404-0.096l27.479-26.901c3.909-3.945,5.278-5.309-15.589-49.288l5.734-13.702
|
||||
c46.496-17.967,46.496-19.853,46.496-25.221v-35.029C340.268,146.361,340.268,144.434,293.629,127.806z M170.128,228.474
|
||||
c-32.798,0-59.504-26.187-59.504-58.364c0-32.153,26.707-58.315,59.504-58.315c32.78,0,59.43,26.168,59.43,58.315
|
||||
C229.552,202.287,202.902,228.474,170.128,228.474z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
18
hosting/static/hosting/img/shopping-cart.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="447.992,336 181.555,336 69.539,80 0.008,80 0.008,48 90.477,48 202.492,304 447.992,304 "/>
|
||||
</g>
|
||||
<path d="M287.992,416c0,26.5-21.5,48-48,48s-48-21.5-48-48s21.5-48,48-48S287.992,389.5,287.992,416z"/>
|
||||
<path d="M447.992,416c0,26.5-21.5,48-48,48s-48-21.5-48-48s21.5-48,48-48S447.992,389.5,447.992,416z"/>
|
||||
<g>
|
||||
<polygon points="499.18,144 511.992,112 160.008,112 172.805,144 "/>
|
||||
<polygon points="211.195,240 223.992,272 447.992,272 460.805,240 "/>
|
||||
<polygon points="486.398,176 185.602,176 198.398,208 473.586,208 "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1 KiB |
12
hosting/static/hosting/img/vm.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="90px" height="90px" viewBox="0 0 90 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 23</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="dashboard" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="img_194174" transform="translate(12.000000, 12.000000)" fill-rule="nonzero">
|
||||
<path d="M49.0289809,14.7100665 C49.0289809,16.6448793 50.6001587,18.2091961 52.5281105,18.2091961 C54.4560623,18.2091961 56.0272401,16.6380183 56.0272401,14.7100665 C56.0272401,12.7752536 54.4560623,11.2109369 52.5281105,11.2109369 C50.6001587,11.2109369 49.0289809,12.7752536 49.0289809,14.7100665 Z M49.0289809,37.1182181 C49.0289809,39.053031 50.6001587,40.6173477 52.5281105,40.6173477 C54.4560623,40.6173477 56.0272401,39.0461699 56.0272401,37.1182181 C56.0272401,35.1834053 54.4560623,33.6190885 52.5281105,33.6190885 C50.6001587,33.6190885 49.0289809,35.1902663 49.0289809,37.1182181 L49.0289809,37.1182181 Z M0,46.2228162 C0,49.3171445 2.50427904,51.8282846 5.60546843,51.8282846 L29.420133,51.8282846 L29.420133,55.6979103 C28.7340291,56.1575999 28.1439798,56.7476492 27.6842902,57.433753 L10.5111109,57.433753 C8.57629809,57.433753 7.01198132,59.0049308 7.01198132,60.9328827 C7.01198132,62.8608345 8.58315913,64.4320123 10.5111109,64.4320123 L27.6842902,64.4320123 C28.8163616,66.1266888 30.7374523,67.231316 32.9261236,67.231316 C35.1079339,67.231316 37.0290246,66.1266888 38.167957,64.4320123 L56.7407882,64.4320123 C58.675601,64.4320123 60.2399178,62.8608345 60.2399178,60.9328827 C60.2399178,59.0049308 58.66874,57.433753 56.7407882,57.433753 L38.161096,57.433753 C37.7014064,56.7476492 37.1113571,56.1575999 36.4252532,55.6979103 L36.4252532,51.8351456 L61.6395696,51.8351456 C64.733898,51.8351456 67.245038,49.3308666 67.245038,46.2296772 L67.245038,5.60546843 C67.245038,2.51114008 64.740759,0 61.6395696,0 L5.60546843,0 C2.50427904,0 0,2.50427904 0,5.60546843 L0,46.2228162 L0,46.2228162 Z M10.0857266,7.00512028 L57.1524505,7.00512028 C58.860849,7.00512028 60.2330567,8.37732797 60.2330567,10.0857266 L60.2330567,19.3275454 C60.2330567,21.035944 58.860849,22.4081516 57.1524505,22.4081516 L10.0857266,22.4081516 C8.37732797,22.4081516 7.00512028,21.035944 7.00512028,19.3275454 L7.00512028,10.0857266 C7.00512028,8.37732797 8.37732797,7.00512028 10.0857266,7.00512028 L10.0857266,7.00512028 L10.0857266,7.00512028 Z M10.0857266,29.4132719 L57.1524505,29.4132719 C58.860849,29.4132719 60.2330567,30.7854796 60.2330567,32.4938782 L60.2330567,41.735697 C60.2330567,43.4440956 58.860849,44.8163033 57.1524505,44.8163033 L10.0857266,44.8163033 C8.37732797,44.8163033 7.00512028,43.4440956 7.00512028,41.735697 L7.00512028,32.5007392 C7.00512028,30.7923407 8.37732797,29.4132719 10.0857266,29.4132719 L10.0857266,29.4132719 L10.0857266,29.4132719 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3 KiB |
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -38,7 +38,6 @@
|
|||
<![endif]-->
|
||||
|
||||
{% with 'hosting/img/'|add:hosting|add:'-intro-bg.png' as image_static %}
|
||||
alt="">
|
||||
<style media="screen" type="text/css">
|
||||
.intro-header {
|
||||
background: url("{% static image_static %}") no-repeat center center;
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<link href="{% static 'hosting/css/orders.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'hosting/css/commons.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'hosting/css/virtual-machine.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'hosting/css/dashboard.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href='//fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
|
||||
|
|
50
hosting/templates/hosting/dashboard.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hosting-dashboard">
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-container-head">
|
||||
<h1 class="dashboard-title-thin">{% trans "My Dashboard" %}</h1>
|
||||
</div>
|
||||
<div class="hosting-dashboard-content">
|
||||
<a href="{% url 'hosting:create_virtual_machine' %}" class="hosting-dashboard-item">
|
||||
<h2>{% trans "Create VM" %}</h2>
|
||||
<div class="hosting-dashboard-image">
|
||||
<img class="svg-img" src="{% static 'hosting/img/plusVM.svg' %}">
|
||||
</div>
|
||||
</a>
|
||||
<a href="{% url 'hosting:virtual_machines' %}" class="hosting-dashboard-item">
|
||||
<h2>{% trans "My VMs" %}</h2>
|
||||
<div class="hosting-dashboard-image">
|
||||
<img class="svg-img" src="{% static 'hosting/img/vm.svg' %}">
|
||||
</div>
|
||||
</a>
|
||||
<a href="{% url 'hosting:ssh_keys' %}" class="hosting-dashboard-item">
|
||||
<h2>{% trans "My SSH Keys" %}</h2>
|
||||
<div class="hosting-dashboard-image">
|
||||
<img class="svg-img" src="{% static 'hosting/img/key.svg' %}">
|
||||
</div>
|
||||
</a>
|
||||
<a href="{% url 'hosting:orders' %}" class="hosting-dashboard-item">
|
||||
<h2>{% trans "My Bills" %}</h2>
|
||||
<div class="hosting-dashboard-image">
|
||||
<img class="svg-img" src="{% static 'hosting/img/billing.svg' %}">
|
||||
</div>
|
||||
</a>
|
||||
<a href="" class="hosting-dashboard-item">
|
||||
<h2>{% trans "My Settings" %}</h2>
|
||||
<div class="hosting-dashboard-image">
|
||||
<img class="svg-img" src="{% static 'hosting/img/dashboard_settings.svg' %}">
|
||||
</div>
|
||||
</a>
|
||||
<a href="mailto:support@datacenterlight.ch" class="hosting-dashboard-item">
|
||||
<h2>{% trans "Support / Contact" %}</h2>
|
||||
<div class="hosting-dashboard-image">
|
||||
<img class="svg-img" src="{% static 'hosting/img/24-hours-support.svg' %}">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%endblock%}
|
|
@ -3,70 +3,46 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-container-head">
|
||||
<h3 class="dashboard-title-thin"><img src="{% static 'hosting/img/shopping-cart.svg' %}" class="un-icon" style="margin-top: -4px; width: 30px;"> {% trans "My Orders" %}</h3>
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning">
|
||||
{% for message in messages %}
|
||||
<span>{{ message }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dashboard-subtitle"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="orders-container">
|
||||
<div class="row">
|
||||
<div class="container-table col-md-8 col-md-offset-2">
|
||||
<table class="table borderless table-hover">
|
||||
<h3><i class="fa fa-credit-card fa-separate"></i>{% trans "My Orders"%}</h3>
|
||||
<br/>
|
||||
<table class="table table-switch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{% trans "Date"%}</th>
|
||||
<th>{% trans "Amount"%}</th>
|
||||
<th>{% trans "Status"%}</th>
|
||||
<th>{% trans "Order Nr." %}</th>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Amount" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td scope="row">{{ order.id }}</td>
|
||||
<td>{{ order.created_at | date:"M d, Y" }}</td>
|
||||
<td>{{ order.price }} CHF</td>
|
||||
<td>{% if order.approved %}
|
||||
<span class="text-success strong">{% trans "Approved"%}</span>
|
||||
<td class="xs-td-inline" data-header="{% trans 'Order Nr.' %}">{{ order.id }}</td>
|
||||
<td class="xs-td-bighalf" data-header="{% trans 'Date' %}">{{ order.created_at | date:"M d, Y" }}</td>
|
||||
<td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price }}</td>
|
||||
<td data-header="{% trans 'Status' %}">
|
||||
{% if order.approved %}
|
||||
<span class="vm-status-active"><strong>Approved</strong></span>
|
||||
{% else %}
|
||||
<span class="text-danger strong">{% trans "Declined"%}</span>
|
||||
<span class="vm-status-failed"><strong>Declined</strong></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-default"
|
||||
href="{% url 'hosting:orders' order.id %}">{% trans "View Detail"%}</a>
|
||||
<button type="button" class="btn btn-default" data-toggle="modal"
|
||||
data-target="#Modal{{ order.id }}"><a
|
||||
href="#">{% trans "Cancel Order"%}</a>
|
||||
</button>
|
||||
<td class="text-right last-td">
|
||||
<a class="btn btn-order-detail" href="{% url 'hosting:orders' order.pk %}">{% trans 'See Invoice' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="modal fade" id="Modal{{ order.id }}" tabindex="-1" role="dialog"
|
||||
aria-labelledby="exampleModalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Confirm"><span
|
||||
aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-icon"><i class="fa fa-trash" aria-hidden="true"></i></div>
|
||||
<h4 class="modal-title" id="ModalLabel">{% trans "Do you want to delete your order?"%}</h4>
|
||||
|
||||
<form method="post"
|
||||
action="{% url 'hosting:delete_order' order.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger">{% trans "Delete"%}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -75,23 +51,16 @@
|
|||
<div class="pagination">
|
||||
<span class="page-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">{% trans "previous"%}</a>
|
||||
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a>
|
||||
{% endif %}
|
||||
<span class="page-current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
{% if page_obj.has_next %}
|
||||
<a href="{{ request.path }}?page={{ page_obj.next_page_number }}">{% trans "next"%}</a>
|
||||
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next" %}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -46,9 +46,9 @@
|
|||
{%trans "Total" %} <span>{%trans "including VAT" %}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-no-padding">
|
||||
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6"></div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-total">{{request.session.specs.price}}
|
||||
CHF
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4"></div>
|
||||
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 tbl-total">{{request.session.specs.price}}
|
||||
CHF<span class="dcl-price-month">/{% trans "Month" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -64,7 +64,6 @@
|
|||
{% csrf_token %}
|
||||
{% bootstrap_field field show_label=False type='fields'%}
|
||||
{% endfor %}
|
||||
{% bootstrap_form_errors form type='non_fields'%}
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-7 col-md-6 creditcard-box dcl-creditcard">
|
||||
|
@ -91,12 +90,25 @@
|
|||
</form>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-addtional-margin">
|
||||
{% blocktrans %}
|
||||
You are not making any payment yet. After submitting your card
|
||||
information, you will be taken to the Confirm Order Page.
|
||||
{% endblocktrans %}
|
||||
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled"><li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li></ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="col-xs-6 pull-right">
|
||||
|
@ -135,12 +147,26 @@
|
|||
<div id="card-errors" role="alert"></div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content">
|
||||
{% blocktrans %}
|
||||
You are not making any payment yet. After submitting your card
|
||||
information, you will be taken to the Confirm Order Page.
|
||||
{% endblocktrans %}
|
||||
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled"><li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li></ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="col-xs-6 pull-right">
|
||||
|
@ -155,15 +181,6 @@
|
|||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</div>
|
||||
{% if paymentError %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>
|
||||
{% bootstrap_alert paymentError alert_type='danger' %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% block content %}
|
||||
<div>
|
||||
<div class="container virtual-machine-container dashboard-container ">
|
||||
<h1 class="h1-thin"><i class="fa fa-key" aria-hidden="true"></i> {% trans "Your SSH Keys" %}</h1>
|
||||
<h1 class="h1-thin"><i class="fa fa-key" aria-hidden="true"></i> {% trans "My SSH Keys" %}</h1>
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning">
|
||||
{% for message in messages %}
|
||||
|
@ -50,7 +50,7 @@
|
|||
<div class="modal-body">
|
||||
<div class="modal-icon"><i class="fa fa-trash" aria-hidden="true"></i></div>
|
||||
<h4 class="modal-title" id="ModalLabel">{% trans "Delete SSH Key"%}</h4>
|
||||
<p class="modal-text">{% trans "Do You want to delete this key?"%}</p>
|
||||
<p class="modal-text">{% trans "Do you want to delete this key?"%}</p>
|
||||
<form method="post" action="{% url 'hosting:delete_ssh_key' user_key.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-footer">
|
||||
|
@ -77,8 +77,8 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h4 class="modal-title" id="ModalLabel_Public_Key">{% trans "Public ssh key" %}</h4>
|
||||
<p style="margin-top: 10px;">{{ user_key.public_key }}</p>
|
||||
<h4 class="modal-title" id="ModalLabel_Public_Key">{% trans "Public SSH Key" %}</h4>
|
||||
<p class="key_contain" style="margin-top: 10px;">{{ user_key.public_key }}</p>
|
||||
<div class="modal-footer">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,149 +3,6 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<div class="virtual-machine-container dashboard-container ">
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-md-offset-2">
|
||||
<div class="col-sm-12">
|
||||
<h3><i class="fa fa-cloud fa-separate" aria-hidden="true"></i> {{virtual_machine.name}}</h3>
|
||||
<hr/>
|
||||
<div class="col-md-3"> <!-- required for floating -->
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs tabs-left sideways">
|
||||
<li class="active">
|
||||
<a href="#settings-v" data-toggle="tab">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{% trans "Settings"%}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#billing-v" data-toggle="tab">
|
||||
<i class="fa fa-money" aria-hidden="true"></i>
|
||||
{% trans "Billing"%}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#status-v" data-toggle="tab">
|
||||
<i class="fa fa-signal" aria-hidden="true"></i> {% trans "Status"%}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="settings-v">
|
||||
<div class="row">
|
||||
<div class="col-md-12 inline-headers">
|
||||
<h3>{{virtual_machine.hosting_company_name}}</h3>
|
||||
|
||||
{% if virtual_machine.ipv6 %}
|
||||
<div class="pull-right right-place">
|
||||
<button type="link"
|
||||
data-clipboard-text="{{virtual_machine.ipv4}}" id="copy_vm_id" class="to_copy btn btn-link"
|
||||
data-toggle="tooltip" data-placement="bottom" title="Copied" data-trigger="click">
|
||||
Ipv4: {{virtual_machine.ipv4}} <i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="link"
|
||||
data-clipboard-text="{{virtual_machine.ipv6}}" id="copy_vm_id" class="to_copy btn btn-link"
|
||||
data-toggle="tooltip" data-placement="bottom" title="Copied" data-trigger="click">
|
||||
Ipv6: {{virtual_machine.ipv6}} <i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="pull-right right-place">
|
||||
<span class="label label-warning"><strong>{% trans "Ip not assigned yet"%}</strong></span>
|
||||
<i data-toggle="tooltip" title="Your ip will be assigned soon" class="fa fa-info-circle" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="well text-center box-setting">
|
||||
<i class="fa fa-cubes" aria-hidden="true"></i>
|
||||
<span>{% trans "Cores"%}</span>
|
||||
<span class="label label-success">{{virtual_machine.cores}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="well text-center box-setting">
|
||||
<i class="fa fa-tachometer" aria-hidden="true"></i> {% trans "Memory"%} <br/>
|
||||
<span class="label label-success">{{virtual_machine.memory}} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="well text-center box-setting">
|
||||
<i class="fa fa-hdd-o" aria-hidden="true"></i>
|
||||
<span>{% trans "Disk"%}</span>
|
||||
<span class="label label-success">{{virtual_machine.disk_size|floatformat:2}} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div><!--/row-->
|
||||
</div><!--/col-12-->
|
||||
</div><!--/row-->
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% trans "Configuration"%}: {{virtual_machine.configuration}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="tab-pane" id="billing-v">
|
||||
|
||||
<div class="row ">
|
||||
<div class="col-md-12 inline-headers">
|
||||
<h3>{% trans "Current pricing"%}</h3>
|
||||
<span class="h3 pull-right"><strong>{{virtual_machine.price|floatformat}} CHF</strong>/month</span>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="status-v">
|
||||
<div class="row ">
|
||||
<div class="col-md-12 inline-headers">
|
||||
<h3>{% trans "Current status"%}</h3>
|
||||
|
||||
<div class="pull-right space-above">
|
||||
{% if virtual_machine.state == 'PENDING' %}
|
||||
<span class="label
|
||||
label-warning"><strong>Pending</strong></span>
|
||||
{% elif virtual_machine.state == 'ACTIVE' %}
|
||||
<span class="label
|
||||
label-success"><strong>Online</strong></span>
|
||||
{% elif virtual_machine.state == 'FAILED'%}
|
||||
<span class="label
|
||||
label-danger"><strong>Failed</strong></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not virtual_machine.status == 'canceled' %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 separate-md">
|
||||
<div class="pull-right">
|
||||
<form method="POST"
|
||||
id="virtual_machine_cancel_form" class="cancel-form" action="{% url 'hosting:virtual_machines' virtual_machine.vm_id %}">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
|
||||
<button type="text" data-href="{% url 'hosting:virtual_machines' virtual_machine.vm_id %}" data-toggle="modal" data-target="#confirm-cancel" class="btn btn-danger">{% trans "Terminate Virtual Machine"%}</button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning">
|
||||
{% for message in messages %}
|
||||
|
@ -153,43 +10,103 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="virtual-machine-container dashboard-container">
|
||||
<h1 class="dashboard-title-thin">{% trans "Your Virtual Machine Detail" %}</h1>
|
||||
<div class="vm-detail-contain">
|
||||
<div class="vm-detail-item">
|
||||
<h2 class="vm-detail-title">{% trans "VM Settings" %} <img src="{% static 'hosting/img/settings.svg' %}" class="un-icon"></h2>
|
||||
<h3 class="vm-name">{{virtual_machine.name}}</h3>
|
||||
{% if virtual_machine.ipv6 %}
|
||||
<div class="vm-detail-ip">
|
||||
<p>
|
||||
<span>IPv4:</span>
|
||||
<span class="value">{{virtual_machine.ipv4}}</span>
|
||||
<button data-clipboard-text="{{virtual_machine.ipv4}}" class="to_copy btn btn-link" data-toggle="tooltip" data-placement="left" title="{% trans 'Copied' %}" data-trigger="click">
|
||||
<img class="un-icon" src="{% static 'hosting/img/copy.svg' %}">
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<span>IPv6:</span>
|
||||
<span class="value value-sm-block">{{virtual_machine.ipv6}}</span>
|
||||
<button data-clipboard-text="{{virtual_machine.ipv6}}" class="to_copy btn btn-link" data-toggle="tooltip" data-placement="left" title="{% trans 'Copied' %}" data-trigger="click">
|
||||
<img class="un-icon" src="{% static 'hosting/img/copy.svg' %}">
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="vm-detail-config">
|
||||
<p><span>{% trans "Cores" %}:</span><span class="value">{{virtual_machine.cores}}</span></p>
|
||||
<p><span>{% trans "Memory" %}:</span><span class="value">{{virtual_machine.memory}} GB</span></p>
|
||||
<p><span>{% trans "Disk" %}:</span><span class="value">{{virtual_machine.disk_size|floatformat:2}} GB</span></p>
|
||||
<p><span>{% trans "Configuration" %}:</span><span class="value">{{virtual_machine.configuration}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-detail-item">
|
||||
<h2 class="vm-detail-title">{% trans "Billing" %} <img src="{% static 'hosting/img/billing.svg' %}" class="un-icon"></h2>
|
||||
<div class="vm-vmid">
|
||||
<div class="vm-item-subtitle">{% trans "Current Pricing" %}</div>
|
||||
<div class="vm-item-lg">{{virtual_machine.price|floatformat}} CHF/{% trans "Month" %}</div>
|
||||
<a class="btn btn-vm-invoice" href="{% url 'hosting:orders' order.pk %}">{% trans "See Invoice" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-detail-item">
|
||||
<h2 class="vm-detail-title">{% trans "Status" %} <img src="{% static 'hosting/img/connected.svg' %}" class="un-icon"></h2>
|
||||
<div class="vm-vmid">
|
||||
<div class="vm-item-subtitle">{% trans "Your VM is" %}</div>
|
||||
{% if virtual_machine.state == 'PENDING' %}
|
||||
<div class="vm-item-lg vm-color-pending">{% trans "Pending" %}</div>
|
||||
{% elif virtual_machine.state == 'ACTIVE' %}
|
||||
<div class="vm-item-lg vm-color-online">{% trans "Online" %}</div>
|
||||
{% elif virtual_machine.state == 'FAILED'%}
|
||||
<div class="vm-item-lg vm-color-failed">{% trans "Failed" %}</div>
|
||||
{% endif %}
|
||||
{% if not virtual_machine.status == 'canceled' %}
|
||||
<form method="POST" id="virtual_machine_cancel_form" class="cancel-form" action="{% url 'hosting:virtual_machines' virtual_machine.vm_id %}">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
<button data-href="{% url 'hosting:virtual_machines' virtual_machine.vm_id %}" data-toggle="modal" data-target="#confirm-cancel" class="btn btn-vm-term">{% trans "Terminate VM" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-contact-us">
|
||||
<div>
|
||||
<h2 class="vm-detail-title">{% trans "Support / Contact" %} <img class="un-icon visible-xs" src="{% static 'hosting/img/24-hours-support.svg' %}"></h2>
|
||||
</div>
|
||||
<div class="vm-contact-us-text text-center">
|
||||
<img class="un-icon hidden-xs" src="{% static 'hosting/img/24-hours-support.svg' %}">
|
||||
<div>
|
||||
<span>{% trans "Something doesn't work?" %}</span> <span>{% trans "We are here to help you!" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a class="btn btn-vm-contact" href="mailto:support@datacenterlight.ch">{% trans "CONTACT" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a class="btn btn-vm-back" href="{% url 'hosting:virtual_machines' %}">{% trans "BACK TO LIST" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Modal -->
|
||||
<div class="modal fade" id="confirm-cancel" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Confirm"><span
|
||||
aria-hidden="true">×</span>
|
||||
</button>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Confirm"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-icon"><i class="fa fa-ban" aria-hidden="true"></i></div>
|
||||
<h4 class="modal-title" id="ModalLabel">{% trans "Terminate your Virtual Machine"%}</h4>
|
||||
<p class="modal-text">{% trans "Are you sure do you want to cancel your Virtual Machine "%} {{virtual_machine.name}} ?</p>
|
||||
<div class="modal-text">
|
||||
<p>{% trans "Do you want to cancel your Virtual Machine" %} ?</p>
|
||||
<p><strong>{{virtual_machine.name}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-danger btn-ok">OK</a>
|
||||
<a class="btn btn-danger btn-ok">{% trans "OK" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Cancel Modal -->
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{%endblock%}
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 i18n %}
|
||||
{% block content %}
|
||||
<div>
|
||||
<div class="dashboard-container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 container-table">
|
||||
<table class="table borderless table-hover">
|
||||
<h3 class="pull-left"><i class="fa fa-server fa-separate" aria-hidden="true"></i> {% trans "Virtual Machines"%} </h3>
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-container-head">
|
||||
<h3 class="dashboard-title-thin"><img src="{% static 'hosting/img/vm.svg' %}" class="un-icon"> {% trans "Virtual Machines" %}</h3>
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning">
|
||||
{% for message in messages %}
|
||||
|
@ -16,46 +11,46 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not error %}
|
||||
<p class="pull-right btn-create-vm">
|
||||
<a class="btn btn-success" href="{% url 'hosting:create_virtual_machine' %}" >{% trans "Create VM"%} </a>
|
||||
</p>
|
||||
<br/>
|
||||
<div class="dashboard-subtitle">
|
||||
<p>{% trans 'To create a new virtual machine, click "Create VM"' %}</p>
|
||||
<div class="text-right">
|
||||
<a class="btn btn-vm" href="{% url 'hosting:create_virtual_machine' %}"><span class="css-plus"></span> <span>{% trans "CREATE VM" %}</span></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not error %}
|
||||
<table class="table table-switch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID"%}</th>
|
||||
<th>{% trans "Ipv4"%}</th>
|
||||
<th>{% trans "Ipv6"%}</th>
|
||||
<th>{% trans "Status"%}</th>
|
||||
<th>ID</th>
|
||||
<th>IPv4</th>
|
||||
<th>IPv6</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vm in vms %}
|
||||
<tr>
|
||||
<td scope="row">{{vm.vm_id}}</td>
|
||||
<td data-header="ID">{{vm.vm_id}}</td>
|
||||
{% if vm.ipv6 %}
|
||||
<td>{{vm.ipv4}}</td>
|
||||
|
||||
<td>{{vm.ipv6}}</td>
|
||||
<td data-header="IPv4">{{vm.ipv4}}</td>
|
||||
<td data-header="IPv6">{{vm.ipv6}}</td>
|
||||
{% endif %}
|
||||
|
||||
<td>
|
||||
|
||||
<td data-header="{% trans 'Status' %}">
|
||||
{% if vm.state == 'ACTIVE' %}
|
||||
<span class="h3 label label-success"><strong> {{vm.state}}</strong></span>
|
||||
<span class="vm-status-active"><strong>{{vm.state|title}}</strong></span>
|
||||
{% elif vm.state == 'FAILED' %}
|
||||
<span class="h3 label label-danger"><strong>{{vm.state}}</strong></span>
|
||||
<span class="vm-status-failed"><strong>{{vm.state|title}}</strong></span>
|
||||
{% else %}
|
||||
<span class="h3 label label-warning"><strong>{{vm.state}}</strong></span>
|
||||
<span class="vm-status"><strong>{{vm.state|title}}</strong></span>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-default"><a
|
||||
href="{% url 'hosting:virtual_machines' vm.vm_id %}">{% trans "View Detail"%}</a></button>
|
||||
<td class="text-right last-td">
|
||||
<a class="btn btn-vm-detail" href="{% url 'hosting:virtual_machines' vm.vm_id %}">{% trans "View Detail" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -67,23 +62,16 @@
|
|||
<div class="pagination">
|
||||
<span class="page-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous"%}</a>
|
||||
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a>
|
||||
{% endif %}
|
||||
<span class="page-current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
{% if page_obj.has_next %}
|
||||
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next"%}</a>
|
||||
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next" %}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{%endblock%}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
from .views import DjangoHostingView, RailsHostingView, PaymentVMView,\
|
||||
NodeJSHostingView, LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView, \
|
||||
OrdersHostingListView, OrdersHostingDetailView, VirtualMachinesPlanListView,\
|
||||
VirtualMachineView, OrdersHostingDeleteView, NotificationsView, \
|
||||
MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView, HostingPricingView,\
|
||||
CreateVirtualMachinesView, HostingBillListView, HostingBillDetailView, \
|
||||
SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView, SSHKeyChoiceView
|
||||
from .views import (
|
||||
DjangoHostingView, RailsHostingView, PaymentVMView, NodeJSHostingView,
|
||||
LoginView, SignupView, SignupValidateView, SignupValidatedView, IndexView,
|
||||
NotificationsView, OrdersHostingListView, OrdersHostingDetailView,
|
||||
VirtualMachinesPlanListView, VirtualMachineView, OrdersHostingDeleteView,
|
||||
MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView,
|
||||
HostingPricingView, CreateVirtualMachinesView, HostingBillListView,
|
||||
HostingBillDetailView, SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView,
|
||||
SSHKeyChoiceView, DashboardView)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'index/?$', IndexView.as_view(), name='index'),
|
||||
url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'),
|
||||
url(r'dashboard/?$', DashboardView.as_view(), name='dashboard'),
|
||||
url(r'nodejs/?$', NodeJSHostingView.as_view(), name='nodejshosting'),
|
||||
url(r'rails/?$', RailsHostingView.as_view(), name='railshosting'),
|
||||
url(r'pricing/?$', HostingPricingView.as_view(), name='pricing'),
|
||||
|
@ -20,9 +23,12 @@ urlpatterns = [
|
|||
url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'),
|
||||
url(r'bills/?$', HostingBillListView.as_view(), name='bills'),
|
||||
url(r'bills/(?P<pk>\d+)/?$', HostingBillDetailView.as_view(), name='bills'),
|
||||
url(r'cancel_order/(?P<pk>\d+)/?$', OrdersHostingDeleteView.as_view(), name='delete_order'),
|
||||
url(r'create_virtual_machine/?$', CreateVirtualMachinesView.as_view(), name='create_virtual_machine'),
|
||||
url(r'my-virtual-machines/?$', VirtualMachinesPlanListView.as_view(), name='virtual_machines'),
|
||||
url(r'cancel_order/(?P<pk>\d+)/?$',
|
||||
OrdersHostingDeleteView.as_view(), name='delete_order'),
|
||||
url(r'create_virtual_machine/?$', CreateVirtualMachinesView.as_view(),
|
||||
name='create_virtual_machine'),
|
||||
url(r'my-virtual-machines/?$',
|
||||
VirtualMachinesPlanListView.as_view(), name='virtual_machines'),
|
||||
url(r'my-virtual-machines/(?P<pk>\d+)/?$', VirtualMachineView.as_view(),
|
||||
name='virtual_machines'),
|
||||
url(r'ssh_keys/?$', SSHKeyListView.as_view(),
|
||||
|
@ -44,5 +50,6 @@ urlpatterns = [
|
|||
PasswordResetConfirmView.as_view(), name='reset_password_confirm'),
|
||||
url(r'^logout/?$', auth_views.logout,
|
||||
{'next_page': '/hosting/login?logged_out=true'}, name='logout'),
|
||||
url(r'^validate/(?P<validate_slug>.*)/$', SignupValidatedView.as_view(), name='validate')
|
||||
url(r'^validate/(?P<validate_slug>.*)/$',
|
||||
SignupValidatedView.as_view(), name='validate')
|
||||
]
|
||||
|
|
|
@ -40,6 +40,18 @@ CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a backend
|
|||
connection error. please try again in a few minutes."
|
||||
|
||||
|
||||
class DashboardView(View):
|
||||
template_name = "hosting/dashboard.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {}
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = self.get_context_data()
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class DjangoHostingView(ProcessVMSelectionMixin, View):
|
||||
template_name = "hosting/django.html"
|
||||
|
||||
|
@ -244,7 +256,8 @@ class SignupValidatedView(SignupValidateView):
|
|||
lurl=login_url)
|
||||
else:
|
||||
home_url = '<a href="' + \
|
||||
reverse('datacenterlight:index') + '">Data Center Light</a>'
|
||||
reverse('datacenterlight:index') + \
|
||||
'">Data Center Light</a>'
|
||||
message = '{sorry_message} <br />{go_back_to} {hurl}'.format(
|
||||
sorry_message=_("Sorry. Your request is invalid."),
|
||||
go_back_to=_('Go back to'),
|
||||
|
@ -342,6 +355,15 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView):
|
|||
success_url = reverse_lazy('hosting:ssh_keys')
|
||||
model = UserHostingKey
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
""" Hook to ensure UserHostingKey object is owned by request.user.
|
||||
We reply with a Http404 if the user is not the owner of the key.
|
||||
"""
|
||||
obj = super(SSHKeyDeleteView, self).get_object()
|
||||
if not obj.user == self.request.user:
|
||||
raise Http404
|
||||
return obj
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager()
|
||||
|
@ -547,8 +569,10 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
customer = StripeCustomer.get_or_create(email=owner.email,
|
||||
token=token)
|
||||
if not customer:
|
||||
form.add_error("__all__", "Invalid credit card")
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
msg = _("Invalid credit card")
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg, extra_tags='make_charge_error')
|
||||
return HttpResponseRedirect(reverse('hosting:payment') + '#payment_error')
|
||||
|
||||
# Create Billing Address
|
||||
billing_address = form.save()
|
||||
|
@ -557,15 +581,13 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
stripe_utils = StripeUtils()
|
||||
charge_response = stripe_utils.make_charge(amount=final_price,
|
||||
customer=customer.stripe_id)
|
||||
charge = charge_response.get('response_object')
|
||||
|
||||
# Check if the payment was approved
|
||||
if not charge:
|
||||
context.update({
|
||||
'paymentError': charge_response.get('error'),
|
||||
'form': form
|
||||
})
|
||||
return render(request, self.template_name, context)
|
||||
if not charge_response.get('response_object'):
|
||||
msg = charge_response.get('error')
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg, extra_tags='make_charge_error')
|
||||
return HttpResponseRedirect(reverse('hosting:payment') + '#payment_error')
|
||||
|
||||
charge = charge_response.get('response_object')
|
||||
|
||||
|
@ -824,6 +846,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
serializer = VirtualMachineSerializer(vm)
|
||||
context = {
|
||||
'virtual_machine': serializer.data,
|
||||
'order': HostingOrder.objects.get(vm_id=serializer.data['vm_id'])
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
|
|
@ -19,7 +19,7 @@ REGISTRATION_MESSAGE = {'subject': "Validation mail",
|
|||
'from': 'test@test.com'}
|
||||
|
||||
|
||||
def get_anonymous_user_instance():
|
||||
def get_anonymous_user_instance(CustomUser):
|
||||
return CustomUser(name='Anonymous', email='anonymous@ungleich.ch',
|
||||
validation_slug=make_password(None))
|
||||
|
||||
|
@ -173,7 +173,6 @@ class StripeCustomer(models.Model):
|
|||
Check if there is a registered stripe customer with that email
|
||||
or create a new one
|
||||
"""
|
||||
stripe_customer = None
|
||||
try:
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_customer = cls.objects.get(user__email=email)
|
||||
|
@ -189,7 +188,7 @@ class StripeCustomer(models.Model):
|
|||
user = CustomUser.objects.get(email=email)
|
||||
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_data = stripe_utils.create_customer(token, email)
|
||||
stripe_data = stripe_utils.create_customer(token, email, user.name)
|
||||
if stripe_data.get('response_object'):
|
||||
stripe_cus_id = stripe_data.get('response_object').get('id')
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import ipaddress
|
||||
|
||||
from builtins import hasattr
|
||||
from rest_framework import serializers
|
||||
|
||||
from oca import OpenNebulaException
|
||||
|
@ -32,7 +33,7 @@ class VirtualMachineTemplateSerializer(serializers.Serializer):
|
|||
return 0
|
||||
|
||||
def get_memory(self, obj):
|
||||
return int(obj.template.memory)/1024
|
||||
return int(obj.template.memory) / 1024
|
||||
|
||||
def get_name(self, obj):
|
||||
return obj.name.strip('public-')
|
||||
|
@ -92,7 +93,7 @@ class VirtualMachineSerializer(serializers.Serializer):
|
|||
return manager.get_vm(opennebula_id)
|
||||
|
||||
def get_memory(self, obj):
|
||||
return int(obj.template.memory)/1024
|
||||
return int(obj.template.memory) / 1024
|
||||
|
||||
def get_disk_size(self, obj):
|
||||
template = obj.template
|
||||
|
@ -104,9 +105,9 @@ class VirtualMachineSerializer(serializers.Serializer):
|
|||
def get_price(self, obj):
|
||||
template = obj.template
|
||||
price = float(template.vcpu) * 5.0
|
||||
price += (int(template.memory)/1024 * 2.0)
|
||||
price += (int(template.memory) / 1024 * 2.0)
|
||||
for disk in template.disks:
|
||||
price += int(disk.size)/1024 * 0.6
|
||||
price += int(disk.size) / 1024 * 0.6
|
||||
return price
|
||||
|
||||
def get_configuration(self, obj):
|
||||
|
@ -115,15 +116,30 @@ class VirtualMachineSerializer(serializers.Serializer):
|
|||
return template.name.strip('public-')
|
||||
|
||||
def get_ipv4(self, obj):
|
||||
nic = obj.template.nics[0]
|
||||
if 'vm-ipv6-nat64-ipv4' in nic.network and is_in_v4_range(nic.mac):
|
||||
return str(v4_from_mac(nic.mac))
|
||||
"""
|
||||
Get the IPv4s from the given VM
|
||||
|
||||
:param obj: The VM in contention
|
||||
:return: Returns csv string of all IPv4s added to this VM otherwise returns "-" if no IPv4 is available
|
||||
"""
|
||||
ipv4 = []
|
||||
for nic in obj.template.nics:
|
||||
if hasattr(nic, 'ip'):
|
||||
ipv4.append(nic.ip)
|
||||
if len(ipv4) > 0:
|
||||
return ', '.join(ipv4)
|
||||
else:
|
||||
return '-'
|
||||
|
||||
def get_ipv6(self, obj):
|
||||
nic = obj.template.nics[0]
|
||||
return nic.ip6_global
|
||||
ipv6 = []
|
||||
for nic in obj.template.nics:
|
||||
if hasattr(nic, 'ip6_global'):
|
||||
ipv6.append(nic.ip6_global)
|
||||
if len(ipv6) > 0:
|
||||
return ', '.join(ipv6)
|
||||
else:
|
||||
return '-'
|
||||
|
||||
def get_name(self, obj):
|
||||
return obj.name.strip('public-')
|
||||
|
|
|
@ -83,6 +83,16 @@ wheel==0.29.0
|
|||
django-admin-honeypot==1.0.0
|
||||
coverage==4.3.4
|
||||
git+https://github.com/ungleich/python-oca.git#egg=python-oca
|
||||
djangorestframework
|
||||
djangorestframework==3.6.3
|
||||
flake8==3.3.0
|
||||
python-memcached==1.58
|
||||
celery==4.0.2
|
||||
redis==2.10.5
|
||||
django-celery-results==1.0.1
|
||||
kombu==4.1.0
|
||||
mccabe==0.6.1
|
||||
pycodestyle==2.3.1
|
||||
pyflakes==1.5.0
|
||||
billiard==3.5.0.3
|
||||
amqp==2.2.1
|
||||
vine==1.1.4
|
|
@ -7,7 +7,14 @@ def google_analytics(request):
|
|||
render your Google Analytics tracking code template.
|
||||
"""
|
||||
host = request.get_host()
|
||||
ga_prop_id = getattr(settings, 'GOOGLE_ANALYTICS_PROPERTY_IDS', False).get(host)
|
||||
ga_prop_id = getattr(settings, 'GOOGLE_ANALYTICS_PROPERTY_IDS', False).get(
|
||||
host)
|
||||
if ga_prop_id is None:
|
||||
# Try checking if we have a www in host, if yes we remove
|
||||
# that and check in the dict again
|
||||
if host.startswith('www.'):
|
||||
ga_prop_id = getattr(settings, 'GOOGLE_ANALYTICS_PROPERTY_IDS',
|
||||
False).get(host[4:])
|
||||
if not settings.DEBUG and ga_prop_id:
|
||||
return {
|
||||
'GOOGLE_ANALYTICS_PROPERTY_ID': ga_prop_id
|
||||
|
|
25
utils/migrations/0006_auto_20170810_1742.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2017-08-10 17:42
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('utils', '0005_auto_20170322_1443'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='billingaddress',
|
||||
name='cardholder_name',
|
||||
field=models.CharField(default='', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userbillingaddress',
|
||||
name='cardholder_name',
|
||||
field=models.CharField(default='', max_length=100),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,10 @@
|
|||
import logging
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
from datacenterlight.models import StripePlan
|
||||
|
||||
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handleStripeError(f):
|
||||
|
@ -11,7 +15,7 @@ def handleStripeError(f):
|
|||
'error': None
|
||||
}
|
||||
|
||||
common_message = "Currently its not possible to make payments."
|
||||
common_message = "Currently it's not possible to make payments."
|
||||
try:
|
||||
response_object = f(*args, **kwargs)
|
||||
response = {
|
||||
|
@ -26,7 +30,8 @@ def handleStripeError(f):
|
|||
response.update({'error': err['message']})
|
||||
return response
|
||||
except stripe.error.RateLimitError as e:
|
||||
response.update({'error': "Too many requests made to the API too quickly"})
|
||||
response.update(
|
||||
{'error': "Too many requests made to the API too quickly"})
|
||||
return response
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
response.update({'error': "Invalid parameters"})
|
||||
|
@ -55,6 +60,10 @@ class StripeUtils(object):
|
|||
CURRENCY = 'chf'
|
||||
INTERVAL = 'month'
|
||||
SUCCEEDED_STATUS = 'succeeded'
|
||||
STRIPE_PLAN_ALREADY_EXISTS = 'Plan already exists'
|
||||
STRIPE_NO_SUCH_PLAN = 'No such plan'
|
||||
PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.'
|
||||
PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.'
|
||||
|
||||
def __init__(self):
|
||||
self.stripe = stripe
|
||||
|
@ -90,13 +99,14 @@ class StripeUtils(object):
|
|||
def check_customer(self, id, user, token):
|
||||
customers = self.stripe.Customer.all()
|
||||
if not customers.get('data'):
|
||||
customer = self.create_customer(token, user.email)
|
||||
customer = self.create_customer(token, user.email, user.name)
|
||||
else:
|
||||
try:
|
||||
customer = stripe.Customer.retrieve(id)
|
||||
except stripe.InvalidRequestError:
|
||||
customer = self.create_customer(token, user.email)
|
||||
user.stripecustomer.stripe_id = customer.get('response_object').get('id')
|
||||
customer = self.create_customer(token, user.email, user.name)
|
||||
user.stripecustomer.stripe_id = customer.get(
|
||||
'response_object').get('id')
|
||||
user.stripecustomer.save()
|
||||
return customer
|
||||
|
||||
|
@ -107,11 +117,12 @@ class StripeUtils(object):
|
|||
return customer
|
||||
|
||||
@handleStripeError
|
||||
def create_customer(self, token, email):
|
||||
|
||||
def create_customer(self, token, email, name=None):
|
||||
if name is None or name.strip() == "":
|
||||
name = email
|
||||
customer = self.stripe.Customer.create(
|
||||
source=token,
|
||||
description='description for testing',
|
||||
description=name,
|
||||
email=email
|
||||
)
|
||||
return customer
|
||||
|
@ -128,13 +139,92 @@ class StripeUtils(object):
|
|||
return charge
|
||||
|
||||
@handleStripeError
|
||||
def create_plan(self, amount, name, id):
|
||||
def get_or_create_stripe_plan(self, amount, name, stripe_plan_id):
|
||||
"""
|
||||
This function checks if a StripePlan with the given
|
||||
stripe_plan_id already exists. If it exists then the function
|
||||
returns this object otherwise it creates a new StripePlan and
|
||||
returns the new object.
|
||||
|
||||
:param amount: The amount in CHF
|
||||
:param name: The name of the Stripe plan to be created.
|
||||
:param stripe_plan_id: The id of the Stripe plan to be
|
||||
created. Use get_stripe_plan_id_string function to
|
||||
obtain the name of the plan to be created
|
||||
:return: The StripePlan object if it exists else creates a
|
||||
Plan object in Stripe and a local StripePlan and
|
||||
returns it. Returns None in case of Stripe error
|
||||
"""
|
||||
_amount = float(amount)
|
||||
amount = int(_amount * 100) # stripe amount unit, in cents
|
||||
stripe_plan_db_obj = None
|
||||
try:
|
||||
stripe_plan_db_obj = StripePlan.objects.get(
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
except StripePlan.DoesNotExist:
|
||||
try:
|
||||
self.stripe.Plan.create(
|
||||
amount=amount,
|
||||
interval=self.INTERVAL,
|
||||
name=name,
|
||||
currency=self.CURRENCY,
|
||||
id=id)
|
||||
id=stripe_plan_id)
|
||||
stripe_plan_db_obj = StripePlan.objects.create(
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
if self.STRIPE_PLAN_ALREADY_EXISTS in str(e):
|
||||
logger.debug(
|
||||
self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id))
|
||||
stripe_plan_db_obj = StripePlan.objects.create(
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
return stripe_plan_db_obj
|
||||
|
||||
@handleStripeError
|
||||
def delete_stripe_plan(self, stripe_plan_id):
|
||||
"""
|
||||
Deletes the Plan in Stripe and also deletes the local db copy
|
||||
of the plan if it exists
|
||||
|
||||
:param stripe_plan_id: The stripe plan id that needs to be
|
||||
deleted
|
||||
:return: True if the plan was deleted successfully from
|
||||
Stripe, False otherwise.
|
||||
"""
|
||||
return_value = False
|
||||
try:
|
||||
plan = self.stripe.Plan.retrieve(stripe_plan_id)
|
||||
plan.delete()
|
||||
return_value = True
|
||||
StripePlan.objects.filter(
|
||||
stripe_plan_id=stripe_plan_id).all().delete()
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
if self.STRIPE_NO_SUCH_PLAN in str(e):
|
||||
logger.debug(
|
||||
self.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(stripe_plan_id))
|
||||
return return_value
|
||||
|
||||
@handleStripeError
|
||||
def subscribe_customer_to_plan(self, customer, plans):
|
||||
"""
|
||||
Subscribes the given customer to the list of given plans
|
||||
|
||||
:param customer: The stripe customer identifier
|
||||
:param plans: A list of stripe plans.
|
||||
Ref: https://stripe.com/docs/api/python#create_subscription-items
|
||||
e.g.
|
||||
plans = [
|
||||
{
|
||||
"plan": "dcl-v1-cpu-2-ram-5gb-ssd-10gb",
|
||||
},
|
||||
]
|
||||
:return: The subscription StripeObject
|
||||
"""
|
||||
|
||||
subscription_result = self.stripe.Subscription.create(
|
||||
customer=customer,
|
||||
items=plans,
|
||||
)
|
||||
return subscription_result
|
||||
|
||||
@handleStripeError
|
||||
def make_payment(self, customer, amount, token):
|
||||
|
@ -144,3 +234,29 @@ class StripeUtils(object):
|
|||
customer=customer
|
||||
)
|
||||
return charge
|
||||
|
||||
@staticmethod
|
||||
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None):
|
||||
"""
|
||||
Returns the stripe plan id string of the form
|
||||
`dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters
|
||||
|
||||
:param cpu: The number of cores
|
||||
:param ram: The size of the RAM in GB
|
||||
:param ssd: The size of ssd storage in GB
|
||||
:param hdd: The size of hdd storage in GB
|
||||
:param version: The version of the Stripe plans
|
||||
:param app: The application to which the stripe plan belongs
|
||||
to. By default it is 'dcl'
|
||||
:return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb`
|
||||
"""
|
||||
dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu,
|
||||
ram=ram,
|
||||
ssd=ssd)
|
||||
if hdd is not None:
|
||||
dcl_plan_string = '{dcl_plan_string}-hdd-{hdd}gb'.format(
|
||||
dcl_plan_string=dcl_plan_string, hdd=hdd)
|
||||
stripe_plan_id_string = '{app}-v{version}-{plan}'.format(app=app,
|
||||
version=version,
|
||||
plan=dcl_plan_string)
|
||||
return stripe_plan_id_string
|
||||
|
|
20
utils/tasks.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
from dynamicweb.celery import app
|
||||
from django.core.mail import EmailMessage
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES)
|
||||
def send_plain_email_task(self, email_data):
|
||||
"""
|
||||
This is a generic celery task to be used for sending emails.
|
||||
A celery wrapper task for EmailMessage
|
||||
|
||||
:param self:
|
||||
:param email_data: A dict of all needed email headers
|
||||
:return:
|
||||
"""
|
||||
email = EmailMessage(**email_data)
|
||||
email.send()
|
162
utils/tests.py
|
@ -1,9 +1,17 @@
|
|||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
from django.http.request import HttpRequest
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
import stripe
|
||||
from django.http.request import HttpRequest
|
||||
from django.test import Client
|
||||
from django.test import TestCase
|
||||
from model_mommy import mommy
|
||||
|
||||
from datacenterlight.models import StripePlan
|
||||
from membership.models import StripeCustomer
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class BaseTestCase(TestCase):
|
||||
"""
|
||||
|
@ -11,12 +19,12 @@ class BaseTestCase(TestCase):
|
|||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Password
|
||||
self.dummy_password = 'test_password'
|
||||
|
||||
# Users
|
||||
self.customer, self.another_customer = mommy.make('membership.CustomUser',
|
||||
self.customer, self.another_customer = mommy.make(
|
||||
'membership.CustomUser',
|
||||
_quantity=2)
|
||||
self.customer.set_password(self.dummy_password)
|
||||
self.customer.save()
|
||||
|
@ -83,3 +91,147 @@ class BaseTestCase(TestCase):
|
|||
view.kwargs = kwargs
|
||||
view.config = None
|
||||
return view
|
||||
|
||||
|
||||
class TestStripeCustomerDescription(TestCase):
|
||||
"""
|
||||
A class to test setting the description field of the stripe customer
|
||||
https://stripe.com/docs/api#metadata
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.customer_password = 'test_password'
|
||||
self.customer_email = 'test@ungleich.ch'
|
||||
self.customer_name = "Monty Python"
|
||||
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'
|
||||
},
|
||||
)
|
||||
self.failed_token = stripe.Token.create(
|
||||
card={
|
||||
"number": '4000000000000341',
|
||||
"exp_month": 12,
|
||||
"exp_year": 2022,
|
||||
"cvc": '123'
|
||||
},
|
||||
)
|
||||
|
||||
def test_creating_stripe_customer(self):
|
||||
stripe_data = self.stripe_utils.create_customer(self.token.id,
|
||||
self.customer.email,
|
||||
self.customer_name)
|
||||
self.assertEqual(stripe_data.get('error'), None)
|
||||
customer_data = stripe_data.get('response_object')
|
||||
self.assertEqual(customer_data.description, self.customer_name)
|
||||
|
||||
|
||||
class StripePlanTestCase(TestStripeCustomerDescription):
|
||||
"""
|
||||
A class to test Stripe plans
|
||||
"""
|
||||
|
||||
def test_get_stripe_plan_id_string(self):
|
||||
plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100,
|
||||
version=1, app='dcl')
|
||||
self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb')
|
||||
plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100,
|
||||
version=1, app='dcl',
|
||||
hdd=200)
|
||||
self.assertEqual(plan_id_string,
|
||||
'dcl-v1-cpu-2-ram-20gb-ssd-100gb-hdd-200gb')
|
||||
|
||||
def test_get_or_create_plan(self):
|
||||
stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000,
|
||||
"test plan 1",
|
||||
stripe_plan_id='test-plan-1')
|
||||
self.assertIsNone(stripe_plan.get('error'))
|
||||
self.assertIsInstance(stripe_plan.get('response_object'), StripePlan)
|
||||
|
||||
@patch('utils.stripe_utils.logger')
|
||||
def test_create_duplicate_plans_error_handling(self, mock_logger):
|
||||
"""
|
||||
Test details:
|
||||
1. Create a test plan in Stripe with a particular id
|
||||
2. Try to recreate the plan with the same id
|
||||
3. This creates a Stripe error, the code should be able to handle the error
|
||||
|
||||
:param mock_logger:
|
||||
:return:
|
||||
"""
|
||||
unique_id = str(uuid.uuid4().hex)
|
||||
new_plan_id_str = 'test-plan-{}'.format(unique_id)
|
||||
stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000,
|
||||
"test plan {}".format(
|
||||
unique_id),
|
||||
stripe_plan_id=new_plan_id_str)
|
||||
self.assertIsInstance(stripe_plan.get('response_object'), StripePlan)
|
||||
self.assertEqual(stripe_plan.get('response_object').stripe_plan_id,
|
||||
new_plan_id_str)
|
||||
|
||||
# Test creating the same plan again and expect the PLAN_EXISTS_ERROR_MSG
|
||||
# We first delete the local Stripe Plan, so that the code tries to create a new plan in Stripe
|
||||
StripePlan.objects.filter(
|
||||
stripe_plan_id=new_plan_id_str).all().delete()
|
||||
stripe_plan_1 = self.stripe_utils.get_or_create_stripe_plan(2000,
|
||||
"test plan {}".format(
|
||||
unique_id),
|
||||
stripe_plan_id=new_plan_id_str)
|
||||
mock_logger.debug.assert_called_with(
|
||||
self.stripe_utils.PLAN_EXISTS_ERROR_MSG.format(new_plan_id_str))
|
||||
self.assertIsInstance(stripe_plan_1.get('response_object'), StripePlan)
|
||||
self.assertEqual(stripe_plan_1.get('response_object').stripe_plan_id,
|
||||
new_plan_id_str)
|
||||
|
||||
# Delete the test stripe plan that we just created
|
||||
delete_result = self.stripe_utils.delete_stripe_plan(new_plan_id_str)
|
||||
self.assertIsInstance(delete_result, dict)
|
||||
self.assertEqual(delete_result.get('response_object'), True)
|
||||
|
||||
@patch('utils.stripe_utils.logger')
|
||||
def test_delete_unexisting_plan_should_fail(self, mock_logger):
|
||||
plan_id = 'crazy-plan-id-that-does-not-exist'
|
||||
result = self.stripe_utils.delete_stripe_plan(plan_id)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result.get('response_object'), False)
|
||||
mock_logger.debug.assert_called_with(
|
||||
self.stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id))
|
||||
|
||||
def test_subscribe_customer_to_plan(self):
|
||||
stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000,
|
||||
"test plan 1",
|
||||
stripe_plan_id='test-plan-1')
|
||||
stripe_customer = StripeCustomer.get_or_create(
|
||||
email=self.customer_email,
|
||||
token=self.token)
|
||||
result = self.stripe_utils.subscribe_customer_to_plan(
|
||||
stripe_customer.stripe_id,
|
||||
[{"plan": stripe_plan.get(
|
||||
'response_object').stripe_plan_id}])
|
||||
self.assertIsInstance(result.get('response_object'),
|
||||
stripe.Subscription)
|
||||
self.assertIsNone(result.get('error'))
|
||||
self.assertEqual(result.get('response_object').get('status'), 'active')
|
||||
|
||||
def test_subscribe_customer_to_plan_failed_payment(self):
|
||||
stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000,
|
||||
"test plan 1",
|
||||
stripe_plan_id='test-plan-1')
|
||||
stripe_customer = StripeCustomer.get_or_create(
|
||||
email=self.customer_email,
|
||||
token=self.failed_token)
|
||||
result = self.stripe_utils.subscribe_customer_to_plan(
|
||||
stripe_customer.stripe_id,
|
||||
[{"plan": stripe_plan.get(
|
||||
'response_object').stripe_plan_id}])
|
||||
self.assertIsNone(result.get('response_object'), None)
|
||||
self.assertIsNotNone(result.get('error'))
|
||||
|
|