diff --git a/Changelog b/Changelog index e342aec6..6d1dfd5d 100644 --- a/Changelog +++ b/Changelog @@ -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 * #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 diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 6511367f..3853d4e3 100644 --- a/datacenterlight/locale/de/LC_MESSAGES/django.po +++ b/datacenterlight/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-03 03:10+0530\n" +"POT-Creation-Date: 2017-08-24 11:28+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -276,6 +276,19 @@ msgstr "Konfiguration" msgid "Total" msgstr "" +#, fuzzy +#| msgid "month" +msgid "Month" +msgstr "Monat" + +#, python-format +msgid "" +"By clicking \"Place order\" this plan will charge your credit card account " +"with the fee of %(vm_price)sCHF/month" +msgstr "" +"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF " +"pro Monat belastet" + msgid "Place order" msgstr "Bestellen" diff --git a/datacenterlight/migrations/0007_stripeplan.py b/datacenterlight/migrations/0007_stripeplan.py new file mode 100644 index 00000000..95892205 --- /dev/null +++ b/datacenterlight/migrations/0007_stripeplan.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-16 19:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0006_vmtemplate'), + ] + + operations = [ + migrations.CreateModel( + name='StripePlan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_plan_id', models.CharField(max_length=100, null=True)), + ], + ), + ] diff --git a/datacenterlight/migrations/0008_auto_20170821_2024.py b/datacenterlight/migrations/0008_auto_20170821_2024.py new file mode 100644 index 00000000..5357a404 --- /dev/null +++ b/datacenterlight/migrations/0008_auto_20170821_2024.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-21 20:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0007_stripeplan'), + ] + + operations = [ + migrations.AlterField( + model_name='stripeplan', + name='stripe_plan_id', + field=models.CharField(max_length=256, null=True), + ), + ] diff --git a/datacenterlight/models.py b/datacenterlight/models.py index fdfebc96..f7b50a01 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -59,3 +59,15 @@ class VMTemplate(models.Model): def create(cls, name, opennebula_vm_template_id): vm_template = cls(name=name, opennebula_vm_template_id=opennebula_vm_template_id) return vm_template + + +class StripePlan(models.Model): + """ + A model to store Data Center Light's created Stripe plans + """ + stripe_plan_id = models.CharField(max_length=256, null=True) + + @classmethod + def create(cls, stripe_plan_id): + stripe_plan = cls(stripe_plan_id=stripe_plan_id) + return stripe_plan diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index b897c54a..1e3e1caa 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -41,13 +41,15 @@ def retry_task(task, exception=None): @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, - charge): + charge, cc_details): vm_id = None try: 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() # Create OpenNebulaManager 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.save() - # Associate an order with a stripe payment + # Associate an order with a stripe subscription 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 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'], 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, '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']], } email = EmailMessage(**email_data) @@ -124,11 +127,13 @@ def create_vm_task(self, vm_template_id, user, specs, template, stripe_customer_ try: retry_task(self) 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) # Try sending email and stop 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, 'to': ['info@ungleich.ch'], 'body': ',\n'.join(str(i) for i in self.request.args) diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 8b1180bb..7a882236 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -67,14 +67,17 @@

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


-

{% trans "Total"%}

{{vm.price}} CHF

+

{% trans "Total"%}

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

{% endwith %}
{% csrf_token %} -
- +
+

