diff --git a/Changelog b/Changelog index cc53af19..b85dd4ff 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,64 @@ +Next: + * bugfix: Use correct version of django-multisite (MR #676) +2.4.1: 2018-10-18 + * bugfix: Update pycryptodome module from 3.4 to 3.6.6 (PR #674) +2.4: 2018-10-18 + * #5681: [hosting,dcl] Allow admin to lower minimum RAM to 512 MB (PR #672) +2.3.1: 2018-10-17 + * bugfix: [hosting, dcl] Show VAT percent rounded to 2 decimal places in the order confirmation page (PR #673) +2.3: 2018-10-08 + * #5690: Generic payment page - allow admin to add a onetime/monthly product and the frontend for user to pay for this product (PR #666) +2.2.2: 2018-09-28 + * #5721: Set calculator OS list in alphabetical order and set `Devuan Ascii` as the default (PR #668) + * bugfix: Fix some typos and correct DE translations (PR #667) +2.2.1: 2018-09-25 + * feature: Change DCLNavbarPlugin to show login option only if set (PR #665) + * bugfix: Log opennebula errors and send proper message when vm terminate is not completed in the stipulated time (PR #648) +2.2: 2018-09-06 + * bugfix: Include price in the Stripe plan name to make it distinct and to correct pricing since version 1.9 +2.1.2: 2018-08-30 + * bugfix: [blog, comic] Set blog rss feed for all blog templates +2.1.1: 2018-08-24 + * #5487: [hosting] Add explicit warning message for teminating VM (PR #656) + * bugfix: [dg] Send email to admin on dg subscription and increase cc_brand field to 128 characters (PR #652) + * #5458: [admin] Make hostingorder more readable (PR #657) + * bugfix: [CMS templates] Set description meta field of ungleich template (was missing before) and set ungleich glarus ag uniformly as author of various CMS pages (PR #653) + * #5473: Ping a VM before saving ssh key of the user (PR #655) +2.1: 2018-08-21 + * Bugfix: Increase CC brand name fields from 10 to 128 characters (PR #654) +2.0.5: 2018-08-08 + * Fix IPv6 VM name in the billing invoice +2.0.4: 2018-08-07 + * Add RSS feed link to the footer of the blog template (PR #651) + * #5308: [ipv6only] Fix - when creating a VM, the name begins with v6only (PR #649) + * #5293: Use `terminate-hard` action instead of `terminate` in the opennebula call to terminate a vm (PR #650) +2.0.3: 2018-07-18 + * Remove unused /comic url (PR #644) + * #5126: Allow dynamicweb sites to be iframed on other by setting `X_FRAME_OPTIONS_ALLOW_FROM_URI` (PR #645) +2.0.2: 2018-07-14 + * bugfix: [blog] Add missing content block in the blog_ungleich.html template file +2.0.1: 2018-07-14 + * bugfix: [blog] Enable content/structure mode in blog page +2.0: 2018-07-07 + * #3747: [dcl,hosting] Add multiple cards support (PR #530) + * #3934: [dcl,hosting] Create HostingOrder outside celery task and add and associate OrderDetail with HostingOrder (PR #624) + * #4890: [hosting] Manage SSH keys using IPv6 of the VM (PR #640) + * bugfix: Fix flake8 error that was ignored in release 1.9.1 +1.9.1: 2018-06-24 + * #4799: [dcl] Show selected vm templates only in calculator (PR #638) + * #4847: [comic] Add google analytics code for comic.ungleich.ch (PR #639) + * feature: add vm_type option to vm_template and dcl calculator to distinguish between public and ipv6only templates (PR #635) +1.9: 2018-05-16 + * #4559: [cms] enable discount on cms calculator +1.8: 2018-05-01 + * #4527: [hosting] cms calculator on non-cms pages for the hosting app + * bgfix: [dcl] navbar dropdown target fix + * bgfix: [hosting] login/signup pages footer link fix +1.7.2: 2018-04-30 + * bgfix: [cms] add favicon extension to ungleich cms pages + * #4474: [cms] reduce heading slider side padding 1.7.1: 2018-04-21 - * #4481: [digitalglarus] Make /blog available on all domains + * #4481: [blog] fix de blog pages 500 error * #4370: [comic] new url /comic to show only comic blogs 1.7: 2018-04-20 * bgfix: [all] Make /blog available on all domains diff --git a/datacenterlight/admin.py b/datacenterlight/admin.py index d95e4f87..5a1fc8a2 100644 --- a/datacenterlight/admin.py +++ b/datacenterlight/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from cms.admin.placeholderadmin import PlaceholderAdminMixin from cms.extensions import PageExtensionAdmin from .cms_models import CMSIntegration, CMSFaviconExtension -from .models import VMPricing +from .models import VMPricing, VMTemplate class CMSIntegrationAdmin(PlaceholderAdminMixin, admin.ModelAdmin): @@ -16,3 +16,4 @@ class CMSFaviconExtensionAdmin(PageExtensionAdmin): admin.site.register(CMSIntegration, CMSIntegrationAdmin) admin.site.register(CMSFaviconExtension, CMSFaviconExtensionAdmin) admin.site.register(VMPricing) +admin.site.register(VMTemplate) diff --git a/datacenterlight/cms_models.py b/datacenterlight/cms_models.py index dd6a165f..2d1a98b5 100644 --- a/datacenterlight/cms_models.py +++ b/datacenterlight/cms_models.py @@ -2,6 +2,9 @@ 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 import forms +from django.conf import settings +from django.contrib.postgres.fields import ArrayField from django.contrib.sites.models import Site from django.db import models from django.utils.safestring import mark_safe @@ -9,7 +12,7 @@ from djangocms_text_ckeditor.fields import HTMLField from filer.fields.file import FilerFileField from filer.fields.image import FilerImageField -from datacenterlight.models import VMPricing +from datacenterlight.models import VMPricing, VMTemplate class CMSIntegration(models.Model): @@ -26,6 +29,10 @@ class CMSIntegration(models.Model): navbar_placeholder = PlaceholderField( 'datacenterlight_navbar', related_name='dcl-navbar-placeholder+' ) + calculator_placeholder = PlaceholderField( + 'datacenterlight_calculator', + related_name='dcl-calculator-placeholder+' + ) domain = models.ForeignKey(Site, null=True, blank=True) class Meta: @@ -173,6 +180,10 @@ class DCLNavbarPluginModel(CMSPlugin): default=True, help_text='Select to include the language selection dropdown.' ) + show_login_option = models.BooleanField( + default=True, + help_text='Uncheck this if you do not want to show login/dashboard.' + ) def get_logo_dark(self): # used only if atleast one logo exists @@ -288,10 +299,66 @@ class DCLSectionPromoPluginModel(CMSPlugin): return extra_classes -class DCLCustomPricingModel(CMSPlugin): +class MultipleChoiceArrayField(ArrayField): + """ + A field that allows us to store an array of choices. + Uses Django's Postgres ArrayField + and a MultipleChoiceField for its formfield. + """ + VMTemplateChoices = [] + if settings.OPENNEBULA_DOMAIN != 'test_domain': + VMTemplateChoices = list( + ( + str(obj.opennebula_vm_template_id), + (obj.name + ' - ' + VMTemplate.IPV6.title() + if obj.vm_type == VMTemplate.IPV6 else obj.name + ) + ) + for obj in VMTemplate.objects.all() + ) + + def formfield(self, **kwargs): + defaults = { + 'form_class': forms.MultipleChoiceField, + 'choices': self.VMTemplateChoices, + } + defaults.update(kwargs) + # Skip our parent's formfield implementation completely as we don't + # care for it. + # pylint:disable=bad-super-call + return super(ArrayField, self).formfield(**defaults) + + +class DCLCalculatorPluginModel(CMSPlugin): pricing = models.ForeignKey( VMPricing, related_name="dcl_custom_pricing_vm_pricing", help_text='Choose a pricing that will be associated with this ' 'Calculator' ) + vm_type = models.CharField( + max_length=50, choices=VMTemplate.VM_TYPE_CHOICES, + default=VMTemplate.PUBLIC + ) + vm_templates_to_show = MultipleChoiceArrayField( + base_field=models.CharField( + blank=True, + max_length=256, + ), + default=list, + blank=True, + help_text="Recommended: If you wish to show all templates of the " + "corresponding VM Type (public/ipv6only), please do not " + "select any of the items in the above field. " + "This will allow any new template(s) added " + "in the backend to be automatically listed in this " + "calculator instance." + ) + default_selected_template = models.CharField( + default="Devuan Ascii", + null=True, + max_length=128, + help_text="Write the name of the template that you need selected as" + " default when the calculator loads" + ) + enable_512mb_ram = models.BooleanField(default=False) diff --git a/datacenterlight/cms_plugins.py b/datacenterlight/cms_plugins.py index 19dc0b39..c3ec974f 100644 --- a/datacenterlight/cms_plugins.py +++ b/datacenterlight/cms_plugins.py @@ -6,9 +6,10 @@ from .cms_models import ( DCLFooterPluginModel, DCLLinkPluginModel, DCLNavbarDropdownPluginModel, DCLSectionIconPluginModel, DCLSectionImagePluginModel, DCLSectionPluginModel, DCLNavbarPluginModel, - DCLSectionPromoPluginModel, DCLCustomPricingModel + DCLSectionPromoPluginModel, DCLCalculatorPluginModel ) -from .models import VMTemplate, VMPricing +from .models import VMTemplate +from datacenterlight.utils import clear_all_session_vars @plugin_pool.register_plugin @@ -21,7 +22,7 @@ class DCLSectionPlugin(CMSPluginBase): allow_children = True child_classes = [ 'DCLSectionIconPlugin', 'DCLSectionImagePlugin', - 'DCLSectionPromoPlugin', 'UngleichHTMLPlugin' + 'DCLSectionPromoPlugin', 'UngleichHTMLPlugin', 'DCLCalculatorPlugin' ] def render(self, context, instance, placeholder): @@ -30,14 +31,17 @@ class DCLSectionPlugin(CMSPluginBase): ) context['children_to_side'] = [] context['children_to_content'] = [] + context['children_calculator'] = [] if instance.child_plugin_instances is not None: right_children = [ 'DCLSectionImagePluginModel', - 'DCLSectionIconPluginModel' + 'DCLSectionIconPluginModel', ] for child in instance.child_plugin_instances: if child.__class__.__name__ in right_children: context['children_to_side'].append(child) + elif child.plugin_type == 'DCLCalculatorPlugin': + context['children_calculator'].append(child) else: context['children_to_content'].append(child) return context @@ -75,52 +79,31 @@ class DCLSectionPromoPlugin(CMSPluginBase): @plugin_pool.register_plugin class DCLCalculatorPlugin(CMSPluginBase): module = "Datacenterlight" - name = "DCL Calculator Section Plugin" - model = DCLSectionPluginModel + name = "DCL Calculator Plugin" + model = DCLCalculatorPluginModel render_template = "datacenterlight/cms/calculator.html" cache = False - allow_children = True - child_classes = [ - 'DCLSectionPromoPlugin', 'UngleichHTMLPlugin', 'DCLCustomPricingPlugin' - ] + require_parent = True def render(self, context, instance, placeholder): + clear_all_session_vars(context['request']) context = super(DCLCalculatorPlugin, self).render( context, instance, placeholder ) - context['templates'] = VMTemplate.objects.all() - 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 - ) + ids = instance.vm_templates_to_show + if ids: + context['templates'] = VMTemplate.objects.filter( + vm_type=instance.vm_type + ).filter(opennebula_vm_template_id__in=ids).order_by('name') else: - context['vm_pricing'] = VMPricing.get_default_pricing() - + context['templates'] = VMTemplate.objects.filter( + vm_type=instance.vm_type + ).order_by('name') + context['instance'] = instance + context['min_ram'] = 0.5 if instance.enable_512mb_ram else 1 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/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 50dbfbe8..d43e91ea 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-04-17 19:26+0000\n" +"POT-Creation-Date: 2018-09-26 20:44+0000\n" "PO-Revision-Date: 2018-03-30 23:22+0000\n" "Last-Translator: b'Anonymous User '\n" "Language-Team: LANGUAGE \n" @@ -19,6 +19,9 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Translated-Using: django-rosetta 0.8.1\n" +msgid "CMS Favicon" +msgstr "" + #, python-format msgid "Your New VM %(vm_name)s at Data Center Light" msgstr "Deine neue VM %(vm_name)s bei Data Center Light" @@ -140,6 +143,9 @@ msgstr "Monat" msgid "VAT included" msgstr "MwSt. inklusive" +msgid "You save" +msgstr "Du sparst" + msgid "Hosted in Switzerland" msgstr "Standort: Schweiz" @@ -287,6 +293,9 @@ msgstr "Registrieren" msgid "Billing Address" msgstr "Rechnungsadresse" +msgid "Make a payment" +msgstr "" + msgid "Your Order" msgstr "Deine Bestellung" @@ -314,9 +323,26 @@ msgstr "exkl. Mehrwertsteuer" msgid "Month" msgstr "Monat" +msgid "Discount" +msgstr "Rabatt" + +msgid "Will be applied at checkout" +msgstr "wird an der Kasse angewendet" + msgid "Credit Card" msgstr "Kreditkarte" +msgid "" +"Please select one of the cards that you used before or fill in your credit " +"card information below. We are using Stripe for payment and do not store your information in our " +"database." +msgstr "" +"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine " +"Kreditkartendetails unten an. Die Bezahlung wird über Stripe abgewickelt. Wir speichern Deine " +"Kreditkartendetails nicht in unserer Datenbank." + msgid "" "Please fill in your credit card information below. We are using Stripe for payment and do not " @@ -326,31 +352,23 @@ msgstr "" "\"https://stripe.com\" target=\"_blank\">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." +msgid "Last" +msgstr "Letzten" -msgid "Card Number" -msgstr "Kreditkartennummer" +msgid "Type" +msgstr "Typ" -msgid "Expiry Date" -msgstr "Ablaufdatum" +msgid "SELECT" +msgstr "AUSWÄHLEN" -msgid "CVC" -msgstr "" +msgid "Add a new credit card" +msgstr "Eine neue Kreditkarte hinzufügen" -msgid "Card Type" -msgstr "Kartentyp" +msgid "NEW CARD" +msgstr "NEUE KARTE" -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." +msgid "New Credit Card" +msgstr "Neue Kreditkarte" msgid "Processing" msgstr "Weiter" @@ -380,6 +398,15 @@ msgstr "Bestellungsübersicht" msgid "Product" msgstr "Produkt" +msgid "Amount" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Recurring" +msgstr "" + msgid "Subtotal" msgstr "Zwischensumme" @@ -388,7 +415,22 @@ msgstr "Mehrwertsteuer" msgid "" "By clicking \"Place order\" this plan will charge your credit card account " -"with the fee of %(vm_total_price)s CHF/month" +"with %(total_price)s CHF/month" +msgstr "" +"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " +"%(vm_total_price)s CHF pro Monat belastet" + +msgid "" +"By clicking \"Place order\" this payment will charge your credit card " +"account with a one time amount of %(total_price)s CHF" +msgstr "" +"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " +"%(vm_total_price)s CHF pro Monat belastet" + +#, python-format +msgid "" +"By clicking \"Place order\" this plan will charge your credit card account " +"with %(vm_total_price)s CHF/month" msgstr "" "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " "%(vm_total_price)s CHF pro Monat belastet" @@ -503,6 +545,13 @@ msgstr "Ungültige Speicher-Grösse" msgid "Incorrect pricing name. Please contact support{support_email}" msgstr "" +#, python-brace-format +msgid "{user} does not have permission to access the card" +msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen" + +msgid "An error occurred. Details: {}" +msgstr "Ein Fehler ist aufgetreten. Details: {}" + msgid "Confirm Order" msgstr "Bestellung Bestätigen" @@ -516,6 +565,36 @@ msgstr "" "Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom " "Popup zur Bezahlseite weitergeleitet." +#, python-brace-format +msgid "An error occurred while associating the card. Details: {details}" +msgstr "" +"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}" + +msgid "Confirmation of your payment" +msgstr "" + +msgid " This is a monthly recurring plan." +msgstr "" + +#, python-brace-format +msgid "" +"Hi {name},\n" +"\n" +"thank you for your order!\n" +"We have just received a payment of CHF {amount:.2f} from you.{recurring}\n" +"\n" +"Cheers,\n" +"Your Data Center Light team" +msgstr "" + +msgid "Thank you for the payment." +msgstr "Danke für Deine Bestellung." + +msgid "" +"You will soon receive a confirmation email of the payment. You can always " +"contact us at info@ungleich.ch for any question that you may have." +msgstr "" + msgid "Thank you for the order." msgstr "Danke für Deine Bestellung." @@ -526,6 +605,28 @@ msgstr "" "Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du " "auf sie zugreifen kannst." +#~ 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." + +#~ msgid "Card Number" +#~ msgstr "Kreditkartennummer" + +#~ msgid "Expiry Date" +#~ msgstr "Ablaufdatum" + +#~ 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." + #~ msgid "Pricing" #~ msgstr "Preise" diff --git a/datacenterlight/management/commands/fetchvmtemplates.py b/datacenterlight/management/commands/fetchvmtemplates.py index 6a45ebad..89271dc4 100644 --- a/datacenterlight/management/commands/fetchvmtemplates.py +++ b/datacenterlight/management/commands/fetchvmtemplates.py @@ -10,16 +10,28 @@ class Command(BaseCommand): help = '''Fetches the VM templates from OpenNebula and populates the dcl VMTemplate model''' + def get_templates(self, manager, prefix): + templates = manager.get_templates('%s-' % prefix) + dcl_vm_templates = [] + for template in templates: + template_name = template.name.lstrip('%s-' % prefix) + template_id = template.id + dcl_vm_template = VMTemplate.create( + template_name, template_id, prefix + ) + dcl_vm_templates.append(dcl_vm_template) + return dcl_vm_templates + def handle(self, *args, **options): try: manager = OpenNebulaManager() - templates = manager.get_templates() dcl_vm_templates = [] - for template in templates: - template_name = template.name.lstrip('public-') - template_id = template.id - dcl_vm_template = VMTemplate.create(template_name, template_id) - dcl_vm_templates.append(dcl_vm_template) + dcl_vm_templates.extend( + self.get_templates(manager, VMTemplate.PUBLIC) + ) + dcl_vm_templates.extend( + self.get_templates(manager, VMTemplate.IPV6) + ) old_vm_templates = VMTemplate.objects.all() old_vm_templates.delete() diff --git a/datacenterlight/migrations/0021_cmsintegration_calculator_placeholder.py b/datacenterlight/migrations/0021_cmsintegration_calculator_placeholder.py new file mode 100644 index 00000000..3ebbb469 --- /dev/null +++ b/datacenterlight/migrations/0021_cmsintegration_calculator_placeholder.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-04-25 09:20 +from __future__ import unicode_literals + +import cms.models.fields +from django.db import migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0020_merge'), + ('cms', '0014_auto_20160404_1908'), + ] + + operations = [ + migrations.AddField( + model_name='cmsintegration', + name='calculator_placeholder', + field=cms.models.fields.PlaceholderField(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='dcl-calculator-placeholder+', slotname='datacenterlight_calculator', to='cms.Placeholder'), + ), + migrations.RenameModel( + old_name='DCLCustomPricingModel', + new_name='DCLCalculatorPluginModel', + ), + ] diff --git a/datacenterlight/migrations/0022_auto_20180506_1950.py b/datacenterlight/migrations/0022_auto_20180506_1950.py new file mode 100644 index 00000000..a5554a58 --- /dev/null +++ b/datacenterlight/migrations/0022_auto_20180506_1950.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-05-07 02:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0021_cmsintegration_calculator_placeholder'), + ] + + operations = [ + migrations.AddField( + model_name='vmpricing', + name='discount_amount', + field=models.DecimalField( + decimal_places=2, default=0, max_digits=6), + ), + migrations.AddField( + model_name='vmpricing', + name='discount_name', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/datacenterlight/migrations/0023_auto_20180524_0349.py b/datacenterlight/migrations/0023_auto_20180524_0349.py new file mode 100644 index 00000000..f37d6634 --- /dev/null +++ b/datacenterlight/migrations/0023_auto_20180524_0349.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-05-23 22:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0022_auto_20180506_1950'), + ] + + operations = [ + migrations.AddField( + model_name='dclcalculatorpluginmodel', + name='vm_type', + field=models.CharField(choices=[('public', 'Public'), ('ipv6only', 'Ipv6Only')], default='public', max_length=50), + ), + migrations.AddField( + model_name='vmtemplate', + name='vm_type', + field=models.CharField(choices=[('public', 'Public'), ('ipv6only', 'Ipv6Only')], default='public', max_length=50), + ), + ] diff --git a/datacenterlight/migrations/0024_dclcalculatorpluginmodel_vm_templates_to_show.py b/datacenterlight/migrations/0024_dclcalculatorpluginmodel_vm_templates_to_show.py new file mode 100644 index 00000000..65bfce21 --- /dev/null +++ b/datacenterlight/migrations/0024_dclcalculatorpluginmodel_vm_templates_to_show.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-06-24 08:23 +from __future__ import unicode_literals + +import datacenterlight.cms_models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0023_auto_20180524_0349'), + ] + + operations = [ + migrations.AddField( + model_name='dclcalculatorpluginmodel', + name='vm_templates_to_show', + field=datacenterlight.cms_models.MultipleChoiceArrayField(base_field=models.CharField(blank=True, max_length=256), blank=True, default=list, help_text='Recommended: If you wish to show all templates of the corresponding VM Type (public/ipv6only), please do not select any of the items in the above field. This will allow any new template(s) added in the backend to be automatically listed in this calculator instance.', size=None), + ), + ] diff --git a/datacenterlight/migrations/0025_dclnavbarpluginmodel_show_login_option.py b/datacenterlight/migrations/0025_dclnavbarpluginmodel_show_login_option.py new file mode 100644 index 00000000..e9ec57ba --- /dev/null +++ b/datacenterlight/migrations/0025_dclnavbarpluginmodel_show_login_option.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-09-25 20:27 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0024_dclcalculatorpluginmodel_vm_templates_to_show'), + ] + + operations = [ + migrations.AddField( + model_name='dclnavbarpluginmodel', + name='show_login_option', + field=models.BooleanField(default=True, help_text='Uncheck this if you do not want to show login/dashboard.'), + ), + ] diff --git a/datacenterlight/migrations/0026_dclcalculatorpluginmodel_default_selected_template.py b/datacenterlight/migrations/0026_dclcalculatorpluginmodel_default_selected_template.py new file mode 100644 index 00000000..047d4096 --- /dev/null +++ b/datacenterlight/migrations/0026_dclcalculatorpluginmodel_default_selected_template.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-09-27 20:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0025_dclnavbarpluginmodel_show_login_option'), + ] + + operations = [ + migrations.AddField( + model_name='dclcalculatorpluginmodel', + name='default_selected_template', + field=models.CharField(default='Devuan Ascii', help_text='Write the name of the template that you need selected as default when the calculator loads', max_length=128, null=True), + ), + ] diff --git a/datacenterlight/migrations/0027_dclcalculatorpluginmodel_enable_512mb_ram.py b/datacenterlight/migrations/0027_dclcalculatorpluginmodel_enable_512mb_ram.py new file mode 100644 index 00000000..bd639c9d --- /dev/null +++ b/datacenterlight/migrations/0027_dclcalculatorpluginmodel_enable_512mb_ram.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-09-29 05:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datacenterlight', '0026_dclcalculatorpluginmodel_default_selected_template'), + ] + + operations = [ + migrations.AddField( + model_name='dclcalculatorpluginmodel', + name='enable_512mb_ram', + field=models.BooleanField(default=False), + ), + ] diff --git a/datacenterlight/models.py b/datacenterlight/models.py index eceb7617..729bbdf9 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -6,13 +6,29 @@ logger = logging.getLogger(__name__) class VMTemplate(models.Model): + PUBLIC = 'public' + IPV6 = 'ipv6only' + VM_TYPE_CHOICES = ( + (PUBLIC, PUBLIC.title()), + (IPV6, IPV6.title()), + ) name = models.CharField(max_length=50) opennebula_vm_template_id = models.IntegerField() + vm_type = models.CharField( + max_length=50, choices=VM_TYPE_CHOICES, default=PUBLIC + ) + + def __str__(self): + return '%s - %s - %s' % ( + self.opennebula_vm_template_id, self.vm_type, self.name + ) @classmethod - def create(cls, name, opennebula_vm_template_id): + def create(cls, name, opennebula_vm_template_id, vm_type): vm_template = cls( - name=name, opennebula_vm_template_id=opennebula_vm_template_id) + name=name, opennebula_vm_template_id=opennebula_vm_template_id, + vm_type=vm_type + ) return vm_template @@ -34,16 +50,29 @@ class VMPricing(models.Model): hdd_unit_price = models.DecimalField( max_digits=7, decimal_places=6, default=0 ) + discount_name = models.CharField(max_length=255, null=True, blank=True) + discount_amount = models.DecimalField( + max_digits=6, decimal_places=2, default=0 + ) def __str__(self): - return self.name + ' => ' + ' - '.join([ + display_str = 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', ] - ) + if not self.vat_inclusive else 'VAT-Incl', + ]) + if self.discount_amount: + display_str = ' - '.join([ + display_str, + '{} {}'.format( + self.discount_amount, + self.discount_name if self.discount_name else 'Discount' + ) + ]) + return display_str @classmethod def get_vm_pricing_by_name(cls, name): diff --git a/datacenterlight/static/datacenterlight/css/common.css b/datacenterlight/static/datacenterlight/css/common.css index 895256ef..00ee52cc 100644 --- a/datacenterlight/static/datacenterlight/css/common.css +++ b/datacenterlight/static/datacenterlight/css/common.css @@ -150,3 +150,39 @@ footer .dcl-link-separator::before { border-radius: 100%; background: #777; } + +.mb-0 { + margin-bottom: 0; +} + +.thin-hr { + margin-top: 10px; + margin-bottom: 10px; +} + +.payment-container .credit-card-info { + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} +.credit-card-info { + display: flex; +} + +.credit-card-info .align-bottom { + align-self: flex-end; + padding-right: 0 !important; +} + +.new-card-head { + margin-top: 10px; +} +.new-card-button-margin button{ + margin-top: 5px; + margin-bottom: 5px; +} + +.input-no-border { + border: none !important; + background: transparent !important; + resize: none; +} diff --git a/datacenterlight/static/datacenterlight/css/header-slider.css b/datacenterlight/static/datacenterlight/css/header-slider.css index d01f02a7..ea01edf7 100644 --- a/datacenterlight/static/datacenterlight/css/header-slider.css +++ b/datacenterlight/static/datacenterlight/css/header-slider.css @@ -55,7 +55,7 @@ flex: 1; } -.header_slider > .carousel .item .container { +.header_slider > .carousel .item .container-fluid { overflow: auto; padding: 50px 20px 60px; height: 100%; @@ -104,9 +104,9 @@ .header_slider .carousel-control .fa { font-size: 4em; } - .header_slider > .carousel .item .container { + .header_slider > .carousel .item .container-fluid { overflow: auto; - padding: 75px 50px; + padding: 75px; } .header_slider .btn-trans { padding: 8px 15px; @@ -120,11 +120,6 @@ .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/css/hosting.css b/datacenterlight/static/datacenterlight/css/hosting.css index b4c5909c..0f16ab77 100644 --- a/datacenterlight/static/datacenterlight/css/hosting.css +++ b/datacenterlight/static/datacenterlight/css/hosting.css @@ -482,6 +482,7 @@ margin: 100px auto 40px; border: 1px solid #ccc; padding: 30px 30px 20px; + color: #595959; } .order-detail-container .dashboard-title-thin { @@ -503,10 +504,6 @@ margin-bottom: 15px; } -.order-detail-container .order-details strong { - color: #595959; -} - .order-detail-container h4 { font-size: 16px; font-weight: bold; @@ -515,13 +512,28 @@ .order-detail-container p { margin-bottom: 5px; - color: #595959; } .order-detail-container hr { margin: 15px 0; } +.order-detail-container .thin-hr { + margin: 10px 0; +} + +.order-detail-container .subtotal-price { + font-size: 16px; +} + +.order-detail-container .subtotal-price .text-primary { + font-size: 17px; +} + +.order-detail-container .total-price { + font-size: 18px; +} + @media (max-width: 767px) { .order-detail-container { padding: 15px; diff --git a/datacenterlight/static/datacenterlight/css/landing-page.css b/datacenterlight/static/datacenterlight/css/landing-page.css index 8e9f2c2d..f241ed71 100755 --- a/datacenterlight/static/datacenterlight/css/landing-page.css +++ b/datacenterlight/static/datacenterlight/css/landing-page.css @@ -776,7 +776,7 @@ textarea { width: 100%; margin: 0 auto; background: #fff; - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 0 6px rgba(0, 0, 0, 0.15); padding-bottom: 40px; border-radius: 7px; text-align: center; @@ -929,7 +929,7 @@ textarea { } -@media(max-width:991px) { +@media(max-width:767px) { .section-sm-center .split-text, .section-sm-center .space { text-align: center !important; diff --git a/datacenterlight/static/datacenterlight/js/main.js b/datacenterlight/static/datacenterlight/js/main.js index 35f2b247..65db1d6b 100644 --- a/datacenterlight/static/datacenterlight/js/main.js +++ b/datacenterlight/static/datacenterlight/js/main.js @@ -5,6 +5,10 @@ /* --------------------------------------------- Scripts initialization --------------------------------------------- */ + var minRam = 1; + if(window.minRam){ + minRam = window.minRam; + } var cardPricing = { 'cpu': { 'id': 'coreValue', @@ -16,7 +20,7 @@ 'ram': { 'id': 'ramValue', 'value': 2, - 'min': 1, + 'min': minRam, 'max': 200, 'interval': 1 }, @@ -40,6 +44,7 @@ _initNavUrl(); _initPricing(); ajaxForms(); + $('#ramValue').data('old-value', $('#ramValue').val()); }); $(window).resize(function() { @@ -144,21 +149,54 @@ var data = $(this).data('minus'); if (cardPricing[data].value > cardPricing[data].min) { - cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval; + if(data === 'ram' && String(cardPricing[data].value) === "1" && minRam === 0.5){ + cardPricing[data].value = 0.5; + $('#ramValue').val('0.5'); + $("#ramValue").attr('step', 0.5); + } else { + cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval; + } } _fetchPricing(); + $('#ramValue').data('old-value', $('#ramValue').val()); }); $('.fa-plus-circle.right').click(function(event) { var data = $(this).data('plus'); if (cardPricing[data].value < cardPricing[data].max) { - cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval; + if(data === 'ram' && String(cardPricing[data].value) === "0.5" && minRam === 0.5){ + cardPricing[data].value = 1; + $('#ramValue').val('1'); + $("#ramValue").attr('step', 1); + } else { + cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval; + } } _fetchPricing(); + $('#ramValue').data('old-value', $('#ramValue').val()); }); $('.input-price').change(function() { var data = $(this).attr("name"); - cardPricing[data].value = $('input[name=' + data + ']').val(); + var input = $('input[name=' + data + ']'); + var inputValue = input.val(); + + if(data === 'ram') { + var ramInput = $('#ramValue'); + if ($('#ramValue').data('old-value') < $('#ramValue').val()) { + if($('#ramValue').val() === '1' && minRam === 0.5) { + $("#ramValue").attr('step', 1); + $('#ramValue').val('1'); + } + } else { + if($('#ramValue').val() === '0' && minRam === 0.5) { + $("#ramValue").attr('step', 0.5); + $('#ramValue').val('0.5'); + } + } + inputValue = $('#ramValue').val(); + $('#ramValue').data('old-value', $('#ramValue').val()); + } + cardPricing[data].value = inputValue; _fetchPricing(); }); } @@ -175,14 +213,18 @@ window.coresUnitPrice = 5; } if(typeof window.ramUnitPrice === 'undefined'){ - window.coresUnitPrice = 2; + window.ramUnitPrice = 2; } if(typeof window.ssdUnitPrice === 'undefined'){ window.ssdUnitPrice = 0.6; } + if(typeof window.discountAmount === 'undefined'){ + window.discountAmount = 0; + } var total = (cardPricing['cpu'].value * window.coresUnitPrice) + (cardPricing['ram'].value * window.ramUnitPrice) + - (cardPricing['storage'].value * window.ssdUnitPrice); + (cardPricing['storage'].value * window.ssdUnitPrice) - + window.discountAmount; total = parseFloat(total.toFixed(2)); $("#total").text(total); } diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index db479b43..2779f79b 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -1,24 +1,25 @@ from datetime import datetime +from celery import current_task from celery.exceptions import MaxRetriesExceededError from celery.utils.log import get_task_logger -from celery import current_task from django.conf import settings from django.core.mail import EmailMessage from django.core.urlresolvers import reverse from django.utils import translation from django.utils.translation import ugettext_lazy as _ +from time import sleep from dynamicweb.celery import app -from hosting.models import HostingOrder, HostingBill -from membership.models import StripeCustomer, CustomUser +from hosting.models import HostingOrder +from membership.models import CustomUser from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import VirtualMachineSerializer -from utils.hosting_utils import get_all_public_keys, get_or_create_vm_detail -from utils.forms import UserBillingAddressForm +from utils.hosting_utils import ( + get_all_public_keys, get_or_create_vm_detail, ping_ok +) from utils.mailer import BaseEmail -from utils.models import BillingAddress - +from utils.stripe_utils import StripeUtils from .models import VMPricing logger = get_task_logger(__name__) @@ -51,24 +52,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, - stripe_subscription_id, cc_details): +def create_vm_task(self, vm_template_id, user, specs, template, order_id): logger.debug( "Running create_vm_task on {}".format(current_task.request.hostname)) vm_id = None try: - 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'], - city=billing_address_data['city'], - postal_code=billing_address_data['postal_code'], - country=billing_address_data['country'] + final_price = ( + specs.get('total_price') if 'total_price' in specs + else specs.get('price') ) - billing_address.save() - customer = StripeCustomer.objects.filter(id=stripe_customer_id).first() if 'pass' in user: on_user = user.get('email') @@ -97,38 +89,43 @@ 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, - vm_pricing=vm_pricing + # Update HostingOrder with the created vm_id + hosting_order = HostingOrder.objects.filter(id=order_id).first() + error_msg = None + + try: + hosting_order.vm_id = vm_id + hosting_order.save() + logger.debug( + "Updated hosting_order {} with vm_id={}".format( + hosting_order.id, vm_id + ) + ) + except Exception as ex: + error_msg = ( + "HostingOrder with id {order_id} not found. This means that " + "the hosting order was not created and/or it is/was not " + "associated with VM with id {vm_id}. Details {details}".format( + order_id=order_id, vm_id=vm_id, details=str(ex) + ) + ) + logger.error(error_msg) + + stripe_utils = StripeUtils() + result = stripe_utils.set_subscription_metadata( + subscription_id=hosting_order.subscription_id, + metadata={"VM_ID": str(vm_id)} ) - # Create a Hosting Bill - HostingBill.create( - customer=customer, billing_address=billing_address - ) - - # Create Billing Address for User if he does not have one - if not customer.user.billing_addresses.count(): - billing_address_data.update({ - 'user': customer.user.id - }) - billing_address_user_form = UserBillingAddressForm( - billing_address_data) - billing_address_user_form.is_valid() - billing_address_user_form.save() - - # Associate an order with a stripe subscription - order.set_subscription_id(stripe_subscription_id, cc_details) - - # If the Stripe payment succeeds, set order status approved - order.set_approved() + if result.get('error') is not None: + emsg = "Could not update subscription metadata for {sub}".format( + sub=hosting_order.subscription_id + ) + logger.error(emsg) + if error_msg: + error_msg += ". " + emsg + else: + error_msg = emsg vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data @@ -142,8 +139,11 @@ def create_vm_task(self, vm_template_id, user, specs, template, 'template': template.get('name'), 'vm_name': vm.get('name'), 'vm_id': vm['vm_id'], - 'order_id': order.id + 'order_id': order_id } + + if error_msg: + context['errors'] = error_msg if 'pricing_name' in specs: context['pricing'] = str(VMPricing.get_vm_pricing_by_name( name=specs['pricing_name'] @@ -171,7 +171,7 @@ def create_vm_task(self, vm_template_id, user, specs, template, 'base_url': "{0}://{1}".format(user.get('request_scheme'), user.get('request_host')), 'order_url': reverse('hosting:orders', - kwargs={'pk': order.id}), + kwargs={'pk': order_id}), 'page_header': _( 'Your New VM %(vm_name)s at Data Center Light') % { 'vm_name': vm.get('name')}, @@ -188,11 +188,11 @@ def create_vm_task(self, vm_template_id, user, specs, template, email = BaseEmail(**email_data) email.send() - # try to see if we have the IP and that if the ssh keys can - # be configured - new_host = manager.get_primary_ipv4(vm_id) + # try to see if we have the IPv6 of the new vm and that if the ssh + # keys can be configured + vm_ipv6 = manager.get_ipv6(vm_id) logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id)) - if new_host is not None: + if vm_ipv6 is not None: custom_user = CustomUser.objects.get(email=user.get('email')) get_or_create_vm_detail(custom_user, manager, vm_id) if custom_user is not None: @@ -203,13 +203,48 @@ def create_vm_task(self, vm_template_id, user, specs, template, logger.debug( "Calling configure on {host} for " "{num_keys} keys".format( - host=new_host, num_keys=len(keys))) - # Let's delay the task by 75 seconds to be sure - # that we run the cdist configure after the host - # is up - manager.manage_public_key(keys, - hosts=[new_host], - countdown=75) + host=vm_ipv6, num_keys=len(keys) + ) + ) + # Let's wait until the IP responds to ping before we + # run the cdist configure on the host + did_manage_public_key = False + for i in range(0, 15): + if ping_ok(vm_ipv6): + logger.debug( + "{} is pingable. Doing a " + "manage_public_key".format(vm_ipv6) + ) + sleep(10) + manager.manage_public_key( + keys, hosts=[vm_ipv6] + ) + did_manage_public_key = True + break + else: + logger.debug( + "Can't ping {}. Wait 5 secs".format( + vm_ipv6 + ) + ) + sleep(5) + if not did_manage_public_key: + emsg = ("Waited for over 75 seconds for {} to be " + "pingable. But the VM was not reachable. " + "So, gave up manage_public_key. Please do " + "this manually".format(vm_ipv6)) + logger.error(emsg) + email_data = { + 'subject': '{} CELERY TASK INCOMPLETE: {} not ' + 'pingable for 75 seconds'.format( + settings.DCL_TEXT, vm_ipv6 + ), + 'from_email': current_task.request.hostname, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, + 'body': emsg + } + email = EmailMessage(**email_data) + email.send() except Exception as e: logger.error(str(e)) try: diff --git a/datacenterlight/templates/datacenterlight/cms/base.html b/datacenterlight/templates/datacenterlight/cms/base.html index 942a0ad4..5202fb90 100644 --- a/datacenterlight/templates/datacenterlight/cms/base.html +++ b/datacenterlight/templates/datacenterlight/cms/base.html @@ -8,8 +8,8 @@ - + {% page_attribute "page_title" %} @@ -61,6 +61,7 @@ {% endplaceholder %} + {% url 'datacenterlight:index' as calculator_form_url %} {% placeholder 'Datacenterlight Content' %} {% placeholder 'datacenterlight_footer'%} diff --git a/datacenterlight/templates/datacenterlight/cms/calculator.html b/datacenterlight/templates/datacenterlight/cms/calculator.html index 27d1f89c..7b123a72 100644 --- a/datacenterlight/templates/datacenterlight/cms/calculator.html +++ b/datacenterlight/templates/datacenterlight/cms/calculator.html @@ -1,16 +1,5 @@ -
-
-
-
- {% include "datacenterlight/cms/includes/_section_split_content.html" %} -
-
-
-
- {% include "datacenterlight/includes/_calculator_form.html" %} -
-
-
-
+
+
+ {% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing %}
\ No newline at end of file diff --git a/datacenterlight/templates/datacenterlight/cms/navbar.html b/datacenterlight/templates/datacenterlight/cms/navbar.html index ae6643aa..886a5009 100644 --- a/datacenterlight/templates/datacenterlight/cms/navbar.html +++ b/datacenterlight/templates/datacenterlight/cms/navbar.html @@ -35,14 +35,16 @@ {% endif %} {% endif %} - {% if not request.user.is_authenticated %} -
  • - {% trans "Login" %}   -
  • - {% else %} -
  • - {% trans "Dashboard" %} -
  • + {% if instance.show_login_option %} + {% if not request.user.is_authenticated %} +
  • + {% trans "Login" %}   +
  • + {% else %} +
  • + {% trans "Dashboard" %} +
  • + {% endif %} {% endif %} {% comment %} diff --git a/datacenterlight/templates/datacenterlight/cms/navbar_dropdown.html b/datacenterlight/templates/datacenterlight/cms/navbar_dropdown.html index 051e8914..70926874 100644 --- a/datacenterlight/templates/datacenterlight/cms/navbar_dropdown.html +++ b/datacenterlight/templates/datacenterlight/cms/navbar_dropdown.html @@ -1,10 +1,10 @@ {% load cms_tags %} \ No newline at end of file +
    diff --git a/datacenterlight/templates/datacenterlight/cms/section.html b/datacenterlight/templates/datacenterlight/cms/section.html index 5a420a99..4438cf7d 100644 --- a/datacenterlight/templates/datacenterlight/cms/section.html +++ b/datacenterlight/templates/datacenterlight/cms/section.html @@ -2,17 +2,24 @@
    - {% if children_to_side|length %} + {% if children_to_side|length or children_calculator|length %}
    {% include "datacenterlight/cms/includes/_section_split_content.html" %}
    -
    - {% for plugin in children_to_side %} + {% if children_calculator|length %} + {% for plugin in children_calculator %} {% render_plugin plugin %} {% endfor %} -
    + {% endif %} + {% if children_to_side %} +
    + {% for plugin in children_to_side %} + {% render_plugin plugin %} + {% endfor %} +
    + {% endif %}
    {% else %} diff --git a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html index e3fe8676..f9896f17 100644 --- a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html +++ b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html @@ -8,22 +8,29 @@ window.ramUnitPrice = {{vm_pricing.ram_unit_price|default:0}}; window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}}; window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}}; + window.discountAmount = {{vm_pricing.discount_amount|default:0}}; + window.minRam = {{min_ram}}; + window.minRamErr = '{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}'; {% endif %} -
    + {% csrf_token %} +

    {% trans "VM hosting" %}

    - 15 + CHF/{% trans "month" %} - {% if vm_pricing.vat_inclusive %}
    -

    {% trans "VAT included" %}

    +

    + {% if vm_pricing.vat_inclusive %}{% trans "VAT included" %}
    {% endif %} + {% if vm_pricing.discount_amount %} + {% trans "You save" %} {{ vm_pricing.discount_amount }} CHF + {% endif %} +

    - {% endif %}
    @@ -50,8 +57,8 @@
    - + GB RAM
    @@ -87,7 +94,8 @@
    diff --git a/datacenterlight/templates/datacenterlight/landing_payment.html b/datacenterlight/templates/datacenterlight/landing_payment.html index b808e033..fb6d51b0 100644 --- a/datacenterlight/templates/datacenterlight/landing_payment.html +++ b/datacenterlight/templates/datacenterlight/landing_payment.html @@ -67,110 +67,102 @@
    -

    {%trans "Your Order" %}

    -
    -
    -

    {% trans "Cores"%} {{request.session.specs.cpu|floatformat}}

    -
    -

    {% trans "Memory"%} {{request.session.specs.memory|floatformat}} GB

    -
    -

    {% trans "Disk space"%} {{request.session.specs.disk_size|floatformat}} GB

    -
    -

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

    -
    -

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

    -
    + {% if generic_payment_form %} +

    {%trans "Make a payment" %}

    +
    + + {% csrf_token %} + + {% for field in generic_payment_form %} + {% bootstrap_field field type='fields'%} + {% endfor %} +

    {{generic_payment_form.non_field_errors|striptags}}

    + + {% else %} +

    {%trans "Your Order" %}

    +
    +
    +

    {% trans "Cores"%} {{request.session.specs.cpu|floatformat}}

    +
    +

    {% trans "Memory"%} {{request.session.specs.memory|floatformat}} GB

    +
    +

    {% trans "Disk space"%} {{request.session.specs.disk_size|floatformat}} GB

    +
    +

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

    +
    +

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

    +
    + {% if vm_pricing.discount_amount %} +

    + {%trans "Discount" as discount_name %} + {{ vm_pricing.discount_name|default:discount_name }}   + - {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %} +

    +

    + ({% trans "Will be applied at checkout" %}) +

    + {% endif %} +
    + {% endif %}
    + {% with card_list_len=cards_list|length %}

    {%trans "Credit Card"%}


    - {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} -

    -
    - {% if credit_card_data.last4 %} -
    -
    Credit Card
    -
    Last 4: *****{{credit_card_data.last4}}
    -
    Type: {{credit_card_data.cc_brand}}
    - -
    - {% if not messages and not form.non_field_errors %} -

    - {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} -

    - {% endif %} -
    - {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
      -
    • -

      {{ message|safe }}

      -
    • -
    - {% endif %} - {% endfor %} - {% for error in form.non_field_errors %} -

    - {{ error|escape }} -

    - {% endfor %} -
    -
    - -
    + {% if card_list_len > 0 %} + {% blocktrans %}Please select one of the cards that you used before or fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} {% else %} -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - - -
    + {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} + {% endif %} +

    +
    + {% for card in cards_list %} +
    +
    +
    {% trans "Credit Card" %}
    +
    {% trans "Last" %} 4: ***** {{card.last4}}
    +
    {% trans "Type" %}: {{card.brand}}
    +
    +
    -
    - {% if not messages and not form.non_field_errors %} -

    - {% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %} -

    - {% endif %} -
    - {% for message in messages %} - {% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %} -
      -
    • {{ message|safe }}

    • -
    - {% endif %} - {% endfor %} + {% endfor %} + {% if card_list_len > 0 %} +
    +
    +
    +

    {% trans "Add a new credit card" %}

    +
    +
    + +
    +
    -
    - +
    +
    +
    +

    {%trans "New Credit Card" %}

    +
    + {% include "hosting/includes/_card_input.html" %} +
    - -
    -

    -
    - - {% endif %} -
    + {% else%} + {% include "hosting/includes/_card_input.html" %} + {% endif %} +
    + {% endwith %}
    @@ -190,13 +182,4 @@ })(); {%endif%} - -{% if credit_card_data.last4 and credit_card_data.cc_brand %} - -{%endif%} - {%endblock%} diff --git a/datacenterlight/templates/datacenterlight/order_detail.html b/datacenterlight/templates/datacenterlight/order_detail.html index 95bfa3c6..31933e12 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -47,48 +47,104 @@

    {% trans "Order summary" %}

    -

    - {% trans "Product" %}:  - {{ request.session.template.name }} -

    -
    -
    + {% if generic_payment_details %}

    - {% trans "Cores" %}: - {{vm.cpu|floatformat}} + {% trans "Product" %}:  + {{ generic_payment_details.product_name }}

    +
    +
    +

    + {% trans "Amount" %}: + CHF {{generic_payment_details.amount|floatformat:2|intcomma}} +

    + {% if generic_payment_details.description %} +

    + {% trans "Description" %}: + {{generic_payment_details.description}} +

    + {% endif %} + {% if generic_payment_details.recurring %} +

    + {% trans "Recurring" %}: + Yes +

    + {% endif %} +
    +
    + {% else %}

    - {% trans "Memory" %}: - {{vm.memory|intcomma}} GB + {% trans "Product" %}:  + {{ request.session.template.name }}

    -

    - {% 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.total_price|floatformat:2|intcomma}} CHF -

    -
    -
    +
    +
    +

    + {% trans "Cores" %}: + {{vm.cpu|floatformat}} +

    +

    + {% trans "Memory" %}: + {{vm.memory|intcomma}} GB +

    +

    + {% trans "Disk space" %}: + {{vm.disk_size|intcomma}} GB +

    +
    +
    +
    +
    + {% if vm.vat > 0 or vm.discount.amount > 0 %} +
    +
    + {% 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 %} + {% if vm.discount.amount > 0 %} +

    + {%trans "Discount" as discount_name %} + {{ vm.discount.name|default:discount_name }} + - {{ vm.discount.amount }} CHF +

    + {% endif %} +
    +
    +
    +
    +
    + {% endif %} +
    +

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

    +
    +
    + {% endif %}
    -
    +
    {% csrf_token %}
    -
    {% 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 %}.
    + {% if generic_payment_details %} + {% if generic_payment_details.recurring %} +
    {% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.
    + {% else %} +
    {% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.
    + {% endif %} + {% else %} +
    {% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.
    + {% endif %}
    +
    + +
    +

    +
    + \ No newline at end of file diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html index 2568aafc..4a62e9fa 100644 --- a/hosting/templates/hosting/order_detail.html +++ b/hosting/templates/hosting/order_detail.html @@ -39,7 +39,7 @@ {% endif %}

    - {% if order %} + {% if order and vm %}

    {% trans "Status" %}: @@ -93,58 +93,106 @@


    {% trans "Order summary" %}

    -

    - {% trans "Product" %}:  - {% if vm.name %} - {{ vm.name }} - {% else %} - {{ request.session.template.name }} - {% endif %} -

    -
    -
    - {% if vm.created_at %} -

    - {% trans "Period" %}: - - {{ vm.created_at|date:'Y-m-d h:i a' }} - {{ subscription_end_date|date:'Y-m-d h:i a' }} - -

    + {% if vm %} +

    + {% trans "Product" %}:  + {% if vm.name %} + {{ vm.name }} + {% else %} + {{ request.session.template.name }} {% endif %} -

    - {% trans "Cores" %}: - {% if vm.cores %} - {{vm.cores|floatformat}} - {% else %} - {{vm.cpu|floatformat}} +

    +
    +
    + {% if vm.created_at %} +

    + {% trans "Period" %}: + + {{ vm.created_at|date:'Y-m-d h:i a' }} - {{ subscription_end_date|date:'Y-m-d h:i a' }} + +

    {% endif %} -

    -

    - {% trans "Memory" %}: - {{vm.memory}} GB -

    -

    - {% trans "Disk space" %}: - {{vm.disk_size}} GB -

    - {% if vm.vat > 0 %}

    - {% trans "Subtotal" %}: - {{vm.price|floatformat:2|intcomma}} CHF + {% trans "Cores" %}: + {% if vm.cores %} + {{vm.cores|floatformat}} + {% else %} + {{vm.cpu|floatformat}} + {% endif %}

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

    +

    + {% trans "Disk space" %}: + {{vm.disk_size}} GB +

    +
    +
    +
    +
    + {% if vm.vat > 0 or vm.discount.amount > 0 %} +
    +
    + {% 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 %} + {% if vm.discount.amount > 0 %} +

    + {%trans "Discount" as discount_name %} + {{ vm.discount.name|default:discount_name }} + - {{ vm.discount.amount }} CHF +

    + {% endif %} +
    +
    +
    +
    +
    {% endif %} -

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

    +
    +

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

    +
    -
    + {% else %} +

    + {% trans "Product" %}:  + {{ product_name }} +

    +
    +
    +

    + {% trans "Amount" %}: + {{order.price|floatformat:2|intcomma}} CHF +

    + {% if order.generic_payment_description %} +

    + {% trans "Description" %}: + {{order.generic_payment_description}} +

    + {% endif %} + {% if order.subscription_id %} +

    + {% trans "Recurring" %}: + {{order.created_at|date:'d'|ordinal}} {% trans "of every month" %} +

    + {% endif %} +
    +
    + {% endif %}
    -
    +
    {% if not order %} {% block submit_btn %} @@ -152,7 +200,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|intcomma }}CHF/month{% endblocktrans %}.
    +
    {% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{ vm_price }} CHF/month{% endblocktrans %}.
    + {% endfor %} + {% if card_list_len > 0 %} +
    +
    +
    +

    {% trans "Add a new credit card" %}

    +
    +
    + +
    +
    - {% else %} -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - - -
    -
    +
    +
    +
    +

    {%trans "New Credit Card" %}

    +
    + {% include "hosting/includes/_card_input.html" %}
    -
    - {% if not messages and not form.non_field_errors %} -

    - {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} -

    - {% endif %} -
    - {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
      -
    • -

      {{ message|safe }}

      -
    • -
    - {% endif %} - {% endfor %} - - {% for error in form.non_field_errors %} -

    - {{ error|escape }} -

    - {% endfor %} -
    -
    - -
    -
    - -
    -

    -
    - +
    + {% else%} + {% include "hosting/includes/_card_input.html" %} {% endif %}
    + {% endwith %}
    @@ -183,7 +183,7 @@ })(); {%endif%} - +{% comment "Looks as if no more used. To test..." %} {% if credit_card_data.last4 and credit_card_data.cc_brand %} {%endif%} - +{% endcomment %} {%endblock%} diff --git a/hosting/templates/hosting/settings.html b/hosting/templates/hosting/settings.html index 0bafe8e5..56818cbf 100644 --- a/hosting/templates/hosting/settings.html +++ b/hosting/templates/hosting/settings.html @@ -7,6 +7,7 @@ {% block content %}
    + {% include 'hosting/includes/_messages.html' %}

    {% trans "My Settings" %}

    @@ -14,116 +15,105 @@
    -

    {%trans "Billing Address"%}

    +

    {%trans "Billing Address" %}


    + {% csrf_token %} {% for field in form %} - {% csrf_token %} {% bootstrap_field field show_label=False type='fields' bound_css_class='' %} {% endfor %}
    - +
    -

    {%trans "Credit Card"%}

    +

    {%trans "Credit Card" %}


    - {% if credit_card_data.last4 %} + {% with card_list_len=cards_list|length %} + {% for card in cards_list %}
    {% trans "Credit Card" %}
    -
    {% trans "Last" %} 4: *****{{credit_card_data.last4}}
    -
    {% trans "Type" %}: {{credit_card_data.cc_brand}}
    - {% comment %} +
    {% trans "Last" %} 4: ***** {{card.last4}}
    +
    {% trans "Type" %}: {{card.brand}}
    + {% if card_list_len > 1 %}
    - {% trans "REMOVE CARD" %} + {% trans "REMOVE CARD" %} +
    + {% endif %}
    - {% trans "EDIT CARD" %} + {% if card.preferred %} + {% trans "DEFAULT" %} + {% else %} +
    + {% csrf_token %} + + {% trans "SELECT" %} +
    + {% endif %}
    - {% endcomment %}
    - {% else %} + {% empty %}

    {% trans "No Credit Cards Added" %}

    {% blocktrans %}We are using Stripe for payment and do not store your information in our database.{% endblocktrans %}

    + {% endfor %} + {% endwith %} - {% comment %} -

    {% trans "Add a new Card." %}

    -

    - {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} -

    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    - {% if not messages and not form.non_field_errors %} -

    - {% blocktrans %}You are not making any payment here.{% endblocktrans %} -

    - {% endif %} -
    - {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
    • -

      {{ message|safe }}

      -
    - {% endif %} - {% endfor %} - - {% for error in form.non_field_errors %} -

    - {{ error|escape }} -

    - {% endfor %} -
    -
    -
    - -
    -
    -
    - -
    -

    -
    -
    - {% endcomment %} - {% endif %} +
    +
    +
    +

    {% trans "Add a new credit card" %}

    +
    +
    + +
    +
    +
    +
    +
    +
    +

    {%trans "New Credit Card" %}

    +
    + {% include "hosting/includes/_card_input.html" %} +
    +
    - {% comment %} {% if stripe_key %} {% get_current_language as LANGUAGE_CODE %} @@ -137,13 +127,4 @@ })(); {%endif%} - - {% if credit_card_data.last4 and credit_card_data.cc_brand %} - - {%endif%} - {% endcomment %} {%endblock%} diff --git a/hosting/templates/hosting/virtual_machine_detail.html b/hosting/templates/hosting/virtual_machine_detail.html index 68894851..ce02036f 100644 --- a/hosting/templates/hosting/virtual_machine_detail.html +++ b/hosting/templates/hosting/virtual_machine_detail.html @@ -51,7 +51,7 @@

    {% trans "Status" %}

    -
    +
    {% trans "Your VM is" %}
    {% if virtual_machine.state == 'PENDING' %} @@ -74,6 +74,10 @@ {% endif %}
    +
    +

    {% trans "Attention:" %}

    +

    {% trans "terminating VM can not be reverted." %}

    +
    @@ -105,7 +109,7 @@ {% endif %} -
    +
    {% if instance.heading %}
    {{ instance.heading }}
    {% endif %} diff --git a/ungleich_page/templates/ungleich_page/ungleich_cms_page.html b/ungleich_page/templates/ungleich_page/ungleich_cms_page.html index f8d32f07..113568e6 100644 --- a/ungleich_page/templates/ungleich_page/ungleich_cms_page.html +++ b/ungleich_page/templates/ungleich_page/ungleich_cms_page.html @@ -7,8 +7,9 @@ - - + + + {% page_attribute "page_title" %} @@ -33,7 +34,11 @@ {% include "google_analytics.html" %} - + {% if request.current_page.cmsfaviconextension %} + + {% else %} + + {% endif %} diff --git a/utils/forms.py b/utils/forms.py index f8a6d103..fdc67d26 100644 --- a/utils/forms.py +++ b/utils/forms.py @@ -1,10 +1,11 @@ from django import forms -from .models import ContactMessage, BillingAddress, UserBillingAddress -from django.template.loader import render_to_string -from django.core.mail import EmailMultiAlternatives -from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import authenticate +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ + from membership.models import CustomUser +from .models import ContactMessage, BillingAddress, UserBillingAddress # from utils.fields import CountryField @@ -66,7 +67,8 @@ class ResendActivationEmailForm(forms.Form): try: c = CustomUser.objects.get(email=email) if c.validated == 1: - raise forms.ValidationError(_("The account is already active.")) + raise forms.ValidationError( + _("The account is already active.")) return email except CustomUser.DoesNotExist: raise forms.ValidationError(_("User does not exist")) @@ -117,6 +119,7 @@ class EditCreditCardForm(forms.Form): class BillingAddressForm(forms.ModelForm): token = forms.CharField(widget=forms.HiddenInput(), required=False) + card = forms.CharField(widget=forms.HiddenInput(), required=False) class Meta: model = BillingAddress @@ -136,6 +139,32 @@ class BillingAddressFormSignup(BillingAddressForm): email = forms.EmailField(label=_('Email Address')) field_order = ['name', 'email'] + class Meta: + model = BillingAddress + fields = ['name', 'email', 'cardholder_name', 'street_address', + 'city', 'postal_code', 'country'] + labels = { + 'name': 'Name', + 'email': _('Email'), + 'cardholder_name': _('Cardholder Name'), + 'street_address': _('Street Address'), + 'city': _('City'), + 'postal_code': _('Postal Code'), + 'Country': _('Country'), + } + + def clean_email(self): + email = self.cleaned_data.get('email') + try: + CustomUser.objects.get(email=email) + raise forms.ValidationError( + _("The email %(email)s is already registered with us. " + "Please reset your password and access your account.") % + {'email': email} + ) + except CustomUser.DoesNotExist: + return email + class UserBillingAddressForm(forms.ModelForm): user = forms.ModelChoiceField(queryset=CustomUser.objects.all(), diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py index 04ed658a..ec97a320 100644 --- a/utils/hosting_utils.py +++ b/utils/hosting_utils.py @@ -1,5 +1,7 @@ import decimal import logging +import subprocess + from oca.pool import WrongIdError from datacenterlight.models import VMPricing @@ -79,7 +81,7 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'): (decimal.Decimal(hdd_size) * pricing.hdd_unit_price)) cents = decimal.Decimal('.01') price = price.quantize(cents, decimal.ROUND_HALF_UP) - return float(price) + return round(float(price), 2) def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, @@ -107,10 +109,12 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, ) 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)) + 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) @@ -121,4 +125,46 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, 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) + discount = { + 'name': pricing.discount_name, + 'amount': round(float(pricing.discount_amount), 2) + } + return (round(float(price), 2), round(float(vat), 2), + round(float(vat_percent), 2), discount) + + +def ping_ok(host_ipv6): + """ + A utility method to check if a host responds to ping requests. Note: the + function relies on `ping6` utility of debian to check. + + :param host_ipv6 str type parameter that represets the ipv6 of the host to + checked + :return True if the host responds to ping else returns False + """ + try: + subprocess.check_output("ping6 -c 1 " + host_ipv6, shell=True) + except Exception as ex: + logger.debug(host_ipv6 + " not reachable via ping. Error = " + str(ex)) + return False + return True + + +class HostingUtils: + @staticmethod + def clear_items_from_list(from_list, items_list): + """ + A utility function to clear items from a given list. + Useful when deleting items in bulk from session. + e.g.: + HostingUtils.clear_items_from_list( + request.session, + ['token', 'billing_address_data', 'card_id',] + ) + :param from_list: + :param items_list: + :return: + """ + for var in items_list: + if var in from_list: + del from_list[var] diff --git a/utils/locale/de/LC_MESSAGES/django.po b/utils/locale/de/LC_MESSAGES/django.po index f18fc9c2..670e15b9 100644 --- a/utils/locale/de/LC_MESSAGES/django.po +++ b/utils/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-10-10 21:35+0530\n" +"POT-Creation-Date: 2018-07-07 19:27+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -777,11 +777,18 @@ msgstr "" msgid "Email Address" msgstr "" -msgid "Street Building" -msgstr "" - msgid "Email" +msgstr "E-Mail" + +msgid "" +"The email %(email)s is already registered with us. Please reset your " +"password and access your account." msgstr "" +"Diese E-Mail-Adresse %(email)s existiert bereits. Bitte setze dein Passwort zurück " +"auf dein Konto zuzugreifen." + +msgid "Street Building" +msgstr "Gebäude" msgid "Phone number" msgstr "Telefon" diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 79bca243..a3224a0e 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -78,6 +78,22 @@ class StripeUtils(object): customer.source = token customer.save() + @handleStripeError + def associate_customer_card(self, stripe_customer_id, token, + set_as_default=False): + customer = stripe.Customer.retrieve(stripe_customer_id) + card = customer.sources.create(source=token) + if set_as_default: + customer.default_source = card.id + customer.save() + return True + + @handleStripeError + def dissociate_customer_card(self, stripe_customer_id, card_id): + customer = stripe.Customer.retrieve(stripe_customer_id) + card = customer.sources.retrieve(card_id) + card.delete() + @handleStripeError def update_customer_card(self, customer_id, token): customer = stripe.Customer.retrieve(customer_id) @@ -93,32 +109,47 @@ class StripeUtils(object): return new_card_data @handleStripeError - def get_card_details(self, customer_id, token): + def get_card_details(self, customer_id): customer = stripe.Customer.retrieve(customer_id) credit_card_raw_data = customer.sources.data.pop() card_details = { 'last4': credit_card_raw_data.last4, - 'brand': credit_card_raw_data.brand + 'brand': credit_card_raw_data.brand, + 'exp_month': credit_card_raw_data.exp_month, + 'exp_year': credit_card_raw_data.exp_year, + 'fingerprint': credit_card_raw_data.fingerprint, + 'card_id': credit_card_raw_data.id } return card_details - def check_customer(self, id, user, token): - customers = self.stripe.Customer.all() - if not customers.get('data'): + @handleStripeError + def get_cards_details_from_token(self, token): + stripe_token = stripe.Token.retrieve(token) + card_details = { + 'last4': stripe_token.card.last4, + 'brand': stripe_token.card.brand, + 'exp_month': stripe_token.card.exp_month, + 'exp_year': stripe_token.card.exp_year, + 'fingerprint': stripe_token.card.fingerprint, + 'card_id': stripe_token.card.id + } + return card_details + + def check_customer(self, stripe_cus_api_id, user, token): + try: + customer = stripe.Customer.retrieve(stripe_cus_api_id) + except stripe.InvalidRequestError: customer = self.create_customer(token, user.email, user.name) - else: - try: - customer = stripe.Customer.retrieve(id) - except stripe.InvalidRequestError: - customer = self.create_customer(token, user.email, user.name) - user.stripecustomer.stripe_id = customer.get( - 'response_object').get('id') - user.stripecustomer.save() + user.stripecustomer.stripe_id = customer.get( + 'response_object').get('id') + user.stripecustomer.save() + if type(customer) is dict: + customer = customer['response_object'] return customer @handleStripeError - def get_customer(self, id): - customer = stripe.Customer.retrieve(id) + def get_customer(self, stripe_api_cus_id): + customer = stripe.Customer.retrieve(stripe_api_cus_id) # data = customer.get('response_object') return customer @@ -233,6 +264,12 @@ class StripeUtils(object): ) return subscription_result + @handleStripeError + def set_subscription_metadata(self, subscription_id, metadata): + subscription = stripe.Subscription.retrieve(subscription_id) + subscription.metadata = metadata + subscription.save() + @handleStripeError def unsubscribe_customer(self, subscription_id): """ @@ -254,7 +291,8 @@ class StripeUtils(object): return charge @staticmethod - def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None): + def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None, + price=None): """ Returns the Stripe plan id string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters @@ -266,6 +304,7 @@ class StripeUtils(object): :param version: The version of the Stripe plans :param app: The application to which the stripe plan belongs to. By default it is 'dcl' + :param price: The price for this plan :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, @@ -277,16 +316,39 @@ class StripeUtils(object): stripe_plan_id_string = '{app}-v{version}-{plan}'.format( app=app, version=version, - plan=dcl_plan_string) - return stripe_plan_id_string + plan=dcl_plan_string + ) + if price is not None: + stripe_plan_id_string_with_price = '{}-{}chf'.format( + stripe_plan_id_string, + round(price, 2) + ) + return stripe_plan_id_string_with_price + else: + return stripe_plan_id_string @staticmethod - def get_stripe_plan_name(cpu, memory, disk_size): + def get_stripe_plan_name(cpu, memory, disk_size, price): """ Returns the Stripe plan name :return: """ - return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format( - cpu=cpu, - memory=memory, - disk_size=disk_size) + return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \ + "{price} CHF".format( + cpu=cpu, + memory=memory, + disk_size=disk_size, + price=round(price, 2) + ) + + @handleStripeError + def set_subscription_meta_data(self, subscription_id, meta_data): + """ + Adds VM metadata to a subscription + :param subscription_id: Stripe identifier for the subscription + :param meta_data: A dict of meta data to be added + :return: + """ + subscription = stripe.Subscription.retrieve(subscription_id) + subscription.metadata = meta_data + subscription.save()