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 8442789d..b85dd4ff 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,72 @@ +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: [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 + * #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..5a1fc8a2 100644 --- a/datacenterlight/admin.py +++ b/datacenterlight/admin.py @@ -1,10 +1,19 @@ 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, VMTemplate 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) +admin.site.register(VMTemplate) diff --git a/datacenterlight/cms_models.py b/datacenterlight/cms_models.py index 9eb55e0c..2d1a98b5 100644 --- a/datacenterlight/cms_models.py +++ b/datacenterlight/cms_models.py @@ -1,11 +1,19 @@ +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 from djangocms_text_ckeditor.fields import HTMLField +from filer.fields.file import FilerFileField from filer.fields.image import FilerImageField +from datacenterlight.models import VMPricing, VMTemplate + class CMSIntegration(models.Model): name = models.CharField( @@ -21,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: @@ -30,9 +42,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, @@ -162,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 @@ -275,3 +297,68 @@ class DCLSectionPromoPluginModel(CMSPlugin): if self.background_image: extra_classes += ' promo-with-bg' return extra_classes + + +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 a1a3833d..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 + DCLSectionPromoPluginModel, DCLCalculatorPluginModel ) 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 @@ -76,25 +80,27 @@ class DCLSectionPromoPlugin(CMSPluginBase): class DCLCalculatorPlugin(CMSPluginBase): module = "Datacenterlight" name = "DCL Calculator Plugin" - model = DCLSectionPluginModel + model = DCLCalculatorPluginModel render_template = "datacenterlight/cms/calculator.html" cache = False - allow_children = True - child_classes = [ - 'DCLSectionPromoPlugin', 'UngleichHTMLPlugin' - ] + 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_side'] = [] - context['children_to_content'] = [] - if instance.child_plugin_instances is not None: - context['children_to_content'].extend( - instance.child_plugin_instances - ) + 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['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 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..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-03-30 21:29+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" @@ -72,9 +75,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 +100,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" @@ -139,6 +143,9 @@ msgstr "Monat" msgid "VAT included" msgstr "MwSt. inklusive" +msgid "You save" +msgstr "Du sparst" + msgid "Hosted in Switzerland" msgstr "Standort: Schweiz" @@ -160,21 +167,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 +176,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 +198,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 +209,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 +221,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 +233,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." @@ -293,6 +293,9 @@ msgstr "Registrieren" msgid "Billing Address" msgstr "Rechnungsadresse" +msgid "Make a payment" +msgstr "" + msgid "Your Order" msgstr "Deine Bestellung" @@ -314,46 +317,58 @@ msgstr "Gesamt" msgid "including VAT" msgstr "inkl. Mehrwertsteuer" +msgid "excluding VAT" +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 fill in your credit card information below. We are using Stripe for payment and do " -"not store your information in our database." +"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 fülle Deine Kreditkarteninformationen unten aus. Wir nutzen Stripe für die Bezahlung " -"und speichern keine Informationen in unserer Datenbank." +"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 "" -"You are not making any payment yet. After submitting your card information, " -"you will be taken to the Confirm Order Page." +"Please fill in your credit card information below. We are using Stripe for payment and do not " +"store your information in our database." 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." +"Bitte fülle Deine Kreditkarteninformationen unten aus. Wir nutzen Stripe für die Bezahlung und " +"speichern keine Informationen in unserer Datenbank." -msgid "Card Number" -msgstr "Kreditkartennummer" +msgid "Last" +msgstr "Letzten" -msgid "Expiry Date" -msgstr "Ablaufdatum" +msgid "Type" +msgstr "Typ" -msgid "CVC" -msgstr "" +msgid "SELECT" +msgstr "AUSWÄHLEN" -msgid "Card Type" -msgstr "Kartentyp" +msgid "Add a new credit card" +msgstr "Eine neue Kreditkarte hinzufügen" -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 CARD" +msgstr "NEUE KARTE" + +msgid "New Credit Card" +msgstr "Neue Kreditkarte" msgid "Processing" msgstr "Weiter" @@ -383,13 +398,42 @@ msgstr "Bestellungsübersicht" msgid "Product" msgstr "Produkt" +msgid "Amount" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Recurring" +msgstr "" + +msgid "Subtotal" +msgstr "Zwischensumme" + +msgid "VAT" +msgstr "Mehrwertsteuer" + +msgid "" +"By clicking \"Place order\" this plan will charge your credit card account " +"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 the fee of %(vm_price)sCHF/month" +"with %(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 +499,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 +541,17 @@ 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 "" + +#, 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" @@ -507,8 +562,38 @@ 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." + +#, 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." @@ -517,8 +602,36 @@ 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 "" +#~ "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" + +#~ msgid "Order VM" +#~ msgstr "VM bestellen" #~ msgid "Enter name" #~ msgstr "Name" @@ -533,18 +646,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 +667,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 +678,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 +695,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 +724,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 +766,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 +796,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 +811,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/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/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/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 6fcf24a9..729bbdf9 100644 --- a/datacenterlight/models.py +++ b/datacenterlight/models.py @@ -1,17 +1,103 @@ +import logging + from django.db import models +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 +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 + ) + 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): + 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 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): + 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/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 e21e2b49..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; 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 33bf6425..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; @@ -1231,6 +1231,15 @@ footer { background-position: center; } +.promo-section.promo-with-bg a { + color: #87B6EA; +} + +.promo-section.promo-with-bg a:hover, +.promo-section.promo-with-bg a:focus { + color: #77a6da; +} + .promo-section h3 { font-weight: 700; font-size: 36px; diff --git a/datacenterlight/static/datacenterlight/img/datacenterlight.png b/datacenterlight/static/datacenterlight/img/datacenterlight.png index 1ae6ff53..9097af9b 100644 Binary files a/datacenterlight/static/datacenterlight/img/datacenterlight.png and b/datacenterlight/static/datacenterlight/img/datacenterlight.png differ diff --git a/datacenterlight/static/datacenterlight/js/main.js b/datacenterlight/static/datacenterlight/js/main.js index 6753695c..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(); }); } @@ -171,7 +209,22 @@ } 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.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) - + window.discountAmount; total = parseFloat(total.toFixed(2)); $("#total").text(total); } diff --git a/datacenterlight/tasks.py b/datacenterlight/tasks.py index 3db6eb54..2779f79b 100644 --- a/datacenterlight/tasks.py +++ b/datacenterlight/tasks.py @@ -1,23 +1,26 @@ 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__) @@ -49,23 +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('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') @@ -94,33 +89,43 @@ def create_vm_task(self, vm_template_id, user, specs, template, if vm_id is None: raise Exception("Could not create VM") - # Create a Hosting Order - order = HostingOrder.create( - price=final_price, - vm_id=vm_id, - customer=customer, - billing_address=billing_address + # 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 @@ -130,12 +135,19 @@ 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 + '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'] + )) email_data = { 'subject': settings.DCL_TEXT + " Order from %s" % context['email'], 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, @@ -159,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')}, @@ -176,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: @@ -191,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 0c356735..5202fb90 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,11 +56,12 @@ {% placeholder 'Datacenterlight Header' or %}
-

{% page_attribute page_title %}

+

{% page_attribute "page_title" %}

{% 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/emails/user_activation.html b/datacenterlight/templates/datacenterlight/emails/user_activation.html index 6e70100f..403482a4 100644 --- a/datacenterlight/templates/datacenterlight/emails/user_activation.html +++ b/datacenterlight/templates/datacenterlight/emails/user_activation.html @@ -14,7 +14,7 @@ diff --git a/datacenterlight/templates/datacenterlight/emails/welcome_user.html b/datacenterlight/templates/datacenterlight/emails/welcome_user.html index e947ac97..f18f9750 100644 --- a/datacenterlight/templates/datacenterlight/emails/welcome_user.html +++ b/datacenterlight/templates/datacenterlight/emails/welcome_user.html @@ -14,7 +14,7 @@
    - +
    diff --git a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html index f38150bb..f9896f17 100644 --- a/datacenterlight/templates/datacenterlight/includes/_calculator_form.html +++ b/datacenterlight/templates/datacenterlight/includes/_calculator_form.html @@ -1,14 +1,35 @@ {% load staticfiles i18n%} - + +{% if vm_pricing %} + +{% endif %} + + {% csrf_token %} +

    {% trans "VM hosting" %}

    - 15 + CHF/{% trans "month" %}
    -

    {% 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 %} +

    @@ -36,8 +57,8 @@
    - + GB RAM
    @@ -73,10 +94,12 @@
    + diff --git a/datacenterlight/templates/datacenterlight/landing_payment.html b/datacenterlight/templates/datacenterlight/landing_payment.html index f21dc54b..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" %}  ({%trans "including VAT" %}) {{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 78ed43c0..31933e12 100644 --- a/datacenterlight/templates/datacenterlight/order_detail.html +++ b/datacenterlight/templates/datacenterlight/order_detail.html @@ -47,38 +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 -

    -

    - {% trans "Total" %} - {{vm.price|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_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 %}.
    + {% 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 %}
    - +
    diff --git a/hosting/templates/hosting/emails/password_reset_email.html b/hosting/templates/hosting/emails/password_reset_email.html index 57831228..c7c1310a 100644 --- a/hosting/templates/hosting/emails/password_reset_email.html +++ b/hosting/templates/hosting/emails/password_reset_email.html @@ -14,7 +14,7 @@
    - +
    diff --git a/hosting/templates/hosting/emails/vm_canceled.html b/hosting/templates/hosting/emails/vm_canceled.html index 3142f6bc..78781d5c 100644 --- a/hosting/templates/hosting/emails/vm_canceled.html +++ b/hosting/templates/hosting/emails/vm_canceled.html @@ -14,7 +14,7 @@
    - +
    @@ -25,7 +25,7 @@
    - +

    - {% blocktrans %}You are receiving this email because your virutal machine {{ vm_name }} has been cancelled.{% endblocktrans %} + {% blocktrans %}You are receiving this email because your virtual machine {{ vm_name }} has been cancelled.{% endblocktrans %}

    {% blocktrans %}You can always order a new VM by clicking the button below.{% endblocktrans %} diff --git a/hosting/templates/hosting/emails/vm_canceled.txt b/hosting/templates/hosting/emails/vm_canceled.txt index 9149a554..43263c40 100644 --- a/hosting/templates/hosting/emails/vm_canceled.txt +++ b/hosting/templates/hosting/emails/vm_canceled.txt @@ -2,7 +2,7 @@ {% trans "Virtual Machine Cancellation" %} -{% blocktrans %}You are receiving this email because your virutal machine {{vm_name}} has been cancelled.{% endblocktrans %} +{% blocktrans %}You are receiving this email because your virtual machine {{vm_name}} has been cancelled.{% endblocktrans %} {% blocktrans %}You can always order a new VM by following the link below.{% endblocktrans %} {{ base_url }}{% url 'hosting:create_virtual_machine' %} diff --git a/hosting/templates/hosting/emails/vm_charged.html b/hosting/templates/hosting/emails/vm_charged.html index 33568d05..3a6c4f95 100644 --- a/hosting/templates/hosting/emails/vm_charged.html +++ b/hosting/templates/hosting/emails/vm_charged.html @@ -74,7 +74,7 @@

    @@ -100,7 +100,7 @@ diff --git a/hosting/templates/hosting/includes/_card_input.html b/hosting/templates/hosting/includes/_card_input.html new file mode 100644 index 00000000..8cf2d55b --- /dev/null +++ b/hosting/templates/hosting/includes/_card_input.html @@ -0,0 +1,50 @@ +{% load i18n %} + + + + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + {% 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 %} +
    +
    + +
    + +
    +

    +
    + \ No newline at end of file diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html index f5ee80b6..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,48 +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 -

    -

    - {% trans "Total" %} - {{vm.price|intcomma}} CHF -

    +

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

    +

    + {% 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 +

    +
    -
    + {% 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 %} @@ -142,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 %}.
    - - + + diff --git a/hosting/templates/hosting/payment.html b/hosting/templates/hosting/payment.html index 4878831e..e09775cf 100644 --- a/hosting/templates/hosting/payment.html +++ b/hosting/templates/hosting/payment.html @@ -9,161 +9,161 @@
    -
    -
    -

    {%trans "Your Order" %}

    -
    -
    - {%trans "Cores" %} -
    -
    - {%trans "Memory" %} -
    -
    - {%trans "Disk space" %} -
    -
    - {%trans "Configuration" %} +
    +

    {%trans "Your Order" %}

    +
    +
    +
    +
    +
    +
    + {%trans "Cores" %} +
    +
    +
    +
    + {%trans "Memory" %} +
    +
    +
    +
    + {%trans "Disk space" %} +
    +
    +
    +
    + {%trans "Configuration" %} +
    +
    +
    -
    -
    - {{request.session.specs.cpu|floatformat}} -
    -
    - {{request.session.specs.memory|floatformat}} GB -
    -
    - {{request.session.specs.disk_size|floatformat|intcomma}} GB -
    -
    - {{request.session.template.name}} -
    -
    -
    -
    - {%trans "Total" %} {%trans "including VAT" %} -
    -
    -
    -
    {{request.session.specs.price|intcomma}} - CHF/{% trans "Month" %} +
    +
    +
    +
    +
    + {{request.session.specs.cpu|floatformat}} +
    +
    +
    +
    + {{request.session.specs.memory|floatformat}} GB +
    +
    +
    +
    + {{request.session.specs.disk_size|floatformat|intcomma}} GB +
    +
    +
    +
    + {{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 }}  
    + ({% trans "Will be applied at checkout" %}) +
    +
    +
    +
    +
    - {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %}
    +
    +
    +
    + {% endif %} +
    -
    -
    -
    -

    {%trans "Billing Address"%}

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

    {%trans "Billing Address"%}

    +
    +
    + {% csrf_token %} + {% for field in form %} + {% bootstrap_field field show_label=False type='fields'%} + {% endfor %} + +
    + {% with card_list_len=cards_list|length %}

    {%trans "Credit Card"%}


    + {% 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 %}

    - {% 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 %} + {% for card in cards_list %} +
    +
    +
    {% trans "Credit Card" %}
    +
    {% trans "Last" %} 4: ***** {{card.last4}}
    +
    {% trans "Type" %}: {{card.brand}}
    +
    +
    -
    - + {% 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 b77e1dca..ce02036f 100644 --- a/hosting/templates/hosting/virtual_machine_detail.html +++ b/hosting/templates/hosting/virtual_machine_detail.html @@ -45,13 +45,13 @@

    {% trans "Billing" %}

    {% trans "Current Pricing" %}
    -
    {{virtual_machine.price|floatformat|intcomma}} CHF/{% trans "Month" %}
    +
    {{order.price|floatformat:2|intcomma}} CHF/{% trans "Month" %}
    {% trans "See Invoice" %}

    {% 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 3c193ad7..ec97a320 100644 --- a/utils/hosting_utils.py +++ b/utils/hosting_utils.py @@ -1,6 +1,10 @@ +import decimal import logging +import subprocess + from oca.pool import WrongIdError +from datacenterlight.models import VMPricing from hosting.models import UserHostingKey, VMDetail from opennebula_api.serializers import VirtualMachineSerializer @@ -49,14 +53,118 @@ 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 round(float(price), 2) + + +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) + 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()
    - logo + logo
    - Your virtual machine {{vm.name}} subscription has been charged,
    you can view your invoice clicking on the button below. + Your virtual machine {{vm.name}} subscription has been charged,
    you can view your invoice clicking on the button below.
    {{ order.id }}{{ order.created_at | date:"M d, Y H:i" }}{{ order.price|intcomma }}{{ order.created_at | date:'Y-m-d h:i a' }}{{ order.price|floatformat:2|intcomma }} {% trans 'See Invoice' %}