diff --git a/.gitignore b/.gitignore index e09fef54..1b2b4d16 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ secret-key /utils/optimize/ # to keep empty dirs -!.gitkeep \ No newline at end of file +!.gitkeep +*.orig diff --git a/Changelog b/Changelog index 46b2534b..471f0720 100644 --- a/Changelog +++ b/Changelog @@ -1,7 +1,10 @@ -next: +1.7: 2018-04-20 * bgfix: [all] Make /blog available on all domains * #4367: [dcl] email logo resolution fix * #4376: [cms] dcl promo section plugin link color changed to brighter shade + * #4379: [dcl] pricing without VAT + * bgfix: [blog] fix top menu items to show only one item + * #4297: [cms] favicon as a page attribute for dcl template 1.6.5: 2018-04-08 * #4396: [ungleich] add favicon to ungleich blog * #4327: [dcl] fix navbar logo repeat diff --git a/datacenterlight/admin.py b/datacenterlight/admin.py index acb93fff..d95e4f87 100644 --- a/datacenterlight/admin.py +++ b/datacenterlight/admin.py @@ -1,10 +1,18 @@ from django.contrib import admin from cms.admin.placeholderadmin import PlaceholderAdminMixin -from .cms_models import CMSIntegration +from cms.extensions import PageExtensionAdmin +from .cms_models import CMSIntegration, CMSFaviconExtension +from .models import VMPricing class CMSIntegrationAdmin(PlaceholderAdminMixin, admin.ModelAdmin): list_display = ('name', 'domain') +class CMSFaviconExtensionAdmin(PageExtensionAdmin): + pass + + admin.site.register(CMSIntegration, CMSIntegrationAdmin) +admin.site.register(CMSFaviconExtension, CMSFaviconExtensionAdmin) +admin.site.register(VMPricing) diff --git a/datacenterlight/cms_models.py b/datacenterlight/cms_models.py index 9eb55e0c..dd6a165f 100644 --- a/datacenterlight/cms_models.py +++ b/datacenterlight/cms_models.py @@ -1,11 +1,16 @@ +from cms.extensions import PageExtension +from cms.extensions.extension_pool import extension_pool from cms.models.fields import PlaceholderField from cms.models.pluginmodel import CMSPlugin from django.contrib.sites.models import Site from django.db import models from django.utils.safestring import mark_safe from djangocms_text_ckeditor.fields import HTMLField +from filer.fields.file import FilerFileField from filer.fields.image import FilerImageField +from datacenterlight.models import VMPricing + class CMSIntegration(models.Model): name = models.CharField( @@ -30,9 +35,15 @@ class CMSIntegration(models.Model): return self.name -# Models for CMS Plugins +class CMSFaviconExtension(PageExtension): + favicon = FilerFileField(related_name="cms_favicon_image") +extension_pool.register(CMSFaviconExtension) + + +# Models for CMS Plugins + class DCLSectionPluginModel(CMSPlugin): heading = models.CharField( blank=True, null=True, max_length=100, @@ -275,3 +286,12 @@ class DCLSectionPromoPluginModel(CMSPlugin): if self.background_image: extra_classes += ' promo-with-bg' return extra_classes + + +class DCLCustomPricingModel(CMSPlugin): + pricing = models.ForeignKey( + VMPricing, + related_name="dcl_custom_pricing_vm_pricing", + help_text='Choose a pricing that will be associated with this ' + 'Calculator' + ) diff --git a/datacenterlight/cms_plugins.py b/datacenterlight/cms_plugins.py index a1a3833d..19dc0b39 100644 --- a/datacenterlight/cms_plugins.py +++ b/datacenterlight/cms_plugins.py @@ -6,9 +6,9 @@ from .cms_models import ( DCLFooterPluginModel, DCLLinkPluginModel, DCLNavbarDropdownPluginModel, DCLSectionIconPluginModel, DCLSectionImagePluginModel, DCLSectionPluginModel, DCLNavbarPluginModel, - DCLSectionPromoPluginModel + DCLSectionPromoPluginModel, DCLCustomPricingModel ) -from .models import VMTemplate +from .models import VMTemplate, VMPricing @plugin_pool.register_plugin @@ -75,13 +75,13 @@ class DCLSectionPromoPlugin(CMSPluginBase): @plugin_pool.register_plugin class DCLCalculatorPlugin(CMSPluginBase): module = "Datacenterlight" - name = "DCL Calculator Plugin" + name = "DCL Calculator Section Plugin" model = DCLSectionPluginModel render_template = "datacenterlight/cms/calculator.html" cache = False allow_children = True child_classes = [ - 'DCLSectionPromoPlugin', 'UngleichHTMLPlugin' + 'DCLSectionPromoPlugin', 'UngleichHTMLPlugin', 'DCLCustomPricingPlugin' ] def render(self, context, instance, placeholder): @@ -89,15 +89,38 @@ class DCLCalculatorPlugin(CMSPluginBase): context, instance, placeholder ) context['templates'] = VMTemplate.objects.all() - context['children_to_side'] = [] context['children_to_content'] = [] + pricing_plugin_model = None if instance.child_plugin_instances is not None: context['children_to_content'].extend( instance.child_plugin_instances ) + for child in instance.child_plugin_instances: + if child.__class__.__name__ == 'DCLCustomPricingModel': + # The second clause is just to make sure we pick up the + # most recent CustomPricing, if more than one is present + if (pricing_plugin_model is None or child.pricing_id > + pricing_plugin_model.model.pricing_id): + pricing_plugin_model = child + + if pricing_plugin_model: + context['vm_pricing'] = VMPricing.get_vm_pricing_by_name( + name=pricing_plugin_model.pricing.name + ) + else: + context['vm_pricing'] = VMPricing.get_default_pricing() + return context +@plugin_pool.register_plugin +class DCLCustomPricingPlugin(CMSPluginBase): + module = "Datacenterlight" + name = "DCL Custom Pricing Plugin" + model = DCLCustomPricingModel + render_plugin = False + + @plugin_pool.register_plugin class DCLBannerListPlugin(CMSPluginBase): module = "Datacenterlight" diff --git a/datacenterlight/cms_toolbar.py b/datacenterlight/cms_toolbar.py new file mode 100644 index 00000000..15a8cb4b --- /dev/null +++ b/datacenterlight/cms_toolbar.py @@ -0,0 +1,24 @@ +from cms.extensions.toolbar import ExtensionToolbar +from cms.toolbar_pool import toolbar_pool +from django.utils.translation import ugettext_lazy as _ + +from .cms_models import CMSFaviconExtension + + +@toolbar_pool.register +class CMSFaviconExtensionToolbar(ExtensionToolbar): + # defineds the model for the current toolbar + model = CMSFaviconExtension + + def populate(self): + # setup the extension toolbar with permissions and sanity checks + current_page_menu = self._setup_extension_toolbar() + # if it's all ok + if current_page_menu: + # retrieves the instance of the current extension (if any) and the toolbar item url + page_extension, url = self.get_page_extension_admin() + if url: + # adds a toolbar item + current_page_menu.add_modal_item( + _('CMS Favicon'), url=url, disabled=not self.toolbar.edit_mode + ) diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 5942573b..50dbfbe8 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: 2018-03-30 21:29+0000\n" +"POT-Creation-Date: 2018-04-17 19:26+0000\n" "PO-Revision-Date: 2018-03-30 23:22+0000\n" "Last-Translator: b'Anonymous User '\n" "Language-Team: LANGUAGE \n" @@ -72,9 +72,9 @@ msgstr "Data Center Light Account Aktivierung" #, python-format msgid "" -"You can activate your Data Center Light account by clicking here." +"You can activate your Data Center Light account by clicking here." msgstr "" "Klicke hier um deinen Data Center " @@ -97,12 +97,13 @@ msgstr "Deine E-Mail-Adresse" msgid "Password" msgstr "Passwort" +#, python-format msgid "" "You can reset your password here." msgstr "" -"Du kannst dein Passwort hier " +"Du kannst dein Passwort hier " "zurücksetzen." msgid "Your Data Center Light Team" @@ -160,21 +161,6 @@ msgstr "Weiter" msgid "Home" msgstr "Home" -msgid "Highlights" -msgstr "" - -msgid "Scale out" -msgstr "Skalierung" - -msgid "Reliable and light" -msgstr "Zuverlässig und leicht" - -msgid "Pricing" -msgstr "Preise" - -msgid "Order VM" -msgstr "VM bestellen" - msgid "Contact" msgstr "Kontakt" @@ -184,6 +170,9 @@ msgstr "Nutzungsbedingungen" msgid "Finally, an affordable VM hosting in Switzerland!" msgstr "Endlich: bezahlbares VM Hosting in der Schweiz" +msgid "Highlights" +msgstr "" + msgid "I want it!" msgstr "Das will ich haben!" @@ -203,8 +192,8 @@ msgid "" "order to make it more sustainable and affordable at the same time." msgstr "" "Ist kreativ, indem es sich ein modernes und alternatives Layout zu Nutze " -"macht um Nachhaltigkeit zu fördern und somit erschwingliche Preise bieten zu" -" können.
" +"macht um Nachhaltigkeit zu fördern und somit erschwingliche Preise bieten zu " +"können.
" msgid "" "Cuts down the costs for you by using FOSS (Free Open Source Software) " @@ -214,6 +203,9 @@ msgstr "" "mit FOSS (Free Open Source Software) arbeitet und wir daher auf " "Lizenzgebühren verzichten können.
" +msgid "Scale out" +msgstr "Skalierung" + msgid "" "We don't use special hardware. We use commodity hardware: we buy computers " "that you buy. Just many more and put them in a cozy home for computers " @@ -223,6 +215,9 @@ msgstr "" "erschwingliche Systeme. Bei grösserer Auslastung werden mehr " "Standardkomponenten hinzugekauft und skalieren so das Datencenter." +msgid "Reliable and light" +msgstr "Zuverlässig und leicht" + msgid "" "Our VMs are located in Switzerland, with reliable power supply and fast " "internet connection. Our VM costs less thanks to our featherlight " @@ -232,8 +227,7 @@ msgstr "" "Energieversorgung, sowie schneller Internetverbindung ausgestattet. Unser " "Angebot ist aufgrund unserer leichten Infrastruktur überaus kostengünstig." -msgid "" -"Simple and affordable: Try our virtual machine with featherlight price." +msgid "Simple and affordable: Try our virtual machine with featherlight price." msgstr "" "Einfach und bezahlbar: Teste nun unsere virtuellen Maschinen mit " "federleichten Preisen." @@ -314,6 +308,9 @@ msgstr "Gesamt" msgid "including VAT" msgstr "inkl. Mehrwertsteuer" +msgid "excluding VAT" +msgstr "exkl. Mehrwertsteuer" + msgid "Month" msgstr "Monat" @@ -321,20 +318,20 @@ msgid "Credit Card" msgstr "Kreditkarte" msgid "" -"Please fill in your credit card information below. We are using Stripe for payment and do " -"not store your information in our database." +"Please fill in your credit card information below. We are using Stripe for payment and do not " +"store your information in our database." msgstr "" -"Bitte fülle Deine Kreditkarteninformationen unten aus. Wir nutzen Stripe für die Bezahlung " -"und speichern keine Informationen in unserer Datenbank." +"Bitte fülle Deine Kreditkarteninformationen unten aus. Wir nutzen Stripe für die Bezahlung und " +"speichern keine Informationen in unserer Datenbank." msgid "" "You are not making any payment yet. After submitting your card information, " "you will be taken to the Confirm Order Page." msgstr "" -"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst," -" nachdem Du die Bestellung auf der nächsten Seite bestätigt hast." +"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, " +"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast." msgid "Card Number" msgstr "Kreditkartennummer" @@ -352,8 +349,8 @@ 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. Die Bezahlung wird erst ausgelöst," -" nachdem Du die Bestellung auf der nächsten Seite bestätigt hast." +"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, " +"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast." msgid "Processing" msgstr "Weiter" @@ -383,13 +380,18 @@ msgstr "Bestellungsübersicht" msgid "Product" msgstr "Produkt" -#, python-format +msgid "Subtotal" +msgstr "Zwischensumme" + +msgid "VAT" +msgstr "Mehrwertsteuer" + msgid "" "By clicking \"Place order\" this plan will charge your credit card account " -"with the fee of %(vm_price)sCHF/month" +"with the fee of %(vm_total_price)s CHF/month" msgstr "" -"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF " -"pro Monat belastet" +"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " +"%(vm_total_price)s CHF pro Monat belastet" msgid "Place order" msgstr "Bestellen" @@ -455,25 +457,25 @@ msgstr "Wir unterstützen die FOSS Community." msgid "" "Data Center Light is the child of free and open source software (FOSS) " "movement.
We grew up with it, live by it, and believe in it.
The " -"more we work on our data center,
the more we contribute back to the FOSS" -" community." +"more we work on our data center,
the more we contribute back to the FOSS " +"community." msgstr "" "Data Center Light ist ein Teil der Free und Opens Source Software (FOSS) " -"Bewegung.
Wir sind damit gross geworden, leben damit und glauben " -"daran.
Je weiter wir mit unserem Data Center Light vorankommen, desto " -"mehr können wir etwas an die FOSS Community zurückgeben." +"Bewegung.
Wir sind damit gross geworden, leben damit und glauben daran." +"
Je weiter wir mit unserem Data Center Light vorankommen, desto mehr " +"können wir etwas an die FOSS Community zurückgeben." msgid "We bring the future to you." msgstr "Wir bringen die Zukunft zu dir." msgid "" "Data Center Light uses the most modern technologies out there.
Your VM " -"needs only IPv6. Data Center Light provides
transparent two-way " -"IPv6/IPv4 translation." +"needs only IPv6. Data Center Light provides
transparent two-way IPv6/" +"IPv4 translation." msgstr "" "Data Center Light verwendet die zur Zeit modernsten Technologien.
Deine " -"VM läuft mit IPv6. Data Center Light bietet eine transparente " -"IPv6/IPv4-Zweiweglösung." +"VM läuft mit IPv6. Data Center Light bietet eine transparente IPv6/IPv4-" +"Zweiweglösung." msgid "" " No more spinning metal plates! Data Center Light uses only SSDs. We keep " @@ -497,6 +499,10 @@ msgstr "Ungültige RAM-Grösse" msgid "Invalid storage size" msgstr "Ungültige Speicher-Grösse" +#, python-brace-format +msgid "Incorrect pricing name. Please contact support{support_email}" +msgstr "" + msgid "Confirm Order" msgstr "Bestellung Bestätigen" @@ -507,8 +513,8 @@ msgid "" "There was a payment related error. On close of this popup, you will be " "redirected back to the payment page." msgstr "" -"Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom" -" Popup zur Bezahlseite weitergeleitet." +"Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom " +"Popup zur Bezahlseite weitergeleitet." msgid "Thank you for the order." msgstr "Danke für Deine Bestellung." @@ -517,8 +523,14 @@ msgid "" "Your VM will be up and running in a few moments. We will send you a " "confirmation email as soon as it is ready." msgstr "" -"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du" -" auf sie zugreifen kannst." +"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du " +"auf sie zugreifen kannst." + +#~ msgid "Pricing" +#~ msgstr "Preise" + +#~ msgid "Order VM" +#~ msgstr "VM bestellen" #~ msgid "Enter name" #~ msgstr "Name" @@ -533,18 +545,19 @@ msgstr "" #~ msgstr "Anfrage verschickt" #~ msgid "" -#~ "Thank you for your subscription! You will receive a confirmation mail from " -#~ "our team" +#~ "Thank you for your subscription! You will receive a confirmation mail " +#~ "from our team" #~ msgstr "" -#~ "Vielen dank für Ihre Anmeldung. Sie erhalten in kürze eine Bestätigungsmail " -#~ "von unserem Team" +#~ "Vielen dank für Ihre Anmeldung. Sie erhalten in kürze eine " +#~ "Bestätigungsmail von unserem Team" #~ msgid "Thank you for your request." #~ msgstr "Vielen Dank für Deine Anfrage." #~ msgid "You are one step away from being our beta tester!" #~ msgstr "" -#~ "Sie sind nur noch einen Schritt davon entfernt, unser Beta-Tester zu werden!" +#~ "Sie sind nur noch einen Schritt davon entfernt, unser Beta-Tester zu " +#~ "werden!" #~ msgid "" #~ "Currently we are running our tests to make sure everything runs perfectly." @@ -553,8 +566,8 @@ msgstr "" #~ "sicherzustellen." #~ msgid "" -#~ "In the meantime, we would like to ask you a little patience
until our " -#~ "team contacts you with beta access." +#~ "In the meantime, we would like to ask you a little patience
until " +#~ "our team contacts you with beta access." #~ msgstr "" #~ "Wir werden dann sobald als möglich Ihren Beta-Zugang erstellen und Sie " #~ "daraufhin kontaktieren.Bis dahin bitten wir Sie um etwas Geduld." @@ -564,8 +577,8 @@ msgstr "" #~ msgid "Thank you for order! Our team will contact you via email" #~ msgstr "" -#~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich mit" -#~ " Dir via E-Mail in Verbindung." +#~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich " +#~ "mit Dir via E-Mail in Verbindung." #~ msgid "Affordable VM hosting based in Switzerland" #~ msgstr "Bezahlbares VM Hosting in der Schweiz" @@ -581,18 +594,18 @@ msgstr "" #~ msgid "" #~ "Our VMs are hosted in Glarus, Switzerland, and our website is currently " -#~ "running in BETA mode. If you want more information that you did not find on " -#~ "our website, or if your order is more detailed, or if you encounter any " -#~ "technical hiccups, please contact us at support@datacenterlight.ch, our team" -#~ " will get in touch with you asap." +#~ "running in BETA mode. If you want more information that you did not find " +#~ "on our website, or if your order is more detailed, or if you encounter " +#~ "any technical hiccups, please contact us at support@datacenterlight.ch, " +#~ "our team will get in touch with you asap." #~ msgstr "" -#~ "Unsere VMs werden in der Schweiz im Kanton Glarus gehostet und befinden sich" -#~ " zur Zeit noch in der BETA-Phase. Möchtest du mehr über uns erfahren und " -#~ "hast auf unserer Website nicht genügend Informationen gefunden? Möchtest " -#~ "eine detailliertere Bestellung aufgeben? Bist du auf technische Probleme " -#~ "gestossen, die du uns mitteilen möchtest? Dann zögere nicht und kontaktiere " -#~ "uns unter support@datacenterlight.ch. Unser Team wird sich umgehend um dein " -#~ "Anliegen kümmern!" +#~ "Unsere VMs werden in der Schweiz im Kanton Glarus gehostet und befinden " +#~ "sich zur Zeit noch in der BETA-Phase. Möchtest du mehr über uns erfahren " +#~ "und hast auf unserer Website nicht genügend Informationen gefunden? " +#~ "Möchtest eine detailliertere Bestellung aufgeben? Bist du auf technische " +#~ "Probleme gestossen, die du uns mitteilen möchtest? Dann zögere nicht und " +#~ "kontaktiere uns unter support@datacenterlight.ch. Unser Team wird sich " +#~ "umgehend um dein Anliegen kümmern!" #~ msgid "is not a proper name" #~ msgstr "ist kein gültiger Name" @@ -610,12 +623,14 @@ msgstr "" #~ "\n" #~ "Hi,\n" #~ "\n" -#~ "You can activate your %(dcl_text)s account by clicking here %(base_url)s%(activation_link)s\n" +#~ "You can activate your %(dcl_text)s account by clicking here %(base_url)s" +#~ "%(activation_link)s\n" #~ msgstr "" #~ "\n" #~ "Hallo,\n" #~ "\n" -#~ "Du kannst deinen %(dcl_text)s Account aktivieren, indem du hier klickst %(base_url)s%(activation_link)s\n" +#~ "Du kannst deinen %(dcl_text)s Account aktivieren, indem du hier klickst " +#~ "%(base_url)s%(activation_link)s\n" #~ msgid "Your" #~ msgstr "Dein" @@ -650,12 +665,14 @@ msgstr "" #~ msgid "I want to have it!" #~ msgstr "Das möchte ich haben!" -#~ msgid "Reuse existing factory halls intead of building an expensive building." +#~ msgid "" +#~ "Reuse existing factory halls intead of building an expensive building." #~ msgstr "" #~ "Nachhaltigkeit: Wiederverwendung ehemaliger Fabrikhallen an Stelle der " #~ "Errichtung eines neuen Gebäudes" -#~ msgid "Being creative, using modern and alternative design for a datacenter." +#~ msgid "" +#~ "Being creative, using modern and alternative design for a datacenter." #~ msgstr "" #~ "Kreativität: Verwendung eines modernen und alternativen Designs für unser " #~ "Datencenter" @@ -678,8 +695,8 @@ msgstr "" #~ msgstr "Standort des Datacenters ist in der Schweiz" #~ msgid "" -#~ " WARNING: We are currently running in BETA mode. We hope you won't encounter" -#~ " any hiccups, but if you do, please let us know at " +#~ " WARNING: We are currently running in BETA mode. We hope you won't " +#~ "encounter any hiccups, but if you do, please let us know at " #~ "support@datacenterlight.ch" #~ msgstr "" #~ " Achtung: Wir befinden uns zurzeit im Beta-Release. Wir hoffen, dass Sie " @@ -693,8 +710,8 @@ msgstr "" #~ msgstr "Unser Versprechen" #~ msgid "" -#~ "Instead of creating an expensive SLA for availability, we promise that we do" -#~ " our best to run things as smooth as possible." +#~ "Instead of creating an expensive SLA for availability, we promise that we " +#~ "do our best to run things as smooth as possible." #~ msgstr "" #~ "Anstatt eines SLAs (Service Levle Agreements) zu vereinbaren,setzen wir " #~ "unsere persönliche Arbeitskraft ein, um Ihnen ein sorgenfreiesHosting zu " diff --git a/datacenterlight/management/commands/create_default_vm_pricing.py b/datacenterlight/management/commands/create_default_vm_pricing.py new file mode 100644 index 00000000..c1b36eea --- /dev/null +++ b/datacenterlight/management/commands/create_default_vm_pricing.py @@ -0,0 +1,36 @@ +from django.core.management.base import BaseCommand + +from datacenterlight.models import VMPricing + + +class Command(BaseCommand): + help = '''Creates default VMPricing object''' + DEFAULT_VMPRICING_NAME = 'default' + + def handle(self, *args, **options): + self.create_default_vm_pricing() + + def create_default_vm_pricing(self): + obj, created = VMPricing.objects.get_or_create( + name=self.DEFAULT_VMPRICING_NAME, + defaults={ + "vat_inclusive": True, + "cores_unit_price": 5, + "ram_unit_price": 2, + "ssd_unit_price": 0.6, + "hdd_unit_price": 0.01 + } + ) + + if created: + print( + 'Successfully created {} VMPricing object'.format( + self.DEFAULT_VMPRICING_NAME + ) + ) + else: + print( + '{} VMPricing exists already.'.format( + self.DEFAULT_VMPRICING_NAME + ) + ) diff --git a/datacenterlight/migrations/0019_auto_20180415_2236.py b/datacenterlight/migrations/0019_auto_20180415_2236.py new file mode 100644 index 00000000..4b711a2b --- /dev/null +++ b/datacenterlight/migrations/0019_auto_20180415_2236.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-04-15 22:36 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cms', '0014_auto_20160404_1908'), + ('datacenterlight', '0018_auto_20180403_1930'), + ] + + operations = [ + migrations.CreateModel( + name='DCLCustomPricingModel', + fields=[ + ('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cms.CMSPlugin')), + ], + options={ + 'abstract': False, + }, + bases=('cms.cmsplugin',), + ), + migrations.CreateModel( + name='VMPricing', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('vat_inclusive', models.BooleanField(default=True)), + ('vat_percentage', models.DecimalField(blank=True, decimal_places=5, default=0, max_digits=7)), + ('cores_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)), + ('ram_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)), + ('ssd_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)), + ('hdd_unit_price', models.DecimalField(decimal_places=6, default=0, max_digits=7)), + ], + ), + migrations.AddField( + model_name='dclcustompricingmodel', + name='pricing', + field=models.ForeignKey(help_text='Choose a pricing that will be associated with this Calculator', on_delete=django.db.models.deletion.CASCADE, related_name='dcl_custom_pricing_vm_pricing', to='datacenterlight.VMPricing'), + ), + ] diff --git a/datacenterlight/migrations/0019_cmsfaviconextension.py b/datacenterlight/migrations/0019_cmsfaviconextension.py new file mode 100644 index 00000000..7b350a70 --- /dev/null +++ b/datacenterlight/migrations/0019_cmsfaviconextension.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-04-12 03:16 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import filer.fields.file + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0018_auto_20180403_1930'), + ] + + operations = [ + migrations.CreateModel( + name='CMSFaviconExtension', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_object', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='cms.Page')), + ('favicon', filer.fields.file.FilerFileField(on_delete=django.db.models.deletion.CASCADE, related_name='cms_favicon_image', to='filer.File')), + ('public_extension', models.OneToOneField(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='draft_extension', to='datacenterlight.CMSFaviconExtension')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/datacenterlight/migrations/0020_merge.py b/datacenterlight/migrations/0020_merge.py new file mode 100644 index 00000000..6bbe0086 --- /dev/null +++ b/datacenterlight/migrations/0020_merge.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-04-20 15:04 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0019_auto_20180415_2236'), + ('datacenterlight', '0019_cmsfaviconextension'), + ] + + operations = [ + ] diff --git a/datacenterlight/models.py b/datacenterlight/models.py index 6fcf24a9..eceb7617 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -1,5 +1,9 @@ +import logging + from django.db import models +logger = logging.getLogger(__name__) + class VMTemplate(models.Model): name = models.CharField(max_length=50) @@ -12,6 +16,59 @@ class VMTemplate(models.Model): return vm_template +class VMPricing(models.Model): + name = models.CharField(max_length=255, unique=True) + vat_inclusive = models.BooleanField(default=True) + vat_percentage = models.DecimalField( + max_digits=7, decimal_places=5, blank=True, default=0 + ) + cores_unit_price = models.DecimalField( + max_digits=7, decimal_places=5, default=0 + ) + ram_unit_price = models.DecimalField( + max_digits=7, decimal_places=5, default=0 + ) + ssd_unit_price = models.DecimalField( + max_digits=7, decimal_places=5, default=0 + ) + hdd_unit_price = models.DecimalField( + max_digits=7, decimal_places=6, default=0 + ) + + def __str__(self): + return self.name + ' => ' + ' - '.join([ + '{}/Core'.format(self.cores_unit_price.normalize()), + '{}/GB RAM'.format(self.ram_unit_price.normalize()), + '{}/GB SSD'.format(self.ssd_unit_price.normalize()), + '{}/GB HDD'.format(self.hdd_unit_price.normalize()), + '{}% VAT'.format(self.vat_percentage.normalize()) + if not self.vat_inclusive else 'VAT-Incl', ] + ) + + @classmethod + def get_vm_pricing_by_name(cls, name): + try: + pricing = VMPricing.objects.get(name=name) + except Exception as e: + logger.error( + "Error getting VMPricing with name {name}. " + "Details: {details}. Attempting to return default" + "pricing.".format(name=name, details=str(e)) + ) + pricing = VMPricing.get_default_pricing() + return pricing + + @classmethod + def get_default_pricing(cls): + """ Returns the default pricing or None """ + try: + default_pricing = VMPricing.objects.get(name='default') + except Exception as e: + logger.error(str(e)) + default_pricing = None + return default_pricing + + class StripePlan(models.Model): """ A model to store Data Center Light's created Stripe plans diff --git a/datacenterlight/static/datacenterlight/css/header-slider.css b/datacenterlight/static/datacenterlight/css/header-slider.css index e21e2b49..d01f02a7 100644 --- a/datacenterlight/static/datacenterlight/css/header-slider.css +++ b/datacenterlight/static/datacenterlight/css/header-slider.css @@ -120,6 +120,11 @@ .header_slider .intro-cap { font-size: 3.25em; } + + .header_slider > .carousel .item .container { + padding-left: 0; + padding-right: 0; + } } .header_slider .intro_lead { diff --git a/datacenterlight/static/datacenterlight/js/main.js b/datacenterlight/static/datacenterlight/js/main.js index 6753695c..35f2b247 100644 --- a/datacenterlight/static/datacenterlight/js/main.js +++ b/datacenterlight/static/datacenterlight/js/main.js @@ -171,7 +171,18 @@ } function _calcPricing() { - var total = (cardPricing['cpu'].value * 5) + (2 * cardPricing['ram'].value) + (0.6 * cardPricing['storage'].value); + if(typeof window.coresUnitPrice === 'undefined'){ + window.coresUnitPrice = 5; + } + if(typeof window.ramUnitPrice === 'undefined'){ + window.coresUnitPrice = 2; + } + if(typeof window.ssdUnitPrice === 'undefined'){ + window.ssdUnitPrice = 0.6; + } + var total = (cardPricing['cpu'].value * window.coresUnitPrice) + + (cardPricing['ram'].value * window.ramUnitPrice) + + (cardPricing['storage'].value * window.ssdUnitPrice); total = parseFloat(total.toFixed(2)); $("#total").text(total); } diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index 3db6eb54..db479b43 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -19,6 +19,8 @@ from utils.forms import UserBillingAddressForm from utils.mailer import BaseEmail from utils.models import BillingAddress +from .models import VMPricing + logger = get_task_logger(__name__) @@ -56,7 +58,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, "Running create_vm_task on {}".format(current_task.request.hostname)) vm_id = None try: - final_price = specs.get('price') + final_price = (specs.get('total_price') if 'total_price' in specs + else specs.get('price')) billing_address = BillingAddress( cardholder_name=billing_address_data['cardholder_name'], street_address=billing_address_data['street_address'], @@ -94,17 +97,22 @@ def create_vm_task(self, vm_template_id, user, specs, template, if vm_id is None: raise Exception("Could not create VM") + vm_pricing = VMPricing.get_vm_pricing_by_name( + name=specs['pricing_name'] + ) if 'pricing_name' in specs else VMPricing.get_default_pricing() # Create a Hosting Order order = HostingOrder.create( price=final_price, vm_id=vm_id, customer=customer, - billing_address=billing_address + billing_address=billing_address, + vm_pricing=vm_pricing ) # Create a Hosting Bill HostingBill.create( - customer=customer, billing_address=billing_address) + customer=customer, billing_address=billing_address + ) # Create Billing Address for User if he does not have one if not customer.user.billing_addresses.count(): @@ -130,12 +138,16 @@ def create_vm_task(self, vm_template_id, user, specs, template, 'cores': specs.get('cpu'), 'memory': specs.get('memory'), 'storage': specs.get('disk_size'), - 'price': specs.get('price'), + 'price': final_price, 'template': template.get('name'), 'vm_name': vm.get('name'), 'vm_id': vm['vm_id'], 'order_id': order.id } + if 'pricing_name' in specs: + context['pricing'] = str(VMPricing.get_vm_pricing_by_name( + name=specs['pricing_name'] + )) email_data = { 'subject': settings.DCL_TEXT + " Order from %s" % context['email'], 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, diff --git a/datacenterlight/templates/datacenterlight/cms/base.html b/datacenterlight/templates/datacenterlight/cms/base.html index 0c356735..942a0ad4 100644 --- a/datacenterlight/templates/datacenterlight/cms/base.html +++ b/datacenterlight/templates/datacenterlight/cms/base.html @@ -8,9 +8,9 @@ - + - {% page_attribute page_title %} + {% page_attribute "page_title" %} @@ -30,7 +30,11 @@ - + {% if request.current_page.cmsfaviconextension %} + + {% else %} + + {% endif %} @@ -52,7 +56,7 @@ {% placeholder 'Datacenterlight Header' or %}
-

