merged master

This commit is contained in:
Arvind Tiwari 2017-08-20 22:04:48 +05:30
commit 653b3654b8
20 changed files with 707 additions and 284 deletions

1
.gitignore vendored
View file

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

View file

@ -1096,19 +1096,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);
@ -1536,3 +1523,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;
}

169
datacenterlight/tasks.py Normal file
View file

@ -0,0 +1,169 @@
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):
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 payment
charge_object = DictDotLookup(charge)
order.set_stripe_charge(charge_object)
# 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

@ -4,7 +4,6 @@ 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
@ -13,14 +12,14 @@ from django.core.exceptions import ValidationError
from django.views.decorators.cache import cache_control
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from utils.forms import BillingAddressForm, UserBillingAddressForm
from utils.forms import BillingAddressForm
from utils.models import BillingAddress
from hosting.models import HostingOrder, HostingBill
from hosting.models import HostingOrder
from utils.stripe_utils import StripeUtils
from datetime import datetime
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 datacenterlight.tasks import create_vm_task
class LandingProgramView(TemplateView):
@ -33,7 +32,6 @@ 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:
@ -79,8 +77,7 @@ class PricingView(TemplateView):
manager = OpenNebulaManager()
template = manager.get_template(template_id)
request.session['template'] = VirtualMachineTemplateSerializer(
template).data
request.session['template'] = VirtualMachineTemplateSerializer(template).data
if not request.user.is_authenticated():
request.session['next'] = reverse('hosting:payment')
@ -132,8 +129,7 @@ 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', {})
@ -185,8 +181,7 @@ 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())
@ -230,8 +225,7 @@ class IndexView(CreateView):
storage_field = forms.IntegerField(validators=[self.validate_storage])
price = request.POST.get('total')
template_id = int(request.POST.get('config'))
template = VMTemplate.objects.filter(
opennebula_vm_template_id=template_id).first()
template = VMTemplate.objects.filter(opennebula_vm_template_id=template_id).first()
template_data = VMTemplateSerializer(template).data
name = request.POST.get('name')
@ -243,40 +237,35 @@ 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')
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')
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')
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')
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')
messages.add_message(self.request, messages.ERROR, msg, extra_tags='email')
return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form")
specs = {
@ -341,8 +330,7 @@ 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)
@ -411,7 +399,6 @@ class PaymentOrderView(FormView):
# 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
@ -436,8 +423,11 @@ 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') and not card_details.get('paid'):
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,8 +443,6 @@ 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')
@ -462,83 +450,16 @@ class OrderConfirmationView(DetailView):
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 = {}
context.update({
'paymentError': charge_response.get('error')
})
return render(request, self.payment_template_name, context)
if not charge_response.get('response_object') and not charge_response.get('paid'):
msg = charge_response.get('error')
messages.add_message(self.request, messages.ERROR, msg, extra_tags='make_charge_error')
return HttpResponseRedirect(reverse('datacenterlight:payment') + '#payment_error')
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,
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")))
)
# 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()
create_vm_task.delay(vm_template_id, user, specs, template, stripe_customer_id, billing_address_data,
billing_address_id,
charge)
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,21 @@ 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 +138,8 @@ INSTALLED_APPS = (
'datacenterlight.templatetags',
'alplora',
'rest_framework',
'opennebula_api'
'opennebula_api',
'django_celery_results',
)
MIDDLEWARE_CLASSES = (
@ -524,6 +543,15 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = {
'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:

View file

@ -487,17 +487,17 @@ 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 "Page"
msgstr ""
msgid "of"
msgstr ""
msgid "login"
msgstr "einloggen"
@ -543,6 +543,12 @@ msgstr ""
#~ msgid "Terminate Virtual Machine"
#~ msgstr "Virtuelle Maschine beenden"
#~ msgid "Ipv4"
#~ msgstr "IPv4"
#~ msgid "Ipv6"
#~ msgstr "IPv6"
#~ msgid "Close"
#~ msgstr "Schliessen"

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'

View file

@ -169,31 +169,68 @@
vertical-align: middle;
margin-right: -4px;
}
}
}
@media (min-width: 768px) {
@media (min-width: 768px) {
.modal-dialog {
/* width: 520px; */
margin: 15px auto;
}
}
}
.modal {
.modal {
text-align: center;
}
}
.modal-dialog {
.modal-dialog {
display: inline-block;
text-align: left;
vertical-align: middle;
}
}
/* Duplicate */
.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

