diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/utils/admin.py b/utils/admin.py new file mode 100755 index 0000000..f2039ee --- /dev/null +++ b/utils/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import UserBillingAddress + +# Register your models here. + + +admin.site.register(UserBillingAddress) diff --git a/utils/apps.py b/utils/apps.py new file mode 100755 index 0000000..7527884 --- /dev/null +++ b/utils/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UtilsConfig(AppConfig): + name = 'utils' diff --git a/utils/backend.py b/utils/backend.py new file mode 100755 index 0000000..2b5c86e --- /dev/null +++ b/utils/backend.py @@ -0,0 +1,13 @@ + +import logging + +from django.contrib.auth.backends import ModelBackend +logger = logging.getLogger(__name__) + + +class MyLDAPBackend(ModelBackend): + def authenticate(self, username=None, password=None, **kwargs): + user = super().authenticate(username, password, **kwargs) + if user: + user.create_ldap_account(password) + return user diff --git a/utils/context_processor.py b/utils/context_processor.py new file mode 100755 index 0000000..f18bf8f --- /dev/null +++ b/utils/context_processor.py @@ -0,0 +1,31 @@ +from django.conf import settings + + +def google_analytics(request): + """ + Use the variables returned in this function to + render your Google Analytics tracking code template. + + Also check whether the site is a tenant site and create a corresponding + variable to indicate this + """ + host = request.get_host() + ga_prop_id = getattr(settings, 'GOOGLE_ANALYTICS_PROPERTY_IDS', False).get( + host) + which_urlspy = settings.MULTISITE_CMS_URLS.get(host) + if ga_prop_id is None: + # Try checking if we have a www in host, if yes we remove + # that and check in the dict again + if host.startswith('www.'): + ga_prop_id = getattr(settings, 'GOOGLE_ANALYTICS_PROPERTY_IDS', + False).get(host[4:]) + which_urlspy = settings.MULTISITE_CMS_URLS.get(host[4:]) + return_dict = {} + if not settings.DEBUG and ga_prop_id: + return_dict['GOOGLE_ANALYTICS_PROPERTY_ID'] = ga_prop_id + + if which_urlspy: + if which_urlspy.endswith("multi"): + return_dict['IS_TENANT_SITE'] = True + + return return_dict diff --git a/utils/fields.py b/utils/fields.py new file mode 100755 index 0000000..ba6b08c --- /dev/null +++ b/utils/fields.py @@ -0,0 +1,256 @@ +from django.utils.translation import gettext as _ +from django.db import models + +# http://xml.coverpages.org/country3166.html +COUNTRIES = ( + ('AD', _('Andorra')), + ('AE', _('United Arab Emirates')), + ('AF', _('Afghanistan')), + ('AG', _('Antigua & Barbuda')), + ('AI', _('Anguilla')), + ('AL', _('Albania')), + ('AM', _('Armenia')), + ('AN', _('Netherlands Antilles')), + ('AO', _('Angola')), + ('AQ', _('Antarctica')), + ('AR', _('Argentina')), + ('AS', _('American Samoa')), + ('AT', _('Austria')), + ('AU', _('Australia')), + ('AW', _('Aruba')), + ('AZ', _('Azerbaijan')), + ('BA', _('Bosnia and Herzegovina')), + ('BB', _('Barbados')), + ('BD', _('Bangladesh')), + ('BE', _('Belgium')), + ('BF', _('Burkina Faso')), + ('BG', _('Bulgaria')), + ('BH', _('Bahrain')), + ('BI', _('Burundi')), + ('BJ', _('Benin')), + ('BM', _('Bermuda')), + ('BN', _('Brunei Darussalam')), + ('BO', _('Bolivia')), + ('BR', _('Brazil')), + ('BS', _('Bahama')), + ('BT', _('Bhutan')), + ('BV', _('Bouvet Island')), + ('BW', _('Botswana')), + ('BY', _('Belarus')), + ('BZ', _('Belize')), + ('CA', _('Canada')), + ('CC', _('Cocos (Keeling) Islands')), + ('CF', _('Central African Republic')), + ('CG', _('Congo')), + ('CH', _('Switzerland')), + ('CI', _('Ivory Coast')), + ('CK', _('Cook Iislands')), + ('CL', _('Chile')), + ('CM', _('Cameroon')), + ('CN', _('China')), + ('CO', _('Colombia')), + ('CR', _('Costa Rica')), + ('CU', _('Cuba')), + ('CV', _('Cape Verde')), + ('CX', _('Christmas Island')), + ('CY', _('Cyprus')), + ('CZ', _('Czech Republic')), + ('DE', _('Germany')), + ('DJ', _('Djibouti')), + ('DK', _('Denmark')), + ('DM', _('Dominica')), + ('DO', _('Dominican Republic')), + ('DZ', _('Algeria')), + ('EC', _('Ecuador')), + ('EE', _('Estonia')), + ('EG', _('Egypt')), + ('EH', _('Western Sahara')), + ('ER', _('Eritrea')), + ('ES', _('Spain')), + ('ET', _('Ethiopia')), + ('FI', _('Finland')), + ('FJ', _('Fiji')), + ('FK', _('Falkland Islands (Malvinas)')), + ('FM', _('Micronesia')), + ('FO', _('Faroe Islands')), + ('FR', _('France')), + ('FX', _('France, Metropolitan')), + ('GA', _('Gabon')), + ('GB', _('United Kingdom (Great Britain)')), + ('GD', _('Grenada')), + ('GE', _('Georgia')), + ('GF', _('French Guiana')), + ('GH', _('Ghana')), + ('GI', _('Gibraltar')), + ('GL', _('Greenland')), + ('GM', _('Gambia')), + ('GN', _('Guinea')), + ('GP', _('Guadeloupe')), + ('GQ', _('Equatorial Guinea')), + ('GR', _('Greece')), + ('GS', _('South Georgia and the South Sandwich Islands')), + ('GT', _('Guatemala')), + ('GU', _('Guam')), + ('GW', _('Guinea-Bissau')), + ('GY', _('Guyana')), + ('HK', _('Hong Kong')), + ('HM', _('Heard & McDonald Islands')), + ('HN', _('Honduras')), + ('HR', _('Croatia')), + ('HT', _('Haiti')), + ('HU', _('Hungary')), + ('ID', _('Indonesia')), + ('IE', _('Ireland')), + ('IL', _('Israel')), + ('IN', _('India')), + ('IO', _('British Indian Ocean Territory')), + ('IQ', _('Iraq')), + ('IR', _('Islamic Republic of Iran')), + ('IS', _('Iceland')), + ('IT', _('Italy')), + ('JM', _('Jamaica')), + ('JO', _('Jordan')), + ('JP', _('Japan')), + ('KE', _('Kenya')), + ('KG', _('Kyrgyzstan')), + ('KH', _('Cambodia')), + ('KI', _('Kiribati')), + ('KM', _('Comoros')), + ('KN', _('St. Kitts and Nevis')), + ('KP', _('Korea, Democratic People\'s Republic of')), + ('KR', _('Korea, Republic of')), + ('KW', _('Kuwait')), + ('KY', _('Cayman Islands')), + ('KZ', _('Kazakhstan')), + ('LA', _('Lao People\'s Democratic Republic')), + ('LB', _('Lebanon')), + ('LC', _('Saint Lucia')), + ('LI', _('Liechtenstein')), + ('LK', _('Sri Lanka')), + ('LR', _('Liberia')), + ('LS', _('Lesotho')), + ('LT', _('Lithuania')), + ('LU', _('Luxembourg')), + ('LV', _('Latvia')), + ('LY', _('Libyan Arab Jamahiriya')), + ('MA', _('Morocco')), + ('MC', _('Monaco')), + ('MD', _('Moldova, Republic of')), + ('MG', _('Madagascar')), + ('MH', _('Marshall Islands')), + ('ML', _('Mali')), + ('MN', _('Mongolia')), + ('MM', _('Myanmar')), + ('MO', _('Macau')), + ('MP', _('Northern Mariana Islands')), + ('MQ', _('Martinique')), + ('MR', _('Mauritania')), + ('MS', _('Monserrat')), + ('MT', _('Malta')), + ('MU', _('Mauritius')), + ('MV', _('Maldives')), + ('MW', _('Malawi')), + ('MX', _('Mexico')), + ('MY', _('Malaysia')), + ('MZ', _('Mozambique')), + ('NA', _('Namibia')), + ('NC', _('New Caledonia')), + ('NE', _('Niger')), + ('NF', _('Norfolk Island')), + ('NG', _('Nigeria')), + ('NI', _('Nicaragua')), + ('NL', _('Netherlands')), + ('NO', _('Norway')), + ('NP', _('Nepal')), + ('NR', _('Nauru')), + ('NU', _('Niue')), + ('NZ', _('New Zealand')), + ('OM', _('Oman')), + ('PA', _('Panama')), + ('PE', _('Peru')), + ('PF', _('French Polynesia')), + ('PG', _('Papua New Guinea')), + ('PH', _('Philippines')), + ('PK', _('Pakistan')), + ('PL', _('Poland')), + ('PM', _('St. Pierre & Miquelon')), + ('PN', _('Pitcairn')), + ('PR', _('Puerto Rico')), + ('PT', _('Portugal')), + ('PW', _('Palau')), + ('PY', _('Paraguay')), + ('QA', _('Qatar')), + ('RE', _('Reunion')), + ('RO', _('Romania')), + ('RU', _('Russian Federation')), + ('RW', _('Rwanda')), + ('SA', _('Saudi Arabia')), + ('SB', _('Solomon Islands')), + ('SC', _('Seychelles')), + ('SD', _('Sudan')), + ('SE', _('Sweden')), + ('SG', _('Singapore')), + ('SH', _('St. Helena')), + ('SI', _('Slovenia')), + ('SJ', _('Svalbard & Jan Mayen Islands')), + ('SK', _('Slovakia')), + ('SL', _('Sierra Leone')), + ('SM', _('San Marino')), + ('SN', _('Senegal')), + ('SO', _('Somalia')), + ('SR', _('Suriname')), + ('ST', _('Sao Tome & Principe')), + ('SV', _('El Salvador')), + ('SY', _('Syrian Arab Republic')), + ('SZ', _('Swaziland')), + ('TC', _('Turks & Caicos Islands')), + ('TD', _('Chad')), + ('TF', _('French Southern Territories')), + ('TG', _('Togo')), + ('TH', _('Thailand')), + ('TJ', _('Tajikistan')), + ('TK', _('Tokelau')), + ('TM', _('Turkmenistan')), + ('TN', _('Tunisia')), + ('TO', _('Tonga')), + ('TP', _('East Timor')), + ('TR', _('Turkey')), + ('TT', _('Trinidad & Tobago')), + ('TV', _('Tuvalu')), + ('TW', _('Taiwan, Province of China')), + ('TZ', _('Tanzania, United Republic of')), + ('UA', _('Ukraine')), + ('UG', _('Uganda')), + ('UM', _('United States Minor Outlying Islands')), + ('US', _('United States of America')), + ('UY', _('Uruguay')), + ('UZ', _('Uzbekistan')), + ('VA', _('Vatican City State (Holy See)')), + ('VC', _('St. Vincent & the Grenadines')), + ('VE', _('Venezuela')), + ('VG', _('British Virgin Islands')), + ('VI', _('United States Virgin Islands')), + ('VN', _('Viet Nam')), + ('VU', _('Vanuatu')), + ('WF', _('Wallis & Futuna Islands')), + ('WS', _('Samoa')), + ('YE', _('Yemen')), + ('YT', _('Mayotte')), + ('YU', _('Yugoslavia')), + ('ZA', _('South Africa')), + ('ZM', _('Zambia')), + ('ZR', _('Zaire')), + ('ZW', _('Zimbabwe')), +) + + +class CountryField(models.CharField): + def __init__(self, *args, **kwargs): + kwargs.setdefault('choices', COUNTRIES) + kwargs.setdefault('default', 'CH') + kwargs.setdefault('max_length', 2) + + super(CountryField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return "CharField" diff --git a/utils/forms.py b/utils/forms.py new file mode 100755 index 0000000..3413588 --- /dev/null +++ b/utils/forms.py @@ -0,0 +1,216 @@ +from django import forms +from django.contrib.auth import authenticate +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ + +from membership.models import CustomUser +from .models import ContactMessage, BillingAddress, UserBillingAddress + + +# from utils.fields import CountryField + + +class SignupFormMixin(forms.ModelForm): + confirm_password = forms.CharField(widget=forms.PasswordInput()) + password = forms.CharField(widget=forms.PasswordInput()) + + class Meta: + model = CustomUser + fields = ['name', 'email', 'password'] + widgets = { + 'name': forms.TextInput( + attrs={'placeholder': _('Enter your name or company name')}), + } + + def clean_confirm_password(self): + password = self.cleaned_data.get('password') + confirm_password = self.cleaned_data.get('confirm_password') + if not confirm_password == password: + raise forms.ValidationError("Passwords don't match") + return confirm_password + + +class LoginFormMixin(forms.Form): + email = forms.CharField(widget=forms.EmailInput()) + password = forms.CharField(widget=forms.PasswordInput()) + + class Meta: + fields = ['email', 'password'] + + def clean(self): + email = self.cleaned_data.get('email') + password = self.cleaned_data.get('password') + is_auth = authenticate(email=email, password=password) + if not is_auth: + raise forms.ValidationError( + _("Your username and/or password were incorrect.")) + return self.cleaned_data + + def clean_email(self): + email = self.cleaned_data.get('email') + try: + CustomUser.objects.get(email=email) + return email + except CustomUser.DoesNotExist: + raise forms.ValidationError(_("User does not exist")) + + +class ResendActivationEmailForm(forms.Form): + email = forms.CharField(widget=forms.EmailInput()) + + class Meta: + fields = ['email'] + + def clean_email(self): + email = self.cleaned_data.get('email') + try: + c = CustomUser.objects.get(email=email) + if c.validated == 1: + raise forms.ValidationError( + _("The account is already active.")) + return email + except CustomUser.DoesNotExist: + raise forms.ValidationError(_("User does not exist")) + + +class PasswordResetRequestForm(forms.Form): + email = forms.CharField(widget=forms.EmailInput()) + + class Meta: + fields = ['email'] + + def clean_email(self): + email = self.cleaned_data.get('email') + try: + CustomUser.objects.get(email=email) + return email + except CustomUser.DoesNotExist: + raise forms.ValidationError(_("User does not exist")) + + +class SetPasswordForm(forms.Form): + """ + A form that lets a user change set their password without entering the old + password + """ + error_messages = { + 'password_mismatch': _("The two password fields didn't match."), + } + new_password1 = forms.CharField(label=_("New password"), + widget=forms.PasswordInput) + new_password2 = forms.CharField(label=_("New password confirmation"), + widget=forms.PasswordInput) + + def clean_new_password2(self): + password1 = self.cleaned_data.get('new_password1') + password2 = self.cleaned_data.get('new_password2') + if password1 and password2: + if password1 != password2: + raise forms.ValidationError( + self.error_messages['password_mismatch'], + code='password_mismatch', ) + return password2 + + +class EditCreditCardForm(forms.Form): + token = forms.CharField(widget=forms.HiddenInput()) + + +class BillingAddressForm(forms.ModelForm): + token = forms.CharField(widget=forms.HiddenInput(), required=False) + card = forms.CharField(widget=forms.HiddenInput(), required=False) + + class Meta: + model = BillingAddress + fields = ['cardholder_name', 'street_address', + 'city', 'postal_code', 'country', 'vat_number'] + labels = { + 'cardholder_name': _('Cardholder Name'), + 'street_address': _('Street Address'), + 'city': _('City'), + 'postal_code': _('Postal Code'), + 'Country': _('Country'), + 'VAT Number': _('VAT Number') + } + + +class BillingAddressFormSignup(BillingAddressForm): + name = forms.CharField(label=_('Name')) + email = forms.EmailField(label=_('Email Address')) + field_order = ['name', 'email'] + + class Meta: + model = BillingAddress + fields = ['name', 'email', 'cardholder_name', 'street_address', + 'city', 'postal_code', 'country', 'vat_number'] + labels = { + 'name': 'Name', + 'email': _('Email'), + 'cardholder_name': _('Cardholder Name'), + 'street_address': _('Street Address'), + 'city': _('City'), + 'postal_code': _('Postal Code'), + 'Country': _('Country'), + 'vat_number': _('VAT Number') + } + + def clean_email(self): + email = self.cleaned_data.get('email') + try: + CustomUser.objects.get(email=email) + raise forms.ValidationError( + _("The email %(email)s is already registered with us. " + "Please reset your password and access your account.") % + {'email': email} + ) + except CustomUser.DoesNotExist: + return email + + +class UserBillingAddressForm(forms.ModelForm): + user = forms.ModelChoiceField(queryset=CustomUser.objects.all(), + widget=forms.HiddenInput()) + + class Meta: + model = UserBillingAddress + fields = ['cardholder_name', 'street_address', + 'city', 'postal_code', 'country', 'user', 'vat_number'] + labels = { + 'cardholder_name': _('Cardholder Name'), + 'street_address': _('Street Building'), + 'city': _('City'), + 'postal_code': _('Postal Code'), + 'Country': _('Country'), + 'vat_number': _('VAT Number'), + } + + +class ContactUsForm(forms.ModelForm): + error_css_class = 'autofocus' + + class Meta: + model = ContactMessage + fields = ['name', 'email', 'phone_number', 'message'] + widgets = { + 'name': forms.TextInput(attrs={'class': u'form-control'}), + 'email': forms.TextInput(attrs={'class': u'form-control'}), + 'phone_number': forms.TextInput(attrs={'class': u'form-control'}), + 'message': forms.Textarea(attrs={'class': u'form-control'}), + } + labels = { + 'name': _('Name'), + 'email': _('Email'), + 'phone_number': _('Phone number'), + 'message': _('Message'), + } + + def send_email(self, email_to='info@digitalglarus.ch'): + text_content = render_to_string( + 'emails/contact.txt', {'data': self.cleaned_data}) + html_content = render_to_string( + 'emails/contact.html', {'data': self.cleaned_data}) + email = EmailMultiAlternatives('Subject', text_content) + email.attach_alternative(html_content, "text/html") + email.to = [email_to] + email.send() diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py new file mode 100755 index 0000000..0b31fed --- /dev/null +++ b/utils/hosting_utils.py @@ -0,0 +1,241 @@ +import decimal +import logging +import subprocess + +from django.conf import settings + +from oca.pool import WrongIdError + +from datacenterlight.models import VMPricing +from hosting.models import UserHostingKey, VMDetail, VATRates +from opennebula_api.serializers import VirtualMachineSerializer + +logger = logging.getLogger(__name__) + + +def get_all_public_keys(customer): + """ + Returns all the public keys of the user + :param customer: The customer whose public keys are needed + :return: A list of public keys + """ + return UserHostingKey.objects.filter(user_id=customer.id).values_list( + "public_key", flat=True).distinct() + + +def get_or_create_vm_detail(user, manager, vm_id): + """ + Returns VMDetail object related to given vm_id. Creates the object + if it does not exist + + :param vm_id: The ID of the VM which should be greater than 0. + :param user: The CustomUser object that owns this VM + :param manager: The OpenNebulaManager object + :return: The VMDetail object. None if vm_id is less than or equal to 0. + Also, for the cases where the VMDetail does not exist and we can not + fetch data about the VM from OpenNebula, the function returns None + """ + if vm_id <= 0: + return None + try: + vm_detail_obj = VMDetail.objects.get(vm_id=vm_id) + except VMDetail.DoesNotExist: + try: + vm_obj = manager.get_vm(vm_id) + except (WrongIdError, ConnectionRefusedError) as e: + logger.error(str(e)) + return None + vm = VirtualMachineSerializer(vm_obj).data + vm_detail_obj = VMDetail.objects.create( + user=user, vm_id=vm_id, disk_size=vm['disk_size'], + cores=vm['cores'], memory=vm['memory'], + configuration=vm['configuration'], ipv4=vm['ipv4'], + ipv6=vm['ipv6'] + ) + return vm_detail_obj + + +def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'): + """ + A helper function that computes price of a VM from given cpu, ram and + ssd parameters + + :param cpu: Number of cores of the VM + :param memory: RAM of the VM + :param disk_size: Disk space of the VM (SSD) + :param hdd_size: The HDD size + :param pricing_name: The pricing name to be used + :return: The price of the VM + """ + try: + pricing = VMPricing.objects.get(name=pricing_name) + except Exception as ex: + logger.error( + "Error getting VMPricing object for {pricing_name}." + "Details: {details}".format( + pricing_name=pricing_name, details=str(ex) + ) + ) + return None + price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) + + (decimal.Decimal(memory) * pricing.ram_unit_price) + + (decimal.Decimal(disk_size) * pricing.ssd_unit_price) + + (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) + + decimal.Decimal(settings.VM_BASE_PRICE)) + cents = decimal.Decimal('.01') + price = price.quantize(cents, decimal.ROUND_HALF_UP) + return round(float(price), 2) + + +def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0, + pricing_name='default', vat_rate=0): + try: + pricing = VMPricing.objects.get(name=pricing_name) + except Exception as ex: + logger.error( + "Error getting VMPricing object for {pricing_name}." + "Details: {details}".format( + pricing_name=pricing_name, details=str(ex) + ) + ) + return None + + price = ( + (decimal.Decimal(cpu) * pricing.cores_unit_price) + + (decimal.Decimal(memory) * pricing.ram_unit_price) + + (decimal.Decimal(ssd_size) * pricing.ssd_unit_price) + + (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) + + decimal.Decimal(settings.VM_BASE_PRICE) + ) + + discount_name = pricing.discount_name + discount_amount = round(float(pricing.discount_amount), 2) + vat = price * decimal.Decimal(vat_rate) * decimal.Decimal(0.01) + vat_percent = vat_rate + + cents = decimal.Decimal('.01') + price = price.quantize(cents, decimal.ROUND_HALF_UP) + vat = vat.quantize(cents, decimal.ROUND_HALF_UP) + discount_amount_with_vat = decimal.Decimal(discount_amount) * (1 + decimal.Decimal(vat_rate) * decimal.Decimal(0.01)) + discount_amount_with_vat = discount_amount_with_vat.quantize(cents, decimal.ROUND_HALF_UP) + discount = { + 'name': discount_name, + 'amount': discount_amount, + 'amount_with_vat': round(float(discount_amount_with_vat), 2), + 'stripe_coupon_id': pricing.stripe_coupon_id + } + return (round(float(price), 2), round(float(vat), 2), + round(float(vat_percent), 2), discount) + + + +def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, + pricing_name='default'): + """ + A helper function that computes price of a VM from given cpu, ram and + ssd, hdd and the pricing parameters + + :param cpu: Number of cores of the VM + :param memory: RAM of the VM + :param ssd_size: Disk space of the VM (SSD) + :param hdd_size: The HDD size + :param pricing_name: The pricing name to be used + :return: The a tuple containing the price of the VM, the VAT and the + VAT percentage + """ + try: + pricing = VMPricing.objects.get(name=pricing_name) + except Exception as ex: + logger.error( + "Error getting VMPricing object for {pricing_name}." + "Details: {details}".format( + pricing_name=pricing_name, details=str(ex) + ) + ) + return None + + price = ( + (decimal.Decimal(cpu) * pricing.cores_unit_price) + + (decimal.Decimal(memory) * pricing.ram_unit_price) + + (decimal.Decimal(ssd_size) * pricing.ssd_unit_price) + + (decimal.Decimal(hdd_size) * pricing.hdd_unit_price) + + decimal.Decimal(settings.VM_BASE_PRICE) + ) + if pricing.vat_inclusive: + vat = decimal.Decimal(0) + vat_percent = decimal.Decimal(0) + else: + vat = price * pricing.vat_percentage * decimal.Decimal(0.01) + vat_percent = pricing.vat_percentage + + cents = decimal.Decimal('.01') + price = price.quantize(cents, decimal.ROUND_HALF_UP) + vat = vat.quantize(cents, decimal.ROUND_HALF_UP) + discount = { + 'name': pricing.discount_name, + 'amount': round(float(pricing.discount_amount), 2), + 'stripe_coupon_id': pricing.stripe_coupon_id + } + return (round(float(price), 2), round(float(vat), 2), + round(float(vat_percent), 2), discount) + + +def ping_ok(host_ipv6): + """ + A utility method to check if a host responds to ping requests. Note: the + function relies on `ping6` utility of debian to check. + + :param host_ipv6 str type parameter that represets the ipv6 of the host to + checked + :return True if the host responds to ping else returns False + """ + try: + subprocess.check_output("ping6 -c 1 " + host_ipv6, shell=True) + except Exception as ex: + logger.debug(host_ipv6 + " not reachable via ping. Error = " + str(ex)) + return False + return True + + +def get_vat_rate_for_country(country): + vat_rate = None + try: + vat_rate = VATRates.objects.get( + territory_codes=country, start_date__isnull=False, stop_date=None + ) + logger.debug("VAT rate for %s is %s" % (country, vat_rate.rate)) + return vat_rate.rate + except VATRates.DoesNotExist as dne: + logger.debug(str(dne)) + logger.debug("Did not find VAT rate for %s, returning 0" % country) + return 0 + + +def get_ip_addresses(vm_id): + try: + vm_detail = VMDetail.objects.get(vm_id=vm_id) + return "%s
%s" % (vm_detail.ipv6, vm_detail.ipv4) + except VMDetail.DoesNotExist as dne: + logger.error(str(dne)) + logger.error("VMDetail for %s does not exist" % vm_id) + return "--" + + +class HostingUtils: + @staticmethod + def clear_items_from_list(from_list, items_list): + """ + A utility function to clear items from a given list. + Useful when deleting items in bulk from session. + e.g.: + HostingUtils.clear_items_from_list( + request.session, + ['token', 'billing_address_data', 'card_id',] + ) + :param from_list: + :param items_list: + :return: + """ + for var in items_list: + if var in from_list: + del from_list[var] diff --git a/utils/ldap_manager.py b/utils/ldap_manager.py new file mode 100755 index 0000000..ff72b86 --- /dev/null +++ b/utils/ldap_manager.py @@ -0,0 +1,281 @@ +import base64 +import hashlib +import random +import ldap3 +import logging +import unicodedata + +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class LdapManager: + __instance = None + + def __new__(cls): + if LdapManager.__instance is None: + LdapManager.__instance = object.__new__(cls) + return LdapManager.__instance + + def __init__(self): + """ + Initialize the LDAP subsystem. + """ + self.rng = random.SystemRandom() + self.server = ldap3.Server(settings.AUTH_LDAP_SERVER) + + def get_admin_conn(self): + """ + Return a bound :class:`ldap3.Connection` instance which has write + permissions on the dn in which the user accounts reside. + """ + conn = self.get_conn(user=settings.LDAP_ADMIN_DN, + password=settings.LDAP_ADMIN_PASSWORD, + raise_exceptions=True) + conn.bind() + return conn + + def get_conn(self, **kwargs): + """ + Return an unbound :class:`ldap3.Connection` which talks to the configured + LDAP server. + + The *kwargs* are passed to the constructor of :class:`ldap3.Connection` and + can be used to set *user*, *password* and other useful arguments. + """ + return ldap3.Connection(self.server, **kwargs) + + def _ssha_password(self, password): + """ + Apply the SSHA password hashing scheme to the given *password*. + *password* must be a :class:`bytes` object, containing the utf-8 + encoded password. + + Return a :class:`bytes` object containing ``ascii``-compatible data + which can be used as LDAP value, e.g. after armoring it once more using + base64 or decoding it to unicode from ``ascii``. + """ + SALT_BYTES = 15 + + sha1 = hashlib.sha1() + salt = self.rng.getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES, "little") + sha1.update(password) + sha1.update(salt) + + digest = sha1.digest() + passwd = b"{SSHA}" + base64.b64encode(digest + salt) + return passwd + + def create_user(self, user, password, firstname, lastname, email): + conn = self.get_admin_conn() + uidNumber = self._get_max_uid() + 1 + logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber)) + user_exists = True + while user_exists: + user_exists, _ = self.check_user_exists( + "", + '(&(objectClass=inetOrgPerson)(objectClass=posixAccount)' + '(objectClass=top)(uidNumber={uidNumber}))'.format( + uidNumber=uidNumber + ) + ) + if user_exists: + logger.debug( + "{uid} exists. Trying next.".format(uid=uidNumber) + ) + uidNumber += 1 + logger.debug("{uid} does not exist. Using it".format(uid=uidNumber)) + self._set_max_uid(uidNumber) + try: + uid = user + conn.add("uid={uid},{customer_dn}".format( + uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN + ), + ["inetOrgPerson", "posixAccount", "ldapPublickey"], + { + "uid": [uid], + "sn": [lastname.encode("utf-8")], + "givenName": [firstname.encode("utf-8")], + "cn": [uid], + "displayName": ["{} {}".format(firstname, lastname).encode("utf-8")], + "uidNumber": [str(uidNumber)], + "gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)], + "loginShell": ["/bin/bash"], + "homeDirectory": ["/home/{}".format(unicodedata.normalize('NFKD', user).encode('ascii','ignore'))], + "mail": email.encode("utf-8"), + "userPassword": [self._ssha_password( + password.encode("utf-8") + )] + } + ) + logger.debug('Created user %s %s' % (user.encode('utf-8'), uidNumber)) + except Exception as ex: + logger.debug('Could not create user %s' % user.encode('utf-8')) + logger.error("Exception: " + str(ex)) + raise Exception(ex) + finally: + conn.unbind() + + def change_password(self, uid, new_password): + """ + Changes the password of the user identified by user_dn + + :param uid: str The uid that identifies the user + :param new_password: str The new password string + :return: True if password was changed successfully False otherwise + """ + conn = self.get_admin_conn() + + # Make sure the user exists first to change his/her details + user_exists, entries = self.check_user_exists( + uid=uid, + search_base=settings.ENTIRE_SEARCH_BASE + ) + return_val = False + if user_exists: + try: + return_val = conn.modify( + entries[0].entry_dn, + { + "userpassword": ( + ldap3.MODIFY_REPLACE, + [self._ssha_password(new_password.encode("utf-8"))] + ) + } + ) + except Exception as ex: + logger.error("Exception: " + str(ex)) + else: + logger.error("User {} not found".format(uid)) + + conn.unbind() + return return_val + + + def change_user_details(self, uid, details): + """ + Updates the user details as per given values in kwargs of the user + identified by user_dn. + + Assumes that all attributes passed in kwargs are valid. + + :param uid: str The uid that identifies the user + :param details: dict A dictionary containing the new values + :return: True if user details were updated successfully False otherwise + """ + conn = self.get_admin_conn() + + # Make sure the user exists first to change his/her details + user_exists, entries = self.check_user_exists( + uid=uid, + search_base=settings.ENTIRE_SEARCH_BASE + ) + + return_val = False + if user_exists: + details_dict = {k: (ldap3.MODIFY_REPLACE, [v.encode("utf-8")]) for + k, v in details.items()} + try: + return_val = conn.modify(entries[0].entry_dn, details_dict) + msg = "success" + except Exception as ex: + msg = str(ex) + logger.error("Exception: " + msg) + finally: + conn.unbind() + else: + msg = "User {} not found".format(uid) + logger.error(msg) + conn.unbind() + return return_val, msg + + def check_user_exists(self, uid, search_filter="", attributes=None, + search_base=settings.LDAP_CUSTOMER_DN, search_attr="uid"): + """ + Check if the user with the given uid exists in the customer group. + + :param uid: str representing the user + :param search_filter: str representing the filter condition to find + users. If its empty, the search finds the user with + the given uid. + :param attributes: list A list of str representing all the attributes + to be obtained in the result entries + :param search_base: str + :return: tuple (bool, [ldap3.abstract.entry.Entry ..]) + A bool indicating if the user exists + A list of all entries obtained in the search + """ + conn = self.get_admin_conn() + entries = [] + try: + result = conn.search( + search_base=search_base, + search_filter=search_filter if len(search_filter) > 0 else + '(uid={uid})'.format(uid=uid), + attributes=attributes + ) + entries = conn.entries + finally: + conn.unbind() + return result, entries + + def delete_user(self, uid): + """ + Deletes the user with the given uid from ldap + + :param uid: str representing the user + :return: True if the delete was successful False otherwise + """ + conn = self.get_admin_conn() + try: + return_val = conn.delete( + ("uid={uid}," + settings.LDAP_CUSTOMER_DN).format(uid=uid), + ) + msg = "success" + except Exception as ex: + msg = str(ex) + logger.error("Exception: " + msg) + return_val = False + finally: + conn.unbind() + return return_val, msg + + def _set_max_uid(self, max_uid): + """ + a utility function to save max_uid value to a file + + :param max_uid: an integer representing the max uid + :return: + """ + with open(settings.LDAP_MAX_UID_FILE_PATH, 'w+') as handler: + handler.write(str(max_uid)) + + def _get_max_uid(self): + """ + A utility function to read the max uid value that was previously set + + :return: An integer representing the max uid value that was previously + set + """ + try: + with open(settings.LDAP_MAX_UID_FILE_PATH, 'r+') as handler: + try: + return_value = int(handler.read()) + except ValueError as ve: + logger.error( + "Error reading int value from {}. {}" + "Returning default value {} instead".format( + settings.LDAP_MAX_UID_FILE_PATH, + str(ve), + settings.LDAP_DEFAULT_START_UID + ) + ) + return_value = settings.LDAP_DEFAULT_START_UID + return return_value + except FileNotFoundError as fnfe: + logger.error("File not found : " + str(fnfe)) + return_value = settings.LDAP_DEFAULT_START_UID + logger.error("So, returning UID={}".format(return_value)) + return return_value + diff --git a/utils/locale/de/LC_MESSAGES/django.mo b/utils/locale/de/LC_MESSAGES/django.mo new file mode 100755 index 0000000..c2a8797 Binary files /dev/null and b/utils/locale/de/LC_MESSAGES/django.mo differ diff --git a/utils/locale/de/LC_MESSAGES/django.po b/utils/locale/de/LC_MESSAGES/django.po new file mode 100755 index 0000000..152c63c --- /dev/null +++ b/utils/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,1085 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-12-03 10:44+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: utils/fields.py:6 +msgid "Andorra" +msgstr "" + +#: utils/fields.py:7 +msgid "United Arab Emirates" +msgstr "" + +#: utils/fields.py:8 +msgid "Afghanistan" +msgstr "" + +#: utils/fields.py:9 +msgid "Antigua & Barbuda" +msgstr "" + +#: utils/fields.py:10 +msgid "Anguilla" +msgstr "" + +#: utils/fields.py:11 +msgid "Albania" +msgstr "" + +#: utils/fields.py:12 +msgid "Armenia" +msgstr "" + +#: utils/fields.py:13 +msgid "Netherlands Antilles" +msgstr "" + +#: utils/fields.py:14 +msgid "Angola" +msgstr "" + +#: utils/fields.py:15 +msgid "Antarctica" +msgstr "" + +#: utils/fields.py:16 +msgid "Argentina" +msgstr "" + +#: utils/fields.py:17 +msgid "American Samoa" +msgstr "" + +#: utils/fields.py:18 +msgid "Austria" +msgstr "" + +#: utils/fields.py:19 +msgid "Australia" +msgstr "" + +#: utils/fields.py:20 +msgid "Aruba" +msgstr "" + +#: utils/fields.py:21 +msgid "Azerbaijan" +msgstr "" + +#: utils/fields.py:22 +msgid "Bosnia and Herzegovina" +msgstr "" + +#: utils/fields.py:23 +msgid "Barbados" +msgstr "" + +#: utils/fields.py:24 +msgid "Bangladesh" +msgstr "" + +#: utils/fields.py:25 +msgid "Belgium" +msgstr "" + +#: utils/fields.py:26 +msgid "Burkina Faso" +msgstr "" + +#: utils/fields.py:27 +msgid "Bulgaria" +msgstr "" + +#: utils/fields.py:28 +msgid "Bahrain" +msgstr "" + +#: utils/fields.py:29 +msgid "Burundi" +msgstr "" + +#: utils/fields.py:30 +msgid "Benin" +msgstr "" + +#: utils/fields.py:31 +msgid "Bermuda" +msgstr "" + +#: utils/fields.py:32 +msgid "Brunei Darussalam" +msgstr "" + +#: utils/fields.py:33 +msgid "Bolivia" +msgstr "" + +#: utils/fields.py:34 +msgid "Brazil" +msgstr "" + +#: utils/fields.py:35 +msgid "Bahama" +msgstr "" + +#: utils/fields.py:36 +msgid "Bhutan" +msgstr "" + +#: utils/fields.py:37 +msgid "Bouvet Island" +msgstr "" + +#: utils/fields.py:38 +msgid "Botswana" +msgstr "" + +#: utils/fields.py:39 +msgid "Belarus" +msgstr "" + +#: utils/fields.py:40 +msgid "Belize" +msgstr "" + +#: utils/fields.py:41 +msgid "Canada" +msgstr "" + +#: utils/fields.py:42 +msgid "Cocos (Keeling) Islands" +msgstr "" + +#: utils/fields.py:43 +msgid "Central African Republic" +msgstr "" + +#: utils/fields.py:44 +msgid "Congo" +msgstr "" + +#: utils/fields.py:45 +msgid "Switzerland" +msgstr "" + +#: utils/fields.py:46 +msgid "Ivory Coast" +msgstr "" + +#: utils/fields.py:47 +msgid "Cook Iislands" +msgstr "" + +#: utils/fields.py:48 +msgid "Chile" +msgstr "" + +#: utils/fields.py:49 +msgid "Cameroon" +msgstr "" + +#: utils/fields.py:50 +msgid "China" +msgstr "" + +#: utils/fields.py:51 +msgid "Colombia" +msgstr "" + +#: utils/fields.py:52 +msgid "Costa Rica" +msgstr "" + +#: utils/fields.py:53 +msgid "Cuba" +msgstr "" + +#: utils/fields.py:54 +msgid "Cape Verde" +msgstr "" + +#: utils/fields.py:55 +msgid "Christmas Island" +msgstr "" + +#: utils/fields.py:56 +msgid "Cyprus" +msgstr "" + +#: utils/fields.py:57 +msgid "Czech Republic" +msgstr "" + +#: utils/fields.py:58 +msgid "Germany" +msgstr "" + +#: utils/fields.py:59 +msgid "Djibouti" +msgstr "" + +#: utils/fields.py:60 +msgid "Denmark" +msgstr "" + +#: utils/fields.py:61 +msgid "Dominica" +msgstr "" + +#: utils/fields.py:62 +msgid "Dominican Republic" +msgstr "" + +#: utils/fields.py:63 +msgid "Algeria" +msgstr "" + +#: utils/fields.py:64 +msgid "Ecuador" +msgstr "" + +#: utils/fields.py:65 +msgid "Estonia" +msgstr "" + +#: utils/fields.py:66 +msgid "Egypt" +msgstr "" + +#: utils/fields.py:67 +msgid "Western Sahara" +msgstr "" + +#: utils/fields.py:68 +msgid "Eritrea" +msgstr "" + +#: utils/fields.py:69 +msgid "Spain" +msgstr "" + +#: utils/fields.py:70 +msgid "Ethiopia" +msgstr "" + +#: utils/fields.py:71 +msgid "Finland" +msgstr "" + +#: utils/fields.py:72 +msgid "Fiji" +msgstr "" + +#: utils/fields.py:73 +msgid "Falkland Islands (Malvinas)" +msgstr "" + +#: utils/fields.py:74 +msgid "Micronesia" +msgstr "" + +#: utils/fields.py:75 +msgid "Faroe Islands" +msgstr "" + +#: utils/fields.py:76 +msgid "France" +msgstr "" + +#: utils/fields.py:77 +msgid "France, Metropolitan" +msgstr "" + +#: utils/fields.py:78 +msgid "Gabon" +msgstr "" + +#: utils/fields.py:79 +msgid "United Kingdom (Great Britain)" +msgstr "" + +#: utils/fields.py:80 +msgid "Grenada" +msgstr "" + +#: utils/fields.py:81 +msgid "Georgia" +msgstr "" + +#: utils/fields.py:82 +msgid "French Guiana" +msgstr "" + +#: utils/fields.py:83 +msgid "Ghana" +msgstr "" + +#: utils/fields.py:84 +msgid "Gibraltar" +msgstr "" + +#: utils/fields.py:85 +msgid "Greenland" +msgstr "" + +#: utils/fields.py:86 +msgid "Gambia" +msgstr "" + +#: utils/fields.py:87 +msgid "Guinea" +msgstr "" + +#: utils/fields.py:88 +msgid "Guadeloupe" +msgstr "" + +#: utils/fields.py:89 +msgid "Equatorial Guinea" +msgstr "" + +#: utils/fields.py:90 +msgid "Greece" +msgstr "" + +#: utils/fields.py:91 +msgid "South Georgia and the South Sandwich Islands" +msgstr "" + +#: utils/fields.py:92 +msgid "Guatemala" +msgstr "" + +#: utils/fields.py:93 +msgid "Guam" +msgstr "" + +#: utils/fields.py:94 +msgid "Guinea-Bissau" +msgstr "" + +#: utils/fields.py:95 +msgid "Guyana" +msgstr "" + +#: utils/fields.py:96 +msgid "Hong Kong" +msgstr "" + +#: utils/fields.py:97 +msgid "Heard & McDonald Islands" +msgstr "" + +#: utils/fields.py:98 +msgid "Honduras" +msgstr "" + +#: utils/fields.py:99 +msgid "Croatia" +msgstr "" + +#: utils/fields.py:100 +msgid "Haiti" +msgstr "" + +#: utils/fields.py:101 +msgid "Hungary" +msgstr "" + +#: utils/fields.py:102 +msgid "Indonesia" +msgstr "" + +#: utils/fields.py:103 +msgid "Ireland" +msgstr "" + +#: utils/fields.py:104 +msgid "Israel" +msgstr "" + +#: utils/fields.py:105 +msgid "India" +msgstr "" + +#: utils/fields.py:106 +msgid "British Indian Ocean Territory" +msgstr "" + +#: utils/fields.py:107 +msgid "Iraq" +msgstr "" + +#: utils/fields.py:108 +msgid "Islamic Republic of Iran" +msgstr "" + +#: utils/fields.py:109 +msgid "Iceland" +msgstr "" + +#: utils/fields.py:110 +msgid "Italy" +msgstr "" + +#: utils/fields.py:111 +msgid "Jamaica" +msgstr "" + +#: utils/fields.py:112 +msgid "Jordan" +msgstr "" + +#: utils/fields.py:113 +msgid "Japan" +msgstr "" + +#: utils/fields.py:114 +msgid "Kenya" +msgstr "" + +#: utils/fields.py:115 +msgid "Kyrgyzstan" +msgstr "" + +#: utils/fields.py:116 +msgid "Cambodia" +msgstr "" + +#: utils/fields.py:117 +msgid "Kiribati" +msgstr "" + +#: utils/fields.py:118 +msgid "Comoros" +msgstr "" + +#: utils/fields.py:119 +msgid "St. Kitts and Nevis" +msgstr "" + +#: utils/fields.py:120 +msgid "Korea, Democratic People's Republic of" +msgstr "" + +#: utils/fields.py:121 +msgid "Korea, Republic of" +msgstr "" + +#: utils/fields.py:122 +msgid "Kuwait" +msgstr "" + +#: utils/fields.py:123 +msgid "Cayman Islands" +msgstr "" + +#: utils/fields.py:124 +msgid "Kazakhstan" +msgstr "" + +#: utils/fields.py:125 +msgid "Lao People's Democratic Republic" +msgstr "" + +#: utils/fields.py:126 +msgid "Lebanon" +msgstr "" + +#: utils/fields.py:127 +msgid "Saint Lucia" +msgstr "" + +#: utils/fields.py:128 +msgid "Liechtenstein" +msgstr "" + +#: utils/fields.py:129 +msgid "Sri Lanka" +msgstr "" + +#: utils/fields.py:130 +msgid "Liberia" +msgstr "" + +#: utils/fields.py:131 +msgid "Lesotho" +msgstr "" + +#: utils/fields.py:132 +msgid "Lithuania" +msgstr "" + +#: utils/fields.py:133 +msgid "Luxembourg" +msgstr "" + +#: utils/fields.py:134 +msgid "Latvia" +msgstr "" + +#: utils/fields.py:135 +msgid "Libyan Arab Jamahiriya" +msgstr "" + +#: utils/fields.py:136 +msgid "Morocco" +msgstr "" + +#: utils/fields.py:137 +msgid "Monaco" +msgstr "" + +#: utils/fields.py:138 +msgid "Moldova, Republic of" +msgstr "" + +#: utils/fields.py:139 +msgid "Madagascar" +msgstr "" + +#: utils/fields.py:140 +msgid "Marshall Islands" +msgstr "" + +#: utils/fields.py:141 +msgid "Mali" +msgstr "" + +#: utils/fields.py:142 +msgid "Mongolia" +msgstr "" + +#: utils/fields.py:143 +msgid "Myanmar" +msgstr "" + +#: utils/fields.py:144 +msgid "Macau" +msgstr "" + +#: utils/fields.py:145 +msgid "Northern Mariana Islands" +msgstr "" + +#: utils/fields.py:146 +msgid "Martinique" +msgstr "" + +#: utils/fields.py:147 +msgid "Mauritania" +msgstr "" + +#: utils/fields.py:148 +msgid "Monserrat" +msgstr "" + +#: utils/fields.py:149 +msgid "Malta" +msgstr "" + +#: utils/fields.py:150 +msgid "Mauritius" +msgstr "" + +#: utils/fields.py:151 +msgid "Maldives" +msgstr "" + +#: utils/fields.py:152 +msgid "Malawi" +msgstr "" + +#: utils/fields.py:153 +msgid "Mexico" +msgstr "" + +#: utils/fields.py:154 +msgid "Malaysia" +msgstr "" + +#: utils/fields.py:155 +msgid "Mozambique" +msgstr "" + +#: utils/fields.py:156 +msgid "Namibia" +msgstr "" + +#: utils/fields.py:157 +msgid "New Caledonia" +msgstr "" + +#: utils/fields.py:158 +msgid "Niger" +msgstr "" + +#: utils/fields.py:159 +msgid "Norfolk Island" +msgstr "" + +#: utils/fields.py:160 +msgid "Nigeria" +msgstr "" + +#: utils/fields.py:161 +msgid "Nicaragua" +msgstr "" + +#: utils/fields.py:162 +msgid "Netherlands" +msgstr "" + +#: utils/fields.py:163 +msgid "Norway" +msgstr "" + +#: utils/fields.py:164 +msgid "Nepal" +msgstr "" + +#: utils/fields.py:165 +msgid "Nauru" +msgstr "" + +#: utils/fields.py:166 +msgid "Niue" +msgstr "" + +#: utils/fields.py:167 +msgid "New Zealand" +msgstr "" + +#: utils/fields.py:168 +msgid "Oman" +msgstr "" + +#: utils/fields.py:169 +msgid "Panama" +msgstr "" + +#: utils/fields.py:170 +msgid "Peru" +msgstr "" + +#: utils/fields.py:171 +msgid "French Polynesia" +msgstr "" + +#: utils/fields.py:172 +msgid "Papua New Guinea" +msgstr "" + +#: utils/fields.py:173 +msgid "Philippines" +msgstr "" + +#: utils/fields.py:174 +msgid "Pakistan" +msgstr "" + +#: utils/fields.py:175 +msgid "Poland" +msgstr "" + +#: utils/fields.py:176 +msgid "St. Pierre & Miquelon" +msgstr "" + +#: utils/fields.py:177 +msgid "Pitcairn" +msgstr "" + +#: utils/fields.py:178 +msgid "Puerto Rico" +msgstr "" + +#: utils/fields.py:179 +msgid "Portugal" +msgstr "" + +#: utils/fields.py:180 +msgid "Palau" +msgstr "" + +#: utils/fields.py:181 +msgid "Paraguay" +msgstr "" + +#: utils/fields.py:182 +msgid "Qatar" +msgstr "" + +#: utils/fields.py:183 +msgid "Reunion" +msgstr "" + +#: utils/fields.py:184 +msgid "Romania" +msgstr "" + +#: utils/fields.py:185 +msgid "Russian Federation" +msgstr "" + +#: utils/fields.py:186 +msgid "Rwanda" +msgstr "" + +#: utils/fields.py:187 +msgid "Saudi Arabia" +msgstr "" + +#: utils/fields.py:188 +msgid "Solomon Islands" +msgstr "" + +#: utils/fields.py:189 +msgid "Seychelles" +msgstr "" + +#: utils/fields.py:190 +msgid "Sudan" +msgstr "" + +#: utils/fields.py:191 +msgid "Sweden" +msgstr "" + +#: utils/fields.py:192 +msgid "Singapore" +msgstr "" + +#: utils/fields.py:193 +msgid "St. Helena" +msgstr "" + +#: utils/fields.py:194 +msgid "Slovenia" +msgstr "" + +#: utils/fields.py:195 +msgid "Svalbard & Jan Mayen Islands" +msgstr "" + +#: utils/fields.py:196 +msgid "Slovakia" +msgstr "" + +#: utils/fields.py:197 +msgid "Sierra Leone" +msgstr "" + +#: utils/fields.py:198 +msgid "San Marino" +msgstr "" + +#: utils/fields.py:199 +msgid "Senegal" +msgstr "" + +#: utils/fields.py:200 +msgid "Somalia" +msgstr "" + +#: utils/fields.py:201 +msgid "Suriname" +msgstr "" + +#: utils/fields.py:202 +msgid "Sao Tome & Principe" +msgstr "" + +#: utils/fields.py:203 +msgid "El Salvador" +msgstr "" + +#: utils/fields.py:204 +msgid "Syrian Arab Republic" +msgstr "" + +#: utils/fields.py:205 +msgid "Swaziland" +msgstr "" + +#: utils/fields.py:206 +msgid "Turks & Caicos Islands" +msgstr "" + +#: utils/fields.py:207 +msgid "Chad" +msgstr "" + +#: utils/fields.py:208 +msgid "French Southern Territories" +msgstr "" + +#: utils/fields.py:209 +msgid "Togo" +msgstr "" + +#: utils/fields.py:210 +msgid "Thailand" +msgstr "" + +#: utils/fields.py:211 +msgid "Tajikistan" +msgstr "" + +#: utils/fields.py:212 +msgid "Tokelau" +msgstr "" + +#: utils/fields.py:213 +msgid "Turkmenistan" +msgstr "" + +#: utils/fields.py:214 +msgid "Tunisia" +msgstr "" + +#: utils/fields.py:215 +msgid "Tonga" +msgstr "" + +#: utils/fields.py:216 +msgid "East Timor" +msgstr "" + +#: utils/fields.py:217 +msgid "Turkey" +msgstr "" + +#: utils/fields.py:218 +msgid "Trinidad & Tobago" +msgstr "" + +#: utils/fields.py:219 +msgid "Tuvalu" +msgstr "" + +#: utils/fields.py:220 +msgid "Taiwan, Province of China" +msgstr "" + +#: utils/fields.py:221 +msgid "Tanzania, United Republic of" +msgstr "" + +#: utils/fields.py:222 +msgid "Ukraine" +msgstr "" + +#: utils/fields.py:223 +msgid "Uganda" +msgstr "" + +#: utils/fields.py:224 +msgid "United States Minor Outlying Islands" +msgstr "" + +#: utils/fields.py:225 +msgid "United States of America" +msgstr "" + +#: utils/fields.py:226 +msgid "Uruguay" +msgstr "" + +#: utils/fields.py:227 +msgid "Uzbekistan" +msgstr "" + +#: utils/fields.py:228 +msgid "Vatican City State (Holy See)" +msgstr "" + +#: utils/fields.py:229 +msgid "St. Vincent & the Grenadines" +msgstr "" + +#: utils/fields.py:230 +msgid "Venezuela" +msgstr "" + +#: utils/fields.py:231 +msgid "British Virgin Islands" +msgstr "" + +#: utils/fields.py:232 +msgid "United States Virgin Islands" +msgstr "" + +#: utils/fields.py:233 +msgid "Viet Nam" +msgstr "" + +#: utils/fields.py:234 +msgid "Vanuatu" +msgstr "" + +#: utils/fields.py:235 +msgid "Wallis & Futuna Islands" +msgstr "" + +#: utils/fields.py:236 +msgid "Samoa" +msgstr "" + +#: utils/fields.py:237 +msgid "Yemen" +msgstr "" + +#: utils/fields.py:238 +msgid "Mayotte" +msgstr "" + +#: utils/fields.py:239 +msgid "Yugoslavia" +msgstr "" + +#: utils/fields.py:240 +msgid "South Africa" +msgstr "" + +#: utils/fields.py:241 +msgid "Zambia" +msgstr "" + +#: utils/fields.py:242 +msgid "Zaire" +msgstr "" + +#: utils/fields.py:243 +msgid "Zimbabwe" +msgstr "" + +#: utils/forms.py:23 +msgid "Enter your name or company name" +msgstr "Gib Deinen Namen oder den Name Deines Unternehmens ein" + +#: utils/forms.py:47 +msgid "Your username and/or password were incorrect." +msgstr "Dein Benutzername und/oder Dein Passwort ist falsch." + +#: utils/forms.py:56 utils/forms.py:74 utils/forms.py:89 +msgid "User does not exist" +msgstr "Der Benutzer existiert nicht" + +#: utils/forms.py:71 +msgid "The account is already active." +msgstr "Das Benutzerkonto ist bereits aktiv." + +#: utils/forms.py:98 +msgid "The two password fields didn't match." +msgstr "Die beiden Passwörter stimmen nicht überein." + +#: utils/forms.py:100 +msgid "New password" +msgstr "Neues Passwort" + +#: utils/forms.py:102 +msgid "New password confirmation" +msgstr "Neues Passwort Bestätigung" + +#: utils/forms.py:129 utils/forms.py:150 utils/forms.py:180 +msgid "Cardholder Name" +msgstr "Name des Kartenbesitzer" + +#: utils/forms.py:130 utils/forms.py:151 +msgid "Street Address" +msgstr "" + +#: utils/forms.py:131 utils/forms.py:152 utils/forms.py:182 +msgid "City" +msgstr "" + +#: utils/forms.py:132 utils/forms.py:153 utils/forms.py:183 +msgid "Postal Code" +msgstr "" + +#: utils/forms.py:133 utils/forms.py:154 utils/forms.py:184 +msgid "Country" +msgstr "" + +#: utils/forms.py:134 utils/forms.py:155 utils/forms.py:185 +msgid "VAT Number" +msgstr "" + +#: utils/forms.py:139 utils/forms.py:202 +msgid "Name" +msgstr "" + +#: utils/forms.py:140 +msgid "Email Address" +msgstr "" + +#: utils/forms.py:149 utils/forms.py:203 +msgid "Email" +msgstr "E-Mail" + +#: utils/forms.py:163 +#, python-format +msgid "" +"The email %(email)s is already registered with us. Please reset your " +"password and access your account." +msgstr "" +"Diese E-Mail-Adresse %(email)s existiert bereits. Bitte setze dein Passwort " +"zurück auf dein Konto zuzugreifen." + +#: utils/forms.py:181 +msgid "Street Building" +msgstr "Gebäude" + +#: utils/forms.py:204 +msgid "Phone number" +msgstr "Telefon" + +#: utils/forms.py:205 +msgid "Message" +msgstr "Nachricht" + +#: utils/views.py:80 +msgid "An email with the activation link has been sent to you" +msgstr "Es wurde eine E-Mail mit dem Aktivierungslink an Dich gesendet." + +#: utils/views.py:103 +msgid "Account Activation" +msgstr "Accountaktivierung" + +#: utils/views.py:118 +msgid "The link to reset your password has been sent to your email" +msgstr "" +"Der Link zum Zurücksetzen deines Passwortes wurde an deine E-Mail gesendet" + +#: utils/views.py:139 +msgid "Password Reset" +msgstr "Passwort zurücksetzen" + +#: utils/views.py:171 +msgid "Password has been reset." +msgstr "Das Passwort wurde zurückgesetzt." + +#: utils/views.py:175 utils/views.py:177 +msgid "Password reset has not been successful." +msgstr "Das Zurücksetzen war nicht erfolgreich." + +#: utils/views.py:182 utils/views.py:184 +msgid "The reset password link is no longer valid." +msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht länger gültig." diff --git a/utils/mailer.py b/utils/mailer.py new file mode 100755 index 0000000..3ca5f82 --- /dev/null +++ b/utils/mailer.py @@ -0,0 +1,67 @@ +import six +from django.core.mail import send_mail +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string + + +class BaseEmail(object): + + def __init__(self, *args, **kwargs): + self.to = kwargs.get('to') + self.template_name = kwargs.get('template_name') + self.template_path = kwargs.get('template_path') + self.subject = kwargs.get('subject') + self.context = kwargs.get('context', {}) + self.template_full_path = '%s%s' % (self.template_path, self.template_name) + text_content = render_to_string('%s.txt' % self.template_full_path, self.context) + html_content = render_to_string('%s.html' % self.template_full_path, self.context) + + self.email = EmailMultiAlternatives(self.subject, text_content) + self.email.attach_alternative(html_content, "text/html") + if 'from_address' in kwargs: + self.email.from_email = kwargs.get('from_address') + else: + self.email.from_email = '(ungleich) ungleich Support ' + self.email.to = [kwargs.get('to', 'info@ungleich.ch')] + + def send(self): + self.email.send() + + +class BaseMailer(object): + def __init__(self): + self._slug = None + self.no_replay_mail = 'info@ungleich.ch' + + if not hasattr(self, '_to'): + self._to = None + + @property + def slug(self): + return self._slug + + @slug.setter + def slug(self, val): + assert isinstance(val, six.string_types), "slug is not string: %r" % val + self._slug = val + + @property + def registration(self): + return self.message + + @registration.setter + def registration(self, val): + msg = "registration is not dict with fields subject,message" + assert type(val) is dict, msg + assert val.get('subject') and val.get('message'), msg + self._message, self._subject, self._from = ( + val.get('message'), val.get('subject'), val.get('from')) + assert isinstance(self.slug, six.string_types), 'slug not set' + + def send_mail(self, to=None): + if not to: + to = self._to + if not self.message: + raise NotImplementedError + send_mail(self._subject, self._message, self.no_replay_mail, [to]) + diff --git a/utils/management/commands/optimize_frontend.py b/utils/management/commands/optimize_frontend.py new file mode 100755 index 0000000..8544ba6 --- /dev/null +++ b/utils/management/commands/optimize_frontend.py @@ -0,0 +1,497 @@ +""" +This command finds and creates a report for all the usage of css rules in +an app. It aims to optimize existing codebase as well as assist the frontend +developer when designing new components by avoiding unnecessary duplication and +suggesting more/optimal alternatives. + +Features: + Currently the command can find out and display: + - Media Breakpoints used in a stylesheet + - Duplicate selectors in a stylesheet + - Unused selectors + Work in progress to enable these features: + - Duplicate style declaration for same selector + - DOM validation + - Finding out dead styles (those that are always cancelled) + - Optimize media declarations + +Example: + $ python manage.py optimize_frontend datacenterlight + above command produces a file ../optimize_frontend.html which contains a + report with the above mentioned features +""" + +# import csv +import json +import logging +import os +import re +from collections import Counter, OrderedDict +# from itertools import zip_longest + +from django import template +from django.conf import settings +from django.contrib.staticfiles import finders +from django.core.management.base import BaseCommand + + +logger = logging.getLogger(__name__) + +RE_PATTERNS = { + 'view_html': '[\'\"](.*\.html)', + 'html_html': '{% (?:extends|include) [\'\"]?(.*\.html)', + 'html_style': '{% static [\'\"]?(.*\.css)', + 'css_media': ( + '^\s*\@media([^{]+)\{\s*([\s\S]*?})\s*}' + ), + 'css_selector': ( + '^\s*([.#\[:_A-Za-z][^{]*?)\s*' + '\s*{\s*([\s\S]*?)\s*}' + ), + 'html_class': 'class=[\'\"]([a-zA-Z0-9-_\s]*)', + 'html_id': 'id=[\'\"]([a-zA-Z0-9-_]*)' +} + + +class Command(BaseCommand): + help = ( + 'Finds unused and duplicate style declarations from the stylesheets ' + 'used in the templates of each app' + ) + requires_system_checks = False + + def add_arguments(self, parser): + # positional arguments + parser.add_argument( + 'apps', nargs='+', type=str, + help='name of the apps to be optimized' + ) + + # Named (optional) arguments + parser.add_argument( + '--together', + action='store_true', + help='optimize the apps together' + ) + parser.add_argument( + '--css', + action='store_true', + help='optimize only the css rules declared in each stylesheet' + ) + + def handle(self, *args, **options): + apps_list = options['apps'] + report = {} + for app in apps_list: + if options['css']: + report[app] = self.optimize_css(app) + # write report + write_report(report) + + def optimize_css(self, app_name): + """Optimize declarations inside a css stylesheet + + Args: + app_name (str): The application name + """ + # get html and css files used in the app + files = get_files(app_name) + # get_selectors_from_css + css_selectors = get_selectors_css(files['style']) + # get_selectors_from_html + html_selectors = get_selectors_html(files['html']) + report = { + 'css_dup': get_css_duplication(css_selectors), + 'css_unused': get_css_unused(css_selectors, html_selectors) + } + return report + + +def get_files(app_name): + """Get all the `html` and `css` files used in an app. + + Args: + app_name (str): The application name + + Returns: + dict: A dictonary containing Counter of occurence of each + html and css file in `html` and `style` fields respectively. + For example: + { + 'html': {'datacenterlight/success.html': 1}, + 'style': {'datacenterlight/css/bootstrap.min.css': 2} + } + """ + # the view file for the app + app_view = os.path.join(settings.PROJECT_DIR, app_name, 'views.py') + # get template files called from the view + all_html_list = file_match_pattern(app_view, 'view_html') + # list of unique template files + uniq_html_list = list(OrderedDict.fromkeys(all_html_list).keys()) + # list of stylesheets + all_style_list = [] + file_patterns = ['html_html', 'html_style'] + # get html and css files called from within templates + i = 0 + while i < len(uniq_html_list): + template_name = uniq_html_list[i] + try: + temp_files = templates_match_pattern( + template_name, file_patterns + ) + except template.exceptions.TemplateDoesNotExist as e: + print("template file not found: ", str(e)) + all_html_list = [ + h for h in all_html_list if h != template_name + ] + del uniq_html_list[i] + else: + all_html_list.extend(temp_files[0]) + uniq_html_list = list( + OrderedDict.fromkeys(all_html_list).keys() + ) + all_style_list.extend(temp_files[1]) + i += 1 + # counter dict for the html files called from view + result = { + 'html': Counter(all_html_list), + 'style': Counter(all_style_list) + } + # print(result) + return result + + +def get_selectors_css(files): + """Gets the selectors and declarations from a stylesheet. + + Args: + files (list): A list of path of stylesheets. + + Returns: + dict: A nested dictionary with the structre as + `{'file': {'media-selector': [('selectors',`declarations')]}}` + For example: + { + 'datacenterlight/css/landing-page.css':{ + '(min-width: 768px)': [ + ('.lead-right', 'text-align: right;'), + ] + } + } + """ + selectors = {} + media_selectors = {} + # get media selectors and other simple declarations + for file in files: + if any(vendor in file for vendor in ['bootstrap', 'font-awesome']): + continue + result = finders.find(file) + if result: + with open(result) as f: + data = f.read() + media_selectors[file] = string_match_pattern(data, 'css_media') + new_data = string_remove_pattern(data, 'css_media') + default_match = string_match_pattern(new_data, 'css_selector') + selectors[file] = { + 'default': [ + [' '.join(grp.split()) for grp in m] for m in default_match + ] + } + # get declarations from media queries + for file, match_list in media_selectors.items(): + for match in match_list: + query = match[0] + block_text = ' '.join(match[1].split()) + results = string_match_pattern( + block_text, 'css_selector' + ) + f_query = ' '.join(query.replace(':', ': ').split()) + if f_query in selectors[file]: + selectors[file][f_query].extend(results) + else: + selectors[file][f_query] = results + return selectors + + +def get_selectors_html(files): + """Get `class` and `id` used in html files. + + Args: + files (list): A list of html files path. + + Returns: + dict: a dictonary of all the classes and ids found in the file, in + `class` and `id` field respectively. + """ + selectors = {} + for file in files: + results = templates_match_pattern(file, ['html_class', 'html_id']) + class_dict = {c: 1 for match in results[0] for c in match.split()} + selectors[file] = { + 'classes': list(class_dict.keys()), + 'ids': results[1], + } + return selectors + + +def file_match_pattern(file, patterns): + """Match a regex pattern in a file + + Args: + file (str): Complete path of file + patterns (list or str): The pattern(s) to be searched in the file + + Returns: + list: A list of all the matches in the file. Each item is a list of + all the captured groups in the pattern. If multiple patterns are given, + the returned list is a list of such lists. + For example: + [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')] + """ + with open(file) as f: + data = f.read() + results = string_match_pattern(data, patterns) + return results + + +def string_match_pattern(data, patterns): + """Match a regex pattern in a string + + Args: + data (str): the string to search for the pattern + patterns (list or str): The pattern(s) to be searched in the file + + Returns: + list: A list of all the matches in the string. Each item is a list of + all the captured groups in the pattern. If multiple patterns are given, + the returned list is a list of such lists. + For example: + [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')] + """ + if not isinstance(patterns, str): + results = [] + for p in patterns: + re_pattern = re.compile(RE_PATTERNS[p], re.MULTILINE) + results.append(re.findall(re_pattern, data)) + else: + re_pattern = re.compile(RE_PATTERNS[patterns], re.MULTILINE) + results = re.findall(re_pattern, data) + return results + + +def string_remove_pattern(data, patterns): + """Remove a pattern from a string + + Args: + data (str): the string to search for the patter + patterns (list or str): The pattern(s) to be removed from the file + + Returns: + str: The new string with all instance of matching pattern + removed from it + """ + if not isinstance(patterns, str): + for p in patterns: + re_pattern = re.compile(RE_PATTERNS[p], re.MULTILINE) + data = re.sub(re_pattern, '', data) + else: + re_pattern = re.compile(RE_PATTERNS[patterns], re.MULTILINE) + data = re.sub(re_pattern, '', data) + return data + + +def templates_match_pattern(template_name, patterns): + """Match a regex pattern in the first found template file + + Args: + file (str): Path of template file + patterns (list or str): The pattern(s) to be searched in the file + + Returns: + list: A list of all the matches in the file. Each item is a list of + all the captured groups in the pattern. If multiple patterns are given, + the returned list is a list of such lists. + For example: + [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')] + """ + t = template.loader.get_template(template_name) + data = t.template.source + results = string_match_pattern(data, patterns) + return results + + +def get_css_duplication(css_selectors): + """Get duplicate selectors from the same stylesheet + + Args: + css_selectors (dict): A dictonary containing css selectors from + all the files in the app in the below structure. + `{'file': {'media-selector': [('selectors',`declarations')]}}` + + Returns: + dict: A dictonary containing the count of any duplicate selector in + each file. + `{'file': {'media-selector': {'selector': count}}}` + """ + # duplicate css selectors in stylesheets + rule_count = {} + for file, media_selectors in css_selectors.items(): + rule_count[file] = {} + for media, rules in media_selectors.items(): + rules_dict = Counter([rule[0] for rule in rules]) + dup_rules_dict = {k: v for k, v in rules_dict.items() if v > 1} + if dup_rules_dict: + rule_count[file][media] = dup_rules_dict + return rule_count + + +def get_css_unused(css_selectors, html_selectors): + """Get selectors from stylesheets that are not used in any of the html + files in which the stylesheet is used. + + Args: + css_selectors (dict): A dictonary containing css selectors from + all the files in the app in the below structure. + `{'file': {'media-selector': [('selectors',`declarations')]}}` + html_selectors (dict): A dictonary containing the 'class' and 'id' + declarations from all html files + """ + with open('utils/optimize/test.json', 'w') as f: + json.dump([html_selectors, css_selectors], f, indent=4) + # print(html_selectors, css_selectors) + + +def write_report(all_reports, filename='frontend'): + """Write the generated report to a file for re-use + + Args; + all_reports (dict): A dictonary of report obtained from different tests + filename (str): An optional suffix for the output file + """ + # full_filename = 'utils/optimize/optimize_' + filename + '.html' + # output_file = os.path.join( + # settings.PROJECT_DIR, full_filename + # ) + with open('utils/optimize/op_frontend.json', 'w') as f: + json.dump(all_reports, f, indent=4) + # with open(output_file, 'w', newline='') as f: + # f.write( + # template.loader.render_to_string( + # 'utils/report.html', {'all_reports': all_reports} + # ) + # ) + # w = csv.writer(f) + # print(zip_longest(*results)) + # for r in zip_longest(*results): + # w.writerow(r) + + +# a list of all the html tags (to be moved in a json file) +html_tags = [ + "a", + "abbr", + "address", + "article", + "area", + "aside", + "audio", + "b", + "base", + "bdi", + "bdo", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "datalist", + "dd", + "del", + "details", + "dfn", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hgroup", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "map", + "mark", + "menu", + "meta", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "script", + "section", + "select", + "source", + "small", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "textarea", + "table", + "tbody", + "td", + "tfoot", + "thead", + "th", + "time", + "title", + "tr", + "u", + "ul", + "var", + "video", + "wbr" +] diff --git a/utils/middleware.py b/utils/middleware.py new file mode 100755 index 0000000..56623bf --- /dev/null +++ b/utils/middleware.py @@ -0,0 +1,17 @@ +#class MultipleProxyMiddleware(object): +# FORWARDED_FOR_FIELDS = [ +# 'HTTP_X_FORWARDED_FOR', +# 'HTTP_X_FORWARDED_HOST', +# 'HTTP_X_FORWARDED_SERVER', +# ] +# +# def process_request(self, request): +# """ +# Rewrites the proxy headers so that only the most +# recent proxy is used. +# """ +# for field in self.FORWARDED_FOR_FIELDS: +# if field in request.META: +# if ',' in request.META[field]: +# parts = request.META[field].split(',') +# request.META[field] = parts[-1].strip() diff --git a/utils/migrations/0001_initial.py b/utils/migrations/0001_initial.py new file mode 100644 index 0000000..4f5dcf3 --- /dev/null +++ b/utils/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 4.2.7 on 2023-12-02 12:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import utils.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BillingAddress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cardholder_name', models.CharField(default='', max_length=100)), + ('street_address', models.CharField(max_length=100)), + ('city', models.CharField(max_length=50)), + ('postal_code', models.CharField(max_length=50)), + ('country', utils.fields.CountryField(choices=[('AD', 'Andorra'), ('AE', 'United Arab Emirates'), ('AF', 'Afghanistan'), ('AG', 'Antigua & Barbuda'), ('AI', 'Anguilla'), ('AL', 'Albania'), ('AM', 'Armenia'), ('AN', 'Netherlands Antilles'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('AR', 'Argentina'), ('AS', 'American Samoa'), ('AT', 'Austria'), ('AU', 'Australia'), ('AW', 'Aruba'), ('AZ', 'Azerbaijan'), ('BA', 'Bosnia and Herzegovina'), ('BB', 'Barbados'), ('BD', 'Bangladesh'), ('BE', 'Belgium'), ('BF', 'Burkina Faso'), ('BG', 'Bulgaria'), ('BH', 'Bahrain'), ('BI', 'Burundi'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BN', 'Brunei Darussalam'), ('BO', 'Bolivia'), ('BR', 'Brazil'), ('BS', 'Bahama'), ('BT', 'Bhutan'), ('BV', 'Bouvet Island'), ('BW', 'Botswana'), ('BY', 'Belarus'), ('BZ', 'Belize'), ('CA', 'Canada'), ('CC', 'Cocos (Keeling) Islands'), ('CF', 'Central African Republic'), ('CG', 'Congo'), ('CH', 'Switzerland'), ('CI', 'Ivory Coast'), ('CK', 'Cook Iislands'), ('CL', 'Chile'), ('CM', 'Cameroon'), ('CN', 'China'), ('CO', 'Colombia'), ('CR', 'Costa Rica'), ('CU', 'Cuba'), ('CV', 'Cape Verde'), ('CX', 'Christmas Island'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DE', 'Germany'), ('DJ', 'Djibouti'), ('DK', 'Denmark'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('DZ', 'Algeria'), ('EC', 'Ecuador'), ('EE', 'Estonia'), ('EG', 'Egypt'), ('EH', 'Western Sahara'), ('ER', 'Eritrea'), ('ES', 'Spain'), ('ET', 'Ethiopia'), ('FI', 'Finland'), ('FJ', 'Fiji'), ('FK', 'Falkland Islands (Malvinas)'), ('FM', 'Micronesia'), ('FO', 'Faroe Islands'), ('FR', 'France'), ('FX', 'France, Metropolitan'), ('GA', 'Gabon'), ('GB', 'United Kingdom (Great Britain)'), ('GD', 'Grenada'), ('GE', 'Georgia'), ('GF', 'French Guiana'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GL', 'Greenland'), ('GM', 'Gambia'), ('GN', 'Guinea'), ('GP', 'Guadeloupe'), ('GQ', 'Equatorial Guinea'), ('GR', 'Greece'), ('GS', 'South Georgia and the South Sandwich Islands'), ('GT', 'Guatemala'), ('GU', 'Guam'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HK', 'Hong Kong'), ('HM', 'Heard & McDonald Islands'), ('HN', 'Honduras'), ('HR', 'Croatia'), ('HT', 'Haiti'), ('HU', 'Hungary'), ('ID', 'Indonesia'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IN', 'India'), ('IO', 'British Indian Ocean Territory'), ('IQ', 'Iraq'), ('IR', 'Islamic Republic of Iran'), ('IS', 'Iceland'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('JP', 'Japan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('KH', 'Cambodia'), ('KI', 'Kiribati'), ('KM', 'Comoros'), ('KN', 'St. Kitts and Nevis'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KY', 'Cayman Islands'), ('KZ', 'Kazakhstan'), ('LA', "Lao People's Democratic Republic"), ('LB', 'Lebanon'), ('LC', 'Saint Lucia'), ('LI', 'Liechtenstein'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('LS', 'Lesotho'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('LV', 'Latvia'), ('LY', 'Libyan Arab Jamahiriya'), ('MA', 'Morocco'), ('MC', 'Monaco'), ('MD', 'Moldova, Republic of'), ('MG', 'Madagascar'), ('MH', 'Marshall Islands'), ('ML', 'Mali'), ('MN', 'Mongolia'), ('MM', 'Myanmar'), ('MO', 'Macau'), ('MP', 'Northern Mariana Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MS', 'Monserrat'), ('MT', 'Malta'), ('MU', 'Mauritius'), ('MV', 'Maldives'), ('MW', 'Malawi'), ('MX', 'Mexico'), ('MY', 'Malaysia'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('NC', 'New Caledonia'), ('NE', 'Niger'), ('NF', 'Norfolk Island'), ('NG', 'Nigeria'), ('NI', 'Nicaragua'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NP', 'Nepal'), ('NR', 'Nauru'), ('NU', 'Niue'), ('NZ', 'New Zealand'), ('OM', 'Oman'), ('PA', 'Panama'), ('PE', 'Peru'), ('PF', 'French Polynesia'), ('PG', 'Papua New Guinea'), ('PH', 'Philippines'), ('PK', 'Pakistan'), ('PL', 'Poland'), ('PM', 'St. Pierre & Miquelon'), ('PN', 'Pitcairn'), ('PR', 'Puerto Rico'), ('PT', 'Portugal'), ('PW', 'Palau'), ('PY', 'Paraguay'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('SC', 'Seychelles'), ('SD', 'Sudan'), ('SE', 'Sweden'), ('SG', 'Singapore'), ('SH', 'St. Helena'), ('SI', 'Slovenia'), ('SJ', 'Svalbard & Jan Mayen Islands'), ('SK', 'Slovakia'), ('SL', 'Sierra Leone'), ('SM', 'San Marino'), ('SN', 'Senegal'), ('SO', 'Somalia'), ('SR', 'Suriname'), ('ST', 'Sao Tome & Principe'), ('SV', 'El Salvador'), ('SY', 'Syrian Arab Republic'), ('SZ', 'Swaziland'), ('TC', 'Turks & Caicos Islands'), ('TD', 'Chad'), ('TF', 'French Southern Territories'), ('TG', 'Togo'), ('TH', 'Thailand'), ('TJ', 'Tajikistan'), ('TK', 'Tokelau'), ('TM', 'Turkmenistan'), ('TN', 'Tunisia'), ('TO', 'Tonga'), ('TP', 'East Timor'), ('TR', 'Turkey'), ('TT', 'Trinidad & Tobago'), ('TV', 'Tuvalu'), ('TW', 'Taiwan, Province of China'), ('TZ', 'Tanzania, United Republic of'), ('UA', 'Ukraine'), ('UG', 'Uganda'), ('UM', 'United States Minor Outlying Islands'), ('US', 'United States of America'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VA', 'Vatican City State (Holy See)'), ('VC', 'St. Vincent & the Grenadines'), ('VE', 'Venezuela'), ('VG', 'British Virgin Islands'), ('VI', 'United States Virgin Islands'), ('VN', 'Viet Nam'), ('VU', 'Vanuatu'), ('WF', 'Wallis & Futuna Islands'), ('WS', 'Samoa'), ('YE', 'Yemen'), ('YT', 'Mayotte'), ('YU', 'Yugoslavia'), ('ZA', 'South Africa'), ('ZM', 'Zambia'), ('ZR', 'Zaire'), ('ZW', 'Zimbabwe')], default='CH', max_length=2)), + ('vat_number', models.CharField(blank=True, default='', max_length=100)), + ('stripe_tax_id', models.CharField(blank=True, default='', max_length=100)), + ('vat_number_validated_on', models.DateTimeField(blank=True, null=True)), + ('vat_validation_status', models.CharField(blank=True, default='', max_length=25)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ContactMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('email', models.EmailField(max_length=254)), + ('phone_number', models.CharField(blank=True, max_length=200)), + ('message', models.TextField()), + ('received_date', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='UserBillingAddress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cardholder_name', models.CharField(default='', max_length=100)), + ('street_address', models.CharField(max_length=100)), + ('city', models.CharField(max_length=50)), + ('postal_code', models.CharField(max_length=50)), + ('country', utils.fields.CountryField(choices=[('AD', 'Andorra'), ('AE', 'United Arab Emirates'), ('AF', 'Afghanistan'), ('AG', 'Antigua & Barbuda'), ('AI', 'Anguilla'), ('AL', 'Albania'), ('AM', 'Armenia'), ('AN', 'Netherlands Antilles'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('AR', 'Argentina'), ('AS', 'American Samoa'), ('AT', 'Austria'), ('AU', 'Australia'), ('AW', 'Aruba'), ('AZ', 'Azerbaijan'), ('BA', 'Bosnia and Herzegovina'), ('BB', 'Barbados'), ('BD', 'Bangladesh'), ('BE', 'Belgium'), ('BF', 'Burkina Faso'), ('BG', 'Bulgaria'), ('BH', 'Bahrain'), ('BI', 'Burundi'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BN', 'Brunei Darussalam'), ('BO', 'Bolivia'), ('BR', 'Brazil'), ('BS', 'Bahama'), ('BT', 'Bhutan'), ('BV', 'Bouvet Island'), ('BW', 'Botswana'), ('BY', 'Belarus'), ('BZ', 'Belize'), ('CA', 'Canada'), ('CC', 'Cocos (Keeling) Islands'), ('CF', 'Central African Republic'), ('CG', 'Congo'), ('CH', 'Switzerland'), ('CI', 'Ivory Coast'), ('CK', 'Cook Iislands'), ('CL', 'Chile'), ('CM', 'Cameroon'), ('CN', 'China'), ('CO', 'Colombia'), ('CR', 'Costa Rica'), ('CU', 'Cuba'), ('CV', 'Cape Verde'), ('CX', 'Christmas Island'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DE', 'Germany'), ('DJ', 'Djibouti'), ('DK', 'Denmark'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('DZ', 'Algeria'), ('EC', 'Ecuador'), ('EE', 'Estonia'), ('EG', 'Egypt'), ('EH', 'Western Sahara'), ('ER', 'Eritrea'), ('ES', 'Spain'), ('ET', 'Ethiopia'), ('FI', 'Finland'), ('FJ', 'Fiji'), ('FK', 'Falkland Islands (Malvinas)'), ('FM', 'Micronesia'), ('FO', 'Faroe Islands'), ('FR', 'France'), ('FX', 'France, Metropolitan'), ('GA', 'Gabon'), ('GB', 'United Kingdom (Great Britain)'), ('GD', 'Grenada'), ('GE', 'Georgia'), ('GF', 'French Guiana'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GL', 'Greenland'), ('GM', 'Gambia'), ('GN', 'Guinea'), ('GP', 'Guadeloupe'), ('GQ', 'Equatorial Guinea'), ('GR', 'Greece'), ('GS', 'South Georgia and the South Sandwich Islands'), ('GT', 'Guatemala'), ('GU', 'Guam'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HK', 'Hong Kong'), ('HM', 'Heard & McDonald Islands'), ('HN', 'Honduras'), ('HR', 'Croatia'), ('HT', 'Haiti'), ('HU', 'Hungary'), ('ID', 'Indonesia'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IN', 'India'), ('IO', 'British Indian Ocean Territory'), ('IQ', 'Iraq'), ('IR', 'Islamic Republic of Iran'), ('IS', 'Iceland'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('JP', 'Japan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('KH', 'Cambodia'), ('KI', 'Kiribati'), ('KM', 'Comoros'), ('KN', 'St. Kitts and Nevis'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KY', 'Cayman Islands'), ('KZ', 'Kazakhstan'), ('LA', "Lao People's Democratic Republic"), ('LB', 'Lebanon'), ('LC', 'Saint Lucia'), ('LI', 'Liechtenstein'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('LS', 'Lesotho'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('LV', 'Latvia'), ('LY', 'Libyan Arab Jamahiriya'), ('MA', 'Morocco'), ('MC', 'Monaco'), ('MD', 'Moldova, Republic of'), ('MG', 'Madagascar'), ('MH', 'Marshall Islands'), ('ML', 'Mali'), ('MN', 'Mongolia'), ('MM', 'Myanmar'), ('MO', 'Macau'), ('MP', 'Northern Mariana Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MS', 'Monserrat'), ('MT', 'Malta'), ('MU', 'Mauritius'), ('MV', 'Maldives'), ('MW', 'Malawi'), ('MX', 'Mexico'), ('MY', 'Malaysia'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('NC', 'New Caledonia'), ('NE', 'Niger'), ('NF', 'Norfolk Island'), ('NG', 'Nigeria'), ('NI', 'Nicaragua'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NP', 'Nepal'), ('NR', 'Nauru'), ('NU', 'Niue'), ('NZ', 'New Zealand'), ('OM', 'Oman'), ('PA', 'Panama'), ('PE', 'Peru'), ('PF', 'French Polynesia'), ('PG', 'Papua New Guinea'), ('PH', 'Philippines'), ('PK', 'Pakistan'), ('PL', 'Poland'), ('PM', 'St. Pierre & Miquelon'), ('PN', 'Pitcairn'), ('PR', 'Puerto Rico'), ('PT', 'Portugal'), ('PW', 'Palau'), ('PY', 'Paraguay'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('SC', 'Seychelles'), ('SD', 'Sudan'), ('SE', 'Sweden'), ('SG', 'Singapore'), ('SH', 'St. Helena'), ('SI', 'Slovenia'), ('SJ', 'Svalbard & Jan Mayen Islands'), ('SK', 'Slovakia'), ('SL', 'Sierra Leone'), ('SM', 'San Marino'), ('SN', 'Senegal'), ('SO', 'Somalia'), ('SR', 'Suriname'), ('ST', 'Sao Tome & Principe'), ('SV', 'El Salvador'), ('SY', 'Syrian Arab Republic'), ('SZ', 'Swaziland'), ('TC', 'Turks & Caicos Islands'), ('TD', 'Chad'), ('TF', 'French Southern Territories'), ('TG', 'Togo'), ('TH', 'Thailand'), ('TJ', 'Tajikistan'), ('TK', 'Tokelau'), ('TM', 'Turkmenistan'), ('TN', 'Tunisia'), ('TO', 'Tonga'), ('TP', 'East Timor'), ('TR', 'Turkey'), ('TT', 'Trinidad & Tobago'), ('TV', 'Tuvalu'), ('TW', 'Taiwan, Province of China'), ('TZ', 'Tanzania, United Republic of'), ('UA', 'Ukraine'), ('UG', 'Uganda'), ('UM', 'United States Minor Outlying Islands'), ('US', 'United States of America'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VA', 'Vatican City State (Holy See)'), ('VC', 'St. Vincent & the Grenadines'), ('VE', 'Venezuela'), ('VG', 'British Virgin Islands'), ('VI', 'United States Virgin Islands'), ('VN', 'Viet Nam'), ('VU', 'Vanuatu'), ('WF', 'Wallis & Futuna Islands'), ('WS', 'Samoa'), ('YE', 'Yemen'), ('YT', 'Mayotte'), ('YU', 'Yugoslavia'), ('ZA', 'South Africa'), ('ZM', 'Zambia'), ('ZR', 'Zaire'), ('ZW', 'Zimbabwe')], default='CH', max_length=2)), + ('vat_number', models.CharField(blank=True, default='', max_length=100)), + ('stripe_tax_id', models.CharField(blank=True, default='', max_length=100)), + ('vat_number_validated_on', models.DateTimeField(blank=True, null=True)), + ('vat_validation_status', models.CharField(blank=True, default='', max_length=25)), + ('current', models.BooleanField(default=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='billing_addresses', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/utils/migrations/__init__.py b/utils/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/mixins.py b/utils/mixins.py new file mode 100755 index 0000000..ebcb221 --- /dev/null +++ b/utils/mixins.py @@ -0,0 +1,31 @@ +from guardian.shortcuts import assign_perm + + +class AssignPermissionsMixin(object): + permissions = tuple() + user = None + obj = None + kwargs = dict() + + def assign_permissions(self, user): + for permission in self.permissions: + assign_perm(permission, user, self) + + # def save(self, *args, **kwargs): + # self.kwargs = kwargs + # self.get_objs() + + # create = False + # if not self.pk: + # create = True + + # super(AssignPermissionsMixin, self).save(*args, **kwargs) + + # if create: + # self.assign_permissions() + + # def get_objs(self): + # self.user = self.kwargs.pop('user', None) + # self.obj = self.kwargs.pop('obj', None) + # assert self.user, 'Se necesita el parámetro user para poder asignar los permisos' + # assert self.obj, 'Se necesita el parámetro obj para poder asignar los permisos' diff --git a/utils/models.py b/utils/models.py new file mode 100755 index 0000000..98882c5 --- /dev/null +++ b/utils/models.py @@ -0,0 +1,79 @@ +from django.db import models + +from membership.models import CustomUser + +from .fields import CountryField + + +# Create your models here. + +class BaseBillingAddress(models.Model): + cardholder_name = models.CharField(max_length=100, default="") + street_address = models.CharField(max_length=100) + city = models.CharField(max_length=50) + postal_code = models.CharField(max_length=50) + country = CountryField() + vat_number = models.CharField(max_length=100, default="", blank=True) + stripe_tax_id = models.CharField(max_length=100, default="", blank=True) + vat_number_validated_on = models.DateTimeField(blank=True, null=True) + vat_validation_status = models.CharField(max_length=25, default="", + blank=True) + + class Meta: + abstract = True + + +class BillingAddress(BaseBillingAddress): + def __str__(self): + if self.vat_number: + return "%s, %s, %s, %s, %s, %s %s %s %s" % ( + self.cardholder_name, self.street_address, self.city, + self.postal_code, self.country, self.vat_number, + self.stripe_tax_id, self.vat_number_validated_on, + self.vat_validation_status + ) + else: + return "%s, %s, %s, %s, %s" % ( + self.cardholder_name, self.street_address, self.city, + self.postal_code, self.country + ) + + +class UserBillingAddress(BaseBillingAddress): + user = models.ForeignKey(CustomUser, related_name='billing_addresses', on_delete=models.CASCADE) + current = models.BooleanField(default=True) + + def __str__(self): + if self.vat_number: + return "%s, %s, %s, %s, %s, %s %s %s %s" % ( + self.cardholder_name, self.street_address, self.city, + self.postal_code, self.country, self.vat_number, + self.stripe_tax_id, self.vat_number_validated_on, + self.vat_validation_status + ) + else: + return "%s, %s, %s, %s, %s" % ( + self.cardholder_name, self.street_address, self.city, + self.postal_code, self.country + ) + + def to_dict(self): + return { + 'Cardholder Name': self.cardholder_name, + 'Street Address': self.street_address, + 'City': self.city, + 'Postal Code': self.postal_code, + 'Country': self.country, + 'VAT Number': self.vat_number + } + + +class ContactMessage(models.Model): + name = models.CharField(max_length=200) + email = models.EmailField() + phone_number = models.CharField(max_length=200, blank=True) + message = models.TextField() + received_date = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return "%s - %s - %s" % (self.name, self.email, self.received_date) diff --git a/utils/optimize/.gitkeep b/utils/optimize/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py new file mode 100644 index 0000000..d4b3e9e --- /dev/null +++ b/utils/stripe_utils.py @@ -0,0 +1,573 @@ +import logging +import re + +import stripe +from django.conf import settings + +from datacenterlight.models import StripePlan + +stripe.api_key = settings.STRIPE_API_PRIVATE_KEY +logger = logging.getLogger(__name__) + + +def handleStripeError(f): + def handleProblems(*args, **kwargs): + response = { + 'paid': False, + 'response_object': None, + 'error': None + } + + common_message = "Currently it's not possible to make payments." + try: + response_object = f(*args, **kwargs) + response = { + 'response_object': response_object, + 'error': None + } + return response + except stripe.error.CardError as e: + # Since it's a decline, stripe.error.CardError will be caught + body = e.json_body + err = body['error'] + response.update({'error': err['message']}) + logger.error(str(e)) + return response + except stripe.error.RateLimitError as e: + logger.error(str(e)) + response.update( + {'error': "Too many requests made to the API too quickly"}) + return response + except stripe.error.InvalidRequestError as e: + logger.error(str(e)) + response.update({'error': str(e._message)}) + return response + except stripe.error.AuthenticationError as e: + # Authentication with Stripe's API failed + # (maybe you changed API keys recently) + logger.error(str(e)) + response.update({'error': str(e)}) + return response + except stripe.error.APIConnectionError as e: + logger.error(str(e)) + response.update({'error': str(e)}) + return response + except stripe.error.StripeError as e: + # maybe send email + logger.error(str(e)) + response.update({'error': str(e)}) + return response + except Exception as e: + # maybe send email + logger.error(str(e)) + response.update({'error': str(e)}) + return response + + return handleProblems + + +class StripeUtils(object): + CURRENCY = 'chf' + INTERVAL = 'month' + SUCCEEDED_STATUS = 'succeeded' + RESOURCE_ALREADY_EXISTS_ERROR_CODE = 'resource_already_exists' + STRIPE_NO_SUCH_PLAN = 'No such plan' + PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.' + PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.' + + def __init__(self): + self.stripe = stripe + + def update_customer_token(self, customer, token): + customer.source = token + customer.save() + + @handleStripeError + def associate_customer_card(self, stripe_customer_id, id_payment_method, + set_as_default=False): + customer = stripe.Customer.retrieve(stripe_customer_id) + stripe.PaymentMethod.attach( + id_payment_method, + customer=stripe_customer_id, + ) + if set_as_default: + customer.invoice_settings.default_payment_method = id_payment_method + customer.save() + return True + + @handleStripeError + def dissociate_customer_card(self, stripe_customer_id, card_id): + customer = stripe.Customer.retrieve(stripe_customer_id) + if card_id.startswith("pm"): + logger.debug("PaymentMethod %s detached %s" % (card_id, + stripe_customer_id)) + pm = stripe.PaymentMethod.retrieve(card_id) + stripe.PaymentMethod.detach(card_id) + pm.delete() + else: + logger.debug("card %s detached %s" % (card_id, stripe_customer_id)) + card = customer.sources.retrieve(card_id) + card.delete() + + @handleStripeError + def update_customer_card(self, customer_id, token): + customer = stripe.Customer.retrieve(customer_id) + current_card_token = customer.default_source + customer.sources.retrieve(current_card_token).delete() + customer.source = token + customer.save() + credit_card_raw_data = customer.sources.data.pop() + new_card_data = { + 'last4': credit_card_raw_data.last4, + 'brand': credit_card_raw_data.brand + } + return new_card_data + + @handleStripeError + def get_card_details(self, customer_id): + customer = stripe.Customer.retrieve(customer_id) + credit_card_raw_data = customer.sources.data.pop() + card_details = { + 'last4': credit_card_raw_data.last4, + 'brand': credit_card_raw_data.brand, + 'exp_month': credit_card_raw_data.exp_month, + 'exp_year': credit_card_raw_data.exp_year, + 'fingerprint': credit_card_raw_data.fingerprint, + 'card_id': credit_card_raw_data.id + } + return card_details + + @handleStripeError + def get_all_invoices(self, customer_id, created_gt): + return_list = [] + has_more_invoices = True + starting_after = False + while has_more_invoices: + if starting_after: + invoices = stripe.Invoice.list( + limit=10, customer=customer_id, created={'gt': created_gt}, + starting_after=starting_after + ) + else: + invoices = stripe.Invoice.list( + limit=10, customer=customer_id, created={'gt': created_gt} + ) + has_more_invoices = invoices.has_more + for invoice in invoices.data: + sub_ids = [] + for line in invoice.lines.data: + if line.type == 'subscription': + sub_ids.append(line.id) + elif line.type == 'invoiceitem': + sub_ids.append(line.subscription) + else: + sub_ids.append('') + invoice_details = { + 'created': invoice.created, + 'receipt_number': invoice.receipt_number, + 'invoice_number': invoice.number, + 'paid_at': invoice.status_transitions.paid_at if invoice.paid else 0, + 'period_start': invoice.period_start, + 'period_end': invoice.period_end, + 'billing_reason': invoice.billing_reason, + 'discount': invoice.discount.coupon.amount_off if invoice.discount else 0, + 'total': invoice.total, + # to see how many line items we have in this invoice and + # then later check if we have more than 1 + 'lines_data_count': len(invoice.lines.data) if invoice.lines.data is not None else 0, + 'invoice_id': invoice.id, + 'lines_meta_data_csv': ','.join( + [line.metadata.VM_ID if hasattr(line.metadata, 'VM_ID') else '' for line in invoice.lines.data] + ), + 'subscription_ids_csv': ','.join(sub_ids), + 'line_items': invoice.lines.data + } + starting_after = invoice.id + return_list.append(invoice_details) + return return_list + + @handleStripeError + def get_cards_details_from_token(self, token): + stripe_token = stripe.Token.retrieve(token) + card_details = { + 'last4': stripe_token.card.last4, + 'brand': stripe_token.card.brand, + 'exp_month': stripe_token.card.exp_month, + 'exp_year': stripe_token.card.exp_year, + 'fingerprint': stripe_token.card.fingerprint, + 'card_id': stripe_token.card.id + } + return card_details + + @handleStripeError + def get_cards_details_from_payment_method(self, payment_method_id): + payment_method = stripe.PaymentMethod.retrieve(payment_method_id) + # payment_method does not always seem to have a card with id + # if that is the case, fallback to payment_method_id for card_id + card_id = payment_method_id + if hasattr(payment_method.card, 'id'): + card_id = payment_method.card.id + card_details = { + 'last4': payment_method.card.last4, + 'brand': payment_method.card.brand, + 'exp_month': payment_method.card.exp_month, + 'exp_year': payment_method.card.exp_year, + 'fingerprint': payment_method.card.fingerprint, + 'card_id': card_id + } + return card_details + + def check_customer(self, stripe_cus_api_id, user, token): + try: + customer = stripe.Customer.retrieve(stripe_cus_api_id) + except stripe.InvalidRequestError: + customer = self.create_customer(token, user.email, user.name) + user.stripecustomer.stripe_id = customer.get( + 'response_object').get('id') + user.stripecustomer.save() + if type(customer) is dict: + customer = customer['response_object'] + return customer + + @handleStripeError + def get_customer(self, stripe_api_cus_id): + customer = stripe.Customer.retrieve(stripe_api_cus_id) + # data = customer.get('response_object') + return customer + + @handleStripeError + def create_customer(self, id_payment_method, email, name=None): + if name is None or name.strip() == "": + name = email + customer = self.stripe.Customer.create( + payment_method=id_payment_method, + description=name, + email=email + ) + return customer + + @handleStripeError + def make_charge(self, amount=None, customer=None): + _amount = float(amount) + amount = int(_amount * 100) # stripe amount unit, in cents + charge = self.stripe.Charge.create( + amount=amount, # in cents + currency=self.CURRENCY, + customer=customer + ) + return charge + + @handleStripeError + def get_or_create_stripe_plan(self, amount, name, stripe_plan_id, + interval=""): + """ + This function checks if a StripePlan with the given + stripe_plan_id already exists. If it exists then the function + returns this object otherwise it creates a new StripePlan and + returns the new object. + + :param amount: The amount in CHF + :param name: The name of the Stripe plan to be created. + :param stripe_plan_id: The id of the Stripe plan to be + created. Use get_stripe_plan_id_string function to + obtain the name of the plan to be created + :param interval: str representing the interval of the Plan + Specifies billing frequency. Either day, week, month or year. + Ref: https://stripe.com/docs/api/plans/create#create_plan-interval + The default is month + :return: The StripePlan object if it exists else creates a + Plan object in Stripe and a local StripePlan and + returns it. Returns None in case of Stripe error + """ + _amount = float(amount) + amount = int(_amount * 100) # stripe amount unit, in cents + stripe_plan_db_obj = None + plan_interval = interval if interval != "" else self.INTERVAL + try: + stripe_plan_db_obj = StripePlan.objects.get( + stripe_plan_id=stripe_plan_id) + except StripePlan.DoesNotExist: + try: + self.stripe.Plan.create( + amount=amount, + interval=plan_interval, + name=name, + currency=self.CURRENCY, + id=stripe_plan_id) + stripe_plan_db_obj = StripePlan.objects.create( + stripe_plan_id=stripe_plan_id) + except stripe.error.InvalidRequestError as e: + logger.error(str(e)) + logger.error("error_code = %s" % str(e.__dict__)) + if self.RESOURCE_ALREADY_EXISTS_ERROR_CODE in e.error.code: + logger.debug( + self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id)) + stripe_plan_db_obj, c = StripePlan.objects.get_or_create( + stripe_plan_id=stripe_plan_id) + if c: + logger.debug("Created stripe plan %s" % stripe_plan_id) + else: + logger.debug("Plan %s exists already" % stripe_plan_id) + return stripe_plan_db_obj + + @handleStripeError + def delete_stripe_plan(self, stripe_plan_id): + """ + Deletes the Plan in Stripe and also deletes the local db copy + of the plan if it exists + + :param stripe_plan_id: The stripe plan id that needs to be + deleted + :return: True if the plan was deleted successfully from + Stripe, False otherwise. + """ + return_value = False + try: + plan = self.stripe.Plan.retrieve(stripe_plan_id) + plan.delete() + return_value = True + StripePlan.objects.filter( + stripe_plan_id=stripe_plan_id).all().delete() + except stripe.error.InvalidRequestError as e: + if self.STRIPE_NO_SUCH_PLAN in str(e): + logger.debug( + self.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(stripe_plan_id)) + return return_value + + @handleStripeError + def subscribe_customer_to_plan(self, customer, plans, trial_end=None, + coupon="", tax_rates=list(), + default_payment_method=""): + """ + Subscribes the given customer to the list of given plans + + :param default_payment_method: + :param tax_rates: + :param coupon: + :param customer: The stripe customer identifier + :param plans: A list of stripe plans. + :param trial_end: An integer representing when the Stripe subscription + is supposed to end + Ref: https://stripe.com/docs/api/python#create_subscription-items + e.g. + plans = [ + { + "plan": "dcl-v1-cpu-2-ram-5gb-ssd-10gb", + }, + ] + :return: The subscription StripeObject + """ + logger.debug("Subscribing %s to plan %s : coupon = %s" % ( + customer, str(plans), str(coupon) + )) + subscription_result = self.stripe.Subscription.create( + customer=customer, items=plans, trial_end=trial_end, + coupon=coupon, + default_tax_rates=tax_rates, + payment_behavior='allow_incomplete', + default_payment_method=default_payment_method + ) + logger.debug("Done subscribing") + return subscription_result + + @handleStripeError + def set_subscription_metadata(self, subscription_id, metadata): + subscription = stripe.Subscription.retrieve(subscription_id) + subscription.metadata = metadata + subscription.save() + + @handleStripeError + def unsubscribe_customer(self, subscription_id): + """ + Cancels a given subscription + + :param subscription_id: The Stripe subscription id string + :return: + """ + sub = stripe.Subscription.retrieve(subscription_id) + return sub.delete() + + @handleStripeError + def make_payment(self, customer, amount, token): + charge = self.stripe.Charge.create( + amount=amount, # in cents + currency=self.CURRENCY, + customer=customer + ) + return charge + + @staticmethod + def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None, + price=None, excl_vat=True): + """ + Returns the Stripe plan id string of the form + `dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters + + :param cpu: The number of cores + :param ram: The size of the RAM in GB + :param ssd: The size of ssd storage in GB + :param hdd: The size of hdd storage in GB + :param version: The version of the Stripe plans + :param app: The application to which the stripe plan belongs + to. By default it is 'dcl' + :param price: The price for this plan + :return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb` + """ + dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu, + ram=ram, + ssd=ssd) + if hdd is not None: + dcl_plan_string = '{dcl_plan_string}-hdd-{hdd}gb'.format( + dcl_plan_string=dcl_plan_string, hdd=hdd) + stripe_plan_id_string = '{app}-v{version}-{plan}'.format( + app=app, + version=version, + plan=dcl_plan_string + ) + if price is not None: + stripe_plan_id_string = '{}-{}chf'.format( + stripe_plan_id_string, + round(price, 2) + ) + if excl_vat: + stripe_plan_id_string = '{}-{}'.format( + stripe_plan_id_string, + "excl_vat" + ) + return stripe_plan_id_string + + @staticmethod + def get_vm_config_from_stripe_id(stripe_id): + """ + Given a string like "dcl-v1-cpu-2-ram-5gb-ssd-10gb" return different + configuration params as a dict + + :param stripe_id|str + :return: dict + """ + pattern = re.compile(r'^dcl-v(\d+)-cpu-(\d+)-ram-(\d+\.?\d*)gb-ssd-(\d+)gb-?(\d*\.?\d*)(chf)?$') + match_res = pattern.match(stripe_id) + if match_res is not None: + price = None + try: + price=match_res.group(5) + except IndexError as ie: + logger.debug("Did not find price in {}".format(stripe_id)) + return { + 'version': match_res.group(1), + 'cores': match_res.group(2), + 'ram': match_res.group(3), + 'ssd': match_res.group(4), + 'price': price + } + + + @staticmethod + def get_stripe_plan_name(cpu, memory, disk_size, price, excl_vat=True): + """ + Returns the Stripe plan name + :return: + """ + if excl_vat: + return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \ + "{price} CHF Excl. VAT".format( + cpu=cpu, + memory=memory, + disk_size=disk_size, + price=round(price, 2) + ) + else: + return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \ + "{price} CHF".format( + cpu=cpu, + memory=memory, + disk_size=disk_size, + price=round(price, 2) + ) + + @handleStripeError + def set_subscription_meta_data(self, subscription_id, meta_data): + """ + Adds VM metadata to a subscription + :param subscription_id: Stripe identifier for the subscription + :param meta_data: A dict of meta data to be added + :return: + """ + subscription = stripe.Subscription.retrieve(subscription_id) + subscription.metadata = meta_data + subscription.save() + + @handleStripeError + def get_or_create_tax_id_for_user(self, stripe_customer_id, vat_number, + type="eu_vat", country=""): + tax_ids_list = stripe.Customer.list_tax_ids( + stripe_customer_id, + limit=100, + ) + for tax_id_obj in tax_ids_list.data: + if self.compare_vat_numbers(tax_id_obj.value, vat_number): + logger.debug("tax id obj exists already") + return tax_id_obj + else: + logger.debug( + "{val1} is not equal to {val2} or {con1} not same as " + "{con2}".format(val1=tax_id_obj.value, val2=vat_number, + con1=tax_id_obj.country.lower(), + con2=country.lower().strip())) + logger.debug( + "tax id obj does not exist for {val}. Creating a new one".format( + val=vat_number + )) + tax_id_obj = stripe.Customer.create_tax_id( + stripe_customer_id, + type=type, + value=vat_number, + ) + return tax_id_obj + + @handleStripeError + def get_payment_intent(self, amount, customer): + """ Create a stripe PaymentIntent of the given amount and return it + :param amount: the amount of payment_intent + :return: + """ + payment_intent_obj = stripe.PaymentIntent.create( + amount=amount, + currency='chf', + customer=customer, + setup_future_usage='off_session' + ) + return payment_intent_obj + + @handleStripeError + def get_available_payment_methods(self, customer): + """ Retrieves all payment methods of the given customer + :param customer: StripeCustomer object + :return: a list of available payment methods + """ + return_list = [] + if customer is None: + return return_list + cu = stripe.Customer.retrieve(customer.stripe_id) + pms = stripe.PaymentMethod.list( + customer=customer.stripe_id, + type="card", + ) + default_source = None + if cu.default_source: + default_source = cu.default_source + else: + default_source = cu.invoice_settings.default_payment_method + for pm in pms.data: + return_list.append({ + 'last4': pm.card.last4, 'brand': pm.card.brand, 'id': pm.id, + 'exp_year': pm.card.exp_year, + 'exp_month': '{:02d}'.format(pm.card.exp_month), + 'preferred': pm.id == default_source + }) + return return_list + + def compare_vat_numbers(self, vat1, vat2): + _vat1 = vat1.replace(" ", "").replace(".", "").replace("-","") + _vat2 = vat2.replace(" ", "").replace(".", "").replace("-","") + return True if _vat1 == _vat2 else False diff --git a/utils/tasks.py b/utils/tasks.py new file mode 100644 index 0000000..06d1fb9 --- /dev/null +++ b/utils/tasks.py @@ -0,0 +1,98 @@ +import tempfile + +import cdist +from cdist.integration import configure_hosts_simple +from celery.result import AsyncResult +from celery import current_task +from celery.utils.log import get_task_logger +from django.conf import settings +from django.core.mail import EmailMessage + +from dynamicweb2.pr_celery import app + +logger = get_task_logger(__name__) + + +@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES) +def send_plain_email_task(self, email_data): + """ + This is a generic celery task to be used for sending emails. + A celery wrapper task for EmailMessage + + :param self: + :param email_data: A dict of all needed email headers + :return: + """ + email = EmailMessage(**email_data) + email.send() + + +@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES) +def save_ssh_key(self, hosts, keys): + """ + Saves ssh key into the VMs of a user using cdist + + :param hosts: A list of hosts to be configured + :param keys: A list of keys to be added. A key should be dict of the + form { + 'value': 'sha-.....', # public key as string + 'state': True # whether key is to be added or + } # removed + """ + logger.debug( + "Running save_ssh_key on {}".format(current_task.request.hostname)) + logger.debug("""Running save_ssh_key task for + Hosts: {hosts_str} + Keys: {keys_str}""".format(hosts_str=", ".join(hosts), + keys_str=", ".join([ + "{value}->{state}".format( + value=key.get('value'), + state=str( + key.get('state'))) + for key in keys])) + ) + return_value = True + with tempfile.NamedTemporaryFile(delete=True) as tmp_manifest: + # Generate manifest to be used for configuring the hosts + lines_list = [ + ' --key "{key}" --state {state} \\\n'.format( + key=key['value'], + state='present' if key['state'] else 'absent' + ).encode('utf-8') + for key in keys] + lines_list.insert(0, b'__ssh_authorized_keys root \\\n') + tmp_manifest.writelines(lines_list) + tmp_manifest.flush() + try: + configure_hosts_simple(hosts, + tmp_manifest.name, + verbose=cdist.argparse.VERBOSE_TRACE) + except Exception as cdist_exception: + logger.error(cdist_exception) + return_value = False + email_data = { + 'subject': "celery save_ssh_key error - task id {0}".format( + self.request.id.__str__()), + 'from_email': current_task.request.hostname, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, + 'body': "Task Id: {0}\nResult: {1}\nTraceback: {2}".format( + self.request.id.__str__(), False, str(cdist_exception)), + } + send_plain_email_task(email_data) + return return_value + + +@app.task +def save_ssh_key_error_handler(uuid): + result = AsyncResult(uuid) + exc = result.get(propagate=False) + logger.error('Task {0} raised exception: {1!r}\n{2!r}'.format( + uuid, exc, result.traceback)) + email_data = { + 'subject': "[celery error] Save SSH key error {0}".format(uuid), + 'from_email': current_task.request.hostname, + 'to': settings.DCL_ERROR_EMAILS_TO_LIST, + 'body': "Task Id: {0}\nResult: {1}\nTraceback: {2}".format( + uuid, exc, result.traceback), + } + send_plain_email_task(email_data) diff --git a/utils/test_forms.py b/utils/test_forms.py new file mode 100755 index 0000000..5bca7f3 --- /dev/null +++ b/utils/test_forms.py @@ -0,0 +1,96 @@ +from django.test import TestCase +from .forms import ContactUsForm, BillingAddressForm, PasswordResetRequestForm,\ + SetPasswordForm + +from model_mommy import mommy + + +class PasswordResetRequestFormTest(TestCase): + + def setUp(self): + self.user = mommy.make('CustomUser') + self.completed_data = { + 'email': self.user.email, + } + + self.incorrect_data = { + 'email': 'test', + } + + def test_valid_form(self): + form = PasswordResetRequestForm(data=self.completed_data) + self.assertTrue(form.is_valid()) + + def test_invalid_form(self): + form = PasswordResetRequestForm(data=self.incorrect_data) + self.assertFalse(form.is_valid()) + + +class SetPasswordFormTest(TestCase): + + def setUp(self): + # self.user = mommy.make('CustomUser') + self.completed_data = { + 'new_password1': 'new_password', + 'new_password2': 'new_password', + } + + self.incorrect_data = { + 'email': 'test', + } + + def test_valid_form(self): + form = SetPasswordForm(data=self.completed_data) + self.assertTrue(form.is_valid()) + + def test_invalid_form(self): + form = SetPasswordForm(data=self.incorrect_data) + self.assertFalse(form.is_valid()) + + +class ContactUsFormTest(TestCase): + + def setUp(self): + self.completed_data = { + 'name': 'test', + 'email': 'test@gmail.com', + 'phone_number': '32123123123123', + 'message': 'This is a message', + } + + self.incompleted_data = { + 'name': 'test', + } + + def test_valid_form(self): + form = ContactUsForm(data=self.completed_data) + self.assertTrue(form.is_valid()) + + def test_invalid_form(self): + form = ContactUsForm(data=self.incompleted_data) + self.assertFalse(form.is_valid()) + + +class BillingAddressFormTest(TestCase): + + def setUp(self): + self.completed_data = { + 'cardholder_name': 'test', + 'street_address': 'street name', + 'city': 'MyCity', + 'postal_code': '32123123123123', + 'country': 'VE', + 'token': 'a23kfmslwxhkwis' + } + + self.incompleted_data = { + 'street_address': 'test', + } + + def test_valid_form(self): + form = BillingAddressForm(data=self.completed_data) + self.assertTrue(form.is_valid()) + + def test_invalid_form(self): + form = BillingAddressForm(data=self.incompleted_data) + self.assertFalse(form.is_valid()) diff --git a/utils/tests.py b/utils/tests.py new file mode 100755 index 0000000..8abbbb1 --- /dev/null +++ b/utils/tests.py @@ -0,0 +1,306 @@ +import uuid +from time import sleep +from unittest.mock import patch + +import stripe +from celery.result import AsyncResult +from django.conf import settings +from django.http.request import HttpRequest +from django.test import Client +from django.test import TestCase, override_settings +from unittest import skipIf +from model_mommy import mommy + +from datacenterlight.models import StripePlan +from membership.models import StripeCustomer +from utils.stripe_utils import StripeUtils +from .tasks import save_ssh_key + + +class BaseTestCase(TestCase): + """ + Base class to initialize the test cases + """ + + def setUp(self): + # Password + self.dummy_password = 'test_password' + + # Users + self.customer, self.another_customer = mommy.make( + 'membership.CustomUser', validated=1, _quantity=2 + ) + self.customer.set_password(self.dummy_password) + self.customer.save() + self.another_customer.set_password(self.dummy_password) + self.another_customer.save() + + # Stripe mocked data + self.stripe_mocked_customer = self.customer_stripe_mocked_data() + + # Clients + self.customer_client = self.get_client(self.customer) + self.another_customer_client = self.get_client(self.another_customer) + + # Request Object + self.request = HttpRequest() + self.request.META['SERVER_NAME'] = 'ungleich.ch' + self.request.META['SERVER_PORT'] = '80' + + def get_client(self, user): + """ + Authenticate a user and return the client + """ + client = Client() + client.login(email=user.email, password=self.dummy_password) + return client + + def customer_stripe_mocked_data(self): + return { + "id": "cus_8R1y9UWaIIjZqr", + "object": "customer", + "currency": "usd", + "default_source": "card_18A9up2eZvKYlo2Cq2RJMGeF", + "email": "vmedixtodd+1@gmail.com", + "livemode": False, + "metadata": { + }, + "shipping": None, + "sources": { + "object": "list", + "data": [{ + "id": "card_18A9up2eZvKYlo2Cq2RJMGeF", + "object": "card", + "brand": "Visa", + "country": "US", + "customer": "cus_8R1y9UWaIIjZqr", + "cvc_check": "pass", + "dynamic_last4": None, + "exp_month": 12, + "exp_year": 2018, + "funding": "credit", + "last4": "4242", + }] + } + } + + def setup_view(self, view, *args, **kwargs): + """Mimic as_view() returned callable, but returns view instance. + + args and kwargs are the same you would pass to ``reverse()`` + + """ + view.request = self.request + view.args = args + view.kwargs = kwargs + view.config = None + return view + + +@skipIf(settings.STRIPE_API_PRIVATE_KEY_TEST is None or + settings.STRIPE_API_PRIVATE_KEY_TEST is "", + """Skip because STRIPE_API_PRIVATE_KEY_TEST is not set""") +class TestStripeCustomerDescription(TestCase): + """ + A class to test setting the description field of the stripe customer + https://stripe.com/docs/api#metadata + """ + + def setUp(self): + self.customer_password = 'test_password' + self.customer_email = 'test@ungleich.ch' + self.customer_name = "Monty Python" + self.customer = mommy.make('membership.CustomUser') + self.customer.set_password(self.customer_password) + self.customer.email = self.customer_email + self.customer.save() + self.stripe_utils = StripeUtils() + stripe.api_key = settings.STRIPE_API_PRIVATE_KEY_TEST + self.token = stripe.Token.create( + card={ + "number": '4111111111111111', + "exp_month": 12, + "exp_year": 2022, + "cvc": '123' + }, + ) + self.failed_token = stripe.Token.create( + card={ + "number": '4000000000000341', + "exp_month": 12, + "exp_year": 2022, + "cvc": '123' + }, + ) + + def test_creating_stripe_customer(self): + stripe_data = self.stripe_utils.create_customer(self.token.id, + self.customer.email, + self.customer_name) + self.assertEqual(stripe_data.get('error'), None) + customer_data = stripe_data.get('response_object') + self.assertEqual(customer_data.description, self.customer_name) + + +@skipIf(settings.STRIPE_API_PRIVATE_KEY_TEST == "" or + settings.TEST_MANAGE_SSH_KEY_HOST == "", + """Skipping test_save_ssh_key_add because either host + or public key were not specified or were empty""") +class StripePlanTestCase(TestStripeCustomerDescription): + """ + A class to test Stripe plans + """ + + def test_get_stripe_plan_id_string(self): + plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, + version=1, app='dcl') + self.assertEqual(plan_id_string, 'dcl-v1-cpu-2-ram-20gb-ssd-100gb') + plan_id_string = StripeUtils.get_stripe_plan_id(cpu=2, ram=20, ssd=100, + version=1, app='dcl', + hdd=200) + self.assertEqual(plan_id_string, + 'dcl-v1-cpu-2-ram-20gb-ssd-100gb-hdd-200gb') + + def test_get_or_create_plan(self): + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan 1", + stripe_plan_id='test-plan-1') + self.assertIsNone(stripe_plan.get('error')) + self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) + + @skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY == "" or + settings.TEST_MANAGE_SSH_KEY_HOST == "", + """Skipping test_save_ssh_key_add because either host + or public key were not specified or were empty""") + @patch('utils.stripe_utils.logger') + def test_create_duplicate_plans_error_handling(self, mock_logger): + """ + Test details: + 1. Create a test plan in Stripe with a particular id + 2. Try to recreate the plan with the same id + 3. This creates a Stripe error, the code should be able to handle the error + + :param mock_logger: + :return: + """ + unique_id = str(uuid.uuid4().hex) + new_plan_id_str = 'test-plan-{}'.format(unique_id) + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan {}".format( + unique_id), + stripe_plan_id=new_plan_id_str) + self.assertIsInstance(stripe_plan.get('response_object'), StripePlan) + self.assertEqual(stripe_plan.get('response_object').stripe_plan_id, + new_plan_id_str) + + # Test creating the same plan again and expect the PLAN_EXISTS_ERROR_MSG + # We first delete the local Stripe Plan, so that the code tries to create a new plan in Stripe + StripePlan.objects.filter( + stripe_plan_id=new_plan_id_str).all().delete() + stripe_plan_1 = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan {}".format( + unique_id), + stripe_plan_id=new_plan_id_str) + mock_logger.debug.assert_called_with( + self.stripe_utils.PLAN_EXISTS_ERROR_MSG.format(new_plan_id_str)) + self.assertIsInstance(stripe_plan_1.get('response_object'), StripePlan) + self.assertEqual(stripe_plan_1.get('response_object').stripe_plan_id, + new_plan_id_str) + + # Delete the test stripe plan that we just created + delete_result = self.stripe_utils.delete_stripe_plan(new_plan_id_str) + self.assertIsInstance(delete_result, dict) + self.assertEqual(delete_result.get('response_object'), True) + + @patch('utils.stripe_utils.logger') + def test_delete_unexisting_plan_should_fail(self, mock_logger): + plan_id = 'crazy-plan-id-that-does-not-exist' + result = self.stripe_utils.delete_stripe_plan(plan_id) + self.assertIsInstance(result, dict) + self.assertEqual(result.get('response_object'), False) + mock_logger.debug.assert_called_with( + self.stripe_utils.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(plan_id)) + + def test_subscribe_customer_to_plan(self): + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan 1", + stripe_plan_id='test-plan-1') + stripe_customer = StripeCustomer.get_or_create( + email=self.customer_email, + token=self.token) + result = self.stripe_utils.subscribe_customer_to_plan( + stripe_customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + self.assertIsInstance(result.get('response_object'), + stripe.Subscription) + self.assertIsNone(result.get('error')) + self.assertEqual(result.get('response_object').get('status'), 'active') + + def test_subscribe_customer_to_plan_failed_payment(self): + stripe_plan = self.stripe_utils.get_or_create_stripe_plan(2000, + "test plan 1", + stripe_plan_id='test-plan-1') + stripe_customer = StripeCustomer.get_or_create( + email=self.customer_email, + token=self.failed_token) + result = self.stripe_utils.subscribe_customer_to_plan( + stripe_customer.stripe_id, + [{"plan": stripe_plan.get( + 'response_object').stripe_plan_id}]) + self.assertIsNone(result.get('response_object'), None) + self.assertIsNotNone(result.get('error')) + + +class SaveSSHKeyTestCase(TestCase): + """ + A test case to test the celery save_ssh_key task + """ + + @override_settings( + task_eager_propagates=True, + task_always_eager=True, + ) + def setUp(self): + self.public_key = settings.TEST_MANAGE_SSH_KEY_PUBKEY + self.hosts = settings.TEST_MANAGE_SSH_KEY_HOST + + @skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY is "" or + settings.TEST_MANAGE_SSH_KEY_PUBKEY is None or + settings.TEST_MANAGE_SSH_KEY_HOST is "" or + settings.TEST_MANAGE_SSH_KEY_HOST is None, + """Skipping test_save_ssh_key_add because either host + or public key were not specified or were empty""") + def test_save_ssh_key_add(self): + async_task = save_ssh_key.delay([self.hosts], + [{'value': self.public_key, + 'state': True}]) + save_ssh_key_result = None + for i in range(0, 10): + sleep(5) + res = AsyncResult(async_task.task_id) + if type(res.result) is bool: + save_ssh_key_result = res.result + break + self.assertIsNotNone(save_ssh_key, "save_ssh_key_result is None") + self.assertTrue(save_ssh_key_result, "save_ssh_key_result is False") + + @skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY is None or + settings.TEST_MANAGE_SSH_KEY_PUBKEY == "" or + settings.TEST_MANAGE_SSH_KEY_HOST is None or + settings.TEST_MANAGE_SSH_KEY_HOST is "", + """Skipping test_save_ssh_key_add because either host + or public key were not specified or were empty""") + def test_save_ssh_key_remove(self): + async_task = save_ssh_key.delay([self.hosts], + [{'value': self.public_key, + 'state': False}]) + save_ssh_key_result = None + for i in range(0, 10): + sleep(5) + res = AsyncResult(async_task.task_id) + if type(res.result) is bool: + save_ssh_key_result = res.result + break + self.assertIsNotNone(save_ssh_key, "save_ssh_key_result is None") + self.assertTrue(save_ssh_key_result, "save_ssh_key_result is False") diff --git a/utils/views.py b/utils/views.py new file mode 100755 index 0000000..a780f5f --- /dev/null +++ b/utils/views.py @@ -0,0 +1,269 @@ +import uuid + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import authenticate, login +from django.contrib.auth.tokens import default_token_generator +from django.core.files.base import ContentFile +from django.urls import reverse_lazy +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.cache import cache_control +from django.views.generic import FormView, CreateView + +from datacenterlight.utils import get_cms_integration +from hosting.forms import UserHostingKeyForm +from hosting.models import UserHostingKey +from membership.models import CustomUser +from opennebula_api.opennebula_manager import OpenNebulaManager +from utils.hosting_utils import get_all_public_keys +from .forms import SetPasswordForm +from .mailer import BaseEmail + + +class SignupViewMixin(CreateView): + model = CustomUser + success_url = None + + def get_success_url(self): + next_url = self.request.POST.get('next') if self.request.POST.get( + 'next') \ + else self.success_url + + return next_url + + def form_valid(self, form): + name = form.cleaned_data.get('name') + email = form.cleaned_data.get('email') + password = form.cleaned_data.get('password') + + CustomUser.register(name, password, email) + auth_user = authenticate(email=email, password=password) + login(self.request, auth_user) + + return HttpResponseRedirect(self.get_success_url()) + + +class LoginViewMixin(FormView): + success_url = None + + def get_success_url(self): + next_url = self.request.POST.get('next', self.success_url) + if not next_url: + return self.success_url + return next_url + + def form_valid(self, form): + email = form.cleaned_data.get('email') + password = form.cleaned_data.get('password') + auth_user = authenticate(email=email, password=password) + + if auth_user: + login(self.request, auth_user) + return HttpResponseRedirect(self.get_success_url()) + + return HttpResponseRedirect(self.get_success_url()) + + @cache_control(no_cache=True, must_revalidate=True, no_store=True) + def get(self, request, *args, **kwargs): + if self.request.user.is_authenticated(): + return HttpResponseRedirect(self.get_success_url()) + + return super(LoginViewMixin, self).get(request, *args, **kwargs) + + +class ResendActivationLinkViewMixin(FormView): + success_message = _( + "An email with the activation link has been sent to you") + + def generate_email_context(self, user): + context = { + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()), + 'activation_link': reverse_lazy( + 'hosting:validate', + kwargs={'validate_slug': user.validation_slug} + ), + 'dcl_text': settings.DCL_TEXT, + } + return context + + def form_valid(self, form): + email = form.cleaned_data.get('email') + user = CustomUser.objects.get(email=email) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) + context = self.generate_email_context(user) + email_data = { + 'subject': '{dcl_text} {account_activation}'.format( + dcl_text=settings.DCL_TEXT, + account_activation=_('Account Activation') + ), + 'to': email, + 'context': context, + 'template_name': self.email_template_name, + 'template_path': self.email_template_path, + 'from_address': settings.DCL_SUPPORT_FROM_ADDRESS + } + email = BaseEmail(**email_data) + email.send() + return HttpResponseRedirect(self.get_success_url()) + + +class PasswordResetViewMixin(FormView): + success_message = _( + "The link to reset your password has been sent to your email") + site = '' + + def test_generate_email_context(self, user): + context = { + 'user': user, + 'token': default_token_generator.make_token(user), + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + 'site_name': 'ungleich' if self.site != 'dcl' else settings.DCL_TEXT, + 'base_url': "{0}://{1}".format(self.request.scheme, + self.request.get_host()) + } + return context + + def form_valid(self, form): + email = form.cleaned_data.get('email') + user = CustomUser.objects.get(email=email) + messages.add_message(self.request, messages.SUCCESS, + self.success_message) + context = self.test_generate_email_context(user) + email_data = { + 'subject': _('Password Reset'), + 'to': email, + 'context': context, + 'template_name': 'password_reset_email', + 'template_path': self.template_email_path + } + if self.site == 'dcl': + email_data['from_address'] = settings.DCL_SUPPORT_FROM_ADDRESS + email = BaseEmail(**email_data) + email.send() + + return HttpResponseRedirect(self.get_success_url()) + + +class PasswordResetConfirmViewMixin(FormView): + form_class = SetPasswordForm + + def post(self, request, uidb64=None, token=None, *arg, **kwargs): + try: + uid = urlsafe_base64_decode(uidb64) + user = CustomUser.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist): + user = None + + form = self.form_class(request.POST) + + if user is not None and default_token_generator.check_token(user, + token): + if form.is_valid(): + new_password = form.cleaned_data['new_password2'] + user.set_password(new_password) + user.save() + messages.success(request, _('Password has been reset.')) + return self.form_valid(form) + else: + messages.error(request, + _('Password reset has not been successful.')) + form.add_error(None, + _('Password reset has not been successful.')) + return self.form_invalid(form) + + else: + messages.error(request, + _('The reset password link is no longer valid.')) + form.add_error(None, + _('The reset password link is no longer valid.')) + return self.form_invalid(form) + + +class SSHKeyCreateView(FormView): + form_class = UserHostingKeyForm + model = UserHostingKey + template_name = 'hosting/user_key.html' + login_url = reverse_lazy('hosting:login') + context_object_name = "virtual_machine" + success_url = reverse_lazy('hosting:ssh_keys') + + def get_form_kwargs(self): + kwargs = super(SSHKeyCreateView, self).get_form_kwargs() + kwargs.update({'request': self.request}) + return kwargs + + def form_valid(self, form): + form.save() + if settings.DCL_SSH_KEY_NAME_PREFIX in form.instance.name: + content = ContentFile(form.cleaned_data.get('private_key')) + filename = form.cleaned_data.get( + 'name') + '_' + str(uuid.uuid4())[:8] + '_private.pem' + form.instance.private_key.save(filename, content) + context = self.get_context_data() + + next_url = self.request.session.get( + 'next', + reverse_lazy('hosting:create_virtual_machine') + ) + + if 'next' in self.request.session: + context.update({ + 'next_url': next_url + }) + del (self.request.session['next']) + + if form.cleaned_data.get('private_key'): + context.update({ + 'private_key': form.cleaned_data.get('private_key'), + 'key_name': form.cleaned_data.get('name'), + 'form': UserHostingKeyForm(request=self.request), + }) + + if self.request.user.is_authenticated(): + owner = self.request.user + manager = OpenNebulaManager( + email=owner.username, + password=owner.password + ) + keys_to_save = get_all_public_keys(self.request.user) + manager.save_key_in_opennebula_user('\n'.join(keys_to_save)) + else: + self.request.session["new_user_hosting_key_id"] = form.instance.id + return HttpResponseRedirect(self.success_url) + + def post(self, request, *args, **kwargs): + form = self.get_form() + required = 'add_ssh' in self.request.POST + form.fields['name'].required = required + form.fields['public_key'].required = required + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + +class AskSSHKeyView(SSHKeyCreateView): + form_class = UserHostingKeyForm + template_name = "datacenterlight/add_ssh_key.html" + success_url = reverse_lazy('datacenterlight:order_confirmation') + context_object_name = "dcl_vm_buy_add_ssh_key" + + @cache_control(no_cache=True, must_revalidate=True, no_store=True) + def get(self, request, *args, **kwargs): + context = { + 'site_url': reverse_lazy('datacenterlight:index'), + 'cms_integration': get_cms_integration('default'), + 'form': UserHostingKeyForm(request=self.request), + 'keys': get_all_public_keys(self.request.user) + } + return render(request, self.template_name, context) + + def post(self, request, *args, **kwargs): + self.success_url = self.request.session.get("order_confirm_url") + return super(AskSSHKeyView, self).post(self, request, *args, **kwargs) \ No newline at end of file