From f91ef1adf250cfdc9f6dae6cf55cba7c7868dc67 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 28 Oct 2013 18:36:42 +0100 Subject: [PATCH] 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 --- modoboa/core/__init__.py | 5 +- modoboa/core/exceptions.py | 14 - modoboa/core/extensions.py | 7 + modoboa/core/models.py | 23 +- modoboa/core/views/admin.py | 27 +- modoboa/core/views/user.py | 27 +- modoboa/extensions/admin/__init__.py | 6 +- modoboa/extensions/admin/exceptions.py | 19 - modoboa/extensions/admin/forms/account.py | 145 +++++--- modoboa/extensions/admin/forms/alias.py | 34 +- modoboa/extensions/admin/forms/domain.py | 41 ++- modoboa/extensions/admin/forms/forward.py | 10 +- .../commands/handle_mailbox_operations.py | 4 +- modoboa/extensions/admin/models/alias.py | 18 +- modoboa/extensions/admin/models/domain.py | 6 +- .../extensions/admin/models/domain_alias.py | 7 +- modoboa/extensions/admin/models/mailbox.py | 8 +- .../extensions/admin/static/admin/js/admin.js | 24 +- .../admin/{dlistform.html => aliasform.html} | 3 +- .../templates/admin/domain_options_form.html | 3 +- modoboa/extensions/admin/tests/account.py | 47 ++- modoboa/extensions/admin/tests/alias.py | 20 +- modoboa/extensions/admin/tests/domain.py | 16 +- .../extensions/admin/tests/domain_alias.py | 4 +- modoboa/extensions/admin/views/alias.py | 77 ++-- modoboa/extensions/admin/views/domain.py | 134 +++---- modoboa/extensions/admin/views/identity.py | 116 +++--- modoboa/extensions/admin/views/user.py | 50 +-- modoboa/extensions/limits/__init__.py | 22 +- modoboa/extensions/limits/app_settings.py | 4 + modoboa/extensions/limits/controls.py | 85 ++--- modoboa/extensions/limits/forms.py | 71 ++-- modoboa/extensions/limits/lib.py | 24 ++ modoboa/extensions/limits/models.py | 61 ++- modoboa/extensions/limits/tests.py | 346 +++++++++++++++++- modoboa/extensions/postfix_autoreply/views.py | 20 +- .../postfix_relay_domains/__init__.py | 71 +++- .../postfix_relay_domains/app_settings.py | 1 - .../extensions/postfix_relay_domains/forms.py | 33 +- .../postfix_relay_domains/limits_controls.py | 31 ++ .../postfix_relay_domains/js/relay_domains.js | 11 +- .../extensions/postfix_relay_domains/tests.py | 25 +- .../extensions/postfix_relay_domains/views.py | 45 +-- modoboa/extensions/sievefilters/lib.py | 5 +- .../static/sievefilters/js/lib.js | 49 ++- .../templates/sievefilters/index.html | 4 +- modoboa/extensions/sievefilters/views.py | 121 +++--- modoboa/extensions/webmail/forms.py | 13 +- .../webmail/static/webmail/js/webmail.js | 4 +- modoboa/extensions/webmail/views.py | 74 ++-- modoboa/lib/events.py | 15 +- modoboa/lib/exceptions.py | 49 ++- modoboa/lib/formutils.py | 42 ++- modoboa/lib/middleware.py | 12 +- modoboa/lib/parameters.py | 19 +- modoboa/lib/permissions.py | 4 +- modoboa/lib/singleton.py | 15 + modoboa/lib/tests.py | 17 +- modoboa/static/js/confirmation.js | 34 +- modoboa/static/js/creation_wizard.js | 56 ++- modoboa/static/js/global.js | 123 +++---- modoboa/static/js/twocols_nav.js | 33 +- 62 files changed, 1484 insertions(+), 950 deletions(-) delete mode 100644 modoboa/core/exceptions.py delete mode 100644 modoboa/extensions/admin/exceptions.py rename modoboa/extensions/admin/templates/admin/{dlistform.html => aliasform.html} (67%) create mode 100644 modoboa/extensions/postfix_relay_domains/limits_controls.py create mode 100644 modoboa/lib/singleton.py diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index 824c49add..8e81e7145 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -15,6 +15,7 @@ base_events = [ "RoleChanged", "GetExtraRoles", "PasswordChange", + "UserCanSetRole", "UserMenuDisplay", "AdminMenuDisplay", @@ -32,7 +33,9 @@ base_events = [ "ExtraAdminContent", "ExtraUprefsRoutes", - "ExtraUprefsJS" + "ExtraUprefsJS", + + "GetExtraParameters" ] diff --git a/modoboa/core/exceptions.py b/modoboa/core/exceptions.py deleted file mode 100644 index 063db7ad9..000000000 --- a/modoboa/core/exceptions.py +++ /dev/null @@ -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 diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index 4b399bd24..ecd65de4a 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -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: diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 4af548ad3..db0050044 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -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) ) diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index fdd377cb2..678c41caf 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -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, }), diff --git a/modoboa/core/views/user.py b/modoboa/core/views/user.py index c42054526..d3cb2e8ac 100644 --- a/modoboa/core/views/user.py +++ b/modoboa/core/views/user.py @@ -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) }) diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 22e84f0c7..7cfd3d63a 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -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): diff --git a/modoboa/extensions/admin/exceptions.py b/modoboa/extensions/admin/exceptions.py deleted file mode 100644 index d0414b27b..000000000 --- a/modoboa/extensions/admin/exceptions.py +++ /dev/null @@ -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 diff --git a/modoboa/extensions/admin/forms/account.py b/modoboa/extensions/admin/forms/account.py index bcbc54c86..c8afb5fae 100644 --- a/modoboa/extensions/admin/forms/account.py +++ b/modoboa/extensions/admin/forms/account.py @@ -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 diff --git a/modoboa/extensions/admin/forms/alias.py b/modoboa/extensions/admin/forms/alias.py index 89bfe4e68..555b98802 100644 --- a/modoboa/extensions/admin/forms/alias.py +++ b/modoboa/extensions/admin/forms/alias.py @@ -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) diff --git a/modoboa/extensions/admin/forms/domain.py b/modoboa/extensions/admin/forms/domain.py index 59682ae6c..c3686fdc9 100644 --- a/modoboa/extensions/admin/forms/domain.py +++ b/modoboa/extensions/admin/forms/domain.py @@ -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) diff --git a/modoboa/extensions/admin/forms/forward.py b/modoboa/extensions/admin/forms/forward.py index fa1e7a53a..9cf7eb643 100644 --- a/modoboa/extensions/admin/forms/forward.py +++ b/modoboa/extensions/admin/forms/forward.py @@ -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.") + ) diff --git a/modoboa/extensions/admin/management/commands/handle_mailbox_operations.py b/modoboa/extensions/admin/management/commands/handle_mailbox_operations.py index 56a5d24a1..056b4a55e 100644 --- a/modoboa/extensions/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/extensions/admin/management/commands/handle_mailbox_operations.py @@ -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: diff --git a/modoboa/extensions/admin/models/alias.py b/modoboa/extensions/admin/models/alias.py index e0ec8ca57..e8720ef5e 100644 --- a/modoboa/extensions/admin/models/alias.py +++ b/modoboa/extensions/admin/models/alias.py @@ -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) diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py index 3e4294070..7a0b909df 100644 --- a/modoboa/extensions/admin/models/domain.py +++ b/modoboa/extensions/admin/models/domain.py @@ -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) diff --git a/modoboa/extensions/admin/models/domain_alias.py b/modoboa/extensions/admin/models/domain_alias.py index 3d087dc11..382ba74e3 100644 --- a/modoboa/extensions/admin/models/domain_alias.py +++ b/modoboa/extensions/admin/models/domain_alias.py @@ -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) diff --git a/modoboa/extensions/admin/models/mailbox.py b/modoboa/extensions/admin/models/mailbox.py index ab1da3584..a16e478a0 100644 --- a/modoboa/extensions/admin/models/mailbox.py +++ b/modoboa/extensions/admin/models/mailbox.py @@ -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. diff --git a/modoboa/extensions/admin/static/admin/js/admin.js b/modoboa/extensions/admin/static/admin/js/admin.js index 87f1a51c3..313bc332e 100644 --- a/modoboa/extensions/admin/static/admin/js/admin.js +++ b/modoboa/extensions/admin/static/admin/js/admin.js @@ -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) }); diff --git a/modoboa/extensions/admin/templates/admin/dlistform.html b/modoboa/extensions/admin/templates/admin/aliasform.html similarity index 67% rename from modoboa/extensions/admin/templates/admin/dlistform.html rename to modoboa/extensions/admin/templates/admin/aliasform.html index 20f3cd51d..6a13e26c9 100644 --- a/modoboa/extensions/admin/templates/admin/dlistform.html +++ b/modoboa/extensions/admin/templates/admin/aliasform.html @@ -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" %} diff --git a/modoboa/extensions/admin/templates/admin/domain_options_form.html b/modoboa/extensions/admin/templates/admin/domain_options_form.html index c8acba112..b1c050f85 100644 --- a/modoboa/extensions/admin/templates/admin/domain_options_form.html +++ b/modoboa/extensions/admin/templates/admin/domain_options_form.html @@ -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 %}
{% trans "You can't create a new domain administrator" %}
{% endif %} diff --git a/modoboa/extensions/admin/tests/account.py b/modoboa/extensions/admin/tests/account.py index fa8773bc1..6f316eb17 100644 --- a/modoboa/extensions/admin/tests/account.py +++ b/modoboa/extensions/admin/tests/account.py @@ -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 ) diff --git a/modoboa/extensions/admin/tests/alias.py b/modoboa/extensions/admin/tests/alias.py index 1cb9a277d..ff6fbc840 100644 --- a/modoboa/extensions/admin/tests/alias.py +++ b/modoboa/extensions/admin/tests/alias.py @@ -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 ) diff --git a/modoboa/extensions/admin/tests/domain.py b/modoboa/extensions/admin/tests/domain.py index 234e0f117..929afd3c8 100644 --- a/modoboa/extensions/admin/tests/domain.py +++ b/modoboa/extensions/admin/tests/domain.py @@ -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]), {} diff --git a/modoboa/extensions/admin/tests/domain_alias.py b/modoboa/extensions/admin/tests/domain_alias.py index d79e2a645..9930d0d16 100644 --- a/modoboa/extensions/admin/tests/domain_alias.py +++ b/modoboa/extensions/admin/tests/domain_alias.py @@ -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 diff --git a/modoboa/extensions/admin/views/alias.py b/modoboa/extensions/admin/views/alias.py index b03c32fd3..c8410615f 100644 --- a/modoboa/extensions/admin/views/alias.py +++ b/modoboa/extensions/admin/views/alias.py @@ -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) diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py index 812cc4928..29b43132c 100644 --- a/modoboa/extensions/admin/views/domain.py +++ b/modoboa/extensions/admin/views/domain.py @@ -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) diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py index a2f1e1bc4..7ab550a68 100644 --- a/modoboa/extensions/admin/views/identity.py +++ b/modoboa/extensions/admin/views/identity.py @@ -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) diff --git a/modoboa/extensions/admin/views/user.py b/modoboa/extensions/admin/views/user.py index 37fdd6d09..7c0382c41 100644 --- a/modoboa/extensions/admin/views/user.py +++ b/modoboa/extensions/admin/views/user.py @@ -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 }) diff --git a/modoboa/extensions/limits/__init__.py b/modoboa/extensions/limits/__init__.py index 5c991598f..7f49c549f 100644 --- a/modoboa/extensions/limits/__init__.py +++ b/modoboa/extensions/limits/__init__.py @@ -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; } diff --git a/modoboa/extensions/limits/app_settings.py b/modoboa/extensions/limits/app_settings.py index 6ce6665cb..e5b510cff 100644 --- a/modoboa/extensions/limits/app_settings.py +++ b/modoboa/extensions/limits/app_settings.py @@ -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') diff --git a/modoboa/extensions/limits/controls.py b/modoboa/extensions/limits/controls.py index 18a7b96c2..7ddd2f212 100644 --- a/modoboa/extensions/limits/controls.py +++ b/modoboa/extensions/limits/controls.py @@ -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') diff --git a/modoboa/extensions/limits/forms.py b/modoboa/extensions/limits/forms.py index 8a5ac4fcf..a763284e2 100644 --- a/modoboa/extensions/limits/forms.py +++ b/modoboa/extensions/limits/forms.py @@ -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]) diff --git a/modoboa/extensions/limits/lib.py b/modoboa/extensions/limits/lib.py index 5a395b86c..a4f7b2f1d 100644 --- a/modoboa/extensions/limits/lib.py +++ b/modoboa/extensions/limits/lib.py @@ -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 diff --git a/modoboa/extensions/limits/models.py b/modoboa/extensions/limits/models.py index 80f521289..c249aa27b 100644 --- a/modoboa/extensions/limits/models.py +++ b/modoboa/extensions/limits/models.py @@ -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 "" diff --git a/modoboa/extensions/limits/tests.py b/modoboa/extensions/limits/tests.py index 410b9f30c..0bae19785 100644 --- a/modoboa/extensions/limits/tests.py +++ b/modoboa/extensions/limits/tests.py @@ -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) diff --git a/modoboa/extensions/postfix_autoreply/views.py b/modoboa/extensions/postfix_autoreply/views.py index 2766d533e..7b234966b 100644 --- a/modoboa/extensions/postfix_autoreply/views.py +++ b/modoboa/extensions/postfix_autoreply/views.py @@ -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" }) diff --git a/modoboa/extensions/postfix_relay_domains/__init__.py b/modoboa/extensions/postfix_relay_domains/__init__.py index 912da41f4..151462236 100644 --- a/modoboa/extensions/postfix_relay_domains/__init__.py +++ b/modoboa/extensions/postfix_relay_domains/__init__.py @@ -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"}) + ) + } diff --git a/modoboa/extensions/postfix_relay_domains/app_settings.py b/modoboa/extensions/postfix_relay_domains/app_settings.py index d84de1aec..49d17ae0c 100644 --- a/modoboa/extensions/postfix_relay_domains/app_settings.py +++ b/modoboa/extensions/postfix_relay_domains/app_settings.py @@ -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 diff --git a/modoboa/extensions/postfix_relay_domains/forms.py b/modoboa/extensions/postfix_relay_domains/forms.py index f3100bd68..767caeb3a 100644 --- a/modoboa/extensions/postfix_relay_domains/forms.py +++ b/modoboa/extensions/postfix_relay_domains/forms.py @@ -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 diff --git a/modoboa/extensions/postfix_relay_domains/limits_controls.py b/modoboa/extensions/postfix_relay_domains/limits_controls.py new file mode 100644 index 000000000..9966945e7 --- /dev/null +++ b/modoboa/extensions/postfix_relay_domains/limits_controls.py @@ -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') diff --git a/modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js b/modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js index 830e3e977..f14188ee0 100644 --- a/modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js +++ b/modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js @@ -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) diff --git a/modoboa/extensions/postfix_relay_domains/tests.py b/modoboa/extensions/postfix_relay_domains/tests.py index 501deb776..dab97161c 100644 --- a/modoboa/extensions/postfix_relay_domains/tests.py +++ b/modoboa/extensions/postfix_relay_domains/tests.py @@ -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() + + diff --git a/modoboa/extensions/postfix_relay_domains/views.py b/modoboa/extensions/postfix_relay_domains/views.py index 0a4281d0d..5eded6ca3 100644 --- a/modoboa/extensions/postfix_relay_domains/views.py +++ b/modoboa/extensions/postfix_relay_domains/views.py @@ -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: diff --git a/modoboa/extensions/sievefilters/lib.py b/modoboa/extensions/sievefilters/lib.py index 3cd57d338..6d6b7e1ae 100644 --- a/modoboa/extensions/sievefilters/lib.py +++ b/modoboa/extensions/sievefilters/lib.py @@ -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): diff --git a/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js b/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js index 7854c0c13..3c8336806 100644 --- a/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js +++ b/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js @@ -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 }); }); diff --git a/modoboa/extensions/sievefilters/templates/sievefilters/index.html b/modoboa/extensions/sievefilters/templates/sievefilters/index.html index 2430cafdc..817d6fd51 100644 --- a/modoboa/extensions/sievefilters/templates/sievefilters/index.html +++ b/modoboa/extensions/sievefilters/templates/sievefilters/index.html @@ -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 }); diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index 31fce1b29..0352a23d4 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -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 = '' % \ (_("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 ) diff --git a/modoboa/extensions/webmail/forms.py b/modoboa/extensions/webmail/forms.py index fb0ba3729..aa6a0bb04 100644 --- a/modoboa/extensions/webmail/forms.py +++ b/modoboa/extensions/webmail/forms.py @@ -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")) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index 8ddd4c5b8..829c477ac 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -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) { diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 4c7c29257..5291f8a7a 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -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", { diff --git a/modoboa/lib/events.py b/modoboa/lib/events.py index 66aa23270..b2e778bbb 100644 --- a/modoboa/lib/events.py +++ b/modoboa/lib/events.py @@ -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 diff --git a/modoboa/lib/exceptions.py b/modoboa/lib/exceptions.py index 026b72bd7..82c1135f0 100644 --- a/modoboa/lib/exceptions.py +++ b/modoboa/lib/exceptions.py @@ -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 diff --git a/modoboa/lib/formutils.py b/modoboa/lib/formutils.py index fa4e62cb3..f74fbec32 100644 --- a/modoboa/lib/formutils.py +++ b/modoboa/lib/formutils.py @@ -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: diff --git a/modoboa/lib/middleware.py b/modoboa/lib/middleware.py index 7b7fc1f15..44f08dd07 100644 --- a/modoboa/lib/middleware.py +++ b/modoboa/lib/middleware.py @@ -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 ) diff --git a/modoboa/lib/parameters.py b/modoboa/lib/parameters.py index 97a3ab270..61d3372d7 100644 --- a/modoboa/lib/parameters.py +++ b/modoboa/lib/parameters.py @@ -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 diff --git a/modoboa/lib/permissions.py b/modoboa/lib/permissions.py index ce5dcb9d7..8b93a2d8c 100644 --- a/modoboa/lib/permissions.py +++ b/modoboa/lib/permissions.py @@ -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]) diff --git a/modoboa/lib/singleton.py b/modoboa/lib/singleton.py new file mode 100644 index 000000000..3fd8f0c17 --- /dev/null +++ b/modoboa/lib/singleton.py @@ -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 diff --git a/modoboa/lib/tests.py b/modoboa/lib/tests.py index 0a0330294..8601f8682 100644 --- a/modoboa/lib/tests.py +++ b/modoboa/lib/tests.py @@ -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): diff --git a/modoboa/static/js/confirmation.js b/modoboa/static/js/confirmation.js index 59bd9314d..3d6296be9 100644 --- a/modoboa/static/js/confirmation.js +++ b/modoboa/static/js/confirmation.js @@ -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(); } diff --git a/modoboa/static/js/creation_wizard.js b/modoboa/static/js/creation_wizard.js index 25c4d2cce..a04e4bb82 100644 --- a/modoboa/static/js/creation_wizard.js +++ b/modoboa/static/js/creation_wizard.js @@ -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); \ No newline at end of file diff --git a/modoboa/static/js/global.js b/modoboa/static/js/global.js index 1016c0672..5b28f076b 100644 --- a/modoboa/static/js/global.js +++ b/modoboa/static/js/global.js @@ -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 = $("", { + "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 = $("", { - "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); } }); } diff --git a/modoboa/static/js/twocols_nav.js b/modoboa/static/js/twocols_nav.js index d8d3ed337..8c955c2f4 100644 --- a/modoboa/static/js/twocols_nav.js +++ b/modoboa/static/js/twocols_nav.js @@ -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 = $("", { - "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) {