@ -333,7 +333,7 @@ h6 {
}
.auth-box .form .red {
color: #ea3a3a;
color: #eb4d5c;
}
.auth-box .form .btn {
@ -557,6 +557,10 @@ a.unlink:hover {
border-radius: 3px;
padding: 5px;
}
.card-warning-error {
border: 1px solid #EB4D5C;
color: #EB4D5C;
}
.card-warning-addtional-margin {
margin-top: 15px;
@ -743,3 +747,39 @@ a.unlink:hover {
.footer-light a:hover, .footer-light a:focus, .footer-light a:active {
color: #ddd;
}
/* 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

@ -231,12 +231,6 @@
/* Vm Details */
/* might be duplicated from other PR */
.dashboard-title-thin {
font-weight: 300;
font-size: 30px;
}
.vm-detail-item, .vm-contact-us {
overflow: hidden;
border: 1px solid #ddd;
@ -425,3 +419,157 @@
.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: -1px;
width: 20px;
}
.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;
}
.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: 20px;
width: 18px;
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: 5px;
padding-bottom: 15px;
}
.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;
}
.table-switch .last-td {
position: absolute;
bottom: 20px;
right: 0;
}
}

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="M724.6,224.4c0,28.2,22.9,51,51,51s51-22.9,51-51c0-28.2-22.9-51-51-51S724.6,196.2,724.6,224.4z M724.6,551c0,28.2,22.9,51,51,51s51-22.9,51-51c0-28.2-22.9-51-51-51S724.6,522.9,724.6,551L724.6,551z M10,683.7c0,45.1,36.5,81.7,81.7,81.7h347.1v56.4c-10,6.7-18.6,15.3-25.3,25.3l-250.3,0c-28.2,0-51,22.9-51,51s22.9,51,51,51h250.3c16.5,24.7,44.5,40.8,76.4,40.8c31.8,0,59.8-16.1,76.4-40.8h270.7c28.2,0,51-22.9,51-51s-22.9-51-51-51H566.2c-6.7-10-15.3-18.6-25.3-25.3v-56.3h367.5c45.1,0,81.7-36.5,81.7-81.7V91.7c0-45.1-36.5-81.7-81.7-81.7H91.7C46.5,10,10,46.5,10,91.7L10,683.7L10,683.7z M157,112.1h686c24.9,0,44.9,20,44.9,44.9v134.7c0,24.9-20,44.9-44.9,44.9H157c-24.9,0-44.9-20-44.9-44.9V157C112.1,132.1,132.1,112.1,157,112.1L157,112.1L157,112.1z M157,438.7h686c24.9,0,44.9,20,44.9,44.9v134.7c0,24.9-20,44.9-44.9,44.9H157c-24.9,0-44.9-20-44.9-44.9V483.7C112.1,458.8,132.1,438.7,157,438.7L157,438.7L157,438.7z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -59,7 +59,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">
@ -86,12 +85,28 @@
</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 %}
</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">
@ -130,12 +145,29 @@
<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 %}
</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">
@ -150,15 +182,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

@ -77,7 +77,7 @@
</button>
</div>
<div class="modal-body">
<h4 class="modal-title" id="ModalLabel_Public_Key">{% trans "Public SSH key" %}</h4>
<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>

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">
<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,74 +11,67 @@
{% endfor %}
</div>
{% endif %}
</div>
{% if not error %}
<p class="pull-right btn-create-vm">
<a class="btn btn-success" href="{% url 'hosting:create_virtual_machine' %}" >{% trans "Create VM"%} </a>
</p>
<br/>
<div class="dashboard-subtitle">
<p>{% trans 'To create a new virtual machine, click "Create VM"' %}</p>
<div class="text-right">
<a class="btn btn-vm" href="{% url 'hosting:create_virtual_machine' %}"><span class="css-plus"></span> <span>{% trans "CREATE VM" %}</span></a>
</div>
</div>
{% endif %}
</div>
{% if not error %}
<table class="table table-switch">
<thead>
<tr>
<th>{% trans "ID"%}</th>
<th>{% trans "Ipv4"%}</th>
<th>{% trans "Ipv6"%}</th>
<th>{% trans "Status"%}</th>
<th>ID</th>
<th>IPv4</th>
<th>IPv6</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for vm in vms %}
<tr>
<td scope="row">{{vm.vm_id}}</td>
<td data-header="ID">{{vm.vm_id}}</td>
{% if vm.ipv6 %}
<td>{{vm.ipv4}}</td>
<td>{{vm.ipv6}}</td>
<td data-header="IPv4">{{vm.ipv4}}</td>
<td data-header="IPv6">{{vm.ipv6}}</td>
{% endif %}
<td>
<td data-header="{% trans 'Status' %}">
{% if vm.state == 'ACTIVE' %}
<span class="h3 label label-success"><strong> {{vm.state}}</strong></span>
<span class="vm-status-active"><strong>{{vm.state|title}}</strong></span>
{% elif vm.state == 'FAILED' %}
<span class="h3 label label-danger"><strong>{{vm.state}}</strong></span>
<span class="vm-status-failed"><strong>{{vm.state|title}}</strong></span>
{% else %}
<span class="h3 label label-warning"><strong>{{vm.state}}</strong></span>
<span class="vm-status"><strong>{{vm.state|title}}</strong></span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-default"><a
href="{% url 'hosting:virtual_machines' vm.vm_id %}">{% trans "View Detail"%}</a></button>
<td class="text-right last-td">
<a class="btn btn-vm-detail" href="{% url 'hosting:virtual_machines' vm.vm_id %}">{% trans "View Detail" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
{% endif %}
</table>
{% endif %}
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous"%}</a>
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next"%}</a>
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next" %}</a>
{% endif %}
</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{%endblock%}

View file

@ -557,8 +557,9 @@ 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()
@ -567,15 +568,12 @@ 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') and not charge_response.get('paid'):
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')

View file

@ -86,3 +86,6 @@ git+https://github.com/ungleich/python-oca.git#egg=python-oca
djangorestframework
flake8==3.3.0
python-memcached==1.58
celery==4.0.2
redis==2.10.5
django-celery-results==1.0.1

View file

@ -11,7 +11,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 = {