{% page_attribute page_title %}

+

{% page_attribute "page_title" %}

{% endplaceholder %} diff --git a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html index f38150bb..e3fe8676 100644 --- a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html +++ b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html @@ -1,4 +1,16 @@ {% load staticfiles i18n%} + +{% if vm_pricing %} + +{% endif %} +
{% csrf_token %}
@@ -7,9 +19,11 @@
15 CHF/{% trans "month" %} + {% if vm_pricing.vat_inclusive %}

{% trans "VAT included" %}

+ {% endif %}
@@ -78,5 +92,6 @@
+ diff --git a/datacenterlight/templates/datacenterlight/landing_payment.html b/datacenterlight/templates/datacenterlight/landing_payment.html index f21dc54b..b808e033 100644 --- a/datacenterlight/templates/datacenterlight/landing_payment.html +++ b/datacenterlight/templates/datacenterlight/landing_payment.html @@ -78,7 +78,7 @@

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


-

{%trans "Total" %}  ({%trans "including VAT" %}) {{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}

+

{%trans "Total" %}  ({% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %}) {{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}

diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 78ed43c0..95bfa3c6 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -65,9 +65,19 @@ {% trans "Disk space" %}: {{vm.disk_size|intcomma}} GB

+ {% if vm.vat > 0 %} +

+ {% trans "Subtotal" %}: + {{vm.price|floatformat:2|intcomma}} CHF +

+

+ {% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%): + {{vm.vat|floatformat:2|intcomma}} CHF +

+ {% endif %}

- {% trans "Total" %} - {{vm.price|intcomma}} CHF + {% trans "Total" %} + {{vm.total_price|floatformat:2|intcomma}} CHF

@@ -78,7 +88,7 @@ {% 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 %}.
+
{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with the fee of {{vm_total_price}} CHF/month{% endblocktrans %}.
diff --git a/hosting/templates/hosting/orders.html b/hosting/templates/hosting/orders.html index f896c98b..140cc4c6 100644 --- a/hosting/templates/hosting/orders.html +++ b/hosting/templates/hosting/orders.html @@ -29,7 +29,7 @@ {{ order.id }} {{ order.created_at | date:"M d, Y H:i" }} - {{ order.price|intcomma }} + {{ order.price|floatformat:2|intcomma }} {% trans 'See Invoice' %} diff --git a/hosting/templates/hosting/virtual_machine_detail.html b/hosting/templates/hosting/virtual_machine_detail.html index b77e1dca..68894851 100644 --- a/hosting/templates/hosting/virtual_machine_detail.html +++ b/hosting/templates/hosting/virtual_machine_detail.html @@ -45,7 +45,7 @@

{% trans "Billing" %}

{% trans "Current Pricing" %}
-
{{virtual_machine.price|floatformat|intcomma}} CHF/{% trans "Month" %}
+
{{order.price|floatformat:2|intcomma}} CHF/{% trans "Month" %}
{% trans "See Invoice" %}
diff --git a/hosting/views.py b/hosting/views.py index 6e143760..ec36836a 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -42,7 +42,7 @@ from utils.forms import ( BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm, ResendActivationEmailForm ) -from utils.hosting_utils import get_vm_price +from utils.hosting_utils import get_vm_price, get_vm_price_with_vat from utils.mailer import BaseEmail from utils.stripe_utils import StripeUtils from utils.tasks import send_plain_email_task @@ -749,11 +749,17 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): context['vm'] = vm_detail.__dict__ context['vm']['name'] = '{}-{}'.format( context['vm']['configuration'], context['vm']['vm_id']) - context['vm']['price'] = get_vm_price( + price, vat, vat_percent = get_vm_price_with_vat( cpu=context['vm']['cores'], - disk_size=context['vm']['disk_size'], - memory=context['vm']['memory'] + ssd_size=context['vm']['disk_size'], + memory=context['vm']['memory'], + pricing_name=(obj.vm_pricing.name + if obj.vm_pricing else 'default') ) + context['vm']['vat'] = vat + context['vm']['price'] = price + context['vm']['vat_percent'] = vat_percent + context['vm']['total_price'] = price + vat context['subscription_end_date'] = vm_detail.end_date() except VMDetail.DoesNotExist: try: @@ -762,6 +768,17 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): ) vm = manager.get_vm(obj.vm_id) context['vm'] = VirtualMachineSerializer(vm).data + price, vat, vat_percent = get_vm_price_with_vat( + cpu=context['vm']['cores'], + ssd_size=context['vm']['disk_size'], + memory=context['vm']['memory'], + pricing_name=(obj.vm_pricing.name + if obj.vm_pricing else 'default') + ) + context['vm']['vat'] = vat + context['vm']['price'] = price + context['vm']['vat_percent'] = vat_percent + context['vm']['total_price'] = price + vat except WrongIdError: messages.error( self.request, @@ -1100,7 +1117,8 @@ class VirtualMachineView(LoginRequiredMixin, View): context = { 'virtual_machine': serializer.data, 'order': HostingOrder.objects.get( - vm_id=serializer.data['vm_id']) + vm_id=serializer.data['vm_id'] + ) } except Exception as ex: logger.debug("Exception generated {}".format(str(ex))) diff --git a/ungleich/templates/cms/ungleichch/_menu.html b/ungleich/templates/cms/ungleichch/_menu.html index e17e90d6..6ccb043b 100644 --- a/ungleich/templates/cms/ungleichch/_menu.html +++ b/ungleich/templates/cms/ungleichch/_menu.html @@ -5,29 +5,34 @@ diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py index 3c193ad7..04ed658a 100644 --- a/utils/hosting_utils.py +++ b/utils/hosting_utils.py @@ -1,6 +1,8 @@ +import decimal import logging from oca.pool import WrongIdError +from datacenterlight.models import VMPricing from hosting.models import UserHostingKey, VMDetail from opennebula_api.serializers import VirtualMachineSerializer @@ -49,14 +51,74 @@ def get_or_create_vm_detail(user, manager, vm_id): return vm_detail_obj -def get_vm_price(cpu, memory, disk_size): +def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'): """ A helper function that computes price of a VM from given cpu, ram and ssd parameters :param cpu: Number of cores of the VM :param memory: RAM of the VM - :param disk_size: Disk space of the VM + :param disk_size: Disk space of the VM (SSD) + :param hdd_size: The HDD size + :param pricing_name: The pricing name to be used :return: The price of the VM """ - return (cpu * 5) + (memory * 2) + (disk_size * 0.6) + try: + pricing = VMPricing.objects.get(name=pricing_name) + except Exception as ex: + logger.error( + "Error getting VMPricing object for {pricing_name}." + "Details: {details}".format( + pricing_name=pricing_name, details=str(ex) + ) + ) + return None + price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) + + (decimal.Decimal(memory) * pricing.ram_unit_price) + + (decimal.Decimal(disk_size) * pricing.ssd_unit_price) + + (decimal.Decimal(hdd_size) * pricing.hdd_unit_price)) + cents = decimal.Decimal('.01') + price = price.quantize(cents, decimal.ROUND_HALF_UP) + return float(price) + + +def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, + pricing_name='default'): + """ + A helper function that computes price of a VM from given cpu, ram and + ssd, hdd and the pricing parameters + + :param cpu: Number of cores of the VM + :param memory: RAM of the VM + :param ssd_size: Disk space of the VM (SSD) + :param hdd_size: The HDD size + :param pricing_name: The pricing name to be used + :return: The a tuple containing the price of the VM, the VAT and the + VAT percentage + """ + try: + pricing = VMPricing.objects.get(name=pricing_name) + except Exception as ex: + logger.error( + "Error getting VMPricing object for {pricing_name}." + "Details: {details}".format( + pricing_name=pricing_name, details=str(ex) + ) + ) + return None + + price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) + + (decimal.Decimal(memory) * pricing.ram_unit_price) + + (decimal.Decimal(ssd_size) * pricing.ssd_unit_price) + + (decimal.Decimal(hdd_size) * pricing.hdd_unit_price)) + if pricing.vat_inclusive: + vat = decimal.Decimal(0) + vat_percent = decimal.Decimal(0) + else: + vat = price * pricing.vat_percentage * decimal.Decimal(0.01) + vat_percent = pricing.vat_percentage + + cents = decimal.Decimal('.01') + price = price.quantize(cents, decimal.ROUND_HALF_UP) + vat = vat.quantize(cents, decimal.ROUND_HALF_UP) + return float(price), float(vat), float(vat_percent)