Merge branch 'master' into task/3709/faq_tos_cms_template

This commit is contained in:
PCoder 2017-08-24 23:59:09 +05:30
commit 2a76167d10
31 changed files with 1462 additions and 455 deletions

View file

@ -1,3 +1,15 @@
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 1.0.24: 2017-08-15
* #3699: [datacenterlight] Added oneadmin ssh key by default to the created VM via DCL landing * #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 * #3687: [datacenterlight] Added the name of the customer as description field of the stripe metadata

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -276,6 +276,19 @@ msgstr "Konfiguration"
msgid "Total" msgid "Total"
msgstr "" 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" msgid "Place order"
msgstr "Bestellen" msgstr "Bestellen"

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

@ -59,3 +59,15 @@ class VMTemplate(models.Model):
def create(cls, name, opennebula_vm_template_id): 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 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

View file

@ -41,13 +41,15 @@ def retry_task(task, exception=None):
@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES) @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, def create_vm_task(self, vm_template_id, user, specs, template,
stripe_customer_id, billing_address_data,
billing_address_id, billing_address_id,
charge): charge, cc_details):
vm_id = None vm_id = None
try: try:
final_price = specs.get('price') final_price = specs.get('price')
billing_address = BillingAddress.objects.filter(id=billing_address_id).first() billing_address = BillingAddress.objects.filter(
id=billing_address_id).first()
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
# Create OpenNebulaManager # Create OpenNebulaManager
manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME, manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME,
@ -89,9 +91,9 @@ def create_vm_task(self, vm_template_id, user, specs, template, stripe_customer_
billing_address_user_form.is_valid() billing_address_user_form.is_valid()
billing_address_user_form.save() billing_address_user_form.save()
# Associate an order with a stripe payment # Associate an order with a stripe subscription
charge_object = DictDotLookup(charge) charge_object = DictDotLookup(charge)
order.set_stripe_charge(charge_object) order.set_subscription_id(charge_object, cc_details)
# If the Stripe payment succeeds, set order status approved # If the Stripe payment succeeds, set order status approved
order.set_approved() order.set_approved()
@ -114,7 +116,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, stripe_customer_
'subject': settings.DCL_TEXT + " Order from %s" % context['email'], 'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'], 'to': ['info@ungleich.ch'],
'body': "\n".join(["%s=%s" % (k, v) for (k, v) in context.items()]), 'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in context.items()]),
'reply_to': [context['email']], 'reply_to': [context['email']],
} }
email = EmailMessage(**email_data) email = EmailMessage(**email_data)
@ -124,11 +127,13 @@ def create_vm_task(self, vm_template_id, user, specs, template, stripe_customer_
try: try:
retry_task(self) retry_task(self)
except MaxRetriesExceededError: except MaxRetriesExceededError:
msg_text = 'Finished {} retries for create_vm_task'.format(self.request.retries) msg_text = 'Finished {} retries for create_vm_task'.format(
self.request.retries)
logger.error(msg_text) logger.error(msg_text)
# Try sending email and stop # Try sending email and stop
email_data = { email_data = {
'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT, msg_text), 'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT,
msg_text),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'], 'to': ['info@ungleich.ch'],
'body': ',\n'.join(str(i) for i in self.request.args) 'body': ',\n'.join(str(i) for i in self.request.args)

View file

@ -67,14 +67,17 @@
<hr> <hr>
<p><b>{% trans "Configuration"%}</b> <span class="pull-right">{{request.session.template.name}}</span></p> <p><b>{% trans "Configuration"%}</b> <span class="pull-right">{{request.session.template.name}}</span></p>
<hr> <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 %} {% endwith %}
</div> </div>
<br/> <br/>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class=" content pull-right"> <div class="col-md-8 col-xs-7 pull-left tbl-no-padding">
<a href="{{next_url}}" ><button class="btn btn-info">{% trans "Place order"%}</button></a> <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-md-4 col-xs-5 content tbl-no-padding">
<a href="{{next_url}}" ><button class="btn btn-info pull-right">{% trans "Place order"%}</button></a>
</div> </div>
</form> </form>
</div> </div>

View file

@ -1,3 +1,145 @@
# from django.test import TestCase # from django.test import TestCase
from time import sleep
import stripe
from celery.result import AsyncResult
from django.conf import settings
from django.core.management import call_command
# Create your tests here. # Create your tests here.
from django.test import TestCase, override_settings
from model_mommy import mommy
from datacenterlight.models import VMTemplate
from datacenterlight.tasks import create_vm_task
from membership.models import StripeCustomer
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

@ -18,7 +18,8 @@ from hosting.models import HostingOrder
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
from membership.models import CustomUser, StripeCustomer from membership.models import CustomUser, StripeCustomer
from opennebula_api.models import OpenNebulaManager from opennebula_api.models import OpenNebulaManager
from opennebula_api.serializers import VirtualMachineTemplateSerializer, VMTemplateSerializer from opennebula_api.serializers import VirtualMachineTemplateSerializer, \
VMTemplateSerializer
from datacenterlight.tasks import create_vm_task from datacenterlight.tasks import create_vm_task
@ -35,9 +36,11 @@ class SuccessView(TemplateView):
elif 'token' not in request.session: elif 'token' not in request.session:
return HttpResponseRedirect(reverse('datacenterlight:payment')) return HttpResponseRedirect(reverse('datacenterlight:payment'))
elif 'order_confirmation' not in request.session: elif 'order_confirmation' not in request.session:
return HttpResponseRedirect(reverse('datacenterlight:order_confirmation')) return HttpResponseRedirect(
reverse('datacenterlight:order_confirmation'))
else: 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']: 'token', 'customer']:
if session_var in request.session: if session_var in request.session:
del request.session[session_var] del request.session[session_var]
@ -53,7 +56,8 @@ class PricingView(TemplateView):
templates = manager.get_templates() templates = manager.get_templates()
context = { context = {
'templates': VirtualMachineTemplateSerializer(templates, many=True).data, 'templates': VirtualMachineTemplateSerializer(templates,
many=True).data,
} }
except: except:
messages.error(request, messages.error(request,
@ -77,7 +81,8 @@ class PricingView(TemplateView):
manager = OpenNebulaManager() manager = OpenNebulaManager()
template = manager.get_template(template_id) template = manager.get_template(template_id)
request.session['template'] = VirtualMachineTemplateSerializer(template).data request.session['template'] = VirtualMachineTemplateSerializer(
template).data
if not request.user.is_authenticated(): if not request.user.is_authenticated():
request.session['next'] = reverse('hosting:payment') request.session['next'] = reverse('hosting:payment')
@ -99,7 +104,8 @@ class BetaAccessView(FormView):
def form_valid(self, form): def form_valid(self, form):
context = { 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 = { email_data = {
@ -129,7 +135,8 @@ class BetaAccessView(FormView):
email = BaseEmail(**email_data) email = BaseEmail(**email_data)
email.send() 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', {}) return render(self.request, 'datacenterlight/beta_success.html', {})
@ -154,7 +161,8 @@ class BetaProgramView(CreateView):
# data = VirtualMachineTemplateSerializer(templates, many=True).data # data = VirtualMachineTemplateSerializer(templates, many=True).data
context.update({ 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 'vms': vms
}) })
return context return context
@ -164,7 +172,8 @@ class BetaProgramView(CreateView):
vms = BetaAccessVM.create(data) vms = BetaAccessVM.create(data)
context = { 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'), 'email': data.get('email'),
'name': data.get('name'), 'name': data.get('name'),
'vms': vms 'vms': vms
@ -181,7 +190,8 @@ class BetaProgramView(CreateView):
email = BaseEmail(**email_data) email = BaseEmail(**email_data)
email.send() 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()) return HttpResponseRedirect(self.get_success_url())
@ -225,7 +235,8 @@ class IndexView(CreateView):
storage_field = forms.IntegerField(validators=[self.validate_storage]) storage_field = forms.IntegerField(validators=[self.validate_storage])
price = request.POST.get('total') price = request.POST.get('total')
template_id = int(request.POST.get('config')) 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 template_data = VMTemplateSerializer(template).data
name = request.POST.get('name') name = request.POST.get('name')
@ -237,36 +248,46 @@ class IndexView(CreateView):
cores = cores_field.clean(cores) cores = cores_field.clean(cores)
except ValidationError as err: except ValidationError as err:
msg = '{} : {}.'.format(cores, str(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,
return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") extra_tags='cores')
return HttpResponseRedirect(
reverse('datacenterlight:index') + "#order_form")
try: try:
memory = memory_field.clean(memory) memory = memory_field.clean(memory)
except ValidationError as err: except ValidationError as err:
msg = '{} : {}.'.format(memory, str(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,
return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") extra_tags='memory')
return HttpResponseRedirect(
reverse('datacenterlight:index') + "#order_form")
try: try:
storage = storage_field.clean(storage) storage = storage_field.clean(storage)
except ValidationError as err: except ValidationError as err:
msg = '{} : {}.'.format(storage, str(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,
return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") extra_tags='storage')
return HttpResponseRedirect(
reverse('datacenterlight:index') + "#order_form")
try: try:
name = name_field.clean(name) name = name_field.clean(name)
except ValidationError as err: except ValidationError as err:
msg = '{} {}.'.format(name, _('is not a proper name')) 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,
return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") extra_tags='name')
return HttpResponseRedirect(
reverse('datacenterlight:index') + "#order_form")
try: try:
email = email_field.clean(email) email = email_field.clean(email)
except ValidationError as err: except ValidationError as err:
msg = '{} {}.'.format(email, _('is not a proper email')) 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,
return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") extra_tags='email')
return HttpResponseRedirect(
reverse('datacenterlight:index') + "#order_form")
specs = { specs = {
'cpu': cores, 'cpu': cores,
@ -293,14 +314,16 @@ class IndexView(CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs) context = super(IndexView, self).get_context_data(**kwargs)
context.update({ 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())
}) })
return context return context
def form_valid(self, form): def form_valid(self, form):
context = { 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 = { email_data = {
@ -330,7 +353,8 @@ class IndexView(CreateView):
email = BaseEmail(**email_data) email = BaseEmail(**email_data)
email.send() 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) return super(IndexView, self).form_valid(form)
@ -403,7 +427,8 @@ class PaymentOrderView(FormView):
request.session['billing_address'] = billing_address.id request.session['billing_address'] = billing_address.id
request.session['token'] = token request.session['token'] = token
request.session['customer'] = customer.id request.session['customer'] = customer.id
return HttpResponseRedirect(reverse('datacenterlight:order_confirmation')) return HttpResponseRedirect(
reverse('datacenterlight:order_confirmation'))
else: else:
return self.form_invalid(form) return self.form_invalid(form)
@ -423,11 +448,15 @@ class OrderConfirmationView(DetailView):
stripe_customer_id = request.session.get('customer') stripe_customer_id = request.session.get('customer')
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
stripe_utils = StripeUtils() 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,
if not card_details.get('response_object') and not card_details.get('paid'): request.session.get(
'token'))
if not card_details.get('response_object'):
msg = card_details.get('error') msg = card_details.get('error')
messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') messages.add_message(self.request, messages.ERROR, msg,
return HttpResponseRedirect(reverse('datacenterlight:payment') + '#payment_error') extra_tags='failed_payment')
return HttpResponseRedirect(
reverse('datacenterlight:payment') + '#payment_error')
context = { context = {
'site_url': reverse('datacenterlight:index'), 'site_url': reverse('datacenterlight:index'),
'cc_last4': card_details.get('response_object').get('last4'), 'cc_last4': card_details.get('response_object').get('last4'),
@ -444,22 +473,53 @@ class OrderConfirmationView(DetailView):
billing_address_data = request.session.get('billing_address_data') billing_address_data = request.session.get('billing_address_data')
billing_address_id = request.session.get('billing_address') billing_address_id = request.session.get('billing_address')
vm_template_id = template.get('id', 1) vm_template_id = template.get('id', 1)
final_price = specs.get('price')
# Make stripe charge to a customer # Make stripe charge to a customer
stripe_utils = StripeUtils() stripe_utils = StripeUtils()
charge_response = stripe_utils.make_charge(amount=final_price, card_details = stripe_utils.get_card_details(customer.stripe_id,
customer=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 stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu,
if not charge_response.get('response_object') and not charge_response.get('paid'): ram=memory,
msg = charge_response.get('error') ssd=disk_size,
messages.add_message(self.request, messages.ERROR, msg, extra_tags='make_charge_error') version=1,
return HttpResponseRedirect(reverse('datacenterlight:payment') + '#payment_error') app='dcl')
stripe_plan = stripe_utils.get_or_create_stripe_plan(
charge = charge_response.get('response_object') amount=amount_to_be_charged,
create_vm_task.delay(vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, 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, billing_address_id,
charge) stripe_subscription_obj, card_details_dict)
request.session['order_confirmation'] = True request.session['order_confirmation'] = True
return HttpResponseRedirect(reverse('datacenterlight:order_success')) return HttpResponseRedirect(reverse('datacenterlight:order_success'))

View file

@ -37,8 +37,10 @@ def int_env(val, default_value=0):
try: try:
return_value = int(os.environ.get(val)) return_value = int(os.environ.get(val))
except Exception as e: except Exception as e:
logger.error("Encountered exception trying to get env value for {}\nException details: {}".format( logger.error(
val, str(e))) ("Encountered exception trying to get env value for {}\nException "
"details: {}").format(
val, str(e)))
return return_value return return_value
@ -169,10 +171,12 @@ TEMPLATES = [
os.path.join(PROJECT_DIR, 'membership'), os.path.join(PROJECT_DIR, 'membership'),
os.path.join(PROJECT_DIR, 'hosting/templates/'), os.path.join(PROJECT_DIR, 'hosting/templates/'),
os.path.join(PROJECT_DIR, 'nosystemd/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/cms/ungleichch'),
os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich'), 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'), os.path.join(PROJECT_DIR, 'templates/analytics'),
], ],
'APP_DIRS': True, 'APP_DIRS': True,
@ -495,6 +499,7 @@ REGISTRATION_MESSAGE = {'subject': "Validation mail",
} }
STRIPE_API_PRIVATE_KEY = env('STRIPE_API_PRIVATE_KEY') STRIPE_API_PRIVATE_KEY = env('STRIPE_API_PRIVATE_KEY')
STRIPE_API_PUBLIC_KEY = env('STRIPE_API_PUBLIC_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' ANONYMOUS_USER_NAME = 'anonymous@ungleich.ch'
GUARDIAN_GET_INIT_ANONYMOUS_USER = 'membership.models.get_anonymous_user_instance' GUARDIAN_GET_INIT_ANONYMOUS_USER = 'membership.models.get_anonymous_user_instance'
@ -537,9 +542,12 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = {
'ungleich.ch': 'UA-62285904-1', 'ungleich.ch': 'UA-62285904-1',
'digitalglarus.ch': 'UA-62285904-2', 'digitalglarus.ch': 'UA-62285904-2',
'blog.ungleich.ch': 'UA-62285904-4', 'blog.ungleich.ch': 'UA-62285904-4',
'hosting': 'UA-62285904-5', 'rails-hosting.ch': 'UA-62285904-5',
'datacenterlight.ch': 'UA-62285904-9', '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', '127.0.0.1:8000': 'localhost',
'dynamicweb-development.ungleich.ch': 'development', 'dynamicweb-development.ungleich.ch': 'development',
'dynamicweb-staging.ungleich.ch': 'staging' 'dynamicweb-staging.ungleich.ch': 'staging'
@ -564,7 +572,8 @@ if ENABLE_DEBUG_LOGGING:
'file': { 'file': {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'logging.FileHandler', 'class': 'logging.FileHandler',
'filename': "{PROJECT_DIR}/debug.log".format(PROJECT_DIR=PROJECT_DIR), 'filename': "{PROJECT_DIR}/debug.log".format(
PROJECT_DIR=PROJECT_DIR),
}, },
}, },
'loggers': { 'loggers': {

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-08-16 04:19+0530\n" "POT-Creation-Date: 2017-08-24 11:12+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -245,31 +245,26 @@ msgstr "Gesamt"
msgid "Finish Configuration" msgid "Finish Configuration"
msgstr "Konfiguration beenden" msgstr "Konfiguration beenden"
msgid "Order Nr."
msgstr "Bestellung Nr."
msgid "Amount" msgid "Amount"
msgstr "Betrag" msgstr "Betrag"
msgid "Status" msgid "Status"
msgstr "" msgstr ""
msgid "Approved" msgid "See Invoice"
msgstr "Akzeptiert" msgstr "Rechnung"
msgid "Declined"
msgstr "Abgelehnt"
msgid "View Detail" msgid "View Detail"
msgstr "Details anzeigen" msgstr "Details anzeigen"
msgid "Cancel Order" msgid "Page"
msgstr "Bestellung stornieren" msgstr ""
#, fuzzy msgid "of"
#| msgid "Do You want to delete your order?" msgstr ""
msgid "Do you want to delete your order?"
msgstr "Willst du deine Bestellung löschen?"
msgid "Delete"
msgstr "Löschen"
msgid "Your Order" msgid "Your Order"
msgstr "Deine Bestellung" msgstr "Deine Bestellung"
@ -280,6 +275,9 @@ msgstr "Konfiguration"
msgid "including VAT" msgid "including VAT"
msgstr "inkl. Mehrwertsteuer" msgstr "inkl. Mehrwertsteuer"
msgid "Month"
msgstr "Monat"
msgid "Billing Address" msgid "Billing Address"
msgstr "Rechnungsadresse" msgstr "Rechnungsadresse"
@ -301,14 +299,9 @@ msgstr ""
"speichern keine Informationen in unserer Datenbank." "speichern keine Informationen in unserer Datenbank."
msgid "" msgid ""
"\n" "You are not making any payment yet. After submitting your card information, "
" You are not making any payment yet. " "you will be taken to the Confirm Order Page."
"After submitting your card\n"
" information, you will be taken to "
"the Confirm Order Page.\n"
" "
msgstr "" msgstr ""
"\n"
"Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner " "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner "
"Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite "
"weitergeleitet." "weitergeleitet."
@ -328,19 +321,6 @@ msgstr ""
msgid "Card Type" msgid "Card Type"
msgstr "Kartentyp" 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" msgid "Processing"
msgstr "Weiter" msgstr "Weiter"
@ -390,6 +370,9 @@ msgstr ""
msgid "Private Key" msgid "Private Key"
msgstr "" msgstr ""
msgid "Delete"
msgstr "Löschen"
msgid "Delete SSH Key" msgid "Delete SSH Key"
msgstr "SSH Key löschen" msgstr "SSH Key löschen"
@ -399,38 +382,70 @@ msgstr "Möchtest Du den Schlüssel löschen?"
msgid "Show" msgid "Show"
msgstr "Anzeigen" msgstr "Anzeigen"
#, fuzzy
#| msgid "Public SSH Key"
msgid "Public SSH Key" msgid "Public SSH Key"
msgstr "Public SSH Key" msgstr "Public SSH Key"
msgid "Download" msgid "Download"
msgstr "" msgstr ""
msgid "Settings" msgid "Your Virtual Machine Detail"
msgstr "Einstellungen" msgstr "Virtuelle Maschinen Detail"
msgid "Billing" msgid "VM Settings"
msgstr "Abrechnungen" msgstr "VM Einstellungen"
msgid "Ip not assigned yet" msgid "Copied"
msgstr "Ip nicht zugewiesen" msgstr "Kopiert"
msgid "Disk" msgid "Disk"
msgstr "Festplatte" msgstr "Festplatte"
msgid "Current pricing" msgid "Billing"
msgstr "Abrechnungen"
msgid "Current Pricing"
msgstr "Aktueller Preis" msgstr "Aktueller Preis"
msgid "Current status" msgid "Your VM is"
msgstr "Aktueller Status" msgstr "Deine VM ist"
msgid "Terminate Virtual Machine" msgid "Pending"
msgstr "Virtuelle Maschine beenden" msgstr "In Vorbereitung"
msgid "Online"
msgstr ""
msgid "Failed"
msgstr "Fehlgeschlagen"
msgid "Terminate VM"
msgstr "VM Beenden"
msgid "Support / Contact"
msgstr "Support / Kontakt"
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" 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 " msgid "Do you want to cancel your Virtual Machine"
msgstr "Sind Sie sicher, dass Sie ihre virtuelle Maschine beenden wollen " msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst"
msgid "OK"
msgstr ""
msgid "Virtual Machines" msgid "Virtual Machines"
msgstr "Virtuelle Maschinen" msgstr "Virtuelle Maschinen"
@ -441,14 +456,8 @@ msgstr ""
msgid "CREATE VM" msgid "CREATE VM"
msgstr "NEUE VM" msgstr "NEUE VM"
msgid "Page"
msgstr ""
msgid "of"
msgstr ""
msgid "login" msgid "login"
msgstr "einloggen" msgstr "Einloggen"
msgid "" msgid ""
"Thank you for signing up. We have sent an email to you. Please follow the " "Thank you for signing up. We have sent an email to you. Please follow the "
@ -474,6 +483,9 @@ msgstr "Du kannst dich nun"
msgid "Sorry. Your request is invalid." msgid "Sorry. Your request is invalid."
msgstr "Entschuldigung, deine Anfrage ist ungültig." msgstr "Entschuldigung, deine Anfrage ist ungültig."
msgid "Invalid credit card"
msgstr "Ungültige Kreditkarte"
msgid "Confirm Order" msgid "Confirm Order"
msgstr "Bestellung Bestätigen" msgstr "Bestellung Bestätigen"
@ -482,6 +494,55 @@ msgid ""
"contact Data Center Light Support." "contact Data Center Light Support."
msgstr "" msgstr ""
#~ 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 "Approved"
#~ msgstr "Akzeptiert"
#~ msgid "Declined"
#~ msgstr "Abgelehnt"
#~ msgid "Cancel Order"
#~ msgstr "Bestellung stornieren"
#, fuzzy
#~| msgid "Do You want to delete your order?"
#~ msgid "Do you want to delete your order?"
#~ msgstr "Willst du deine Bestellung löschen?"
#~ msgid ""
#~ "\n"
#~ " You are not making any payment "
#~ "yet. After submitting your card\n"
#~ " information, you will be taken to "
#~ "the Confirm Order Page.\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner "
#~ "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite "
#~ "weitergeleitet."
#~ msgid "Ip not assigned yet"
#~ msgstr "Ip nicht zugewiesen"
#~ msgid "Current status"
#~ msgstr "Aktueller Status"
#~ msgid "Terminate Virtual Machine"
#~ msgstr "Virtuelle Maschine beenden"
#~ msgid "Ipv4" #~ msgid "Ipv4"
#~ msgstr "IPv4" #~ msgstr "IPv4"
@ -609,14 +670,6 @@ msgstr ""
#~ msgid "Place Order" #~ msgid "Place Order"
#~ msgstr "Bestelle" #~ 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" #~ msgid "CARD NUMBER"
#~ msgstr "Kreditkartennummer" #~ 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

@ -50,6 +50,7 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
cc_brand = models.CharField(max_length=10) cc_brand = models.CharField(max_length=10)
stripe_charge_id = models.CharField(max_length=100, null=True) stripe_charge_id = models.CharField(max_length=100, null=True)
price = models.FloatField() price = models.FloatField()
subscription_id = models.CharField(max_length=100, null=True)
permissions = ('view_hostingorder',) permissions = ('view_hostingorder',)
@ -66,7 +67,8 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS
@classmethod @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( instance = cls.objects.create(
price=price, price=price,
vm_id=vm_id, vm_id=vm_id,
@ -86,6 +88,23 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
self.cc_brand = stripe_charge.source.brand self.cc_brand = stripe_charge.source.brand
self.save() 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): def get_cc_data(self):
return { return {
'last4': self.last4, 'last4': self.last4,
@ -137,5 +156,6 @@ class HostingBill(AssignPermissionsMixin, models.Model):
@classmethod @classmethod
def create(cls, customer=None, billing_address=None): 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 return instance

View file

@ -533,9 +533,21 @@ a.unlink:hover {
padding-left: 5px; padding-left: 5px;
} }
.dcl-place-order-text{
font-size: 13px;
color: #808080;
}
.dcl-order-table-total .tbl-total { .dcl-order-table-total .tbl-total {
text-align: center; text-align: center;
color: #000; color: #000;
padding-left: 44px;
}
.tbl-total .dcl-price-month {
font-size: 16px;
text-transform: capitalize;
color: #000;
} }
.tbl-no-padding { .tbl-no-padding {

View file

@ -1,3 +1,6 @@
.virtual-machine-container {
max-width: 900px;
}
.virtual-machine-container .tabs-left, .virtual-machine-container .tabs-right { .virtual-machine-container .tabs-left, .virtual-machine-container .tabs-right {
border-bottom: none; border-bottom: none;
padding-top: 2px; padding-top: 2px;
@ -229,6 +232,204 @@
} }
} }
/* 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 */ /* New styles */
.dashboard-container-head { .dashboard-container-head {
padding: 0 8px; padding: 0 8px;
@ -239,10 +440,10 @@
} }
.dashboard-title-thin .un-icon { .dashboard-title-thin .un-icon {
height: 34px; height: 30px;
margin-right: 5px; margin-right: 5px;
margin-top: -1px; margin-top: -1px;
width: 20px; width: 30px;
} }
.dashboard-subtitle { .dashboard-subtitle {
@ -287,6 +488,24 @@
color: #3770CC; 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 { .vm-status, .vm-status-active, .vm-status-failed {
font-weight: 600; font-weight: 600;
} }
@ -355,8 +574,8 @@
position: relative; position: relative;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
/* margin-top: 15px; */ /* margin-top: 15px; */
padding-top: 5px; padding-top: 10px;
padding-bottom: 15px; padding-bottom: 13px;
} }
.table-switch tbody tr:last-child { .table-switch tbody tr:last-child {
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
@ -373,11 +592,28 @@
font-weight: 600; font-weight: 600;
position: absolute; position: absolute;
top: 5px; top: 5px;
left: 8px;
} }
.table-switch .last-td { .table-switch .last-td {
position: absolute; position: absolute;
bottom: 20px; bottom: 13px;
right: 0; 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,61 @@
<?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 279.525 279.525" style="enable-background:new 0 0 279.525 279.525;" xml:space="preserve">
<g>
<path d="M165.066,1.544c-29.272,0-56.007,11.05-76.268,29.191c4.494,7.146,7.047,15.46,7.287,24.042l0.001,0.025l0.001,0.025
c0.102,3.867,0.333,7.735,0.664,11.597c15.368-21.117,40.258-34.88,68.315-34.88c46.571,0,84.459,37.888,84.459,84.459
c0,46.08-37.098,83.634-82.994,84.422c4.191,3.502,8.518,6.84,12.976,9.974l0.02,0.015l0.021,0.014
c6.07,4.282,11.014,9.896,14.483,16.317c49.133-12.861,85.493-57.633,85.493-110.742C279.525,52.89,228.18,1.544,165.066,1.544z"/>
<path d="M162.256,234.942c-13.076-10.438-21.234-17.389-32.909-28.204c-3.435-3.182-7.633-5.164-11.944-5.164
c-3.299,0-6.557,1.051-9.239,3.252c-2.768,2.33-5.536,4.66-8.305,6.989c-22.499-26.738-39.206-57.895-49.027-91.431
c3.472-1.016,6.945-2.033,10.417-3.049c7.652-2.343,11.252-10.512,10.129-18.701c-2.443-17.824-3.77-26.679-5.282-43.018
c-0.775-8.375-6.349-15.65-14.338-16.085c-1.246-0.121-2.491-0.181-3.726-0.181c-29.71,0-55.578,34.436-46.009,76.564
c11.907,52.172,37.684,100.243,74.551,139.031c15.102,15.856,33.603,23.036,50.312,23.036c17.627,0,33.261-7.984,40.833-22.195
C171.778,248.891,168.83,240.19,162.256,234.942z"/>
<path d="M130.645,118.121c-7.912,7.341-13.089,13.113-15.823,17.643c-1.93,3.195-3.338,6.573-4.187,10.04
c-0.399,1.632-0.032,3.326,1.007,4.649c1.038,1.321,2.596,2.079,4.276,2.079h37.758c4.626,0,8.39-3.764,8.39-8.39
c0-4.626-3.764-8.39-8.39-8.39h-17.051c0.139-0.164,0.282-0.328,0.428-0.493c1.114-1.254,3.842-3.874,8.107-7.785
c4.473-4.105,7.493-7.179,9.232-9.398c2.621-3.336,4.571-6.593,5.794-9.679c1.247-3.145,1.88-6.498,1.88-9.967
c0-6.224-2.254-11.507-6.699-15.705c-4.416-4.164-10.495-6.274-18.071-6.274c-6.884,0-12.731,1.802-17.377,5.356
c-2.803,2.146-4.961,5.119-6.415,8.839c-0.982,2.513-0.728,5.388,0.68,7.689c1.408,2.302,3.852,3.837,6.537,4.105
c0.299,0.03,0.597,0.045,0.891,0.045c3.779,0,7.149-2.403,8.387-5.979c0.388-1.121,0.901-2.012,1.527-2.65
c1.318-1.343,3.093-1.997,5.428-1.997c2.373,0,4.146,0.618,5.418,1.889c1.269,1.269,1.886,3.12,1.886,5.66
c0,2.359-0.843,4.819-2.505,7.314C140.862,108.028,138.199,111.083,130.645,118.121z"/>
<path d="M206.235,76.451h-6.307c-1.797,0-3.475,0.886-4.489,2.37l-29.168,42.698c-0.851,1.246-1.301,2.703-1.301,4.212v6.919
c0,2.997,2.439,5.436,5.436,5.436h23.945v5.787c0,4.775,3.885,8.66,8.66,8.66c4.775,0,8.66-3.885,8.66-8.66v-5.787h0.865
c4.437,0,8.047-3.61,8.047-8.047c0-4.437-3.61-8.047-8.047-8.047h-0.865V81.887C211.671,78.89,209.232,76.451,206.235,76.451z
M194.352,121.992h-10.748l10.748-15.978V121.992z"/>
</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: 3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="symbol symbol-billing" aria-labelledby="title" role="img"><title id="title">billing icon</title><g data-name="Layer 1"><path class="cls-1" d="M.37.023v15.954l2.775-1.387 2.775 1.387L8 14.59l2.775 1.387 2.081-1.387 2.775 1.387V.023zm13.873 13.709l-1.487-.744-2.081 1.387L7.9 12.989l-2.08 1.387-2.675-1.337-1.387.694V1.41h12.485z" role="presentation"/><path class="cls-1" d="M4.206 3.617h7.741v1.348H4.206zm0 2.697h7.741v1.349H4.206zm0 2.697h7.741v1.349H4.206z" role="presentation"/></g></svg>

After

Width:  |  Height:  |  Size: 558 B

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

@ -13,4 +13,62 @@ $( document ).ready(function() {
}, 1000); }, 1000);
}); });
$('.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]--> <![endif]-->
{% with 'hosting/img/'|add:hosting|add:'-intro-bg.png' as image_static %} {% with 'hosting/img/'|add:hosting|add:'-intro-bg.png' as image_static %}
alt="">
<style media="screen" type="text/css"> <style media="screen" type="text/css">
.intro-header { .intro-header {
background: url("{% static image_static %}") no-repeat center center; background: url("{% static image_static %}") no-repeat center center;

View file

@ -3,95 +3,64 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="dashboard-container">
<div> <div class="dashboard-container-head">
<div class="orders-container"> <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>
<div class="row"> {% if messages %}
<div class="container-table col-md-8 col-md-offset-2"> <div class="alert alert-warning">
<table class="table borderless table-hover"> {% for message in messages %}
<h3><i class="fa fa-credit-card fa-separate"></i>{% trans "My Orders"%}</h3> <span>{{ message }}</span>
<br/> {% endfor %}
<thead>
<tr>
<th>#</th>
<th>{% trans "Date"%}</th>
<th>{% trans "Amount"%}</th>
<th>{% trans "Status"%}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td scope="row">{{ order.id }}</td>
<td>{{ order.created_at | date:"M d, Y" }}</td>
<td>{{ order.price }} CHF</td>
<td>{% if order.approved %}
<span class="text-success strong">{% trans "Approved"%}</span>
{% else %}
<span class="text-danger strong">{% trans "Declined"%}</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>
</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>
{% 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>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} 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>
{% endif %}
</span>
</div>
{% endif %}
</div>
</div> </div>
</div> {% endif %}
<div class="dashboard-subtitle"></div>
</div> </div>
<table class="table table-switch">
<thead>
<tr>
<th>{% trans "Order Nr." %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Amount" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td 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="vm-status-failed"><strong>Declined</strong></span>
{% endif %}
</td>
<td class="text-right last-td">
<a class="btn btn-order-detail alt-text" href="{% url 'hosting:orders' order.pk %}" data-alt="{% trans 'See Invoice' %}">{% trans "View Detail" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% 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>
{% endif %}
<span class="page-current">
{% 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>
{% endif %}
</span>
</div>
{% endif %}
</div>
{% endblock %} {% endblock %}

View file

@ -41,9 +41,9 @@
{%trans "Total" %} <span>{%trans "including VAT" %}</span> {%trans "Total" %} <span>{%trans "including VAT" %}</span>
</div> </div>
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-no-padding"> <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"></div>
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-total">{{request.session.specs.price}} <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 tbl-total">{{request.session.specs.price}}
CHF CHF<span class="dcl-price-month">/{% trans "Month" %}</span>
</div> </div>
</div> </div>
</div> </div>
@ -87,10 +87,7 @@
<div class="col-xs-12"> <div class="col-xs-12">
{% if not messages and not form.non_field_errors %} {% if not messages and not form.non_field_errors %}
<p class="card-warning-content card-warning-addtional-margin"> <p class="card-warning-content card-warning-addtional-margin">
{% blocktrans %} {% blocktrans %}You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page.{% endblocktrans %}
You are not making any payment yet. After submitting your card
information, you will be taken to the Confirm Order Page.
{% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
<div id='payment_error'> <div id='payment_error'>
@ -147,10 +144,7 @@
<div class="col-xs-12"> <div class="col-xs-12">
{% if not messages and not form.non_field_errors %} {% if not messages and not form.non_field_errors %}
<p class="card-warning-content"> <p class="card-warning-content">
{% blocktrans %} {% blocktrans %}You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page.{% endblocktrans %}
You are not making any payment yet. After submitting your card
information, you will be taken to the Confirm Order Page.
{% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
<div id='payment_error'> <div id='payment_error'>

View file

@ -3,193 +3,110 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div> {% if messages %}
<div class="virtual-machine-container dashboard-container "> <div class="alert alert-warning">
<div class="row"> {% for message in messages %}
<div class="col-md-9 col-md-offset-2"> <span>{{ message }}</span>
<div class="col-sm-12"> {% endfor %}
<h3><i class="fa fa-cloud fa-separate" aria-hidden="true"></i> {{virtual_machine.name}}</h3> </div>
<hr/> {% endif %}
<div class="col-md-3"> <!-- required for floating --> <div class="virtual-machine-container dashboard-container">
<!-- Nav tabs --> <h1 class="dashboard-title-thin">{% trans "Your Virtual Machine Detail" %}</h1>
<ul class="nav nav-tabs tabs-left sideways"> <div class="vm-detail-contain">
<li class="active"> <div class="vm-detail-item">
<a href="#settings-v" data-toggle="tab"> <h2 class="vm-detail-title">{% trans "VM Settings" %} <img src="{% static 'hosting/img/settings.svg' %}" class="un-icon"></h2>
<i class="fa fa-cogs" aria-hidden="true"></i> <h3 class="vm-name">{{virtual_machine.name}}</h3>
{% trans "Settings"%} {% if virtual_machine.ipv6 %}
</a> <div class="vm-detail-ip">
</li> <p>
<li> <span>IPv4:</span>
<a href="#billing-v" data-toggle="tab"> <span class="value">{{virtual_machine.ipv4}}</span>
<i class="fa fa-money" aria-hidden="true"></i> <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">
{% trans "Billing"%} <img class="un-icon" src="{% static 'hosting/img/copy.svg' %}">
</a> </button>
</li> </p>
<li> <p>
<a href="#status-v" data-toggle="tab"> <span>IPv6:</span>
<i class="fa fa-signal" aria-hidden="true"></i> {% trans "Status"%} <span class="value value-sm-block">{{virtual_machine.ipv6}}</span>
</a> <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">
</li> <img class="un-icon" src="{% static 'hosting/img/copy.svg' %}">
</ul> </button>
</div> </p>
</div>
<div class="col-md-9"> {% endif %}
<!-- Tab panes --> <div class="vm-detail-config">
<div class="tab-content"> <p><span>{% trans "Cores" %}:</span><span class="value">{{virtual_machine.cores}}</span></p>
<div class="tab-pane active" id="settings-v"> <p><span>{% trans "Memory" %}:</span><span class="value">{{virtual_machine.memory}} GB</span></p>
<div class="row"> <p><span>{% trans "Disk" %}:</span><span class="value">{{virtual_machine.disk_size|floatformat:2}} GB</span></p>
<div class="col-md-12 inline-headers"> <p><span>{% trans "Configuration" %}:</span><span class="value">{{virtual_machine.configuration}}</span></p>
<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 %}
<span>{{ message }}</span>
{% endfor %}
</div>
{% endif %}
</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>
</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>
<div class="modal-footer">
<a class="btn btn-danger btn-ok">OK</a>
</div>
</div>
</div>
</div>
<!-- / Cancel Modal -->
</div>
{% endif %}
</div>
</div>
</div>
<div class="clearfix"></div>
</div> </div>
</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>
</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>
<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">{% trans "OK" %}</a>
</div>
</div>
</div> </div>
</div> </div>
<!-- / Cancel Modal -->
</div>
{%endblock%} {%endblock%}

View file

@ -20,9 +20,12 @@ urlpatterns = [
url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'), url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'),
url(r'bills/?$', HostingBillListView.as_view(), name='bills'), url(r'bills/?$', HostingBillListView.as_view(), name='bills'),
url(r'bills/(?P<pk>\d+)/?$', HostingBillDetailView.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'cancel_order/(?P<pk>\d+)/?$',
url(r'create_virtual_machine/?$', CreateVirtualMachinesView.as_view(), name='create_virtual_machine'), OrdersHostingDeleteView.as_view(), name='delete_order'),
url(r'my-virtual-machines/?$', VirtualMachinesPlanListView.as_view(), name='virtual_machines'), 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(), url(r'my-virtual-machines/(?P<pk>\d+)/?$', VirtualMachineView.as_view(),
name='virtual_machines'), name='virtual_machines'),
url(r'ssh_keys/?$', SSHKeyListView.as_view(), url(r'ssh_keys/?$', SSHKeyListView.as_view(),
@ -44,5 +47,6 @@ urlpatterns = [
PasswordResetConfirmView.as_view(), name='reset_password_confirm'), PasswordResetConfirmView.as_view(), name='reset_password_confirm'),
url(r'^logout/?$', auth_views.logout, url(r'^logout/?$', auth_views.logout,
{'next_page': '/hosting/login?logged_out=true'}, name='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

@ -244,7 +244,8 @@ class SignupValidatedView(SignupValidateView):
lurl=login_url) lurl=login_url)
else: else:
home_url = '<a href="' + \ 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( message = '{sorry_message} <br />{go_back_to} {hurl}'.format(
sorry_message=_("Sorry. Your request is invalid."), sorry_message=_("Sorry. Your request is invalid."),
go_back_to=_('Go back to'), go_back_to=_('Go back to'),
@ -569,7 +570,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
customer=customer.stripe_id) customer=customer.stripe_id)
# Check if the payment was approved # Check if the payment was approved
if not charge_response.get('response_object') and not charge_response.get('paid'): if not charge_response.get('response_object'):
msg = charge_response.get('error') msg = charge_response.get('error')
messages.add_message(self.request, messages.ERROR, msg, extra_tags='make_charge_error') messages.add_message(self.request, messages.ERROR, msg, extra_tags='make_charge_error')
return HttpResponseRedirect(reverse('hosting:payment') + '#payment_error') return HttpResponseRedirect(reverse('hosting:payment') + '#payment_error')
@ -831,6 +832,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
serializer = VirtualMachineSerializer(vm) serializer = VirtualMachineSerializer(vm)
context = { context = {
'virtual_machine': serializer.data, 'virtual_machine': serializer.data,
'order': HostingOrder.objects.get(vm_id=serializer.data['vm_id'])
} }
except: except:
pass pass

View file

@ -83,9 +83,16 @@ wheel==0.29.0
django-admin-honeypot==1.0.0 django-admin-honeypot==1.0.0
coverage==4.3.4 coverage==4.3.4
git+https://github.com/ungleich/python-oca.git#egg=python-oca git+https://github.com/ungleich/python-oca.git#egg=python-oca
djangorestframework djangorestframework==3.6.3
flake8==3.3.0 flake8==3.3.0
python-memcached==1.58 python-memcached==1.58
celery==4.0.2 celery==4.0.2
redis==2.10.5 redis==2.10.5
django-celery-results==1.0.1 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. render your Google Analytics tracking code template.
""" """
host = request.get_host() 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: if not settings.DEBUG and ga_prop_id:
return { return {
'GOOGLE_ANALYTICS_PROPERTY_ID': ga_prop_id 'GOOGLE_ANALYTICS_PROPERTY_ID': ga_prop_id

View file

@ -1,6 +1,10 @@
import logging
import stripe import stripe
from django.conf import settings from django.conf import settings
from datacenterlight.models import StripePlan
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY stripe.api_key = settings.STRIPE_API_PRIVATE_KEY
logger = logging.getLogger(__name__)
def handleStripeError(f): def handleStripeError(f):
@ -26,7 +30,8 @@ def handleStripeError(f):
response.update({'error': err['message']}) response.update({'error': err['message']})
return response return response
except stripe.error.RateLimitError as e: 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 return response
except stripe.error.InvalidRequestError as e: except stripe.error.InvalidRequestError as e:
response.update({'error': "Invalid parameters"}) response.update({'error': "Invalid parameters"})
@ -55,6 +60,10 @@ class StripeUtils(object):
CURRENCY = 'chf' CURRENCY = 'chf'
INTERVAL = 'month' INTERVAL = 'month'
SUCCEEDED_STATUS = 'succeeded' 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): def __init__(self):
self.stripe = stripe self.stripe = stripe
@ -96,7 +105,8 @@ class StripeUtils(object):
customer = stripe.Customer.retrieve(id) customer = stripe.Customer.retrieve(id)
except stripe.InvalidRequestError: except stripe.InvalidRequestError:
customer = self.create_customer(token, user.email, user.name) customer = self.create_customer(token, user.email, user.name)
user.stripecustomer.stripe_id = customer.get('response_object').get('id') user.stripecustomer.stripe_id = customer.get(
'response_object').get('id')
user.stripecustomer.save() user.stripecustomer.save()
return customer return customer
@ -129,13 +139,92 @@ class StripeUtils(object):
return charge return charge
@handleStripeError @handleStripeError
def create_plan(self, amount, name, id): def get_or_create_stripe_plan(self, amount, name, stripe_plan_id):
self.stripe.Plan.create( """
amount=amount, This function checks if a StripePlan with the given
interval=self.INTERVAL, stripe_plan_id already exists. If it exists then the function
name=name, returns this object otherwise it creates a new StripePlan and
currency=self.CURRENCY, returns the new object.
id=id)
: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=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 @handleStripeError
def make_payment(self, customer, amount, token): def make_payment(self, customer, amount, token):
@ -145,3 +234,29 @@ class StripeUtils(object):
customer=customer customer=customer
) )
return charge 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

View file

@ -1,10 +1,15 @@
from django.test import TestCase import uuid
from django.test import Client from unittest.mock import patch
from django.http.request import HttpRequest
from model_mommy import mommy
from utils.stripe_utils import StripeUtils
import stripe 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 from django.conf import settings
@ -18,8 +23,9 @@ class BaseTestCase(TestCase):
self.dummy_password = 'test_password' self.dummy_password = 'test_password'
# Users # Users
self.customer, self.another_customer = mommy.make('membership.CustomUser', self.customer, self.another_customer = mommy.make(
_quantity=2) 'membership.CustomUser',
_quantity=2)
self.customer.set_password(self.dummy_password) self.customer.set_password(self.dummy_password)
self.customer.save() self.customer.save()
self.another_customer.set_password(self.dummy_password) self.another_customer.set_password(self.dummy_password)
@ -94,17 +100,16 @@ class TestStripeCustomerDescription(TestCase):
""" """
def setUp(self): def setUp(self):
self.dummy_password = 'test_password' self.customer_password = 'test_password'
self.dummy_email = 'test@ungleich.ch' self.customer_email = 'test@ungleich.ch'
self.customer_name = "Monty Python"
self.customer = mommy.make('membership.CustomUser') self.customer = mommy.make('membership.CustomUser')
self.customer.set_password(self.dummy_password) self.customer.set_password(self.customer_password)
self.customer.email = self.dummy_email self.customer.email = self.customer_email
self.customer.save() self.customer.save()
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY self.stripe_utils = StripeUtils()
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY_TEST
def test_creating_stripe_customer(self): self.token = stripe.Token.create(
test_name = "Monty Python"
token = stripe.Token.create(
card={ card={
"number": '4111111111111111', "number": '4111111111111111',
"exp_month": 12, "exp_month": 12,
@ -112,8 +117,121 @@ class TestStripeCustomerDescription(TestCase):
"cvc": '123' "cvc": '123'
}, },
) )
stripe_utils = StripeUtils() self.failed_token = stripe.Token.create(
stripe_data = stripe_utils.create_customer(token.id, self.customer.email, test_name) 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) self.assertEqual(stripe_data.get('error'), None)
customer_data = stripe_data.get('response_object') customer_data = stripe_data.get('response_object')
self.assertEqual(customer_data.description, test_name) 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'))