From 2a0e9fdf3a966373591439cf542106893426c1da Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 16 Jan 2012 14:44:32 +0100 Subject: [PATCH] wip: resellers and limits * New events * Updated events API * New models to implement limits (more flexible) --- admin/__init__.py | 2 +- admin/models.py | 4 +- admin/templatetags/admin_extras.py | 9 ++--- admin/views.py | 29 +++++++++----- auth/views.py | 10 ++--- extensions/amavis_quarantine/main.py | 4 +- extensions/limits/controls.py | 44 +++++++++++++++++++++ extensions/limits/forms.py | 33 +++++++++++++--- extensions/limits/lib.py | 24 ++++++++++++ extensions/limits/main.py | 10 +++-- extensions/limits/models.py | 51 +++++++++++++++++++------ extensions/limits/views.py | 16 ++++++-- extensions/postfix_autoreply/main.py | 4 +- extensions/sievefilters/main.py | 4 +- extensions/stats/main.py | 4 +- extensions/webmail/main.py | 4 +- lib/events.py | 51 ++++++++++++++----------- lib/templatetags/libextras.py | 3 +- userprefs/templatetags/uprefs_extras.py | 6 +-- 19 files changed, 225 insertions(+), 87 deletions(-) create mode 100644 extensions/limits/controls.py create mode 100644 extensions/limits/lib.py diff --git a/admin/__init__.py b/admin/__init__.py index 2d8a8ff17..20e90bee8 100644 --- a/admin/__init__.py +++ b/admin/__init__.py @@ -60,7 +60,7 @@ def unset_default_topredirection(**kwargs): if topredirection == kwargs["ext"].name: parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs", app="general") -def update_available_applications(**kwargs): +def update_available_applications(extension): """Simple callback to update the list of available applications Must be called each time an extension is disabled/enabled. diff --git a/admin/models.py b/admin/models.py index 46f979051..98593d0c7 100644 --- a/admin/models.py +++ b/admin/models.py @@ -493,7 +493,7 @@ class Extension(models.Model): exec_cmd("ln -s %s/static %s/static/%s" % \ (extdir, settings.MODOBOA_DIR, self.name)) - events.raiseEvent("ExtEnabled", ext=self) + events.raiseEvent("ExtEnabled", self) def off(self): self.unload() @@ -508,7 +508,7 @@ class Extension(models.Model): if os.path.exists(staticpath): exec_cmd("rm -r %s/static/%s" % (settings.MODOBOA_DIR, self.name)) - events.raiseEvent("ExtDisabled", ext=self) + events.raiseEvent("ExtDisabled", self) def populate_callback(sender, user=None, **kwargs): """Populate signal callback diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index 71ba2dd8c..5796f6461 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -37,8 +37,7 @@ def admin_menu(user): "img" : static_url("pics/mailbox.png")} ] entries[0]["menu"] += \ - events.raiseQueryEvent("AdminMenuDisplay", target="top_menu", - user=user) + events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user) if user.has_perm("auth.view_permissions"): entries[0]["menu"] += [ {"name" : "permissions", @@ -135,8 +134,7 @@ def domains_menu(selection, user, domid): reverse(admin.views.mbaliases) + args, reverse(admin.views.newmbalias) + args), ] - entries += events.raiseQueryEvent("AdminMenuDisplay", target="admin_menu_box", - user=user) + entries += events.raiseQueryEvent("AdminMenuDisplay", "admin_menu_box", user) return render_to_string('common/menu.html', {"entries" : entries, "selection" : selection, @@ -220,8 +218,7 @@ def mbalias_actions(user, aliasid): @register.simple_tag def loadadminextmenu(user): - menu = events.raiseQueryEvent("AdminMenuDisplay", target="admin_menu_box", - user=user) + menu = events.raiseQueryEvent("AdminMenuDisplay", "admin_menu_box", user) return render_to_string('common/menulist.html', {"entries" : menu, "user" : user}) diff --git a/admin/views.py b/admin/views.py index f9e80b68d..f9a9aad9d 100644 --- a/admin/views.py +++ b/admin/views.py @@ -8,7 +8,7 @@ from django.contrib.auth.decorators \ from django.db.models import Q from django.contrib.auth.models import User, Group from django.contrib import messages -from django.db import transaction, IntegrityError +from django.db import transaction, IntegrityError, transaction from lib import * from modoboa import admin, userprefs @@ -31,7 +31,10 @@ def domains(request): return HttpResponseRedirect(reverse(userprefs.views.preferences)) - domains = Domain.objects.all() + if request.user.is_superuser: + domains = Domain.objects.all() + else: + domains = events.raiseQueryEvent("GetUserDomains", request.user) for dom in domains: dom.mbalias_counter = len(Alias.objects.filter(domain=dom.id)) deloptions = {"keepdir" : _("Do not delete domain directory")} @@ -50,6 +53,7 @@ def _validate_domain(request, form, successmsg, commonctx, """ error = None if form.is_valid(): + events.raiseEvent("CanCreateDomain", request.user) try: domain = form.save() except AdminError, e: @@ -58,7 +62,7 @@ def _validate_domain(request, form, successmsg, commonctx, error = _("Domain with this name already defined") else: if callback is not None: - callback(domain) + callback(request.user, domain) messages.info(request, successmsg, fail_silently=True) return ajax_response(request, url=reverse(admin.views.domains)) @@ -68,9 +72,10 @@ def _validate_domain(request, form, successmsg, commonctx, @login_required @permission_required("admin.add_domain") +@transaction.commit_on_success def newdomain(request, tplname="admin/adminform.html"): - def newdomain_cb(domain): - events.raiseEvent("CreateDomain", dom=domain) + def newdomain_cb(user, domain): + events.raiseEvent("CreateDomain", user, domain) commonctx = {"title" : _("New domain"), "submit_label" : _("Create"), @@ -106,17 +111,21 @@ def editdomain(request, dom_id, tplname="admin/adminform.html"): def deldomain(request): selection = request.GET["selection"].split(",") error = None - if request.user.id != 1: + try: mb = Mailbox.objects.get(user__id=request.user.id) + except Mailbox.DoesNotExist: + pass + else: if str(mb.domain.id) in selection: error = _("You can't delete your own domain") + if error is None: if request.GET.has_key("keepdir") and request.GET["keepdir"] == "true": keepdir = True else: keepdir = False for dom in Domain.objects.filter(id__in=selection): - events.raiseEvent("DeleteDomain", dom=dom) + events.raiseEvent("DeleteDomain", dom) dom.delete(keepdir=keepdir) msg = ungettext("Domain deleted", "Domains deleted", len(selection)) messages.info(request, msg, fail_silently=True) @@ -278,7 +287,7 @@ def newmailbox(request, tplname="admin/adminform.html"): except AdminError as e: error = str(e) else: - events.raiseEvent("CreateMailbox", mbox=mb) + events.raiseEvent("CreateMailbox", mb) messages.info(request, _("Mailbox created"), fail_silently=True) return ajax_response(request, url=reverse(admin.views.mailboxes) + "?domid=%d" % mb.domain.id) @@ -312,7 +321,7 @@ def editmailbox(request, mbox_id=None, tplname="admin/adminform.html"): else: if oldmb.rename_dir(mb.domain.name, mb.address): form.commit_save(mb) - events.raiseEvent("ModifyMailbox", mbox=mb, oldmbox=oldmb) + events.raiseEvent("ModifyMailbox", mb, oldmb) messages.info(request, _("Mailbox modified"), fail_silently=True) return ajax_response(request, url=reverse(admin.views.mailboxes) + "?domid=%d" % mb.domain.id) @@ -343,7 +352,7 @@ def delmailbox(request): else: keepdir = False for mb in Mailbox.objects.filter(id__in=selection): - events.raiseEvent("DeleteMailbox", mbox=mb) + events.raiseEvent("DeleteMailbox", mb) mb.delete(keepdir=keepdir) msg = ungettext("Mailbox deleted", "Mailboxes deleted", len(selection)) messages.info(request, msg, fail_silently=True) diff --git a/auth/views.py b/auth/views.py index 07c99ab03..7647d244d 100755 --- a/auth/views.py +++ b/auth/views.py @@ -26,9 +26,9 @@ def dologin(request): request.session["django_language"] = \ parameters.get_user(request.user, "LANG", app="general") - events.raiseEvent("UserLogin", request=request, - username=request.POST["username"], - password=request.POST["password"]) + events.raiseEvent("UserLogin", request, + request.POST["username"], + request.POST["password"]) next = request.POST["next"] if next.strip() in ["", "None"]: @@ -43,11 +43,11 @@ def dologin(request): next = request.GET.has_key("next") and request.GET["next"] or None return render_to_response("registration/login.html", { "form" : form, "error" : error, "next" : next, - "annoucements" : events.raiseQueryEvent("GetAnnouncement", target="loginpage") + "annoucements" : events.raiseQueryEvent("GetAnnouncement", "loginpage") }, context_instance=RequestContext(request)) def dologout(request): - events.raiseEvent("UserLogout", request=request) + events.raiseEvent("UserLogout", request) logout(request) return HttpResponseRedirect(reverse(dologin)) diff --git a/extensions/amavis_quarantine/main.py b/extensions/amavis_quarantine/main.py index 7bf7d69a1..01f1605bc 100644 --- a/extensions/amavis_quarantine/main.py +++ b/extensions/amavis_quarantine/main.py @@ -49,10 +49,10 @@ def urls(prefix): return (r'^%squarantine/' % prefix, include('modoboa.extensions.amavis_quarantine.urls')) -def menu(**kwargs): +def menu(target, menu): import views - if kwargs["target"] == "top_menu": + if target == "top_menu": return [ {"name" : "quarantine", "label" : _("Quarantine"), diff --git a/extensions/limits/controls.py b/extensions/limits/controls.py new file mode 100644 index 000000000..067a224c2 --- /dev/null +++ b/extensions/limits/controls.py @@ -0,0 +1,44 @@ +# coding: utf-8 +""" +:mod:`controls` --- provides event handlers to check if limits are reached +-------------------------------------------------------------------------- + +""" + +from lib import * +from models import * + +def check_domains_limit(user): + """Check if user has reached its limit for domains creation + + Raise a ``LimitReached`` exception if the limit is reached. + + :param user: a ``User`` object + """ + if not is_reseller(user): + return + if not user.limitspool.inc_curvalue('domains_limit'): + raise LimitReached(user.limitspool.get_limit('domains_limit')) + +def associate_domain_to_reseller(user, domain): + if not is_reseller(user): + return + ro = ResellerObject(user=user, content_object=domain) + ro.save() + +def get_reseller_domains(user): + qs = user.resellerobject_set.filter(content_type__app_label="admin", + content_type__model="domain") + domains = [] + for ro in qs.all(): + domains += [ro.content_object] + return domains + +def check_domaliases_limit(user, domalias): + pass + +def check_mboxes_limit(user, mbox): + pass + +def check_mbaliases_limit(user, mbalias): + pass diff --git a/extensions/limits/forms.py b/extensions/limits/forms.py index efabfc74e..030b36d28 100644 --- a/extensions/limits/forms.py +++ b/extensions/limits/forms.py @@ -21,11 +21,18 @@ class ResellerForm(forms.ModelForm): user.groups.add(grp) user.save() try: - pool = LimitsPool.objects.get(user=user) + pool = user.limitspool except LimitsPool.DoesNotExist: pool = LimitsPool() pool.user = user pool.save() + + for lname in reseller_limits_tpl: + l = Limit() + l.name = lname + l.pool = pool + l.save() + return user class ResellerWithPasswordForm(ResellerForm): @@ -41,10 +48,24 @@ class ResellerWithPasswordForm(ResellerForm): raise forms.ValidationError(_("The two password fields didn't match.")) return password2 -class LimitsPoolForm(forms.ModelForm): - class Meta: - model = LimitsPool - fields = ("maxdomains", "maxdomaliases", "maxmboxes", "maxmbaliases") - +class ResellerPoolForm(forms.Form): + domains_limit = forms.IntegerField(ugettext_noop("Max domains"), + help_text=ugettext_noop("Maximum number of domains that can be created by this user")) + domain_aliases_limit = forms.IntegerField(ugettext_noop("Max domain aliases"), + help_text=ugettext_noop("Maximum number of domain aliases that can be created by this user")) + mailboxes_limit = forms.IntegerField(ugettext_noop("Max mailboxes"), + help_text=ugettext_noop("Maximum number of mailboxes that can be created by this user")) + mailbox_aliases_limit = forms.IntegerField(ugettext_noop("Max mailbox aliases"), + help_text=ugettext_noop("Maximum number of mailbox aliases that can be created by this user")) + def load_from_user(self, user): + for l in reseller_limits_tpl: + self.fields[l].initial = user.limitspool.getmaxvalue(l) + def save_new_limits(self, pool): + for lname in reseller_limits_tpl: + l = pool.limit_set.get(name=lname) + l.maxvalue = self.cleaned_data[lname] + l.save() + + diff --git a/extensions/limits/lib.py b/extensions/limits/lib.py new file mode 100644 index 000000000..6a52e5c39 --- /dev/null +++ b/extensions/limits/lib.py @@ -0,0 +1,24 @@ +# coding: utf-8 +""" +:mod:`lib` --- public functions +------------------------------- + +""" + +from django.contrib.auth.models import Group + +class LimitReached(Exception): + def __init__(self, limit): + self.limit = limit + + def __str__(self): + return "%s reached" % self.limit.name + +def is_reseller(user): + """Tell if a user is a reseller or not + + :param user: a User object + :return: True if the user is a reseller, False otherwise. + """ + grp = Group.objects.get(name="Resellers") + return grp in user.groups.all() diff --git a/extensions/limits/main.py b/extensions/limits/main.py index b55a77923..929d22e81 100644 --- a/extensions/limits/main.py +++ b/extensions/limits/main.py @@ -11,11 +11,15 @@ from django.core.urlresolvers import reverse from django.conf.urls.defaults import include from modoboa.lib import events, parameters from modoboa.lib.webutils import static_url +from controls import * baseurl = "limits" def init(): events.register("AdminMenuDisplay", menu) + events.register("CanCreateDomain", check_domains_limit) + events.register("CreateDomain", associate_domain_to_reseller) + events.register("GetUserDomains", get_reseller_domains) def destroy(): events.unregister("AdminMenuDisplay", menu) @@ -28,12 +32,12 @@ def infos(): "url" : baseurl } -def menu(**kwargs): +def menu(target, user): import views - if kwargs["target"] != "top_menu": + if target != "top_menu": return [] - if not kwargs["user"].is_superuser: + if not user.is_superuser: return [] return [ {"name" : "resellers", diff --git a/extensions/limits/models.py b/extensions/limits/models.py index d00ebcf0f..bcbe4693a 100644 --- a/extensions/limits/models.py +++ b/extensions/limits/models.py @@ -1,21 +1,48 @@ # coding: utf-8 from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic from django.contrib.auth.models import User from django.utils.translation import ugettext as _, ugettext_noop +reseller_limits_tpl = [ + "domains_limit", + "domain_aliases_limit", + "mailboxes_limit", + "mailbox_aliases_limit", + ] + class LimitsPool(models.Model): user = models.OneToOneField(User) - maxdomains = models.IntegerField(ugettext_noop("Max domains"), - default=0, - help_text=ugettext_noop("Maximum number of domains that can be created by this user")) - maxdomaliases = models.IntegerField(ugettext_noop("Max domain aliases"), - default=0, - help_text=ugettext_noop("Maximum number of domain aliases that can be created by this user")) - maxmboxes = models.IntegerField(ugettext_noop("Max mailboxes"), - default=0, - help_text=ugettext_noop("Maximum number of mailboxes that can be created by this user")) - maxmbaliases = models.IntegerField(ugettext_noop("Max mailbox aliases"), - default=0, - help_text=ugettext_noop("Maximum number of mailbox aliases that can be created by this user")) + def getcurvalue(self, lname): + l = Limit.objects.get(name=lname) + return l.curvalue + + def getmaxvalue(self, lname): + l = Limit.objects.get(name=lname) + return l.maxvalue + + def inc_curvalue(self, lname, nb=1): + l = Limit.objects.get(name=lname) + if l.curvalue + nb > l.maxvalue: + return False + l.curvalue += nb + l.save() + return True + + def get_limit(self, lname): + return Limit.objects.get(name=lname) + +class Limit(models.Model): + name = models.CharField(max_length=255) + curvalue = models.IntegerField(default=0) + maxvalue = models.IntegerField(default=0) + pool = models.ForeignKey(LimitsPool) + +class ResellerObject(models.Model): + user = models.ForeignKey(User) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') diff --git a/extensions/limits/views.py b/extensions/limits/views.py index d40a4dcc3..9c9475a4f 100644 --- a/extensions/limits/views.py +++ b/extensions/limits/views.py @@ -87,7 +87,17 @@ def delete_resellers(request): @login_required @user_passes_test(lambda u: u.is_superuser) def edit_limits_pool(request, resid, tplname="limits/pool.html"): - pool = LimitsPool.objects.get(user=resid) - form = LimitsPoolForm(instance=pool) - ctx = dict(form=form, reseller=pool.user) + reseller = User.objects.get(pk=resid) + if request.method == "POST": + form = ResellerPoolForm(request.POST) + if form.is_valid(): + form.save_new_limits(reseller.limitspool) + messages.info(request, _("Pool updated"), fail_silently=True) + return ajax_response(request, url=reverse(resellers)) + ctx = dict(form=form) + return ajax_response(request, status="ko", template=tplname, **ctx) + + form = ResellerPoolForm() + form.load_from_user(reseller) + ctx = dict(form=form, reseller=reseller) return _render(request, tplname, ctx) diff --git a/extensions/postfix_autoreply/main.py b/extensions/postfix_autoreply/main.py index 4b340ee27..3d82fa7c5 100644 --- a/extensions/postfix_autoreply/main.py +++ b/extensions/postfix_autoreply/main.py @@ -47,10 +47,10 @@ def urls(prefix): return (r'^%spostfix_autoreply/' % prefix, include('modoboa.extensions.postfix_autoreply.urls')) -def menu(**kwargs): +def menu(target, user): import views - if kwargs["target"] != "uprefs_menu": + if target != "uprefs_menu": return [] return [ {"name" : "autoreply", diff --git a/extensions/sievefilters/main.py b/extensions/sievefilters/main.py index 334d05069..58e79606d 100644 --- a/extensions/sievefilters/main.py +++ b/extensions/sievefilters/main.py @@ -47,10 +47,10 @@ def urls(prefix): return (r'^%ssfilters/' % prefix, include('modoboa.extensions.sievefilters.urls')) -def menu(**kwargs): +def menu(target, user): import views - if kwargs["target"] != "options_menu": + if target != "options_menu": return [] return [ {"name" : "sievefilters", diff --git a/extensions/stats/main.py b/extensions/stats/main.py index a6a260880..f805e4717 100644 --- a/extensions/stats/main.py +++ b/extensions/stats/main.py @@ -40,8 +40,8 @@ def urls(prefix): return (r'^%sstats/' % prefix, include('modoboa.extensions.stats.urls')) -def menu(**kwargs): - if kwargs["target"] == "admin_menu_box": +def menu(target, user): + if target == "admin_menu_box": return [ {"name" : "stats", "label" : ugettext("Statistics"), diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py index 9caaf085c..b49ac14de 100644 --- a/extensions/webmail/main.py +++ b/extensions/webmail/main.py @@ -81,10 +81,10 @@ def urls(prefix): return (r'^%swebmail/' % prefix, include('modoboa.extensions.webmail.urls')) -def menu(**kwargs): +def menu(target, user): import views - if kwargs["target"] != "top_menu": + if target != "top_menu": return [] return [ {"name" : "webmail", diff --git a/lib/events.py b/lib/events.py index 732ea08bc..25d23004a 100644 --- a/lib/events.py +++ b/lib/events.py @@ -9,27 +9,32 @@ core application and additional components. """ -events = ["CreateDomain", - "DeleteDomain", - "CreateMailbox", - "DeleteMailbox", - "ModifyMailbox", +events = [ + "CanCreateDomain", + "CreateDomain", + "DeleteDomain", + "CreateMailbox", + "DeleteMailbox", + "ModifyMailbox", - "PasswordChange", + "GetUserDomains", + + "PasswordChange", + + "UserMenuDisplay", + "AdminMenuDisplay", + + "PermsGetTables", + "PermsGetClass", + + "ExtEnabled", + "ExtDisabled", - "UserMenuDisplay", - "AdminMenuDisplay", - - "PermsGetTables", - "PermsGetClass", - - "ExtEnabled", - "ExtDisabled", - - "UserLogin", - "UserLogout", - - "GetAnnouncement"] + "UserLogin", + "UserLogout", + + "GetAnnouncement" + ] callbacks = {} @@ -64,7 +69,7 @@ def unregister(event, callback): except ValueError: pass -def raiseEvent(event, **kwargs): +def raiseEvent(event, *args): """Raise a specific event Any additional keyword argument will be passed to registered @@ -75,10 +80,10 @@ def raiseEvent(event, **kwargs): if not event in events or not event in callbacks.keys(): return 0 for callback in callbacks[event]: - callback(**kwargs) + callback(*args) return 1 -def raiseQueryEvent(event, **kwargs): +def raiseQueryEvent(event, *args): """Raise a specific event and wait for answers from callbacks Any additional keyword argument will be passed to registered @@ -90,5 +95,5 @@ def raiseQueryEvent(event, **kwargs): if not event in events or not event in callbacks.keys(): return result for callback in callbacks[event]: - result += callback(**kwargs) + result += callback(*args) return result diff --git a/lib/templatetags/libextras.py b/lib/templatetags/libextras.py index 6e6015743..8f0c172c0 100644 --- a/lib/templatetags/libextras.py +++ b/lib/templatetags/libextras.py @@ -52,7 +52,6 @@ def display_messages(msgs): @register.simple_tag def load_optionalmenu(user): - menu = events.raiseQueryEvent("UserMenuDisplay", target="top_menu_middle", - user=user) + menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu_middle", user) return template.loader.render_to_string('common/menulist.html', {"entries" : menu, "user" : user}) diff --git a/userprefs/templatetags/uprefs_extras.py b/userprefs/templatetags/uprefs_extras.py index 3f3f0acf4..b69fe1eec 100644 --- a/userprefs/templatetags/uprefs_extras.py +++ b/userprefs/templatetags/uprefs_extras.py @@ -41,8 +41,7 @@ def options_menu(user): "rel" : "{handler:'iframe',size:{x:360,y:350},closeBtn:true}" }] - entries[0]["menu"] += events.raiseQueryEvent("UserMenuDisplay", - target="options_menu") + entries[0]["menu"] += events.raiseQueryEvent("UserMenuDisplay", "options_menu") return render_to_string('common/menulist.html', {"entries" : entries, "user" : user}) @@ -58,7 +57,6 @@ def uprefs_menu(user): @register.simple_tag def loadextmenu(user): - menu = events.raiseQueryEvent("UserMenuDisplay", target="top_menu", - user=user) + menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user) return render_to_string('common/menulist.html', {"entries" : menu, "user" : user})