diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index 15ee53de3..433a21618 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -71,6 +71,15 @@ class AdminParametersForm(param_forms.AdminParametersForm): ) ) + default_mailbox_quota = forms.IntegerField( + label=ugettext_lazy("Default mailbox quota"), + initial=0, + help_text=ugettext_lazy( + "Default mailbox quota (in MB) applied to freshly created domains " + "with no value specified. A value of 0 means no quota." + ) + ) + auto_account_removal = YesNoField( label=ugettext_lazy("Automatic account removal"), initial=False, @@ -96,7 +105,8 @@ class AdminParametersForm(param_forms.AdminParametersForm): def __init__(self, *args, **kwargs): super(AdminParametersForm, self).__init__(*args, **kwargs) self.field_widths = { - "default_domain_quota": 2 + "default_domain_quota": 2, + "default_mailbox_quota": 2 } hide_fields = False dpath = None @@ -127,11 +137,19 @@ class AdminParametersForm(param_forms.AdminParametersForm): def clean_default_domain_quota(self): """Ensure quota is a positive integer.""" - if self.cleaned_data['default_domain_quota'] < 0: + if self.cleaned_data["default_domain_quota"] < 0: raise forms.ValidationError( - ugettext_lazy('Must be a positive integer') + ugettext_lazy("Must be a positive integer") ) - return self.cleaned_data['default_domain_quota'] + return self.cleaned_data["default_domain_quota"] + + def clean_default_mailbox_quota(self): + """Ensure quota is a positive integer.""" + if self.cleaned_data["default_mailbox_quota"] < 0: + raise forms.ValidationError( + ugettext_lazy("Must be a positive integer") + ) + return self.cleaned_data["default_mailbox_quota"] def load_admin_settings(): diff --git a/modoboa/admin/factories.py b/modoboa/admin/factories.py index 8a3272eb9..ad7eb13a3 100644 --- a/modoboa/admin/factories.py +++ b/modoboa/admin/factories.py @@ -16,7 +16,8 @@ class DomainFactory(PermissionFactory): django_get_or_create = ("name", ) type = "domain" - quota = 10 + quota = 0 + default_mailbox_quota = 10 enabled = True @@ -64,7 +65,7 @@ def populate_database(): 2 domains, 1 domain admin, 2 users """ - dom = DomainFactory(name="test.com") + dom = DomainFactory(name="test.com", quota=50) admin = UserFactory( username="admin@test.com", groups=('DomainAdmins', ), password='{PLAIN}toto' @@ -96,7 +97,7 @@ def populate_database(): dom.add_admin(admin) - dom2 = DomainFactory.create(name='test2.com', quota=0) + dom2 = DomainFactory.create(name='test2.com', default_mailbox_quota=0) admin = UserFactory.create( username='admin@test2.com', groups=('DomainAdmins',), password='{PLAIN}toto' diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 03c91ecc0..fd9b1035b 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -187,7 +187,6 @@ class AccountFormGeneral(forms.ModelForm): class AccountFormMail(forms.Form, DynamicForm): - """Form to handle mail part.""" email = lib_fields.UTF8EmailField( @@ -280,8 +279,9 @@ class AccountFormMail(forms.Form, DynamicForm): Check if quota is >= 0 only when the domain value is not used. """ cleaned_data = super(AccountFormMail, self).clean() + use_default_domain_quota = cleaned_data["quota_act"] condition = ( - not cleaned_data["quota_act"] and + not use_default_domain_quota and cleaned_data["quota"] is not None and cleaned_data["quota"] < 0) if condition: diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index 57cd80618..5205a96e5 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -33,14 +33,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): type = forms.ChoiceField( label=ugettext_lazy("Type"), ) - quota = forms.IntegerField( - label=ugettext_lazy("Quota"), - required=False, - help_text=ugettext_lazy( - "Default quota in MB applied to mailboxes. Leave empty to use the " - "default value." - ) - ) aliases = DomainNameField( label=ugettext_lazy("Alias(es)"), required=False, @@ -53,14 +45,18 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): class Meta: model = Domain fields = ( - "name", "type", "quota", "aliases", "enabled", "enable_dns_checks") + "name", "type", "quota", "default_mailbox_quota", "aliases", + "enabled", "enable_dns_checks") def __init__(self, *args, **kwargs): self.oldname = None if "instance" in kwargs: self.oldname = kwargs["instance"].name super(DomainFormGeneral, self).__init__(*args, **kwargs) - + params = dict(param_tools.get_global_parameters("admin")) + self.fields["quota"].initial = params["default_domain_quota"] + self.fields["default_mailbox_quota"].initial = ( + params["default_mailbox_quota"]) extra_domain_types = reduce( lambda a, b: a + b, [result[1] for result in signals.extra_domain_types.send( @@ -68,7 +64,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): ) self.fields["type"].choices = DOMAIN_TYPES + extra_domain_types self.field_widths = { - "quota": 3 + "quota": 3, + "default_mailbox_quota": 3 } if len(args) and isinstance(args[0], QueryDict): @@ -79,14 +76,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): name = "aliases_%d" % (pos + 1) self._create_field(forms.CharField, name, dalias.name, 3) - def clean_quota(self): - """Check quota value.""" - if self.cleaned_data["quota"] is None: - return param_tools.get_global_parameter("default_domain_quota") - if self.cleaned_data["quota"] < 0: - raise forms.ValidationError(_("Must be a positive integer")) - return self.cleaned_data["quota"] - def clean(self): """Custom fields validation. @@ -164,7 +153,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): class DomainFormOptions(forms.Form): - """A form containing options for domain creation.""" create_dom_admin = YesNoField( @@ -271,7 +259,6 @@ class DomainFormOptions(forms.Form): class DomainForm(TabForms): - """Domain edition form.""" def __init__(self, request, *args, **kwargs): @@ -341,7 +328,6 @@ class DomainForm(TabForms): class DomainWizard(WizardForm): - """Domain creation wizard.""" def __init__(self, request): diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index 1d75e8c53..53e7e7bd8 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -27,7 +27,7 @@ def update_domain_mxs_and_mailboxes(sender, instance, **kwargs): if kwargs.get("created"): return instance.mailbox_set.filter(use_domain_quota=True).update( - quota=instance.quota) + quota=instance.default_mailbox_quota) if instance.old_mail_homes is None: return qset = ( @@ -155,7 +155,8 @@ def account_auto_created(sender, user, **kwargs): domname, [(models.DomainAlias, _("domain alias"))]) if label is not None: return - domain = models.Domain(name=domname, enabled=True, quota=0) + domain = models.Domain( + name=domname, enabled=True, default_mailbox_quota=0) domain.save(creator=sadmins[0]) for su in sadmins[1:]: permissions.grant_access_to_object(su, domain) diff --git a/modoboa/admin/migrations/0009_auto_20170215_0948.py b/modoboa/admin/migrations/0009_auto_20170215_0948.py new file mode 100644 index 000000000..b032a7190 --- /dev/null +++ b/modoboa/admin/migrations/0009_auto_20170215_0948.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-15 08:48 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0008_domain_enable_dns_checks'), + ] + + operations = [ + migrations.RenameField( + model_name='domain', + old_name='quota', + new_name='default_mailbox_quota', + ), + ] diff --git a/modoboa/admin/migrations/0010_auto_20170215_1328.py b/modoboa/admin/migrations/0010_auto_20170215_1328.py new file mode 100644 index 000000000..e71d408ca --- /dev/null +++ b/modoboa/admin/migrations/0010_auto_20170215_1328.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-15 12:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0009_auto_20170215_0948'), + ] + + operations = [ + migrations.AddField( + model_name='domain', + name='quota', + field=models.PositiveIntegerField(default=0, help_text='Quota in MB shared between mailboxes. A value of 0 means no quota.'), + ), + migrations.AlterField( + model_name='domain', + name='default_mailbox_quota', + field=models.PositiveIntegerField(default=0, help_text='Default quota in MB applied to mailboxes. A value of 0 means no quota.', verbose_name='Default mailbox quota'), + ), + migrations.AlterField( + model_name='mailbox', + name='quota', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index b8d09695f..49547189e 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -42,7 +42,21 @@ class Domain(AdminObject): name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True, help_text=ugettext_lazy("The domain name")) - quota = models.IntegerField() + quota = models.PositiveIntegerField( + default=0, + help_text=ugettext_lazy( + "Quota in MB shared between mailboxes. A value of 0 means " + "no quota." + ) + ) + default_mailbox_quota = models.PositiveIntegerField( + verbose_name=ugettext_lazy("Default mailbox quota"), + default=0, + help_text=ugettext_lazy( + "Default quota in MB applied to mailboxes. A value of 0 means " + "no quota." + ) + ) enabled = models.BooleanField( ugettext_lazy('enabled'), help_text=ugettext_lazy("Check to activate this domain"), @@ -143,6 +157,23 @@ class Domain(AdminObject): else: return "success" + @cached_property + def quota_usage(self): + """Return current quota usage.""" + if not self.quota: + return 0 + if not self.mailbox_set.exists(): + return 0 + return self.mailbox_set.aggregate( + total=models.Sum("quota"))["total"] + + @cached_property + def quota_usage_in_percent(self): + """Return quota usage in percent.""" + if not self.quota_usage: + return 0 + return int(self.quota_usage / float(self.quota) * 100) + def add_admin(self, account): """Add a new administrator to this domain. @@ -210,12 +241,12 @@ class Domain(AdminObject): The expected fields order is the following:: - "domain", name, quota, enabled + "domain", name, quota, default_mailbox_quota, enabled :param ``core.User`` user: user creating the domain :param str row: a list containing domain's definition """ - if len(row) < 4: + if len(row) < 5: raise BadRequest(_("Invalid line")) self.name = row[1].strip() if Domain.objects.filter(name=self.name).exists(): @@ -224,12 +255,25 @@ class Domain(AdminObject): self.quota = int(row[2].strip()) except ValueError: raise BadRequest( - _("Invalid quota value for domain '%s'") % self.name) - self.enabled = (row[3].strip() in ["True", "1", "yes", "y"]) + _("Invalid quota value for domain '{}'") + .format(self.name) + ) + try: + self.default_mailbox_quota = int(row[3].strip()) + except ValueError: + raise BadRequest( + _("Invalid default mailbox quota value for domain '{}'") + .format(self.name) + ) + self.enabled = (row[4].strip() in ["True", "1", "yes", "y"]) self.save(creator=user) def to_csv(self, csvwriter): - csvwriter.writerow(["domain", self.name, self.quota, self.enabled]) + """Export domain and domain aliases to CSV format.""" + csvwriter.writerow([ + "domain", self.name, self.quota, self.default_mailbox_quota, + self.enabled + ]) for dalias in self.domainalias_set.all(): dalias.to_csv(csvwriter) diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 9605fc77a..215c5a155 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -81,7 +81,7 @@ class Mailbox(AdminObject): help_text=ugettext_lazy( "Mailbox address (without the @domain.tld part)") ) - quota = models.PositiveIntegerField() + quota = models.PositiveIntegerField(default=0) use_domain_quota = models.BooleanField(default=False) domain = models.ForeignKey(Domain) user = models.OneToOneField(User) @@ -218,7 +218,7 @@ class Mailbox(AdminObject): MailboxOperation.objects.create(type="delete", argument=self.mail_home) def set_quota(self, value=None, override_rules=False): - """Set or update quota's value for this mailbox. + """Set or update quota value for this mailbox. A value equal to 0 means the mailbox won't have any quota. The following cases allow people to define such behaviour: @@ -228,20 +228,23 @@ class Mailbox(AdminObject): :param integer value: the quota's value :param bool override_rules: allow to override defined quota rules """ + old_quota = self.quota if value is None: if self.use_domain_quota: - self.quota = self.domain.quota + self.quota = self.domain.default_mailbox_quota else: self.quota = 0 - elif int(value) > self.domain.quota and not override_rules: - raise lib_exceptions.BadRequest( - _("Quota is greater than the allowed domain's limit (%dM)") % - self.domain.quota - ) else: self.quota = value - if not self.quota and self.domain.quota and not override_rules: - raise lib_exceptions.BadRequest(_("A quota is required")) + if self.quota == 0: + if self.domain.quota and not override_rules: + raise lib_exceptions.BadRequest(_("A quota is required")) + elif self.domain.quota: + quota_usage = self.domain.quota_usage + if old_quota: + quota_usage -= old_quota + if quota_usage + self.quota > self.domain.quota: + raise lib_exceptions.BadRequest(_("Domain quota exceeded")) def get_quota(self): """Get quota limit. diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index a37907d86..e0a6a33b0 100644 --- a/modoboa/admin/serializers.py +++ b/modoboa/admin/serializers.py @@ -23,7 +23,9 @@ class DomainSerializer(serializers.ModelSerializer): class Meta: model = models.Domain - fields = ("pk", "name", "quota", "enabled", "type", ) + fields = ( + "pk", "name", "quota", "default_mailbox_quota", "enabled", "type", + ) def create(self, validated_data): """Set permissions.""" @@ -236,11 +238,10 @@ class WritableAccountSerializer(AccountSerializer): except lib_exceptions.ModoboaException as inst: raise serializers.ValidationError({ "domain": unicode(inst)}) + quota = data.pop("quota", None) mb = admin_models.Mailbox( user=account, address=address, domain=domain, **data) - mb.set_quota( - data.get("quota"), creator.has_perm("admin.add_domain") - ) + mb.set_quota(quota, creator.has_perm("admin.add_domain")) mb.save(creator=creator) account.email = full_address return mb diff --git a/modoboa/admin/templates/admin/domain_detail.html b/modoboa/admin/templates/admin/domain_detail.html index 349c9e3c4..0299af528 100644 --- a/modoboa/admin/templates/admin/domain_detail.html +++ b/modoboa/admin/templates/admin/domain_detail.html @@ -20,21 +20,50 @@
| {% trans "DNS status" %} | ++ {% include "admin/_domain_dnschecks_status.html" %} + | +
|---|---|
| {% trans "Creation date" %} | +{{ domain.creation }} | +
| {% trans "Last modification date" %} | +{{ domain.last_modification }} | +
| {% trans "Domain aliases" %} | +{{ domain.domainalias_count }} | +
| {% trans "Mailboxes" %} | +{{ domain.mailbox_count }} | +
| {% trans "Mailbox aliases" %} | +{{ domain.mbalias_count }} | +
| {% trans "Quota" %} | +{{ domain.quota }} {% trans "MB" %} | +
| {% trans "Usage" %} | +{% include "common/progressbar.html" with id="quotabar" value=domain.quota_usage_in_percent %} | +
| {% trans "Default mailbox quota" %} | +{{ domain.default_mailbox_quota }} {% trans "MB" %} | +