mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
@@ -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
|
||||
===============
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
BASE_EVENTS = [
|
||||
"CanCreate",
|
||||
|
||||
"AccountCreated",
|
||||
"AccountAutoCreated",
|
||||
"AccountModified",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Modoboa core signals."""
|
||||
|
||||
import django.dispatch
|
||||
|
||||
can_create_object = django.dispatch.Signal(
|
||||
providing_args=["user", "object_type", "count"])
|
||||
@@ -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
|
||||
|
||||
@@ -29,3 +29,5 @@ class LimitsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
load_limits_settings()
|
||||
|
||||
from . import handlers
|
||||
|
||||
@@ -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"}})
|
||||
))
|
||||
+13
-151
@@ -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')
|
||||
|
||||
+38
-29
@@ -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()
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
+41
-132
@@ -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
|
||||
|
||||
+13
-12
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user