From 4279d0cabdc86ebbc79b88b07dd55201a169b50a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 15 Feb 2017 10:04:13 +0100 Subject: [PATCH 1/9] Renamed domain quota field to default_mailbox_quota. see #1030 --- modoboa/admin/factories.py | 4 +- modoboa/admin/forms/domain.py | 17 ++++---- modoboa/admin/handlers.py | 5 ++- .../migrations/0009_auto_20170215_0948.py | 20 +++++++++ modoboa/admin/models/domain.py | 13 +++--- modoboa/admin/models/mailbox.py | 9 ++-- modoboa/admin/serializers.py | 2 +- .../templates/admin/domain_general_form.html | 2 +- modoboa/admin/tests/test_api.py | 19 +++++---- modoboa/admin/tests/test_domain.py | 42 +++++++++---------- modoboa/admin/tests/test_domain_alias.py | 6 +-- modoboa/admin/tests/test_import_.py | 6 +-- .../migrations/0006_auto_20170215_0948.py | 22 ++++++++++ 13 files changed, 110 insertions(+), 57 deletions(-) create mode 100644 modoboa/admin/migrations/0009_auto_20170215_0948.py create mode 100644 modoboa/relaydomains/migrations/0006_auto_20170215_0948.py diff --git a/modoboa/admin/factories.py b/modoboa/admin/factories.py index 8a3272eb9..e1bfce0b7 100644 --- a/modoboa/admin/factories.py +++ b/modoboa/admin/factories.py @@ -16,7 +16,7 @@ class DomainFactory(PermissionFactory): django_get_or_create = ("name", ) type = "domain" - quota = 10 + default_mailbox_quota = 10 enabled = True @@ -96,7 +96,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/domain.py b/modoboa/admin/forms/domain.py index 57cd80618..6cb90f7b2 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -33,8 +33,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): type = forms.ChoiceField( label=ugettext_lazy("Type"), ) - quota = forms.IntegerField( - label=ugettext_lazy("Quota"), + 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 " @@ -53,7 +53,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): class Meta: model = Domain fields = ( - "name", "type", "quota", "aliases", "enabled", "enable_dns_checks") + "name", "type", "default_mailbox_quota", "aliases", "enabled", + "enable_dns_checks") def __init__(self, *args, **kwargs): self.oldname = None @@ -68,7 +69,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): ) self.fields["type"].choices = DOMAIN_TYPES + extra_domain_types self.field_widths = { - "quota": 3 + "default_mailbox_quota": 3 } if len(args) and isinstance(args[0], QueryDict): @@ -79,13 +80,13 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): name = "aliases_%d" % (pos + 1) self._create_field(forms.CharField, name, dalias.name, 3) - def clean_quota(self): + def clean_default_mailbox_quota(self): """Check quota value.""" - if self.cleaned_data["quota"] is None: + if self.cleaned_data["default_mailbox_quota"] is None: return param_tools.get_global_parameter("default_domain_quota") - if self.cleaned_data["quota"] < 0: + if self.cleaned_data["default_mailbox_quota"] < 0: raise forms.ValidationError(_("Must be a positive integer")) - return self.cleaned_data["quota"] + return self.cleaned_data["default_mailbox_quota"] def clean(self): """Custom fields validation. 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/models/domain.py b/modoboa/admin/models/domain.py index b8d09695f..feeb8b7a8 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -42,7 +42,8 @@ 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.IntegerField() + default_mailbox_quota = models.IntegerField() enabled = models.BooleanField( ugettext_lazy('enabled'), help_text=ugettext_lazy("Check to activate this domain"), @@ -210,7 +211,7 @@ class Domain(AdminObject): The expected fields order is the following:: - "domain", name, quota, enabled + "domain", name, default_mailbox_quota, enabled :param ``core.User`` user: user creating the domain :param str row: a list containing domain's definition @@ -221,15 +222,17 @@ class Domain(AdminObject): if Domain.objects.filter(name=self.name).exists(): raise Conflict try: - self.quota = int(row[2].strip()) + self.default_mailbox_quota = int(row[2].strip()) except ValueError: raise BadRequest( - _("Invalid quota value for domain '%s'") % self.name) + _("Invalid default mailbox quota value for domain '%s'") + % self.name) self.enabled = (row[3].strip() in ["True", "1", "yes", "y"]) self.save(creator=user) def to_csv(self, csvwriter): - csvwriter.writerow(["domain", self.name, self.quota, self.enabled]) + csvwriter.writerow([ + "domain", self.name, self.default_mailbox_quot, 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..3d26d77d4 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -228,19 +228,20 @@ 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 if value is None: if self.use_domain_quota: - self.quota = self.domain.quota + self.quota = default_mailbox_quota else: self.quota = 0 - elif int(value) > self.domain.quota and not override_rules: + elif int(value) > default_mailbox_quota and not override_rules: raise lib_exceptions.BadRequest( _("Quota is greater than the allowed domain's limit (%dM)") % - self.domain.quota + default_mailbox_quota ) else: self.quota = value - if not self.quota and self.domain.quota and not override_rules: + if not self.quota and default_mailbox_quota and not override_rules: raise lib_exceptions.BadRequest(_("A quota is required")) def get_quota(self): diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index a37907d86..6930c92cd 100644 --- a/modoboa/admin/serializers.py +++ b/modoboa/admin/serializers.py @@ -23,7 +23,7 @@ class DomainSerializer(serializers.ModelSerializer): class Meta: model = models.Domain - fields = ("pk", "name", "quota", "enabled", "type", ) + fields = ("pk", "name", "default_mailbox_quota", "enabled", "type", ) def create(self, validated_data): """Set permissions.""" diff --git a/modoboa/admin/templates/admin/domain_general_form.html b/modoboa/admin/templates/admin/domain_general_form.html index 06c2590b1..0364e74e1 100644 --- a/modoboa/admin/templates/admin/domain_general_form.html +++ b/modoboa/admin/templates/admin/domain_general_form.html @@ -1,7 +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 %} {% render_field form.enable_dns_checks %} diff --git a/modoboa/admin/tests/test_api.py b/modoboa/admin/tests/test_api.py index c6d09f489..cee1f4c44 100644 --- a/modoboa/admin/tests/test_api.py +++ b/modoboa/admin/tests/test_api.py @@ -43,18 +43,20 @@ class DomainAPITestCase(ModoAPITestCase): def test_create_domain(self): """Check domain creation.""" url = reverse("external_api:domain-list") - response = self.client.post(url, {"name": "test3.com", "quota": 10}) + response = self.client.post( + url, {"name": "test3.com", "default_mailbox_quota": 10}) self.assertEqual(response.status_code, 201) self.assertTrue( models.Domain.objects.filter(name="test3.com").exists()) response = self.client.post(url, {}) self.assertEqual(response.status_code, 400) self.assertIn("name", response.data) - self.assertIn("quota", response.data) + self.assertIn("default_mailbox_quota", response.data) self.client.credentials( HTTP_AUTHORIZATION='Token ' + self.da_token.key) - response = self.client.post(url, {"name": "test4.com", "quota": 10}) + response = self.client.post( + url, {"name": "test4.com", "default_mailbox_quota": 10}) self.assertEqual(response.status_code, 403) def test_update_domain(self): @@ -64,15 +66,17 @@ class DomainAPITestCase(ModoAPITestCase): domain__name="test.com", address="user").update( use_domain_quota=True) url = reverse("external_api:domain-detail", args=[domain.pk]) - response = self.client.put(url, {"name": "test.com", "quota": 1000}) + response = self.client.put( + url, {"name": "test.com", "default_mailbox_quota": 1000}) self.assertEqual(response.status_code, 200) domain.refresh_from_db() - self.assertEqual(domain.quota, 1000) + self.assertEqual(domain.default_mailbox_quota, 1000) mb = models.Mailbox.objects.get( domain__name="test.com", address="user") self.assertEqual(mb.quota, 1000) - response = self.client.put(url, {"name": "test42.com", "quota": 1000}) + response = self.client.put( + url, {"name": "test42.com", "default_mailbox_quota": 1000}) self.assertEqual(response.status_code, 200) self.assertTrue( models.Mailbox.objects.filter( @@ -267,7 +271,8 @@ class AccountAPITestCase(ModoAPITestCase): user = core_models.User.objects.filter(pk=account["pk"]).first() self.assertIsNot(user, None) self.assertIsNot(user.mailbox, None) - self.assertEqual(user.mailbox.quota, user.mailbox.domain.quota) + self.assertEqual( + user.mailbox.quota, user.mailbox.domain.default_mailbox_quota) def test_create_existing_account(self): """Check if unicity is respected.""" diff --git a/modoboa/admin/tests/test_domain.py b/modoboa/admin/tests/test_domain.py index 4c7aef499..6e60b6dc9 100644 --- a/modoboa/admin/tests/test_domain.py +++ b/modoboa/admin/tests/test_domain.py @@ -25,35 +25,35 @@ class DomainTestCase(ModoTestCase): def test_create(self): """Test the creation of a domain.""" values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": False, - "type": "domain", "stepid": "step3" + "name": "pouet.com", "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, 100) + self.assertEqual(dom.default_mailbox_quota, 100) self.assertEqual(dom.enabled, False) self.assertFalse(dom.admins) def test_create_utf8(self): """Test the creation of a domain with non-ASCII characters.""" values = { - "name": "pouét.com", "quota": 100, "create_dom_admin": False, - "type": "domain", "stepid": "step3" + "name": "pouét.com", "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, 100) + self.assertEqual(dom.default_mailbox_quota, 100) self.assertEqual(dom.enabled, False) self.assertFalse(dom.admins) def test_create_with_template(self): """Test the creation of a domain with a template.""" values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": True, - "dom_admin_username": "toto", "create_aliases": True, - "type": "domain", "stepid": 'step3' + "name": "pouet.com", "default_mailbox_quota": 100, + "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") @@ -67,9 +67,9 @@ class DomainTestCase(ModoTestCase): self.assertTrue(da.can_access(al)) values = { - "name": "pouet2.com", "quota": 100, "create_dom_admin": True, - "dom_admin_username": "postmaster", "create_aliases": True, - "type": "domain", "stepid": "step3" + "name": "pouet2.com", "default_mailbox_quota": 100, + "create_dom_admin": True, "dom_admin_username": "postmaster", + "create_aliases": True, "type": "domain", "stepid": "step3" } self.ajax_post(reverse("admin:domain_add"), values) self.assertTrue( @@ -81,9 +81,9 @@ 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", "quota": 0, "create_dom_admin": True, - "dom_admin_username": "toto", "create_aliases": True, - "type": "domain", "stepid": 'step3' + "name": "pouet.com", "default_mailbox_quota": 0, + "create_dom_admin": True, "dom_admin_username": "toto", + "create_aliases": True, "type": "domain", "stepid": 'step3' } self.ajax_post( reverse("admin:domain_add"), @@ -104,9 +104,9 @@ class DomainTestCase(ModoTestCase): password = "Toto1000" self.set_global_parameter("default_password", password, app="core") values = { - "name": "pouet.com", "quota": 0, "create_dom_admin": True, - "dom_admin_username": "toto", "create_aliases": True, - "type": "domain", "stepid": 'step3' + "name": "pouet.com", "default_mailbox_quota": 0, + "create_dom_admin": True, "dom_admin_username": "toto", + "create_aliases": True, "type": "domain", "stepid": 'step3' } self.ajax_post( reverse("admin:domain_add"), @@ -136,7 +136,7 @@ class DomainTestCase(ModoTestCase): values ) dom = Domain.objects.get(name="pouet.com") - self.assertEqual(dom.quota, 50) + self.assertEqual(dom.default_mailbox_quota, 50) da = User.objects.get(username="toto@pouet.com") self.assertEqual(da.mailbox.quota, 50) @@ -146,8 +146,8 @@ class DomainTestCase(ModoTestCase): Rename 'test.com' domain to 'pouet.com' """ values = { - "name": "pouet.com", "quota": 100, "type": "domain", - "enabled": True + "name": "pouet.com", "default_mailbox_quota": 100, + "type": "domain", "enabled": True } dom = Domain.objects.get(name="test.com") self.ajax_post( diff --git a/modoboa/admin/tests/test_domain_alias.py b/modoboa/admin/tests/test_domain_alias.py index 3a665b3e4..ef731097c 100644 --- a/modoboa/admin/tests/test_domain_alias.py +++ b/modoboa/admin/tests/test_domain_alias.py @@ -39,9 +39,9 @@ class DomainAliasTestCase(ModoTestCase): def test_form(self): dom = Domain.objects.get(name="test.com") values = dict( - name=dom.name, quota=dom.quota, enabled=dom.enabled, - aliases="domalias.net", aliases_1="domaliasé.com", - type="domain" + name=dom.name, default_mailbox_quota=dom.default_mailbox_quota, + enabled=dom.enabled, aliases="domalias.net", + aliases_1="domaliasé.com", type="domain" ) self.ajax_post( reverse("admin:domain_change", diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index 24b50733d..3f33a6c2a 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -36,7 +36,7 @@ domainalias; domalias1.com; domain1.com; True ) admin = User.objects.get(username="admin") dom = Domain.objects.get(name="domain1.com") - self.assertEqual(dom.quota, 100) + self.assertEqual(dom.default_mailbox_quota, 100) self.assertTrue(dom.enabled) self.assertTrue(admin.is_owner(dom)) domalias = DomainAlias.objects.get(name="domalias1.com") @@ -44,7 +44,7 @@ domainalias; domalias1.com; domain1.com; True self.assertTrue(dom.enabled) self.assertTrue(admin.is_owner(domalias)) dom = Domain.objects.get(name="domain2.com") - self.assertEqual(dom.quota, 200) + self.assertEqual(dom.default_mailbox_quota, 200) self.assertFalse(dom.enabled) self.assertTrue(admin.is_owner(dom)) @@ -200,7 +200,7 @@ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com account = User.objects.get(username="user1@test.com") self.assertEqual( account.mailbox.quota, - account.mailbox.domain.quota + account.mailbox.domain.default_mailbox_quota ) def test_import_duplicate(self): diff --git a/modoboa/relaydomains/migrations/0006_auto_20170215_0948.py b/modoboa/relaydomains/migrations/0006_auto_20170215_0948.py new file mode 100644 index 000000000..6f0ed989d --- /dev/null +++ b/modoboa/relaydomains/migrations/0006_auto_20170215_0948.py @@ -0,0 +1,22 @@ +# -*- 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, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('relaydomains', '0005_auto_20161105_1426'), + ] + + operations = [ + migrations.AlterField( + model_name='relaydomain', + name='last_modification', + field=models.DateTimeField(auto_now=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] From 496624a711f7e9884d2c118d861dc14832a82e17 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 15 Feb 2017 13:30:12 +0100 Subject: [PATCH 2/9] Added support for domain quota. see #1030 --- modoboa/admin/app_settings.py | 26 +++++++++++--- modoboa/admin/factories.py | 3 +- modoboa/admin/forms/account.py | 9 ++--- modoboa/admin/forms/domain.py | 29 ++++----------- .../migrations/0010_auto_20170215_1328.py | 30 ++++++++++++++++ modoboa/admin/models/domain.py | 27 ++++++++++++-- modoboa/admin/models/mailbox.py | 24 +++++++------ .../templates/admin/domain_general_form.html | 1 + .../templates/admin/import_domains_form.html | 2 +- modoboa/admin/tests/test_account.py | 31 +++++++++++++--- modoboa/admin/tests/test_domain.py | 36 ++++++++----------- .../migrations/0012_auto_20170215_1034.py | 32 +++++++++++++++++ modoboa/lib/form_utils.py | 2 -- 13 files changed, 179 insertions(+), 73 deletions(-) create mode 100644 modoboa/admin/migrations/0010_auto_20170215_1328.py create mode 100644 modoboa/core/migrations/0012_auto_20170215_1034.py 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 e1bfce0b7..ad7eb13a3 100644 --- a/modoboa/admin/factories.py +++ b/modoboa/admin/factories.py @@ -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' diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 03c91ecc0..e41484396 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( @@ -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: diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index 6cb90f7b2..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"), ) - 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): 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 feeb8b7a8..3032fab66 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -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. diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 3d26d77d4..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,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. diff --git a/modoboa/admin/templates/admin/domain_general_form.html b/modoboa/admin/templates/admin/domain_general_form.html index 0364e74e1..4caca3a92 100644 --- a/modoboa/admin/templates/admin/domain_general_form.html +++ b/modoboa/admin/templates/admin/domain_general_form.html @@ -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 %} diff --git a/modoboa/admin/templates/admin/import_domains_form.html b/modoboa/admin/templates/admin/import_domains_form.html index 5a8666ef5..d1777468d 100644 --- a/modoboa/admin/templates/admin/import_domains_form.html +++ b/modoboa/admin/templates/admin/import_domains_form.html @@ -6,7 +6,7 @@ {% blocktrans trimmed %} Provide a CSV file where lines respect one of the following formats: diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index 334effba5..f8b9478a2 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -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() diff --git a/modoboa/admin/tests/test_domain.py b/modoboa/admin/tests/test_domain.py index 6e60b6dc9..859d985d6 100644 --- a/modoboa/admin/tests/test_domain.py +++ b/modoboa/admin/tests/test_domain.py @@ -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") diff --git a/modoboa/core/migrations/0012_auto_20170215_1034.py b/modoboa/core/migrations/0012_auto_20170215_1034.py new file mode 100644 index 000000000..51e0fd7d3 --- /dev/null +++ b/modoboa/core/migrations/0012_auto_20170215_1034.py @@ -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) + ] diff --git a/modoboa/lib/form_utils.py b/modoboa/lib/form_utils.py index d8914c4d4..f04c6f5fc 100644 --- a/modoboa/lib/form_utils.py +++ b/modoboa/lib/form_utils.py @@ -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 From c368b62f83439848fb29e446f5fa314f8457c746 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 15 Feb 2017 14:03:39 +0100 Subject: [PATCH 3/9] Fixed unit tests. see #1030 --- modoboa/admin/forms/account.py | 2 -- modoboa/admin/serializers.py | 9 +++++---- modoboa/admin/tests/test_api.py | 6 +++--- modoboa/admin/tests/test_domain_alias.py | 11 ++++++----- modoboa/admin/tests/test_import_.py | 4 ++-- modoboa/limits/tests/test_domain_limits.py | 2 ++ modoboa/limits/tests/test_user_limits.py | 12 +++++++----- modoboa/relaydomains/tests.py | 16 ++++++++++++---- 8 files changed, 37 insertions(+), 25 deletions(-) diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index e41484396..bf6963664 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -278,8 +278,6 @@ 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 use_default_domain_quota and diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index 6930c92cd..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", "default_mailbox_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/tests/test_api.py b/modoboa/admin/tests/test_api.py index cee1f4c44..a56eb0161 100644 --- a/modoboa/admin/tests/test_api.py +++ b/modoboa/admin/tests/test_api.py @@ -44,14 +44,14 @@ class DomainAPITestCase(ModoAPITestCase): """Check domain creation.""" url = reverse("external_api:domain-list") response = self.client.post( - url, {"name": "test3.com", "default_mailbox_quota": 10}) + url, {"name": "test3.com", "quota": 0, "default_mailbox_quota": 10} + ) self.assertEqual(response.status_code, 201) self.assertTrue( models.Domain.objects.filter(name="test3.com").exists()) response = self.client.post(url, {}) self.assertEqual(response.status_code, 400) self.assertIn("name", response.data) - self.assertIn("default_mailbox_quota", response.data) self.client.credentials( HTTP_AUTHORIZATION='Token ' + self.da_token.key) @@ -302,7 +302,7 @@ class AccountAPITestCase(ModoAPITestCase): self.client.credentials( HTTP_AUTHORIZATION='Token ' + self.da_token.key) data = copy.deepcopy(self.ACCOUNT_DATA) - data["mailbox"]["quota"] = 20 + data["mailbox"]["quota"] = 1000 url = reverse("external_api:account-list") response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) diff --git a/modoboa/admin/tests/test_domain_alias.py b/modoboa/admin/tests/test_domain_alias.py index ef731097c..f5f21efcc 100644 --- a/modoboa/admin/tests/test_domain_alias.py +++ b/modoboa/admin/tests/test_domain_alias.py @@ -38,11 +38,12 @@ class DomainAliasTestCase(ModoTestCase): def test_form(self): dom = Domain.objects.get(name="test.com") - values = dict( - name=dom.name, default_mailbox_quota=dom.default_mailbox_quota, - enabled=dom.enabled, aliases="domalias.net", - aliases_1="domaliasé.com", type="domain" - ) + values = { + "name": dom.name, "quota": dom.quota, + "default_mailbox_quota": dom.default_mailbox_quota, + "enabled": dom.enabled, "aliases": "domalias.net", + "aliases_1": "domaliasé.com", "type": "domain" + } self.ajax_post( reverse("admin:domain_change", args=[dom.id]), diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index 3f33a6c2a..634a6a64c 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -181,13 +181,13 @@ domainalias; domalias1.com; test.com; True self.client.logout() self.client.login(username="admin@test.com", password="toto") f = ContentFile(b""" -account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 20 +account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 40 """, name="identities.csv") resp = self.client.post( reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) - self.assertIn('Quota is greater than the allowed', resp.content) + self.assertIn("Domain quota exceeded", resp.content) def test_import_missing_quota(self): f = ContentFile(b""" diff --git a/modoboa/limits/tests/test_domain_limits.py b/modoboa/limits/tests/test_domain_limits.py index bf70ac4c9..dede7d468 100644 --- a/modoboa/limits/tests/test_domain_limits.py +++ b/modoboa/limits/tests/test_domain_limits.py @@ -34,6 +34,7 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): domain = Domain.objects.get(name="test.com") values = { "name": domain.name, "quota": domain.quota, + "default_mailbox_quota": domain.default_mailbox_quota, "enabled": domain.enabled, "type": "domain", "mailboxes_limit": 3, "mailbox_aliases_limit": 3, "domain_aliases_limit": 3, "domain_admins_limit": 3 @@ -62,6 +63,7 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): self.assertFalse(limit.is_exceeded()) values = { "name": domain.name, "quota": domain.quota, + "default_mailbox_quota": domain.default_mailbox_quota, "enabled": domain.enabled, "type": "domain", "mailboxes_limit": 2, "mailbox_aliases_limit": 2, "domain_aliases_limit": 2, "domain_admins_limit": 2, diff --git a/modoboa/limits/tests/test_user_limits.py b/modoboa/limits/tests/test_user_limits.py index 78c2b109b..8a2f72c46 100644 --- a/modoboa/limits/tests/test_user_limits.py +++ b/modoboa/limits/tests/test_user_limits.py @@ -78,7 +78,8 @@ class ResourceTestCase(lib_tests.ModoTestCase): def _create_domain(self, name, status=200, withtpl=False): values = { - "name": name, "quota": 100, "create_dom_admin": False, + "name": name, "quota": 100, "default_mailbox_quota": 10, + "create_dom_admin": False, "create_aliases": False, "stepid": "step3", "type": "domain" } if withtpl: @@ -95,16 +96,17 @@ class ResourceTestCase(lib_tests.ModoTestCase): def _domain_alias_operation(self, optype, domain, name, status=200): dom = Domain.objects.get(name=domain) values = { - 'name': dom.name, 'quota': dom.quota, 'enabled': dom.enabled, - "type": "domain" + "name": dom.name, "quota": dom.quota, "enabled": dom.enabled, + "type": "domain", + "default_mailbox_quota": dom.default_mailbox_quota, } aliases = [alias.name for alias in dom.domainalias_set.all()] - if optype == 'add': + if optype == "add": aliases.append(name) else: aliases.remove(name) for cpt, alias in enumerate(aliases): - fname = 'aliases' if not cpt else 'aliases_%d' % cpt + fname = "aliases" if not cpt else "aliases_%d" % cpt values[fname] = alias self.ajax_post( reverse("admin:domain_change", args=[dom.id]), diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index 443d1523e..b76606928 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -23,7 +23,8 @@ class Operations(object): values = { "name": name, "create_dom_admin": False, "type": "relaydomain", "target_host": "external.host.tld", "target_port": 25, - "service": srv.id, "enabled": True, "stepid": "step3" + "service": srv.id, "enabled": True, "stepid": "step3", + "quota": 0, "default_mailbox_quota": 0 } return self.ajax_post( reverse("admin:domain_add"), @@ -35,7 +36,8 @@ class Operations(object): values = { "name": rdom.domain.name, "target_host": rdom.target_host, "target_port": rdom.target_port, - "type": "relaydomain", "service": rdom.service.id + "type": "relaydomain", "service": rdom.service.id, + "quota": 0, "default_mailbox_quota": 0 } aliases = [alias.name for alias in rdom.domain.domainalias_set.all()] if optype == 'add': @@ -144,7 +146,8 @@ class RelayDomainsTestCase(ModoTestCase, Operations): values = { "name": "relaydomain.org", "target_host": self.rdom.target_host, "target_port": 4040, "type": "relaydomain", "enabled": True, - "service": self.rdom.service.id + "service": self.rdom.service.id, + "quota": 0, "default_mailbox_quota": 0 } self.ajax_post( reverse("admin:domain_change", args=[self.rdom.domain.id]), @@ -159,6 +162,8 @@ class RelayDomainsTestCase(ModoTestCase, Operations): values = { "name": "relaydomain.tld", "type": "domain", + "quota": 0, + "default_mailbox_quota": 0, "enabled": True, "target_host": self.rdom.target_host, "target_port": self.rdom.target_port, @@ -176,6 +181,8 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "name": "relaydomain.tld", "type": "relaydomain", "enabled": True, + "quota": 0, + "default_mailbox_quota": 0 } self.ajax_post( reverse("admin:domain_change", args=[domain_pk]), values) @@ -195,7 +202,8 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "name": "relaydomain.org", "target_host": self.rdom.target_host, "target_port": self.rdom.target_port, "type": "relaydomain", "service": self.rdom.service.id, - "aliases": "relaydomainalias.net" + "aliases": "relaydomainalias.net", + "quota": 0, "default_mailbox_quota": 0 } self.ajax_post( reverse('admin:domain_change', From cdd2fcc2bc3d42aaec0703ed4840373b091e8d5b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 15 Feb 2017 14:11:56 +0100 Subject: [PATCH 4/9] Fixed unit test. see #1030 --- modoboa/core/tests/test_views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index 4f304d1b8..46f333830 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -115,6 +115,7 @@ class SettingsTestCase(ModoTestCase): "admin-auto_create_domain_and_mailbox": "True", "admin-enable_dnsbl_checks": "True", "admin-default_domain_quota": "0", + "admin-default_mailbox_quota": "0", "core-ldap_server_address": "localhost", "core-authentication_type": "local", "core-ldap_admin_groups": "", From b9342f78017d26cf52b97841b0b3628b2b58e64f Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 15 Feb 2017 15:41:57 +0100 Subject: [PATCH 5/9] Display domain quota usage. see #1030 --- modoboa/admin/forms/account.py | 3 +- modoboa/admin/models/domain.py | 7 ++ .../admin/templates/admin/domain_detail.html | 91 +++++++++++++------ 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index bf6963664..fd9b1035b 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -197,7 +197,8 @@ 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).") + "administrators)."), + widget=forms.widgets.TextInput(attrs={"class": "form-control"}) ) quota_act = forms.BooleanField(required=False) aliases = lib_fields.UTF8AndEmptyUserEmailField( diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 3032fab66..2eeb83e49 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -167,6 +167,13 @@ class Domain(AdminObject): 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. 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 "Summary" %}

-
+ {% if enable_mx_checks or enable_dnsbl_checks %} -
- {% trans "DNS status" %} - {% include "admin/_domain_dnschecks_status.html" %} -
+ + + + {% endif %} -
- {% 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 }}
- + + + + + + + + + + + + + + + + + + + + + + + + + {% if domain.quota %} + + + + + {% endif %} + + + + +
{% 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" %}
{% if object.domainalias_set.exists %} @@ -58,24 +87,26 @@

{% trans "Administrators" %}

-
- +
+ - {% for da in domain.admins %} - - - - - - {% empty %} - - {% endfor %} -
{% trans "Username" %}{% trans "Name" %}
{{ da.username }}{{ da.fullname }} - {% if perms.admin.change_domain %} - - {% endif %} -
{% trans "No domain administrator defined yet." %}
-
+ + + {% for da in domain.admins %} + + {{ da.username }} + {{ da.fullname }} + + {% if perms.admin.change_domain %} + + {% endif %} + + + {% empty %} + {% trans "No domain administrator defined yet." %} + {% endfor %} + +
{% for template in templates.right %} From e8ceadaa1d2f510650379f0b7640f37010b2b481 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 15 Feb 2017 16:36:12 +0100 Subject: [PATCH 6/9] Updated import. see #1030 --- modoboa/admin/models/domain.py | 24 +++++++++++++------ .../templates/admin/import_domains_form.html | 2 +- modoboa/admin/tests/test_import_.py | 9 +++---- modoboa/limits/tests/test_import.py | 6 ++--- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 2eeb83e49..145edfc8a 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -241,28 +241,38 @@ class Domain(AdminObject): The expected fields order is the following:: - "domain", name, default_mailbox_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(): raise Conflict try: - self.default_mailbox_quota = int(row[2].strip()) + self.quota = int(row[2].strip()) except ValueError: raise BadRequest( - _("Invalid default mailbox 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.default_mailbox_quot, self.enabled]) + "domain", self.name, self.quota, self.default_mailbox_quot, + self.enabled + ]) for dalias in self.domainalias_set.all(): dalias.to_csv(csvwriter) diff --git a/modoboa/admin/templates/admin/import_domains_form.html b/modoboa/admin/templates/admin/import_domains_form.html index d1777468d..28fe99dea 100644 --- a/modoboa/admin/templates/admin/import_domains_form.html +++ b/modoboa/admin/templates/admin/import_domains_form.html @@ -6,7 +6,7 @@ {% blocktrans trimmed %} Provide a CSV file where lines respect one of the following formats:
    -
  • domain; name; default mailbox quota; enabled
  • +
  • domain; name; quota; default mailbox quota; enabled
  • domainalias; name; targeted domain; enabled
  • relaydomain; name; target host; target port; service; enabled; verify recipients
diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index 634a6a64c..b95c4b637 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -25,8 +25,8 @@ class ImportTestCase(ModoTestCase): self.assertEqual(response.status_code, 200) self.assertIn("Provide a CSV", response.content) - f = ContentFile(b"""domain; domain1.com; 100; True -domain; domain2.com; 200; False + f = ContentFile(b"""domain; domain1.com; 1000; 100; True +domain; domain2.com; 1000; 200; False domainalias; domalias1.com; domain1.com; True """, name="domains.csv") self.client.post( @@ -36,6 +36,7 @@ domainalias; domalias1.com; domain1.com; True ) admin = User.objects.get(username="admin") dom = Domain.objects.get(name="domain1.com") + self.assertEqual(dom.quota, 1000) self.assertEqual(dom.default_mailbox_quota, 100) self.assertTrue(dom.enabled) self.assertTrue(admin.is_owner(dom)) @@ -49,7 +50,7 @@ domainalias; domalias1.com; domain1.com; True self.assertTrue(admin.is_owner(dom)) def test_import_domains_with_conflict(self): - f = ContentFile(b"""domain;test.alias;10;True + f = ContentFile(b"""domain;test.alias;100;10;True domainalias;test.alias;test.com;True """, name="domains.csv") resp = self.client.post( @@ -160,7 +161,7 @@ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; ; t self.client.logout() self.client.login(username="admin@test.com", password="toto") f = ContentFile(b""" -domain; domain2.com; 200; False +domain; domain2.com; 1000; 200; False """, name="identities.csv") resp = self.client.post( reverse("admin:identity_import"), diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py index dc222f426..c3ca812b3 100644 --- a/modoboa/limits/tests/test_import.py +++ b/modoboa/limits/tests/test_import.py @@ -147,8 +147,8 @@ class UserLimitImportTestCase(LimitImportTestCase): username=self.reseller.username, password="toto") limit = self.reseller.userobjectlimit_set.get(name="domains") self.assertFalse(limit.is_exceeded()) - f = ContentFile(b"""domain; domain1.com; 100; True -domain; domain2.com; 200; False + f = ContentFile(b"""domain; domain1.com; 1000; 100; True +domain; domain2.com; 1000; 200; False """, name="domains.csv") self.client.post( reverse("admin:domain_import"), { @@ -157,7 +157,7 @@ domain; domain2.com; 200; False ) self.assertTrue(limit.is_exceeded()) - f = ContentFile(b"""domain; domain3.com; 100; True + f = ContentFile(b"""domain; domain3.com; 1000; 100; True """, name="domains.csv") response = self.client.post( reverse("admin:domain_import"), { From cdd4e7e89eef1c149aa6c5446dee38770b12353a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 15 Feb 2017 20:46:47 +0100 Subject: [PATCH 7/9] Increase coverage. see #1030 --- modoboa/admin/models/domain.py | 3 ++- modoboa/admin/tests/test_export.py | 17 ++++++++++++++++- modoboa/admin/tests/test_import_.py | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 145edfc8a..49547189e 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -269,8 +269,9 @@ class Domain(AdminObject): self.save(creator=user) def to_csv(self, csvwriter): + """Export domain and domain aliases to CSV format.""" csvwriter.writerow([ - "domain", self.name, self.quota, self.default_mailbox_quot, + "domain", self.name, self.quota, self.default_mailbox_quota, self.enabled ]) for dalias in self.domainalias_set.all(): diff --git a/modoboa/admin/tests/test_export.py b/modoboa/admin/tests/test_export.py index 639ae767a..0de4ed545 100644 --- a/modoboa/admin/tests/test_export.py +++ b/modoboa/admin/tests/test_export.py @@ -8,7 +8,6 @@ from .. import factories class ExportTestCase(ModoTestCase): - """Test case for export operations.""" @classmethod @@ -17,6 +16,14 @@ class ExportTestCase(ModoTestCase): super(ExportTestCase, cls).setUpTestData() factories.populate_database() + def __export_domains(self, domfilter=""): + self.client.get( + "{}?domfilter={}".format( + reverse("admin:_domain_list"), domfilter)) + return self.client.post( + reverse("admin:domain_export"), {"filename": "test.csv"} + ) + def __export_identities(self, idtfilter="", grpfilter=""): self.client.get( reverse("admin:_identity_list") + @@ -34,6 +41,14 @@ class ExportTestCase(ModoTestCase): continue self.assertIn(entry, list2) + def test_export_domains(self): + """Check domain export.""" + response = self.__export_domains() + self.assertListEqual( + "domain;test2.com;0;0;True\r\ndomain;test.com;50;10;True\r\n", + response.content.strip() + ) + def test_export_identities(self): response = self.__export_identities() self.assertListEqual( diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index b95c4b637..067c50cab 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -49,6 +49,22 @@ domainalias; domalias1.com; domain1.com; True self.assertFalse(dom.enabled) self.assertTrue(admin.is_owner(dom)) + def test_domain_import_bad_syntax(self): + """Check errors handling.""" + url = reverse("admin:domain_import") + f = ContentFile("domain; domain1.com; 100; True", + name="domains.csv") + response = self.client.post(url, {"sourcefile": f}) + self.assertContains(response, "Invalid line") + f = ContentFile("domain; domain1.com; XX; 100; True", + name="domains.csv") + response = self.client.post(url, {"sourcefile": f}) + self.assertContains(response, "Invalid quota value") + f = ContentFile("domain; domain1.com; 100; XX; True", + name="domains.csv") + response = self.client.post(url, {"sourcefile": f}) + self.assertContains(response, "Invalid default mailbox quota") + def test_import_domains_with_conflict(self): f = ContentFile(b"""domain;test.alias;100;10;True domainalias;test.alias;test.com;True From d0d6b92409c1f11adb8d8bd22917e891e7cff1f8 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 15 Feb 2017 21:15:57 +0100 Subject: [PATCH 8/9] Increase coverage. see #1030 --- modoboa/admin/tests/test_export.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/modoboa/admin/tests/test_export.py b/modoboa/admin/tests/test_export.py index 0de4ed545..15220988b 100644 --- a/modoboa/admin/tests/test_export.py +++ b/modoboa/admin/tests/test_export.py @@ -1,10 +1,13 @@ """Export related test cases.""" +from testfixtures import compare + from django.core.urlresolvers import reverse from modoboa.lib.tests import ModoTestCase from .. import factories +from .. import models class ExportTestCase(ModoTestCase): @@ -43,11 +46,15 @@ class ExportTestCase(ModoTestCase): def test_export_domains(self): """Check domain export.""" + dom = models.Domain.objects.get(name="test.com") + factories.DomainAliasFactory(name="alias.test", target=dom) response = self.__export_domains() - self.assertListEqual( - "domain;test2.com;0;0;True\r\ndomain;test.com;50;10;True\r\n", - response.content.strip() - ) + expected_response = [ + "domain;test2.com;0;0;True", + "domain;test.com;50;10;True", + "domainalias;alias.test;test.com;True" + ] + compare(response.content.strip().split("\r\n"), expected_response) def test_export_identities(self): response = self.__export_identities() From f3e7644fc25a56f3650c7d10b580dbc5282841ff Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 16 Feb 2017 08:40:34 +0100 Subject: [PATCH 9/9] Fixed unit tests. see #1030 --- modoboa/admin/tests/test_export.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/modoboa/admin/tests/test_export.py b/modoboa/admin/tests/test_export.py index 15220988b..f35eb8a9f 100644 --- a/modoboa/admin/tests/test_export.py +++ b/modoboa/admin/tests/test_export.py @@ -1,7 +1,5 @@ """Export related test cases.""" -from testfixtures import compare - from django.core.urlresolvers import reverse from modoboa.lib.tests import ModoTestCase @@ -38,8 +36,10 @@ class ExportTestCase(ModoTestCase): ) def assertListEqual(self, list1, list2): - list2 = list2.split('\r\n') - for entry in list1.split('\r\n'): + list1 = list1.split("\r\n") + list2 = list2.split("\r\n") + self.assertEqual(len(list1), len(list2)) + for entry in list1: if not entry: continue self.assertIn(entry, list2) @@ -50,11 +50,14 @@ class ExportTestCase(ModoTestCase): factories.DomainAliasFactory(name="alias.test", target=dom) response = self.__export_domains() expected_response = [ - "domain;test2.com;0;0;True", "domain;test.com;50;10;True", - "domainalias;alias.test;test.com;True" + "domainalias;alias.test;test.com;True", + "domain;test2.com;0;0;True", ] - compare(response.content.strip().split("\r\n"), expected_response) + self.assertListEqual( + "\r\n".join(expected_response), + response.content.strip() + ) def test_export_identities(self): response = self.__export_identities()