""" This module contains the hosting package models. """ from __future__ import absolute_import from django.conf import settings from django.db import models, transaction from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext 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 from osusers.models import 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)) 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. """ 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"] 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 ngettext( "{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"] 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 ngettext( "{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 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) 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, )