wip: relay domains support

* Unit tests added (limits application)
* Added support for limits
* Big changes were required
* Using standard HTTP codes to identify errors (json)
* Custom exceptions have been reworked (not finished yet)

see #491
This commit is contained in:
Antoine Nguyen
2013-10-28 18:36:42 +01:00
parent d7a2e5c30b
commit f91ef1adf2
62 changed files with 1484 additions and 950 deletions
+4 -1
View File
@@ -15,6 +15,7 @@ base_events = [
"RoleChanged",
"GetExtraRoles",
"PasswordChange",
"UserCanSetRole",
"UserMenuDisplay",
"AdminMenuDisplay",
@@ -32,7 +33,9 @@ base_events = [
"ExtraAdminContent",
"ExtraUprefsRoutes",
"ExtraUprefsJS"
"ExtraUprefsJS",
"GetExtraParameters"
]
-14
View File
@@ -1,14 +0,0 @@
"""
:mod:`exceptions` --- Custom exceptions of the admin. application
-----------------------------------------------------------------
"""
from modoboa.lib.exceptions import ModoboaException
class AdminError(ModoboaException):
"""
Just a Custom exception to identify errors coming from the admin
application.
"""
pass
+7
View File
@@ -49,6 +49,13 @@ class ExtensionsPool(object):
self.extensions[name]["instance"] = self.extensions[name]["cls"]()
return self.extensions[name]["instance"]
def is_extension_enabled(self, name):
from modoboa.core.models import Extension
if not name in self.extensions:
return False
return Extension.objects.get(name=name).enabled
def get_extension_infos(self, name):
instance = self.get_extension(name)
if instance is None:
+13 -10
View File
@@ -17,10 +17,11 @@ from django.contrib.auth.models import (
UserManager, Group, AbstractBaseUser, PermissionsMixin
)
from modoboa.lib import events, md5crypt, parameters
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.exceptions import (
PermDeniedException, InternalError, BadRequest
)
from modoboa.lib.sysutils import exec_cmd
from modoboa.core.extensions import exts_pool
from modoboa.core.exceptions import AdminError
try:
from modoboa.lib.ldaputils import *
@@ -72,7 +73,9 @@ class User(AbstractBaseUser, PermissionsMixin):
get_object_owner, grant_access_to_object, ungrant_access_to_object
if fromuser == self:
raise AdminError(_("You can't delete your own account"))
raise PermDeniedException(
_("You can't delete your own account")
)
if not fromuser.can_access(self):
raise PermDeniedException
@@ -133,15 +136,15 @@ class User(AbstractBaseUser, PermissionsMixin):
self.password = self._crypt_password(raw_value)
else:
if not ldap_available:
raise AdminError(
raise InternalError(
_("Failed to update password: LDAP module not installed")
)
ab = LDAPAuthBackend()
try:
ab.update_user_password(self.username, curvalue, raw_value)
except LDAPException, e:
raise AdminError(_("Failed to update password: %s" % str(e)))
except LDAPException as e:
raise InternalError(_("Failed to update password: %s" % str(e)))
events.raiseEvent(
"PasswordUpdated", self, raw_value, self.pk is None
)
@@ -197,7 +200,7 @@ class User(AbstractBaseUser, PermissionsMixin):
try:
return self.groups.all()[0].name
except IndexError:
return "SimpleUsers"
return "---"
@property
def enabled(self):
@@ -318,7 +321,7 @@ class User(AbstractBaseUser, PermissionsMixin):
:param crypt_password:
"""
if len(row) < 7:
raise AdminError(_("Invalid line"))
raise BadRequest(_("Invalid line"))
role = row[6].strip()
if not user.is_superuser and not role in ["SimpleUsers", "DomainAdmins"]:
raise PermDeniedException(
@@ -327,11 +330,11 @@ class User(AbstractBaseUser, PermissionsMixin):
self.username = row[1].strip()
if role == "SimpleUsers":
if (len(row) < 8 or not row[7].strip()):
raise AdminError(
raise BadRequest(
_("The simple user '%s' must have a valid email address" % self.username)
)
if self.username != row[7].strip():
raise AdminError(
raise BadRequest(
_("username and email fields must not differ for '%s'" % self.username)
)
+10 -17
View File
@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import (
)
from modoboa.lib import parameters
from modoboa.lib.webutils import (
ajax_simple_response, _render_to_string
_render_to_string, render_to_json_response
)
from modoboa.lib.listing import get_sort_order, get_listing_page
from modoboa.core.models import Extension, Log
@@ -24,7 +24,7 @@ def viewsettings(request, tplname='core/settings_header.html'):
@login_required
@user_passes_test(lambda u: u.is_superuser)
def viewparameters(request, tplname='core/parameters.html'):
return ajax_simple_response({
return render_to_json_response({
"status": "ok",
"left_selection": "parameters",
"content": _render_to_string(request, tplname, {
@@ -42,12 +42,10 @@ def saveparameters(request):
form.save()
form.to_django_settings()
continue
return ajax_simple_response({
"status": "ko", "prefix": form.app, "errors": form.errors
})
return ajax_simple_response(dict(
status="ok", respmsg=_("Parameters saved")
))
return render_to_json_response(
{'form_errors': form.errors, 'prefix': form.app}, status=400
)
return render_to_json_response(_("Parameters saved"))
@login_required
@@ -68,8 +66,7 @@ def viewextensions(request, tplname='core/extensions.html'):
ext["selection"] = False
tbl = ExtensionsTable(request, exts)
return ajax_simple_response({
"status": "ok",
return render_to_json_response({
"content": _render_to_string(request, tplname, {"extensions": tbl})
})
@@ -91,16 +88,13 @@ def saveextensions(request):
if not ext in found:
ext.off()
return ajax_simple_response(dict(
status="ok", respmsg=_("Modifications applied.")
))
return render_to_json_response(_("Modifications applied."))
@login_required
@user_passes_test(lambda u: u.is_superuser)
def information(request, tplname="core/information.html"):
return ajax_simple_response({
"status": "ok",
return render_to_json_response({
"content": render_to_string(tplname, {})
})
@@ -118,8 +112,7 @@ def logs(request, tplname="core/logs.html"):
Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)),
request.GET.get("page", 1)
)
return ajax_simple_response({
"status": "ok",
return render_to_json_response({
"content": render_to_string(tplname, {
"logs": page.object_list,
}),
+9 -18
View File
@@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _
from django.contrib.auth.decorators import login_required
from modoboa.lib import events, parameters
from modoboa.lib.webutils import (
ajax_simple_response, _render_to_string
_render_to_string, render_to_json_response
)
from modoboa.lib.cryptutils import encrypt
from modoboa.core.forms import ProfileForm
@@ -32,17 +32,11 @@ def profile(request, tplname='core/user_profile.html'):
form.save()
if update_password and form.cleaned_data["confirmation"] != "":
request.session["password"] = encrypt(form.cleaned_data["confirmation"])
return ajax_simple_response(dict(
status="ok", respmsg=_("Profile updated")
))
return ajax_simple_response({
"status": "ko",
"errors": form.errors
})
return render_to_json_response(_("Profile updated"))
return render_to_json_response({'form_errors': form.errors}, status=400)
form = ProfileForm(update_password, instance=request.user)
return ajax_simple_response({
"status": "ok",
return render_to_json_response({
"content": _render_to_string(request, tplname, {
"form": form
})
@@ -57,16 +51,13 @@ def preferences(request):
if form.is_valid():
form.save()
continue
return ajax_simple_response({
"status": "ko", "prefix": form.app, "errors": form.errors
})
return render_to_json_response({
"prefix": form.app, "form_errors": form.errors
}, status=400)
return ajax_simple_response({
"status": "ok", "respmsg": _("Preferences saved")
})
return render_to_json_response(_("Preferences saved"))
return ajax_simple_response({
"status": "ok",
return render_to_json_response({
"content": _render_to_string(request, "core/user_preferences.html", {
"forms": parameters.get_user_forms(request.user)
})
+2 -4
View File
@@ -3,14 +3,12 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from django.core.urlresolvers import reverse
from modoboa.core.extensions import ModoExtension, exts_pool
from modoboa.lib import parameters, events
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.exceptions import PermDeniedException, BadRequest
from modoboa.lib.emailutils import split_mailbox
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, DomainAlias, Mailbox, Alias
)
admin_events = [
"DomainCreated",
"DomainModified",
@@ -146,7 +144,7 @@ def import_account_mailbox(user, account, row):
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise AdminError(
raise BadRequest(
_("Account import failed (%s): domain does not exist" % account.username)
)
if not user.can_access(domain):
-19
View File
@@ -1,19 +0,0 @@
# coding: utf-8
"""
:mod:`exceptions` --- Custom exceptions of the admin. application
-----------------------------------------------------------------
"""
from modoboa.lib.exceptions import ModoboaException
class AdminError(ModoboaException):
"""
Just a Custom exception to identify errors coming from the admin
application.
"""
pass
class BadDestination(Exception):
pass
+84 -61
View File
@@ -2,14 +2,13 @@ from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from django.http import QueryDict
from modoboa.lib import events, parameters
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.exceptions import PermDeniedException, Conflict, NotFound
from modoboa.lib.permissions import get_account_roles
from modoboa.lib.emailutils import split_mailbox
from modoboa.lib.formutils import (
DomainNameField, DynamicForm, TabForms
)
from modoboa.core.models import User
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, Mailbox, Alias
)
@@ -22,7 +21,9 @@ class AccountFormGeneral(forms.ModelForm):
choices=[('', ugettext_lazy("Choose"))],
help_text=ugettext_lazy("What level of permission this user will have")
)
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
password1 = forms.CharField(
label=_("Password"), widget=forms.PasswordInput
)
password2 = forms.CharField(
label=ugettext_lazy("Confirmation"),
widget=forms.PasswordInput,
@@ -90,7 +91,7 @@ class AccountFormGeneral(forms.ModelForm):
def save(self, commit=True):
account = super(AccountFormGeneral, self).save(commit=False)
if self.user == account and not self.cleaned_data["is_active"]:
raise AdminError(_("You can't disable your own account"))
raise PermDeniedException(_("You can't disable your own account"))
if commit:
if "password1" in self.cleaned_data \
and self.cleaned_data["password1"] != "":
@@ -105,14 +106,21 @@ class AccountFormMail(forms.Form, DynamicForm):
quota = forms.IntegerField(
label=ugettext_lazy("Quota"),
required=False,
help_text=_("Quota in MB for this mailbox. Define a custom value or use domain's default one. Leave empty to define an unlimited value (not allowed for domain administrators)."),
help_text=_("Quota in MB for this mailbox. Define a custom value or "
"use domain's default one. Leave empty to define an "
"unlimited value (not allowed for domain "
"administrators)."),
widget=forms.widgets.TextInput(attrs={"class": "span1"})
)
quota_act = forms.BooleanField(required=False)
aliases = forms.EmailField(
label=ugettext_lazy("Alias(es)"),
required=False,
help_text=ugettext_lazy("Alias(es) of this mailbox. Indicate only one address per input, press ENTER to add a new input. Use the '*' character to create a 'catchall' alias (ex: *@domain.tld).")
help_text=ugettext_lazy(
"Alias(es) of this mailbox. Indicate only one address per input, "
"press ENTER to add a new input. Use the '*' character to create "
"a 'catchall' alias (ex: *@domain.tld)."
)
)
def __init__(self, *args, **kwargs):
@@ -143,6 +151,52 @@ class AccountFormMail(forms.Form, DynamicForm):
"""Ensure lower case emails"""
return self.cleaned_data["email"].lower()
def create_mailbox(self, user, account):
locpart, domname = split_mailbox(self.cleaned_data["email"])
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise NotFound(_("Domain does not exist"))
if not user.can_access(domain):
raise PermDeniedException
try:
Mailbox.objects.get(address=locpart, domain=domain)
except Mailbox.DoesNotExist:
pass
else:
raise Conflict(
_("Mailbox %s already exists" % self.cleaned_data["email"])
)
events.raiseEvent("CanCreate", user, "mailboxes")
self.mb = Mailbox(address=locpart, domain=domain, user=account,
use_domain_quota=self.cleaned_data["quota_act"])
self.mb.set_quota(self.cleaned_data["quota"],
user.has_perm("admin.add_domain"))
self.mb.save(creator=user)
def update_mailbox(self, user, account):
newaddress = None
if self.cleaned_data["email"] != self.mb.full_address:
newaddress = self.cleaned_data["email"]
elif account.group == "SimpleUsers" and account.username != self.mb.full_address:
newaddress = account.username
if newaddress is not None:
local_part, domname = split_mailbox(newaddress)
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise NotFound(_("Domain does not exist"))
if not user.can_access(domain):
raise PermDeniedException
self.mb.rename(local_part, domain)
self.mb.use_domain_quota = self.cleaned_data["quota_act"]
override_rules = True \
if not self.mb.quota or user.has_perm("admin.add_domain") \
else False
self.mb.set_quota(self.cleaned_data["quota"], override_rules)
self.mb.save()
def save(self, user, account):
if self.cleaned_data["email"] == "":
return None
@@ -151,74 +205,43 @@ class AccountFormMail(forms.Form, DynamicForm):
self.cleaned_data["quota"] = None
if not hasattr(self, "mb") or self.mb is None:
locpart, domname = split_mailbox(self.cleaned_data["email"])
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise AdminError(_("Domain does not exist"))
if not user.can_access(domain):
raise PermDeniedException
try:
Mailbox.objects.get(address=locpart, domain=domain)
except Mailbox.DoesNotExist:
pass
else:
raise AdminError(_("Mailbox %s already exists" % self.cleaned_data["email"]))
events.raiseEvent("CanCreate", user, "mailboxes")
self.mb = Mailbox(address=locpart, domain=domain, user=account,
use_domain_quota=self.cleaned_data["quota_act"])
self.mb.set_quota(self.cleaned_data["quota"],
user.has_perm("admin.add_domain"))
self.mb.save(creator=user)
self.create_mailbox(user, account)
else:
newaddress = None
if self.cleaned_data["email"] != self.mb.full_address:
newaddress = self.cleaned_data["email"]
elif account.group == "SimpleUsers" and account.username != self.mb.full_address:
newaddress = account.username
if newaddress is not None:
local_part, domname = split_mailbox(newaddress)
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise AdminError(_("Domain does not exist"))
if not user.can_access(domain):
raise PermDeniedException
self.mb.rename(local_part, domain)
self.mb.use_domain_quota = self.cleaned_data["quota_act"]
override_rules = True \
if not self.mb.quota or user.has_perm("admin.add_domain") \
else False
self.mb.set_quota(self.cleaned_data["quota"], override_rules)
self.mb.save()
self.update_mailbox(user, account)
account.email = self.cleaned_data["email"]
account.save()
aliases = []
for name, value in self.cleaned_data.iteritems():
if not name.startswith("aliases"):
continue
if value == "":
continue
local_part, domname = split_mailbox(value)
try:
self.mb.alias_set.get(address=local_part, domain__name=domname)
except Alias.DoesNotExist:
pass
else:
continue
events.raiseEvent("CanCreate", user, "mailbox_aliases")
al = Alias(address=local_part, enabled=account.is_active)
al.domain = Domain.objects.get(name=domname)
al.save(int_rcpts=[self.mb], creator=user)
aliases.append(value)
for alias in self.mb.alias_set.all():
if len(alias.get_recipients()) >= 2:
continue
if not len(filter(lambda name: self.cleaned_data[name] == alias.full_address,
self.cleaned_data.keys())):
if not alias.full_address in aliases:
if len(alias.get_recipients()) >= 2:
continue
alias.delete()
else:
aliases.remove(alias.full_address)
if aliases:
events.raiseEvent(
"CanCreate", user, "mailbox_aliases", len(aliases)
)
for alias in aliases:
local_part, domname = split_mailbox(alias)
try:
self.mb.alias_set.get(address=local_part, domain__name=domname)
except Alias.DoesNotExist:
pass
else:
continue
al = Alias(address=local_part, enabled=account.is_active)
al.domain = Domain.objects.get(name=domname)
al.save(int_rcpts=[self.mb], creator=user)
return self.mb
+25 -9
View File
@@ -1,11 +1,11 @@
from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from django.http import QueryDict
from modoboa.lib.exceptions import BadRequest, NotFound, Conflict
from modoboa.lib.emailutils import split_mailbox
from modoboa.lib.formutils import (
DynamicForm
)
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, Mailbox, Alias
)
@@ -14,11 +14,17 @@ from modoboa.extensions.admin.models import (
class AliasForm(forms.ModelForm, DynamicForm):
email = forms.EmailField(
label=ugettext_lazy("Email address"),
help_text=ugettext_lazy("The distribution list address. Use the '*' character to create a 'catchall' address (ex: *@domain.tld).")
help_text=ugettext_lazy(
"The distribution list address. Use the '*' character to create a "
"'catchall' address (ex: *@domain.tld)."
)
)
recipients = forms.EmailField(
label=ugettext_lazy("Recipients"), required=False,
help_text=ugettext_lazy("Mailbox(es) this alias will point to. Indicate only one address per input, press ENTER to add a new input.")
help_text=ugettext_lazy(
"Mailbox(es) this alias will point to. Indicate only one address "
"per input, press ENTER to add a new input."
)
)
class Meta:
@@ -58,7 +64,9 @@ class AliasForm(forms.ModelForm, DynamicForm):
except Domain.DoesNotExist:
raise forms.ValidationError(_("Domain does not exist"))
if not self.user.can_access(domain):
raise forms.ValidationError(_("You don't have access to this domain"))
raise forms.ValidationError(
_("You don't have access to this domain")
)
return self.cleaned_data["email"]
def set_recipients(self):
@@ -78,7 +86,9 @@ class AliasForm(forms.ModelForm, DynamicForm):
continue
local_part, domname = split_mailbox(v)
if domname is None:
raise AdminError("%s %s" % (_("Invalid mailbox"), v))
raise BadRequest(
u"%s %s" % (_("Invalid mailbox"), v)
)
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
@@ -94,20 +104,26 @@ class AliasForm(forms.ModelForm, DynamicForm):
try:
rcpt = Mailbox.objects.get(domain=domain, address=local_part)
except Mailbox.DoesNotExist:
raise AdminError(_("Local recipient %s not found" % v))
raise NotFound(
_("Local recipient %s not found" % v)
)
if rcpt in self.int_rcpts:
raise AdminError(_("Recipient %s already present" % v))
raise Conflict(
_("Recipient %s already present" % v)
)
self.int_rcpts += [rcpt]
total += 1
continue
if v in self.ext_rcpts:
raise AdminError(_("Recipient %s already present" % v))
raise Conflict(
_("Recipient %s already present" % v)
)
self.ext_rcpts += [v]
total += 1
if total == 0:
raise AdminError(_("No recipient defined"))
raise BadRequest(_("No recipient defined"))
def save(self, commit=True):
alias = super(AliasForm, self).save(commit=False)
+26 -15
View File
@@ -2,11 +2,11 @@ from django import forms
from django.http import QueryDict
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events
from modoboa.lib.exceptions import Conflict
from modoboa.lib.formutils import (
DomainNameField, YesNoField, DynamicForm, TabForms
)
from modoboa.core.models import User
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, DomainAlias, Mailbox, Alias, Quota
)
@@ -126,25 +126,29 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
d.save()
Mailbox.objects.filter(domain=d, use_domain_quota=True) \
.update(quota=d.quota)
aliases = []
for k, v in self.cleaned_data.iteritems():
if not k.startswith("aliases"):
continue
if v in ["", None]:
continue
try:
d.domainalias_set.get(name=v)
except DomainAlias.DoesNotExist:
pass
else:
continue
events.raiseEvent("CanCreate", user, "domain_aliases")
al = DomainAlias(name=v, target=d, enabled=d.enabled)
al.save(creator=user)
aliases.append(v)
for dalias in d.domainalias_set.all():
if not len(filter(lambda name: self.cleaned_data[name] == dalias.name,
self.cleaned_data.keys())):
if not dalias.name in aliases:
dalias.delete()
else:
aliases.remove(dalias.name)
if aliases:
events.raiseEvent("CanCreate", user, "domain_aliases", len(aliases))
for alias in aliases:
try:
d.domainalias_set.get(name=alias)
except DomainAlias.DoesNotExist:
pass
else:
continue
al = DomainAlias(name=alias, target=d, enabled=d.enabled)
al.save(creator=user)
if old_mail_homes is not None:
self.update_mailbox_quotas(d)
@@ -179,8 +183,11 @@ class DomainFormOptions(forms.Form):
required=False
)
def __init__(self, *args, **kwargs):
def __init__(self, user, *args, **kwargs):
super(DomainFormOptions, self).__init__(*args, **kwargs)
if False in events.raiseQueryEvent('UserCanSetRole', user, 'DomainAdmins'):
self.fields = {}
return
if args:
if args[0].get("create_dom_admin", "no") == "yes":
self.fields["dom_admin_username"].required = True
@@ -192,6 +199,8 @@ class DomainFormOptions(forms.Form):
return self.cleaned_data["dom_admin_username"]
def save(self, user, domain):
if not self.fields:
return
if self.cleaned_data["create_dom_admin"] == "no":
return
username = "%s@%s" % (self.cleaned_data["dom_admin_username"], domain.name)
@@ -200,7 +209,8 @@ class DomainFormOptions(forms.Form):
except User.DoesNotExist:
pass
else:
raise AdminError(_("User '%s' already exists" % username))
raise Conflict(_("User '%s' already exists" % username))
events.raiseEvent("CanCreate", user, "mailboxes")
da = User(username=username, email=username, is_active=True)
da.set_password("password")
da.save()
@@ -212,6 +222,7 @@ class DomainFormOptions(forms.Form):
mb.save(creator=user)
if self.cleaned_data["create_aliases"] == "yes":
events.raiseEvent("CanCreate", user, "mailbox_aliases")
al = Alias(address="postmaster", domain=domain, enabled=True)
al.save(int_rcpts=[mb], creator=user)
+7 -3
View File
@@ -1,7 +1,7 @@
from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib.exceptions import BadRequest, PermDeniedException
from modoboa.lib.emailutils import split_mailbox
from modoboa.extensions.admin.exceptions import BadDestination
from modoboa.extensions.admin.models import (
Domain
)
@@ -28,10 +28,14 @@ class ForwardForm(forms.Form):
for d in rawdata.split(","):
local_part, domname = split_mailbox(d)
if not local_part or not domname or not len(domname):
raise BadDestination("Invalid mailbox syntax for %s" % d)
raise BadRequest("Invalid mailbox syntax for %s" % d)
try:
Domain.objects.get(name=domname)
except Domain.DoesNotExist:
self.dests += [d]
else:
raise BadDestination(_("You can't define a forward to a local destination. Please ask your administrator to create an alias instead."))
raise PermDeniedException(
_("You can't define a forward to a local destination. "
"Please ask your administrator to create an alias "
"instead.")
)
@@ -4,8 +4,8 @@ from optparse import make_option
from django.core.management.base import BaseCommand
from modoboa.lib import parameters
from modoboa.lib.sysutils import exec_cmd
from modoboa.lib.exceptions import InternalError
from modoboa.extensions.admin import AdminConsole
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import MailboxOperation
@@ -83,7 +83,7 @@ class Command(BaseCommand):
continue
try:
f(ope)
except (OperationError, AdminError) as e:
except (OperationError, InternalError) as e:
self.logger.critical('%s failed (reason: %s)',
ope, str(e).encode('utf-8'))
else:
+11 -7
View File
@@ -1,19 +1,23 @@
import reversion
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events
from modoboa.lib.emailutils import split_mailbox
from modoboa.lib.exceptions import PermDeniedException
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.lib.exceptions import PermDeniedException, BadRequest
from .base import AdminObject
from .domain import Domain
from .mailbox import Mailbox
class Alias(AdminObject):
"""
Mailbox alias.
"""
address = models.CharField(
ugettext_lazy('address'), max_length=254,
help_text=ugettext_lazy("The alias address (without the domain part). For a 'catch-all' address, just enter an * character.")
help_text=ugettext_lazy(
"The alias address (without the domain part). For a 'catch-all' "
"address, just enter an * character."
)
)
domain = models.ForeignKey(Domain)
mboxes = models.ManyToManyField(
@@ -139,12 +143,12 @@ class Alias(AdminObject):
"""
if len(row) < expected_elements:
raise AdminError(_("Invalid line: %s" % row))
raise BadRequest(_("Invalid line: %s" % row))
localpart, domname = split_mailbox(row[1].strip())
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise AdminError(_("Domain '%s' does not exist" % domname))
raise BadRequest(_("Domain '%s' does not exist" % domname))
if not user.can_access(domain):
raise PermDeniedException
self.address = localpart
@@ -171,7 +175,7 @@ class Alias(AdminObject):
target = Mailbox.objects.get(address=localpart,
domain__name=domname)
except Mailbox.DoesNotExist:
raise AdminError(_("Local recipient %s not found" % rcpt))
raise BadRequest(_("Local recipient %s not found" % rcpt))
int_rcpts += [target]
self.save(int_rcpts=int_rcpts, ext_rcpts=ext_rcpts, creator=user)
+3 -3
View File
@@ -4,8 +4,8 @@ from django.db.models.manager import Manager
from django.utils.translation import ugettext as _, ugettext_lazy
from django.contrib.contenttypes import generic
from modoboa.lib import events, parameters
from modoboa.lib.exceptions import BadRequest
from modoboa.core.models import User, ObjectAccess
from modoboa.extensions.admin.exceptions import AdminError
from .base import AdminObject
@@ -123,12 +123,12 @@ class Domain(AdminObject):
def from_csv(self, user, row):
if len(row) < 4:
raise AdminError(_("Invalid line"))
raise BadRequest(_("Invalid line"))
self.name = row[1].strip()
try:
self.quota = int(row[2].strip())
except ValueError:
raise AdminError(_("Invalid quota value for domain '%s'" % self.name))
raise BadRequest(_("Invalid quota value for domain '%s'" % self.name))
self.enabled = (row[3].strip() == 'True')
self.save(creator=user)
@@ -1,8 +1,7 @@
import reversion
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.lib.exceptions import BadRequest
from .base import AdminObject
from .domain import Domain
@@ -37,13 +36,13 @@ class DomainAlias(AdminObject):
:param row: a list containing the alias definition
"""
if len(row) < 4:
raise AdminError(_("Invalid line"))
raise BadRequest(_("Invalid line"))
self.name = row[1].strip()
domname = row[2].strip()
try:
self.target = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise AdminError(_("Unknown domain %s" % domname))
raise BadRequest(_("Unknown domain %s" % domname))
self.enabled = row[3].strip() == 'True'
self.save(creator=user)
+4 -4
View File
@@ -9,9 +9,9 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from modoboa.lib import parameters, events
from modoboa.lib.exceptions import BadRequest, InternalError
from modoboa.lib.sysutils import exec_cmd
from modoboa.core.models import User
from modoboa.extensions.admin.exceptions import AdminError
from .base import AdminObject
from .domain import Domain
@@ -110,7 +110,7 @@ class Mailbox(AdminObject):
"doveadm user %s -f home" % self.full_address, **options
)
if code:
raise AdminError(_("Failed to retrieve mailbox location (%s)" % output))
raise InternalError(_("Failed to retrieve mailbox location (%s)" % output))
self.__mail_home = output.strip()
return self.__mail_home
@@ -158,13 +158,13 @@ class Mailbox(AdminObject):
else:
self.quota = 0
elif int(value) > self.domain.quota and not override_rules:
raise AdminError(
raise BadRequest(
_("Quota is greater than the allowed domain's limit (%dM)" % self.domain.quota)
)
else:
self.quota = value
if not self.quota and self.domain.quota and not override_rules:
raise AdminError(_("A quota is required"))
raise BadRequest(_("A quota is required"))
def get_quota(self):
"""Get quota limit.
@@ -143,8 +143,8 @@ Admin.prototype = {
reload_listing: function(data) {
this.navobj.update(true);
if (data.respmsg) {
$("body").notify("success", data.respmsg, 2000);
if (data) {
$("body").notify("success", data, 2000);
}
}
};
@@ -180,11 +180,7 @@ Domains.prototype = {
checkboxes: deloptions,
success_cb: $.proxy(this.reload_listing, this)
});
$("a[name=delrelaydomain]").confirm({
question: function() { return this.$element.attr('title'); },
method: "DELETE",
success_cb: $.proxy(this.reload_listing, this)
});
$(document).trigger('domain_listing_refresh');
},
change_inputs_state: function(value) {
@@ -239,7 +235,7 @@ Domains.prototype = {
1: this.optionsform_prefill
},
error_callbacks: {
1: this.generalform_init,
//1: this.generalform_init,
2: $.proxy(this.optionsform_init, this)
},
success_callback: $.proxy(this.reload_listing, this)
@@ -397,11 +393,7 @@ Identities.prototype = {
$("#wizard").cwizard({
formid: "newaccount_form",
transition_callbacks: {
1: this.mailform_prefill
},
error_callbacks: {
1: $.proxy(this.generalform_init, this),
2: $.proxy(this.mailform_init, this)
2: this.mailform_prefill
},
success_callback: $.proxy(this.reload_listing, this)
});
@@ -409,10 +401,9 @@ Identities.prototype = {
editaccount_cb: function() {
this.accountform_init();
$('.submit').one('click', $.proxy(function(e) {
$('.submit').on('click', $.proxy(function(e) {
simple_ajax_form_post(e, {
formid: "accountform",
error_cb: $.proxy(this.editaccount_cb, this),
reload_on_success: false,
success_cb: $.proxy(this.reload_listing, this)
});
@@ -425,10 +416,9 @@ Identities.prototype = {
choices: get_domains_list
});
$("#id_recipients").dynamic_input();
$(".submit").one('click', $.proxy(function(e) {
$(".submit").on('click', $.proxy(function(e) {
simple_ajax_form_post(e, {
formid: "aliasform",
error_cb: $.proxy(this.aliasform_cb, this),
reload_on_success: false,
success_cb: $.proxy(this.reload_listing, this)
});
@@ -1,5 +1,4 @@
{% extends "common/generic_modal_form.html" %}
{% load lib_tags %}
{% extends "common/generic_modal_form.html" %}{% load lib_tags %}
{% block formbody %}
{% render_field form.email %}
{% render_fields_group form "recipients" %}
@@ -1,4 +1,5 @@
{% load i18n lib_tags %}
{% load i18n lib_tags %}{% if form.fields %}
{% render_field form.create_dom_admin %}
{% render_field_appended form.dom_admin_username "@" %}
{% render_field form.create_aliases %}
{% else %}<div class="alert alert-warning">{% trans "You can't create a new domain administrator" %}</div>{% endif %}
+22 -25
View File
@@ -19,9 +19,9 @@ class AccountTestCase(ModoTestCase):
username="tester@test.com", first_name="Tester", last_name="Toto",
password1="toto", password2="toto", role="SimpleUsers",
quota_act=True,
is_active=True, email="tester@test.com", stepid=2
is_active=True, email="tester@test.com", stepid='step2'
)
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.newaccount"),
values
)
@@ -39,26 +39,26 @@ class AccountTestCase(ModoTestCase):
self.assertEqual(mb.domain.mailbox_count, 3)
values["username"] = "pouet@test.com"
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[account.id]),
values
)
mb = Mailbox.objects.get(pk=mb.id)
self.assertEqual(mb.full_address, "pouet@test.com")
self.check_ajax_get(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.delaccount",
args=[account.id]),
{}
)
def _set_quota(self, email, value, expected_status="ok"):
def _set_quota(self, email, value, expected_status=200):
account = User.objects.get(username=email)
values = {
"username": email, "role": "SimpleUsers", "quota_act": False,
"is_active": True, "quota": value, "email": email
}
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[account.id]),
values, status=expected_status
@@ -70,7 +70,7 @@ class AccountTestCase(ModoTestCase):
def test_set_nul_quota_as_domainadmin(self):
self.clt.logout()
self.assertTrue(self.clt.login(username="admin@test.com", password="toto"))
self._set_quota("user@test.com", 0, "ko")
self._set_quota("user@test.com", 0, 424)
self.clt.logout()
self.assertTrue(self.clt.login(username="admin@test2.com", password="toto"))
self._set_quota("user@test2.com", 0)
@@ -92,25 +92,24 @@ class PermissionsTestCase(ModoTestCase):
self.clt.logout()
def test_domain_admins(self):
self.values["role"] = "DomainAdmins"
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[self.user.id]),
self.values
)
self.assertEqual(self.user.group == "DomainAdmins", True)
self.assertEqual(self.user.group, "DomainAdmins")
self.values["role"] = "SimpleUsers"
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[self.user.id]),
self.values
)
self.assertEqual(self.user.group == 'DomainAdmins', False)
self.assertNotEqual(self.user.group, 'DomainAdmins')
def test_superusers(self):
self.values["role"] = "SuperAdmins"
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[self.user.id]),
self.values
@@ -118,7 +117,7 @@ class PermissionsTestCase(ModoTestCase):
self.assertEqual(User.objects.get(username="user@test.com").is_superuser, True)
self.values["role"] = "SimpleUsers"
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[self.user.id]),
self.values
@@ -135,7 +134,7 @@ class PermissionsTestCase(ModoTestCase):
password1="", password2="",
quota=10, is_active=True, email="admin@test.com"
)
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[admin.id]),
values
@@ -144,7 +143,7 @@ class PermissionsTestCase(ModoTestCase):
self.assertEqual(admin.can_access(Domain.objects.get(name="test.com")), True)
values["role"] = "SuperAdmins"
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[admin.id]),
values
@@ -178,25 +177,23 @@ class PermissionsTestCase(ModoTestCase):
username="superadmin2@test.com", first_name="Super",
last_name="Admin", password1="toto", password2="toto",
role="SuperAdmins", is_active=True,
email="superadmin2@test.com", stepid=2
email="superadmin2@test.com", stepid='step2'
)
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.newaccount"),
values
)
account = User.objects.get(username="superadmin2@test.com")
self.clt.logout()
self.clt.login(username="admin@test.com", password="toto")
self.check_ajax_get(
self.ajax_get(
reverse("modoboa.extensions.admin.views.identity.delaccount",
args=[account.id]),
{},
status="ko", respmsg="Permission denied"
{}, 403
)
self.check_ajax_get(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.delaccount",
args=[account.id]), {},
status="ko", respmsg="Permission denied"
args=[account.id]), {}, 403
)
def test_domainadmin_dlist_local_domain_not_owned(self):
@@ -208,6 +205,6 @@ class PermissionsTestCase(ModoTestCase):
recipients_1="user@test2.com",
enabled=True
)
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.newdlist"), values
)
+10 -10
View File
@@ -21,7 +21,7 @@ class AliasTestCase(ModoTestCase):
is_active=user.is_active, email="user@test.com",
aliases="toto@test.com", aliases_1="titi@test.com"
)
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values
@@ -29,7 +29,7 @@ class AliasTestCase(ModoTestCase):
self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 2)
del values["aliases_1"]
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values
@@ -42,7 +42,7 @@ class AliasTestCase(ModoTestCase):
recipients_1="admin@test.com",
recipients_2="ext@titi.com",
enabled=True)
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.newdlist"), values
)
user = User.objects.get(username="user@test.com")
@@ -53,14 +53,14 @@ class AliasTestCase(ModoTestCase):
dlist = Alias.objects.get(address="all", domain__name="test.com")
self.assertEqual(len(dlist.get_recipients()), 3)
del values["recipients_1"]
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.editalias",
args=[dlist.id]),
values
)
self.assertEqual(dlist.get_recipients_count(), 2)
self.check_ajax_get(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.delalias") + "?selection=%d" \
% dlist.id, {}
)
@@ -69,21 +69,21 @@ class AliasTestCase(ModoTestCase):
def test_forward(self):
values = dict(email="forward2@test.com", recipients="rcpt@dest.com")
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.newforward"), values
)
fwd = Alias.objects.get(address="forward2", domain__name="test.com")
self.assertEqual(fwd.get_recipients_count(), 1)
values["recipients"] = "rcpt2@dest.com"
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.editalias",
args=[fwd.id]),
values
)
self.assertEqual(fwd.get_recipients_count(), 1)
self.check_ajax_get(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.delalias") + "?selection=%d" \
% fwd.id, {}
)
@@ -92,7 +92,7 @@ class AliasTestCase(ModoTestCase):
def test_forward_and_local_copies(self):
values = dict(email="user@test.com", recipients="rcpt@dest.com")
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.newforward"), values
)
fwd = Alias.objects.get(address="user", domain__name="test.com")
@@ -100,7 +100,7 @@ class AliasTestCase(ModoTestCase):
values["recipients"] = "rcpt@dest.com"
values["recipients_1"] = "user@test.com"
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.editalias", args=[fwd.id]),
values
)
+8 -8
View File
@@ -19,9 +19,9 @@ class DomainTestCase(ModoTestCase):
"""
values = {
"name": "pouet.com", "quota": 100, "create_dom_admin": "no",
"stepid": 2
"stepid": 'step2'
}
self.check_ajax_post(reverse("modoboa.extensions.admin.views.domain.newdomain"), values)
self.ajax_post(reverse("modoboa.extensions.admin.views.domain.newdomain"), values)
dom = Domain.objects.get(name="pouet.com")
self.assertEqual(dom.name, "pouet.com")
self.assertEqual(dom.quota, 100)
@@ -35,9 +35,9 @@ class DomainTestCase(ModoTestCase):
values = {
"name": "pouet.com", "quota": 100, "create_dom_admin": "yes",
"dom_admin_username": "toto", "create_aliases": "yes",
"stepid": 2
"stepid": 'step2'
}
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.domain.newdomain"),
values
)
@@ -53,9 +53,9 @@ class DomainTestCase(ModoTestCase):
values = {
"name": "pouet.com", "quota": 0, "create_dom_admin": "yes",
"dom_admin_username": "toto", "create_aliases": "yes",
"stepid": 2
"stepid": 'step2'
}
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.domain.newdomain"),
values
)
@@ -75,7 +75,7 @@ class DomainTestCase(ModoTestCase):
"name": "pouet.com", "quota": 100, "enabled": True
}
dom = Domain.objects.get(name="test.com")
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.domain.editdomain",
args=[dom.id]),
values
@@ -88,7 +88,7 @@ class DomainTestCase(ModoTestCase):
"""Test the removal of a domain
"""
dom = Domain.objects.get(name="test.com")
self.check_ajax_get(
self.ajax_post(
reverse("modoboa.extensions.admin.views.domain.deldomain",
args=[dom.id]),
{}
@@ -31,7 +31,7 @@ class DomainAliasTestCase(ModoTestCase):
dom = Domain.objects.get(name="test.com")
values = dict(name=dom.name, quota=dom.quota, enabled=dom.enabled,
aliases="domalias.net", aliases_1="domalias.com")
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.domain.editdomain",
args=[dom.id]),
values
@@ -39,7 +39,7 @@ class DomainAliasTestCase(ModoTestCase):
self.assertEqual(dom.domainalias_set.count(), 2)
del values["aliases_1"]
self.check_ajax_post(
self.ajax_post(
reverse("modoboa.extensions.admin.views.domain.editdomain",
args=[dom.id]),
values
+34 -43
View File
@@ -6,59 +6,50 @@ from django.contrib.auth.decorators import (
login_required, permission_required
)
from modoboa.lib import events
from modoboa.lib.webutils import ajax_response, ajax_simple_response
from modoboa.lib.exceptions import PermDeniedException
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.lib.webutils import render_to_json_response
from modoboa.lib.exceptions import PermDeniedException, Conflict
from modoboa.extensions.admin.forms import AliasForm
from modoboa.extensions.admin.models import Alias
def _validate_alias(request, form, successmsg, tplname, commonctx, callback=None):
def _validate_alias(request, form, successmsg, callback=None):
"""Alias validation
Common function shared between creation and modification actions.
"""
error = None
if form.is_valid():
form.set_recipients()
try:
alias = form.save()
except IntegrityError:
raise AdminError(_("Alias with this name already exists"))
raise Conflict(_("Alias with this name already exists"))
if callback:
callback(request.user, alias)
return ajax_simple_response({"status": "ok", "respmsg": successmsg})
return render_to_json_response(successmsg)
if "targets" in request.POST:
targets = request.POST.getlist("targets")
commonctx["targets"] = targets[:-1]
commonctx["form"] = form
commonctx["error"] = error
return ajax_response(request, status="ko", template=tplname, **commonctx)
return render_to_json_response({'form_errors': form.errors}, status=400)
def _new_alias(request, title, action, successmsg,
tplname="common/generic_modal_form.html"):
tplname="admin/aliasform.html"):
events.raiseEvent("CanCreate", request.user, "mailbox_aliases")
ctx = {
"title": title,
"action": action,
"formid": "aliasform",
"action_label": _("Create"),
"action_classes": "submit"
}
if request.method == "POST":
def callback(user, alias):
alias.post_create(user)
form = AliasForm(request.user, request.POST)
return _validate_alias(
request, form, successmsg, tplname, ctx, callback
request, form, successmsg, callback
)
form = AliasForm(request.user)
ctx["form"] = form
ctx = {
"title": title,
"action": action,
"formid": "aliasform",
"action_label": _("Create"),
"action_classes": "submit",
"form": AliasForm(request.user)
}
return render(request, tplname, ctx)
@@ -94,29 +85,29 @@ def newforward(request):
@login_required
@permission_required("admin.change_alias")
def editalias(request, alid, tplname="common/generic_modal_form.html"):
@transaction.commit_on_success
def editalias(request, alid, tplname="admin/aliasform.html"):
alias = Alias.objects.get(pk=alid)
if not request.user.can_access(alias):
raise PermDeniedException
ctx = dict(
action=reverse(editalias, args=[alias.id]),
formid="aliasform",
title=alias.full_address,
action_label=_("Update"),
action_classes="submit"
)
if len(alias.get_recipients()) >= 2:
successmsg = _("Distribution list modified")
elif alias.extmboxes != "":
successmsg = _("Forward modified")
else:
successmsg = _("Alias modified")
if request.method == "POST":
if len(alias.get_recipients()) >= 2:
successmsg = _("Distribution list modified")
elif alias.extmboxes != "":
successmsg = _("Forward modified")
else:
successmsg = _("Alias modified")
form = AliasForm(request.user, request.POST, instance=alias)
return _validate_alias(request, form, successmsg, tplname, ctx)
return _validate_alias(request, form, successmsg)
form = AliasForm(request.user, instance=alias)
ctx["form"] = form
ctx = {
'action': reverse(editalias, args=[alias.id]),
'formid': 'aliasform',
'title': alias.full_address,
'action_label': _('Update'),
'action_classes': 'submit',
'form': AliasForm(request.user, instance=alias)
}
return render(request, tplname, ctx)
@@ -141,4 +132,4 @@ def delalias(request):
alias.delete()
msg = ungettext(msg, msgs, len(selection))
return ajax_simple_response({"status": "ok", "respmsg": msg})
return render_to_json_response(msg)
+61 -73
View File
@@ -2,7 +2,6 @@ from django.http import HttpResponseRedirect
from django.utils.translation import ugettext as _, ungettext
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Q
from django.shortcuts import render
from django.views.decorators.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import (
@@ -10,13 +9,15 @@ from django.contrib.auth.decorators import (
)
from modoboa.lib import parameters, events
from modoboa.lib.webutils import (
ajax_simple_response, _render_to_string
ajax_simple_response, _render_to_string,
render_to_json_response
)
from modoboa.lib.formutils import CreationWizard
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.exceptions import (
ModoboaException, PermDeniedException, BadRequest
)
from modoboa.lib.templatetags.lib_tags import pagination_bar
from modoboa.extensions.admin.lib import get_sort_order, get_listing_page
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import Domain, Mailbox
from modoboa.extensions.admin.forms import (
DomainForm, DomainFormGeneral, DomainFormOptions
@@ -90,50 +91,49 @@ def domains_list(request):
def newdomain(request, tplname="common/wizard_forms.html"):
events.raiseEvent("CanCreate", request.user, "domains")
def newdomain_cb(steps):
genform = steps[0]["form"]
genform.is_valid()
domain = genform.save(request.user)
domain.post_create(request.user)
steps[1]["form"].save(request.user, domain)
commonctx = {"title": _("New domain"),
"action_label": _("Create"),
"action_classes": "submit",
"action": reverse(newdomain),
"formid": "domform"}
cwizard = CreationWizard(newdomain_cb)
cwizard = CreationWizard()
cwizard.add_step(DomainFormGeneral, _("General"),
[dict(classes="btn-inverse next", label=_("Next"))],
formtpl="admin/domain_general_form.html")
cwizard.add_step(DomainFormOptions, _("Options"),
[dict(classes="btn-primary submit", label=_("Create")),
dict(classes="btn-inverse prev", label=_("Previous"))],
formtpl="admin/domain_options_form.html")
cwizard.add_step(
DomainFormOptions, _("Options"),
[dict(classes="btn-primary submit", label=_("Create")),
dict(classes="btn-inverse prev", label=_("Previous"))],
formtpl="admin/domain_options_form.html",
new_args=[request.user]
)
if request.method == "POST":
retcode, data = cwizard.validate_step(request)
if retcode == -1:
raise AdminError(data)
raise BadRequest(data)
if retcode == 1:
return ajax_simple_response(dict(
status="ok", title=cwizard.get_title(data + 1), stepid=data
))
if retcode == 2:
return ajax_simple_response(
dict(status="ok", respmsg=_("Domain created"))
return render_to_json_response(
{'title': cwizard.get_title(data + 1), 'stepid': data}
)
if retcode == 2:
genform = cwizard.steps[0]["form"]
domain = genform.save(request.user)
domain.post_create(request.user)
try:
cwizard.steps[1]["form"].save(request.user, domain)
except ModoboaException as e:
transaction.rollback()
raise
return render_to_json_response(_("Domain created"))
return render_to_json_response({
'stepid': data, 'form_errors': cwizard.errors
}, status=400)
from modoboa.lib.templatetags.lib_tags import render_form
return ajax_simple_response(dict(
status="ko", stepid=data,
form=render_form(cwizard.steps[data]["form"])
))
ctx = {"title": _("New domain"),
"action_label": _("Create"),
"action_classes": "submit",
"action": reverse(newdomain),
"formid": "domform"}
cwizard.create_forms()
commonctx.update(steps=cwizard.steps)
commonctx.update(subtitle="1. %s" % cwizard.steps[0]['title'])
return render(request, tplname, commonctx)
ctx.update(steps=cwizard.steps)
ctx.update(subtitle="1. %s" % cwizard.steps[0]['title'])
return render(request, tplname, ctx)
@login_required
@@ -144,44 +144,32 @@ def editdomain(request, dom_id, tplname="admin/editdomainform.html"):
if not request.user.can_access(domain):
raise PermDeniedException
instances = dict(general=domain)
events.raiseEvent("FillDomainInstances", request.user, domain, instances)
if request.method == "POST":
domain.oldname = domain.name
form = DomainForm(request.user, request.POST, instances=instances)
if form.is_valid():
form.save(request.user)
events.raiseEvent("DomainModified", domain)
return render_to_json_response(_("Domain modified"))
return render_to_json_response({
'form_errors': form.errors
}, status=400)
domadmins = [u for u in domain.admins
if request.user.can_access(u) and not u.is_superuser]
if not request.user.is_superuser:
domadmins = [u for u in domadmins if u.group == "DomainAdmins"]
instances = dict(general=domain)
events.raiseEvent("FillDomainInstances", request.user, domain, instances)
commonctx = {"title": domain.name,
"action_label": _("Update"),
"action_classes": "submit",
"action": reverse(editdomain, args=[dom_id]),
"formid": "domform",
"domain": domain}
if request.method == "POST":
error = None
domain.oldname = domain.name
form = DomainForm(request.user, request.POST, instances=instances)
if form.is_valid():
try:
form.save(request.user)
except AdminError, e:
error = str(e)
else:
events.raiseEvent("DomainModified", domain)
return ajax_simple_response({
"status": "ok", "respmsg": _("Domain modified")
})
commonctx["tabs"] = form
return ajax_simple_response({
"status": "ko", "respmsg": error,
"content": _render_to_string(request, tplname, commonctx)
})
commonctx["tabs"] = DomainForm(request.user, instances=instances)
commonctx["domadmins"] = domadmins
return render(request, tplname, commonctx)
ctx = {"title": domain.name,
"action_label": _("Update"),
"action_classes": "submit",
"action": reverse(editdomain, args=[dom_id]),
"formid": "domform",
"domain": domain,
"tabs": DomainForm(request.user, instances=instances),
"domadmins": domadmins}
return render(request, tplname, ctx)
@login_required
@@ -198,8 +186,8 @@ def deldomain(request, dom_id):
if not request.user.can_access(dom):
raise PermDeniedException
if mb and mb.domain == dom:
raise AdminError(_("You can't delete your own domain"))
raise PermDeniedException(_("You can't delete your own domain"))
dom.delete(request.user, keepdir)
msg = ungettext("Domain deleted", "Domains deleted", 1)
return ajax_simple_response({"status": "ok", "respmsg": msg})
return render_to_json_response(msg)
+48 -68
View File
@@ -7,9 +7,12 @@ from django.contrib.auth.decorators import (
)
from django.views.decorators.csrf import ensure_csrf_cookie
from modoboa.lib import parameters, events
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.exceptions import (
ModoboaException, PermDeniedException, BadRequest
)
from modoboa.lib.webutils import (
ajax_simple_response, _render_to_string, ajax_response
ajax_simple_response, _render_to_string,
render_to_json_response
)
from modoboa.lib.formutils import CreationWizard
from modoboa.lib.templatetags.lib_tags import pagination_bar
@@ -18,7 +21,6 @@ from modoboa.extensions.admin.models import Mailbox, Domain
from modoboa.extensions.admin.lib import (
get_sort_order, get_listing_page, get_identities
)
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.forms import (
AccountForm, AccountFormGeneral, AccountFormMail
)
@@ -99,7 +101,7 @@ def list_quotas(request, tplname="admin/quotas.html"):
order_by=["%s%s" % (sort_dir, sort_order)]
)
else:
raise AdminError(_("Invalid request"))
raise BadRequest(_("Invalid request"))
page = get_listing_page(mboxes, request.GET.get("page", 1))
return ajax_simple_response({
"status": "ok",
@@ -115,35 +117,7 @@ def list_quotas(request, tplname="admin/quotas.html"):
@permission_required("core.add_user")
@transaction.commit_on_success
def newaccount(request, tplname='common/wizard_forms.html'):
def create_account(steps):
"""Account creation callback
Called when all creation steps have been validated.
:param steps: the steps data
"""
genform = steps[0]["form"]
genform.is_valid()
account = genform.save()
account.post_create(request.user)
mailform = steps[1]["form"]
try:
mailform.save(request.user, account)
except AdminError:
# A bit uggly: transaction management doesn't work very
# well with nested functions. Need to wait for django 1.6
# and atomicity.
account.delete(request.user, False)
raise
ctx = dict(
title=_("New account"),
action=reverse(newaccount),
formid="newaccount_form",
submit_label=_("Create")
)
cwizard = CreationWizard(create_account)
cwizard = CreationWizard()
cwizard.add_step(AccountFormGeneral, _("General"),
[dict(classes="btn-inverse next", label=_("Next"))],
new_args=[request.user])
@@ -155,22 +129,35 @@ def newaccount(request, tplname='common/wizard_forms.html'):
if request.method == "POST":
retcode, data = cwizard.validate_step(request)
if retcode == -1:
raise AdminError(data)
raise BadRequest(data)
if retcode == 1:
return ajax_simple_response(dict(
status="ok", title=cwizard.get_title(data + 1), stepid=data
))
return render_to_json_response(
{'title': cwizard.get_title(data + 1), 'stepid': data}
)
if retcode == 2:
return ajax_simple_response(dict(
status="ok", respmsg=_("Account created")
))
from modoboa.lib.templatetags.lib_tags import render_form
return ajax_simple_response(dict(
status="ko", stepid=data,
form=render_form(cwizard.steps[data]["form"])
))
genform = cwizard.steps[0]["form"]
account = genform.save()
try:
account.post_create(request.user)
mailform = cwizard.steps[1]["form"]
mailform.save(request.user, account)
except ModoboaException:
# A bit uggly: transaction management doesn't work very
# well with nested functions. Need to wait for django 1.6
# and atomicity.
account.delete(request.user, False)
raise
return render_to_json_response(_("Account created"))
return render_to_json_response({
'stepid': data, 'form_errors': cwizard.errors
}, status=400)
ctx = {
'title': _("New account"),
'action': reverse(newaccount),
'formid': 'newaccount_form',
'submit_label': _("Create")
}
cwizard.create_forms()
ctx.update(steps=cwizard.steps)
ctx.update(subtitle="1. %s" % cwizard.steps[0]['title'])
@@ -191,14 +178,6 @@ def editaccount(request, accountid, tplname="common/tabforms.html"):
instances = dict(general=account, mail=mb, perms=account)
events.raiseEvent("FillAccountInstances", request.user, account, instances)
ctx = dict(
title=account.username,
formid="accountform",
action=reverse(editaccount, args=[accountid]),
action_label=_("Update"),
action_classes="submit"
)
if request.method == "POST":
classes = {}
form = AccountForm(request.user, request.POST,
@@ -209,15 +188,17 @@ def editaccount(request, accountid, tplname="common/tabforms.html"):
if form.is_valid(optional_only=True):
events.raiseEvent("AccountModified", account, form.account)
form.save()
return ajax_simple_response(
dict(status="ok", respmsg=_("Account updated"))
)
transaction.rollback()
return render_to_json_response(_("Account updated"))
return render_to_json_response({'form_errors': form.errors}, status=400)
ctx["tabs"] = form
return ajax_response(request, status="ko", template=tplname, **ctx)
ctx["tabs"] = AccountForm(request.user, instances=instances)
ctx = {
'title': account.username,
'formid': 'accountform',
'action': reverse(editaccount, args=[accountid]),
'action_label': _('Update'),
'action_classes': 'submit',
'tabs': AccountForm(request.user, instances=instances)
}
active_tab_id = request.GET.get("active_tab", "default")
if active_tab_id != "default":
ctx["tabs"].active_id = active_tab_id
@@ -229,11 +210,10 @@ def editaccount(request, accountid, tplname="common/tabforms.html"):
@transaction.commit_on_success
def delaccount(request, accountid):
keepdir = True if request.POST.get("keepdir", "false") == "true" else False
User.objects.get(pk=accountid).delete(request.user, keep_mb_dir=keepdir)
msg = ungettext("Account deleted", "Accounts deleted", 1)
return ajax_simple_response({"status": "ok", "respmsg": msg})
return render_to_json_response(
ungettext("Account deleted", "Accounts deleted", 1)
)
@login_required
@@ -242,12 +222,12 @@ def remove_permission(request):
domid = request.GET.get("domid", None)
daid = request.GET.get("daid", None)
if domid is None or daid is None:
raise AdminError(_("Invalid request"))
raise BadRequest(_("Invalid request"))
try:
account = User.objects.get(pk=daid)
domain = Domain.objects.get(pk=domid)
except (User.DoesNotExist, Domain.DoesNotExist):
raise AdminError(_("Invalid request"))
raise BadRequest(_("Invalid request"))
if not request.user.can_access(account) or not request.user.can_access(domain):
raise PermDeniedException
domain.remove_admin(account)
+19 -31
View File
@@ -2,10 +2,9 @@ from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _
from modoboa.lib.exceptions import ModoboaException
from modoboa.lib.webutils import (
ajax_response, ajax_simple_response, _render_to_string
render_to_json_response, _render_to_string
)
from modoboa.extensions.admin.lib import needs_mailbox
from modoboa.extensions.admin.exceptions import BadDestination
from modoboa.extensions.admin.models import Alias, Mailbox
from modoboa.extensions.admin.forms import ForwardForm
@@ -13,12 +12,7 @@ from modoboa.extensions.admin.forms import ForwardForm
@login_required
@needs_mailbox()
def forward(request, tplname='admin/forward.html'):
try:
mb = request.user.mailbox_set.all()[0]
except IndexError:
raise ModoboaException(
_("You need a mailbox in order to define a forward")
)
mb = request.user.mailbox_set.all()[0]
try:
al = Alias.objects.get(address=mb.address,
domain__name=mb.domain.name)
@@ -28,28 +22,23 @@ def forward(request, tplname='admin/forward.html'):
form = ForwardForm(request.POST)
error = None
if form.is_valid():
try:
if al is None:
al = Alias()
al.address = mb.address
al.domain = mb.domain
al.enabled = mb.user.is_active
intdests = []
if form.cleaned_data["keepcopies"]:
intdests += [mb]
form.parse_dest()
al.save(int_rcpts=intdests, ext_rcpts=form.dests)
if request.user.group != "SimpleUsers":
al.post_create(request.user)
return ajax_response(request, respmsg=_("Forward updated"))
except BadDestination, e:
error = str(e)
if al is None:
al = Alias()
al.address = mb.address
al.domain = mb.domain
al.enabled = mb.user.is_active
intdests = []
if form.cleaned_data["keepcopies"]:
intdests += [mb]
form.parse_dest()
al.save(int_rcpts=intdests, ext_rcpts=form.dests)
if request.user.group != "SimpleUsers":
al.post_create(request.user)
return render_to_json_response(_("Forward updated"))
return ajax_simple_response(dict(
status="ko",
errors=form.errors,
respmsg=error
))
return render_to_json_response(
{'form_errors': form.errors}, status=400
)
form = ForwardForm()
if al is not None:
@@ -60,8 +49,7 @@ def forward(request, tplname='admin/forward.html'):
pass
else:
form.fields["keepcopies"].initial = True
return ajax_simple_response({
"status": "ok",
return render_to_json_response({
"content": _render_to_string(request, tplname, {
"form": form
})
+18 -4
View File
@@ -10,16 +10,22 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events, parameters
from modoboa.core.extensions import ModoExtension, exts_pool
from .models import LimitTemplates
from .forms import ResourcePoolForm
import controls
levents = [
'GetExtraLimitTemplates'
]
class Limits(ModoExtension):
name = "limits"
label = "Limits"
version = "1.0"
description = ugettext_lazy(
"Per administrator resources to limit the number of objects they can create"
"Per administrator resources to limit the number of objects "
"they can create"
)
def init(self):
@@ -48,6 +54,7 @@ class Limits(ModoExtension):
def load(self):
from modoboa.extensions.limits.app_settings import ParametersForm
parameters.register(ParametersForm, ugettext_lazy("Limits"))
events.declare(levents)
def destroy(self):
parameters.unregister()
@@ -74,7 +81,13 @@ def display_pool_usage(user, target, currentpage):
if user.has_perm("admin.add_domain"):
names += ["domain_admins_limit"]
else:
names = ["domains_limit", "domain_aliases_limit"]
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)
]
limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0)
if len(limits) == 0:
return []
@@ -85,7 +98,8 @@ def display_pool_usage(user, target, currentpage):
def extra_account_form(user, account=None):
if not user.group in ["SuperAdmins", "Resellers"]:
return []
if account is not None and not account.group in ["Resellers", "DomainAdmins"]:
if account is not None and \
not account.group in ["Resellers", "DomainAdmins"]:
return []
return [
@@ -126,7 +140,7 @@ def get_static_content(caller, user):
margin: 5px 0;
}
.resource .progress {
.resource .progress {
margin-bottom: 0px;
}
@@ -39,3 +39,7 @@ class ParametersForm(AdminParametersForm):
help_text=_("Maximum number of allowed aliases for a new administrator"),
widget=forms.widgets.TextInput(attrs={"class": "span1"})
)
def __init__(self, *args, **kwargs):
super(AdminParametersForm, self).__init__(*args, **kwargs)
self._load_extra_parameters('A')
+44 -41
View File
@@ -6,43 +6,27 @@
"""
from modoboa.lib import events
from modoboa.lib.permissions import get_object_owner
from .lib import LimitReached
from .models import limits_tpl, LimitsPool
from .lib import LimitReached, inc_limit_usage, dec_limit_usage
from .models import LimitTemplates, LimitsPool
def check_limit(user, lname):
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.
Raise a ``LimitReached`` exception if the limit is reached.
: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):
if user.limitspool.will_be_reached(lname, count):
raise LimitReached(user.limitspool.get_limit(lname))
except LimitsPool.DoesNotExist:
pass
def inc_limit(user, lname):
try:
user.limitspool.inc_curvalue(lname)
except LimitsPool.DoesNotExist:
pass
def dec_limit(user, lname):
if user is None:
return
try:
user.limitspool.dec_curvalue(lname)
except LimitsPool.DoesNotExist:
pass
def move_pool_resource(owner, user):
"""Move resource from one pool to another
@@ -54,7 +38,7 @@ def move_pool_resource(owner, user):
except LimitsPool.DoesNotExist:
return
if not owner.is_superuser:
for ltpl in limits_tpl:
for ltpl in LimitTemplates().templates:
l = user.limitspool.get_limit(ltpl[0])
if l.maxvalue < 0:
continue
@@ -68,13 +52,13 @@ def move_pool_resource(owner, user):
@events.observe('DomainCreated')
def inc_nb_domains(user, domain):
inc_limit(user, 'domains_limit')
inc_limit_usage(user, 'domains_limit')
@events.observe('DomainDeleted')
def dec_nb_domains(domain):
owner = get_object_owner(domain)
dec_limit(owner, 'domains_limit')
dec_limit_usage(owner, 'domains_limit')
for domalias in domain.domainalias_set.all():
dec_nb_domaliases(domalias)
for mailbox in domain.mailbox_set.all():
@@ -85,7 +69,7 @@ def dec_nb_domains(domain):
@events.observe('DomainAliasCreated')
def inc_nb_domaliases(user, domalias):
inc_limit(user, 'domain_aliases_limit')
inc_limit_usage(user, 'domain_aliases_limit')
@events.observe('DomainAliasDeleted')
@@ -96,12 +80,12 @@ def dec_nb_domaliases(domainaliases):
domainaliases = [domainaliases]
for domainalias in domainaliases:
owner = get_object_owner(domainalias)
dec_limit(owner, 'domain_aliases_limit')
dec_limit_usage(owner, 'domain_aliases_limit')
@events.observe('MailboxCreated')
def inc_nb_mailboxes(user, mailbox):
inc_limit(user, 'mailboxes_limit')
inc_limit_usage(user, 'mailboxes_limit')
@events.observe('MailboxDeleted')
@@ -112,23 +96,23 @@ def dec_nb_mailboxes(mailboxes):
mailboxes = [mailboxes]
for mailbox in mailboxes:
owner = get_object_owner(mailbox)
dec_limit(owner, 'mailboxes_limit')
dec_limit_usage(owner, 'mailboxes_limit')
@events.observe('MailboxAliasCreated')
def inc_nb_mbaliases(user, mailboxalias):
inc_limit(user, 'mailbox_aliases_limit')
inc_limit_usage(user, 'mailbox_aliases_limit')
@events.observe('MailboxAliasDeleted')
def dec_nb_mbaliases(mailboxalias):
owner = get_object_owner(mailboxalias)
dec_limit(owner, 'mailbox_aliases_limit')
dec_limit_usage(owner, 'mailbox_aliases_limit')
@events.observe('CanCreate')
def can_create_new_object(user, objtype):
check_limit(user, '%s_limit' % objtype)
def can_create_new_object(user, objtype, count=1):
check_limit(user, '%s_limit' % objtype, count)
@events.observe("AccountCreated")
@@ -140,12 +124,31 @@ def create_pool(user):
if user.belongs_to_group("DomainAdmins"):
check_limit(owner, 'domain_admins_limit')
inc_limit(owner, 'domain_admins_limit')
inc_limit_usage(owner, 'domain_admins_limit')
if user.group in ["DomainAdmins", "Resellers"]:
p = LimitsPool(user=user)
p.save()
p.create_limits()
p, created = LimitsPool.objects.get_or_create(user=user)
p.create_limits(owner)
@events.observe("UserCanSetRole")
def user_can_set_role(user, role):
"""Check if the user can still set this role.
The only interesting case concerns resellers defining new domain
administrators. We want to check if they are allowed to do this
operation before any modification is made to :keyword:`account`.
:param ``User`` user: connected user
:param str newrole: role to check
"""
if role == 'DomainAdmins':
lname = 'domain_admins_limit'
else:
return [True]
if user.is_superuser or not user.limitspool.will_be_reached(lname):
return [True]
return [False]
@events.observe("AccountModified")
@@ -164,19 +167,19 @@ def on_account_modified(old, new):
except LimitsPool.DoesNotExist:
p = LimitsPool(user=new)
p.save()
p.create_limits()
p.create_limits(owner)
if not new.group in ["DomainAdmins", "Resellers"]:
move_pool_resource(owner, new)
if old.oldgroup == "DomainAdmins":
if new.group != "DomainAdmins":
dec_limit(owner, 'domain_admins_limit')
dec_limit_usage(owner, 'domain_admins_limit')
return
if new.group == "DomainAdmins":
check_limit(owner, 'domain_admins_limit')
inc_limit(owner, 'domain_admins_limit')
inc_limit_usage(owner, 'domain_admins_limit')
@events.observe("AccountDeleted")
@@ -188,4 +191,4 @@ def on_account_deleted(account, byuser, **kwargs):
move_pool_resource(owner, account)
if account.group == "DomainAdmins":
dec_limit(owner, 'domain_admins_limit')
dec_limit_usage(owner, 'domain_admins_limit')
+27 -44
View File
@@ -1,44 +1,26 @@
# coding: utf-8
from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import parameters
from .models import limits_tpl
from .models import LimitTemplates
from .lib import BadLimitValue, UnsufficientResource
class ResourcePoolForm(forms.Form):
domain_admins_limit = forms.IntegerField(
label=ugettext_lazy("Domain admins"),
help_text=ugettext_lazy("Maximum number of domain administrators this user can create")
)
domains_limit = forms.IntegerField(
label=ugettext_lazy("Domains"),
help_text=ugettext_lazy("Maximum number of domains this user can create")
)
domain_aliases_limit = forms.IntegerField(
label=ugettext_lazy("Domain aliases"),
help_text=ugettext_lazy("Maximum number of domain aliases this user can create")
)
mailboxes_limit = forms.IntegerField(
label=ugettext_lazy("Mailboxes"),
help_text=ugettext_lazy("Maximum number of mailboxes this user can create")
)
mailbox_aliases_limit = forms.IntegerField(
label=ugettext_lazy("Mailbox aliases"),
help_text=ugettext_lazy("Maximum number of mailbox aliases this user can create")
)
def __init__(self, *args, **kwargs):
self.account = None
if "instance" in kwargs:
self.account = kwargs["instance"]
del kwargs["instance"]
super(ResourcePoolForm, self).__init__(*args, **kwargs)
for tpl in LimitTemplates().templates:
if len(tpl) > 3 and self.account is not None and self.account.group != tpl[3]:
continue
self.fields[tpl[0]] = forms.IntegerField(
label=tpl[1], help_text=tpl[2]
)
if hasattr(self, "account"):
if self.account.group != "Resellers":
del self.fields["domain_admins_limit"]
del self.fields["domains_limit"]
del self.fields["domain_aliases_limit"]
self.load_from_user(self.account)
def check_limit_value(self, lname):
@@ -46,29 +28,30 @@ class ResourcePoolForm(forms.Form):
raise forms.ValidationError(_("Invalid limit"))
return self.cleaned_data[lname]
def clean_domains_limit(self):
return self.check_limit_value("domains_limit")
def clean_domain_aliases_limit(self):
return self.check_limit_value("domain_aliases_limit")
def clean_mailboxes_limit(self):
return self.check_limit_value("mailboxes_limit")
def clean_mailbox_aliases_limit(self):
return self.check_limit_value("mailbox_aliases_limit")
def clean(self):
cleaned_data = super(ResourcePoolForm, self).clean()
for lname in self.fields.keys():
if not lname in self._errors and cleaned_data[lname] < -1:
self._errors[lname] = self.error_class([_('Invalid limit')])
del cleaned_data[lname]
return cleaned_data
def load_from_user(self, user):
for l in limits_tpl:
if not l[0] in self.fields:
continue
self.fields[l[0]].initial = user.limitspool.getmaxvalue(l[0])
for lname in self.fields.keys():
self.fields[lname].initial = user.limitspool.getmaxvalue(lname)
# The following lines will become useless in a near
# future.
if self.fields[l[0]].initial == -2:
self.fields[l[0]].initial = parameters.get_admin("DEFLT_%s" % l[0].upper())
if self.fields[lname].initial == -2:
self.fields[lname].initial = parameters.get_admin("DEFLT_%s" % lname.upper())
def allocate_from_pool(self, limit, pool):
"""Allocate resource using an existing pool.
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:
raise BadLimitValue(_("Your resources are not initialized yet"))
@@ -92,7 +75,7 @@ class ResourcePoolForm(forms.Form):
from modoboa.lib.permissions import get_object_owner
owner = get_object_owner(self.account)
for ltpl in limits_tpl:
for ltpl in LimitTemplates().templates:
if not ltpl[0] in self.cleaned_data:
continue
l = self.account.limitspool.limit_set.get(name=ltpl[0])
+24
View File
@@ -6,9 +6,12 @@
"""
from django.utils.translation import ugettext as _
from modoboa.lib.exceptions import ModoboaException
from .models import LimitsPool
class LimitReached(ModoboaException):
http_code = 403
def __init__(self, limit):
self.limit = limit
@@ -17,6 +20,8 @@ class LimitReached(ModoboaException):
class UnsufficientResource(ModoboaException):
http_code = 424
def __init__(self, limit):
self.limit = limit
@@ -26,3 +31,22 @@ class UnsufficientResource(ModoboaException):
class BadLimitValue(ModoboaException):
pass
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
+46 -15
View File
@@ -2,27 +2,53 @@
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import parameters
from modoboa.lib.singleton import Singleton
from django.conf import settings
from modoboa.lib import events
limits_tpl = [
("domain_admins_limit", ugettext_lazy("Domain admins")),
("domains_limit", ugettext_lazy("Domains")),
("domain_aliases_limit", ugettext_lazy("Domain aliases")),
("mailboxes_limit", ugettext_lazy("Mailboxes")),
("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases"))
]
class LimitTemplates(Singleton):
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"))
]
self.templates += events.raiseQueryEvent('GetExtraLimitTemplates')
class LimitsPool(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
def create_limits(self):
for ltpl in limits_tpl:
l = Limit()
l.name = ltpl[0]
l.pool = self
l.maxvalue = int(parameters.get_admin("DEFLT_%s" % ltpl[0].upper()))
l.save()
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)
@@ -32,6 +58,11 @@ class LimitsPool(models.Model):
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
@@ -92,7 +123,7 @@ class Limit(models.Model):
@property
def label(self):
for l in limits_tpl:
for l in LimitTemplates().templates:
if l[0] == self.name:
return l[1]
return ""
+340 -6
View File
@@ -1,8 +1,13 @@
from django.core.urlresolvers import reverse
from modoboa.lib.tests import ExtTestCase
from modoboa.core.factories import UserFactory
from modoboa.extensions.admin.factories import DomainFactory, MailboxFactory
from modoboa.lib import parameters
from modoboa.core.models import User
from modoboa.core.factories import UserFactory
from modoboa.extensions.admin.factories import (
DomainFactory, MailboxFactory, populate_database
)
from modoboa.extensions.admin.models import Alias, Domain, DomainAlias
from modoboa.extensions.limits.models import LimitTemplates
class PermissionsTestCase(ExtTestCase):
@@ -26,13 +31,342 @@ class PermissionsTestCase(ExtTestCase):
values = dict(
username="reseller@test.com", first_name="Reseller", last_name="",
password1="toto", password2="toto", role="Resellers",
is_active=True, email="reseller@test.com", stepid=2
is_active=True, email="reseller@test.com", stepid='step2'
)
self.check_ajax_post(reverse("modoboa.extensions.admin.views.identity.newaccount"), values)
self.ajax_post(reverse("modoboa.extensions.admin.views.identity.newaccount"), values)
account = User.objects.get(username="reseller@test.com")
self.clt.logout()
self.clt.login(username="admin@test.com", password="toto")
self.check_ajax_get(
resp = self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.delaccount", args=[account.id]),
{}, status="ko", respmsg="Permission denied"
{}, status=403
)
self.assertEqual(resp, "Permission denied")
class ResourceTestCase(ExtTestCase):
fixtures = ["initial_users.json"]
name = "limits"
def setUp(self):
super(ResourceTestCase, self).setUp()
populate_database()
for tpl in LimitTemplates().templates:
parameters.save_admin('DEFLT_%s' % tpl[0].upper(), 2, app='limits')
def _create_account(self, username, role='SimpleUsers', status=200):
values = dict(
username=username, first_name="Tester", last_name="Toto",
password1="toto", password2="toto", role=role,
quota_act=True,
is_active=True, email=username, stepid='step2',
)
return self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.newaccount"),
values, status
)
def _create_alias(self, email, rcpt='user@test.com', status=200):
values = dict(
email=email, recipients=rcpt, enabled=True
)
return self.ajax_post(
reverse("modoboa.extensions.admin.views.alias.newalias"),
values, status
)
def _create_domain(self, name, status=200, withtpl=False):
values = {
"name": name, "quota": 100, "create_dom_admin": "no",
"create_aliases": "no", "stepid": 'step2'
}
if withtpl:
values['create_dom_admin'] = 'yes'
values['dom_admin_username'] = 'admin'
values['create_aliases'] = 'yes'
return self.ajax_post(
reverse("modoboa.extensions.admin.views.domain.newdomain"),
values, status
)
def _domain_alias_operation(self, optype, domain, name, status=200):
dom = Domain.objects.get(name=domain)
values = {
'name': dom.name, 'quota': dom.quota, 'enabled': dom.enabled,
}
aliases = [alias.name for alias in dom.domainalias_set.all()]
if optype == 'add':
aliases.append(name)
else:
aliases.remove(name)
for cpt, alias in enumerate(aliases):
fname = 'aliases' if not cpt else 'aliases_%d' % cpt
values[fname] = alias
self.ajax_post(
reverse("modoboa.extensions.admin.views.domain.editdomain",
args=[dom.id]),
values, status
)
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)
class DomainAdminTestCase(ResourceTestCase):
def setUp(self):
super(DomainAdminTestCase, self).setUp()
self.user = User.objects.get(username='admin@test.com')
self.user.limitspool.set_maxvalue('mailboxes_limit', 2)
self.user.limitspool.set_maxvalue('mailbox_aliases_limit', 2)
self.clt.logout()
self.clt.login(username='admin@test.com', password='toto')
def test_mailboxes_limit(self):
self._create_account('tester1@test.com')
self._check_limit('mailboxes', 1, 2)
self._create_account('tester2@test.com')
self._check_limit('mailboxes', 2, 2)
resp = self._create_account('tester3@test.com', status=403)
self._check_limit('mailboxes', 2, 2)
self.ajax_post(
reverse('modoboa.extensions.admin.views.identity.delaccount',
args=[User.objects.get(username='tester2@test.com').id]),
{}
)
self._check_limit('mailboxes', 1, 2)
def test_aliases_limit(self):
self._create_alias('alias1@test.com')
self._check_limit('mailbox_aliases', 1, 2)
self._create_alias('alias2@test.com')
self._check_limit('mailbox_aliases', 2, 2)
resp = self._create_alias('alias3@test.com', status=403)
self._check_limit('mailbox_aliases', 2, 2)
self.ajax_post(
reverse('modoboa.extensions.admin.views.alias.delalias') + '?selection=%d' \
% Alias.objects.get(address='alias2', domain__name='test.com').id,
{}
)
self._check_limit('mailbox_aliases', 1, 2)
def test_aliases_limit_through_account_form(self):
user = User.objects.get(username='user@test.com')
values = dict(
username=user.username, role=user.group,
is_active=user.is_active, email=user.email, quota_act=True,
aliases="alias1@test.com", aliases_1="alias2@test.com"
)
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values
)
Alias.objects.get(address='alias1', domain__name='test.com')
self._check_limit('mailbox_aliases', 2, 2)
class ResellerTestCase(ResourceTestCase):
def setUp(self):
super(ResellerTestCase, self).setUp()
self.user = UserFactory.create(
username='reseller', groups=('Resellers',)
)
self.clt.logout()
self.clt.login(username='reseller', password='toto')
def test_domains_limit(self):
self._create_domain('domain1.tld')
self._check_limit('domains', 1, 2)
self._create_domain('domain2.tld')
self._check_limit('domains', 2, 2)
resp = self._create_domain('domain3.tld', 403)
self._check_limit('domains', 2, 2)
self.ajax_post(
reverse('modoboa.extensions.admin.views.domain.deldomain',
args=[Domain.objects.get(name='domain2.tld').id]),
{}
)
self._check_limit('domains', 1, 2)
def test_domain_aliases_limit(self):
self._create_domain('pouet.com')
self._domain_alias_operation('add', 'pouet.com', 'domain_alias1.tld')
self._check_limit('domain_aliases', 1, 2)
self._domain_alias_operation('add', 'pouet.com', 'domain_alias2.tld')
self._check_limit('domain_aliases', 2, 2)
resp = self._domain_alias_operation(
'add', 'pouet.com', 'domain_alias3.tld', 403
)
self._check_limit('domain_aliases', 2, 2)
self._domain_alias_operation('delete', 'pouet.com', 'domain_alias2.tld')
self._check_limit('domain_aliases', 1, 2)
def test_domain_admins_limit(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
self._check_limit('domain_admins', 1, 2)
self._create_account('admin2@domain.tld', role='DomainAdmins')
self._check_limit('domain_admins', 2, 2)
resp = self._create_account('admin3@domain.tld', role='DomainAdmins', status=400)
self.assertEqual(
resp['form_errors']['role'][0],
'Select a valid choice. DomainAdmins is not one of the available choices.'
)
self._check_limit('domain_admins', 2, 2)
self.user.limitspool.set_maxvalue('mailboxes_limit', 3)
self._create_account('user1@domain.tld')
user = User.objects.get(username='user1@domain.tld')
values = {
'username': user.username, 'role': 'DomainAdmins', 'quota_act': True,
'is_active': user.is_active, 'email': user.email
}
resp = self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values, status=400
)
self.assertEqual(
resp['form_errors']['role'][0],
'Select a valid choice. DomainAdmins is not one of the available choices.'
)
self._check_limit('domain_admins', 2, 2)
def test_domain_admin_resource_are_empty(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
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
)
def test_domain_admins_limit_from_domain_tpl(self):
self.user.limitspool.set_maxvalue('domains_limit', 3)
self._create_domain('domain1.tld', withtpl=True)
self._create_domain('domain2.tld', withtpl=True)
self._check_limit('domain_admins', 2, 2)
self._check_limit('domains', 2, 3)
resp = self._create_domain('domain3.tld', status=200, withtpl=True)
self._check_limit('domain_admins', 2, 2)
self._check_limit('domains', 3, 3)
def test_allocate_from_pool(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
user = User.objects.get(username='admin1@domain.tld')
# Give 1 mailbox and 2 aliases to the admin -> should work
values = {
'username': user.username, 'role': user.group, 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
}
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values
)
self._check_limit('mailboxes', 1, 1)
self._check_limit('mailbox_aliases', 0, 0)
# Delete the admin -> resources should go back to the
# reseller's pool
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.delaccount",
args=[user.id]),
{}
)
self._check_limit('mailboxes', 0, 2)
self._check_limit('mailbox_aliases', 0, 2)
def test_restore_resources(self):
self._create_domain('domain.tld')
dom = Domain.objects.get(name='domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
user = User.objects.get(username='admin1@domain.tld')
values = {
'username': user.username, 'role': user.group, 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
}
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values
)
dom.add_admin(user)
self.clt.logout()
self.clt.login(username='admin1@domain.tld', password='toto')
self._create_account('user1@domain.tld')
self._create_alias('alias1@domain.tld', 'user1@domain.tld')
self._create_alias('alias2@domain.tld', 'user1@domain.tld')
self.clt.logout()
self.clt.login(username='reseller', password='toto')
# Delete the admin -> resources should go back to the
# reseller's pool
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.delaccount",
args=[user.id]),
{}
)
self._check_limit('mailboxes', 1, 2)
self._check_limit('mailbox_aliases', 2, 2)
def test_change_role(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
user = User.objects.get(username='admin1@domain.tld')
# Give 1 mailbox and 2 aliases to the admin -> should work
values = {
'username': user.username, 'role': user.group, 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
}
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values
)
self._check_limit('mailboxes', 1, 1)
self._check_limit('mailbox_aliases', 0, 0)
# Change admin role to SimpleUser -> resources should go back
# to the reseller.
values = {
'username': user.username, 'role': 'SimpleUsers', 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
}
self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values
)
self._check_limit('mailboxes', 1, 2)
self._check_limit('mailbox_aliases', 0, 2)
def test_allocate_too_much(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
user = User.objects.get(username='admin1@domain.tld')
# Give 2 mailboxes and 3 aliases to the admin -> should fail.
values = {
'username': user.username, 'role': user.group, 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
'mailboxes_limit': 2, 'mailbox_aliases_limit': 3
}
resp = self.ajax_post(
reverse("modoboa.extensions.admin.views.identity.editaccount",
args=[user.id]),
values, 424
)
self.assertEqual(resp, 'Not enough resources')
self._check_limit('mailboxes', 1, 2)
self._check_limit('mailbox_aliases', 0, 2)
+8 -12
View File
@@ -3,7 +3,7 @@ from datetime import date
from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _
from modoboa.lib.webutils import (
ajax_simple_response, _render_to_string
render_to_json_response, _render_to_string
)
from modoboa.extensions.admin.lib import needs_mailbox
from modoboa.extensions.admin.models import Mailbox
@@ -29,24 +29,20 @@ def autoreply(request, tplname="postfix_autoreply/autoreply.html"):
arm.untildate = form.cleaned_data["untildate"]
arm.mbox = mb
arm.save()
return ajax_simple_response(dict(
status="ok",
respmsg=_("Auto reply message updated successfully.")
))
return render_to_json_response(
_("Auto reply message updated successfully.")
)
return ajax_simple_response({
"status": "ko",
"errors": form.errors,
"onload_cb": "autoreply_cb"
})
return render_to_json_response(
{"form_errors": form.errors}, status=400
)
form = ARmessageForm(instance=arm)
if arm is not None:
form.fields['untildate'].initial = arm.untildate
else:
form.fields['untildate'].initial = date.today()
return ajax_simple_response({
"status": "ok",
return render_to_json_response({
"content": _render_to_string(request, tplname, {"form": form}),
"onload_cb": "autoreply_cb"
})
@@ -1,5 +1,7 @@
# coding: utf-8
from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.template import Template, Context
from django.conf import settings
from django.utils.translation import ugettext_lazy
@@ -10,6 +12,9 @@ from .models import RelayDomain, RelayDomainAlias, Service
extension_events = [
"RelayDomainCreated",
"RelayDomainDeleted",
"RelayDomainAliasCreated",
"RelayDomainAliasDeleted"
]
@@ -27,15 +32,41 @@ class PostfixRelayDomains(ModoExtension):
"""
for service_name in ['relay', 'smtp']:
Service.objects.get_or_create(name=service_name)
if not exts_pool.is_extension_enabled('limits'):
return
from modoboa.core.models import User
from modoboa.extensions.limits import controls
for u in User.objects.all():
controls.create_pool(u)
grp = Group.objects.get(name='Resellers')
for model in [RelayDomain, RelayDomainAlias, Service]:
ct = ContentType.objects.get_for_model(model)
name = model.__name__.lower()
for action in ['add', 'change', 'delete']:
grp.permissions.add(
Permission.objects.get(
content_type=ct, codename='%s_%s' % (action, name)
)
)
grp.save()
def load(self):
from .app_settings import AdminParametersForm
parameters.register(AdminParametersForm, ugettext_lazy("Relay domains"))
parameters.register(
AdminParametersForm, ugettext_lazy("Relay domains")
)
events.declare(extension_events)
if not exts_pool.is_extension_enabled('limits'):
return
import limits_controls
def destroy(self):
events.unregister('GetExtraDomainEntries', extra_domain_entries)
events.unregister('GetExtraParameters', extra_parameters)
exts_pool.register_extension(PostfixRelayDomains)
@@ -132,3 +163,41 @@ def check_domain_name():
(RelayDomain, ugettext_lazy('relay domain')),
(RelayDomainAlias, ugettext_lazy('relay domain alias'))
]
@events.observe('GetExtraLimitTemplates')
def extra_limit_templates():
return [
('relay_domains_limit', ugettext_lazy('Relay domains'),
ugettext_lazy('Maximum number of relay domains this user can create'),
'Resellers'),
("relay_domain_aliases_limit", ugettext_lazy("Relay domain aliases"),
ugettext_lazy('Maximum number of relay domain aliases this user can create'),
'Resellers'),
]
@events.observe('GetExtraParameters')
def extra_parameters(app, level):
from django import forms
if app != 'limits' or level != 'A':
return {}
return {
'deflt_relay_domains_limit': forms.IntegerField(
label=ugettext_lazy("Relay domains"),
initial=0,
help_text=ugettext_lazy(
"Maximum number of allowed relay domains for a new administrator"
),
widget=forms.widgets.TextInput(attrs={"class": "span1"})
),
'deflt_relay_domain_aliases_limit': forms.IntegerField(
label=ugettext_lazy("Relay domain aliases"),
initial=0,
help_text=ugettext_lazy(
"Maximum number of allowed relay domain aliases for a new administrator"
),
widget=forms.widgets.TextInput(attrs={"class": "span1"})
)
}
@@ -1,6 +1,5 @@
from django import forms
from django.utils.translation import ugettext_lazy
from modoboa.lib.formutils import SeparatorField
from modoboa.lib import parameters
@@ -1,6 +1,7 @@
from django import forms
from django.http import QueryDict
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events
from modoboa.lib.formutils import DynamicForm, DomainNameField
from modoboa.extensions.admin.models import Domain, DomainAlias
from .models import RelayDomain, RelayDomainAlias
@@ -92,23 +93,31 @@ class RelayDomainForm(forms.ModelForm, DynamicForm):
rd = super(RelayDomainForm, self).save(commit=False)
if commit:
rd.save()
aliases = []
for k, v in self.cleaned_data.iteritems():
if not k.startswith("aliases"):
continue
if v in ["", None]:
continue
try:
rd.relaydomainalias_set.get(name=v)
except RelayDomainAlias.DoesNotExist:
pass
else:
continue
#events.raiseEvent("CanCreate", user, "domain_aliases")
al = RelayDomainAlias(name=v, target=rd, enabled=rd.enabled)
al.save(creator=user)
aliases.append(v)
for rdalias in rd.relaydomainalias_set.all():
if not filter(lambda name: self.cleaned_data[name] == rdalias.name,
self.cleaned_data.keys()):
if not rdalias.name in aliases:
rdalias.delete()
else:
aliases.remove(rdalias.name)
if aliases:
events.raiseEvent(
"CanCreate", user, "relay_domain_aliases", len(aliases)
)
for alias in aliases:
try:
rd.relaydomainalias_set.get(name=alias)
except RelayDomainAlias.DoesNotExist:
pass
else:
continue
al = RelayDomainAlias(
name=alias, target=rd, enabled=rd.enabled
)
al.save(creator=user)
return rd
@@ -0,0 +1,31 @@
from modoboa.lib import events
from modoboa.lib.permissions import get_object_owner
from modoboa.extensions.limits.lib import inc_limit_usage, dec_limit_usage
from .models import RelayDomainAlias
@events.observe('RelayDomainCreated')
def inc_relaydomains_count(user, rdomain):
inc_limit_usage(user, 'relay_domains_limit')
@events.observe('RelayDomainDeleted')
def dec_relaydomains_count(rdomain):
owner = get_object_owner(rdomain)
dec_limit_usage(owner, 'relay_domains_limit')
for rdomalias in rdomain.relaydomainalias_set.all():
dec_rdomaliases_count(rdomalias)
@events.observe('RelayDomainAliasCreated')
def inc_rdomaliases_count(user, rdomalias):
inc_limit_usage(user, 'relay_domain_aliases_limit')
@events.observe('RelayDomainAliasDeleted')
def dec_rdomaliases_count(rdomainaliases):
if isinstance(rdomainaliases, RelayDomainAlias):
rdomainaliases = [rdomainaliases]
for rdomainalias in rdomainaliases:
owner = get_object_owner(rdomainalias)
dec_limit_usage(owner, 'relay_domain_aliases_limit')
@@ -16,6 +16,15 @@ RelayDomains.prototype = {
initialize: function(options) {
this.options = $.extend({}, this.defaults, options);
admin.register_tag_handler("srv", this.srv_tag_handler);
$(document).bind('domain_listing_refresh', this.listing_refresh_cb);
},
listing_refresh_cb: function() {
$("a[name=delrelaydomain]").confirm({
question: function() { return this.$element.attr('title'); },
method: "DELETE",
success_cb: $.proxy(admin.reload_listing, admin)
});
},
launch_scan: function(e) {
@@ -54,7 +63,7 @@ RelayDomains.prototype = {
$('input:text:visible:first').focus();
$("#id_aliases").dynamic_input();
$(".submit").on('click', $.proxy(function(e) {
simple_ajax_form_post2(e, {
simple_ajax_form_post(e, {
formid: "rdomform",
reload_on_success: false,
success_cb: $.proxy(admin.reload_listing, admin)
@@ -1,16 +1,13 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
from modoboa.lib.tests import ExtTestCase
from modoboa.extensions.admin import factories
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)
class RelayDomainsTestCase(ExtTestCase):
fixtures = ["initial_users.json"]
name = "postfix_relay_domains"
def setUp(self):
super(RelayDomainsTestCase, self).setUp()
factories.populate_database()
@@ -4,24 +4,20 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
login_required, permission_required
)
from modoboa.lib import events
from modoboa.lib.exceptions import ModoboaException, PermDeniedException
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.webutils import render_to_json_response
from .models import RelayDomain, Service
from .forms import RelayDomainForm
@login_required
@permission_required("admin.add_relaydomain")
@permission_required("postfix_relay_domains.add_relaydomain")
@transaction.commit_on_success
def create(request, tplname="postfix_relay_domains/relaydomain_form.html"):
commonctx = {"title": _("New relay domain"),
"action_label": _("Create"),
"action_classes": "submit",
"action": reverse(create),
"formid": "rdomform"}
events.raiseEvent("CanCreate", request.user, "relay_domains")
if request.method == 'POST':
form = RelayDomainForm(request.POST)
if form.is_valid():
@@ -30,13 +26,21 @@ def create(request, tplname="postfix_relay_domains/relaydomain_form.html"):
return render_to_json_response(
{"respmsg": _("Relay domain created")}
)
return render_to_json_response(form.errors, status=400)
commonctx['form'] = RelayDomainForm()
return render(request, tplname, commonctx)
return render_to_json_response(
{'form_errors': form.errors}, status=400
)
ctx = {"title": _("New relay domain"),
"action_label": _("Create"),
"action_classes": "submit",
"action": reverse(create),
"formid": "rdomform",
"form": RelayDomainForm()}
return render(request, tplname, ctx)
@login_required
@permission_required("admin.change_relaydomain")
@permission_required("postfix_relay_domains.change_relaydomain")
def edit(request, rdom_id, tplname='postfix_relay_domains/relaydomain_form.html'):
rdom = RelayDomain.objects.get(pk=rdom_id)
if not request.user.can_access(rdom):
@@ -46,10 +50,11 @@ def edit(request, rdom_id, tplname='postfix_relay_domains/relaydomain_form.html'
if form.is_valid():
form.save(request.user)
events.raiseEvent('RelayDomainModified', rdom)
return render_to_json_response(
{'respmsg': _('Relay domain modified')}
)
return render_to_json_response(form.errors, status=400)
return render_to_json_response(_('Relay domain modified'))
return render_to_json_response(
{'form_errors': form.errors}, status=400
)
ctx = {
'action': reverse(edit, args=[rdom.id]),
'formid': 'rdomform',
@@ -62,19 +67,17 @@ def edit(request, rdom_id, tplname='postfix_relay_domains/relaydomain_form.html'
@login_required
@permission_required("admin.delete_relaydomain")
@permission_required("postfix_relay_domains.delete_relaydomain")
def delete(request, rdom_id):
rdom = RelayDomain.objects.get(pk=rdom_id)
if not request.user.can_access(rdom):
raise PermDeniedException
rdom.delete()
return render_to_json_response({
'status': 'ok', 'respmsg': _('Relay domain deleted')
})
return render_to_json_response(_('Relay domain deleted'))
@login_required
@user_passes_test(lambda u: u.is_superuser)
@permission_required("postfix_relay_domains.add_service")
@require_http_methods(["POST"])
def scan_for_services(request):
try:
+3 -2
View File
@@ -5,11 +5,12 @@ from sievelib.managesieve import Client, Error
from sievelib.parser import Parser
from sievelib.factory import FiltersSet
from modoboa.lib import parameters
from modoboa.lib.exceptions import ModoboaException
from modoboa.lib.connections import ConnectionsManager, ConnectionError
class SieveClientError(Exception):
pass
class SieveClientError(ModoboaException):
http_code = 424
class SieveClient(object):
@@ -18,15 +18,13 @@ function removefilter(evt) {
$.ajax({
url: $this.attr("href"),
dataType: 'json',
success: function(response) {
if (response.status == "ok") {
history.update(true);
$("body").notify('success', response.respmsg, 2000);
} else {
$("body").notify('error', response.respmsg);
}
}
dataType: 'json'
}).done(function(response) {
history.update(true);
$("body").notify('success', response, 2000);
}).fail(function(jqxhr) {
var response = $.parseJSON(jqxhr.responseText);
$("body").notify('error', response);
});
}
@@ -181,16 +179,14 @@ function toggle_filter_state(event) {
event.preventDefault();
$.ajax({
url: $this.attr("href"),
dataType: 'json',
success: function(response) {
if (response.status == "ok") {
$this.html(response.label);
$this.attr("class", "");
$this.addClass(response.color);
} else {
$("body").notify('error', response.respmsg);
}
}
dataType: 'json'
}).done(function(response) {
$this.html(response.label);
$this.attr("class", "");
$this.addClass(response.color);
}).fail(function(jqhxr) {
var response = $.parseJSON(jqxhr.responseText);
$("body").notify('error', response);
});
}
@@ -200,11 +196,13 @@ function move_filter(event) {
event.preventDefault();
$.ajax({
url: $this.attr('href'),
dataType: 'json',
success: function(response) {
$("#set_content").html(response.content);
init_filters_list();
}
dataType: 'json'
}).done(function(response) {
$("#set_content").html(response.content);
init_filters_list();
}).fail(function(jqxhr) {
var response = $.parseJSON(jqxhr.responseText);
$("body").notify('error', response);
});
}
@@ -234,11 +232,10 @@ function filtersetform_cb() {
$("#newfiltersset").find("input").keypress(function(e) {
if (e.which == 13) e.preventDefault();
});
$(".submit").one('click', function(e) {
$(".submit").on('click', function(e) {
simple_ajax_form_post(e, {
formid: "newfiltersset",
reload_on_success: false,
error_cb: filtersetform_cb,
success_cb: filterset_created
});
});
@@ -34,11 +34,11 @@ function filterform_cb() {
}
});
$(".submit").one('click', function(e) {
$(".submit").on('click', function(e) {
var counters = "conds=" + $dc.fields_cnt + "&actions=" + $da.fields_cnt;
simple_ajax_form_post(e, {
error_cb: filterform_cb,
formid: 'filterform',
reload_mode: 'history',
extradata: counters
});
+52 -69
View File
@@ -9,7 +9,7 @@ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from modoboa.lib import parameters
from modoboa.lib.webutils import _render_error, \
ajax_response, ajax_simple_response
ajax_response, render_to_json_response
from modoboa.lib.connections import ConnectionError
from modoboa.extensions.admin.lib import needs_mailbox
from .lib import SieveClient, SieveClientError
@@ -54,8 +54,12 @@ def index(request, tplname="sievefilters/index.html"):
@needs_mailbox()
def get_templates(request, ftype):
if ftype == "condition":
return ajax_simple_response(FilterForm([], [], request).cond_templates)
return ajax_simple_response(FilterForm([], [], request).action_templates)
return render_to_json_response(
FilterForm([], [], request).cond_templates
)
return render_to_json_response(
FilterForm([], [], request).action_templates
)
@login_required
@@ -87,8 +91,8 @@ def getfs(request, name):
menu = '<ul id="fsetmenu" class="nav nav-list"><li class="nav-header">%s</li>%s</ul>' % \
(_("Actions"), fset_menu(editormode, name))
resp = dict(status="ok", menu=menu, content=htmlcontent)
return ajax_simple_response(resp)
resp = dict(menu=menu, content=htmlcontent)
return render_to_json_response(resp)
def build_filter_ctx(ctx, form):
@@ -119,13 +123,12 @@ def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None
fset.updatefilter(
oldname, fltname, conditions, actions, match_type
)
try:
sc.pushscript(fset.name, str(fset))
except SieveClientError as e:
return ajax_response(request, "ko", respmsg=str(e))
return ajax_response(request, respmsg=okmsg, ajaxnav=True)
tplctx = build_filter_ctx(tplctx, form)
return ajax_response(request, status="ko", template=tplname, **tplctx)
sc.pushscript(fset.name, str(fset))
return render_to_json_response(okmsg)
#tplctx = build_filter_ctx(tplctx, form)
#return ajax_response(request, status="ko", template=tplname, **tplctx)
return render_to_json_response({'form_errors': form.errors}, status=400)
@login_required
@@ -186,8 +189,8 @@ def removefilter(request, setname, fname):
fset = sc.getscript(setname, format="fset")
if fset.removefilter(fname.encode("utf-8")):
sc.pushscript(fset.name, str(fset))
return ajax_response(request, respmsg=_("Filter removed"))
return ajax_response(request, "ko", respmsg=_("Failed to remove filter"))
return render_to_json_response(_("Filter removed"))
return render_to_json_response(_("Failed to remove filter"), status=500)
@login_required
@@ -208,37 +211,28 @@ def savefs(request, name):
@login_required
@needs_mailbox()
def new_filters_set(request, tplname="common/generic_modal_form.html"):
if request.method == "POST":
form = FiltersSetForm(request.POST)
if form.is_valid():
sc = SieveClient(user=request.user.username,
password=request.session["password"])
sc.pushscript(form.cleaned_data["name"], "# Empty script",
form.cleaned_data["active"])
return render_to_json_response({
"url": form.cleaned_data["name"],
"active": form.cleaned_data["active"],
"respmsg": _("Filters set created")
})
return render_to_json_response({'form_errors': form.errors}, status=400)
ctx = {"title": _("Create a new filters set"),
"formid": "newfiltersset",
"action_label": _("Create"),
"action_classes": "submit",
"action": reverse(new_filters_set),
"withmenu": False,
"withunseen": False}
if request.method == "POST":
form = FiltersSetForm(request.POST)
error = None
if form.is_valid():
sc = SieveClient(user=request.user.username,
password=request.session["password"])
try:
sc.pushscript(form.cleaned_data["name"], "# Empty script",
form.cleaned_data["active"])
except SieveClientError, e:
error = str(e)
else:
return ajax_simple_response({
"status": "ok",
"url": form.cleaned_data["name"],
"active": form.cleaned_data["active"],
"respmsg": _("Filters set created")
})
ctx["form"] = form
ctx["error"] = error
return ajax_response(request, status="ko", template=tplname, **ctx)
ctx["form"] = FiltersSetForm()
"withunseen": False,
"form": FiltersSetForm()}
return render(request, tplname, ctx)
@@ -247,13 +241,10 @@ def new_filters_set(request, tplname="common/generic_modal_form.html"):
def remove_filters_set(request, name):
sc = SieveClient(user=request.user.username,
password=request.session["password"])
try:
sc.deletescript(name)
except SieveClientError, e:
return ajax_simple_response(dict(status="ko", respmsg=str(e)))
sc.deletescript(name)
acs, scripts = sc.listscripts()
return ajax_simple_response(dict(
status="ok", respmsg=_("Filters set deleted"), newfs=acs
return render_to_json_response(dict(
respmsg=_("Filters set deleted"), newfs=acs
))
@@ -293,24 +284,19 @@ def toggle_filter_state(request, setname, fname):
password=request.session["password"])
if type(fname) is unicode:
fname = fname.encode("utf-8")
try:
fset = sc.getscript(setname, format="fset")
if fset.is_filter_disabled(fname):
ret = fset.enablefilter(fname)
newstate = _("yes")
color = "green"
else:
ret = fset.disablefilter(fname)
newstate = _("no")
color = "red"
if not ret:
pass
sc.pushscript(setname, str(fset))
except SieveClientError, e:
return ajax_response(request, "ko", respmsg=str(e))
return ajax_simple_response({
"status": "ok",
fset = sc.getscript(setname, format="fset")
if fset.is_filter_disabled(fname):
ret = fset.enablefilter(fname)
newstate = _("yes")
color = "green"
else:
ret = fset.disablefilter(fname)
newstate = _("no")
color = "red"
if not ret:
pass
sc.pushscript(setname, str(fset))
return render_to_json_response({
"label": newstate,
"color": color
})
@@ -319,12 +305,9 @@ def toggle_filter_state(request, setname, fname):
def move_filter(request, setname, fname, direction):
sc = SieveClient(user=request.user.username,
password=request.session["password"])
try:
fset = sc.getscript(setname, format="fset")
fset.movefilter(fname.encode("utf-8"), direction)
sc.pushscript(setname, str(fset))
except (SieveClientError), e:
return ajax_response(request, "ko", respmsg=str(e))
fset = sc.getscript(setname, format="fset")
fset.movefilter(fname.encode("utf-8"), direction)
sc.pushscript(setname, str(fset))
return ajax_response(
request, template="sievefilters/guieditor.html", fs=fset
)
+10 -3
View File
@@ -7,13 +7,20 @@ class ComposeMailForm(forms.Form):
to = forms.CharField(label=_("To"))
cc = forms.CharField(label=_("Cc"), required=False)
cci = forms.CharField(label=_("Cci"), required=False)
subject = forms.CharField(label=_("Subject"), max_length=255, required=False)
subject = forms.CharField(
label=_("Subject"), max_length=255, required=False
)
origmsgid = forms.CharField(
label="", widget=forms.HiddenInput(), required=False
)
origmsgid = forms.CharField(label="", widget=forms.HiddenInput(), required=False)
class FolderForm(forms.Form):
oldname = forms.CharField(label="", widget=forms.HiddenInput(), required=False)
oldname = forms.CharField(
label="", widget=forms.HiddenInput(), required=False
)
name = forms.CharField()
class AttachmentForm(forms.Form):
attachment = forms.FileField(label=_("Select a file"))
@@ -893,13 +893,12 @@ Webmail.prototype = {
$("#mboxform").find("input").keypress(function(e) {
if (e.which == 13) e.preventDefault();
});
$(".submit").one('click', $.proxy(function(e) {
$(".submit").on('click', $.proxy(function(e) {
var $link = $("#folders2 li.active").children("a");
simple_ajax_form_post(e, {
reload_on_success: false,
formid: "mboxform",
error_cb: this.mboxform_cb,
extradata: ($link.length) ? "parent_folder=" + $link.attr("href") : "",
success_cb: $.proxy(this.mboxform_success, this)
});
@@ -907,6 +906,7 @@ Webmail.prototype = {
},
mboxform_success: function(data) {
$("body").notify('success', data.respmsg, 2000);
if (data.oldmb === undefined) {
this.add_mailbox_to_tree(data.parent, data.newmb);
} else if (data.newmb) {
+40 -34
View File
@@ -12,7 +12,8 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.gzip import gzip_page
from modoboa.lib import parameters
from modoboa.lib.webutils import (
_render_to_string, ajax_response, ajax_simple_response
_render_to_string, ajax_response, ajax_simple_response,
render_to_json_response
)
from modoboa.extensions.admin.lib import needs_mailbox
from .exceptions import WebmailError
@@ -132,6 +133,19 @@ def compact(request, name):
def newfolder(request, tplname="webmail/folder.html"):
mbc = IMAPconnector(user=request.user.username,
password=request.session["password"])
if request.method == "POST":
form = FolderForm(request.POST)
if form.is_valid():
pf = request.POST.get("parent_folder", None)
mbc.create_folder(form.cleaned_data["name"], pf)
return render_to_json_response({
'respmsg': _("Mailbox created"),
'newmb': form.cleaned_data["name"], 'parent': pf
})
return render_to_json_response({'form_errors': form.errors}, status=400)
ctx = {"title": _("Create a new mailbox"),
"formid": "mboxform",
"action": reverse(newfolder),
@@ -140,24 +154,9 @@ def newfolder(request, tplname="webmail/folder.html"):
"withunseen": False,
"selectonly": True,
"mboxes": mbc.getmboxes(request.user),
"hdelimiter": mbc.hdelimiter}
if request.method == "POST":
form = FolderForm(request.POST)
if form.is_valid():
pf = request.POST.get("parent_folder", None)
mbc.create_folder(form.cleaned_data["name"], pf)
return ajax_simple_response(dict(
status="ok", respmsg=_("Mailbox created"),
newmb=form.cleaned_data["name"], parent=pf
))
ctx["form"] = form
ctx["selected"] = None
return ajax_response(request, status="ko", template=tplname, **ctx)
ctx["form"] = FolderForm()
ctx["selected"] = None
"hdelimiter": mbc.hdelimiter,
"form": FolderForm(),
"selected": None}
return render(request, tplname, ctx)
@@ -179,9 +178,10 @@ def editfolder(request, tplname="webmail/folder.html"):
form = FolderForm(request.POST)
if form.is_valid():
pf = request.POST.get("parent_folder", None)
ctx["selected"] = pf
oldname, oldparent = separate_mailbox(request.POST["oldname"], sep=mbc.hdelimiter)
res = dict(status="ok", respmsg=_("Mailbox updated"))
oldname, oldparent = separate_mailbox(
request.POST["oldname"], sep=mbc.hdelimiter
)
res = {'respmsg': _("Mailbox updated")}
if form.cleaned_data["name"] != oldname \
or (pf != oldparent):
newname = form.cleaned_data["name"] if pf is None \
@@ -193,21 +193,27 @@ def editfolder(request, tplname="webmail/folder.html"):
res["newparent"] = pf
if "mbox" in request.session:
del request.session["mbox"]
return ajax_simple_response(res)
return render_to_json_response(res)
ctx["mboxes"] = mbc.getmboxes(request.user)
ctx["form"] = form
return ajax_response(request, status="ko", template=tplname, **ctx)
return render_to_json_response({'form_errors': form.errors}, status=400)
name = request.GET.get("name", None)
if name is None:
raise WebmailError(_("Invalid request"))
shortname, parent = separate_mailbox(name, sep=mbc.hdelimiter)
ctx["mboxes"] = mbc.getmboxes(request.user, until_mailbox=parent)
ctx["form"] = FolderForm()
ctx = {"title": _("Edit mailbox"),
"formid": "mboxform",
"action": reverse(editfolder),
"action_label": _("Update"),
"action_classes": "submit",
"withunseen": False,
"selectonly": True,
"hdelimiter": mbc.hdelimiter,
"mboxes": mbc.getmboxes(request.user, until_mailbox=parent),
"form": FolderForm(),
"selected": parent}
ctx["form"].fields["oldname"].initial = name
ctx["form"].fields["name"].initial = shortname
ctx["selected"] = parent
return render(request, tplname, ctx)
@@ -377,11 +383,11 @@ def render_compose(request, form, posturl, email=None, insert_signature=False):
randid = set_compose_session(request)
attachments = request.session["compose_mail"]["attachments"]
if len(attachments):
short_att_list = "(%s)" \
% ", ".join(map(lambda att: att["fname"],
attachments[:2] + [{"fname": "..."}] \
if len(attachments) > 2 else attachments))
if attachments:
short_att_list = "(%s)" % ", ".join(
[att['fname'] for att in (attachments[:2] + [{"fname": "..."}]
if len(attachments) > 2 else attachments)]
)
else:
short_att_list = ""
content = _render_to_string(request, "webmail/compose.html", {
+12 -3
View File
@@ -91,7 +91,7 @@ class observe(object):
return []
else:
if not ext.enabled:
return []
return None
elif not modname in settings.MODOBOA_APPS:
return []
return f(*args, **kwargs)
@@ -144,7 +144,12 @@ def raiseQueryEvent(event, *args, **kwargs):
if not event in events or not event in callbacks.keys():
return result
for callback in callbacks[event].values():
result += callback(*args, **kwargs)
tmp = callback(*args, **kwargs)
if tmp is None:
# Callback is registered but associated extension is
# disabled.
continue
result += tmp
return result
@@ -158,10 +163,14 @@ def raiseDictEvent(event, *args):
:return: a dictionnary
"""
result = {}
if not event in events or not event in callbacks.keys():
if not event in events or not event in callbacks:
return result
for callback in callbacks[event].values():
tmp = callback(*args)
if tmp is None:
# Callback is registered but associated extension is
# disabled.
continue
for k, v in tmp.iteritems():
result[k] = v
return result
+48 -1
View File
@@ -8,10 +8,57 @@ from django.utils.translation import ugettext as _
class ModoboaException(Exception):
pass
"""
Base class for Modoboa custom exceptions.
"""
http_code = None
def __init__(self, *args, **kwargs):
if 'http_code' in kwargs:
self.http_code = kwargs['http_code']
del kwargs['http_code']
super(ModoboaException, self).__init__(*args, **kwargs)
class InternalError(ModoboaException):
"""
Use this exception for system errors, missing dependencies, etc.
"""
http_code = 500
class BadRequest(ModoboaException):
"""
Use this exception when received data doesn't validate a specific
format (example: wrong CSV line) or doesn't respect validation
rules.
"""
http_code = 400
class NotFound(ModoboaException):
"""
Use this exception to indicate the requested resource could not be
found.
"""
http_code = 404
class Conflict(ModoboaException):
"""
Use this exception to indicate that the request could not be
processed because of conflict in the request.
"""
http_code = 409
class PermDeniedException(ModoboaException):
"""
Use this exception when a user tries to do something he is not
allowed to.
"""
http_code = 403
def __init__(self, msg=None):
self.msg = msg
+33 -9
View File
@@ -10,10 +10,20 @@ from django.utils.html import conditional_escape
class CreationWizard(object):
def __init__(self, done_cb):
def __init__(self, done_cb=None):
self.steps = []
self.done_cb = done_cb
@property
def errors(self):
result = {}
for step in self.steps:
for name, value in step['form'].errors.items():
if name == '__all__':
continue
result[name] = value
return result
def add_step(self, cls, title, buttons, formtpl=None, new_args=None):
self.steps += [dict(cls=cls, title=title, buttons=buttons,
formtpl=formtpl, new_args=new_args)]
@@ -33,19 +43,23 @@ class CreationWizard(object):
return -1, _("Invalid request")
stepid = stepid.replace("step", "")
stepid = int(stepid) - 1
if stepid < 0 or stepid >= len(self.steps):
stepid = int(stepid)
if stepid < 0 or stepid > len(self.steps):
return -1, _("Invalid request")
self.create_forms(request.POST)
if self.steps[stepid]["form"].is_valid():
if stepid == len(self.steps) - 1:
statuses = []
for cpt in xrange(0, stepid):
statuses.append(self.steps[cpt]["form"].is_valid())
if False in statuses:
return 0, stepid
if stepid == len(self.steps):
if self.done_cb is not None:
self.done_cb(self.steps)
return 2, None
return 1, stepid
return 0, stepid
return 2, None
return 1, stepid
def get_title(self, stepid):
return "%d. %s" % (stepid + 1, self.steps[stepid]["title"])
return "%d. %s" % (stepid, self.steps[stepid - 1]["title"])
class DynamicForm(object):
@@ -109,6 +123,16 @@ class TabForms(object):
def _before_is_valid(self, form):
return True
@property
def errors(self):
result = {}
for f in self.forms:
for name, value in f['instance'].errors.items():
if name == '__all__':
continue
result[name] = value
return result
def is_valid(self, mandatory_only=False, optional_only=False):
to_remove = []
for f in self.forms:
+9 -3
View File
@@ -3,7 +3,9 @@ import re
from django.http import Http404, HttpResponseRedirect
from modoboa.core.models import Extension
from modoboa.core.extensions import exts_pool
from modoboa.lib.webutils import _render_error, ajax_response
from modoboa.lib.webutils import (
_render_error, ajax_response, render_to_json_response
)
from modoboa.lib.exceptions import ModoboaException
@@ -41,6 +43,10 @@ class CommonExceptionCatcher(object):
return _render_error(
request, user_context=dict(error=str(exception))
)
return ajax_response(
request, status="ko", respmsg=unicode(exception), norefresh=True
if exception.http_code is None:
return ajax_response(
request, status="ko", respmsg=unicode(exception), norefresh=True
)
return render_to_json_response(
unicode(exception), status=exception.http_code
)
+16 -3
View File
@@ -11,9 +11,9 @@ Only super users will be able to access this part of the web interface.
import inspect
import re
from django import forms
from . import events
from exceptions import ModoboaException
_params = {'A': {}, 'U': {}}
@@ -28,7 +28,12 @@ class NotDefined(ModoboaException):
class GenericParametersForm(forms.Form):
"""Base class for parameter forms.
Each extension has the possibility to define global parameters.
"""
app = None
visibility_rules = None
def __init__(self, *args, **kwargs):
if self.app is None:
@@ -38,10 +43,12 @@ class GenericParametersForm(forms.Form):
super(GenericParametersForm, self).__init__(*args, **kwargs)
self.visirules = {}
if hasattr(self, "visibility_rules"):
if self.visibility_rules is not None:
for key, rule in self.visibility_rules.items():
field, value = rule.split("=")
visibility = {"field": "id_%s-%s" % (self.app, field), "value": value}
visibility = {
"field": "id_%s-%s" % (self.app, field), "value": value
}
self.visirules["%s-%s" % (self.app, key)] = visibility
if not args:
@@ -50,6 +57,11 @@ class GenericParametersForm(forms.Form):
def _load_initial_values(self):
raise NotImplementedError
def _load_extra_parameters(self, level):
params = events.raiseDictEvent('GetExtraParameters', self.app, level)
for pname, pdef in params.items():
self.fields[pname] = pdef
def _save_parameter(self, p, name, value):
if p.value == value:
return
@@ -101,6 +113,7 @@ class AdminParametersForm(GenericParametersForm):
pass
return values
class UserParametersForm(GenericParametersForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user") if "user" in kwargs else None
+3 -1
View File
@@ -18,7 +18,9 @@ def get_account_roles(user):
std_roles = [("SimpleUsers", _("Simple user"))]
if user.is_superuser:
std_roles += [("SuperAdmins", _("Super administrator"))]
if user.has_perm("admin.add_domain"):
filters = events.raiseQueryEvent('UserCanSetRole', user, 'DomainAdmins')
if user.has_perm("admin.add_domain") and \
(not filters or True in filters):
std_roles += [("DomainAdmins", _("Domain administrator"))]
std_roles += events.raiseQueryEvent("GetExtraRoles", user)
return sorted(std_roles, key=lambda role: role[1])
+15
View File
@@ -0,0 +1,15 @@
# works in Python 2 & 3
class _Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = \
super(_Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Singleton(_Singleton('SingletonMeta', (object,), {})):
pass
+7 -10
View File
@@ -13,20 +13,17 @@ class ModoTestCase(TestCase):
self.clt = Client()
self.assertEqual(self.clt.login(username=username, password=password), True)
def check_ajax_request(self, method, url, params, status="ok", **kwargs):
def ajax_request(self, method, url, params, status=200):
response = getattr(self.clt, method) \
(url, params, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
obj = simplejson.loads(response.content)
self.assertEqual(obj["status"], status)
for kw, v in kwargs.items():
self.assertEqual(obj[kw], v)
self.assertEqual(response.status_code, status)
return simplejson.loads(response.content)
def check_ajax_post(self, *args, **kwargs):
self.check_ajax_request("post", *args, **kwargs)
def ajax_post(self, *args, **kwargs):
return self.ajax_request('post', *args, **kwargs)
def check_ajax_get(self, *args, **kwargs):
self.check_ajax_request("get", *args, **kwargs)
def ajax_get(self, *args, **kwargs):
return self.ajax_request('get', *args, **kwargs)
class ExtTestCase(ModoTestCase):
+15 -19
View File
@@ -87,29 +87,25 @@
}
params += $(this).attr("name") + "=true";
});
$.ajax({
cache: false,
method: this.options.method,
data: params,
url: this.$element.attr('href'),
complete: $.proxy(function(XMLHttpRequest, textStatus) {
var data;
try {
data = $.parseJSON(XMLHttpRequest.responseText);
} catch (e) {
data = { status: 'ko', respmsg: gettext('Internal Error') };
}
if (data.status == "ok") {
if (this.options.success_cb != undefined) {
this.options.success_cb(data);
return;
}
window.location.reload();
} else {
$("body").notify("error", data.respmsg);
}
}, this)
url: this.$element.attr('href')
}).done($.proxy(function(data) {
if (this.options.success_cb != undefined) {
this.options.success_cb(data);
return;
}
window.location.reload();
}, this)).fail(function(jqxhr) {
var data;
try {
data = $.parseJSON(jqxhr.responseText);
} catch (e) {
data = gettext('Internal Error');
}
$("body").notify("error", data);
});
this.box.remove();
}
+38 -18
View File
@@ -12,6 +12,7 @@
this.$element.carousel({pause: true});
this.$element.carousel("pause");
this.listen();
this.titles = {};
$('input:text:visible:first').focus();
},
@@ -21,16 +22,28 @@
$(".submit").on("click", $.proxy(this.submit, this));
},
get_current_step_id: function() {
return $(".item.active").attr("id");
},
get_current_title: function() {
return $(".modal-header").find("small").html();
},
set_current_title: function(title) {
$(".modal-header").find("small").html(title);
},
post: function(last) {
var $form = (this.options.formid) ? $('#' + this.options.formid) : $('form');
var data = $form.serialize() + "&stepid=" + $(".item.active").attr("id");
var data = $form.serialize() + "&stepid=" + this.get_current_step_id();
$.post($form.attr("action"), data, $.proxy(function(resp) {
if (resp.status == "ok") {
$.ajax({type: 'POST', url: $form.attr("action"), data: data})
.done($.proxy(function(resp) {
if (!last) {
var stepid = resp.stepid + 1;
$('input:text:visible:first').focus();
$(".modal-header").find("small").html(resp.title);
this.set_current_title(resp.title);
if (this.options.transition_callbacks[stepid] != undefined) {
this.options.transition_callbacks[stepid]();
}
@@ -43,20 +56,23 @@
window.location.reload();
}
}
return;
}
if (resp.stepid != undefined && resp.form != undefined) {
var stepid = resp.stepid + 1;
$("#step" + stepid).html(resp.form);
if (this.options.error_callbacks[stepid] != undefined) {
this.options.error_callbacks[stepid]();
}, this))
.fail($.proxy(function(jqxhr) {
var resp = $.parseJSON(jqxhr.responseText);
if (resp.stepid != undefined) {
var stepid = resp.stepid;
display_form_errors("step" + stepid, resp);
if (this.options.error_callbacks[stepid] != undefined) {
this.options.error_callbacks[stepid]();
}
$('input:text:visible:first').focus();
if (resp.respmsg) {
$(".modal-body").prepend(build_error_alert(resp.respmsg));
}
return;
}
$('input:text:visible:first').focus();
}
if (resp.respmsg) {
$(".modal-body").prepend(build_error_alert(resp.respmsg));
}
}, this));
$(".modal-body").prepend(build_error_alert(resp));
}, this));
},
update_buttons: function() {
@@ -66,6 +82,8 @@
next: function(evt) {
evt.preventDefault();
var step_id = this.get_current_step_id();
this.titles[step_id] = this.get_current_title();
this.$element.on('slid', this.update_buttons);
this.post(false);
},
@@ -74,6 +92,7 @@
evt.preventDefault();
this.$element.on('slid', this.update_buttons);
this.$element.carousel('prev');
this.set_current_title(this.titles[this.get_current_step_id()]);
},
submit: function(evt) {
@@ -98,7 +117,8 @@
};
$.fn.cwizard.defaults = {
transition_callbacks: {}
transition_callbacks: {},
error_callbacks: {}
};
})(jQuery);
+52 -71
View File
@@ -31,8 +31,11 @@ function modalbox(e, css, defhref, defcb, defclosecb) {
$(href).modal('open');
return;
}
$.get(href, function(data) {
$.ajax({type: "GET", url: href}).done(function(data) {
if (typeof data === "object") {
/*
* Compatibility with the old way...
*/
if (data.status == "ko" && data.respmsg) {
$("body").notify("error", data.respmsg);
}
@@ -67,6 +70,9 @@ function modalbox(e, css, defhref, defcb, defclosecb) {
if (css != undefined) {
$div.css(css);
}
}).fail(function(jqxhr) {
var data = $.parseJSON(jqxhr.responseText);
$("body").notify("error", data);
});
}
@@ -111,57 +117,46 @@ if (!Object.keys) {
}
/*
* Simple function that sends a form using an 'ajax' post request.
*
* The function is intended to be used in a modal environment.
* Clean all errors in a given form.
*/
function simple_ajax_form_post(e, options) {
e.preventDefault();
var $form = (options.formid !== undefined) ? $("#" + options.formid) : $("form");
var defaults = {reload_on_success: true, reload_mode: 'full', modal: true};
var opts = $.extend({}, defaults, options);
var args = $form.serialize();
function clean_form_errors(formid) {
$("#" + formid + " div.error").removeClass("error");
$("#" + formid + " span.help-inline").remove();
}
if (options.extradata != undefined) {
args += "&" + options.extradata;
}
$.post($form.attr("action"), args, function(data) {
if (data.status == "ok") {
$("#modalbox").modal('hide');
if (opts.success_cb != undefined) {
opts.success_cb(data);
return;
}
if (opts.reload_on_success) {
if (opts.reload_mode == 'full') {
window.location.reload();
} else {
history.update(true);
}
}
if (data.respmsg) {
$("body").notify('success', data.respmsg, 2000);
}
/*
* Display validation errors for a given form.
*/
function display_form_errors(formid, data) {
clean_form_errors(formid);
$.each(data.form_errors, function(id, value) {
var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id;
var $widget = $("#" + formid + " #" + fullid);
var spanid = fullid + "-error";
var $span = $("#" + spanid);
if (!$widget.parents(".control-group").hasClass("error")) {
$widget.parents(".control-group").addClass("error");
}
if (!$span.length) {
$span = $("<span />", {
"class": "help-inline",
"html": value[0],
"id": spanid
});
$widget.parents(".controls").append($span);
} else {
if (opts.modal) {
if (data.content != "") {
$('.modal').html(data.content);
}
if (data.respmsg) {
$('.modal-body').prepend(build_error_alert(data.respmsg));
}
}
if (opts.error_cb != undefined) {
opts.error_cb(data);
}
$span.html(value[0]);
}
});
}
/*
* Replacement
* Simple function that sends a form using an 'ajax' post request.
*
* The function is intended to be used in a modal environment.
*/
function simple_ajax_form_post2(e, options) {
function simple_ajax_form_post(e, options) {
e.preventDefault();
var $form = (options.formid != undefined) ? $("#" + options.formid) : $("form");
var defaults = {reload_on_success: true, reload_mode: 'full', modal: true};
@@ -176,7 +171,9 @@ function simple_ajax_form_post2(e, options) {
url: $form.attr("action"),
data: args
}).done(function(data) {
$("#modalbox").modal('hide');
if (opts.modal) {
$("#modalbox").modal('hide');
}
if (opts.success_cb != undefined) {
opts.success_cb(data);
return;
@@ -188,38 +185,22 @@ function simple_ajax_form_post2(e, options) {
history.update(true);
}
}
if (data.respmsg) {
$("body").notify('success', data.respmsg, 2000);
if (data) {
$("body").notify('success', data, 2000);
}
}).fail(function(jqxhr) {
if (jqxhr.status != 400) {
return;
}
var result = $.parseJSON(jqxhr.responseText);
$("div.error").removeClass("error");
$("span.help-inline").remove();
$.each(result, function(id, value) {
var fullid = "id_" + id;
var $widget = $("#" + fullid);
var spanid = fullid + "-error";
var $span = $("#" + spanid);
if (!$widget.parents(".control-group").hasClass("error")) {
$widget.parents(".control-group").addClass("error");
}
if (!$span.length) {
$span = $("<span />", {
"class": "help-inline",
"html": value[0],
"id": spanid
});
$widget.parents(".controls").append($span);
var data = $.parseJSON(jqxhr.responseText);
if (data.form_errors) {
display_form_errors(options.formid, data);
} else {
if (opts.modal) {
$('.modal-body').prepend(build_error_alert(data));
} else {
$span.html(value[0]);
$('body').notify('error', data);
}
});
}
if (opts.error_cb) {
opts.error_cb(result);
opts.error_cb(data);
}
});
}
+2 -31
View File
@@ -25,41 +25,11 @@ TwocolsNav.prototype = {
formid: this.options.formid,
modal: false,
reload_on_success: false,
error_cb: $.proxy(this.display_errors, this),
success_cb: $.proxy(this.save_cb, this)
});
}, this));
},
display_errors: function(data) {
if (data.errors === undefined || !data.errors.length) {
if (data.respmsg) {
$("body").notify("error", data.respmsg);
}
return;
}
$.each(data.errors, function(id, value) {
var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id;
var $widget = $("#" + fullid);
var spanid = fullid + "-error";
var $span = $("#" + spanid);
if (!$widget.parents(".control-group").hasClass("error")) {
$widget.parents(".control-group").addClass("error");
}
if (!$span.length) {
$span = $("<span />", {
"class": "help-inline",
"html": value[0],
"id": spanid
});
$widget.parents(".controls").append($span);
} else {
$span.html(value[0]);
}
});
},
update_content: function(data) {
if (data.content) {
$('#' + this.options.divid).html(data.content);
@@ -134,6 +104,7 @@ TwocolsNav.prototype = {
},
save_cb: function(data) {
clean_form_errors(this.options.formid);
if (this.options.reload_exceptions) {
for (var i = 0; i < this.options.reload_exceptions.length; i++) {
if (this.navobj.getbaseurl() == this.options.reload_exceptions[i]) {
@@ -142,7 +113,7 @@ TwocolsNav.prototype = {
}
}
}
$("body").notify("success", data.respmsg, 2000);
$("body").notify("success", data, 2000);
},
load_section: function(e) {