wip: resellers and limits

* New events
* Updated events API
* New models to implement limits (more flexible)
This commit is contained in:
Antoine Nguyen
2012-01-16 14:44:32 +01:00
parent 99888a861b
commit 2a0e9fdf3a
19 changed files with 225 additions and 87 deletions
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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
+3 -6
View File
@@ -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})
+19 -10
View File
@@ -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)
+5 -5
View File
@@ -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))
+2 -2
View File
@@ -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"),
+44
View File
@@ -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
+27 -6
View File
@@ -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()
+24
View File
@@ -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()
+7 -3
View File
@@ -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",
+39 -12
View File
@@ -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')
+13 -3
View File
@@ -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)
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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"),
+2 -2
View File
@@ -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",
+28 -23
View File
@@ -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
+1 -2
View File
@@ -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})
+2 -4
View File
@@ -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})