merge master

This commit is contained in:
Arvind Tiwari 2017-09-01 00:39:01 +05:30
commit 3ed2399337
68 changed files with 3099 additions and 950 deletions

1
.gitignore vendored
View file

@ -35,3 +35,4 @@ secret-key
.env
*.mo
*.log

View file

@ -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

View file

@ -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())

View file

@ -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."

View 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()),
],
),
]

View 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)),
],
),
]

View 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),
),
]

View 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,
),
]

View 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 = [
]

View 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 = [
]

View file

@ -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)

View 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%;
}
}

View file

@ -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;
}

View 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

View file

@ -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
View 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())

View file

@ -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" %}

View 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 %}

View 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 %}

View file

@ -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>
</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>

View file

@ -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>

View file

@ -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()))

View file

@ -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'),
]

View file

@ -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'))

View file

@ -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

View file

@ -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
View 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))

View file

@ -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': {

View file

@ -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,

View file

@ -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"

View 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),
),
]

View file

@ -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

View file

@ -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;
@ -190,3 +194,49 @@
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%);
}

View 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%;
}
}

View file

@ -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;
}

View file

@ -9,7 +9,7 @@
font-size: 14px;
padding-left: 0;
margin-bottom: 30px;
font-family: 'Lato';
font-family: Lato, sans-serif;
}

View file

@ -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;
}

View file

@ -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;
}
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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);
}
});
});

View file

@ -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;

View file

@ -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'>

View 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%}

View file

@ -3,17 +3,23 @@
{% 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 "Order Nr." %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Amount" %}</th>
<th>{% trans "Status" %}</th>
@ -23,50 +29,20 @@
<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">&times;</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>
@ -78,7 +54,7 @@
<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>
@ -86,12 +62,5 @@
</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -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 %}

View file

@ -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>&nbsp;{% trans "Your SSH Keys" %}</h1>
<h1 class="h1-thin"><i class="fa fa-key" aria-hidden="true"></i>&nbsp;{% 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>

View file

@ -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">&times;</span>
</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Confirm"><span aria-hidden="true">&times;</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%}

View file

@ -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-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,18 +11,23 @@
{% 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>ID</th>
<th>IPv4</th>
<th>IPv6</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
@ -35,27 +35,22 @@
<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 %}
@ -70,7 +65,7 @@
<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>
@ -78,12 +73,5 @@
</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{%endblock%}

View file

@ -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')
]

View file

@ -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

View file

@ -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')

View file

@ -1,5 +1,6 @@
import ipaddress
from builtins import hasattr
from rest_framework import serializers
from oca import OpenNebulaException
@ -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-')

View file

@ -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

View file

@ -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

View 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),
),
]

View file

@ -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
View 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()

View file

@ -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'))