{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.

+
+
diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index a79ca8be..602fb403 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -1,3 +1,145 @@ # 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. +from django.test import TestCase, override_settings +from model_mommy import mommy + +from datacenterlight.models import VMTemplate +from datacenterlight.tasks import create_vm_task +from membership.models import StripeCustomer +from opennebula_api.serializers import VMTemplateSerializer +from utils.models import BillingAddress +from utils.stripe_utils import StripeUtils + + +class CeleryTaskTestCase(TestCase): + @override_settings( + task_eager_propagates=True, + task_always_eager=True, + ) + def setUp(self): + self.customer_password = 'test_password' + self.customer_email = 'celery-createvm-task-test@ungleich.ch' + self.customer_name = "Monty Python" + self.user = { + 'email': self.customer_email, + 'name': self.customer_name + } + self.customer = mommy.make('membership.CustomUser') + self.customer.set_password(self.customer_password) + self.customer.email = self.customer_email + self.customer.save() + self.stripe_utils = StripeUtils() + stripe.api_key = settings.STRIPE_API_PRIVATE_KEY_TEST + self.token = stripe.Token.create( + card={ + "number": '4111111111111111', + "exp_month": 12, + "exp_year": 2022, + "cvc": '123' + }, + ) + # Run fetchvmtemplates so that we have the VM templates from + # OpenNebula + call_command('fetchvmtemplates') + + def test_create_vm_task(self): + """Tests the create vm task for monthly subscription + + This test is supposed to validate the proper execution + of celery create_vm_task on production, as we have no + other way to do this. + """ + + # We create a VM from the first template available to DCL + vm_template = VMTemplate.objects.all().first() + template_data = VMTemplateSerializer(vm_template).data + + # The specs of VM that we want to create + specs = { + 'cpu': 1, + 'memory': 2, + 'disk_size': 10, + 'price': 15 + } + + stripe_customer = StripeCustomer.get_or_create( + email=self.customer_email, + token=self.token) + card_details = self.stripe_utils.get_card_details( + stripe_customer.stripe_id, + self.token) + card_details_dict = card_details.get('response_object') + billing_address = BillingAddress( + cardholder_name=self.customer_name, + postal_code='1232', + country='CH', + street_address='Monty\'s Street', + city='Hollywood') + billing_address.save() + billing_address_data = {'cardholder_name': self.customer_name, + 'postal_code': '1231', + 'country': 'CH', + 'token': self.token, + 'street_address': 'Monty\'s Street', + 'city': 'Hollywood'} + + billing_address_id = billing_address.id + vm_template_id = template_data.get('id', 1) + + cpu = specs.get('cpu') + memory = specs.get('memory') + disk_size = specs.get('disk_size') + amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) + plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( + cpu=cpu, + memory=memory, + disk_size=disk_size) + + stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, + ram=memory, + ssd=disk_size, + version=1, + app='dcl') + stripe_plan = self.stripe_utils.get_or_create_stripe_plan( + amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id) + subscription_result = self.stripe_utils.subscribe_customer_to_plan( + stripe_customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + stripe_subscription_obj = subscription_result.get('response_object') + # Check if the subscription was approved and is active + if stripe_subscription_obj is None or \ + stripe_subscription_obj.status != 'active': + msg = subscription_result.get('error') + raise Exception("Creating subscription failed: {}".format(msg)) + + async_task = create_vm_task.delay(vm_template_id, self.user, + specs, + template_data, + stripe_customer.id, + billing_address_data, + billing_address_id, + stripe_subscription_obj, + card_details_dict) + new_vm_id = 0 + res = None + for i in range(0, 10): + sleep(5) + res = AsyncResult(async_task.task_id) + if res.result is not None and res.result > 0: + new_vm_id = res.result + break + + # We expect a VM to be created within 50 seconds + self.assertGreater(new_vm_id, 0, + "VM could not be created. res._get_task_meta() = {}" + .format(res._get_task_meta())) diff --git a/datacenterlight/views.py b/datacenterlight/views.py index db7f2e53..fd1435a1 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -18,7 +18,8 @@ from hosting.models import HostingOrder from utils.stripe_utils import StripeUtils from membership.models import CustomUser, StripeCustomer 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 @@ -35,9 +36,11 @@ class SuccessView(TemplateView): elif 'token' not in request.session: return HttpResponseRedirect(reverse('datacenterlight:payment')) elif 'order_confirmation' not in request.session: - return HttpResponseRedirect(reverse('datacenterlight:order_confirmation')) + return HttpResponseRedirect( + reverse('datacenterlight:order_confirmation')) else: - for session_var in ['specs', 'user', 'template', 'billing_address', 'billing_address_data', + for session_var in ['specs', 'user', 'template', 'billing_address', + 'billing_address_data', 'token', 'customer']: if session_var in request.session: del request.session[session_var] @@ -53,7 +56,8 @@ class PricingView(TemplateView): templates = manager.get_templates() context = { - 'templates': VirtualMachineTemplateSerializer(templates, many=True).data, + 'templates': VirtualMachineTemplateSerializer(templates, + many=True).data, } except: messages.error(request, @@ -77,7 +81,8 @@ 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') @@ -99,7 +104,8 @@ class BetaAccessView(FormView): def form_valid(self, form): context = { - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()) } email_data = { @@ -129,7 +135,8 @@ class BetaAccessView(FormView): email = BaseEmail(**email_data) email.send() - messages.add_message(self.request, messages.SUCCESS, self.success_message) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) return render(self.request, 'datacenterlight/beta_success.html', {}) @@ -154,7 +161,8 @@ class BetaProgramView(CreateView): # data = VirtualMachineTemplateSerializer(templates, many=True).data context.update({ - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()), + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()), 'vms': vms }) return context @@ -164,7 +172,8 @@ class BetaProgramView(CreateView): vms = BetaAccessVM.create(data) context = { - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()), + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()), 'email': data.get('email'), 'name': data.get('name'), 'vms': vms @@ -181,7 +190,8 @@ class BetaProgramView(CreateView): email = BaseEmail(**email_data) email.send() - messages.add_message(self.request, messages.SUCCESS, self.success_message) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) return HttpResponseRedirect(self.get_success_url()) @@ -225,7 +235,8 @@ 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') @@ -237,36 +248,46 @@ class IndexView(CreateView): cores = cores_field.clean(cores) except ValidationError as err: msg = '{} : {}.'.format(cores, str(err)) - messages.add_message(self.request, messages.ERROR, msg, extra_tags='cores') - return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='cores') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") try: memory = memory_field.clean(memory) except ValidationError as err: msg = '{} : {}.'.format(memory, str(err)) - messages.add_message(self.request, messages.ERROR, msg, extra_tags='memory') - return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='memory') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") try: storage = storage_field.clean(storage) except ValidationError as err: msg = '{} : {}.'.format(storage, str(err)) - messages.add_message(self.request, messages.ERROR, msg, extra_tags='storage') - return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='storage') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") try: name = name_field.clean(name) except ValidationError as err: msg = '{} {}.'.format(name, _('is not a proper name')) - messages.add_message(self.request, messages.ERROR, msg, extra_tags='name') - return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='name') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") try: email = email_field.clean(email) except ValidationError as err: msg = '{} {}.'.format(email, _('is not a proper email')) - messages.add_message(self.request, messages.ERROR, msg, extra_tags='email') - return HttpResponseRedirect(reverse('datacenterlight:index') + "#order_form") + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='email') + return HttpResponseRedirect( + reverse('datacenterlight:index') + "#order_form") specs = { 'cpu': cores, @@ -293,14 +314,16 @@ class IndexView(CreateView): def get_context_data(self, **kwargs): context = super(IndexView, self).get_context_data(**kwargs) context.update({ - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()) }) return context def form_valid(self, form): context = { - 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()) } email_data = { @@ -330,7 +353,8 @@ class IndexView(CreateView): email = BaseEmail(**email_data) email.send() - messages.add_message(self.request, messages.SUCCESS, self.success_message) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) return super(IndexView, self).form_valid(form) @@ -403,7 +427,8 @@ class PaymentOrderView(FormView): request.session['billing_address'] = billing_address.id request.session['token'] = token request.session['customer'] = customer.id - return HttpResponseRedirect(reverse('datacenterlight:order_confirmation')) + return HttpResponseRedirect( + reverse('datacenterlight:order_confirmation')) else: return self.form_invalid(form) @@ -423,11 +448,15 @@ class OrderConfirmationView(DetailView): stripe_customer_id = request.session.get('customer') customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() stripe_utils = StripeUtils() - card_details = stripe_utils.get_card_details(customer.stripe_id, request.session.get('token')) - if not card_details.get('response_object') and not card_details.get('paid'): + card_details = stripe_utils.get_card_details(customer.stripe_id, + request.session.get( + 'token')) + if not card_details.get('response_object'): msg = card_details.get('error') - messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') - return HttpResponseRedirect(reverse('datacenterlight:payment') + '#payment_error') + 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'), @@ -444,22 +473,53 @@ class OrderConfirmationView(DetailView): billing_address_data = request.session.get('billing_address_data') billing_address_id = request.session.get('billing_address') vm_template_id = template.get('id', 1) - final_price = specs.get('price') # Make stripe charge to a customer stripe_utils = StripeUtils() - charge_response = stripe_utils.make_charge(amount=final_price, - customer=customer.stripe_id) + card_details = stripe_utils.get_card_details(customer.stripe_id, + request.session.get( + 'token')) + if not card_details.get('response_object'): + msg = card_details.get('error') + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='failed_payment') + return HttpResponseRedirect( + reverse('datacenterlight:payment') + '#payment_error') + card_details_dict = card_details.get('response_object') + cpu = specs.get('cpu') + memory = specs.get('memory') + disk_size = specs.get('disk_size') + amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6) + plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( + cpu=cpu, + memory=memory, + disk_size=disk_size) - # Check if the payment was approved - if not charge_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_vm_task.delay(vm_template_id, user, specs, template, stripe_customer_id, billing_address_data, + stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu, + ram=memory, + ssd=disk_size, + version=1, + app='dcl') + stripe_plan = stripe_utils.get_or_create_stripe_plan( + amount=amount_to_be_charged, + name=plan_name, + stripe_plan_id=stripe_plan_id) + subscription_result = stripe_utils.subscribe_customer_to_plan( + customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + stripe_subscription_obj = subscription_result.get('response_object') + # Check if the subscription was approved and is active + if stripe_subscription_obj is None or \ + stripe_subscription_obj.status != 'active': + msg = subscription_result.get('error') + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='failed_payment') + return HttpResponseRedirect( + reverse('datacenterlight:payment') + '#payment_error') + create_vm_task.delay(vm_template_id, user, specs, template, + stripe_customer_id, billing_address_data, billing_address_id, - charge) + stripe_subscription_obj, card_details_dict) request.session['order_confirmation'] = True return HttpResponseRedirect(reverse('datacenterlight:order_success')) diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index 53e9bf0e..08ce457d 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -37,8 +37,10 @@ def int_env(val, default_value=0): 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))) + logger.error( + ("Encountered exception trying to get env value for {}\nException " + "details: {}").format( + val, str(e))) return return_value @@ -169,10 +171,12 @@ TEMPLATES = [ os.path.join(PROJECT_DIR, 'membership'), os.path.join(PROJECT_DIR, 'hosting/templates/'), os.path.join(PROJECT_DIR, 'nosystemd/templates/'), - os.path.join(PROJECT_DIR, 'ungleich/templates/djangocms_blog/'), + os.path.join(PROJECT_DIR, + 'ungleich/templates/djangocms_blog/'), os.path.join(PROJECT_DIR, 'ungleich/templates/cms/ungleichch'), os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich'), - os.path.join(PROJECT_DIR, 'ungleich_page/templates/ungleich_page'), + os.path.join(PROJECT_DIR, + 'ungleich_page/templates/ungleich_page'), os.path.join(PROJECT_DIR, 'templates/analytics'), ], 'APP_DIRS': True, @@ -495,6 +499,7 @@ REGISTRATION_MESSAGE = {'subject': "Validation mail", } STRIPE_API_PRIVATE_KEY = env('STRIPE_API_PRIVATE_KEY') STRIPE_API_PUBLIC_KEY = env('STRIPE_API_PUBLIC_KEY') +STRIPE_API_PRIVATE_KEY_TEST = env('STRIPE_API_PRIVATE_KEY_TEST') ANONYMOUS_USER_NAME = 'anonymous@ungleich.ch' GUARDIAN_GET_INIT_ANONYMOUS_USER = 'membership.models.get_anonymous_user_instance' @@ -537,9 +542,12 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = { 'ungleich.ch': 'UA-62285904-1', 'digitalglarus.ch': 'UA-62285904-2', 'blog.ungleich.ch': 'UA-62285904-4', - 'hosting': 'UA-62285904-5', - 'datacenterlight.ch': 'UA-62285904-9', - + 'rails-hosting.ch': 'UA-62285904-5', + 'django-hosting.ch': 'UA-62285904-6', + 'node-hosting.ch': 'UA-62285904-7', + 'datacenterlight.ch': 'UA-62285904-8', + 'devuanhosting.ch': 'UA-62285904-9', + 'ipv6onlyhosting.ch': 'UA-62285904-10', '127.0.0.1:8000': 'localhost', 'dynamicweb-development.ungleich.ch': 'development', 'dynamicweb-staging.ungleich.ch': 'staging' @@ -564,7 +572,8 @@ if ENABLE_DEBUG_LOGGING: 'file': { 'level': 'DEBUG', 'class': 'logging.FileHandler', - 'filename': "{PROJECT_DIR}/debug.log".format(PROJECT_DIR=PROJECT_DIR), + 'filename': "{PROJECT_DIR}/debug.log".format( + PROJECT_DIR=PROJECT_DIR), }, }, 'loggers': { diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index 3cc30292..ddb853da 100644 --- a/hosting/locale/de/LC_MESSAGES/django.po +++ b/hosting/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-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" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -245,31 +245,26 @@ msgstr "Gesamt" msgid "Finish Configuration" msgstr "Konfiguration beenden" +msgid "Order Nr." +msgstr "Bestellung Nr." + msgid "Amount" msgstr "Betrag" msgid "Status" msgstr "" -msgid "Approved" -msgstr "Akzeptiert" - -msgid "Declined" -msgstr "Abgelehnt" +msgid "See Invoice" +msgstr "Rechnung" msgid "View Detail" msgstr "Details anzeigen" -msgid "Cancel Order" -msgstr "Bestellung stornieren" +msgid "Page" +msgstr "" -#, fuzzy -#| msgid "Do You want to delete your order?" -msgid "Do you want to delete your order?" -msgstr "Willst du deine Bestellung löschen?" - -msgid "Delete" -msgstr "Löschen" +msgid "of" +msgstr "" msgid "Your Order" msgstr "Deine Bestellung" @@ -280,6 +275,9 @@ msgstr "Konfiguration" msgid "including VAT" msgstr "inkl. Mehrwertsteuer" +msgid "Month" +msgstr "Monat" + msgid "Billing Address" msgstr "Rechnungsadresse" @@ -301,14 +299,9 @@ msgstr "" "speichern keine Informationen in unserer Datenbank." msgid "" -"\n" -" You are not making any payment yet. " -"After submitting your card\n" -" information, you will be taken to " -"the Confirm Order Page.\n" -" " +"You are not making any payment yet. After submitting your card information, " +"you will be taken to the Confirm Order Page." msgstr "" -"\n" "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner " "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " "weitergeleitet." @@ -328,19 +321,6 @@ msgstr "" msgid "Card Type" msgstr "Kartentyp" -msgid "" -"\n" -" You are not making any payment " -"yet. After submitting your card\n" -" information, you will be taken " -"to the Confirm Order Page.\n" -" " -msgstr "" -"\n" -"Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe Deiner " -"Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " -"weitergeleitet." - msgid "Processing" msgstr "Weiter" @@ -390,6 +370,9 @@ msgstr "" msgid "Private Key" msgstr "" +msgid "Delete" +msgstr "Löschen" + msgid "Delete SSH Key" msgstr "SSH Key löschen" @@ -399,38 +382,70 @@ msgstr "Möchtest Du den Schlüssel löschen?" msgid "Show" msgstr "Anzeigen" +#, fuzzy +#| msgid "Public SSH Key" msgid "Public SSH Key" msgstr "Public SSH Key" msgid "Download" msgstr "" -msgid "Settings" -msgstr "Einstellungen" +msgid "Your Virtual Machine Detail" +msgstr "Virtuelle Maschinen Detail" -msgid "Billing" -msgstr "Abrechnungen" +msgid "VM Settings" +msgstr "VM Einstellungen" -msgid "Ip not assigned yet" -msgstr "Ip nicht zugewiesen" +msgid "Copied" +msgstr "Kopiert" msgid "Disk" msgstr "Festplatte" -msgid "Current pricing" +msgid "Billing" +msgstr "Abrechnungen" + +msgid "Current Pricing" msgstr "Aktueller Preis" -msgid "Current status" -msgstr "Aktueller Status" +msgid "Your VM is" +msgstr "Deine VM ist" -msgid "Terminate Virtual Machine" -msgstr "Virtuelle Maschine beenden" +msgid "Pending" +msgstr "In Vorbereitung" + +msgid "Online" +msgstr "" + +msgid "Failed" +msgstr "Fehlgeschlagen" + +msgid "Terminate VM" +msgstr "VM Beenden" + +msgid "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" -msgstr "Ihre virtuelle Maschine beenden" +msgstr "Deine Virtuelle Maschine beenden" -msgid "Are you sure do you want to cancel your Virtual Machine " -msgstr "Sind Sie sicher, dass Sie ihre virtuelle Maschine beenden wollen " +msgid "Do you want to cancel your Virtual Machine" +msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst" + +msgid "OK" +msgstr "" msgid "Virtual Machines" msgstr "Virtuelle Maschinen" @@ -441,14 +456,8 @@ msgstr "" msgid "CREATE VM" msgstr "NEUE VM" -msgid "Page" -msgstr "" - -msgid "of" -msgstr "" - msgid "login" -msgstr "einloggen" +msgstr "Einloggen" msgid "" "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." msgstr "Entschuldigung, deine Anfrage ist ungültig." +msgid "Invalid credit card" +msgstr "Ungültige Kreditkarte" + msgid "Confirm Order" msgstr "Bestellung Bestätigen" @@ -482,6 +494,55 @@ msgid "" "contact Data Center Light Support." 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" #~ msgstr "IPv4" @@ -609,14 +670,6 @@ msgstr "" #~ msgid "Place Order" #~ msgstr "Bestelle" -#~ msgid "" -#~ "You are not making any payment yet. After placing your order, you will be " -#~ "taken to the Submit Payment Page." -#~ msgstr "" -#~ "Es wird noch keine Bezahlung vorgenommen. Nach der Eingabe deiner " -#~ "Kreditkateninformationen wirst du auf die Bestellbestätigungsseite " -#~ "weitergeleitet." - #~ msgid "CARD NUMBER" #~ msgstr "Kreditkartennummer" diff --git a/hosting/migrations/0042_hostingorder_subscription_id.py b/hosting/migrations/0042_hostingorder_subscription_id.py new file mode 100644 index 00000000..2aa634a8 --- /dev/null +++ b/hosting/migrations/0042_hostingorder_subscription_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-08-17 16:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0041_userhostingkey_private_key'), + ] + + operations = [ + migrations.AddField( + model_name='hostingorder', + name='subscription_id', + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index 8cdc6114..478ed745 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -50,6 +50,7 @@ class HostingOrder(AssignPermissionsMixin, models.Model): cc_brand = models.CharField(max_length=10) stripe_charge_id = models.CharField(max_length=100, null=True) price = models.FloatField() + subscription_id = models.CharField(max_length=100, null=True) permissions = ('view_hostingorder',) @@ -66,7 +67,8 @@ class HostingOrder(AssignPermissionsMixin, models.Model): return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS @classmethod - def create(cls, price=None, vm_id=None, customer=None, billing_address=None): + def create(cls, price=None, vm_id=None, customer=None, + billing_address=None): instance = cls.objects.create( price=price, vm_id=vm_id, @@ -86,6 +88,23 @@ class HostingOrder(AssignPermissionsMixin, models.Model): self.cc_brand = stripe_charge.source.brand self.save() + def set_subscription_id(self, subscription_object, cc_details): + """ + When creating a Stripe subscription, we have subscription id. + We store this in the subscription_id field. + This method sets the subscription id from subscription_object + and also the last4 and credit card brands used for this order. + + :param subscription_object: Stripe's subscription object + :param cc_details: A dict containing card details + {last4, brand} + :return: + """ + self.subscription_id = subscription_object.id + self.last4 = cc_details.get('last4') + self.cc_brand = cc_details.get('brand') + self.save() + def get_cc_data(self): return { 'last4': self.last4, @@ -137,5 +156,6 @@ class HostingBill(AssignPermissionsMixin, models.Model): @classmethod def create(cls, customer=None, billing_address=None): - instance = cls.objects.create(customer=customer, billing_address=billing_address) + instance = cls.objects.create(customer=customer, + billing_address=billing_address) return instance diff --git a/hosting/static/hosting/css/landing-page.css b/hosting/static/hosting/css/landing-page.css index 7a569dc8..d1dc657a 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -533,9 +533,21 @@ a.unlink:hover { padding-left: 5px; } +.dcl-place-order-text{ + font-size: 13px; + color: #808080; +} + .dcl-order-table-total .tbl-total { text-align: center; color: #000; + padding-left: 44px; +} + +.tbl-total .dcl-price-month { + font-size: 16px; + text-transform: capitalize; + color: #000; } .tbl-no-padding { @@ -782,4 +794,4 @@ a.list-group-item-danger.active:focus { } .panel-danger > .panel-heading .badge { background-color: #eb4d5c; -} \ No newline at end of file +} diff --git a/hosting/static/hosting/css/virtual-machine.css b/hosting/static/hosting/css/virtual-machine.css index 420a9452..45aa68ff 100644 --- a/hosting/static/hosting/css/virtual-machine.css +++ b/hosting/static/hosting/css/virtual-machine.css @@ -1,3 +1,6 @@ +.virtual-machine-container { + max-width: 900px; +} .virtual-machine-container .tabs-left, .virtual-machine-container .tabs-right { border-bottom: none; padding-top: 2px; @@ -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 */ .dashboard-container-head { padding: 0 8px; @@ -239,10 +440,10 @@ } .dashboard-title-thin .un-icon { - height: 34px; + height: 30px; margin-right: 5px; margin-top: -1px; - width: 20px; + width: 30px; } .dashboard-subtitle { @@ -287,6 +488,24 @@ color: #3770CC; } +.btn-order-detail { + background: #87B6EA; + color: #fff; + font-weight: 400; + letter-spacing: 0.6px; + font-size: 14px; + border-radius: 3px; + border: 2px solid #87B6EA; + padding: 4px 20px; + min-width: 155px; + /* padding-bottom: 7px; */ +} + +.btn-order-detail:hover, .btn-order-detail:focus, .btn-order-detail:active { + background: #fff; + color: #87B6EA; +} + .vm-status, .vm-status-active, .vm-status-failed { font-weight: 600; } @@ -355,8 +574,8 @@ position: relative; border-top: 1px solid #ddd; /* margin-top: 15px; */ - padding-top: 5px; - padding-bottom: 15px; + padding-top: 10px; + padding-bottom: 13px; } .table-switch tbody tr:last-child { border-bottom: 1px solid #ddd; @@ -373,11 +592,28 @@ font-weight: 600; position: absolute; top: 5px; - + left: 8px; } .table-switch .last-td { position: absolute; - bottom: 20px; + bottom: 13px; right: 0; } + .table-switch tbody tr .xs-td-inline { + text-align: right; + padding-top: 6px; + } + .table-switch tbody tr .xs-td-bighalf { + width: 52%; + display: inline-block; + } + .table-switch tbody tr .xs-td-smallhalf { + width: 47%; + text-align: right; + display: inline-block; + } + .table-switch tbody tr .xs-td-smallhalf:before { + left: auto; + right: 8px; + } } \ No newline at end of file diff --git a/hosting/static/hosting/img/24-hours-support.svg b/hosting/static/hosting/img/24-hours-support.svg new file mode 100644 index 00000000..4db05be3 --- /dev/null +++ b/hosting/static/hosting/img/24-hours-support.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosting/static/hosting/img/billing.svg b/hosting/static/hosting/img/billing.svg new file mode 100644 index 00000000..d002fa6c --- /dev/null +++ b/hosting/static/hosting/img/billing.svg @@ -0,0 +1 @@ +billing icon \ No newline at end of file diff --git a/hosting/static/hosting/img/connected.svg b/hosting/static/hosting/img/connected.svg new file mode 100644 index 00000000..fa3875dc --- /dev/null +++ b/hosting/static/hosting/img/connected.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosting/static/hosting/img/settings.svg b/hosting/static/hosting/img/settings.svg new file mode 100644 index 00000000..61dc8613 --- /dev/null +++ b/hosting/static/hosting/img/settings.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosting/static/hosting/img/shopping-cart.svg b/hosting/static/hosting/img/shopping-cart.svg new file mode 100644 index 00000000..19e70e1d --- /dev/null +++ b/hosting/static/hosting/img/shopping-cart.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/hosting/static/hosting/js/initial.js b/hosting/static/hosting/js/initial.js index da2887c6..50975806 100644 --- a/hosting/static/hosting/js/initial.js +++ b/hosting/static/hosting/js/initial.js @@ -13,4 +13,62 @@ $( document ).ready(function() { }, 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); + } + }); }); \ No newline at end of file diff --git a/hosting/templates/hosting/base.html b/hosting/templates/hosting/base.html index b485451f..ec57475d 100644 --- a/hosting/templates/hosting/base.html +++ b/hosting/templates/hosting/base.html @@ -38,7 +38,6 @@ {% with 'hosting/img/'|add:hosting|add:'-intro-bg.png' as image_static %} - alt="">