Merge remote-tracking branch 'mainRepo/master' into task/3530/upgrade_to_django_1.11
This commit is contained in:
commit
99ca10be51
123 changed files with 4545 additions and 1874 deletions
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-12-21 00:23+0000\n"
|
||||
"POT-Creation-Date: 2018-08-24 09:56+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -274,6 +274,28 @@ msgstr ""
|
|||
msgid "You can always order a new VM by following the link below."
|
||||
msgstr ""
|
||||
|
||||
msgid "Card Number"
|
||||
msgstr "Kreditkartennummer"
|
||||
|
||||
msgid "Expiry Date"
|
||||
msgstr "Ablaufdatum"
|
||||
|
||||
msgid "CVC"
|
||||
msgstr ""
|
||||
|
||||
msgid "Card Type"
|
||||
msgstr "Kartentyp"
|
||||
|
||||
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 "SUBMIT"
|
||||
msgstr "ABSENDEN"
|
||||
|
||||
msgid "Toggle navigation"
|
||||
msgstr "Umschalten"
|
||||
|
||||
|
|
@ -365,15 +387,24 @@ msgstr "Arbeitsspeicher"
|
|||
msgid "Disk space"
|
||||
msgstr "Festplattenkapazität"
|
||||
|
||||
msgid "Subtotal"
|
||||
msgstr "Zwischensumme"
|
||||
|
||||
msgid "VAT"
|
||||
msgstr "Mehrwertsteuer"
|
||||
|
||||
msgid "Discount"
|
||||
msgstr "Rabatt"
|
||||
|
||||
msgid "Total"
|
||||
msgstr "Gesamt"
|
||||
|
||||
#, 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_price)s CHF/month"
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF "
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)s CHF "
|
||||
"pro Monat belastet"
|
||||
|
||||
msgid "Place order"
|
||||
|
|
@ -421,9 +452,26 @@ msgstr "Konfiguration"
|
|||
msgid "including VAT"
|
||||
msgstr "inkl. Mehrwertsteuer"
|
||||
|
||||
msgid "excluding VAT"
|
||||
msgstr "exkl. Mehrwertsteuer"
|
||||
|
||||
msgid "Will be applied at checkout"
|
||||
msgstr "wird an der Kasse angewendet"
|
||||
|
||||
msgid "Billing Address"
|
||||
msgstr "Rechnungsadresse"
|
||||
|
||||
msgid ""
|
||||
"Please select one of the cards that you used before or fill in your credit "
|
||||
"card information below. We are using <a href=\"https://stripe.com\" target="
|
||||
"\"_blank\">Stripe</a> for payment and do not store your information in our "
|
||||
"database."
|
||||
msgstr ""
|
||||
"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine "
|
||||
"Kreditkartendetails unten an. Die Bezahlung wird über <a href=\"https://"
|
||||
"stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. Wir speichern Deine "
|
||||
"Kreditkartendetails nicht in unserer Datenbank."
|
||||
|
||||
msgid ""
|
||||
"Please fill in your credit card information below. We are using <a href="
|
||||
"\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not "
|
||||
|
|
@ -433,28 +481,24 @@ msgstr ""
|
|||
"\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und "
|
||||
"speichern keine Informationen in unserer Datenbank."
|
||||
|
||||
msgid ""
|
||||
"You are not making any payment yet. After submitting your card information, "
|
||||
"you will be taken to the Confirm Order Page."
|
||||
msgstr ""
|
||||
"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, "
|
||||
"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast."
|
||||
msgid "Last"
|
||||
msgstr "Letzten"
|
||||
|
||||
msgid "SUBMIT"
|
||||
msgstr "ABSENDEN"
|
||||
|
||||
msgid "Card Number"
|
||||
msgstr "Kreditkartennummer"
|
||||
|
||||
msgid "Expiry Date"
|
||||
msgstr "Ablaufdatum"
|
||||
|
||||
msgid "CVC"
|
||||
msgstr ""
|
||||
|
||||
msgid "Card Type"
|
||||
msgid "Type"
|
||||
msgstr "Kartentyp"
|
||||
|
||||
msgid "SELECT"
|
||||
msgstr "AUSWÄHLEN"
|
||||
|
||||
msgid "Add a new credit card"
|
||||
msgstr "Eine neue Kreditkarte hinzufügen"
|
||||
|
||||
msgid "NEW CARD"
|
||||
msgstr "BEARBEITEN"
|
||||
|
||||
msgid "New Credit Card"
|
||||
msgstr "Neue Kreditkarte"
|
||||
|
||||
msgid "Processing"
|
||||
msgstr "Weiter"
|
||||
|
||||
|
|
@ -468,13 +512,22 @@ msgid "Password reset"
|
|||
msgstr "Passwort zurücksetzen"
|
||||
|
||||
msgid "UPDATE"
|
||||
msgstr ""
|
||||
msgstr "AKTUALISIEREN"
|
||||
|
||||
msgid "Last"
|
||||
msgstr ""
|
||||
msgid "REMOVE CARD"
|
||||
msgstr "KARTE ENTFERNEN"
|
||||
|
||||
msgid "Type"
|
||||
msgstr "Kartentyp"
|
||||
msgid "Remove Card"
|
||||
msgstr "Karte entfernen"
|
||||
|
||||
msgid "Do you want to remove this associated card?"
|
||||
msgstr "Möchtest Du den Schlüssel löschen?"
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
msgid "DEFAULT"
|
||||
msgstr "STANDARD"
|
||||
|
||||
msgid "No Credit Cards Added"
|
||||
msgstr "Es wurde keine Kreditkarte hinzugefügt"
|
||||
|
|
@ -519,10 +572,7 @@ msgid "Public Key"
|
|||
msgstr ""
|
||||
|
||||
msgid "Private Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
msgstr "Privater Schlüssel"
|
||||
|
||||
msgid "Delete SSH Key"
|
||||
msgstr "SSH Key löschen"
|
||||
|
|
@ -580,6 +630,12 @@ msgstr ""
|
|||
"Bitte entschuldige, es scheint ein unerwarteter Fehler aufgetreten zu sein. "
|
||||
"Versuche es doch bitte noch einmal."
|
||||
|
||||
msgid "Attention:"
|
||||
msgstr "Achtung:"
|
||||
|
||||
msgid "terminating VM can not be reverted."
|
||||
msgstr "Das Beenden kann nicht rückgängig gemacht werden."
|
||||
|
||||
msgid "Something doesn't work?"
|
||||
msgstr "Etwas funktioniert nicht?"
|
||||
|
||||
|
|
@ -592,8 +648,12 @@ msgstr "KONTAKT"
|
|||
msgid "Terminate your Virtual Machine"
|
||||
msgstr "Deine Virtuelle Maschine beenden"
|
||||
|
||||
msgid "Do you want to cancel your Virtual Machine"
|
||||
msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst"
|
||||
msgid ""
|
||||
"Terminated VMs can not be revived and will not be refunded. Do you want to "
|
||||
"terminate your VM?"
|
||||
msgstr ""
|
||||
"Beendete VMs können nicht wiederhergestellt oder erstattet werden. Möchtest "
|
||||
"du die VM beenden?"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
|
|
@ -655,6 +715,36 @@ msgstr "Dein Passwort konnte nicht zurückgesetzt werden."
|
|||
msgid "The reset password link is no longer valid."
|
||||
msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig."
|
||||
|
||||
msgid "Card deassociation successful"
|
||||
msgstr "Die Verbindung mit der Karte wurde erfolgreich aufgehoben"
|
||||
|
||||
msgid "You are not permitted to do this operation"
|
||||
msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen"
|
||||
|
||||
msgid "The selected card does not exist"
|
||||
msgstr "Die ausgewählte Karte existiert nicht"
|
||||
|
||||
msgid "Billing address updated successfully"
|
||||
msgstr "Die Rechnungsadresse wurde erfolgreich aktualisiert"
|
||||
|
||||
msgid "You seem to have already added this card"
|
||||
msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
|
||||
|
||||
#, 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 "Successfully associated the card with your account"
|
||||
msgstr "Die Karte wurde erfolgreich mit deinem Konto verbunden"
|
||||
|
||||
#, 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 "Invalid credit card"
|
||||
msgstr "Ungültige Kreditkarte"
|
||||
|
||||
|
|
@ -699,6 +789,10 @@ msgstr "Ungültige RAM-Grösse"
|
|||
msgid "Invalid storage size"
|
||||
msgstr "Ungültige Speicher-Grösse"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Incorrect pricing name. Please contact support{support_email}"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"We could not find the requested VM. Please "
|
||||
"contact Data Center Light Support."
|
||||
|
|
@ -722,6 +816,9 @@ msgstr ""
|
|||
"Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es "
|
||||
"noch einmal."
|
||||
|
||||
#~ msgid "Do you want to cancel your Virtual Machine"
|
||||
#~ msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst"
|
||||
|
||||
#~ msgid "Reset your password"
|
||||
#~ msgstr "Passwort zurücksetzen"
|
||||
|
||||
|
|
@ -788,15 +885,6 @@ msgstr ""
|
|||
#~ msgid "Notifications "
|
||||
#~ msgstr "Benachrichtigungen"
|
||||
|
||||
#~ msgid "REMOVE CARD"
|
||||
#~ msgstr "KARTE ENTFERNEN"
|
||||
|
||||
#~ msgid "EDIT CARD"
|
||||
#~ msgstr "BEARBEITEN"
|
||||
|
||||
#~ msgid "Add a new Card."
|
||||
#~ msgstr "Neue Kreditkarte hinzufügen."
|
||||
|
||||
#~ msgid "You are not making any payment here."
|
||||
#~ msgstr "Es wird noch keine Bezahlung vorgenommen"
|
||||
|
||||
|
|
|
|||
45
hosting/management/commands/import_usercarddetails.py
Normal file
45
hosting/management/commands/import_usercarddetails.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from hosting.models import UserCardDetail
|
||||
from membership.models import CustomUser
|
||||
from utils.stripe_utils import StripeUtils
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Imports the usercard details of all customers. Created just for
|
||||
multiple card support.'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
stripe_utils = StripeUtils()
|
||||
for user in CustomUser.objects.all():
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
if user.stripecustomer:
|
||||
card_details_resp = stripe_utils.get_card_details(
|
||||
user.stripecustomer.stripe_id
|
||||
)
|
||||
card_details = card_details_resp['response_object']
|
||||
if card_details:
|
||||
ucd = UserCardDetail.get_or_create_user_card_detail(
|
||||
stripe_customer=user.stripecustomer,
|
||||
card_details=card_details
|
||||
)
|
||||
UserCardDetail.save_default_card_local(
|
||||
user.stripecustomer.stripe_id,
|
||||
ucd.card_id
|
||||
)
|
||||
print("Saved user card details for {}".format(
|
||||
user.email
|
||||
))
|
||||
else:
|
||||
print(" --- Could not get card details for "
|
||||
"{}".format(user.email))
|
||||
print(" --- Error: {}".format(
|
||||
card_details_resp['error']
|
||||
))
|
||||
else:
|
||||
print(" === {} does not have a StripeCustomer object".format(
|
||||
user.email
|
||||
))
|
||||
except Exception as e:
|
||||
print(" *** Error occurred. Details {}".format(str(e)))
|
||||
23
hosting/migrations/0044_hostingorder_vm_pricing.py
Normal file
23
hosting/migrations/0044_hostingorder_vm_pricing.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-04-16 00:22
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0019_auto_20180415_2236'),
|
||||
('hosting', '0043_vmdetail'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hostingorder',
|
||||
name='vm_pricing',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='datacenterlight.VMPricing'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
35
hosting/migrations/0045_auto_20180701_2028.py
Normal file
35
hosting/migrations/0045_auto_20180701_2028.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-07-01 20:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0024_dclcalculatorpluginmodel_vm_templates_to_show'),
|
||||
('hosting', '0044_hostingorder_vm_pricing'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrderDetail',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('cores', models.IntegerField(default=0)),
|
||||
('memory', models.IntegerField(default=0)),
|
||||
('hdd_size', models.IntegerField(default=0)),
|
||||
('ssd_size', models.IntegerField(default=0)),
|
||||
('vm_template', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='datacenterlight.VMTemplate')),
|
||||
],
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hostingorder',
|
||||
name='order_detail',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hosting.OrderDetail'),
|
||||
),
|
||||
]
|
||||
36
hosting/migrations/0046_usercarddetail.py
Normal file
36
hosting/migrations/0046_usercarddetail.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-07-03 20:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('membership', '0007_auto_20180213_0128'),
|
||||
('hosting', '0045_auto_20180701_2028'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserCardDetail',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last4', models.CharField(max_length=4)),
|
||||
('brand', models.CharField(max_length=10)),
|
||||
('card_id', models.CharField(blank=True, default='', max_length=100)),
|
||||
('fingerprint', models.CharField(max_length=100)),
|
||||
('exp_month', models.IntegerField()),
|
||||
('exp_year', models.IntegerField()),
|
||||
('preferred', models.BooleanField(default=False)),
|
||||
('stripe_customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='membership.StripeCustomer')),
|
||||
],
|
||||
options={
|
||||
'permissions': (('view_usercarddetail', 'View User Card'),),
|
||||
},
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
]
|
||||
25
hosting/migrations/0047_auto_20180821_1240.py
Normal file
25
hosting/migrations/0047_auto_20180821_1240.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-08-21 12:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0046_usercarddetail'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='hostingorder',
|
||||
name='cc_brand',
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usercarddetail',
|
||||
name='brand',
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from django.shortcuts import redirect
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from opennebula_api.serializers import VirtualMachineTemplateSerializer
|
||||
|
|
@ -24,3 +25,10 @@ class ProcessVMSelectionMixin(object):
|
|||
request.session['next'] = reverse('hosting:payment')
|
||||
return redirect(reverse('hosting:login'))
|
||||
return redirect(reverse('hosting:payment'))
|
||||
|
||||
|
||||
class HostingContextMixin(object):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['MULTISITE_CMS_FALLBACK'] = settings.MULTISITE_CMS_FALLBACK
|
||||
return context
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import os
|
||||
import logging
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import os
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
from datacenterlight.models import VMPricing, VMTemplate
|
||||
from membership.models import StripeCustomer, CustomUser
|
||||
from utils.models import BillingAddress
|
||||
from utils.mixins import AssignPermissionsMixin
|
||||
from utils.stripe_utils import StripeUtils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -39,6 +42,25 @@ class HostingPlan(models.Model):
|
|||
return price
|
||||
|
||||
|
||||
class OrderDetail(AssignPermissionsMixin, models.Model):
|
||||
vm_template = models.ForeignKey(
|
||||
VMTemplate, blank=True, null=True, default=None,
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
cores = models.IntegerField(default=0)
|
||||
memory = models.IntegerField(default=0)
|
||||
hdd_size = models.IntegerField(default=0)
|
||||
ssd_size = models.IntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return "Not available" if self.vm_template is None else (
|
||||
"%s - %s, %s cores, %s GB RAM, %s GB SSD" % (
|
||||
self.vm_template.name, self.vm_template.vm_type, self.cores,
|
||||
self.memory, self.ssd_size
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class HostingOrder(AssignPermissionsMixin, models.Model):
|
||||
ORDER_APPROVED_STATUS = 'Approved'
|
||||
ORDER_DECLINED_STATUS = 'Declined'
|
||||
|
|
@ -49,10 +71,15 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
|
|||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
approved = models.BooleanField(default=False)
|
||||
last4 = models.CharField(max_length=4)
|
||||
cc_brand = models.CharField(max_length=10)
|
||||
cc_brand = models.CharField(max_length=128)
|
||||
stripe_charge_id = models.CharField(max_length=100, null=True)
|
||||
price = models.FloatField()
|
||||
subscription_id = models.CharField(max_length=100, null=True)
|
||||
vm_pricing = models.ForeignKey(VMPricing)
|
||||
order_detail = models.ForeignKey(
|
||||
OrderDetail, null=True, blank=True, default=None,
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
permissions = ('view_hostingorder',)
|
||||
|
||||
|
|
@ -62,20 +89,25 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
|
|||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % (self.id)
|
||||
return ("Order Nr: #{} - VM_ID: {} - {} - {} - "
|
||||
"Specs: {} - Price: {}").format(
|
||||
self.id, self.vm_id, self.customer.user.email, self.created_at,
|
||||
self.order_detail, self.price
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def status(self):
|
||||
return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS
|
||||
|
||||
@classmethod
|
||||
def create(cls, price=None, vm_id=None, customer=None,
|
||||
billing_address=None):
|
||||
def create(cls, price=None, vm_id=0, customer=None,
|
||||
billing_address=None, vm_pricing=None):
|
||||
instance = cls.objects.create(
|
||||
price=price,
|
||||
vm_id=vm_id,
|
||||
customer=customer,
|
||||
billing_address=billing_address
|
||||
billing_address=billing_address,
|
||||
vm_pricing=vm_pricing
|
||||
)
|
||||
instance.assign_permissions(customer.user)
|
||||
return instance
|
||||
|
|
@ -180,3 +212,156 @@ class VMDetail(models.Model):
|
|||
months = relativedelta(end_date, self.created_at).months or 1
|
||||
end_date = self.created_at + relativedelta(months=months, days=-1)
|
||||
return end_date
|
||||
|
||||
|
||||
class UserCardDetail(AssignPermissionsMixin, models.Model):
|
||||
permissions = ('view_usercarddetail',)
|
||||
stripe_customer = models.ForeignKey(StripeCustomer)
|
||||
last4 = models.CharField(max_length=4)
|
||||
brand = models.CharField(max_length=128)
|
||||
card_id = models.CharField(max_length=100, blank=True, default='')
|
||||
fingerprint = models.CharField(max_length=100)
|
||||
exp_month = models.IntegerField(null=False)
|
||||
exp_year = models.IntegerField(null=False)
|
||||
preferred = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_usercarddetail', 'View User Card'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, stripe_customer=None, last4=None, brand=None,
|
||||
fingerprint=None, exp_month=None, exp_year=None, card_id=None,
|
||||
preferred=False):
|
||||
instance = cls.objects.create(
|
||||
stripe_customer=stripe_customer, last4=last4, brand=brand,
|
||||
fingerprint=fingerprint, exp_month=exp_month, exp_year=exp_year,
|
||||
card_id=card_id, preferred=preferred
|
||||
)
|
||||
instance.assign_permissions(stripe_customer.user)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def get_all_cards_list(cls, stripe_customer):
|
||||
"""
|
||||
Get all the cards of the given customer as a list
|
||||
|
||||
:param stripe_customer: The StripeCustomer object
|
||||
:return: A list of all cards; an empty list if the customer object is
|
||||
None
|
||||
"""
|
||||
cards_list = []
|
||||
if stripe_customer is None:
|
||||
return cards_list
|
||||
user_card_details = UserCardDetail.objects.filter(
|
||||
stripe_customer_id=stripe_customer.id
|
||||
).order_by('-preferred', 'id')
|
||||
for card in user_card_details:
|
||||
cards_list.append({
|
||||
'last4': card.last4, 'brand': card.brand, 'id': card.id,
|
||||
'preferred': card.preferred
|
||||
})
|
||||
return cards_list
|
||||
|
||||
@classmethod
|
||||
def get_or_create_user_card_detail(cls, stripe_customer, card_details):
|
||||
"""
|
||||
A method that checks if a UserCardDetail object exists already
|
||||
based upon the given card_details and creates it for the given
|
||||
customer if it does not exist. It returns the UserCardDetail object
|
||||
matching the given card_details if it exists.
|
||||
|
||||
:param stripe_customer: The given StripeCustomer object to whom the
|
||||
card object should belong to
|
||||
:param card_details: A dictionary identifying a given card
|
||||
:return: UserCardDetail object
|
||||
"""
|
||||
try:
|
||||
if ('fingerprint' in card_details and 'exp_month' in card_details
|
||||
and 'exp_year' in card_details):
|
||||
card_detail = UserCardDetail.objects.get(
|
||||
stripe_customer=stripe_customer,
|
||||
fingerprint=card_details['fingerprint'],
|
||||
exp_month=card_details['exp_month'],
|
||||
exp_year=card_details['exp_year']
|
||||
)
|
||||
else:
|
||||
raise UserCardDetail.DoesNotExist()
|
||||
except UserCardDetail.DoesNotExist:
|
||||
preferred = False
|
||||
if 'preferred' in card_details:
|
||||
preferred = card_details['preferred']
|
||||
card_detail = UserCardDetail.create(
|
||||
stripe_customer=stripe_customer,
|
||||
last4=card_details['last4'],
|
||||
brand=card_details['brand'],
|
||||
fingerprint=card_details['fingerprint'],
|
||||
exp_month=card_details['exp_month'],
|
||||
exp_year=card_details['exp_year'],
|
||||
card_id=card_details['card_id'],
|
||||
preferred=preferred
|
||||
)
|
||||
return card_detail
|
||||
|
||||
@staticmethod
|
||||
def set_default_card(stripe_api_cus_id, stripe_source_id):
|
||||
"""
|
||||
Sets the given stripe source as the default source for the given
|
||||
Stripe customer
|
||||
:param stripe_api_cus_id: Stripe customer id
|
||||
:param stripe_source_id: The Stripe source id
|
||||
:return:
|
||||
"""
|
||||
stripe_utils = StripeUtils()
|
||||
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
|
||||
cu = cus_response['response_object']
|
||||
cu.default_source = stripe_source_id
|
||||
cu.save()
|
||||
UserCardDetail.save_default_card_local(
|
||||
stripe_api_cus_id, stripe_source_id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def set_default_card_from_stripe(stripe_api_cus_id):
|
||||
stripe_utils = StripeUtils()
|
||||
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
|
||||
cu = cus_response['response_object']
|
||||
default_source = cu.default_source
|
||||
if default_source is not None:
|
||||
UserCardDetail.save_default_card_local(
|
||||
stripe_api_cus_id, default_source
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def save_default_card_local(stripe_api_cus_id, card_id):
|
||||
stripe_cust = StripeCustomer.objects.get(stripe_id=stripe_api_cus_id)
|
||||
user_card_detail = UserCardDetail.objects.get(
|
||||
stripe_customer=stripe_cust, card_id=card_id
|
||||
)
|
||||
for card in stripe_cust.usercarddetail_set.all():
|
||||
card.preferred = False
|
||||
card.save()
|
||||
user_card_detail.preferred = True
|
||||
user_card_detail.save()
|
||||
|
||||
@staticmethod
|
||||
def get_user_card_details(stripe_customer, card_details):
|
||||
"""
|
||||
A utility function to check whether a StripeCustomer is already
|
||||
associated with the card having given details
|
||||
|
||||
:param stripe_customer:
|
||||
:param card_details:
|
||||
:return: The UserCardDetails object if it exists, None otherwise
|
||||
"""
|
||||
try:
|
||||
ucd = UserCardDetail.objects.get(
|
||||
stripe_customer=stripe_customer,
|
||||
fingerprint=card_details['fingerprint'],
|
||||
exp_month=card_details['exp_month'],
|
||||
exp_year=card_details['exp_year']
|
||||
)
|
||||
return ucd
|
||||
except UserCardDetail.DoesNotExist:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@
|
|||
margin: 0 auto;
|
||||
max-width: 1120px;
|
||||
}
|
||||
.container-table{
|
||||
margin-top: 35px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.container-table table{
|
||||
overflow-y: auto;
|
||||
}
|
||||
.borderless td {
|
||||
border: none !important;
|
||||
}
|
||||
|
|
@ -35,6 +42,19 @@
|
|||
color: transparent;
|
||||
}
|
||||
|
||||
.inline-headers h3, .inline-headers h4 {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.space-above {
|
||||
margin-top: 4%;
|
||||
}
|
||||
|
||||
.space-above-big {
|
||||
margin-top: 20%;
|
||||
}
|
||||
|
||||
.table>tbody>tr>td{
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
@ -274,6 +294,26 @@
|
|||
font-size: 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.another-card-text {
|
||||
padding: 20px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.credit-card-form {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
|
@ -293,8 +333,15 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.settings-container .credit-card-details-opt {
|
||||
padding-top: 15px;
|
||||
.settings-container .new-card-head {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.settings-container .new-card-head h4 {
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.caps-link .svg-img {
|
||||
|
|
@ -313,7 +360,11 @@
|
|||
.settings-container .btn-vm-contact {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
/* padding: 4px 15px; */
|
||||
}
|
||||
|
||||
.settings-container .choice-btn {
|
||||
letter-spacing: 2px;
|
||||
min-width: 127px;
|
||||
}
|
||||
|
||||
.btn-wide {
|
||||
|
|
@ -355,10 +406,37 @@
|
|||
fill: #999;
|
||||
}
|
||||
|
||||
.card-details-box {
|
||||
border: 1px solid #eee;
|
||||
padding: 5px 25px 25px;
|
||||
}
|
||||
|
||||
.thick-hr {
|
||||
border-top: 5px solid #eee;
|
||||
}
|
||||
|
||||
.locale_date {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.locale_date.done{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.thin-hr {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.new-card-head {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.new-card-button-margin button{
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
|
@ -426,6 +426,7 @@ footer {
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
p.copyright {
|
||||
|
|
@ -448,230 +449,6 @@ a.unlink:hover {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
/***** DCL payment page **********/
|
||||
.dcl-order-container {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-order-table-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-order-table-content {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tbl-content {
|
||||
}
|
||||
|
||||
.dcl-order-table-total {
|
||||
border-bottom: 4px solid #eee;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.dcl-order-table-total span {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.dcl-place-order-text{
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total {
|
||||
text-align: center;
|
||||
color: #000;
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.tbl-total .dcl-price-month {
|
||||
font-size: 16px;
|
||||
text-transform: capitalize;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tbl-no-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dcl-billing-sec {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.dcl-order-sec {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.card-warning-content {
|
||||
font-weight: 300;
|
||||
border: 1px solid #a1a1a1;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.card-warning-error {
|
||||
border: 1px solid #EB4D5C;
|
||||
color: #EB4D5C;
|
||||
}
|
||||
|
||||
.card-warning-addtional-margin {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stripe-payment-btn {
|
||||
outline: none;
|
||||
width: auto;
|
||||
float: right;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
position: absolute;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.card-cvc-element label {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.card-element {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-element label{
|
||||
width:100%;
|
||||
margin-bottom:0px;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.card-cvc-element .my-input {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#card-errors {
|
||||
clear: both;
|
||||
padding: 0 0 10px;
|
||||
color: #eb4d5c;
|
||||
}
|
||||
|
||||
.credit-card-goup{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.dcl-order-table-total span {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.dcl-order-sec {
|
||||
padding: 10px 20px 30px 20px;
|
||||
border-bottom: 4px solid #eee;
|
||||
}
|
||||
|
||||
.tbl-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tbl-content {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.dcl-order-table-header {
|
||||
border-bottom: 0px solid #eee;
|
||||
padding: 10px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dcl-order-table-content {
|
||||
border-bottom: 0px solid #eee;
|
||||
padding: 10px 0;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total {
|
||||
font-size: 18px;
|
||||
color: #000;
|
||||
padding: 10px 0;
|
||||
border-bottom: 0px solid #eee;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total {
|
||||
padding: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dcl-billing-sec {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card-expiry-element {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.card-cvc-element {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#billing-form .form-control {
|
||||
box-shadow: none !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.dcl-order-container {
|
||||
width: 990px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dcl-billing {
|
||||
padding-right: 65px;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dcl-creditcard {
|
||||
padding-left: 65px;
|
||||
}
|
||||
|
||||
.tbl-tot {
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
.content-dashboard {
|
||||
/*width: auto !important;*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1040px) and (min-width: 768px) {
|
||||
.content-dashboard {
|
||||
width: 96% !important;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
margin: 100px auto 40px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
@media(min-width: 768px) {
|
||||
|
|
@ -48,10 +49,6 @@
|
|||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.order-detail-container .order-details strong {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.order-detail-container h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
|
@ -60,13 +57,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-confirm-btn {
|
||||
text-align: center;
|
||||
|
|
@ -96,4 +108,8 @@
|
|||
|
||||
#virtual_machine_create_form {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.dcl-place-order-text {
|
||||
color: #808080;
|
||||
}
|
||||
|
|
@ -1,19 +1,35 @@
|
|||
|
||||
.payment-container {padding-top:70px; padding-bottom: 11%;}
|
||||
.creditcard-box .panel-title {display: inline;font-weight: bold; font-size:17px;}
|
||||
.creditcard-box .checkbox.pull-right { margin: 0; }
|
||||
.creditcard-box .pl-ziro { padding-left: 0px; }
|
||||
.creditcard-box .form-control.error {
|
||||
border-color: red;
|
||||
outline: 0;
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(255,0,0,0.6);
|
||||
.payment-container {
|
||||
padding-top: 70px;
|
||||
padding-bottom: 11%;
|
||||
}
|
||||
|
||||
.creditcard-box .panel-title {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.creditcard-box .checkbox.pull-right {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.creditcard-box .pl-ziro {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.creditcard-box .form-control.error {
|
||||
border-color: red;
|
||||
outline: 0;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(255, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.creditcard-box label.error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
padding: 2px 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.creditcard-box .payment-errors {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
|
|
@ -21,96 +37,221 @@
|
|||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* landing page payment new style */
|
||||
.last-p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.dcl-payment-section {
|
||||
max-width: 391px;
|
||||
margin: 0 auto 30px;
|
||||
padding: 0 10px 30px;
|
||||
border-bottom: 1px solid #edebeb;
|
||||
height: 100%;
|
||||
}
|
||||
.dcl-payment-section hr{
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.dcl-payment-section .top-hr {
|
||||
margin-left: -10px;
|
||||
}
|
||||
.dcl-payment-section h3 {
|
||||
font-weight: 600;
|
||||
}
|
||||
.dcl-payment-section p {
|
||||
/*padding: 0 5px;*/
|
||||
font-weight: 400;
|
||||
}
|
||||
.dcl-payment-section .card-warning-content {
|
||||
padding: 8px 10px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.dcl-payment-order strong{
|
||||
font-size: 17px;
|
||||
}
|
||||
.dcl-payment-order p {
|
||||
font-weight: 300;
|
||||
}
|
||||
.dcl-payment-section .form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dcl-payment-section .form-control {
|
||||
box-shadow: none;
|
||||
padding: 6px 12px;
|
||||
height: 32px;
|
||||
}
|
||||
.dcl-payment-user {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.dcl-order-sec {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.dcl-payment-user h4 {
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
.dcl-billing-sec {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.dcl-order-container {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-order-table-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-order-table-content {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dcl-order-table-total {
|
||||
border-bottom: 4px solid #eee;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dcl-order-table-total span {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total {
|
||||
text-align: right;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tbl-no-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.card-warning-content {
|
||||
font-weight: 300;
|
||||
border: 1px solid #a1a1a1;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-warning-error {
|
||||
border: 1px solid #EB4D5C;
|
||||
color: #EB4D5C;
|
||||
}
|
||||
|
||||
.card-warning-addtional-margin {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stripe-payment-btn {
|
||||
outline: none;
|
||||
width: auto;
|
||||
float: right;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
position: absolute;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.card-cvc-element label {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.card-element {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-element label {
|
||||
width: 100%;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.card-cvc-element .my-input {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#card-errors {
|
||||
clear: both;
|
||||
padding: 0 0 10px;
|
||||
color: #eb4d5c;
|
||||
}
|
||||
|
||||
.credit-card-goup {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.dcl-order-sec {
|
||||
padding: 10px 5px 30px;
|
||||
border-bottom: 4px solid #eee;
|
||||
}
|
||||
|
||||
.dcl-billing-sec {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dcl-billing {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.tbl-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total {
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-tot {
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
.tbl-content {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.dcl-order-table-header {
|
||||
border-bottom: 0px solid #eee;
|
||||
padding: 10px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dcl-order-table-content {
|
||||
border-bottom: 0px solid #eee;
|
||||
padding: 10px 0;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total {
|
||||
font-size: 18px;
|
||||
color: #000;
|
||||
padding: 10px 0;
|
||||
border-bottom: 0px solid #eee;
|
||||
}
|
||||
|
||||
.card-expiry-element {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.card-cvc-element {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#billing-form .form-control {
|
||||
box-shadow: none !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dcl-payment-grid {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dcl-payment-box {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
padding: 0 30px;
|
||||
}
|
||||
.dcl-payment-box:nth-child(2) {
|
||||
order: 1;
|
||||
}
|
||||
.dcl-payment-box:nth-child(4) {
|
||||
order: 2;
|
||||
}
|
||||
.dcl-payment-section {
|
||||
padding: 15px 10px;
|
||||
margin-bottom: 0;
|
||||
border-bottom-width: 5px;
|
||||
}
|
||||
.dcl-payment-box:nth-child(2n) .dcl-payment-section {
|
||||
border-bottom: none;
|
||||
}
|
||||
.dcl-payment-box:nth-child(1):after,
|
||||
.dcl-payment-box:nth-child(2):after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
background: #eee;
|
||||
width: 1px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
.dcl-billing {
|
||||
padding-right: 65px;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dcl-creditcard {
|
||||
padding-left: 65px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total,
|
||||
.dcl-order-table-total .tbl-tot {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tbl-header-center,
|
||||
.tbl-content-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tbl-header-right,
|
||||
.tbl-content-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.dcl-order-container {
|
||||
width: 990px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
/* Create VM calculator */
|
||||
|
||||
.price-calc-section {
|
||||
padding: 80px 40px !important;
|
||||
padding: 20px 0 !important;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
|
@ -40,19 +42,19 @@
|
|||
}
|
||||
|
||||
.price-calc-section .card {
|
||||
width: 50%;
|
||||
border-radius: 7px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
box-shadow: 1px 3px 6px 2px rgba(0, 0, 0, 0.2);
|
||||
padding-bottom: 30px;
|
||||
text-align: center;
|
||||
max-width: 320px;
|
||||
max-width: 4000px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.price-calc-section .card {
|
||||
margin-left: 0;
|
||||
/* margin-left: 0; */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +87,7 @@
|
|||
}
|
||||
|
||||
.price-calc-section .card .description {
|
||||
padding: 7px 8px 2px;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around !important;
|
||||
|
|
@ -93,7 +95,7 @@
|
|||
}
|
||||
|
||||
.price-calc-section .card .description span {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
margin-left: 5px;
|
||||
/* margin-left: 0px; */
|
||||
/* justify-self: start; */
|
||||
|
|
@ -104,17 +106,18 @@
|
|||
}
|
||||
|
||||
.price-calc-section .card .description .select-number{
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
width: 85px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.price-calc-section .card .description i {
|
||||
color: #29427a;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px 6px 3px;
|
||||
/* border: 1px solid #ccc; */
|
||||
/* padding: 5px 6px 3px; */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +196,7 @@
|
|||
.price-calc-section .help-block.with-errors {
|
||||
text-align: center;
|
||||
margin: 0 0;
|
||||
padding: 0 0 5px;
|
||||
padding: 0 0;
|
||||
}
|
||||
.price-calc-section .help-block.with-errors ul {
|
||||
margin-bottom: 0;
|
||||
|
|
@ -209,10 +212,10 @@
|
|||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 18%;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
height: 1px;
|
||||
width: 65%;
|
||||
width: 100%;
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.vm-vmid-with-warning {
|
||||
padding: 50px 0 33px !important;
|
||||
}
|
||||
|
||||
.vm-vmid .alert {
|
||||
margin-top: 15px;
|
||||
margin-bottom: -60px;
|
||||
|
|
@ -183,6 +187,13 @@
|
|||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.vm-terminate-warning {
|
||||
letter-spacing: 0.6px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #373636;
|
||||
}
|
||||
|
||||
.vm-contact-us {
|
||||
margin: 25px 0 30px;
|
||||
/* text-align: center; */
|
||||
|
|
@ -269,7 +280,7 @@
|
|||
border: 2px solid #A3C0E2;
|
||||
padding: 5px 25px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1.3px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.btn-vm-contact:hover, .btn-vm-contact:focus {
|
||||
background: #fff;
|
||||
|
|
|
|||
|
|
@ -153,4 +153,87 @@ $( document ).ready(function() {
|
|||
$('.navbar-fixed-top.topnav').css('padding-right', topnavPadding-scrollbarWidth);
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------------------------------------------
|
||||
Scripts initialization
|
||||
--------------------------------------------- */
|
||||
var cardPricing = {
|
||||
'cpu': {
|
||||
'id': 'coreValue',
|
||||
'value': 1,
|
||||
'min': 1,
|
||||
'max': 48,
|
||||
'interval': 1
|
||||
},
|
||||
'ram': {
|
||||
'id': 'ramValue',
|
||||
'value': 2,
|
||||
'min': 1,
|
||||
'max': 200,
|
||||
'interval': 1
|
||||
},
|
||||
'storage': {
|
||||
'id': 'storageValue',
|
||||
'value': 10,
|
||||
'min': 10,
|
||||
'max': 2000,
|
||||
'interval': 10
|
||||
}
|
||||
};
|
||||
|
||||
function _initPricing() {
|
||||
_fetchPricing();
|
||||
|
||||
$('.fa-minus-circle.left').click(function(event) {
|
||||
var data = $(this).data('minus');
|
||||
|
||||
if (cardPricing[data].value > cardPricing[data].min) {
|
||||
cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval;
|
||||
}
|
||||
_fetchPricing();
|
||||
});
|
||||
$('.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;
|
||||
}
|
||||
_fetchPricing();
|
||||
});
|
||||
|
||||
$('.input-price').change(function() {
|
||||
var data = $(this).attr("name");
|
||||
cardPricing[data].value = $('input[name=' + data + ']').val();
|
||||
_fetchPricing();
|
||||
});
|
||||
}
|
||||
|
||||
function _fetchPricing() {
|
||||
Object.keys(cardPricing).map(function(element) {
|
||||
$('input[name=' + element + ']').val(cardPricing[element].value);
|
||||
});
|
||||
_calcPricing();
|
||||
}
|
||||
|
||||
function _calcPricing() {
|
||||
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);
|
||||
}
|
||||
|
||||
_initPricing();
|
||||
});
|
||||
|
|
@ -195,5 +195,11 @@ $(document).ready(function () {
|
|||
$(element).closest('.form-group').append(error);
|
||||
}
|
||||
});
|
||||
|
||||
$('.credit-card-info .btn.choice-btn').click(function(){
|
||||
var id = this.dataset['id_card'];
|
||||
$('#id_card').val(id);
|
||||
$('#billing-form').submit();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="author" content="ungleich glarus ag">
|
||||
|
||||
<title>{{ domain }} - {{ hosting }} hosting as easy as possible</title>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% load staticfiles bootstrap3%}
|
||||
{% load i18n %}
|
||||
{% load staticfiles i18n cms_tags sekizai_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
|
|
@ -29,6 +29,9 @@
|
|||
{% block css_extra %}
|
||||
{% endblock css_extra %}
|
||||
|
||||
{% render_block "css" postprocessor "compressor.contrib.sekizai.compress" %}
|
||||
{% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %}
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href='//fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
|
||||
<link href="{% static 'datacenterlight/font-awesome/css/font-awesome.min.css' %}" rel="stylesheet" type="text/css">
|
||||
|
|
@ -48,7 +51,7 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% cms_toolbar %}
|
||||
|
||||
{% block navbar %}
|
||||
{% include "hosting/includes/_navbar_user.html" %}
|
||||
|
|
@ -63,7 +66,7 @@
|
|||
{% if request.user.is_authenticated %}
|
||||
<footer class="footer-vm">
|
||||
<div class="container">
|
||||
<p class="copyright text-muted small">Copyright © ungleich GmbH {% now "Y" %}. {% trans "All Rights Reserved" %}</p>
|
||||
<p class="copyright text-muted small">Copyright © ungleich glarus ag {% now "Y" %}. {% trans "All Rights Reserved" %}</p>
|
||||
</div>
|
||||
</footer>
|
||||
{% else %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 i18n %}
|
||||
{% load staticfiles bootstrap3 i18n cms_tags %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="dashboard-container create-vm-container">
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
|
|
@ -17,14 +19,8 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="price-calc-section no-padding">
|
||||
<div class="landing card">
|
||||
<div class="caption">
|
||||
{% include "hosting/calculator_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 hosting-calculator">
|
||||
{% render_placeholder cms_integration.calculator_placeholder %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ base_url }}{% static 'datacenterlight/img/logo_black.png' %}" style="width: 200px; height: 50px;">
|
||||
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{base_url}}{% static 'datacenterlight/img/logo_black.png' %}" style="width: 200px; height: 50px;">
|
||||
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ base_url }}{% static 'datacenterlight/img/logo_black.png' %}" style="width: 200px; height: 50px;">
|
||||
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
<center style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;">
|
||||
<table cellpadding="0" cellspacing="0" width="600" class="w320" style="border-collapse: collapse !important; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;"><tr style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;">
|
||||
<td class="pull-left mobile-header-padding-left" style="vertical-align: middle; border-collapse: collapse; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; font-size: 14px; color: #777777; text-align: left; line-height: 21px; width: 290px; padding-left: 10px;" align="left" valign="middle">
|
||||
<a href="{{base_url}}" style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; color: #676767; text-decoration: none !important;"><img width="137" src="{{base_url}}{% static "hosting/img/logo_black.png" %}" alt="logo" style="max-width: 600px; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; border: none;"></a>
|
||||
<a href="{{base_url}}" style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; color: #676767; text-decoration: none !important;"><img width="137" src="{{base_url}}{% static 'hosting/img/datacenterlight.png' %}" alt="logo" style="max-width: 600px; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; border: none;"></a>
|
||||
</td>
|
||||
<td class="pull-right mobile-header-padding-right" style="color: #4d4d4d; border-collapse: collapse; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; font-size: 14px; text-align: right; line-height: 21px; width: 290px; padding-left: 10px;" align="right">
|
||||
</td>
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
</tr>
|
||||
<tr style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;">
|
||||
<td class="free-text" style="border-collapse: collapse; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; font-size: 14px; color: #777777; text-align: center; line-height: 21px; width: 100% !important; padding: 10px 60px 0px;" align="center">
|
||||
Your virtual machine {{vm.name}} subscription has been charged, <br/> you can view your invoice clicking on the button below.
|
||||
Your virtual machine {{vm.name}} subscription has been charged, <br/> you can view your invoice clicking on the button below.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;">
|
||||
|
|
|
|||
50
hosting/templates/hosting/includes/_card_input.html
Normal file
50
hosting/templates/hosting/includes/_card_input.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{% load i18n %}
|
||||
|
||||
<form action="" id="payment-form-new" method="POST">
|
||||
<input type="hidden" name="token"/>
|
||||
<input type="hidden" name="id_card" id="id_card" value=""/>
|
||||
<div class="group">
|
||||
<div class="credit-card-goup">
|
||||
<div class="card-element card-number-element">
|
||||
<label>{%trans "Card Number" %}</label>
|
||||
<div id="card-number-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-5 card-element card-expiry-element">
|
||||
<label>{%trans "Expiry Date" %}</label>
|
||||
<div id="card-expiry-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element">
|
||||
<label>{%trans "CVC" %}</label>
|
||||
<div id="card-cvc-element" class="field my-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-element brand">
|
||||
<label>{%trans "Card Type" %}</label>
|
||||
<i class="pf pf-credit-card" id="brand-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="card-errors"></div>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content">
|
||||
{% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li><p class="card-warning-content card-warning-error">{{ message|safe }}</p></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-vm-contact btn-wide" type="submit" name="payment-form">{%trans "SUBMIT" %}</button>
|
||||
</div>
|
||||
|
||||
<div style="display:none;">
|
||||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
<a href="#contact">Contact</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="copyright text-muted small">Copyright © ungleich GmbH {% now "Y" %}. {% trans "All Rights Reserved" %}</p>
|
||||
<p class="copyright text-muted small">Copyright © ungleich glarus ag {% now "Y" %}. {% trans "All Rights Reserved" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
{% load staticfiles bootstrap3 humanize i18n custom_tags %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div id="order-detail{{order.pk}}" class="order-detail-container">
|
||||
|
|
@ -115,27 +114,58 @@
|
|||
<p>
|
||||
<span>{% trans "Cores" %}: </span>
|
||||
{% if vm.cores %}
|
||||
<span class="pull-right">{{vm.cores|floatformat}}</span>
|
||||
<strong class="pull-right">{{vm.cores|floatformat}}</strong>
|
||||
{% else %}
|
||||
<span class="pull-right">{{vm.cpu|floatformat}}</span>
|
||||
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Memory" %}: </span>
|
||||
<span class="pull-right">{{vm.memory}} GB</span>
|
||||
<strong class="pull-right">{{vm.memory}} GB</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Disk space" %}: </span>
|
||||
<span class="pull-right">{{vm.disk_size}} GB</span>
|
||||
<strong class="pull-right">{{vm.disk_size}} GB</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Total" %}</span>
|
||||
<span class="pull-right">{{vm.price}} CHF</span>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% if vm.vat > 0 or vm.discount.amount > 0 %}
|
||||
<div class="col-sm-6">
|
||||
<div class="subtotal-price">
|
||||
{% if vm.vat > 0 %}
|
||||
<p>
|
||||
<strong>{% trans "Subtotal" %} </strong>
|
||||
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
<p>
|
||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
|
||||
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if vm.discount.amount > 0 %}
|
||||
<p class="text-primary">
|
||||
{%trans "Discount" as discount_name %}
|
||||
<strong>{{ vm.discount.name|default:discount_name }} </strong>
|
||||
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6">
|
||||
<p class="total-price">
|
||||
<strong>{% trans "Total" %} </strong>
|
||||
<strong class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% if not order %}
|
||||
{% block submit_btn %}
|
||||
|
|
@ -143,7 +173,7 @@
|
|||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.</div>
|
||||
<div class="dcl-place-order-text">{% 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 %}.</div>
|
||||
</div>
|
||||
<div class="col-sm-4 order-confirm-btn text-right">
|
||||
<button class="btn choice-btn" id="btn-create-vm" data-href="{% url 'hosting:order-confirmation' %}" data-toggle="modal" data-target="#createvm-modal">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 %}
|
||||
{% load i18n l10n %}
|
||||
{% load staticfiles bootstrap3 humanize i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard-container">
|
||||
|
|
@ -30,7 +29,7 @@
|
|||
<tr>
|
||||
<td class="xs-td-inline" data-header="{% trans 'Order Nr.' %}">{{ order.id }}</td>
|
||||
<td class="xs-td-bighalf" data-header="{% trans 'Date' %}">{{ order.created_at | date:"M d, Y H:i" }}</td>
|
||||
<td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|unlocalize }}</td>
|
||||
<td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|floatformat:2|intcomma }}</td>
|
||||
<td class="text-right last-td">
|
||||
<a class="btn btn-order-detail" href="{% url 'hosting:orders' order.pk %}">{% trans 'See Invoice' %}</a>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 i18n %}
|
||||
{% load staticfiles bootstrap3 i18n humanize %}
|
||||
|
||||
{% block css_extra %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/paymentfont/1.1.2/css/paymentfont.min.css"/>
|
||||
|
|
@ -9,161 +9,161 @@
|
|||
<!-- Credit card form -->
|
||||
<div class="dcl-order-container">
|
||||
<div class="payment-container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-order-sec">
|
||||
<h3><strong>{%trans "Your Order" %}</strong></h3>
|
||||
<div class="col-xs-6 col-sm-12 col-md-12 col-lg-12 dcl-order-table-header">
|
||||
<div class="col-xs-12 col-sm-2 col-md-1 col-lg-1 tbl-header">
|
||||
{%trans "Cores" %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-4 col-lg-4 tbl-header">
|
||||
{%trans "Memory" %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-3 col-lg-3 tbl-header">
|
||||
{%trans "Disk space" %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-header">
|
||||
{%trans "Configuration" %}
|
||||
<div class="dcl-order-sec">
|
||||
<h3><strong>{%trans "Your Order" %}</strong></h3>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-12">
|
||||
<div class="dcl-order-table-header">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<div class="tbl-header">
|
||||
{%trans "Cores" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="tbl-header tbl-header-center">
|
||||
{%trans "Memory" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="tbl-header tbl-header-center">
|
||||
{%trans "Disk space" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="tbl-header tbl-header-right">
|
||||
{%trans "Configuration" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-12 col-md-12 col-lg-12 dcl-order-table-content">
|
||||
<div class="col-xs-12 col-sm-2 col-md-1 col-lg-1 tbl-content">
|
||||
{{request.session.specs.cpu|floatformat}}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-4 col-lg-4 tbl-content">
|
||||
{{request.session.specs.memory|floatformat}} GB
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-3 col-lg-3 tbl-content">
|
||||
{{request.session.specs.disk_size|floatformat}} GB
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-content">
|
||||
{{request.session.template.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-order-table-total">
|
||||
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-tot tbl-no-padding">
|
||||
{%trans "Total" %} <span>{%trans "including VAT" %}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-no-padding">
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4"></div>
|
||||
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 tbl-total">{{request.session.specs.price}}
|
||||
CHF<span class="dcl-price-month">/{% trans "Month" %}</span>
|
||||
<div class="col-xs-6 col-sm-12">
|
||||
<div class="dcl-order-table-content">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<div class="tbl-content">
|
||||
{{request.session.specs.cpu|floatformat}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="tbl-content tbl-content-center">
|
||||
{{request.session.specs.memory|floatformat}} GB
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="tbl-content tbl-content-center">
|
||||
{{request.session.specs.disk_size|floatformat|intcomma}} GB
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="tbl-content tbl-content-right">
|
||||
{{request.session.template.name}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dcl-order-table-total">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="tbl-tot">
|
||||
{%trans "Total" %}
|
||||
<span>{% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<div class="tbl-total">
|
||||
{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if vm_pricing.discount_amount %}
|
||||
<hr class="thin-hr">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="tbl-tot">
|
||||
{%trans "Discount" as discount_name %}
|
||||
{{ vm_pricing.discount_name|default:discount_name }} <br>
|
||||
<span>({% trans "Will be applied at checkout" %})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<div class="tbl-total">
|
||||
<div class="text-primary">- {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-billing-sec">
|
||||
<div class="col-xs-12 col-sm-5 col-md-6 billing dcl-billing">
|
||||
<h3><b>{%trans "Billing Address"%}</b></h3>
|
||||
<hr>
|
||||
<form role="form" id="billing-form" method="post" action="" novalidate>
|
||||
{% for field in form %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field field show_label=False type='fields'%}
|
||||
{% endfor %}
|
||||
</form>
|
||||
<div class="dcl-billing-sec">
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-6">
|
||||
<div class="billing dcl-billing">
|
||||
<h3><b>{%trans "Billing Address"%}</b></h3>
|
||||
<hr>
|
||||
<form role="form" id="billing-form" method="post" action="" novalidate>
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% bootstrap_field field show_label=False type='fields'%}
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-7 col-md-6 creditcard-box dcl-creditcard">
|
||||
{% with card_list_len=cards_list|length %}
|
||||
<h3><b>{%trans "Credit Card"%}</b></h3>
|
||||
<hr>
|
||||
<div>
|
||||
<p>
|
||||
{% 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 <a href="https://stripe.com" target="_blank">Stripe</a> 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 <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div>
|
||||
{% if credit_card_data.last4 %}
|
||||
<form role="form" id="payment-form-with-creditcard" novalidate>
|
||||
<h5 class="billing-head">Credit Card</h5>
|
||||
<h5 class="membership-lead">Last 4: *****{{credit_card_data.last4}}</h5>
|
||||
<h5 class="membership-lead">Type: {{credit_card_data.cc_brand}}</h5>
|
||||
<input type="hidden" name="credit_card_needed" value="false"/>
|
||||
</form>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-addtional-margin">
|
||||
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% for card in cards_list %}
|
||||
<div class="credit-card-info">
|
||||
<div class="col-xs-6 no-padding">
|
||||
<h5 class="billing-head">{% trans "Credit Card" %}</h5>
|
||||
<h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right align-bottom">
|
||||
<a class="btn choice-btn choice-btn-faded" href="#" data-id_card="{{card.id}}">{% trans "SELECT" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button id="payment_button_with_creditcard" class="btn btn-vm-contact" type="submit">{%trans "SUBMIT" %}</button>
|
||||
{% endfor %}
|
||||
{% if card_list_len > 0 %}
|
||||
<div class="new-card-head">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<h4>{% trans "Add a new credit card" %}</h4>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<button data-toggle="collapse" data-target="#newcard" class="btn choice-btn">
|
||||
<span class="fa fa-plus"></span> {% trans "NEW CARD" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<form action="" id="payment-form-new" method="POST">
|
||||
<input type="hidden" name="token"/>
|
||||
<div class="group">
|
||||
<div class="credit-card-goup">
|
||||
<div class="card-element card-number-element">
|
||||
<label>{%trans "Card Number" %}</label>
|
||||
<div id="card-number-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-5 card-element card-expiry-element">
|
||||
<label>{%trans "Expiry Date" %}</label>
|
||||
<div id="card-expiry-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element">
|
||||
<label>{%trans "CVC" %}</label>
|
||||
<div id="card-cvc-element" class="field my-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-element brand">
|
||||
<label>{%trans "Card Type" %}</label>
|
||||
<i class="pf pf-credit-card" id="brand-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newcard" class="collapse">
|
||||
<hr class="thick-hr">
|
||||
<div class="card-details-box">
|
||||
<h3>{%trans "New Credit Card" %}</h3>
|
||||
<hr>
|
||||
{% include "hosting/includes/_card_input.html" %}
|
||||
</div>
|
||||
<div id="card-errors"></div>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content">
|
||||
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-vm-contact btn-wide" type="submit">{%trans "SUBMIT" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:none;">
|
||||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% else%}
|
||||
{% include "hosting/includes/_card_input.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
{% comment "Looks as if no more used. To test..." %}
|
||||
{% if credit_card_data.last4 and credit_card_data.cc_brand %}
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
|
|
@ -191,5 +191,5 @@
|
|||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
{% endcomment %}
|
||||
{%endblock%}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="dashboard-container wide">
|
||||
{% include 'hosting/includes/_messages.html' %}
|
||||
<div class="dashboard-container-head">
|
||||
<h1 class="dashboard-title-thin"><img src="{% static 'hosting/img/dashboard_settings.svg' %}" class="un-icon wide"> {% trans "My Settings" %}</h1>
|
||||
</div>
|
||||
|
|
@ -14,7 +15,7 @@
|
|||
<div class="settings-container">
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-6 billing dcl-billing">
|
||||
<h3>{%trans "Billing Address"%}</h3>
|
||||
<h3>{%trans "Billing Address" %}</h3>
|
||||
<hr>
|
||||
<form role="form" id="billing-form" method="post" action="" novalidate>
|
||||
{% for field in form %}
|
||||
|
|
@ -22,108 +23,97 @@
|
|||
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
|
||||
{% endfor %}
|
||||
<div class="form-group text-right">
|
||||
<button type="submit" class="btn btn-vm-contact btn-wide">{% trans "UPDATE" %}</button>
|
||||
<button type="submit" class="btn btn-vm-contact btn-wide" name="billing-form">{% trans "UPDATE" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-6 creditcard-box dcl-creditcard">
|
||||
<h3>{%trans "Credit Card"%}</h3>
|
||||
<h3>{%trans "Credit Card" %}</h3>
|
||||
<hr>
|
||||
<div>
|
||||
{% if credit_card_data.last4 %}
|
||||
{% with card_list_len=cards_list|length %}
|
||||
{% for card in cards_list %}
|
||||
<div class="credit-card-details">
|
||||
<h5 class="billing-head">{% trans "Credit Card" %}</h5>
|
||||
<h5 class="membership-lead">{% trans "Last" %} 4: *****{{credit_card_data.last4}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Type" %}: {{credit_card_data.cc_brand}}</h5>
|
||||
{% comment %}
|
||||
<h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
|
||||
<div class="credit-card-details-opt">
|
||||
<div class="row">
|
||||
{% if card_list_len > 1 %}
|
||||
<div class="col-xs-6">
|
||||
<a class="caps-link" href=""><img src="{% static 'hosting/img/delete.svg' %}" class="svg-img">{% trans "REMOVE CARD" %}</a>
|
||||
<a class="caps-link" href="" data-toggle="modal" data-target="#Modal{{ card.id }}"><img src="{% static 'hosting/img/delete.svg' %}" class="svg-img">{% trans "REMOVE CARD" %}</a>
|
||||
<div class="modal fade" id="Modal{{card.id }}" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Confirm"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-icon"><i class="fa fa-trash" aria-hidden="true"></i></div>
|
||||
<h4 class="modal-title" id="ModalLabel">{% trans "Remove Card"%}</h4>
|
||||
<div class="modal-text">
|
||||
<p>{% trans "Do you want to remove this associated card?"%}</p>
|
||||
</div>
|
||||
<form method="post" action="{% url 'hosting:delete_card' card.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-wide" name="delete_card">{% trans "Delete"%}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-xs-6 text-right">
|
||||
<a class="btn btn-vm-contact" href="">{% trans "EDIT CARD" %}</a>
|
||||
{% if card.preferred %}
|
||||
{% trans "DEFAULT" %}
|
||||
{% else %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="card" value="{{card.id}}">
|
||||
<a class="btn choice-btn choice-btn-faded" href="#" onclick="$(this).closest('form').submit()">{% trans "SELECT" %}</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% empty %}
|
||||
<div class="no-cards">
|
||||
<h4>{% trans "No Credit Cards Added" %}</h4>
|
||||
<p>{% blocktrans %}We are using <a href="https://stripe.com">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{% comment %}
|
||||
<h4>{% trans "Add a new Card." %}</h4>
|
||||
<p style="margin-bottom: 15px;">
|
||||
{% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
</p>
|
||||
<form action="" id="payment-form-new" class="credit-card-form" method="POST">
|
||||
<input type="hidden" name="token"/>
|
||||
<div class="credit-card-goup">
|
||||
<div class="card-element card-number-element">
|
||||
<label>{%trans "Card Number" %}</label>
|
||||
<div id="card-number-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-4 card-element card-expiry-element">
|
||||
<label>{%trans "Expiry Date" %}</label>
|
||||
<div id="card-expiry-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-4 col-sm-offset-4 card-element card-cvc-element">
|
||||
<label>{%trans "CVC" %}</label>
|
||||
<div id="card-cvc-element" class="field my-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-element brand">
|
||||
<label>{%trans "Card Type" %}</label>
|
||||
<i class="pf pf-credit-card" id="brand-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="card-errors" role="alert"></div>
|
||||
<div>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content">
|
||||
{% blocktrans %}You are not making any payment here.{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled"><li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li></ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-xs-offset-6 text-right">
|
||||
<button class="btn btn-success stripe-payment-btn" type="submit">{%trans "Submit" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:none;">
|
||||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</form>
|
||||
{% endcomment %}
|
||||
{% endif %}
|
||||
<div class="new-card-head">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<h4>{% trans "Add a new credit card" %}</h4>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<button data-toggle="collapse" data-target="#newcard" class="btn choice-btn">
|
||||
<span class="fa fa-plus"></span> {% trans "NEW CARD" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newcard" class="collapse">
|
||||
<hr class="thick-hr">
|
||||
<div class="card-details-box">
|
||||
<h3>{%trans "New Credit Card" %}</h3>
|
||||
<hr>
|
||||
{% include "hosting/includes/_card_input.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
<!-- stripe key data -->
|
||||
{% if stripe_key %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
|
@ -137,13 +127,4 @@
|
|||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
{% if credit_card_data.last4 and credit_card_data.cc_brand %}
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
window.hasCreditcard = true;
|
||||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
{% endcomment %}
|
||||
{%endblock%}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 %}
|
||||
{% load staticfiles bootstrap3 humanize %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<div class="vm-detail-config">
|
||||
<p><span>{% trans "Cores" %}:</span><span class="value">{{virtual_machine.cores}}</span></p>
|
||||
<p><span>{% trans "Memory" %}:</span><span class="value">{{virtual_machine.memory}} GB</span></p>
|
||||
<p><span>{% trans "Disk" %}:</span><span class="value">{{virtual_machine.disk_size|floatformat:2}} GB</span></p>
|
||||
<p><span>{% trans "Disk" %}:</span><span class="value">{{virtual_machine.disk_size|floatformat:2|intcomma}} GB</span></p>
|
||||
<p><span>{% trans "Configuration" %}:</span><span class="value">{{virtual_machine.configuration}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -45,13 +45,13 @@
|
|||
<h2 class="vm-detail-title">{% trans "Billing" %} <img src="{% static 'hosting/img/billing.svg' %}" class="un-icon"></h2>
|
||||
<div class="vm-vmid">
|
||||
<div class="vm-item-subtitle">{% trans "Current Pricing" %}</div>
|
||||
<div class="vm-item-lg">{{virtual_machine.price|floatformat}} CHF/{% trans "Month" %}</div>
|
||||
<div class="vm-item-lg">{{order.price|floatformat:2|intcomma}} CHF/{% trans "Month" %}</div>
|
||||
<a class="btn btn-vm-invoice" href="{% url 'hosting:orders' order.pk %}">{% trans "See Invoice" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-detail-item">
|
||||
<h2 class="vm-detail-title">{% trans "Status" %} <img src="{% static 'hosting/img/connected.svg' %}" class="un-icon"></h2>
|
||||
<div class="vm-vmid">
|
||||
<div class="vm-vmid vm-vmid-with-warning">
|
||||
<div class="vm-item-subtitle">{% trans "Your VM is" %}</div>
|
||||
<div id="terminate-VM" data-alt="{% trans 'Terminating' %}">
|
||||
{% if virtual_machine.state == 'PENDING' %}
|
||||
|
|
@ -74,6 +74,10 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-terminate-warning text-center">
|
||||
<p>{% trans "Attention:" %}</p>
|
||||
<p>{% trans "terminating VM can not be reverted." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-contact-us">
|
||||
|
|
@ -105,7 +109,7 @@
|
|||
<div class="modal-icon"><i class="fa fa-ban" aria-hidden="true"></i></div>
|
||||
<h4 class="modal-title" id="ModalLabel">{% trans "Terminate your Virtual Machine" %}</h4>
|
||||
<div class="modal-text">
|
||||
<p>{% trans "Do you want to cancel your Virtual Machine" %} ?</p>
|
||||
<p>{% trans "Terminated VMs can not be revived and will not be refunded. Do you want to terminate your VM?" %}</p>
|
||||
<p><strong>{{virtual_machine.name}}</strong></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ urlpatterns = [
|
|||
name='choice_ssh_keys'),
|
||||
url(r'delete_ssh_key/(?P<pk>\d+)/?$', SSHKeyDeleteView.as_view(),
|
||||
name='delete_ssh_key'),
|
||||
url(r'delete_card/(?P<pk>\d+)/?$', SettingsView.as_view(),
|
||||
name='delete_card'),
|
||||
url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
|
||||
name='create_ssh_key'),
|
||||
url(r'^notifications/$', NotificationsView.as_view(),
|
||||
|
|
|
|||
497
hosting/views.py
497
hosting/views.py
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
|
@ -12,13 +11,16 @@ from django.contrib.auth.tokens import default_token_generator
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse_lazy, reverse
|
||||
from django.http import Http404, HttpResponseRedirect, HttpResponse
|
||||
from django.http import (
|
||||
Http404, HttpResponseRedirect, HttpResponse, JsonResponse
|
||||
)
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import (
|
||||
View, CreateView, FormView, ListView, DetailView, DeleteView,
|
||||
|
|
@ -30,8 +32,9 @@ from stored_messages.api import mark_read
|
|||
from stored_messages.models import Message
|
||||
from stored_messages.settings import stored_messages_settings
|
||||
|
||||
from datacenterlight.models import VMTemplate
|
||||
from datacenterlight.tasks import create_vm_task
|
||||
from datacenterlight.models import VMTemplate, VMPricing
|
||||
from datacenterlight.utils import create_vm, get_cms_integration
|
||||
from hosting.models import UserCardDetail
|
||||
from membership.models import CustomUser, StripeCustomer
|
||||
from opennebula_api.models import OpenNebulaManager
|
||||
from opennebula_api.serializers import (
|
||||
|
|
@ -42,7 +45,7 @@ from utils.forms import (
|
|||
BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm,
|
||||
ResendActivationEmailForm
|
||||
)
|
||||
from utils.hosting_utils import get_vm_price
|
||||
from utils.hosting_utils import get_vm_price_with_vat, HostingUtils
|
||||
from utils.mailer import BaseEmail
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from utils.tasks import send_plain_email_task
|
||||
|
|
@ -54,7 +57,7 @@ from .forms import (
|
|||
HostingUserSignupForm, HostingUserLoginForm, UserHostingKeyForm,
|
||||
generate_ssh_key_name
|
||||
)
|
||||
from .mixins import ProcessVMSelectionMixin
|
||||
from .mixins import ProcessVMSelectionMixin, HostingContextMixin
|
||||
from .models import (
|
||||
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail
|
||||
)
|
||||
|
|
@ -213,13 +216,13 @@ class IndexView(View):
|
|||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class LoginView(LoginViewMixin):
|
||||
class LoginView(HostingContextMixin, LoginViewMixin):
|
||||
template_name = "hosting/login.html"
|
||||
form_class = HostingUserLoginForm
|
||||
success_url = reverse_lazy('hosting:dashboard')
|
||||
|
||||
|
||||
class SignupView(CreateView):
|
||||
class SignupView(HostingContextMixin, CreateView):
|
||||
template_name = 'hosting/signup.html'
|
||||
form_class = HostingUserSignupForm
|
||||
model = CustomUser
|
||||
|
|
@ -248,7 +251,7 @@ class SignupView(CreateView):
|
|||
return super(SignupView, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class SignupValidateView(TemplateView):
|
||||
class SignupValidateView(HostingContextMixin, TemplateView):
|
||||
template_name = "hosting/signup_validate.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
|
@ -272,7 +275,7 @@ class SignupValidateView(TemplateView):
|
|||
return context
|
||||
|
||||
|
||||
class SignupValidatedView(SignupValidateView):
|
||||
class SignupValidatedView(SignupValidateView, HostingContextMixin):
|
||||
template_name = "hosting/signup_validate.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
|
@ -307,7 +310,7 @@ class SignupValidatedView(SignupValidateView):
|
|||
email.send()
|
||||
else:
|
||||
home_url = '<a href="' + \
|
||||
reverse('datacenterlight:index') + \
|
||||
reverse('datacenterlight:cms_index') + \
|
||||
'">Data Center Light</a>'
|
||||
message = '{sorry_message} <br />{go_back_to} {hurl}'.format(
|
||||
sorry_message=_("Sorry. Your request is invalid."),
|
||||
|
|
@ -325,7 +328,8 @@ class SignupValidatedView(SignupValidateView):
|
|||
return super(SignupValidatedView, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ResendActivationEmailView(ResendActivationLinkViewMixin):
|
||||
class ResendActivationEmailView(HostingContextMixin,
|
||||
ResendActivationLinkViewMixin):
|
||||
template_name = 'hosting/resend_activation_link.html'
|
||||
form_class = ResendActivationEmailForm
|
||||
success_url = reverse_lazy('hosting:login')
|
||||
|
|
@ -333,7 +337,7 @@ class ResendActivationEmailView(ResendActivationLinkViewMixin):
|
|||
email_template_name = 'user_activation'
|
||||
|
||||
|
||||
class PasswordResetView(PasswordResetViewMixin):
|
||||
class PasswordResetView(HostingContextMixin, PasswordResetViewMixin):
|
||||
site = 'dcl'
|
||||
template_name = 'hosting/reset_password.html'
|
||||
form_class = PasswordResetRequestForm
|
||||
|
|
@ -341,7 +345,8 @@ class PasswordResetView(PasswordResetViewMixin):
|
|||
template_email_path = 'hosting/emails/'
|
||||
|
||||
|
||||
class PasswordResetConfirmView(PasswordResetConfirmViewMixin):
|
||||
class PasswordResetConfirmView(HostingContextMixin,
|
||||
PasswordResetConfirmViewMixin):
|
||||
template_name = 'hosting/confirm_reset_password.html'
|
||||
success_url = reverse_lazy('hosting:login')
|
||||
|
||||
|
|
@ -494,6 +499,7 @@ class SSHKeyChoiceView(LoginRequiredMixin, View):
|
|||
return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar')
|
||||
|
||||
|
||||
@method_decorator(decorators, name='dispatch')
|
||||
class SSHKeyCreateView(LoginRequiredMixin, FormView):
|
||||
form_class = UserHostingKeyForm
|
||||
model = UserHostingKey
|
||||
|
|
@ -545,11 +551,6 @@ class SSHKeyCreateView(LoginRequiredMixin, FormView):
|
|||
manager.manage_public_key([{'value': public_key, 'state': True}])
|
||||
return HttpResponseRedirect(self.success_url)
|
||||
|
||||
@method_decorator(decorators)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return render(request, self.template_name)
|
||||
|
||||
@method_decorator(decorators)
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
required = 'add_ssh' in self.request.POST
|
||||
|
|
@ -561,10 +562,12 @@ class SSHKeyCreateView(LoginRequiredMixin, FormView):
|
|||
return self.form_invalid(form)
|
||||
|
||||
|
||||
@method_decorator(decorators, name='dispatch')
|
||||
class SettingsView(LoginRequiredMixin, FormView):
|
||||
template_name = "hosting/settings.html"
|
||||
login_url = reverse_lazy('hosting:login')
|
||||
form_class = BillingAddressForm
|
||||
permission_required = ['view_usercarddetail']
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
"""
|
||||
|
|
@ -581,38 +584,124 @@ class SettingsView(LoginRequiredMixin, FormView):
|
|||
context = super(SettingsView, self).get_context_data(**kwargs)
|
||||
# Get user
|
||||
user = self.request.user
|
||||
# Get user last order
|
||||
last_hosting_order = HostingOrder.objects.filter(
|
||||
customer__user=user).last()
|
||||
# If user has already an hosting order, get the credit card data from
|
||||
# it
|
||||
if last_hosting_order:
|
||||
credit_card_data = last_hosting_order.get_cc_data()
|
||||
context.update({
|
||||
'credit_card_data': credit_card_data if credit_card_data else None,
|
||||
})
|
||||
stripe_customer = None
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
stripe_customer = user.stripecustomer
|
||||
cards_list = UserCardDetail.get_all_cards_list(
|
||||
stripe_customer=stripe_customer
|
||||
)
|
||||
context.update({
|
||||
'cards_list': cards_list,
|
||||
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
@method_decorator(decorators)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return render(request, self.template_name)
|
||||
|
||||
@method_decorator(decorators)
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'card' in request.POST and request.POST['card'] is not '':
|
||||
card_id = escape(request.POST['card'])
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
UserCardDetail.set_default_card(
|
||||
stripe_api_cus_id=request.user.stripecustomer.stripe_id,
|
||||
stripe_source_id=user_card_detail.card_id
|
||||
)
|
||||
msg = _(
|
||||
("Your {brand} card ending in {last4} set as "
|
||||
"default card").format(
|
||||
brand=user_card_detail.brand,
|
||||
last4=user_card_detail.last4
|
||||
)
|
||||
)
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
|
||||
if 'delete_card' in request.POST:
|
||||
try:
|
||||
card = UserCardDetail.objects.get(pk=self.kwargs.get('pk'))
|
||||
if (request.user.has_perm(self.permission_required[0], card)
|
||||
and
|
||||
request.user
|
||||
.stripecustomer
|
||||
.usercarddetail_set
|
||||
.count() > 1):
|
||||
if card.card_id is not None:
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_utils.dissociate_customer_card(
|
||||
request.user.stripecustomer.stripe_id,
|
||||
card.card_id
|
||||
)
|
||||
if card.preferred:
|
||||
UserCardDetail.set_default_card_from_stripe(
|
||||
request.user.stripecustomer.stripe_id
|
||||
)
|
||||
card.delete()
|
||||
msg = _("Card deassociation successful")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
else:
|
||||
msg = _("You are not permitted to do this operation")
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
except UserCardDetail.DoesNotExist:
|
||||
msg = _("The selected card does not exist")
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
billing_address_data = form.cleaned_data
|
||||
billing_address_data.update({
|
||||
'user': self.request.user.id
|
||||
})
|
||||
billing_address_user_form = UserBillingAddressForm(
|
||||
instance=self.request.user.billing_addresses.first(),
|
||||
data=billing_address_data)
|
||||
billing_address_user_form.save()
|
||||
if 'billing-form' in request.POST:
|
||||
billing_address_data = form.cleaned_data
|
||||
billing_address_data.update({
|
||||
'user': self.request.user.id
|
||||
})
|
||||
billing_address_user_form = UserBillingAddressForm(
|
||||
instance=self.request.user.billing_addresses.first(),
|
||||
data=billing_address_data)
|
||||
billing_address_user_form.save()
|
||||
msg = _("Billing address updated successfully")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
else:
|
||||
token = form.cleaned_data.get('token')
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
token
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
form.add_error("__all__", card_details.get('error'))
|
||||
return self.render_to_response(self.get_context_data())
|
||||
stripe_customer = StripeCustomer.get_or_create(
|
||||
email=request.user.email, token=token
|
||||
)
|
||||
card = card_details['response_object']
|
||||
if UserCardDetail.get_user_card_details(stripe_customer, card):
|
||||
msg = _('You seem to have already added this card')
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
else:
|
||||
acc_result = stripe_utils.associate_customer_card(
|
||||
request.user.stripecustomer.stripe_id, token
|
||||
)
|
||||
if acc_result['response_object'] is None:
|
||||
msg = _(
|
||||
'An error occurred while associating the card.'
|
||||
' Details: {details}'.format(
|
||||
details=acc_result['error']
|
||||
)
|
||||
)
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
preferred = False
|
||||
if stripe_customer.usercarddetail_set.count() == 0:
|
||||
preferred = True
|
||||
UserCardDetail.create(
|
||||
stripe_customer=stripe_customer,
|
||||
last4=card['last4'],
|
||||
brand=card['brand'],
|
||||
fingerprint=card['fingerprint'],
|
||||
exp_month=card['exp_month'],
|
||||
exp_year=card['exp_year'],
|
||||
card_id=card['card_id'],
|
||||
preferred=preferred
|
||||
)
|
||||
msg = _(
|
||||
"Successfully associated the card with your account"
|
||||
)
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
else:
|
||||
billing_address_data = form.cleaned_data
|
||||
|
|
@ -645,21 +734,19 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
context = super(PaymentVMView, self).get_context_data(**kwargs)
|
||||
# Get user
|
||||
user = self.request.user
|
||||
|
||||
# Get user last order
|
||||
last_hosting_order = HostingOrder.objects.filter(
|
||||
customer__user=user).last()
|
||||
|
||||
# If user has already an hosting order, get the credit card data from
|
||||
# it
|
||||
if last_hosting_order:
|
||||
credit_card_data = last_hosting_order.get_cc_data()
|
||||
context.update({
|
||||
'credit_card_data': credit_card_data if credit_card_data else None,
|
||||
})
|
||||
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
stripe_customer = user.stripecustomer
|
||||
else:
|
||||
stripe_customer = None
|
||||
cards_list = UserCardDetail.get_all_cards_list(
|
||||
stripe_customer=stripe_customer
|
||||
)
|
||||
context.update({
|
||||
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
|
||||
'stripe_key': settings.STRIPE_API_PUBLIC_KEY,
|
||||
'vm_pricing': VMPricing.get_vm_pricing_by_name(
|
||||
self.request.session.get('specs', {}).get('pricing_name')
|
||||
),
|
||||
'cards_list': cards_list,
|
||||
})
|
||||
|
||||
return context
|
||||
|
|
@ -668,6 +755,10 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
def get(self, request, *args, **kwargs):
|
||||
if 'next' in request.session:
|
||||
del request.session['next']
|
||||
HostingUtils.clear_items_from_list(
|
||||
request.session,
|
||||
['token', 'card_id', 'customer', 'user']
|
||||
)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
@method_decorator(decorators)
|
||||
|
|
@ -678,23 +769,51 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
billing_address_data = form.cleaned_data
|
||||
token = form.cleaned_data.get('token')
|
||||
owner = self.request.user
|
||||
# Get or create stripe customer
|
||||
customer = StripeCustomer.get_or_create(email=owner.email,
|
||||
token=token)
|
||||
if not customer:
|
||||
msg = _("Invalid credit card")
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg,
|
||||
extra_tags='make_charge_error')
|
||||
return HttpResponseRedirect(
|
||||
reverse('hosting:payment') + '#payment_error')
|
||||
|
||||
if token is '':
|
||||
card_id = form.cleaned_data.get('card')
|
||||
customer = owner.stripecustomer
|
||||
try:
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
if not request.user.has_perm(
|
||||
'view_usercarddetail', user_card_detail
|
||||
):
|
||||
raise UserCardDetail.DoesNotExist(
|
||||
_("{user} does not have permission to access the "
|
||||
"card").format(user=request.user.email)
|
||||
)
|
||||
except UserCardDetail.DoesNotExist as e:
|
||||
ex = str(e)
|
||||
logger.error("Card Id: {card_id}, Exception: {ex}".format(
|
||||
card_id=card_id, ex=ex
|
||||
)
|
||||
)
|
||||
msg = _("An error occurred. Details: {}".format(ex))
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg,
|
||||
extra_tags='make_charge_error'
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse('hosting:payment') + '#payment_error'
|
||||
)
|
||||
request.session['card_id'] = user_card_detail.id
|
||||
else:
|
||||
# Get or create stripe customer
|
||||
customer = StripeCustomer.get_or_create(
|
||||
email=owner.email, token=token
|
||||
)
|
||||
if not customer:
|
||||
msg = _("Invalid credit card")
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg,
|
||||
extra_tags='make_charge_error')
|
||||
return HttpResponseRedirect(
|
||||
reverse('hosting:payment') + '#payment_error')
|
||||
request.session['token'] = token
|
||||
request.session['billing_address_data'] = billing_address_data
|
||||
request.session['token'] = token
|
||||
request.session['customer'] = customer.stripe_id
|
||||
return HttpResponseRedirect("{url}?{query_params}".format(
|
||||
url=reverse('hosting:order-confirmation'),
|
||||
query_params='page=payment'))
|
||||
query_params='page=payment')
|
||||
)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
|
@ -727,12 +846,6 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
).get_context_data(**kwargs)
|
||||
obj = self.get_object()
|
||||
owner = self.request.user
|
||||
stripe_api_cus_id = self.request.session.get('customer')
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_card_details(
|
||||
stripe_api_cus_id,
|
||||
self.request.session.get('token')
|
||||
)
|
||||
|
||||
if self.request.GET.get('page') == 'payment':
|
||||
context['page_header_text'] = _('Confirm Order')
|
||||
|
|
@ -757,11 +870,18 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
context['vm'] = vm_detail.__dict__
|
||||
context['vm']['name'] = '{}-{}'.format(
|
||||
context['vm']['configuration'], context['vm']['vm_id'])
|
||||
context['vm']['price'] = get_vm_price(
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
disk_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory']
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
pricing_name=(obj.vm_pricing.name
|
||||
if obj.vm_pricing else 'default')
|
||||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['discount'] = discount
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = price + vat - discount['amount']
|
||||
context['subscription_end_date'] = vm_detail.end_date()
|
||||
except VMDetail.DoesNotExist:
|
||||
try:
|
||||
|
|
@ -770,6 +890,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
)
|
||||
vm = manager.get_vm(obj.vm_id)
|
||||
context['vm'] = VirtualMachineSerializer(vm).data
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
pricing_name=(obj.vm_pricing.name
|
||||
if obj.vm_pricing else 'default')
|
||||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['discount'] = discount
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = (
|
||||
price + vat - discount['amount']
|
||||
)
|
||||
except WrongIdError:
|
||||
messages.error(
|
||||
self.request,
|
||||
|
|
@ -784,17 +918,25 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
_('In order to create a VM, you need to create/upload '
|
||||
'your SSH KEY first.')
|
||||
)
|
||||
elif not card_details.get('response_object'):
|
||||
# new order, failed to get card details
|
||||
context['failed_payment'] = True
|
||||
context['card_details'] = card_details
|
||||
else:
|
||||
# new order, confirm payment
|
||||
if 'token' in self.request.session:
|
||||
token = self.request.session['token']
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
token
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
return HttpResponseRedirect(reverse('hosting:payment'))
|
||||
card_details_response = card_details['response_object']
|
||||
context['cc_last4'] = card_details_response['last4']
|
||||
context['cc_brand'] = card_details_response['brand']
|
||||
else:
|
||||
card_id = self.request.session.get('card_id')
|
||||
card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
context['cc_last4'] = card_detail.last4
|
||||
context['cc_brand'] = card_detail.brand
|
||||
context['site_url'] = reverse('hosting:create_virtual_machine')
|
||||
context['cc_last4'] = card_details.get('response_object').get(
|
||||
'last4')
|
||||
context['cc_brand'] = card_details.get('response_object').get(
|
||||
'cc_brand')
|
||||
context['vm'] = self.request.session.get('specs')
|
||||
return context
|
||||
|
||||
|
|
@ -805,7 +947,9 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
return HttpResponseRedirect(
|
||||
reverse('hosting:create_virtual_machine')
|
||||
)
|
||||
if 'token' not in self.request.session:
|
||||
|
||||
if ('token' not in self.request.session and
|
||||
'card_id' not in self.request.session):
|
||||
return HttpResponseRedirect(reverse('hosting:payment'))
|
||||
self.object = self.get_object()
|
||||
context = self.get_context_data(object=self.object)
|
||||
|
|
@ -824,28 +968,72 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
def post(self, request):
|
||||
template = request.session.get('template')
|
||||
specs = request.session.get('specs')
|
||||
stripe_utils = StripeUtils()
|
||||
# We assume that if the user is here, his/her StripeCustomer
|
||||
# object already exists
|
||||
stripe_customer_id = request.user.stripecustomer.id
|
||||
billing_address_data = request.session.get('billing_address_data')
|
||||
vm_template_id = template.get('id', 1)
|
||||
stripe_api_cus_id = self.request.session.get('customer')
|
||||
# Make stripe charge to a customer
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_card_details(stripe_api_cus_id,
|
||||
request.session.get(
|
||||
'token'))
|
||||
if not card_details.get('response_object'):
|
||||
msg = card_details.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:payment') + '#payment_error')
|
||||
card_details_dict = card_details.get('response_object')
|
||||
stripe_api_cus_id = request.user.stripecustomer.stripe_id
|
||||
if 'token' in self.request.session:
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
request.session['token']
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
return HttpResponseRedirect(reverse('hosting:payment'))
|
||||
card_details_response = card_details['response_object']
|
||||
card_details_dict = {
|
||||
'last4': card_details_response['last4'],
|
||||
'brand': card_details_response['brand'],
|
||||
'card_id': card_details_response['card_id']
|
||||
}
|
||||
ucd = UserCardDetail.get_user_card_details(
|
||||
request.user.stripecustomer, card_details_response
|
||||
)
|
||||
if not ucd:
|
||||
acc_result = stripe_utils.associate_customer_card(
|
||||
stripe_api_cus_id, request.session['token'],
|
||||
set_as_default=True
|
||||
)
|
||||
if acc_result['response_object'] is None:
|
||||
msg = _(
|
||||
'An error occurred while associating the card.'
|
||||
' Details: {details}'.format(
|
||||
details=acc_result['error']
|
||||
)
|
||||
)
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
url=reverse('hosting:payment'),
|
||||
section='payment_error'),
|
||||
'msg_title': str(_('Error.')),
|
||||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected'
|
||||
' back to the payment page.')
|
||||
)
|
||||
}
|
||||
return JsonResponse(response)
|
||||
else:
|
||||
card_id = request.session.get('card_id')
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
card_details_dict = {
|
||||
'last4': user_card_detail.last4,
|
||||
'brand': user_card_detail.brand,
|
||||
'card_id': user_card_detail.card_id
|
||||
}
|
||||
if not user_card_detail.preferred:
|
||||
UserCardDetail.set_default_card(
|
||||
stripe_api_cus_id=stripe_api_cus_id,
|
||||
stripe_source_id=user_card_detail.card_id
|
||||
)
|
||||
cpu = specs.get('cpu')
|
||||
memory = specs.get('memory')
|
||||
disk_size = specs.get('disk_size')
|
||||
amount_to_be_charged = specs.get('price')
|
||||
amount_to_be_charged = specs.get('total_price')
|
||||
plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size)
|
||||
|
|
@ -866,6 +1054,12 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
# Check if the subscription was approved and is active
|
||||
if (stripe_subscription_obj is None or
|
||||
stripe_subscription_obj.status != 'active'):
|
||||
# At this point, we have created a Stripe API card and
|
||||
# associated it with the customer; but the transaction failed
|
||||
# due to some reason. So, we would want to dissociate this card
|
||||
# here.
|
||||
# ...
|
||||
|
||||
msg = subscription_result.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
|
|
@ -878,10 +1072,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected back to'
|
||||
' the payment page.'))
|
||||
' the payment page.')
|
||||
)
|
||||
}
|
||||
return HttpResponse(json.dumps(response),
|
||||
content_type="application/json")
|
||||
return JsonResponse(response)
|
||||
|
||||
if 'token' in request.session:
|
||||
ucd = UserCardDetail.get_or_create_user_card_detail(
|
||||
stripe_customer=self.request.user.stripecustomer,
|
||||
card_details=card_details_response
|
||||
)
|
||||
UserCardDetail.save_default_card_local(
|
||||
self.request.user.stripecustomer.stripe_id,
|
||||
ucd.card_id
|
||||
)
|
||||
user = {
|
||||
'name': self.request.user.name,
|
||||
'email': self.request.user.email,
|
||||
|
|
@ -890,15 +1094,12 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
'request_host': request.get_host(),
|
||||
'language': get_language(),
|
||||
}
|
||||
create_vm_task.delay(vm_template_id, user, specs, template,
|
||||
stripe_customer_id, billing_address_data,
|
||||
stripe_subscription_obj.id, card_details_dict)
|
||||
|
||||
for session_var in ['specs', 'template', 'billing_address',
|
||||
'billing_address_data',
|
||||
'token', 'customer']:
|
||||
if session_var in request.session:
|
||||
del request.session[session_var]
|
||||
create_vm(
|
||||
billing_address_data, stripe_customer_id, specs,
|
||||
stripe_subscription_obj, card_details_dict, request,
|
||||
vm_template_id, template, user
|
||||
)
|
||||
|
||||
response = {
|
||||
'status': True,
|
||||
|
|
@ -910,8 +1111,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
' it is ready.'))
|
||||
}
|
||||
|
||||
return HttpResponse(json.dumps(response),
|
||||
content_type="application/json")
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
class OrdersHostingListView(LoginRequiredMixin, ListView):
|
||||
|
|
@ -994,7 +1194,10 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
|
||||
@method_decorator(decorators)
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {'templates': VMTemplate.objects.all()}
|
||||
context = {
|
||||
'templates': VMTemplate.objects.all(),
|
||||
'cms_integration': get_cms_integration('default'),
|
||||
}
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
@method_decorator(decorators)
|
||||
|
|
@ -1006,18 +1209,34 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
storage = request.POST.get('storage')
|
||||
storage_field = forms.IntegerField(validators=[self.validate_storage])
|
||||
template_id = int(request.POST.get('config'))
|
||||
pricing_name = request.POST.get('pricing_name')
|
||||
vm_pricing = VMPricing.get_vm_pricing_by_name(pricing_name)
|
||||
template = VMTemplate.objects.filter(
|
||||
opennebula_vm_template_id=template_id).first()
|
||||
template_data = VMTemplateSerializer(template).data
|
||||
|
||||
if vm_pricing is None:
|
||||
vm_pricing_name_msg = _(
|
||||
"Incorrect pricing name. Please contact support"
|
||||
"{support_email}".format(
|
||||
support_email=settings.DCL_SUPPORT_FROM_ADDRESS
|
||||
)
|
||||
)
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, vm_pricing_name_msg,
|
||||
extra_tags='pricing'
|
||||
)
|
||||
return redirect(CreateVirtualMachinesView.as_view())
|
||||
else:
|
||||
vm_pricing_name = vm_pricing.name
|
||||
|
||||
try:
|
||||
cores = cores_field.clean(cores)
|
||||
except ValidationError as err:
|
||||
msg = '{} : {}.'.format(cores, str(err))
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='cores')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:index') + "#order_form")
|
||||
return redirect(CreateVirtualMachinesView.as_view())
|
||||
|
||||
try:
|
||||
memory = memory_field.clean(memory)
|
||||
|
|
@ -1025,8 +1244,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
msg = '{} : {}.'.format(memory, str(err))
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='memory')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:index') + "#order_form")
|
||||
return redirect(CreateVirtualMachinesView.as_view())
|
||||
|
||||
try:
|
||||
storage = storage_field.clean(storage)
|
||||
|
|
@ -1034,15 +1252,25 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
msg = '{} : {}.'.format(storage, str(err))
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='storage')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:index') + "#order_form")
|
||||
price = get_vm_price(cpu=cores, memory=memory,
|
||||
disk_size=storage)
|
||||
return redirect(CreateVirtualMachinesView.as_view())
|
||||
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
cpu=cores,
|
||||
memory=memory,
|
||||
ssd_size=storage,
|
||||
pricing_name=vm_pricing_name
|
||||
)
|
||||
|
||||
specs = {
|
||||
'cpu': cores,
|
||||
'memory': memory,
|
||||
'disk_size': storage,
|
||||
'price': price
|
||||
'discount': discount,
|
||||
'price': price,
|
||||
'vat': vat,
|
||||
'vat_percent': vat_percent,
|
||||
'total_price': price + vat - discount['amount'],
|
||||
'pricing_name': vm_pricing_name
|
||||
}
|
||||
|
||||
request.session['specs'] = specs
|
||||
|
|
@ -1094,10 +1322,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
for m in storage:
|
||||
pass
|
||||
storage.used = True
|
||||
return HttpResponse(
|
||||
json.dumps({'text': ugettext('Terminated')}),
|
||||
content_type="application/json"
|
||||
)
|
||||
return JsonResponse({'text': ugettext('Terminated')})
|
||||
else:
|
||||
return redirect(reverse('hosting:virtual_machines'))
|
||||
elif self.request.is_ajax():
|
||||
|
|
@ -1108,7 +1333,8 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
context = {
|
||||
'virtual_machine': serializer.data,
|
||||
'order': HostingOrder.objects.get(
|
||||
vm_id=serializer.data['vm_id'])
|
||||
vm_id=serializer.data['vm_id']
|
||||
)
|
||||
}
|
||||
except Exception as ex:
|
||||
logger.debug("Exception generated {}".format(str(ex)))
|
||||
|
|
@ -1220,18 +1446,15 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
email_to_admin_data = {
|
||||
'subject': "Deleted VM and Subscription for VM {vm_id} and "
|
||||
"user: {user}".format(
|
||||
vm_id=vm.id, user=owner.email
|
||||
),
|
||||
vm_id=vm.id, user=owner.email
|
||||
),
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': ['info@ungleich.ch'],
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in admin_email_body.items()]),
|
||||
}
|
||||
send_plain_email_task.delay(email_to_admin_data)
|
||||
return HttpResponse(
|
||||
json.dumps(response),
|
||||
content_type="application/json"
|
||||
)
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
class HostingBillListView(PermissionRequiredMixin, LoginRequiredMixin,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue