From 49e5362f411f5ca31c09d27df08632fb16250280 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 16 Apr 2016 15:27:33 +0200 Subject: [PATCH] WIP. see #799 --- modoboa/admin/forms/account.py | 27 ++++--- modoboa/admin/forms/alias.py | 9 +++ modoboa/admin/forms/domain.py | 36 ++++----- modoboa/admin/models/domain.py | 1 + modoboa/admin/tests/test_account.py | 4 +- modoboa/admin/views/alias.py | 2 +- modoboa/admin/views/domain.py | 2 +- modoboa/core/signals.py | 2 +- .../lib/migrations/0005_auto_20160416_1449.py | 23 ++++++ modoboa/limits/app_settings.py | 57 ++++++++++++-- modoboa/limits/constants.py | 17 ++++- modoboa/limits/controls.py | 8 +- modoboa/limits/forms.py | 57 +++++++++++--- modoboa/limits/general_callbacks.py | 34 +++++++-- modoboa/limits/handlers.py | 34 +++++++-- .../migrations/0003_auto_20160413_1046.py | 10 +-- .../migrations/0005_auto_20160415_1654.py | 50 +++++++++++++ modoboa/limits/models.py | 74 ++++++++++++++++--- modoboa/limits/tests.py | 14 ++-- modoboa/limits/utils.py | 9 ++- modoboa/relaydomains/tests.py | 6 +- 21 files changed, 375 insertions(+), 101 deletions(-) create mode 100644 modoboa/lib/migrations/0005_auto_20160416_1449.py create mode 100644 modoboa/limits/migrations/0005_auto_20160415_1654.py diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 098c6cc56..455290f6f 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -13,7 +13,7 @@ from modoboa.core.models import User from modoboa.core import signals as core_signals from modoboa.lib import events, parameters from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import PermDeniedException +from modoboa.lib import exceptions as lib_exceptions from modoboa.lib import fields as lib_fields from modoboa.lib.form_utils import ( DynamicForm, TabForms, WizardForm, WizardStep @@ -161,7 +161,8 @@ class AccountFormGeneral(forms.ModelForm): def save(self, commit=True): account = super(AccountFormGeneral, self).save(commit=False) if self.user == account and not self.cleaned_data["is_active"]: - raise PermDeniedException(_("You can't disable your own account")) + raise lib_exceptions.PermDeniedException( + _("You can't disable your own account")) if commit: if "password1" in self.cleaned_data \ and self.cleaned_data["password1"] != "": @@ -198,11 +199,7 @@ class AccountFormMail(forms.Form, DynamicForm): ) def __init__(self, *args, **kwargs): - if "instance" in kwargs: - self.mb = kwargs["instance"] - del kwargs["instance"] - else: - self.mb = None + self.mb = kwargs.pop("instance", None) super(AccountFormMail, self).__init__(*args, **kwargs) self.field_widths = { "quota": 3 @@ -245,6 +242,13 @@ class AccountFormMail(forms.Form, DynamicForm): self.domain = Domain.objects.get(name=domname) except Domain.DoesNotExist: raise forms.ValidationError(_("Domain does not exist")) + if not self.mb: + try: + core_signals.can_create_object.send( + sender=self.__class__, context=self.domain, + object_type="mailboxes") + except lib_exceptions.ModoboaException as inst: + raise forms.ValidationError(inst) return email def clean(self): @@ -262,9 +266,9 @@ class AccountFormMail(forms.Form, DynamicForm): def create_mailbox(self, user, account): """Create a mailbox associated to :kw:`account`.""" if not user.can_access(self.domain): - raise PermDeniedException + raise lib_exceptions.PermDeniedException core_signals.can_create_object.send( - self.__class__, user=user, object_type="mailboxes") + self.__class__, context=user, object_type="mailboxes") self.mb = Mailbox( address=self.locpart, domain=self.domain, user=account, use_domain_quota=self.cleaned_data["quota_act"]) @@ -296,8 +300,11 @@ class AccountFormMail(forms.Form, DynamicForm): if not aliases: return core_signals.can_create_object.send( - self.__class__, user=user, object_type="mailbox_aliases", + self.__class__, context=user, object_type="mailbox_aliases", count=len(aliases)) + core_signals.can_create_object.send( + self.__class__, context=self.mb.domain, + object_type="mailbox_aliases", count=len(aliases)) for alias in aliases: if self.mb.aliasrecipient_set.select_related("alias").filter( alias__address=alias).exists(): diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py index 76fa537ed..f18063c58 100644 --- a/modoboa/admin/forms/alias.py +++ b/modoboa/admin/forms/alias.py @@ -6,6 +6,8 @@ from django import forms from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.core import signals as core_signals +from modoboa.lib import exceptions as lib_exceptions from modoboa.lib.email_utils import split_mailbox from modoboa.lib import fields as lib_fields from modoboa.lib.form_utils import ( @@ -74,6 +76,13 @@ class AliasForm(forms.ModelForm, DynamicForm): raise forms.ValidationError( _("You don't have access to this domain") ) + if self.instance.pk: + try: + core_signals.can_create_object.send( + sender=self.__class__, context=domain, + object_type="mailbox_aliases") + except lib_exceptions.ModoboaException as inst: + raise forms.ValidationError(inst) return self.cleaned_data["address"].lower() def clean(self): diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index 636923d77..eb9bbd9b0 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -101,6 +101,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): "name", _("A %s with this name already exists") % unicode(label) ) + self.aliases = [] for k in cleaned_data.keys(): if not k.startswith("aliases"): continue @@ -120,6 +121,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): k, _("A %s with this name already exists") % unicode(label) ) + self.aliases.append(cleaned_data[k]) return cleaned_data def save(self, user, commit=True, domalias_post_create=False): @@ -134,28 +136,20 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if not commit: return d d.save() - aliases = [] - for k, v in self.cleaned_data.iteritems(): - if not k.startswith("aliases"): - continue - if v in ["", None]: - continue - aliases.append(v) for dalias in d.domainalias_set.all(): - if dalias.name not in aliases: + if dalias.name not in self.aliases: dalias.delete() else: - aliases.remove(dalias.name) - if aliases: + self.aliases.remove(dalias.name) + if self.aliases: core_signals.can_create_object.send( - self.__class__, user=user, object_type="domain_aliases", - count=len(aliases)) - for alias in aliases: - try: - d.domainalias_set.get(name=alias) - except DomainAlias.DoesNotExist: - pass - else: + self.__class__, context=user, object_type="domain_aliases", + count=len(self.aliases)) + core_signals.can_create_object.send( + self.__class__, context=d, object_type="domain_aliases", + count=len(self.aliases)) + for alias in self.aliases: + if d.domainalias_set.filter(name=alias).exists(): continue options = {"creator": user} if domalias_post_create else {} DomainAlias(name=alias, target=d, enabled=d.enabled).save( @@ -225,9 +219,9 @@ class DomainFormOptions(forms.Form): except User.DoesNotExist: pass else: - raise Conflict(_("User '%s' already exists" % username)) + raise Conflict(_("User '%s' already exists") % username) core_signals.can_create_object.send( - self.__class__, user=user, object_type="mailboxes") + self.__class__, context=user, object_type="mailboxes") da = User(username=username, email=username, is_active=True) da.set_password("password") da.save() @@ -245,7 +239,7 @@ class DomainFormOptions(forms.Form): if domain.type == "domain" and \ self.cleaned_data["create_aliases"] == "yes": core_signals.can_create_object.send( - self.__class__, user=user, object_type="mailbox_aliases") + self.__class__, context=user, object_type="mailbox_aliases") alias = Alias( address=u"postmaster@{}".format(domain.name), domain=domain, enabled=True diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 65267a6a9..cd4d425d8 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -101,6 +101,7 @@ class Domain(AdminObject): :param User account: the administrotor to add """ from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(account, self) for mb in self.mailbox_set.all(): if mb.user.has_perm("admin.add_domain"): diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index 522899025..4ecd3ae5e 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -161,9 +161,9 @@ class PermissionsTestCase(ModoTestCase): from modoboa.limits import utils as limits_utils super(PermissionsTestCase, cls).setUpTestData() - for name, tpl in limits_utils.get_limit_templates(): + for name, tpl in limits_utils.get_user_limit_templates(): parameters.save_admin( - "DEFLT_{0}_LIMIT".format(name.upper()), 2, + "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, app="limits" ) factories.populate_database() diff --git a/modoboa/admin/views/alias.py b/modoboa/admin/views/alias.py index a40bd29cf..ef771b8f1 100644 --- a/modoboa/admin/views/alias.py +++ b/modoboa/admin/views/alias.py @@ -37,7 +37,7 @@ def _validate_alias(request, form, successmsg, callback=None): def _new_alias(request, title, action, successmsg, tplname="admin/aliasform.html"): core_signals.can_create_object.send( - "new_alias", user=request.user, object_type="mailbox_aliases") + "new_alias", context=request.user, object_type="mailbox_aliases") if request.method == "POST": def callback(user, alias): alias.post_create(user) diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index 0d222542f..2e3f57289 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -100,7 +100,7 @@ def domains_list(request): @reversion.create_revision() def newdomain(request): core_signals.can_create_object.send( - "newdomain", user=request.user, object_type="domains") + "newdomain", context=request.user, object_type="domains") return DomainWizard(request).process() diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py index 1171e3747..1275ffe1b 100644 --- a/modoboa/core/signals.py +++ b/modoboa/core/signals.py @@ -3,4 +3,4 @@ import django.dispatch can_create_object = django.dispatch.Signal( - providing_args=["user", "object_type", "count"]) + providing_args=["context", "object_type", "count"]) diff --git a/modoboa/lib/migrations/0005_auto_20160416_1449.py b/modoboa/lib/migrations/0005_auto_20160416_1449.py new file mode 100644 index 000000000..6c594dec3 --- /dev/null +++ b/modoboa/lib/migrations/0005_auto_20160416_1449.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def rename_limits_parameters(apps, schema_editor): + """Rename parameters.""" + Parameter = apps.get_model("lib", "Parameter") + for param in Parameter.objects.filter(name__startswith="DEFLT_"): + param.name = param.name.replace("DEFLT_", "DEFLT_USER_") + param.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('lib', '0004_auto_20151114_1409'), + ] + + operations = [ + migrations.RunPython(rename_limits_parameters) + ] diff --git a/modoboa/limits/app_settings.py b/modoboa/limits/app_settings.py index 8c2129ccb..0c4f1f98a 100644 --- a/modoboa/limits/app_settings.py +++ b/modoboa/limits/app_settings.py @@ -10,9 +10,9 @@ from modoboa.lib.parameters import AdminParametersForm class ParametersForm(AdminParametersForm): app = "limits" - defv_sep = SeparatorField(label=_("Default limits")) + defv_sep = SeparatorField(label=_("Default per-admin limits")) - deflt_domain_admins_limit = forms.IntegerField( + deflt_user_domain_admins_limit = forms.IntegerField( label=_("Domain admins"), initial=0, help_text=_( @@ -23,7 +23,7 @@ class ParametersForm(AdminParametersForm): widget=forms.widgets.TextInput( attrs={"class": "col-md-1 form-control"}) ) - deflt_domains_limit = forms.IntegerField( + deflt_user_domains_limit = forms.IntegerField( label=_("Domains"), initial=0, help_text=_( @@ -33,7 +33,7 @@ class ParametersForm(AdminParametersForm): widget=forms.widgets.TextInput( attrs={"class": "col-md-1 form-control"}) ) - deflt_domain_aliases_limit = forms.IntegerField( + deflt_user_domain_aliases_limit = forms.IntegerField( label=_("Domain aliases"), initial=0, help_text=_( @@ -44,7 +44,7 @@ class ParametersForm(AdminParametersForm): widget=forms.widgets.TextInput( attrs={"class": "col-md-1 form-control"}) ) - deflt_mailboxes_limit = forms.IntegerField( + deflt_user_mailboxes_limit = forms.IntegerField( label=_("Mailboxes"), initial=0, help_text=_( @@ -54,7 +54,7 @@ class ParametersForm(AdminParametersForm): widget=forms.widgets.TextInput( attrs={"class": "col-md-1 form-control"}) ) - deflt_mailbox_aliases_limit = forms.IntegerField( + deflt_user_mailbox_aliases_limit = forms.IntegerField( label=_("Mailbox aliases"), initial=0, help_text=_( @@ -65,6 +65,51 @@ class ParametersForm(AdminParametersForm): attrs={"class": "col-md-1 form-control"}) ) + domain_limits_sep = SeparatorField(label=_("Default per-domain limits")) + + # deflt_domain_domain_admins_limit = forms.IntegerField( + # label=_("Domain admins"), + # initial=0, + # help_text=_( + # "Maximum number of allowed domain administrators for a new " + # "domain. (0 to deny any creation, -1 to allow unlimited " + # "creations)" + # ), + # widget=forms.widgets.TextInput( + # attrs={"class": "col-md-1 form-control"}) + # ) + deflt_domain_domain_aliases_limit = forms.IntegerField( + label=_("Domain aliases"), + initial=0, + help_text=_( + "Maximum number of allowed domain aliases for a new " + "domain. (0 to deny any creation, -1 to allow " + "unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + deflt_domain_mailboxes_limit = forms.IntegerField( + label=_("Mailboxes"), + initial=0, + help_text=_( + "Maximum number of allowed mailboxes for a new domain. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + deflt_domain_mailbox_aliases_limit = forms.IntegerField( + label=_("Mailbox aliases"), + initial=0, + help_text=_( + "Maximum number of allowed aliases for a new domain. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + widget=forms.widgets.TextInput( + attrs={"class": "col-md-1 form-control"}) + ) + def __init__(self, *args, **kwargs): super(AdminParametersForm, self).__init__(*args, **kwargs) self._load_extra_parameters('A') diff --git a/modoboa/limits/constants.py b/modoboa/limits/constants.py index 947adbe62..16a074c1b 100644 --- a/modoboa/limits/constants.py +++ b/modoboa/limits/constants.py @@ -5,7 +5,7 @@ import collections from django.utils.translation import ugettext_lazy as _ -DEFAULT_LIMITS = collections.OrderedDict(( +DEFAULT_USER_LIMITS = collections.OrderedDict(( ("domains", { "content_type": "admin.domain", "label": _("Domains"), "help": _("Maximum number of domains this user can create"), @@ -28,3 +28,18 @@ DEFAULT_LIMITS = collections.OrderedDict(( "required_role": "Resellers", "extra_filters": {"groups__name": "DomainAdmins"}}) )) + + +DEFAULT_DOMAIN_LIMITS = collections.OrderedDict(( + ("domain_aliases", { + "relation": "domainalias_set", "label": _("Domain aliases"), + "help": _("Maximum number of domain aliases allowed for this domain.") + }), + ("mailboxes", { + "relation": "mailbox_set", "label": _("Mailboxes"), + "help": _("Maximum number of mailboxes allowed for this domain.")}), + ("mailbox_aliases", { + "relation": "alias_set", "label": _("Mailbox aliases"), + "help": _("Maximum number of mailbox aliases allowed for this domain.") + }) +)) diff --git a/modoboa/limits/controls.py b/modoboa/limits/controls.py index 4d0c20c35..bec50673e 100644 --- a/modoboa/limits/controls.py +++ b/modoboa/limits/controls.py @@ -18,11 +18,11 @@ def move_pool_resource(owner, user): associated resource to the pool of its owner. """ if not owner.is_superuser: - for name, ltpl in utils.get_limit_templates(): - l = user.objectlimit_set.get(name=name) + for name, ltpl in utils.get_user_limit_templates(): + l = user.userobjectlimit_set.get(name=name) if l.max_value < 0: continue - ol = owner.objectlimit_set.get(name=name) + ol = owner.userobjectlimit_set.get(name=name) ol.max_value += l.max_value ol.save() @@ -44,7 +44,7 @@ def user_can_set_role(user, role, account=None): lname = "domain_admins" condition = ( user.is_superuser or - not user.objectlimit_set.get(name=lname).is_exceeded() + not user.userobjectlimit_set.get(name=lname).is_exceeded() ) if condition: return [True] diff --git a/modoboa/limits/forms.py b/modoboa/limits/forms.py index 8d3285cf2..0af239697 100644 --- a/modoboa/limits/forms.py +++ b/modoboa/limits/forms.py @@ -5,7 +5,6 @@ from django import forms from django.utils.translation import ugettext as _ -from modoboa.lib import parameters from modoboa.lib.permissions import get_object_owner from .lib import BadLimitValue, UnsufficientResource @@ -16,12 +15,9 @@ class ResourcePoolForm(forms.Form): """Dynamic form to display user limits.""" def __init__(self, *args, **kwargs): - self.account = None - if "instance" in kwargs: - self.account = kwargs["instance"] - del kwargs["instance"] + self.account = kwargs.pop("instance", None) super(ResourcePoolForm, self).__init__(*args, **kwargs) - for name, tpl in utils.get_limit_templates(): + for name, tpl in utils.get_user_limit_templates(): if "required_role" in tpl: condition = ( self.account is not None and @@ -32,7 +28,7 @@ class ResourcePoolForm(forms.Form): self.fields["{}_limit".format(name)] = forms.IntegerField( label=tpl["label"], help_text=tpl["help"] ) - if hasattr(self, "account"): + if self.account: self.load_from_user(self.account) def clean(self): @@ -50,7 +46,7 @@ class ResourcePoolForm(forms.Form): for fieldname in self.fields.keys(): lname = fieldname.replace("_limit", "") self.fields[fieldname].initial = ( - user.objectlimit_set.get(name=lname).max_value) + user.userobjectlimit_set.get(name=lname).max_value) def allocate_from_user(self, limit, user): """Allocate resource using an existing user. @@ -60,7 +56,7 @@ class ResourcePoolForm(forms.Form): also be limited, the resource he gives is taken from its own pool. """ - ol = user.objectlimit_set.get(name=limit.name) + ol = user.userobjectlimit_set.get(name=limit.name) fieldname = "{}_limit".format(limit.name) newvalue = self.cleaned_data[fieldname] if newvalue == -1 and ol.max_value != -1: @@ -80,12 +76,51 @@ class ResourcePoolForm(forms.Form): def save(self): owner = get_object_owner(self.account) - for name, ltpl in utils.get_limit_templates(): + for name, ltpl in utils.get_user_limit_templates(): fieldname = "{}_limit".format(name) if fieldname not in self.cleaned_data: continue - l = self.account.objectlimit_set.get(name=name) + l = self.account.userobjectlimit_set.get(name=name) if not owner.is_superuser: self.allocate_from_user(l, owner) l.max_value = self.cleaned_data[fieldname] l.save() + + +class DomainLimitsForm(forms.Form): + """Per-domain limits form.""" + + def __init__(self, *args, **kwargs): + """Define limits as fields.""" + self.domain = kwargs.pop("instance", None) + super(DomainLimitsForm, self).__init__(*args, **kwargs) + for name, tpl in utils.get_domain_limit_templates(): + self.fields["{}_limit".format(name)] = forms.IntegerField( + label=tpl["label"], help_text=tpl["help"] + ) + if not self.domain: + return + for fieldname in self.fields.keys(): + lname = fieldname.replace("_limit", "") + self.fields[fieldname].initial = ( + self.domain.domainobjectlimit_set.get(name=lname).max_value) + + def clean(self): + """Ensure limit values are correct.""" + cleaned_data = super(DomainLimitsForm, self).clean() + if self.errors: + return cleaned_data + for lname in self.fields.keys(): + if cleaned_data[lname] < -1: + self.add_error(lname, _("Invalid limit")) + return cleaned_data + + def save(self, user): + """Set limits.""" + for name, ltpl in utils.get_domain_limit_templates(): + fieldname = "{}_limit".format(name) + if fieldname not in self.cleaned_data: + continue + l = self.domain.domainobjectlimit_set.get(name=name) + l.max_value = self.cleaned_data[fieldname] + l.save() diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py index 655497c82..92d6aded6 100644 --- a/modoboa/limits/general_callbacks.py +++ b/modoboa/limits/general_callbacks.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext as _ from modoboa.lib import events -from .forms import ResourcePoolForm +from . import forms from . import utils @@ -21,13 +21,13 @@ def display_pool_usage(user, target, currentpage): else: exceptions = ["domain_admins", "mailboxes", "mailbox_aliases"] names = [ - name for name, tpl in utils.get_limit_templates() + name for name, tpl in utils.get_user_limit_templates() if name not in exceptions and ("required_role" not in tpl or tpl["required_role"] == user.group) ] - limits = user.objectlimit_set.filter(name__in=names, max_value__gt=0) + limits = user.userobjectlimit_set.filter(name__in=names, max_value__gt=0) if len(limits) == 0: return [] return [ @@ -44,11 +44,29 @@ def extra_account_form(user, account=None): account.group not in ["Resellers", "DomainAdmins"]: return [] - return [ - dict( - id="resources", title=_("Resources"), cls=ResourcePoolForm - ) - ] + return [{ + "id": "resources", "title": _("Resources"), + "cls": forms.ResourcePoolForm + }] + + +@events.observe("ExtraDomainForm") +def extra_domain_form(user, domain): + """Include domain limits form.""" + if not user.has_perm("admin.change_domain"): + return [] + return [{ + "id": "resources", "title": _("Resources"), + "cls": forms.DomainLimitsForm + }] + + +@events.observe("FillDomainInstances") +def fill_domain_instances(user, domain, instances): + """Set domain instance for resources form.""" + if not user.has_perm("admin.change_domain"): + return + instances["resources"] = domain @events.observe("CheckExtraAccountForm") diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index 834c36bab..27dd1a19c 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -5,6 +5,7 @@ from django.dispatch import receiver from django.contrib.contenttypes.models import ContentType +from modoboa.admin import models as admin_models from modoboa.core import models as core_models from modoboa.core import signals as core_signals from modoboa.lib import signals as lib_signals @@ -16,11 +17,16 @@ from . import utils @receiver(core_signals.can_create_object) -def check_object_limit(sender, user, object_type, **kwargs): +def check_object_limit(sender, context, object_type, **kwargs): """Check if user can create a new object.""" - if user.is_superuser: - return True - limit = user.objectlimit_set.get(name=object_type) + if context.__class__.__name__ == "User": + if context.is_superuser: + return True + limit = context.userobjectlimit_set.get(name=object_type) + elif context.__class__.__name__ == "Domain": + limit = context.domainobjectlimit_set.get(name=object_type) + else: + raise NotImplementedError count = kwargs.get("count", 1) if limit.is_exceeded(count): raise lib.LimitReached(limit) @@ -33,13 +39,27 @@ def create_user_limits(sender, instance, **kwargs): return request = lib_signals.get_request() creator = request.user if request else None - for name, definition in utils.get_limit_templates(): + for name, definition in utils.get_user_limit_templates(): ct = ContentType.objects.get_by_natural_key( *definition["content_type"].split(".")) max_value = 0 # creator can be None if user was created by a factory if not creator or creator.is_superuser: max_value = int( - parameters.get_admin("DEFLT_{}_LIMIT".format(name.upper()))) - models.ObjectLimit.objects.create( + parameters.get_admin( + "DEFLT_USER_{}_LIMIT".format(name.upper()))) + models.UserObjectLimit.objects.create( user=instance, name=name, content_type=ct, max_value=max_value) + + +@receiver(signals.post_save, sender=admin_models.Domain) +def create_domain_limits(sender, instance, **kwargs): + """Create limits for new domain.""" + if not kwargs.get("created"): + return + for name, definition in utils.get_domain_limit_templates(): + max_value = 0 + max_value = int( + parameters.get_admin("DEFLT_DOMAIN_{}_LIMIT".format(name.upper()))) + models.DomainObjectLimit.objects.create( + domain=instance, name=name, max_value=max_value) diff --git a/modoboa/limits/migrations/0003_auto_20160413_1046.py b/modoboa/limits/migrations/0003_auto_20160413_1046.py index 5a5fa13d7..438479931 100644 --- a/modoboa/limits/migrations/0003_auto_20160413_1046.py +++ b/modoboa/limits/migrations/0003_auto_20160413_1046.py @@ -8,7 +8,7 @@ from django.conf import settings def move_limits_to_user(apps, schema_editor): """Move limits from Pool to User.""" LimitsPool = apps.get_model("limits", "LimitsPool") - ObjectLimit = apps.get_model("limits", "ObjectLimit") + UserObjectLimit = apps.get_model("limits", "UserObjectLimit") ContentType = apps.get_model("contenttypes", "ContentType") if not ContentType.objects.exists(): @@ -34,12 +34,12 @@ def move_limits_to_user(apps, schema_editor): .prefetch_related("limit_set")) for pool in qset: for limit in pool.limit_set.all(): - objlimit = ObjectLimit( + objlimit = UserObjectLimit( user=pool.user, max_value=limit.maxvalue, name=limit.name.replace("_limit", ""), content_type=name_to_content_type[limit.name]) to_create.append(objlimit) - ObjectLimit.objects.bulk_create(to_create) + UserObjectLimit.objects.bulk_create(to_create) class Migration(migrations.Migration): @@ -53,7 +53,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='ObjectLimit', + name='UserObjectLimit', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=254)), @@ -63,7 +63,7 @@ class Migration(migrations.Migration): ], ), migrations.AlterUniqueTogether( - name='objectlimit', + name='userobjectlimit', unique_together=set([('user', 'name')]), ), migrations.RunPython(move_limits_to_user) diff --git a/modoboa/limits/migrations/0005_auto_20160415_1654.py b/modoboa/limits/migrations/0005_auto_20160415_1654.py new file mode 100644 index 000000000..ab97e7807 --- /dev/null +++ b/modoboa/limits/migrations/0005_auto_20160415_1654.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import modoboa.limits.models + +from .. import utils + + +def create_limits(apps, schema_editor): + """Create limits for existing domains.""" + Domain = apps.get_model("admin", "Domain") + DomainObjectLimit = apps.get_model("limits", "DomainObjectLimit") + to_create = [] + for domain in Domain.objects.all(): + for name, tpl in utils.get_domain_limit_templates(): + to_create.append( + DomainObjectLimit(domain=domain, name=name, max_value=-1)) + DomainObjectLimit.objects.bulk_create(to_create) + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0003_auto_20151118_1215'), + ('limits', '0004_auto_20160413_1312'), + ] + + operations = [ + migrations.CreateModel( + name='DomainObjectLimit', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=254)), + ('max_value', models.IntegerField(default=0)), + ('domain', models.ForeignKey(to='admin.Domain')), + ], + bases=(modoboa.limits.models.ObjectLimitMixin, models.Model), + ), + migrations.AlterField( + model_name='userobjectlimit', + name='max_value', + field=models.IntegerField(default=0), + ), + migrations.AlterUniqueTogether( + name='domainobjectlimit', + unique_together=set([('domain', 'name')]), + ), + migrations.RunPython(create_limits) + ] diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py index c97deadff..3a3d3dba0 100644 --- a/modoboa/limits/models.py +++ b/modoboa/limits/models.py @@ -12,14 +12,42 @@ from modoboa.core import models as core_models from . import utils +class ObjectLimitMixin(object): + """Common methods.""" + + @property + def label(self): + """Return display name.""" + return self.definition["label"] + + @property + def usage(self): + """Return current limit usage in %.""" + if self.max_value < 0: + return -1 + if self.max_value == 0: + return 100 + return int(float(self.current_value) / self.max_value * 100) + + def is_exceeded(self, count=1): + """Check if limit will be reached if we add count object(s).""" + return self.current_value + 1 > self.max_value + + def __str__(self): + """Display current usage.""" + if self.max_value == -1: + return _("unlimited") + return "{}%".format(self.usage) + + @python_2_unicode_compatible -class ObjectLimit(models.Model): - """Per-user limits on object creation.""" +class UserObjectLimit(ObjectLimitMixin, models.Model): + """Object level limit.""" user = models.ForeignKey(settings.AUTH_USER_MODEL) name = models.CharField(max_length=254) content_type = models.ForeignKey("contenttypes.ContentType") - max_value = models.IntegerField(default=-2) + max_value = models.IntegerField(default=0) class Meta: unique_together = (("user", "name"), ) @@ -27,7 +55,7 @@ class ObjectLimit(models.Model): @property def definition(self): """Return the definition of this limit.""" - for name, tpl in utils.get_limit_templates(): + for name, tpl in utils.get_user_limit_templates(): if name == self.name: return tpl return None @@ -46,6 +74,11 @@ class ObjectLimit(models.Model): return model_class.objects.filter( pk__in=id_list, **self.definition["extra_filters"]).count() + @property + def label(self): + """Return display name.""" + return self.definition["label"] + @property def usage(self): """Return current limit usage in %.""" @@ -55,19 +88,38 @@ class ObjectLimit(models.Model): return 100 return int(float(self.current_value) / self.max_value * 100) - @property - def label(self): - """Return display name.""" - return self.definition["label"] - def is_exceeded(self, count=1): """Check if limit will be reached if we add count object(s).""" return self.current_value + 1 > self.max_value def __str__(self): """Display current usage.""" - if self.max_value == -2: - return _("undefined") if self.max_value == -1: return _("unlimited") return "{}%".format(self.usage) + + +class DomainObjectLimit(ObjectLimitMixin, models.Model): + """Per-domain limits on object creation.""" + + domain = models.ForeignKey("admin.Domain") + name = models.CharField(max_length=254) + max_value = models.IntegerField(default=0) + + class Meta: + unique_together = (("domain", "name"), ) + + @property + def definition(self): + """Return the definition of this limit.""" + for name, tpl in utils.get_domain_limit_templates(): + if name == self.name: + return tpl + return None + + @property + def current_value(self): + """Return the current number of objects.""" + if not self.definition: + raise RuntimeError("Bad limit {}".format(self.name)) + return getattr(self.domain, self.definition["relation"]).count() diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py index 40af6cc93..0b6616a21 100644 --- a/modoboa/limits/tests.py +++ b/modoboa/limits/tests.py @@ -47,9 +47,9 @@ class ResourceTestCase(ModoTestCase): def setUpTestData(cls): """Custom setUpTestData method.""" super(ResourceTestCase, cls).setUpTestData() - for name, tpl in utils.get_limit_templates(): + for name, tpl in utils.get_user_limit_templates(): parameters.save_admin( - "DEFLT_{0}_LIMIT".format(name.upper()), 2, + "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, app="limits" ) populate_database() @@ -107,7 +107,7 @@ class ResourceTestCase(ModoTestCase): ) def _check_limit(self, name, curvalue, maxvalue): - l = self.user.objectlimit_set.get(name=name) + l = self.user.userobjectlimit_set.get(name=name) self.assertEqual(l.current_value, curvalue) self.assertEqual(l.max_value, maxvalue) @@ -119,7 +119,7 @@ class DomainAdminTestCase(ResourceTestCase): """Create test data.""" super(DomainAdminTestCase, cls).setUpTestData() cls.user = User.objects.get(username='admin@test.com') - cls.user.objectlimit_set.filter( + cls.user.userobjectlimit_set.filter( name__in=["mailboxes", "mailbox_aliases"]).update(max_value=2) def setUp(self): @@ -240,7 +240,7 @@ class ResellerTestCase(ResourceTestCase): ) self._check_limit('domain_admins', 2, 2) - self.user.objectlimit_set.filter(name="mailboxes").update(max_value=3) + self.user.userobjectlimit_set.filter(name="mailboxes").update(max_value=3) self._create_account('user1@domain.tld') user = User.objects.get(username='user1@domain.tld') values = { @@ -265,11 +265,11 @@ class ResellerTestCase(ResourceTestCase): domadmin = User.objects.get(username='admin1@domain.tld') for l in ['mailboxes', 'mailbox_aliases']: self.assertEqual( - domadmin.objectlimit_set.get(name=l).max_value, 0 + domadmin.userobjectlimit_set.get(name=l).max_value, 0 ) def test_domain_admins_limit_from_domain_tpl(self): - self.user.objectlimit_set.filter(name="domains").update(max_value=3) + self.user.userobjectlimit_set.filter(name="domains").update(max_value=3) self._create_domain('domain1.tld', withtpl=True) self._create_domain('domain2.tld', withtpl=True) self._check_limit('domain_admins', 2, 2) diff --git a/modoboa/limits/utils.py b/modoboa/limits/utils.py index 1a8e4343f..6cbf3d1b0 100644 --- a/modoboa/limits/utils.py +++ b/modoboa/limits/utils.py @@ -3,7 +3,12 @@ from . import constants -def get_limit_templates(): +def get_user_limit_templates(): """Return defined templates.""" # FIXME: change GetExtraLimitTemplates - return constants.DEFAULT_LIMITS.items() + return constants.DEFAULT_USER_LIMITS.items() + + +def get_domain_limit_templates(): + """Return defined templates.""" + return constants.DEFAULT_DOMAIN_LIMITS.items() diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index 9e9dbbcb2..1c78f4ce3 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -50,7 +50,7 @@ class Operations(object): ) def _check_limit(self, name, curvalue, maxvalue): - l = self.user.objectlimit_set.get(name=name) + l = self.user.userobjectlimit_set.get(name=name) self.assertEqual(l.current_value, curvalue) self.assertEqual(l.max_value, maxvalue) @@ -233,9 +233,9 @@ class LimitsTestCase(ModoTestCase, Operations): """Create test data.""" super(LimitsTestCase, cls).setUpTestData() - for name, tpl in limits_utils.get_limit_templates(): + for name, tpl in limits_utils.get_user_limit_templates(): parameters.save_admin( - "DEFLT_{0}_LIMIT".format(name.upper()), 2, app="limits" + "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, app="limits" ) cls.user = UserFactory.create( username='reseller', groups=('Resellers',)