Added support for domain quota.

see #1030
This commit is contained in:
Antoine Nguyen
2017-02-15 13:30:12 +01:00
parent 4279d0cabd
commit 496624a711
13 changed files with 179 additions and 73 deletions
+22 -4
View File
@@ -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():
+2 -1
View File
@@ -16,6 +16,7 @@ class DomainFactory(PermissionFactory):
django_get_or_create = ("name", )
type = "domain"
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'
+5 -4
View File
@@ -187,7 +187,6 @@ class AccountFormGeneral(forms.ModelForm):
class AccountFormMail(forms.Form, DynamicForm):
"""Form to handle mail part."""
email = lib_fields.UTF8EmailField(
@@ -198,8 +197,7 @@ class AccountFormMail(forms.Form, DynamicForm):
help_text=_("Quota in MB for this mailbox. Define a custom value or "
"use domain's default one. Leave empty to define an "
"unlimited value (not allowed for domain "
"administrators)."),
widget=forms.widgets.TextInput(attrs={"class": "form-control"})
"administrators).")
)
quota_act = forms.BooleanField(required=False)
aliases = lib_fields.UTF8AndEmptyUserEmailField(
@@ -280,8 +278,11 @@ class AccountFormMail(forms.Form, DynamicForm):
Check if quota is >= 0 only when the domain value is not used.
"""
cleaned_data = super(AccountFormMail, self).clean()
if not cleaned_data["email"]:
return cleaned_data
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:
+7 -22
View File
@@ -33,14 +33,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
type = forms.ChoiceField(
label=ugettext_lazy("Type"),
)
default_mailbox_quota = forms.IntegerField(
label=ugettext_lazy("Default mailbox 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,15 +45,18 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
class Meta:
model = Domain
fields = (
"name", "type", "default_mailbox_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(
@@ -69,6 +64,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
)
self.fields["type"].choices = DOMAIN_TYPES + extra_domain_types
self.field_widths = {
"quota": 3,
"default_mailbox_quota": 3
}
@@ -80,14 +76,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
name = "aliases_%d" % (pos + 1)
self._create_field(forms.CharField, name, dalias.name, 3)
def clean_default_mailbox_quota(self):
"""Check quota value."""
if self.cleaned_data["default_mailbox_quota"] is None:
return param_tools.get_global_parameter("default_domain_quota")
if self.cleaned_data["default_mailbox_quota"] < 0:
raise forms.ValidationError(_("Must be a positive integer"))
return self.cleaned_data["default_mailbox_quota"]
def clean(self):
"""Custom fields validation.
@@ -165,7 +153,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
class DomainFormOptions(forms.Form):
"""A form containing options for domain creation."""
create_dom_admin = YesNoField(
@@ -272,7 +259,6 @@ class DomainFormOptions(forms.Form):
class DomainForm(TabForms):
"""Domain edition form."""
def __init__(self, request, *args, **kwargs):
@@ -342,7 +328,6 @@ class DomainForm(TabForms):
class DomainWizard(WizardForm):
"""Domain creation wizard."""
def __init__(self, request):
@@ -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),
),
]
+25 -2
View File
@@ -42,8 +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()
default_mailbox_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"),
@@ -144,6 +157,16 @@ 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"]
def add_admin(self, account):
"""Add a new administrator to this domain.
+13 -11
View File
@@ -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,21 +228,23 @@ class Mailbox(AdminObject):
:param integer value: the quota's value
:param bool override_rules: allow to override defined quota rules
"""
default_mailbox_quota = self.domain.default_mailbox_quota
old_quota = self.quota
if value is None:
if self.use_domain_quota:
self.quota = default_mailbox_quota
self.quota = self.domain.default_mailbox_quota
else:
self.quota = 0
elif int(value) > default_mailbox_quota and not override_rules:
raise lib_exceptions.BadRequest(
_("Quota is greater than the allowed domain's limit (%dM)") %
default_mailbox_quota
)
else:
self.quota = value
if not self.quota and default_mailbox_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.
@@ -1,6 +1,7 @@
{% load i18n form_tags %}
{% render_field form.name %}
{% render_field form.type %}
{% render_field form.quota appended_text=_("MB") %}
{% render_field form.default_mailbox_quota appended_text=_("MB") %}
{% render_fields_group form "aliases" %}
{% render_field form.enabled %}
@@ -6,7 +6,7 @@
{% blocktrans trimmed %}
Provide a CSV file where lines respect one of the following formats:
<ul>
<li><em>domain; name; quota; enabled</em></li>
<li><em>domain; name; default mailbox quota; enabled</em></li>
<li><em>domainalias; name; targeted domain; enabled</em></li>
<li><em>relaydomain; name; target host; target port; service; enabled; verify recipients</em></li>
</ul>
+26 -5
View File
@@ -165,17 +165,34 @@ class AccountTestCase(ModoTestCase):
self._set_quota("user@test.com", 0)
def test_set_nul_quota_as_domainadmin(self):
"""Check cases where a domain admin set unlimited quota."""
self.client.logout()
self.assertTrue(
self.client.login(username="admin@test.com", password="toto")
)
# Fails because domain has a quota
self._set_quota("user@test.com", 0, 400)
self.client.logout()
self.assertTrue(
self.client.login(username="admin@test2.com", password="toto")
)
# Ok because domain has no quota
self._set_quota("user@test2.com", 0)
def test_domain_quota(self):
"""Check domain quota."""
dom = models.Domain.objects.get(name="test.com")
dom.quota = 100
dom.save(update_fields=["quota"])
# 2 x 10MB
self.assertEqual(dom.quota_usage, 20)
self._set_quota("user@test.com", 80)
del dom.quota_usage
# 10 + 80 < 100 => ok
self.assertEqual(dom.quota_usage, 90)
# 30 + 80 > 100 => failure
self._set_quota("admin@test.com", 30, 400)
def test_master_user(self):
"""Validate the master user mode."""
values = {
@@ -250,11 +267,15 @@ class PermissionsTestCase(ModoTestCase):
cls.reseller = core_factories.UserFactory(username="reseller")
cls.reseller.role = "Resellers"
cls.user = User.objects.get(username="user@test.com")
cls.values = dict(
username=cls.user.username, role="DomainAdmins",
is_active=cls.user.is_active, email="user@test.com",
quota_act=True
)
def setUp(self):
"""Initiate test context."""
super(PermissionsTestCase, self).setUp()
self.values = {
"username": self.user.username, "role": "DomainAdmins",
"is_active": self.user.is_active, "email": "user@test.com",
"quota_act": True
}
def tearDown(self):
self.client.logout()
+15 -21
View File
@@ -25,12 +25,13 @@ class DomainTestCase(ModoTestCase):
def test_create(self):
"""Test the creation of a domain."""
values = {
"name": "pouet.com", "default_mailbox_quota": 100,
"name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100,
"create_dom_admin": False, "type": "domain", "stepid": "step3"
}
self.ajax_post(reverse("admin:domain_add"), values)
dom = Domain.objects.get(name="pouet.com")
self.assertEqual(dom.name, "pouet.com")
self.assertEqual(dom.quota, 1000)
self.assertEqual(dom.default_mailbox_quota, 100)
self.assertEqual(dom.enabled, False)
self.assertFalse(dom.admins)
@@ -38,12 +39,13 @@ class DomainTestCase(ModoTestCase):
def test_create_utf8(self):
"""Test the creation of a domain with non-ASCII characters."""
values = {
"name": "pouét.com", "default_mailbox_quota": 100,
"name": "pouét.com", "quota": 1000, "default_mailbox_quota": 100,
"create_dom_admin": False, "type": "domain", "stepid": "step3"
}
self.ajax_post(reverse("admin:domain_add"), values)
dom = Domain.objects.get(name=u"pouét.com")
self.assertEqual(dom.name, u"pouét.com")
self.assertEqual(dom.quota, 1000)
self.assertEqual(dom.default_mailbox_quota, 100)
self.assertEqual(dom.enabled, False)
self.assertFalse(dom.admins)
@@ -51,7 +53,7 @@ class DomainTestCase(ModoTestCase):
def test_create_with_template(self):
"""Test the creation of a domain with a template."""
values = {
"name": "pouet.com", "default_mailbox_quota": 100,
"name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100,
"create_dom_admin": True, "dom_admin_username": "toto",
"create_aliases": True, "type": "domain", "stepid": 'step3'
}
@@ -67,7 +69,7 @@ class DomainTestCase(ModoTestCase):
self.assertTrue(da.can_access(al))
values = {
"name": "pouet2.com", "default_mailbox_quota": 100,
"name": "pouet2.com", "quota": 1000, "default_mailbox_quota": 100,
"create_dom_admin": True, "dom_admin_username": "postmaster",
"create_aliases": True, "type": "domain", "stepid": "step3"
}
@@ -81,7 +83,7 @@ class DomainTestCase(ModoTestCase):
def test_create_with_template_and_empty_quota(self):
"""Test the creation of a domain with a template and no quota"""
values = {
"name": "pouet.com", "default_mailbox_quota": 0,
"name": "pouet.com", "quota": 0, "default_mailbox_quota": 0,
"create_dom_admin": True, "dom_admin_username": "toto",
"create_aliases": True, "type": "domain", "stepid": 'step3'
}
@@ -104,7 +106,7 @@ class DomainTestCase(ModoTestCase):
password = "Toto1000"
self.set_global_parameter("default_password", password, app="core")
values = {
"name": "pouet.com", "default_mailbox_quota": 0,
"name": "pouet.com", "quota": 0, "default_mailbox_quota": 0,
"create_dom_admin": True, "dom_admin_username": "toto",
"create_aliases": True, "type": "domain", "stepid": 'step3'
}
@@ -125,20 +127,12 @@ class DomainTestCase(ModoTestCase):
self.client.login(username="toto@pouet.com", password=password))
def test_create_using_default_quota(self):
self.set_global_parameter("default_domain_quota", 50)
values = {
"name": "pouet.com", "create_dom_admin": True,
"dom_admin_username": "toto", "create_aliases": True,
"type": "domain", "stepid": 'step3'
}
self.ajax_post(
reverse("admin:domain_add"),
values
)
dom = Domain.objects.get(name="pouet.com")
self.assertEqual(dom.default_mailbox_quota, 50)
da = User.objects.get(username="toto@pouet.com")
self.assertEqual(da.mailbox.quota, 50)
"""Check that default value is used for creation."""
self.set_global_parameter("default_domain_quota", 500)
self.set_global_parameter("default_mailbox_quota", 50)
response = self.client.get(reverse("admin:domain_add"))
self.assertContains(response, "value=\"500\"")
self.assertContains(response, "value=\"50\"")
def test_modify(self):
"""Test the modification of a domain
@@ -146,7 +140,7 @@ class DomainTestCase(ModoTestCase):
Rename 'test.com' domain to 'pouet.com'
"""
values = {
"name": "pouet.com", "default_mailbox_quota": 100,
"name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100,
"type": "domain", "enabled": True
}
dom = Domain.objects.get(name="test.com")
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-15 09:34
from __future__ import unicode_literals
from django.db import migrations
def rename_settings(apps, schema_editor):
"""Rename settings."""
LocalConfig = apps.get_model("core", "LocalConfig")
lc = LocalConfig.objects.first()
condition = (
lc and
"admin" in lc._parameters and
"default_domain_quota" in lc._parameters["admin"]
)
if condition:
lc._parameters["admin"]["default_mailbox_quota"] = (
lc._parameters["admin"]["default_domain_quota"])
del lc._parameters["admin"]["default_domain_quota"]
lc.save(update_fields=["_parameters"])
class Migration(migrations.Migration):
dependencies = [
('core', '0011_auto_20161104_2217'),
]
operations = [
migrations.RunPython(rename_settings)
]
-2
View File
@@ -21,7 +21,6 @@ from modoboa.lib.web_utils import render_to_json_response
class WizardStep(object):
"""A wizard step."""
def __init__(self, uid, formclass, title, formtpl=None, new_args=None):
@@ -66,7 +65,6 @@ class WizardStep(object):
class WizardForm(object):
"""Custom wizard."""
__metaclass__ = abc.ABCMeta