From 02102754994852c155d247064788fe2ff4c240ff Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 14 Apr 2016 12:03:05 +0200 Subject: [PATCH] WIP. see #860 --- doc/extending/events.rst | 14 -- modoboa/admin/forms/account.py | 10 +- modoboa/admin/forms/domain.py | 12 +- modoboa/admin/views/alias.py | 5 +- modoboa/admin/views/domain.py | 4 +- modoboa/core/apps.py | 2 - modoboa/core/signals.py | 6 + modoboa/lib/signals.py | 9 +- modoboa/limits/apps.py | 2 + modoboa/limits/constants.py | 30 +++ modoboa/limits/controls.py | 164 ++--------------- modoboa/limits/forms.py | 67 ++++--- modoboa/limits/general_callbacks.py | 17 +- modoboa/limits/handlers.py | 44 +++++ modoboa/limits/lib.py | 19 -- .../migrations/0003_auto_20160413_1046.py | 70 +++++++ .../migrations/0004_auto_20160413_1312.py | 32 ++++ modoboa/limits/models.py | 173 +++++------------- modoboa/limits/tests.py | 25 +-- modoboa/limits/utils.py | 9 + 20 files changed, 333 insertions(+), 381 deletions(-) create mode 100644 modoboa/core/signals.py create mode 100644 modoboa/limits/constants.py create mode 100644 modoboa/limits/handlers.py create mode 100644 modoboa/limits/migrations/0003_auto_20160413_1046.py create mode 100644 modoboa/limits/migrations/0004_auto_20160413_1312.py create mode 100644 modoboa/limits/utils.py diff --git a/doc/extending/events.rst b/doc/extending/events.rst index 5d4964a2a..6e7e48889 100644 --- a/doc/extending/events.rst +++ b/doc/extending/events.rst @@ -147,20 +147,6 @@ displayed. Possible values are: See :ref:`usermenudisplay` for a description of what callbacks that listen to this event must return. -CanCreate -========= - -Raised just before a user tries to create a new object. - -*Callback prototype*:: - - def callback(user): pass - -* ``user`` is a ``User`` instance - -Return ``True`` or ``False`` to indicate if this user can respectively -create or not create a new ``Domain`` object. - CheckDomainName =============== diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 96b63e9b5..098c6cc56 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy from passwords.fields import PasswordField 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 @@ -262,7 +263,8 @@ class AccountFormMail(forms.Form, DynamicForm): """Create a mailbox associated to :kw:`account`.""" if not user.can_access(self.domain): raise PermDeniedException - events.raiseEvent("CanCreate", user, "mailboxes") + core_signals.can_create_object.send( + self.__class__, user=user, object_type="mailboxes") self.mb = Mailbox( address=self.locpart, domain=self.domain, user=account, use_domain_quota=self.cleaned_data["quota_act"]) @@ -293,9 +295,9 @@ class AccountFormMail(forms.Form, DynamicForm): aliases.remove(ralias.alias.address) if not aliases: return - events.raiseEvent( - "CanCreate", user, "mailbox_aliases", len(aliases) - ) + core_signals.can_create_object.send( + self.__class__, user=user, 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/domain.py b/modoboa/admin/forms/domain.py index aa11113a6..636923d77 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -6,6 +6,7 @@ from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.core.models import User +from modoboa.core import signals as core_signals from modoboa.lib import events, parameters from modoboa.lib.exceptions import ModoboaException, Conflict from modoboa.lib.fields import DomainNameField @@ -146,8 +147,9 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): else: aliases.remove(dalias.name) if aliases: - events.raiseEvent( - "CanCreate", user, "domain_aliases", len(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) @@ -224,7 +226,8 @@ class DomainFormOptions(forms.Form): pass else: raise Conflict(_("User '%s' already exists" % username)) - events.raiseEvent("CanCreate", user, "mailboxes") + core_signals.can_create_object.send( + self.__class__, user=user, object_type="mailboxes") da = User(username=username, email=username, is_active=True) da.set_password("password") da.save() @@ -241,7 +244,8 @@ class DomainFormOptions(forms.Form): if domain.type == "domain" and \ self.cleaned_data["create_aliases"] == "yes": - events.raiseEvent("CanCreate", user, "mailbox_aliases") + core_signals.can_create_object.send( + self.__class__, user=user, object_type="mailbox_aliases") alias = Alias( address=u"postmaster@{}".format(domain.name), domain=domain, enabled=True diff --git a/modoboa/admin/views/alias.py b/modoboa/admin/views/alias.py index 6b7a32199..a40bd29cf 100644 --- a/modoboa/admin/views/alias.py +++ b/modoboa/admin/views/alias.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext as _, ungettext import reversion -from modoboa.lib import events +from modoboa.core import signals as core_signals from modoboa.lib.exceptions import PermDeniedException, Conflict from modoboa.lib.web_utils import render_to_json_response @@ -36,7 +36,8 @@ def _validate_alias(request, form, successmsg, callback=None): def _new_alias(request, title, action, successmsg, tplname="admin/aliasform.html"): - events.raiseEvent("CanCreate", request.user, "mailbox_aliases") + core_signals.can_create_object.send( + "new_alias", user=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 433896e1f..0d222542f 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -14,6 +14,7 @@ from django.contrib.auth.decorators import ( import reversion from modoboa.core.models import User +from modoboa.core import signals as core_signals from modoboa.lib import parameters, events from modoboa.lib.web_utils import ( _render_to_string, render_to_json_response @@ -98,7 +99,8 @@ def domains_list(request): @permission_required("admin.add_domain") @reversion.create_revision() def newdomain(request): - events.raiseEvent("CanCreate", request.user, "domains") + core_signals.can_create_object.send( + "newdomain", user=request.user, object_type="domains") return DomainWizard(request).process() diff --git a/modoboa/core/apps.py b/modoboa/core/apps.py index 67ba209d7..61435efbb 100644 --- a/modoboa/core/apps.py +++ b/modoboa/core/apps.py @@ -4,8 +4,6 @@ from django.apps import AppConfig BASE_EVENTS = [ - "CanCreate", - "AccountCreated", "AccountAutoCreated", "AccountModified", diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py new file mode 100644 index 000000000..1171e3747 --- /dev/null +++ b/modoboa/core/signals.py @@ -0,0 +1,6 @@ +"""Modoboa core signals.""" + +import django.dispatch + +can_create_object = django.dispatch.Signal( + providing_args=["user", "object_type", "count"]) diff --git a/modoboa/lib/signals.py b/modoboa/lib/signals.py index 551898a40..3bc8aa545 100644 --- a/modoboa/lib/signals.py +++ b/modoboa/lib/signals.py @@ -1,10 +1,13 @@ """Custom signals.""" -from django.dispatch import Signal +import django.dispatch -request_accessor = Signal() +request_accessor = django.dispatch.Signal() def get_request(): """Get the current request from anywhere.""" - return request_accessor.send(None)[0][1] + request = request_accessor.send(None) + if request: + return request[0][1] + return None diff --git a/modoboa/limits/apps.py b/modoboa/limits/apps.py index f6401a959..37d31581f 100644 --- a/modoboa/limits/apps.py +++ b/modoboa/limits/apps.py @@ -29,3 +29,5 @@ class LimitsConfig(AppConfig): def ready(self): load_limits_settings() + + from . import handlers diff --git a/modoboa/limits/constants.py b/modoboa/limits/constants.py new file mode 100644 index 000000000..7d076a2c8 --- /dev/null +++ b/modoboa/limits/constants.py @@ -0,0 +1,30 @@ +"""Modoboa limits constants.""" + +import collections + +from django.utils.translation import ugettext_lazy as _ + + +DEFAULT_LIMITS = collections.OrderedDict(( + ("domains", { + "content_type": "admin.domain", "label": _("Domains"), + "help": _("Maximum number of domains this user can create"), + "required_role": "Reselles"}), + ("domain_aliases", { + "content_type": "admin.domainalias", "label": _("Domain aliases"), + "help": _("Maximum number of domain aliases this user can create"), + "required_role": "Resellers"}), + ("mailboxes", { + "content_type": "admin.mailbox", "label": _("Mailboxes"), + "help": _("Maximum number of mailboxes this user can create")}), + ("mailbox_aliases", { + "content_type": "admin.alias", "label": _("Mailbox aliases"), + "help": _("Maximum number of mailbox aliases this user " + "can create")}), + ("domain_admins", { + "content_type": "core.user", "label": _("Domain admins"), + "help": _("Maximum number of domain administrators this user " + "can create"), + "required_role": "Resellers", + "extra_filters": {"groups__name": "DomainAdmins"}}) +)) diff --git a/modoboa/limits/controls.py b/modoboa/limits/controls.py index 4239c0bd6..4d0c20c35 100644 --- a/modoboa/limits/controls.py +++ b/modoboa/limits/controls.py @@ -8,27 +8,7 @@ from modoboa.lib import events from modoboa.lib.permissions import get_object_owner -import modoboa.admin.models as admin_models - -from .lib import LimitReached, inc_limit_usage, dec_limit_usage -from .models import LimitTemplates, LimitsPool - - -def check_limit(user, lname, count=1): - """Check if a user has reached a defined limit - - We check if the user will not reach the given limit after this - creation. - - :param user: a ``User`` object - :param int count: the number of objects the user tries to create - :raises: ``LimitReached`` - """ - try: - if user.limitspool.will_be_reached(lname, count): - raise LimitReached(user.limitspool.get_limit(lname)) - except LimitsPool.DoesNotExist: - pass +from . import utils def move_pool_resource(owner, user): @@ -37,99 +17,15 @@ def move_pool_resource(owner, user): When an account doesn't need a pool anymore, we move the associated resource to the pool of its owner. """ - if not hasattr(user, "limitspool"): - return if not owner.is_superuser: - for ltpl in LimitTemplates().templates: - l = user.limitspool.get_limit(ltpl[0]) - if l.maxvalue < 0: + for name, ltpl in utils.get_limit_templates(): + l = user.objectlimit_set.get(name=name) + if l.max_value < 0: continue - ol = owner.limitspool.get_limit(ltpl[0]) - ol.curvalue += l.curvalue - ol.maxvalue += l.maxvalue + ol = owner.objectlimit_set.get(name=name) + ol.max_value += l.max_value ol.save() - user.limitspool.delete() - - -@events.observe('DomainCreated') -def inc_nb_domains(user, domain): - inc_limit_usage(user, 'domains_limit') - - -@events.observe('DomainDeleted') -def dec_nb_domains(domain, owner=None): - if owner is None: - owner = get_object_owner(domain) - dec_limit_usage(owner, 'domains_limit') - for domalias in domain.domainalias_set.all(): - dec_nb_domaliases(domalias) - for mailbox in domain.mailbox_set.all(): - dec_nb_mailboxes(mailbox) - for mbalias in domain.alias_set.all(): - dec_nb_mbaliases(mbalias) - - -@events.observe('DomainAliasCreated') -def inc_nb_domaliases(user, domalias): - inc_limit_usage(user, 'domain_aliases_limit') - - -@events.observe('DomainAliasDeleted') -def dec_nb_domaliases(domainaliases): - if isinstance(domainaliases, admin_models.DomainAlias): - domainaliases = [domainaliases] - for domainalias in domainaliases: - owner = get_object_owner(domainalias) - dec_limit_usage(owner, 'domain_aliases_limit') - - -@events.observe('MailboxCreated') -def inc_nb_mailboxes(user, mailbox): - inc_limit_usage(user, 'mailboxes_limit') - - -@events.observe('MailboxDeleted') -def dec_nb_mailboxes(mailboxes): - if isinstance(mailboxes, admin_models.Mailbox): - mailboxes = [mailboxes] - for mailbox in mailboxes: - owner = get_object_owner(mailbox) - dec_limit_usage(owner, 'mailboxes_limit') - - -@events.observe('MailboxAliasCreated') -def inc_nb_mbaliases(user, mailboxalias): - inc_limit_usage(user, 'mailbox_aliases_limit') - - -@events.observe('MailboxAliasDeleted') -def dec_nb_mbaliases(mailboxaliases): - if isinstance(mailboxaliases, admin_models.Alias): - mailboxaliases = [mailboxaliases] - for alias in mailboxaliases: - owner = get_object_owner(alias) - dec_limit_usage(owner, 'mailbox_aliases_limit') - - -@events.observe('CanCreate') -def can_create_new_object(user, objtype, count=1): - check_limit(user, '%s_limit' % objtype, count) - - -@events.observe("AccountCreated") -def create_pool(user): - owner = get_object_owner(user) - if owner.group not in ['SuperAdmins', 'Resellers']: - return - if user.group == 'DomainAdmins': - check_limit(owner, 'domain_admins_limit') - inc_limit_usage(owner, 'domain_admins_limit') - - if user.group in ['DomainAdmins', 'Resellers']: - p, created = LimitsPool.objects.get_or_create(user=user) - p.create_limits(owner) - @events.observe("UserCanSetRole") def user_can_set_role(user, role, account=None): @@ -143,11 +39,14 @@ def user_can_set_role(user, role, account=None): :param ``User`` account: account modified (None on creation) :param str newrole: role to check """ - if role == 'DomainAdmins': - lname = 'domain_admins_limit' - else: + if role != "DomainAdmins": return [True] - if user.is_superuser or not user.limitspool.will_be_reached(lname): + lname = "domain_admins" + condition = ( + user.is_superuser or + not user.objectlimit_set.get(name=lname).is_exceeded() + ) + if condition: return [True] if account is not None and account.group == role: return [True] @@ -162,26 +61,9 @@ def on_account_modified(old, new): # Domain admins can't change the role so nothing to check. return - if new.group != "SuperAdmins": - # Check if account needs a pool (case: a superadmin is - # downgraded) - if not hasattr(new, "limitspool"): - p = LimitsPool(user=new) - p.save() - p.create_limits(owner) - if new.group not in ["DomainAdmins", "Resellers"]: move_pool_resource(owner, new) - if old.oldgroup == "DomainAdmins": - if new.group != "DomainAdmins": - dec_limit_usage(owner, 'domain_admins_limit') - return - - if new.group == "DomainAdmins": - check_limit(owner, 'domain_admins_limit') - inc_limit_usage(owner, 'domain_admins_limit') - @events.observe("AccountDeleted") def on_account_deleted(account, byuser, **kwargs): @@ -190,23 +72,3 @@ def on_account_deleted(account, byuser, **kwargs): return move_pool_resource(owner, account) - - if account.group == "DomainAdmins": - dec_limit_usage(owner, 'domain_admins_limit') - - -@events.observe('DomainOwnershipRemoved') -def domain_ownership_removed(reseller, domain): - """DomainOwnershipRemoved listener. - - The access :keyword:`reseller` had to :keyword:`domain` has been - removed by a super adminstrator. We must decrement all limit - usages. - - :param ``User`` reseller: reseller that created :keyword:`domain` - :param ``Domain`` domain: domain - """ - dec_nb_domains(domain, reseller) - for dadmin in domain.admins: - if reseller.is_owner(dadmin): - dec_limit_usage(reseller, 'domain_admins_limit') diff --git a/modoboa/limits/forms.py b/modoboa/limits/forms.py index 7baa91eb7..22cc07390 100644 --- a/modoboa/limits/forms.py +++ b/modoboa/limits/forms.py @@ -9,10 +9,11 @@ from modoboa.lib import parameters from modoboa.lib.permissions import get_object_owner from .lib import BadLimitValue, UnsufficientResource -from .models import LimitTemplates +from . import utils class ResourcePoolForm(forms.Form): + """Dynamic form to display user limits.""" def __init__(self, *args, **kwargs): self.account = None @@ -20,20 +21,25 @@ class ResourcePoolForm(forms.Form): self.account = kwargs["instance"] del kwargs["instance"] super(ResourcePoolForm, self).__init__(*args, **kwargs) - for tpl in LimitTemplates().templates: - if len(tpl) > 3: - if self.account is not None and self.account.group != tpl[3]: + for name, tpl in utils.get_limit_templates(): + if "required_role" in tpl: + condition = ( + self.account is not None and + self.account.group != tpl["required_role"] + ) + if condition: continue - self.fields[tpl[0]] = forms.IntegerField( - label=tpl[1], help_text=tpl[2] + self.fields["{}_limit".format(name)] = forms.IntegerField( + label=tpl["label"], help_text=tpl["help"] ) if hasattr(self, "account"): self.load_from_user(self.account) def check_limit_value(self, lname): - if self.cleaned_data[lname] < -1: + fieldname = "{}_limit".format(lname) + if self.cleaned_data[fieldname] < -1: raise forms.ValidationError(_("Invalid limit")) - return self.cleaned_data[lname] + return self.cleaned_data[fieldname] def clean(self): cleaned_data = super(ResourcePoolForm, self).clean() @@ -43,49 +49,52 @@ class ResourcePoolForm(forms.Form): return cleaned_data def load_from_user(self, user): - for lname in self.fields.keys(): - self.fields[lname].initial = user.limitspool.getmaxvalue(lname) + for fieldname in self.fields.keys(): + lname = fieldname.replace("_limit", "") + self.fields[fieldname].initial = ( + user.objectlimit_set.get(name=lname).max_value) # The following lines will become useless in a near # future. - if self.fields[lname].initial == -2: - self.fields[lname].initial = parameters.get_admin( - "DEFLT_%s" % - lname.upper()) + if self.fields[fieldname].initial == -2: + self.fields[fieldname].initial = parameters.get_admin( + "DEFLT_{}_LIMIT".format(lname.upper())) - def allocate_from_pool(self, limit, pool): - """Allocate resource using an existing pool. + def allocate_from_user(self, limit, user): + """Allocate resource using an existing user. When a reseller creates a domain administrator, he generally assigns him resource to create new objetcs. As a reseller may also be limited, the resource he gives is taken from its own pool. """ - ol = pool.get_limit(limit.name) - if ol.maxvalue == -2: + ol = user.objectlimit_set.get(name=limit.name) + if ol.max_value == -2: raise BadLimitValue(_("Your resources are not initialized yet")) - newvalue = self.cleaned_data[limit.name] - if newvalue == -1 and ol.maxvalue != -1: + fieldname = "{}_limit".format(limit.name) + newvalue = self.cleaned_data[fieldname] + if newvalue == -1 and ol.max_value != -1: raise BadLimitValue( _("You're not allowed to define unlimited values") ) - if limit.maxvalue > -1: - newvalue -= limit.maxvalue + if limit.max_value > -1: + newvalue -= limit.max_value if newvalue == 0: return - remain = ol.maxvalue - ol.curvalue + remain = ol.max_value - ol.current_value if newvalue > remain: raise UnsufficientResource(ol) - ol.maxvalue -= newvalue + ol.max_value -= newvalue ol.save() def save(self): owner = get_object_owner(self.account) - for ltpl in LimitTemplates().templates: - if ltpl[0] not in self.cleaned_data: + for name, ltpl in utils.get_limit_templates(): + fieldname = "{}_limit".format(name) + if fieldname not in self.cleaned_data: continue - l = self.account.limitspool.limit_set.get(name=ltpl[0]) + l = self.account.objectlimit_set.get(name=name) if not owner.is_superuser: - self.allocate_from_pool(l, owner.limitspool) - l.maxvalue = self.cleaned_data[ltpl[0]] + self.allocate_from_user(l, owner) + l.max_value = self.cleaned_data[fieldname] l.save() diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py index e1cac86e1..655497c82 100644 --- a/modoboa/limits/general_callbacks.py +++ b/modoboa/limits/general_callbacks.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _ from modoboa.lib import events from .forms import ResourcePoolForm -from .models import LimitTemplates +from . import utils @events.observe("ExtraAdminContent") @@ -15,18 +15,19 @@ def display_pool_usage(user, target, currentpage): if target != "leftcol" or user.is_superuser: return [] if currentpage == "identities": - names = ["mailboxes_limit", "mailbox_aliases_limit"] + names = ["mailboxes", "mailbox_aliases"] if user.has_perm("admin.add_domain"): - names += ["domain_admins_limit"] + names += ["domain_admins"] else: + exceptions = ["domain_admins", "mailboxes", "mailbox_aliases"] names = [ - tpl[0] for tpl in LimitTemplates().templates - if tpl[0] not in ["domain_admins_limit", "mailboxes_limit", - "mailbox_aliases_limit"] and - (len(tpl) == 3 or tpl[3] == user.group) + name for name, tpl in utils.get_limit_templates() + if name not in exceptions and + ("required_role" not in tpl or + tpl["required_role"] == user.group) ] - limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0) + limits = user.objectlimit_set.filter(name__in=names, max_value__gt=0) if len(limits) == 0: return [] return [ diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py new file mode 100644 index 000000000..6eeddc6d4 --- /dev/null +++ b/modoboa/limits/handlers.py @@ -0,0 +1,44 @@ +"""Django signal handlers for limits.""" + +from django.db.models import signals +from django.dispatch import receiver + +from django.contrib.contenttypes.models import ContentType + +from modoboa.core import models as core_models +from modoboa.core import signals as core_signals +from modoboa.lib import signals as lib_signals +from modoboa.lib import parameters + +from . import lib +from . import models +from . import utils + + +@receiver(core_signals.can_create_object) +def check_object_limit(sender, user, 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) + count = kwargs.get("count", 1) + if limit.is_exceeded(count): + raise lib.LimitReached(limit) + + +@receiver(signals.post_save, sender=core_models.User) +def create_user_limits(sender, instance, **kwargs): + """Create limits for new user.""" + if not kwargs.get("created"): + return + request = lib_signals.get_request() + creator = request.user if request else None + for name, definition in utils.get_limit_templates(): + ct = ContentType.objects.get_by_natural_key( + *definition["content_type"].split(".")) + max_value = 0 + if not creator or creator.is_superuser: + max_value = int( + parameters.get_admin("DEFLT_{}_LIMIT".format(name.upper()))) + models.ObjectLimit.objects.create( + user=instance, name=name, content_type=ct, max_value=max_value) diff --git a/modoboa/limits/lib.py b/modoboa/limits/lib.py index 409bf758e..6daaf3f90 100644 --- a/modoboa/limits/lib.py +++ b/modoboa/limits/lib.py @@ -8,8 +8,6 @@ from django.utils.translation import ugettext as _ from modoboa.lib.exceptions import ModoboaException -from .models import LimitsPool - class LimitReached(ModoboaException): http_code = 403 @@ -33,20 +31,3 @@ class UnsufficientResource(ModoboaException): class BadLimitValue(ModoboaException): http_code = 400 - - -def inc_limit_usage(user, lname): - """Increase a given limit usage.""" - try: - user.limitspool.inc_curvalue(lname) - except LimitsPool.DoesNotExist: - pass - - -def dec_limit_usage(user, lname): - if user is None: - return - try: - user.limitspool.dec_curvalue(lname) - except LimitsPool.DoesNotExist: - pass diff --git a/modoboa/limits/migrations/0003_auto_20160413_1046.py b/modoboa/limits/migrations/0003_auto_20160413_1046.py new file mode 100644 index 000000000..5a5fa13d7 --- /dev/null +++ b/modoboa/limits/migrations/0003_auto_20160413_1046.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +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") + ContentType = apps.get_model("contenttypes", "ContentType") + + if not ContentType.objects.exists(): + # No ContentType means this is a fresh install + return + + name_to_content_type = { + "domains_limit": ( + ContentType.objects.get(app_label="admin", model="domain")), + "domain_aliases_limit": ( + ContentType.objects.get(app_label="admin", model="domainalias")), + "mailboxes_limit": ( + ContentType.objects.get(app_label="admin", model="mailbox")), + "mailbox_aliases_limit": ( + ContentType.objects.get(app_label="admin", model="alias")), + "domain_admins_limit": ( + ContentType.objects.get(app_label="core", model="user")), + } + + to_create = [] + qset = ( + LimitsPool.objects.select_related("user") + .prefetch_related("limit_set")) + for pool in qset: + for limit in pool.limit_set.all(): + objlimit = ObjectLimit( + 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) + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('limits', '0002_auto_20151114_1518'), + ('admin', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ObjectLimit', + 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=-2)), + ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterUniqueTogether( + name='objectlimit', + unique_together=set([('user', 'name')]), + ), + migrations.RunPython(move_limits_to_user) + ] diff --git a/modoboa/limits/migrations/0004_auto_20160413_1312.py b/modoboa/limits/migrations/0004_auto_20160413_1312.py new file mode 100644 index 000000000..fe085dd35 --- /dev/null +++ b/modoboa/limits/migrations/0004_auto_20160413_1312.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('limits', '0003_auto_20160413_1046'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='limit', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='limit', + name='pool', + ), + migrations.RemoveField( + model_name='limitspool', + name='user', + ), + migrations.DeleteModel( + name='Limit', + ), + migrations.DeleteModel( + name='LimitsPool', + ), + ] diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py index 7a139ed58..7f3cf4412 100644 --- a/modoboa/limits/models.py +++ b/modoboa/limits/models.py @@ -4,150 +4,59 @@ from django.conf import settings from django.db import models -from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.lib import parameters, events -from modoboa.lib.singleton import Singleton +from modoboa.core import models as core_models + +from . import utils -class LimitTemplates(Singleton): +class ObjectLimit(models.Model): + """Per-user limits on object creation.""" - def __init__(self): - self.__templates = [ - ("domain_admins_limit", ugettext_lazy("Domain admins"), - ugettext_lazy("Maximum number of domain administrators this user " - "can create"), - 'Resellers'), - ("domains_limit", ugettext_lazy("Domains"), - ugettext_lazy("Maximum number of domains this user can create"), - 'Resellers'), - ("domain_aliases_limit", ugettext_lazy("Domain aliases"), - ugettext_lazy( - "Maximum number of domain aliases this user can create"), - 'Resellers'), - ("mailboxes_limit", ugettext_lazy("Mailboxes"), - ugettext_lazy("Maximum number of mailboxes this user can create")), - ("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases"), - ugettext_lazy("Maximum number of mailbox aliases this user " - "can create")) - ] - - @property - def templates(self): - return self.__templates + \ - events.raiseQueryEvent('GetExtraLimitTemplates') - - -class LimitsPool(models.Model): - - user = models.OneToOneField(settings.AUTH_USER_MODEL) - - def create_limits(self, creator): - """Create limits for this pool. - - All limits defined into ``LimitTemplates`` will be created. If - the creator is a super administrator, the default maximum values - will be used. - - :param ``User`` creator: user creating this pool - """ - for ltpl in LimitTemplates().templates: - try: - Limit.objects.get(name=ltpl[0], pool=self) - except Limit.DoesNotExist: - maxvalue = ( - int(parameters.get_admin("DEFLT_%s" % ltpl[0].upper())) - if creator.is_superuser else 0 - ) - Limit.objects.create( - name=ltpl[0], pool=self, maxvalue=maxvalue - ) - - def getcurvalue(self, lname): - l = self.limit_set.get(name=lname) - return l.curvalue - - def getmaxvalue(self, lname): - l = self.limit_set.get(name=lname) - return l.maxvalue - - def set_maxvalue(self, lname, value): - l = self.limit_set.get(name=lname) - l.maxvalue = value - l.save() - - def inc_curvalue(self, lname, nb=1): - l = self.limit_set.get(name=lname) - l.curvalue += nb - l.save() - - def dec_curvalue(self, lname, nb=1): - l = self.limit_set.get(name=lname) - if not l.curvalue: - return - l.curvalue -= nb - l.save() - - def dec_limit(self, lname, nb=1): - l = self.limit_set.get(name=lname) - l.curvalue -= nb - if l.maxvalue > -1: - l.maxvalue -= nb - l.save() - - def inc_limit(self, lname, nb=1): - l = self.limit_set.get(name=lname) - l.curvalue += nb - if l.maxvalue > -1: - l.maxvalue += nb - l.save() - - def will_be_reached(self, lname, nb=1): - l = self.limit_set.get(name=lname) - if l.maxvalue <= -1: - return False - if l.curvalue + nb > l.maxvalue: - return True - return False - - def get_limit(self, lname): - try: - return self.limit_set.get(name=lname) - except Limit.DoesNotExist: - return None - - -class Limit(models.Model): - - name = models.CharField(max_length=255) - curvalue = models.IntegerField(default=0) - maxvalue = models.IntegerField(default=-2) - pool = models.ForeignKey(LimitsPool) + 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) class Meta: - unique_together = (("name", "pool"),) + unique_together = (("user", "name"), ) + + @property + def definition(self): + """Return the definition of this limit.""" + for name, tpl in utils.get_limit_templates(): + if name == self.name: + return tpl + return None + + @property + def current_value(self): + """Return the current number of objects.""" + if "extra_filters" not in self.definition: + return core_models.ObjectAccess.objects.filter( + user=self.user, is_owner=True, + content_type=self.content_type).count() + id_list = core_models.ObjectAccess.objects.filter( + user=self.user, is_owner=True, + content_type=self.content_type).values_list("object_id", flat=True) + model_class = self.content_type.model_class() + return model_class.objects.filter( + pk__in=id_list, **self.definition["extra_filters"]).count() @property def usage(self): - if self.maxvalue < 0: + """Return current limit usage in %.""" + if self.max_value < 0: return -1 - if self.maxvalue == 0: + if self.max_value == 0: return 0 - return int(float(self.curvalue) / self.maxvalue * 100) + return int(float(self.current_value) / self.max_value * 100) @property def label(self): - for l in LimitTemplates().templates: - if l[0] == self.name: - return l[1] - return "" + """Return display name.""" + return self.definition["label"] - def __str__(self): - if self.maxvalue == -2: - return _("undefined") - if self.maxvalue == -1: - return _("unlimited") - if self.maxvalue == 0: - return "100%" - - return "%d%%" % self.usage + 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 diff --git a/modoboa/limits/tests.py b/modoboa/limits/tests.py index dd7626613..c235ef1ef 100644 --- a/modoboa/limits/tests.py +++ b/modoboa/limits/tests.py @@ -9,7 +9,7 @@ from modoboa.core.models import User from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase -from .models import LimitTemplates +from . import utils class PermissionsTestCase(ModoTestCase): @@ -47,9 +47,9 @@ class ResourceTestCase(ModoTestCase): def setUpTestData(cls): """Custom setUpTestData method.""" super(ResourceTestCase, cls).setUpTestData() - for tpl in LimitTemplates().templates: + for name, tpl in utils.get_limit_templates(): parameters.save_admin( - "DEFLT_{0}".format(tpl[0].upper()), 2, + "DEFLT_{0}_LIMIT".format(name.upper()), 2, app="limits" ) populate_database() @@ -107,9 +107,9 @@ class ResourceTestCase(ModoTestCase): ) def _check_limit(self, name, curvalue, maxvalue): - l = self.user.limitspool.get_limit('%s_limit' % name) - self.assertEqual(l.curvalue, curvalue) - self.assertEqual(l.maxvalue, maxvalue) + l = self.user.objectlimit_set.get(name=name) + self.assertEqual(l.current_value, curvalue) + self.assertEqual(l.max_value, maxvalue) class DomainAdminTestCase(ResourceTestCase): @@ -119,8 +119,8 @@ class DomainAdminTestCase(ResourceTestCase): """Create test data.""" super(DomainAdminTestCase, cls).setUpTestData() cls.user = User.objects.get(username='admin@test.com') - cls.user.limitspool.set_maxvalue('mailboxes_limit', 2) - cls.user.limitspool.set_maxvalue('mailbox_aliases_limit', 2) + cls.user.objectlimit_set.filter( + name__in=["mailboxes", "mailbox_aliases"]).update(max_value=2) def setUp(self): """Test initialization.""" @@ -177,9 +177,10 @@ class ResellerTestCase(ResourceTestCase): def setUpTestData(cls): """Create test data.""" super(ResellerTestCase, cls).setUpTestData() - cls.user = UserFactory.create( + cls.user = UserFactory( username='reseller', groups=('Resellers',) ) + cls.user.objectlimit_set.update(max_value=2) def setUp(self): """Test initialization.""" @@ -232,7 +233,7 @@ class ResellerTestCase(ResourceTestCase): ) self._check_limit('domain_admins', 2, 2) - self.user.limitspool.set_maxvalue('mailboxes_limit', 3) + self.user.objectlimit_set.filter(name="mailboxes").update(max_value=3) self._create_account('user1@domain.tld') user = User.objects.get(username='user1@domain.tld') values = { @@ -257,11 +258,11 @@ class ResellerTestCase(ResourceTestCase): domadmin = User.objects.get(username='admin1@domain.tld') for l in ['mailboxes', 'mailbox_aliases']: self.assertEqual( - domadmin.limitspool.get_limit('%s_limit' % l).maxvalue, 0 + domadmin.objectlimit_set.get(name=l).max_value, 0 ) def test_domain_admins_limit_from_domain_tpl(self): - self.user.limitspool.set_maxvalue('domains_limit', 3) + self.user.objectlimit_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 new file mode 100644 index 000000000..1a8e4343f --- /dev/null +++ b/modoboa/limits/utils.py @@ -0,0 +1,9 @@ +"""Modoboa limits utilities.""" + +from . import constants + + +def get_limit_templates(): + """Return defined templates.""" + # FIXME: change GetExtraLimitTemplates + return constants.DEFAULT_LIMITS.items()