""" This module contains the hosting package models. """ from __future__ import absolute_import, unicode_literals from django.conf import settings from django.db import transaction from django.db import models from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _, ungettext from model_utils import Choices from model_utils.models import TimeStampedModel from domains.models import HostingDomain from managemails.models import Mailbox from osusers.models import ( AdditionalGroup, Group, User as OsUser, ) from userdbs.models import ( DB_TYPES, UserDatabase, ) DISK_SPACE_UNITS = Choices( (0, 'M', _('MiB')), (1, 'G', _('GiB')), (2, 'T', _('TiB')), ) DISK_SPACE_FACTORS = ( (1, None, None), (1024, 1, None), (1024 * 1024, 1024, 1), ) @python_2_unicode_compatible class HostingPackageBase(TimeStampedModel): description = models.TextField(_('description'), blank=True) mailboxcount = models.PositiveIntegerField(_('mailbox count')) diskspace = models.PositiveIntegerField( _('disk space'), help_text=_('disk space for the hosting package')) diskspace_unit = models.PositiveSmallIntegerField( _('unit of disk space'), choices=DISK_SPACE_UNITS) class Meta: abstract = True def __str__(self): return self.name class HostingPackageTemplate(HostingPackageBase): name = models.CharField(_('name'), max_length=128, unique=True) class Meta: verbose_name = _('Hosting package') verbose_name_plural = _('Hosting packages') class HostingOption(TimeStampedModel): """ This is the base class for several types of hosting options. """ @python_2_unicode_compatible class DiskSpaceOptionBase(models.Model): diskspace = models.PositiveIntegerField(_('disk space')) diskspace_unit = models.PositiveSmallIntegerField( _('unit of disk space'), choices=DISK_SPACE_UNITS) class Meta: abstract = True ordering = ['diskspace_unit', 'diskspace'] verbose_name = _('Disk space option') verbose_name_plural = _('Disk space options') def __str__(self): return _("Additional disk space {space} {unit}").format( space=self.diskspace, unit=self.get_diskspace_unit_display()) class DiskSpaceOption(DiskSpaceOptionBase, HostingOption): """ This is a class for hosting options adding additional disk space to existing hosting packages. """ class Meta: unique_together = ['diskspace', 'diskspace_unit'] @python_2_unicode_compatible class UserDatabaseOptionBase(models.Model): number = models.PositiveIntegerField( _('number of databases'), default=1) db_type = models.PositiveSmallIntegerField( _('database type'), choices=DB_TYPES) class Meta: abstract = True ordering = ['db_type', 'number'] verbose_name = _('Database option') verbose_name_plural = _('Database options') def __str__(self): return ungettext( '{type} database', '{count} {type} databases', self.number ).format( type=self.get_db_type_display(), count=self.number ) class UserDatabaseOption(UserDatabaseOptionBase, HostingOption): """ This is a class for hosting options adding user databases to existing hosting packages. """ class Meta: unique_together = ['number', 'db_type'] @python_2_unicode_compatible class MailboxOptionBase(models.Model): """ Base class for mailbox options. """ number = models.PositiveIntegerField( _('number of mailboxes'), unique=True) class Meta: abstract = True ordering = ['number'] verbose_name = _('Mailbox option') verbose_name_plural = _('Mailbox options') def __str__(self): return ungettext( '{count} additional mailbox', '{count} additional mailboxes', self.number ).format( count=self.number ) class MailboxOption(MailboxOptionBase, HostingOption): """ This is a class for hosting options adding more mailboxes to existing hosting packages. """ class CustomerHostingPackageManager(models.Manager): """ This is the default manager implementation for :py:class:`CustomerHostingPackage`. """ def create_from_template(self, customer, template, name, **kwargs): """ Use this method to create a new :py:class:`CustomerHostingPackage` from a :py:class:`HostingPackageTemplate`. The method copies data from the template to the new :py:class:`CustomerHostingPackage` instance. :param customer: a Django user representing a customer :param template: a :py:class:`HostingPackageTemplate` :param str name: the name of the hosting package there must only be one hosting package of this name for each customer :return: customer hosting package :rtype: :py:class:`CustomerHostingPackage` """ package = CustomerHostingPackage( customer=customer, template=template, name=name) package.description = template.description package.copy_template_attributes() if 'commit' in kwargs and kwargs['commit'] is True: package.save(**kwargs) return package @python_2_unicode_compatible class CustomerHostingPackage(HostingPackageBase): """ This class defines customer specific hosting packages. """ customer = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('customer'), on_delete=models.CASCADE) template = models.ForeignKey( HostingPackageTemplate, verbose_name=_('hosting package template'), help_text=_( 'The hosting package template that this hosting package is based' ' on' ), on_delete=models.CASCADE) name = models.CharField(_('name'), max_length=128) osuser = models.OneToOneField( OsUser, verbose_name=_('Operating system user'), blank=True, null=True, on_delete=models.CASCADE) objects = CustomerHostingPackageManager() class Meta: unique_together = ['customer', 'name'] verbose_name = _('customer hosting package') verbose_name_plural = _('customer hosting packages') def __str__(self): return _("{name} for {customer}").format( name=self.name, customer=self.customer ) def get_absolute_url(self): return reverse('hosting_package_details', kwargs={ 'user': self.customer.username, 'pk': self.id, }) def copy_template_attributes(self): """ Copy the attributes of the hosting package's template to the package. """ for attrname in ('diskspace', 'diskspace_unit', 'mailboxcount'): setattr(self, attrname, getattr(self.template, attrname)) def get_hostingoptions(self): opts = [] for opt_type in [ CustomerDiskSpaceOption, CustomerMailboxOption, CustomerUserDatabaseOption ]: opts.extend(opt_type.objects.filter(hosting_package=self)) return opts hostingoptions = property(get_hostingoptions) def get_disk_space(self, unit=None): """ Get the total disk space reserved for this hosting package and all its additional disk space options. :param unit: value from :py:data:`DISK_SPACE_UNITS` or :py:const:`None` :return: disk space in unit or bytes (if parameter unit is :py:const:`None`) :rtype: int """ diskspace = self.diskspace min_unit = self.diskspace_unit options = CustomerDiskSpaceOption.objects.filter(hosting_package=self) for option in options: if option.diskspace_unit == min_unit: diskspace += option.diskspace elif option.diskspace_unit > min_unit: diskspace += ( DISK_SPACE_FACTORS[option.diskspace_unit][min_unit] * option.diskspace) else: diskspace = ( DISK_SPACE_FACTORS[min_unit][ option.diskspace_unit] * diskspace) + option.diskspace min_unit = option.diskspace_unit if unit is None: return DISK_SPACE_FACTORS[min_unit][0] * diskspace * 1024 ** 2 if unit > min_unit: return DISK_SPACE_FACTORS[unit][min_unit] * diskspace return DISK_SPACE_FACTORS[min_unit][unit] * diskspace def get_package_space(self, unit=None): """ Get the total disk space reserved for this package without looking at any additional dis space options. :param unit: value from :py:data:`DISK_SPACE_UNITS` or :py:const:`None` :return: disk space in unit or bytes (if parameter unit is :py:const:`None`) :rtype: int """ if unit is None: return (DISK_SPACE_FACTORS[self.diskspace_unit][0] * self.diskspace * 1024 ** 2) if unit > self.diskspace_unit: return (DISK_SPACE_FACTORS[unit][self.diskspace_unit] * self.diskspace) return DISK_SPACE_FACTORS[self.diskspace_unit][unit] * self.diskspace def get_quota(self): soft = 1024 * self.get_disk_space(DISK_SPACE_UNITS.M) hard = soft * 105 / 100 return (soft, hard) def get_mailboxes(self): if self.osuser: return Mailbox.objects.filter(osuser=self.osuser).all() mailboxes = property(get_mailboxes) def get_used_mailbox_count(self): """ Get the number of used mailboxes for this hosting package. """ if self.osuser: return Mailbox.objects.filter(osuser=self.osuser).count() return 0 used_mailbox_count = property(get_used_mailbox_count) def get_mailbox_count(self): """ Get the number of mailboxes provided by this hosting package and all of its mailbox options. """ result = CustomerMailboxOption.objects.filter( hosting_package=self ).aggregate( mailbox_sum=models.Sum('number') ) return self.mailboxcount + (result['mailbox_sum'] or 0) mailbox_count = property(get_mailbox_count) def may_add_mailbox(self): return self.used_mailbox_count < self.mailbox_count def get_databases(self): """ Get the number of user databases provided by user database hosting options for this hosting package. """ return CustomerUserDatabaseOption.objects.values( 'db_type' ).filter(hosting_package=self).annotate( number=models.Sum('number') ).all() def get_databases_flat(self): if self.osuser: return UserDatabase.objects.filter( db_user__osuser=self.osuser).all() databases = property(get_databases_flat) def may_add_database(self): return ( CustomerUserDatabaseOption.objects.filter( hosting_package=self).count() > UserDatabase.objects.filter( db_user__osuser=self.osuser).count() ) @transaction.atomic def save(self, *args, **kwargs): """ Save the hosting package to the database. If this is a new hosting package a new operating system user is created and assigned to the hosting package. :param args: positional arguments to be passed on to :py:meth:`django.db.Model.save` :param kwargs: keyword arguments to be passed on to :py:meth:`django.db.Model.save` :return: self :rtype: :py:class:`CustomerHostingPackage` """ if self.pk is None: self.copy_template_attributes() self.osuser = OsUser.objects.create_user(self.customer) for group in settings.OSUSER_DEFAULT_GROUPS: AdditionalGroup.objects.create( user=self.osuser, group=Group.objects.get(groupname=group) ) return super(CustomerHostingPackage, self).save(*args, **kwargs) @python_2_unicode_compatible class CustomerHostingPackageDomain(TimeStampedModel): """ This class defines the relationship from a hosting package to a hosting domain. """ hosting_package = models.ForeignKey( CustomerHostingPackage, verbose_name=_('hosting package'), related_name='domains', on_delete=models.CASCADE) domain = models.OneToOneField( HostingDomain, verbose_name=_('hosting domain'), on_delete=models.CASCADE) def __str__(self): return self.domain.domain def is_usable_for_email(self): """ Tells wether the related domain is usable for email addresses. """ return self.domain.maildomain is not None class CustomerHostingPackageOption(TimeStampedModel): """ This class defines options for customer hosting packages. """ hosting_package = models.ForeignKey( CustomerHostingPackage, verbose_name=_('hosting package'), on_delete=models.CASCADE) class Meta: verbose_name = _('customer hosting option') verbose_name_plural = _('customer hosting options') class CustomerDiskSpaceOption(DiskSpaceOptionBase, CustomerHostingPackageOption): """ This is a class for customer hosting package options adding additional disk space to existing customer hosting package. """ template = models.ForeignKey( DiskSpaceOption, verbose_name=_('disk space option template'), help_text=_( 'The disk space option template that this disk space option is' ' based on' ), on_delete=models.CASCADE) class CustomerUserDatabaseOption(UserDatabaseOptionBase, CustomerHostingPackageOption): """ This is a class for customer hosting package options adding user databases to existing customer hosting packages. """ template = models.ForeignKey( UserDatabaseOption, verbose_name=_('user database option template'), help_text=_( 'The user database option template that this database option is' ' based on' ), on_delete=models.CASCADE) class CustomerMailboxOption(MailboxOptionBase, CustomerHostingPackageOption): """ This is a class for customer hosting package options adding additional mailboxes to existing customer hosting packages. """ template = models.ForeignKey( MailboxOption, verbose_name=_('mailbox option template'), help_text=_( 'The mailbox option template that this mailbox option is based on' ), on_delete=models.CASCADE)