Added support for reseller quota.

see #1035
This commit is contained in:
Antoine Nguyen
2017-02-16 11:28:40 +01:00
parent cc2bca5b1f
commit 8ef3dc8e06
20 changed files with 188 additions and 67 deletions
+2 -2
View File
@@ -306,7 +306,7 @@ class AccountFormMail(forms.Form, DynamicForm):
if not user.can_access(self.domain):
raise lib_exceptions.PermDeniedException
core_signals.can_create_object.send(
self.__class__, context=user, object_type="mailboxes")
self.__class__, context=user, klass=models.Mailbox)
self.mb = models.Mailbox(
address=self.locpart, domain=self.domain, user=account,
use_domain_quota=self.cleaned_data["quota_act"])
@@ -330,7 +330,7 @@ class AccountFormMail(forms.Form, DynamicForm):
if not self.aliases:
return
core_signals.can_create_object.send(
self.__class__, context=user, object_type="mailbox_aliases",
self.__class__, context=user, klass=models.Alias,
count=len(self.aliases))
core_signals.can_create_object.send(
self.__class__, context=self.mb.domain,
+5 -3
View File
@@ -128,6 +128,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
"""
d = super(DomainFormGeneral, self).save(commit=False)
core_signals.can_create_object.send(
sender=self.__class__, context=user, klass=Domain, instance=d)
if not commit:
return d
d.save()
@@ -138,7 +140,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
self.aliases.remove(dalias.name)
if self.aliases:
core_signals.can_create_object.send(
self.__class__, context=user, object_type="domain_aliases",
self.__class__, context=user, klass=DomainAlias,
count=len(self.aliases))
core_signals.can_create_object.send(
self.__class__, context=d, object_type="domain_aliases",
@@ -224,7 +226,7 @@ class DomainFormOptions(forms.Form):
else:
raise Conflict(_("User '%s' already exists") % username)
core_signals.can_create_object.send(
self.__class__, context=user, object_type="mailboxes")
self.__class__, context=user, klass=Mailbox)
da = User(username=username, email=username, is_active=True)
da.set_password(
param_tools.get_global_parameter("default_password", app="core"))
@@ -248,7 +250,7 @@ class DomainFormOptions(forms.Form):
)
if condition:
core_signals.can_create_object.send(
self.__class__, context=user, object_type="mailbox_aliases")
self.__class__, context=user, klass=Alias)
address = u"postmaster@{}".format(domain.name)
alias = Alias.objects.create(
address=address, domain=domain, enabled=True)
+1 -1
View File
@@ -205,7 +205,7 @@ def import_account_mailbox(sender, user, account, row, **kwargs):
if not user.can_access(domain):
raise exceptions.PermDeniedException
core_signals.can_create_object.send(
sender="import", context=user, object_type="mailboxes")
sender="import", context=user, klass=models.Mailbox)
core_signals.can_create_object.send(
sender="import", context=domain, object_type="mailboxes")
account.save()
+2 -2
View File
@@ -121,7 +121,7 @@ def import_domain(user, row, formopts):
if not user.has_perm("admin.add_domain"):
raise PermDeniedException(_("You are not allowed to import domains"))
core_signals.can_create_object.send(
sender="import", context=user, object_type="domains")
sender="import", context=user, klass=Domain)
dom = Domain()
dom.from_csv(user, row)
@@ -132,7 +132,7 @@ def import_domainalias(user, row, formopts):
raise PermDeniedException(
_("You are not allowed to import domain aliases."))
core_signals.can_create_object.send(
sender="import", context=user, object_type="domain_aliases")
sender="import", context=user, klass=DomainAlias)
domalias = DomainAlias()
domalias.from_csv(user, row)
+1 -1
View File
@@ -180,7 +180,7 @@ class Alias(AdminObject):
if not user.can_access(domain):
raise PermDeniedException
core_signals.can_create_object.send(
sender="import", context=user, object_type="mailbox_aliases")
sender="import", context=user, klass=Alias)
core_signals.can_create_object.send(
sender="import", context=domain, object_type="mailbox_aliases")
if Alias.objects.filter(address=address).exists():
+12
View File
@@ -23,6 +23,18 @@ class AdminObject(models.Model):
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
"""Custom constructor."""
super(AdminObject, self).__init__(*args, **kwargs)
self._loaded_values = {}
@classmethod
def from_db(cls, db, field_names, values):
"""Store loaded values."""
instance = super(AdminObject, cls).from_db(db, field_names, values)
instance._loaded_values = dict(zip(field_names, values))
return instance
@property
def objectname(self):
if self._objectname is None:
+3
View File
@@ -266,6 +266,9 @@ class Domain(AdminObject):
.format(self.name)
)
self.enabled = (row[4].strip() in ["True", "1", "yes", "y"])
core_signals.can_create_object.send(
sender=self.__class__, context=user, klass=Domain,
instance=self)
self.save(creator=user)
def to_csv(self, csvwriter):
+8 -4
View File
@@ -30,7 +30,11 @@ class DomainSerializer(serializers.ModelSerializer):
def create(self, validated_data):
"""Set permissions."""
domain = models.Domain(**validated_data)
domain.save(creator=self.context["request"].user)
creator = self.context["request"].user
core_signals.can_create_object.send(
sender=self.__class__, context=creator,
klass=models.Domain, instance=domain)
domain.save(creator=creator)
return domain
@@ -54,7 +58,7 @@ class DomainAliasSerializer(serializers.ModelSerializer):
try:
core_signals.can_create_object.send(
sender=self.__class__, context=creator,
object_type="domain_aliases")
klass=models.DomainAlias)
core_signals.can_create_object.send(
sender=self.__class__, context=domain_alias.target,
object_type="domain_aliases")
@@ -231,7 +235,7 @@ class WritableAccountSerializer(AccountSerializer):
try:
core_signals.can_create_object.send(
sender=self.__class__, context=creator,
object_type="mailboxes")
klass=admin_models.Mailbox)
core_signals.can_create_object.send(
sender=self.__class__, context=domain,
object_type="mailboxes")
@@ -324,7 +328,7 @@ class AliasSerializer(serializers.ModelSerializer):
try:
core_signals.can_create_object.send(
sender=self.__class__, context=creator,
object_type="mailbox_aliases")
klass=admin_models.Alias)
core_signals.can_create_object.send(
sender=self.__class__, context=self.domain,
object_type="mailbox_aliases")
@@ -1,7 +1,11 @@
{% extends "admin/header.html" %}
{% load i18n %}
{% load admin_tags %}
{% load admin_tags i18n lib_tags %}
{% block extra_css %}
{{ block.super }}
{% extra_static_content "domains" "css" user %}
{% endblock %}
{% block leftcol %}
<ul class="nav nav-sidebar">
+1 -1
View File
@@ -41,7 +41,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", context=request.user, object_type="mailbox_aliases")
"new_alias", context=request.user, klass=Alias)
if request.method == "POST":
def callback(user, alias):
alias.post_create(user)
+1 -1
View File
@@ -111,7 +111,7 @@ def domains_list(request):
@reversion.create_revision()
def newdomain(request):
core_signals.can_create_object.send(
"newdomain", context=request.user, object_type="domains")
"newdomain", context=request.user, klass=Domain)
return DomainWizard(request).process()
+1 -1
View File
@@ -13,7 +13,7 @@ account_password_updated = django.dispatch.Signal(
providing_args=["account", "password", "created"])
allow_password_change = django.dispatch.Signal(providing_args=["user"])
can_create_object = django.dispatch.Signal(
providing_args=["context", "object_type", "count"])
providing_args=["context", "klass", "object_type", "count", "instance"])
extra_account_actions = django.dispatch.Signal(providing_args=["account"])
extra_admin_dashboard_widgets = django.dispatch.Signal(
providing_args=["user"])
+10
View File
@@ -72,6 +72,16 @@ class ParametersForm(param_forms.AdminParametersForm):
widget=forms.widgets.TextInput(
attrs={"class": "col-md-1 form-control"})
)
deflt_user_quota_limit = forms.IntegerField(
label=_("Quota"),
initial=0,
help_text=_(
"The quota a reseller will be allowed to share between the "
"domains he creates. (0 means no quota)"
),
widget=forms.widgets.TextInput(
attrs={"class": "col-md-1 form-control"})
)
domain_limits_sep = SeparatorField(label=_("Default per-domain limits"))
+8 -1
View File
@@ -26,7 +26,14 @@ DEFAULT_USER_LIMITS = collections.OrderedDict((
"help": _("Maximum number of domain administrators this user "
"can create"),
"required_role": "Resellers",
"extra_filters": {"groups__name": "DomainAdmins"}})
"extra_filters": {"groups__name": "DomainAdmins"}}),
("quota", {
"content_type": "admin.domain", "label": _("Quota"),
"help": _("Quota shared between domains of this reseller"),
"required_role": "Resellers",
"type": "sum",
"field": "quota"
})
))
+8 -6
View File
@@ -22,23 +22,25 @@ from . import utils
@receiver(core_signals.can_create_object)
def check_object_limit(sender, context, object_type, **kwargs):
def check_object_limit(sender, context, **kwargs):
"""Check if user can create a new object."""
if context.__class__.__name__ == "User":
if not param_tools.get_global_parameter("enable_admin_limits"):
return
if context.is_superuser:
return True
limit = context.userobjectlimit_set.get(name=object_type)
ct = ContentType.objects.get_for_model(kwargs.get("klass"))
limits = context.userobjectlimit_set.filter(content_type=ct)
elif context.__class__.__name__ == "Domain":
if not param_tools.get_global_parameter("enable_domain_limits"):
return
limit = context.domainobjectlimit_set.get(name=object_type)
object_type = kwargs.get("object_type")
limits = context.domainobjectlimit_set.filter(name=object_type)
else:
raise NotImplementedError
count = kwargs.get("count", 1)
if limit.is_exceeded(count):
raise lib.LimitReached(limit)
for limit in limits:
if limit.is_exceeded(kwargs.get("count", 1), kwargs.get("instance")):
raise lib.LimitReached(limit)
@receiver(signals.post_save, sender=core_models.User)
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-16 10:12
from __future__ import unicode_literals
from django.db import migrations
def create_quota_limits(apps, schema_editor):
"""Create quota limits."""
User = apps.get_model("core", "User")
if not User.objects.exists():
return
ContentType = apps.get_model("contenttypes", "ContentType")
UserObjectLimit = apps.get_model("limits", "UserObjectLimit")
to_create = []
ct = ContentType.objects.get(app_label="admin", model="domain")
for u in User.objects.all():
to_create.append(
UserObjectLimit(user=u, name="quota", max_value=0, content_type=ct)
)
UserObjectLimit.objects.bulk_create(to_create)
class Migration(migrations.Migration):
dependencies = [
('limits', '0005_auto_20160415_1654'),
]
operations = [
migrations.RunPython(create_quota_limits)
]
+32 -9
View File
@@ -21,6 +21,11 @@ class ObjectLimitMixin(object):
"""Return display name."""
return self.definition["label"]
@property
def type(self):
"""Return limit type."""
return self.definition.get("type", "count")
@property
def usage(self):
"""Return current limit usage in %."""
@@ -30,12 +35,21 @@ class ObjectLimitMixin(object):
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.max_value != -1 and
self.current_value + 1 > self.max_value
)
def is_exceeded(self, count=1, instance=None):
"""Check if limit will be reached if we add this object."""
if self.type == "count":
if self.max_value == -1:
return False
return self.current_value + count > self.max_value
if self.max_value == 0 or instance is None:
return False
field = self.definition["field"]
value = getattr(instance, field)
try:
old_value = instance._loaded_values[field]
except (AttributeError, KeyError):
old_value = 0
return self.current_value + (value - old_value) > self.max_value
def __str__(self):
"""Display current usage."""
@@ -67,7 +81,10 @@ class UserObjectLimit(ObjectLimitMixin, models.Model):
@property
def current_value(self):
"""Return the current number of objects."""
if "extra_filters" not in self.definition:
condition = (
self.type == "count" and
"extra_filters" not in self.definition)
if condition:
return core_models.ObjectAccess.objects.filter(
user=self.user, is_owner=True,
content_type=self.content_type).count()
@@ -75,8 +92,14 @@ class UserObjectLimit(ObjectLimitMixin, models.Model):
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()
if self.type == "count":
return model_class.objects.filter(
pk__in=id_list, **self.definition["extra_filters"]).count()
qset = model_class.objects.filter(pk__in=id_list)
if not qset.exists():
return 0
return qset.aggregate(
total=models.Sum(self.definition["field"]))["total"]
@property
def label(self):
+4 -1
View File
@@ -79,16 +79,19 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase):
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + self.r_token.key)
limit = self.reseller.userobjectlimit_set.get(name="domains")
quota = self.reseller.userobjectlimit_set.get(name="quota")
url = reverse("external_api:domain-list")
data = {"name": "test3.com", "quota": 10}
data = {"name": "test3.com", "quota": 1}
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, 201)
self.assertFalse(limit.is_exceeded())
self.assertFalse(quota.is_exceeded())
data["name"] = "test4.com"
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, 201)
self.assertTrue(limit.is_exceeded())
self.assertFalse(quota.is_exceeded())
data["username"] = "test5.com"
response = self.client.post(url, data, format="json")
+24 -24
View File
@@ -26,8 +26,7 @@ class LimitImportTestCase(ModoTestCase):
def _test_domain_alias_import(self, limit):
"""Check domain aliases limit."""
self.client.login(
username=self.reseller.username, password="toto")
self.client.force_login(self.reseller)
self.assertFalse(limit.is_exceeded())
f = ContentFile(b"""domainalias; domalias1.com; {domain}; True
domainalias; domalias2.com; {domain}; True
@@ -51,8 +50,7 @@ domainalias; domalias2.com; {domain}; True
def _test_domain_admins_import(self, limit, initial_count=0):
"""Check domain admins limit."""
self.client.login(
username=self.reseller.username, password="toto")
self.client.force_login(self.reseller)
self.assertFalse(limit.is_exceeded())
content = ""
for cpt in xrange(initial_count, 2):
@@ -80,12 +78,15 @@ domainalias; domalias2.com; {domain}; True
def _test_mailboxes_import(self, limit):
"""Check mailboxes limit."""
self.client.login(
username=self.dadmin.username, password="toto")
self.client.force_login(self.dadmin)
self.assertFalse(limit.is_exceeded())
f = ContentFile(b"""account; user1@{domain}; toto; User; One; True; SimpleUsers; user1@{domain}; 5
account; truc@{domain}; toto; René; Truc; True; SimpleUsers; truc@{domain}; 5
""".format(domain=self.domain), name="domains.csv")
f = ContentFile(
"account; user1@{domain}; toto; User; One; True; SimpleUsers; "
"user1@{domain}; 5\r\n"
"account; truc@{domain}; toto; René; Truc; True; SimpleUsers; "
"truc@{domain}; 5\r\n".format(domain=self.domain),
name="domains.csv"
)
response = self.client.post(
reverse("admin:identity_import"), {
"sourcefile": f
@@ -105,8 +106,7 @@ account; user3@{domain}; toto; User; One; True; SimpleUsers; user3@{domain}; 5
def _test_mailbox_aliases_import(self, limit):
"""Check mailbox aliases limit."""
self.client.login(
username=self.dadmin.username, password="toto")
self.client.force_login(self.dadmin)
self.assertFalse(limit.is_exceeded())
f = ContentFile(b"""alias; alias1@{domain}; True; user@{domain}
alias; alias2@{domain}; True; user@{domain}
@@ -143,26 +143,26 @@ class UserLimitImportTestCase(LimitImportTestCase):
def test_domains_import(self):
"""Check domains limit."""
self.client.login(
username=self.reseller.username, password="toto")
self.client.force_login(self.reseller)
limit = self.reseller.userobjectlimit_set.get(name="domains")
self.assertFalse(limit.is_exceeded())
f = ContentFile(b"""domain; domain1.com; 1000; 100; True
domain; domain2.com; 1000; 200; False
""", name="domains.csv")
f = ContentFile("domain; domain1.com; 1; 1; True", name="domains.csv")
self.client.post(
reverse("admin:domain_import"), {
"sourcefile": f
}
reverse("admin:domain_import"), {"sourcefile": f}
)
self.assertTrue(limit.is_exceeded())
self.assertFalse(limit.is_exceeded())
f = ContentFile(b"""domain; domain3.com; 1000; 100; True
f = ContentFile("domain; domain2.com; 2; 1; True", name="domains.csv")
response = self.client.post(
reverse("admin:domain_import"), {"sourcefile": f}
)
self.assertContains(response, "Quota: limit reached")
f = ContentFile(b"""domain; domain2.com; 1; 1; True
domain; domain3.com; 1000; 100; True
""", name="domains.csv")
response = self.client.post(
reverse("admin:domain_import"), {
"sourcefile": f
}
reverse("admin:domain_import"), {"sourcefile": f}
)
self.assertContains(response, "Domains: limit reached")
+27 -8
View File
@@ -76,9 +76,9 @@ class ResourceTestCase(lib_tests.ModoTestCase):
reverse("admin:alias_add"), values, status
)
def _create_domain(self, name, status=200, withtpl=False):
def _create_domain(self, name, status=200, withtpl=False, quota=100):
values = {
"name": name, "quota": 100, "default_mailbox_quota": 10,
"name": name, "quota": quota, "default_mailbox_quota": 10,
"create_dom_admin": False,
"create_aliases": False, "stepid": "step3", "type": "domain"
}
@@ -132,8 +132,7 @@ class DomainAdminTestCase(ResourceTestCase):
def setUp(self):
"""Test initialization."""
super(DomainAdminTestCase, self).setUp()
self.client.logout()
self.client.login(username='admin@test.com', password='toto')
self.client.force_login(self.user)
def test_mailboxes_limit(self):
self._create_account('tester1@test.com')
@@ -182,6 +181,11 @@ class DomainAdminTestCase(ResourceTestCase):
Alias.objects.get(address='alias1@test.com')
self._check_limit('mailbox_aliases', 2, 2)
def test_quota(self):
"""Check that quota limit does not exist."""
self.assertFalse(
self.user.userobjectlimit_set.filter(name="quota").exists())
class ResellerTestCase(ResourceTestCase):
@@ -189,6 +193,8 @@ class ResellerTestCase(ResourceTestCase):
def setUpTestData(cls):
"""Create test data."""
super(ResellerTestCase, cls).setUpTestData()
cls.localconfig.parameters.set_value("deflt_user_quota_limit", 1000)
cls.localconfig.save()
cls.user = UserFactory(
username='reseller', groups=('Resellers',)
)
@@ -196,8 +202,7 @@ class ResellerTestCase(ResourceTestCase):
def setUp(self):
"""Test initialization."""
super(ResellerTestCase, self).setUp()
self.client.logout()
self.client.login(username='reseller', password='toto')
self.client.force_login(self.user)
def test_domains_limit(self):
response = self.client.get(reverse("admin:domain_list"))
@@ -292,9 +297,23 @@ class ResellerTestCase(ResourceTestCase):
self._check_limit('domain_admins', 2, 2)
self._check_limit('domains', 3, 3)
def test_quota(self):
"""Check quota resource."""
self._create_domain("domain1.tld", withtpl=True, quota=1000)
response = self._create_domain(
"domain2.tld", status=403, withtpl=True, quota=1000)
self.assertEqual(response, "Quota: limit reached")
dom1 = Domain.objects.get(name="domain1.tld")
url = reverse("admin:domain_change", args=[dom1.pk])
values = {
"name": dom1.name, "type": dom1.type, "enabled": dom1.enabled,
"default_mailbox_quota": dom1.default_mailbox_quota,
"quota": 500
}
self.ajax_post(url, values)
def test_reseller_deletes_domain(self):
"""Check if all resources are restored after the deletion.
"""
"""Check if all resources are restored after the deletion."""
self._create_domain('domain.tld', withtpl=True)
dom = Domain.objects.get(name="domain.tld")
self.ajax_post(