From 9ee200ea5b2fc9267514ea15e7e004addde9cc1c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 19 Nov 2016 16:45:09 +0100 Subject: [PATCH 01/15] WIP. --- modoboa/admin/api.py | 4 - modoboa/admin/app_settings.py | 2 +- modoboa/admin/callbacks.py | 201 ------------------ modoboa/admin/forms/account.py | 18 +- modoboa/admin/forms/domain.py | 5 +- modoboa/admin/handlers.py | 188 +++++++++++++++- modoboa/admin/models/mailbox.py | 16 -- modoboa/admin/signals.py | 6 +- modoboa/admin/templatetags/admin_tags.py | 24 ++- modoboa/admin/views/identity.py | 3 +- modoboa/core/__init__.py | 66 +----- modoboa/core/apps.py | 33 +-- modoboa/core/constants.py | 41 ++++ modoboa/core/context_processors.py | 9 +- modoboa/core/handlers.py | 59 ++++- .../management/commands/load_initial_data.py | 27 ++- modoboa/core/models.py | 58 ++--- modoboa/core/signals.py | 31 +++ modoboa/core/templates/core/user_index.html | 2 +- modoboa/core/templatetags/core_tags.py | 54 +++-- modoboa/core/views/admin.py | 16 +- modoboa/core/views/auth.py | 23 +- modoboa/core/views/user.py | 13 +- modoboa/lib/permissions.py | 18 +- modoboa/lib/templatetags/lib_tags.py | 11 +- modoboa/limits/apps.py | 1 - modoboa/limits/controls.py | 75 ------- modoboa/limits/general_callbacks.py | 62 ------ modoboa/limits/handlers.py | 116 ++++++++++ modoboa/limits/utils.py | 15 ++ modoboa/parameters/tools.py | 1 - modoboa/relaydomains/constants.py | 12 ++ modoboa/relaydomains/general_callbacks.py | 35 --- modoboa/relaydomains/handlers.py | 26 +++ modoboa/urls.py | 14 +- 35 files changed, 645 insertions(+), 640 deletions(-) delete mode 100644 modoboa/limits/controls.py create mode 100644 modoboa/relaydomains/constants.py diff --git a/modoboa/admin/api.py b/modoboa/admin/api.py index 24684c4fa..08facd697 100644 --- a/modoboa/admin/api.py +++ b/modoboa/admin/api.py @@ -75,10 +75,6 @@ class AccountViewSet(viewsets.ModelViewSet): queryset = queryset.filter(mailbox__domain__name=domain) return queryset - def perform_destroy(self, instance): - """Add custom args to delete call.""" - instance.delete(self.request.user) - @detail_route(methods=["put"]) def password(self, request, pk=None): """Change account password.""" diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index f695ee673..9f530be27 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -18,7 +18,7 @@ ADMIN_EVENTS = [ "DomainDeleted", "DomainOwnershipRemoved", "GetTagsForDomain", - "ExtraDomainMenuEntries", + "ExtraDomainFilters", "ExtraDomainQsetFilters", "ExtraDomainTypes", diff --git a/modoboa/admin/callbacks.py b/modoboa/admin/callbacks.py index 59e59313a..b092c2516 100644 --- a/modoboa/admin/callbacks.py +++ b/modoboa/admin/callbacks.py @@ -1,208 +1,7 @@ """Event callbacks for modoboa admin.""" -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _, ugettext_lazy - -from modoboa.core import signals as core_signals from modoboa.lib import events -from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import PermDeniedException, BadRequest, Conflict -from modoboa.parameters import tools as param_tools from .models import ( Domain, DomainAlias, Mailbox, Alias ) - - -@events.observe("AdminMenuDisplay") -def admin_menu(target, user): - if target != "top_menu": - return [] - entries = [] - if user.has_perm("admin.view_domains"): - entries += [ - {"name": "domains", - "url": reverse("admin:domain_list"), - "label": _("Domains")} - ] - conditions = ( - user.has_perm("core.add_user"), - user.has_perm("admin.add_alias") - ) - if any(conditions): - entries += [ - {"name": "identities", - "url": reverse("admin:identity_list"), - "label": _("Identities")}, - ] - return entries - - -@events.observe("UserMenuDisplay") -def user_menu(target, user): - if target != "uprefs_menu": - return [] - if not hasattr(user, "mailbox"): - return [] - return [ - {"name": "forward", - "class": "ajaxnav", - "url": "forward/", - "label": ugettext_lazy("Forward")} - ] - - -@events.observe("RoleChanged") -def grant_access_to_all_objects(user, role): - from django.contrib.contenttypes.models import ContentType - from modoboa.lib.permissions import grant_access_to_objects - from modoboa.core.models import User - - if role != "SuperAdmins": - return - grant_access_to_objects( - user, User.objects.all(), - ContentType.objects.get_for_model(User) - ) - grant_access_to_objects( - user, Domain.objects.all(), - ContentType.objects.get_for_model(Domain) - ) - grant_access_to_objects( - user, DomainAlias.objects.all(), - ContentType.objects.get_for_model(DomainAlias) - ) - grant_access_to_objects( - user, Mailbox.objects.all(), - ContentType.objects.get_for_model(Mailbox) - ) - grant_access_to_objects( - user, Alias.objects.all(), - ContentType.objects.get_for_model(Alias) - ) - - -@events.observe("AccountExported") -def export_admin_domains(admin): - result = [admin.mailbox.quota] \ - if hasattr(admin, "mailbox") else [""] - if admin.role != "DomainAdmins": - return result - return result + [dom.name for dom in Domain.objects.get_for_admin(admin)] - - -@events.observe("AccountImported") -def import_account_mailbox(user, account, row): - """Handle extra fields when an account is imported. - - Expected fields: - - email address; quota; [domain; ...] - - :param User user: user importing the account - :param User account: account being imported - :param list rom: list of fields (strings) - """ - account.email = row[0].strip() - if account.email: - mailbox, domname = split_mailbox(account.email) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise BadRequest( - _("Account import failed (%s): domain does not exist" - % account.username) - ) - if not user.can_access(domain): - raise PermDeniedException - core_signals.can_create_object.send( - sender="import", context=user, object_type="mailboxes") - core_signals.can_create_object.send( - sender="import", context=domain, object_type="mailboxes") - account.save() - if Mailbox.objects.filter(address=mailbox, domain=domain).exists(): - raise Conflict( - _("Mailbox {} already exists").format(account.email)) - if len(row) == 1: - quota = None - else: - try: - quota = int(row[1].strip()) - except ValueError: - raise BadRequest( - _("Account import failed (%s): wrong quota value" - % account.username) - ) - use_domain_quota = True if not quota else False - mb = Mailbox(address=mailbox, domain=domain, - user=account, use_domain_quota=use_domain_quota) - mb.set_quota( - quota, override_rules=user.has_perm("admin.change_domain") - ) - mb.save(creator=user) - if account.role == "DomainAdmins": - for domname in row[2:]: - try: - dom = Domain.objects.get(name=domname.strip()) - except Domain.DoesNotExist: - continue - dom.add_admin(account) - - -@events.observe("AccountAutoCreated") -def account_auto_created(user): - from modoboa.core.models import User - from modoboa.lib.permissions import grant_access_to_object - from .lib import check_if_domain_exists - - if not param_tools.get_global_parameter("auto_create_domain_and_mailbox"): - return - localpart, domname = split_mailbox(user.username) - if user.role != 'SimpleUsers' and domname is None: - return - sadmins = User.objects.filter(is_superuser=True) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - label = check_if_domain_exists( - domname, [(DomainAlias, _('domain alias'))]) - if label is not None: - return - domain = Domain(name=domname, enabled=True, quota=0) - domain.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, domain) - try: - mb = Mailbox.objects.get(domain=domain, address=localpart) - except Mailbox.DoesNotExist: - mb = Mailbox( - address=localpart, domain=domain, user=user, use_domain_quota=True - ) - mb.set_quota(override_rules=True) - mb.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, mb) - - -@events.observe("UserLogin") -def user_logged_in(request, username, password): - from modoboa.lib.cryptutils import encrypt - - if hasattr(request.user, "mailbox"): - request.session["password"] = encrypt(password) - - -@events.observe("AccountDeleted") -def account_deleted(account, byuser, **kwargs): - """'AccountDeleted' listener. - - When an account is deleted, we also need to remove its mailbox (if - any). - """ - if not hasattr(account, "mailbox"): - return - mb = account.mailbox - if not byuser.can_access(mb): - raise PermDeniedException - keep_mb_dir = kwargs.get("keep_mb_dir", True) - mb.delete(keepdir=keep_mb_dir) diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 920b0cd10..19afd9871 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -229,10 +229,14 @@ class AccountFormMail(forms.Form, DynamicForm): "quota": 3 } self.extra_fields = [] - result = events.raiseQueryEvent('ExtraFormFields', 'mailform', self.mb) - for fname, field in result: - self.fields[fname] = field - self.extra_fields.append(fname) + results = core_signals.extra_form_fields.send( + sender=self.__class__, form="mailform", instance=self.mb) + if results: + results = reduce( + lambda a, b: a + b, [result for result in results]) + for fname, field in results: + self.fields[fname] = field + self.extra_fields.append(fname) if self.mb is not None: self.fields["email"].required = True qset = self.mb.aliasrecipient_set.filter(alias__internal=False) @@ -380,9 +384,9 @@ class AccountFormMail(forms.Form, DynamicForm): self.cleaned_data["use_domain_quota"] = ( self.cleaned_data["quota_act"]) self.mb.update_from_dict(user, self.cleaned_data) - events.raiseEvent( - 'SaveExtraFormFields', 'mailform', self.mb, self.cleaned_data - ) + core_signals.extra_form_fields.send( + sender=self.__class__, form="mailform", instance=self.mb, + cleaned_data=self.cleaned_data) account.email = self.cleaned_data["email"] account.save() diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index a1c649526..445986f1e 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -193,8 +193,9 @@ class DomainFormOptions(forms.Form): def __init__(self, user, *args, **kwargs): super(DomainFormOptions, self).__init__(*args, **kwargs) - result = events.raiseQueryEvent("UserCanSetRole", user, "DomainAdmins") - if False in result: + results = core_signals.user_can_set_role.send( + sender=self.__class__, user=user, role="DomainAdmins") + if False in [result[1] for result in results]: self.fields = {} return diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index b7b6554e3..89bc051b6 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -1,10 +1,22 @@ """Django signal handlers for admin.""" +from django.core.urlresolvers import reverse from django.db.models import signals from django.dispatch import receiver +from django.utils.translation import ugettext as _ +from django.contrib.contenttypes.models import ContentType + +from modoboa.core import models as core_models from modoboa.core import signals as core_signals +from modoboa.lib.cryptutils import encrypt +from modoboa.lib.email_utils import split_mailbox +from modoboa.lib import exceptions +from modoboa.lib import permissions +from modoboa.lib import signals as lib_signals +from modoboa.parameters import tools as param_tools +from . import lib from . import models from . import postfix_maps @@ -74,7 +86,7 @@ def manage_alias_for_mailbox(sender, instance, **kwargs): @receiver(signals.pre_delete, sender=models.Mailbox) def mailbox_deleted_handler(sender, **kwargs): - """``Mailbox`` pre_delete signal receiver + """``Mailbox`` pre_delete signal receiver. In order to properly handle deletions (ie. we don't want to leave orphan records into the db), we define this custom receiver. @@ -85,7 +97,7 @@ def mailbox_deleted_handler(sender, **kwargs): from modoboa.lib import events from modoboa.lib.permissions import ungrant_access_to_object - mb = kwargs['instance'] + mb = kwargs["instance"] events.raiseEvent("MailboxDeleted", mb) ungrant_access_to_object(mb) for ralias in mb.aliasrecipient_set.select_related("alias"): @@ -93,6 +105,15 @@ def mailbox_deleted_handler(sender, **kwargs): ralias.delete() if not alias.aliasrecipient_set.exists(): alias.delete() + models.Quota.objects.filter(username=mb.full_address).delete() + request = lib_signals.get_request() + if not request.localconfig.parameters.get_value( + "handle_mailboxes", raise_exception=False): + return + keepdir = request.POST.get("keepdir", "false") == "true" + if keepdir: + return + mb.delete_dir() @receiver(signals.post_delete, sender=models.Mailbox) @@ -111,3 +132,166 @@ def register_postfix_maps(sender, **kwargs): postfix_maps.SenderLoginAliasMap, postfix_maps.SenderLoginMailboxMap, postfix_maps.SenderLoginMailboxExtraMap ] + + +@receiver(core_signals.account_auto_created) +def account_auto_created(sender, user, **kwargs): + """New account has been auto-created, build the rest.""" + if not param_tools.get_global_parameter("auto_create_domain_and_mailbox"): + return + localpart, domname = split_mailbox(user.username) + if user.role != 'SimpleUsers' and domname is None: + return + sadmins = core_models.User.objects.filter(is_superuser=True) + try: + domain = models.Domain.objects.get(name=domname) + except models.Domain.DoesNotExist: + label = lib.check_if_domain_exists( + domname, [(models.DomainAlias, _("domain alias"))]) + if label is not None: + return + domain = models.Domain(name=domname, enabled=True, quota=0) + domain.save(creator=sadmins[0]) + for su in sadmins[1:]: + permissions.grant_access_to_object(su, domain) + qset = models.Mailbox.objects.filter(domain=domain, address=localpart) + if not qset.exists(): + mb = models.Mailbox( + address=localpart, domain=domain, user=user, use_domain_quota=True + ) + mb.set_quota(override_rules=True) + mb.save(creator=sadmins[0]) + for su in sadmins[1:]: + permissions.grant_access_to_object(su, mb) + + +@receiver(core_signals.account_exported) +def export_admin_domains(sender, user, **kwargs): + """Export administered domains too.""" + result = [user.mailbox.quota] if hasattr(user, "mailbox") else [""] + if user.role != "DomainAdmins": + return result + return result + [ + dom.name for dom in models.Domain.objects.get_for_admin(user)] + + +@receiver(core_signals.account_imported) +def import_account_mailbox(sender, user, account, row, **kwargs): + """Handle extra fields when an account is imported. + + Expected fields: + + email address; quota; [domain; ...] + + :param User user: user importing the account + :param User account: account being imported + :param list rom: list of fields (strings) + """ + account.email = row[0].strip() + if account.email: + mailbox, domname = split_mailbox(account.email) + domain = models.Domain.objects.filter(name=domname).first() + if not domain: + raise exceptions.BadRequest( + _("Account import failed (%s): domain does not exist") + % account.username + ) + if not user.can_access(domain): + raise exceptions.PermDeniedException + core_signals.can_create_object.send( + sender="import", context=user, object_type="mailboxes") + core_signals.can_create_object.send( + sender="import", context=domain, object_type="mailboxes") + account.save() + qset = models.Mailbox.objects.filter(address=mailbox, domain=domain) + if qset.exists(): + raise exceptions.Conflict( + _("Mailbox {} already exists").format(account.email)) + if len(row) == 1: + quota = None + else: + try: + quota = int(row[1].strip()) + except ValueError: + raise exceptions.BadRequest( + _("Account import failed (%s): wrong quota value") + % account.username + ) + use_domain_quota = True if not quota else False + mb = models.Mailbox( + address=mailbox, domain=domain, + user=account, use_domain_quota=use_domain_quota) + mb.set_quota( + quota, override_rules=user.has_perm("admin.change_domain") + ) + mb.save(creator=user) + if account.role == "DomainAdmins": + for domname in row[2:]: + try: + dom = models.Domain.objects.get(name=domname.strip()) + except models.Domain.DoesNotExist: + continue + dom.add_admin(account) + + +@receiver(core_signals.extra_admin_menu_entries) +def admin_menu(sender, location, user, **kwargs): + """Add extra menu entries for admins.""" + if location != "top_menu": + return [] + entries = [] + if user.has_perm("admin.view_domains"): + entries += [ + {"name": "domains", + "url": reverse("admin:domain_list"), + "label": _("Domains")} + ] + conditions = ( + user.has_perm("core.add_user"), + user.has_perm("admin.add_alias") + ) + if any(conditions): + entries += [ + {"name": "identities", + "url": reverse("admin:identity_list"), + "label": _("Identities")}, + ] + return entries + + +@receiver(core_signals.extra_user_menu_entries) +def user_menu(sender, location, user, **kwargs): + """Add extra menu entries for users.""" + if location != "uprefs_menu": + return [] + if not hasattr(user, "mailbox"): + return [] + return [ + {"name": "forward", + "class": "ajaxnav", + "url": "forward/", + "label": _("Forward")} + ] + + +@receiver(core_signals.user_login) +def user_logged_in(sender, username, password, **kwargs): + """Store user password in session.""" + request = lib_signals.get_request() + if hasattr(request.user, "mailbox"): + request.session["password"] = encrypt(password) + + +@receiver(core_signals.account_role_changed) +def grant_access_to_all_objects(sender, account, role, **kwargs): + """Grant all permissions if new role is SuperAdmin.""" + if role != "SuperAdmins": + return + perm_models = [ + core_models.User, models.Domain, models.DomainAlias, models.Mailbox, + models.Alias] + for model in perm_models: + permissions.grant_access_to_objects( + account, model.objects.all(), + ContentType.objects.get_for_model(model) + ) diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 3e7c10f8f..14bc1d18e 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -75,7 +75,6 @@ class MailboxManager(Manager): @python_2_unicode_compatible class Mailbox(AdminObject): - """User mailbox.""" address = models.CharField( @@ -326,21 +325,6 @@ class Mailbox(AdminObject): username=self.full_address) super(Mailbox, self).save(*args, **kwargs) - def delete(self, keepdir=False): - """Custom delete method - - We try to delete the associated quota in the same time (it may - has already been removed if we're deleting a domain). - - :param bool keepdir: delete the mailbox home dir on the - filesystem or not - - """ - Quota.objects.filter(username=self.full_address).delete() - if not keepdir: - self.delete_dir() - super(Mailbox, self).delete() - reversion.register(Mailbox) diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py index 7d9be0803..dae12a7e0 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -2,10 +2,14 @@ import django.dispatch -use_external_recipients = django.dispatch.Signal(providing_args=["recipients"]) +extra_admin_content = django.dispatch.Signal( + providing_args=["user", "location", "currentpage"]) extra_domain_actions = django.dispatch.Signal( providing_args=["user", "domain"]) extra_domain_dashboard_widgets = django.dispatch.Signal( providing_args=["user", "domain"]) +extra_domain_menu_entries = django.dispatch.Signal( + providing_args=["user"]) extra_account_dashboard_widgets = django.dispatch.Signal( providing_args=["user", "account"]) +use_external_recipients = django.dispatch.Signal(providing_args=["recipients"]) diff --git a/modoboa/admin/templatetags/admin_tags.py b/modoboa/admin/templatetags/admin_tags.py index 3cab73a96..a204105c4 100644 --- a/modoboa/admin/templatetags/admin_tags.py +++ b/modoboa/admin/templatetags/admin_tags.py @@ -6,7 +6,7 @@ from django.template.loader import render_to_string from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.lib import events +from modoboa.core import signals as core_signals from modoboa.lib.templatetags.lib_tags import render_link from modoboa.lib.web_utils import render_actions @@ -47,7 +47,10 @@ def domains_menu(selection, user, ajax_mode=True): "url": reverse("admin:domain_statistics")} ] if user.has_perm("admin.add_domain"): - entries += events.raiseQueryEvent("ExtraDomainMenuEntries", user) + extra_entries = signals.extra_domain_menu_entries.send( + sender="domains_menu", user=user) + for entry in extra_entries: + entries += entry[1] entries += [ {"name": "import", "label": _("Import"), @@ -157,7 +160,11 @@ def identity_actions(user, ident): name = ident.__class__.__name__ objid = ident.id if name == "User": - actions = events.raiseQueryEvent("ExtraAccountActions", ident) + actions = [] + result = core_signals.extra_account_actions.send( + sender="identity_actions", account=ident) + for action in result: + actions += result[1] url = ( reverse("admin:account_change", args=[objid]) + "?active_tab=default" @@ -265,7 +272,10 @@ def gender(value, target): @register.simple_tag def get_extra_admin_content(user, target, currentpage): - res = events.raiseQueryEvent( - "ExtraAdminContent", user, target, currentpage - ) - return mark_safe("".join(res)) + results = signals.extra_admin_content.send( + sender="get_extra_admin_content", + user=user, location=target, currentpage=currentpage) + if not results: + return "" + results = reduce(lambda a, b: a + b, [result[1] for result in results]) + return mark_safe("".join(results)) diff --git a/modoboa/admin/views/identity.py b/modoboa/admin/views/identity.py index 466ced291..f2401bceb 100644 --- a/modoboa/admin/views/identity.py +++ b/modoboa/admin/views/identity.py @@ -188,8 +188,7 @@ def editaccount(request, pk): @login_required @permission_required("core.delete_user") def delaccount(request, pk): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - User.objects.get(pk=pk).delete(request.user, keep_mb_dir=keepdir) + User.objects.get(pk=pk).delete() return render_to_json_response( ungettext("Account deleted", "Accounts deleted", 1) ) diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index 95ccfde83..0f5234285 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -1,67 +1,3 @@ - -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _ - -from modoboa.lib import events - - -PERMISSIONS = { - "SimpleUsers": [], - "DomainAdmins": [ - ["core", "user", "add_user"], - ["core", "user", "change_user"], - ["core", "user", "delete_user"], - ["admin", "domain", "view_domains"], - ["admin", "domain", "view_domain"], - ["admin", "mailbox", "add_mailbox"], - ["admin", "mailbox", "change_mailbox"], - ["admin", "mailbox", "delete_mailbox"], - ["admin", "alias", "add_alias"], - ["admin", "alias", "change_alias"], - ["admin", "alias", "delete_alias"], - ["admin", "mailbox", "view_mailboxes"], - ["admin", "alias", "view_aliases"], - ], - "Resellers": [ - ["core", "user", "add_user"], - ["core", "user", "change_user"], - ["core", "user", "delete_user"], - ["admin", "domain", "view_domains"], - ["admin", "domain", "view_domain"], - ["admin", "mailbox", "add_mailbox"], - ["admin", "mailbox", "change_mailbox"], - ["admin", "mailbox", "delete_mailbox"], - ["admin", "alias", "add_alias"], - ["admin", "alias", "change_alias"], - ["admin", "alias", "delete_alias"], - ["admin", "mailbox", "view_mailboxes"], - ["admin", "alias", "view_aliases"], - ["admin", "domain", "view_domains"], - ["admin", "domain", "add_domain"], - ["admin", "domain", "change_domain"], - ["admin", "domain", "delete_domain"], - ["admin", "domainalias", "add_domainalias"], - ["admin", "domainalias", "change_domainalias"], - ["admin", "domainalias", "delete_domainalias"], - ] -} - - -@events.observe("TopNotifications") -def check_for_new_versions(request, include_all): - """Check if new versions are available.""" - from modoboa.core.utils import check_for_updates - - if not request.user.is_superuser: - return [] - status, extensions = check_for_updates(request) - if not status: - return [{"id": "newversionavailable"}] if include_all else [] - return [{ - "id": "newversionavailable", - "url": reverse("core:index") + "#info/", - "text": _("One or more updates are available"), - "level": "info", - }] +"""App config for core.""" default_app_config = "modoboa.core.apps.CoreConfig" diff --git a/modoboa/core/apps.py b/modoboa/core/apps.py index df124bd32..0dca0a28f 100644 --- a/modoboa/core/apps.py +++ b/modoboa/core/apps.py @@ -6,38 +6,8 @@ from django.utils.translation import ugettext_lazy BASE_EVENTS = [ "AccountCreated", - "AccountAutoCreated", + "AccountModified", - "AccountDeleted", - "AccountExported", - "AccountImported", - "PasswordUpdated", - "ExtraAccountActions", - "RoleChanged", - "GetExtraRoles", - "GetExtraRolePermissions", - "PasswordChange", - "UserCanSetRole", - - "InitialDataLoaded", - - "UserMenuDisplay", - "AdminMenuDisplay", - "GetStaticContent", - - "UserLogin", - "UserLogout", - - "GetAnnouncement", - - "TopNotifications", - "ExtraAdminContent", - - "ExtraUprefsRoutes", - "ExtraUprefsJS", - - "ExtraFormFields", - "SaveExtraFormFields", ] @@ -57,7 +27,6 @@ def load_core_settings(): class CoreConfig(AppConfig): - """App configuration.""" name = "modoboa.core" diff --git a/modoboa/core/constants.py b/modoboa/core/constants.py index 8adce607f..6a9d2be6e 100644 --- a/modoboa/core/constants.py +++ b/modoboa/core/constants.py @@ -42,3 +42,44 @@ LDAP_GROUP_TYPES = ( ("posixgroup", "PosixGroup"), ("groupofnames", "GroupOfNames"), ) + +PERMISSIONS = { + "SimpleUsers": [], + "DomainAdmins": [ + ["core", "user", "add_user"], + ["core", "user", "change_user"], + ["core", "user", "delete_user"], + ["admin", "domain", "view_domains"], + ["admin", "domain", "view_domain"], + ["admin", "mailbox", "add_mailbox"], + ["admin", "mailbox", "change_mailbox"], + ["admin", "mailbox", "delete_mailbox"], + ["admin", "alias", "add_alias"], + ["admin", "alias", "change_alias"], + ["admin", "alias", "delete_alias"], + ["admin", "mailbox", "view_mailboxes"], + ["admin", "alias", "view_aliases"], + ], + "Resellers": [ + ["core", "user", "add_user"], + ["core", "user", "change_user"], + ["core", "user", "delete_user"], + ["admin", "domain", "view_domains"], + ["admin", "domain", "view_domain"], + ["admin", "mailbox", "add_mailbox"], + ["admin", "mailbox", "change_mailbox"], + ["admin", "mailbox", "delete_mailbox"], + ["admin", "alias", "add_alias"], + ["admin", "alias", "change_alias"], + ["admin", "alias", "delete_alias"], + ["admin", "mailbox", "view_mailboxes"], + ["admin", "alias", "view_aliases"], + ["admin", "domain", "view_domains"], + ["admin", "domain", "add_domain"], + ["admin", "domain", "change_domain"], + ["admin", "domain", "delete_domain"], + ["admin", "domainalias", "add_domainalias"], + ["admin", "domainalias", "change_domainalias"], + ["admin", "domainalias", "delete_domainalias"], + ] +} diff --git a/modoboa/core/context_processors.py b/modoboa/core/context_processors.py index 69e3a338a..d3e3d3690 100644 --- a/modoboa/core/context_processors.py +++ b/modoboa/core/context_processors.py @@ -1,6 +1,6 @@ """Custom context processors.""" -from modoboa.lib import events +from . import signals def top_notifications(request): @@ -9,8 +9,11 @@ def top_notifications(request): return {} interval = request.localconfig.parameters.get_value( "top_notifications_check_interval") + notifications = signals.get_top_notifications.send( + sender="top_notifications", include_all=False) + notifications = reduce( + lambda a, b: a + b, [notif[1] for notif in notifications]) return { "notifications_check_interval": interval * 1000, - "top_notifications": events.raiseQueryEvent( - "TopNotifications", request, False) + "top_notifications": notifications } diff --git a/modoboa/core/handlers.py b/modoboa/core/handlers.py index 576ed3798..477795b9b 100644 --- a/modoboa/core/handlers.py +++ b/modoboa/core/handlers.py @@ -2,7 +2,8 @@ import logging -from django.db.models.signals import post_delete +from django.core.urlresolvers import reverse +from django.db.models import signals from django.dispatch import receiver from django.utils.translation import ugettext as _ @@ -10,7 +11,13 @@ from django.contrib.sites import models as sites_models from reversion import revisions as reversion +from modoboa.lib import exceptions +from modoboa.lib import permissions +from modoboa.lib.signals import get_request + from . import models +from . import signals as core_signals +from . import utils @receiver(reversion.post_revision_commit) @@ -44,14 +51,13 @@ def post_revision_commit(sender, **kwargs): getattr(logger, level)(message) -@receiver(post_delete) +@receiver(signals.post_delete) def log_object_removal(sender, instance, **kwargs): """Custom post-delete hook. We want to know who was responsible for an object deletion. """ from reversion.models import Version - from modoboa.lib.signals import get_request if not reversion.is_registered(sender): return @@ -75,3 +81,50 @@ def create_local_config(sender, **kwargs): return models.LocalConfig.objects.create( site=sites_models.Site.objects.get_current()) + + +@receiver(signals.pre_delete, sender=models.User) +def update_permissions(sender, instance, **kwargs): + """Permissions cleanup.""" + from_user = get_request().user + if from_user == instance: + raise exceptions.PermDeniedException( + _("You can't delete your own account") + ) + + if not from_user.can_access(instance): + raise exceptions.PermDeniedException + + # We send an additional signal before permissions are removed + core_signals.account_deleted.send( + sender="update_permissions", user=instance) + owner = permissions.get_object_owner(instance) + if owner == instance: + # The default admin is being removed... + owner = from_user + # Change ownership of existing objects + for ooentry in instance.objectaccess_set.filter(is_owner=True): + if ooentry.content_object is not None: + permissions.grant_access_to_object( + owner, ooentry.content_object, True) + permissions.ungrant_access_to_object( + ooentry.content_object, instance) + # Remove existing permissions on this user + permissions.ungrant_access_to_object(instance) + + +@receiver(core_signals.get_top_notifications) +def check_for_new_versions(sender, include_all, **kwargs): + """Check if new versions are available.""" + request = get_request() + if not request.user.is_superuser: + return [] + status, extensions = utils.check_for_updates(request) + if not status: + return [{"id": "newversionavailable"}] if include_all else [] + return [{ + "id": "newversionavailable", + "url": reverse("core:index") + "#info/", + "text": _("One or more updates are available"), + "level": "info", + }] diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index 09dbcd7bb..d9df3d803 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -8,14 +8,14 @@ from django.contrib.auth.models import Group from django.core.management.base import BaseCommand -from modoboa.core import PERMISSIONS -from modoboa.core.extensions import exts_pool from modoboa.lib.cryptutils import random_key -from modoboa.lib import events from modoboa.lib.permissions import add_permissions_to_group import modoboa.relaydomains.models as relay_models +from ... import constants +from ... import extensions from ... import models +from ... import signals class Command(BaseCommand): @@ -54,27 +54,26 @@ class Command(BaseCommand): for service_name in ["relay", "smtp"]: relay_models.Service.objects.get_or_create(name=service_name) - exts_pool.load_all() + extensions.exts_pool.load_all() - superadmin = models.User.objects.filter(is_superuser=True).first() - groups = PERMISSIONS.keys() + [ - role[0] for role - in events.raiseQueryEvent("GetExtraRoles", superadmin, None) - ] + groups = constants.PERMISSIONS.keys() for groupname in groups: group, created = Group.objects.get_or_create(name=groupname) + results = signals.extra_role_permissions.send( + sender=self.__class__, role=groupname) permissions = ( - PERMISSIONS.get(groupname, []) + - events.raiseQueryEvent("GetExtraRolePermissions", groupname) + constants.PERMISSIONS.get(groupname, []) + + reduce(lambda a, b: a + b, [result[1] for result in results]) ) if not permissions: continue add_permissions_to_group(group, permissions) - for extname in exts_pool.extensions.keys(): - extension = exts_pool.get_extension(extname) + for extname in extensions.exts_pool.extensions.keys(): + extension = extensions.exts_pool.get_extension(extname) extension.load_initial_data() - events.raiseEvent("InitialDataLoaded", extname) + signals.initial_data_loaded.send( + sender=self.__class__, extname=extname) if options["extra_fixtures"]: from modoboa.admin import factories diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 5e4c1a7f6..29617c4ed 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -27,6 +27,7 @@ from modoboa.lib.exceptions import ( from modoboa.parameters import tools as param_tools from . import constants +from . import signals try: @@ -103,41 +104,6 @@ class User(PermissionsMixin): super(User, self).__init__(*args, **kwargs) self.parameters = param_tools.Manager("user", self._parameters) - def delete(self, fromuser, *args, **kwargs): - """Custom delete method - - To check permissions properly, we need to make a distinction - between 2 cases: - - * If the user owns a mailbox, the check is made on that object - (useful for domain admins) - - * Otherwise, the check is made on the user - """ - from modoboa.lib.permissions import \ - get_object_owner, grant_access_to_object, ungrant_access_to_object - - if fromuser == self: - raise PermDeniedException( - _("You can't delete your own account") - ) - - if not fromuser.can_access(self): - raise PermDeniedException - - owner = get_object_owner(self) - if owner == self: - # The default admin is being removed... - owner = fromuser - for ooentry in self.objectaccess_set.filter(is_owner=True): - if ooentry.content_object is not None: - grant_access_to_object(owner, ooentry.content_object, True) - ungrant_access_to_object(ooentry.content_object, self) - - events.raiseEvent("AccountDeleted", self, fromuser, **kwargs) - ungrant_access_to_object(self) - super(User, self).delete() - def _crypt_password(self, raw_value): """Crypt the local password using the appropriate scheme. @@ -177,9 +143,9 @@ class User(PermissionsMixin): LDAPAuthBackend().update_user_password( self.username, curvalue, raw_value ) - events.raiseEvent( - "PasswordUpdated", self, raw_value, self.pk is None - ) + signals.account_password_updated.send( + sender=self.__class__, + account=self, password=raw_value, created=self.pk is None) def check_password(self, raw_value): """Compare raw_value to current password.""" @@ -329,7 +295,8 @@ class User(PermissionsMixin): """ if role is None or self.role == role: return - events.raiseEvent("RoleChanged", self, role) + signals.account_role_changed.send( + sender=self.__class__, account=self, role=role) self.groups.clear() if role == "SuperAdmins": self.is_superuser = True @@ -378,7 +345,7 @@ class User(PermissionsMixin): "account", loginname, password, first name, last name, enabled, role - Additional fields can be added using the *AccountImported* event. + Additional fields can be added using the *account_imported* signal. :param user: a ``core.User`` instance :param row: a list containing the expected information @@ -431,7 +398,8 @@ class User(PermissionsMixin): self.post_create(user) if len(row) < 8: return - events.raiseEvent("AccountImported", user, self, row[7:]) + signals.account_imported.send( + sender=self.__class__, user=user, account=self, row=row[7:]) def to_csv(self, csvwriter): """Export this account. @@ -450,7 +418,10 @@ class User(PermissionsMixin): self.role, self.email.encode("utf-8") ] - row += events.raiseQueryEvent("AccountExported", self) + results = signals.account_exported.send( + sender=self.__class__, user=self) + for result in results: + row += result[1] csvwriter.writerow(row) reversion.register(User) @@ -473,7 +444,8 @@ def populate_callback(user, group="SimpleUsers"): user.post_create(sadmins[0]) for su in sadmins[1:]: grant_access_to_object(su, user) - events.raiseEvent("AccountAutoCreated", user) + signals.account_auto_created.send( + sender="populate_callback", user=user) class ObjectAccess(models.Model): diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py index 3169a6313..c6d406739 100644 --- a/modoboa/core/signals.py +++ b/modoboa/core/signals.py @@ -2,6 +2,37 @@ import django.dispatch +account_auto_created = django.dispatch.Signal(providing_args=["user"]) +account_exported = django.dispatch.Signal(providing_args=["account"]) +account_imported = django.dispatch.Signal( + providing_args=["user", "account", "row"]) +account_deleted = django.dispatch.Signal(providing_args=["user"]) +account_role_changed = django.dispatch.Signal( + providing_args=["account", "role"]) +account_password_updated = django.dispatch.Signal( + providing_args=["account", "password", "created"]) +allow_password_change = django.dispatch.Signal(providing_args=["user"]) can_create_object = django.dispatch.Signal( providing_args=["context", "object_type", "count"]) +extra_account_actions = django.dispatch.Signal(providing_args=["account"]) +extra_admin_menu_entries = django.dispatch.Signal( + providing_args=["location", "user"]) +extra_form_fields = django.dispatch.Signal( + providing_args=["form", "instance"]) +extra_role_permissions = django.dispatch.Signal(providing_args=["role"]) +extra_static_content = django.dispatch.Signal( + providing_args=["caller", "st_type", "user"]) +extra_uprefs_routes = django.dispatch.Signal() +extra_user_menu_entries = django.dispatch.Signal( + providing_args=["location", "user"]) +get_announcements = django.dispatch.Signal(providing_args=["location"]) +get_top_notifications = django.dispatch.Signal( + providing_args=["include_all"]) +initial_data_loaded = django.dispatch.Signal(providing_args=["extname"]) register_postfix_maps = django.dispatch.Signal() +save_extra_form_fields = django.dispatch.Signal( + providing_args=["form", "instance", "cleaned_data"]) +user_can_set_role = django.dispatch.Signal( + providing_args=["user", "role", "account"]) +user_login = django.dispatch.Signal( + providing_args=["username", "password"]) diff --git a/modoboa/core/templates/core/user_index.html b/modoboa/core/templates/core/user_index.html index db4896e44..bf38bfb46 100644 --- a/modoboa/core/templates/core/user_index.html +++ b/modoboa/core/templates/core/user_index.html @@ -12,7 +12,7 @@ -"""] diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index b2ffad27d..75657ed29 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -2,6 +2,7 @@ from django.db.models import signals from django.dispatch import receiver +from django.template.loader import render_to_string from django.contrib.contenttypes.models import ContentType @@ -10,6 +11,7 @@ from modoboa.admin import signals as admin_signals from modoboa.core import models as core_models from modoboa.core import signals as core_signals from modoboa.lib import signals as lib_signals +from modoboa.lib import permissions from modoboa.parameters import tools as param_tools from . import lib @@ -99,3 +101,117 @@ def display_admin_limits(sender, user, account, **kwargs): account.userobjectlimit_set.select_related("content_type")) } }] + + +@receiver(core_signals.account_deleted) +def move_resource(sender, user, **kwargs): + """Move remaining resource to another user.""" + owner = permissions.get_object_owner(user) + if owner.is_superuser or owner.role != "Resellers": + return + utils.move_pool_resource(owner, user) + + +@receiver(core_signals.user_can_set_role) +def user_can_set_role(sender, user, role, account=None, **kwargs): + """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 role: role to check + :param ``User`` account: account modified (None on creation) + """ + condition = ( + not param_tools.get_global_parameter("enable_admin_limits") or + role != "DomainAdmins") + if condition: + return True + lname = "domain_admins" + condition = ( + user.is_superuser or + not user.userobjectlimit_set.get(name=lname).is_exceeded() + ) + if condition: + return True + if account is not None and account.role == role: + return True + return False + + +@receiver(core_signals.extra_static_content) +def get_static_content(sender, caller, st_type, user, **kwargs): + """Add extra static content.""" + condition = ( + not param_tools.get_global_parameter("enable_admin_limits") or + caller not in ["domains", "identities"] or + user.role in ["SuperAdmins", "SimpleUsers"] + ) + if condition: + return [] + if st_type == "css": + return [""" +"""] + return [""" + +"""] + + +@receiver(admin_signals.extra_admin_content) +def display_pool_usage(sender, user, location, currentpage, **kwargs): + """Display current usage.""" + condition = ( + not param_tools.get_global_parameter("enable_admin_limits") or + location != "leftcol" or user.is_superuser) + if condition: + return [] + if currentpage == "identities": + names = ["mailboxes", "mailbox_aliases"] + if user.has_perm("admin.add_domain"): + names += ["domain_admins"] + else: + exceptions = ["domain_admins", "mailboxes", "mailbox_aliases"] + names = [ + name for name, tpl in utils.get_user_limit_templates() + if name not in exceptions and + ("required_role" not in tpl or + tpl["required_role"] == user.role) + ] + + limits = user.userobjectlimit_set.filter(name__in=names, max_value__gt=0) + if len(limits) == 0: + return [] + return [ + render_to_string("limits/poolusage.html", + dict(limits=limits)) + ] + + +@receiver(core_signals.account_role_changed) +def move_pool_resource(sender, account, role, **kwargs): + """Move remaining resource to owner if needed.""" + owner = permissions.get_object_owner(account) + if not owner or owner.is_superuser or owner.role != "Resellers": + # Domain admins can't change the role so nothing to check. + return + + if role not in ["DomainAdmins", "Resellers"]: + utils.move_pool_resource(owner, account) diff --git a/modoboa/limits/utils.py b/modoboa/limits/utils.py index 6cbf3d1b0..25cb45629 100644 --- a/modoboa/limits/utils.py +++ b/modoboa/limits/utils.py @@ -12,3 +12,18 @@ def get_user_limit_templates(): def get_domain_limit_templates(): """Return defined templates.""" return constants.DEFAULT_DOMAIN_LIMITS.items() + + +def move_pool_resource(owner, user): + """Move resource from one pool to another. + + When an account doesn't need a pool anymore, we move the + associated resource to the pool of its owner. + """ + for name, ltpl in get_user_limit_templates(): + l = user.userobjectlimit_set.get(name=name) + if l.max_value < 0: + continue + ol = owner.userobjectlimit_set.get(name=name) + ol.max_value += l.max_value + ol.save() diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py index b7c173ae3..1be26d468 100644 --- a/modoboa/parameters/tools.py +++ b/modoboa/parameters/tools.py @@ -174,7 +174,6 @@ def get_global_parameter(name, app=None, **kwargs): :param name: the parameter's name :param app: the application owning the parameter :return: the corresponding value - """ if app is None: app = guess_extension_name() diff --git a/modoboa/relaydomains/constants.py b/modoboa/relaydomains/constants.py new file mode 100644 index 000000000..951bfa1af --- /dev/null +++ b/modoboa/relaydomains/constants.py @@ -0,0 +1,12 @@ +"""Relaydomains app constants.""" + +PERMISSIONS = { + "Resellers": [ + ("relaydomains", "relaydomain", "add_relaydomain"), + ("relaydomains", "relaydomain", "change_relaydomain"), + ("relaydomains", "relaydomain", "delete_relaydomain"), + ("relaydomains", "service", "add_service"), + ("relaydomains", "service", "change_service"), + ("relaydomains", "service", "delete_service") + ] +} diff --git a/modoboa/relaydomains/general_callbacks.py b/modoboa/relaydomains/general_callbacks.py index f1342d564..56f3e7de3 100644 --- a/modoboa/relaydomains/general_callbacks.py +++ b/modoboa/relaydomains/general_callbacks.py @@ -1,46 +1,11 @@ """General event callbacks.""" -from django.conf import settings -from django.template import Template, Context from django.utils.translation import ugettext_lazy, ugettext as _ from modoboa.lib import events from .forms import RelayDomainWizardStep, RelayDomainFormGeneral -PERMISSIONS = { - "Resellers": [ - ("relaydomains", "relaydomain", "add_relaydomain"), - ("relaydomains", "relaydomain", "change_relaydomain"), - ("relaydomains", "relaydomain", "delete_relaydomain"), - ("relaydomains", "service", "add_service"), - ("relaydomains", "service", "change_service"), - ("relaydomains", "service", "delete_service") - ] -} - - -@events.observe("GetExtraRolePermissions") -def extra_permissions(rolename): - """Return extra permissions for Resellers.""" - return PERMISSIONS.get(rolename, []) - - -@events.observe('GetStaticContent') -def static_content(caller, st_type, user): - if caller != 'domains' or st_type != 'js': - return [] - - t = Template(""" - -""") - return [t.render(Context({'STATIC_URL': settings.STATIC_URL}))] - @events.observe('ExtraDomainFilters') def extra_domain_filters(): diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py index bd326199e..c2d5adcee 100644 --- a/modoboa/relaydomains/handlers.py +++ b/modoboa/relaydomains/handlers.py @@ -1,13 +1,16 @@ """Django signal handlers for relaydomains.""" +from django.conf import settings from django.db.models import signals from django.dispatch import receiver +from django.template import Template, Context from modoboa.admin import models as admin_models from modoboa.admin.signals import use_external_recipients from modoboa.core import signals as core_signals from modoboa.lib.email_utils import split_mailbox +from . import constants from . import models from . import postfix_maps @@ -52,3 +55,26 @@ def register_postfix_maps(sender, **kwargs): postfix_maps.SplitedDomainsTransportMap, postfix_maps.RelayRecipientVerification ] + + +@receiver(core_signals.extra_role_permissions) +def extra_role_permissions(sender, role, **kwargs): + """Add permissions to the Resellers group.""" + return constants.PERMISSIONS.get(role, []) + + +@receiver(core_signals.extra_static_content) +def static_content(sender, caller, st_type, user, **kwargs): + """Add extra static content.""" + if caller != "domains" or st_type != "js": + return [] + + t = Template(""" + +""") + return [t.render(Context({"STATIC_URL": settings.STATIC_URL}))] diff --git a/modoboa/urls.py b/modoboa/urls.py index 8568c5558..e01bc7268 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -2,7 +2,7 @@ from django.conf import settings from django.conf.urls import patterns, include, url from modoboa.core.extensions import exts_pool -from modoboa.lib import events +from modoboa.core import signals as core_signals urlpatterns = patterns( @@ -22,10 +22,14 @@ urlpatterns += patterns( *exts_pool.load_all() ) -urlpatterns += patterns( - '', - *events.raiseQueryEvent("ExtraUprefsRoutes") -) +extra_routes = core_signals.extra_uprefs_routes.send(sender="urls") +if extra_routes: + extra_routes = reduce( + lambda a, b: a + b, [route for route in extra_routes]) + urlpatterns += patterns( + '', + *extra_routes + ) # API urls urlpatterns += patterns( From 5480a96a732e8950325e6e121c0917486992d95c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 23 Nov 2016 11:31:57 +0100 Subject: [PATCH 02/15] WIP. --- modoboa/admin/app_settings.py | 41 +----- modoboa/admin/callbacks.py | 7 - modoboa/admin/forms/account.py | 22 +-- modoboa/admin/forms/domain.py | 33 +++-- modoboa/admin/handlers.py | 2 - modoboa/admin/lib.py | 18 +-- .../commands/subcommands/_import.py | 8 +- modoboa/admin/models/base.py | 3 - modoboa/admin/models/domain.py | 14 +- modoboa/admin/models/mailbox.py | 2 - modoboa/admin/signals.py | 22 ++- modoboa/admin/views/domain.py | 13 +- modoboa/admin/views/identity.py | 6 +- modoboa/admin/views/import_.py | 14 +- modoboa/core/apps.py | 8 -- modoboa/core/models.py | 2 - modoboa/core/signals.py | 1 + modoboa/core/views/auth.py | 1 - modoboa/lib/events.py | 135 ------------------ modoboa/limits/apps.py | 7 - modoboa/limits/general_callbacks.py | 75 ---------- modoboa/limits/handlers.py | 70 +++++++++ modoboa/limits/utils.py | 1 - modoboa/relaydomains/apps.py | 13 -- modoboa/relaydomains/general_callbacks.py | 85 ----------- modoboa/relaydomains/handlers.py | 89 +++++++++++- 26 files changed, 246 insertions(+), 446 deletions(-) delete mode 100644 modoboa/admin/callbacks.py delete mode 100644 modoboa/lib/events.py delete mode 100644 modoboa/limits/general_callbacks.py delete mode 100644 modoboa/relaydomains/general_callbacks.py diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index 9f530be27..3bbd13b5d 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -1,4 +1,4 @@ -"""Modoboa admin settings and events.""" +"""Modoboa admin settings.""" import os @@ -10,43 +10,6 @@ from modoboa.lib.form_utils import YesNoField, SeparatorField from modoboa.lib.sysutils import exec_cmd from modoboa.parameters import forms as param_forms -from ..lib import events - -ADMIN_EVENTS = [ - "DomainCreated", - "DomainModified", - "DomainDeleted", - "DomainOwnershipRemoved", - "GetTagsForDomain", - - "ExtraDomainFilters", - "ExtraDomainQsetFilters", - "ExtraDomainTypes", - "ExtraDomainWizardSteps", - "GetDomainModifyLink", - "CheckDomainName", - - "DomainAliasCreated", - "DomainAliasDeleted", - - "MailboxCreated", - "MailboxDeleted", - "MailboxModified", - - "MailboxAliasCreated", - "MailboxAliasDeleted", - - "ExtraDomainForm", - "FillDomainInstances", - - "ExtraAccountForm", - "CheckExtraAccountForm", - "FillAccountInstances", - - "ExtraDomainImportHelp", - "ImportObject" -] - class AdminParametersForm(param_forms.AdminParametersForm): app = "admin" @@ -178,5 +141,3 @@ def load_admin_settings(): param_tools.registry.add( "global", AdminParametersForm, ugettext_lazy("Administration")) - events.declare(ADMIN_EVENTS) - from . import callbacks diff --git a/modoboa/admin/callbacks.py b/modoboa/admin/callbacks.py deleted file mode 100644 index b092c2516..000000000 --- a/modoboa/admin/callbacks.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Event callbacks for modoboa admin.""" - -from modoboa.lib import events - -from .models import ( - Domain, DomainAlias, Mailbox, Alias -) diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 19afd9871..417a293ed 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -11,7 +11,6 @@ from passwords.fields import PasswordField from modoboa.core import signals as core_signals from modoboa.core.models import User -from modoboa.lib import events from modoboa.lib.email_utils import split_mailbox from modoboa.lib import exceptions as lib_exceptions from modoboa.lib import fields as lib_fields @@ -24,6 +23,7 @@ from modoboa.lib.web_utils import render_to_json_response from modoboa.parameters import tools as param_tools from .. import models +from .. import signals class AccountFormGeneral(forms.ModelForm): @@ -460,11 +460,13 @@ class AccountForm(TabForms): "formtpl": "admin/permsform.html", "cls": AccountPermissionsForm} ] - cbargs = [self.user] + cbargs = {"user": self.user} if "instances" in kwargs: - cbargs += [kwargs["instances"]["general"]] - self.forms += events.raiseQueryEvent("ExtraAccountForm", *cbargs) - + cbargs["account"] = kwargs["instances"]["general"] + results = signals.extra_account_forms.send( + sender=self.__class__, **cbargs) + self.forms += reduce( + lambda a, b: a + b, [result[1] for result in results]) super(AccountForm, self).__init__(request, *args, **kwargs) def extra_context(self, context): @@ -493,9 +495,10 @@ class AccountForm(TabForms): return False return True - extra_forms = events.raiseQueryEvent( - "CheckExtraAccountForm", self.account, form) - if False in extra_forms: + results = signals.check_extra_account_form.send( + sender=self.__class__, account=self.account, form=form) + results = [result[1] for result in results] + if False in results: return False return True @@ -513,9 +516,6 @@ class AccountForm(TabForms): As forms interact with each other, it is simpler to make custom code to save them. """ - events.raiseEvent( - "AccountModified", self.instances["general"], self.account - ) self.forms[1]["instance"].save(self.user, self.account) if len(self.forms) <= 2: return diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index 445986f1e..785ac346c 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -7,7 +7,6 @@ from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.core import signals as core_signals from modoboa.core.models import User -from modoboa.lib import events from modoboa.lib.exceptions import Conflict from modoboa.lib.fields import DomainNameField from modoboa.lib.form_utils import ( @@ -16,11 +15,13 @@ from modoboa.lib.form_utils import ( from modoboa.lib.web_utils import render_to_json_response from modoboa.parameters import tools as param_tools +from .. import signals from ..lib import check_if_domain_exists from ..models import ( Domain, DomainAlias, Mailbox, Alias ) + DOMAIN_TYPES = [ ("domain", _("Domain")), ] @@ -60,9 +61,12 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): self.oldname = kwargs["instance"].name super(DomainFormGeneral, self).__init__(*args, **kwargs) - self.fields["type"].choices = ( - DOMAIN_TYPES + events.raiseQueryEvent("ExtraDomainTypes")) - + extra_domain_types = reduce( + lambda a, b: a + b, + [result[1] for result in signals.extra_domain_types.send( + sender=self.__class__)] + ) + self.fields["type"].choices = DOMAIN_TYPES + extra_domain_types self.field_widths = { "quota": 3 } @@ -87,8 +91,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): """Custom fields validation. We want to prevent duplicate names between domains and domain - aliases. Extensions have the possibility to declare other - objects (see *CheckDomainName* event). + aliases. The validation way is not very smart... """ @@ -279,11 +282,14 @@ class DomainForm(TabForms): "mandatory": True }) - cbargs = [self.user] + cbargs = {"user": self.user} if "instances" in kwargs: self.domain = kwargs["instances"]["general"] - cbargs += [self.domain] - self.forms += events.raiseQueryEvent("ExtraDomainForm", *cbargs) + cbargs["domain"] = self.domain + results = signals.extra_domain_forms.send( + sender=self.__class__, **cbargs) + self.forms += reduce( + lambda a, b: a + b, [result[1] for result in results]) if not self.forms: self.active_id = "admins" super(DomainForm, self).__init__(request, *args, **kwargs) @@ -326,8 +332,6 @@ class DomainForm(TabForms): f["instance"].save(self.request.user) def done(self): - if "general" in self.instances: - events.raiseEvent("DomainModified", self.instances["general"]) return render_to_json_response(_("Domain modified")) @@ -343,9 +347,10 @@ class DomainWizard(WizardForm): "admin/domain_general_form.html" ) ) - steps = events.raiseQueryEvent("ExtraDomainWizardSteps") - for step in steps: - self.add_step(step) + results = signals.extra_domain_wizard_steps.send(sender=self.__class__) + for result in results: + for step in result[1]: + self.add_step(step) self.add_step( WizardStep( "options", DomainFormOptions, _("Options"), diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index 89bc051b6..4da7e6304 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -94,11 +94,9 @@ def mailbox_deleted_handler(sender, **kwargs): It manually removes the mailbox from the aliases it is linked to and then remove all empty aliases. """ - from modoboa.lib import events from modoboa.lib.permissions import ungrant_access_to_object mb = kwargs["instance"] - events.raiseEvent("MailboxDeleted", mb) ungrant_access_to_object(mb) for ralias in mb.aliasrecipient_set.select_related("alias"): alias = ralias.alias diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index 932a1504e..1888f0826 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -11,11 +11,12 @@ from django.utils.translation import ugettext as _ from modoboa.core.models import User from modoboa.core import signals as core_signals -from modoboa.lib import events from modoboa.lib.exceptions import PermDeniedException from .models import Domain, DomainAlias, Alias +from . import signals + def needs_mailbox(): """Check if the current user owns at least one mailbox @@ -92,24 +93,23 @@ def get_domains(user, domfilter=None, searchquery=None, **extrafilters): q = Q(name__contains=searchquery) q |= Q(domainalias__name__contains=searchquery) domains = domains.filter(q).distinct() - qset_filters = events.raiseDictEvent( - "ExtraDomainQsetFilters", domfilter, extrafilters - ) - if qset_filters: + results = signals.extra_domain_qset_filters.send( + sender="get_domains", domfilter=domfilter, extrafilters=extrafilters) + if results: + qset_filters = {} + for result in results: + qset_filters.update(result[1]) domains = domains.filter(**qset_filters) return domains -def check_if_domain_exists(name, extra_checks=None): +def check_if_domain_exists(name, dtypes): """Check if a domain already exists. We not only look for domains, we also look for every object that could conflict with a domain (domain alias, etc.) """ - dtypes = events.raiseQueryEvent('CheckDomainName') - if extra_checks is not None: - dtypes = extra_checks + dtypes for dtype, label in dtypes: if dtype.objects.filter(name=name).exists(): return label diff --git a/modoboa/admin/management/commands/subcommands/_import.py b/modoboa/admin/management/commands/subcommands/_import.py index 83eecd522..b5fb3c49c 100644 --- a/modoboa/admin/management/commands/subcommands/_import.py +++ b/modoboa/admin/management/commands/subcommands/_import.py @@ -9,10 +9,10 @@ from django.core.management.base import BaseCommand, CommandError from modoboa.core import models as core_models from modoboa.core.extensions import exts_pool -from modoboa.lib import events from modoboa.lib.exceptions import Conflict from .... import lib +from .... import signals class ImportCommand(BaseCommand): @@ -59,11 +59,11 @@ class ImportCommand(BaseCommand): try: fct = getattr(lib, "import_%s" % row[0].strip()) except AttributeError: - fct = events.raiseQueryEvent( - "ImportObject", row[0].strip()) + fct = signals.import_object.send( + sender=self.__class__, objtype=row[0].strip()) if not fct: continue - fct = fct[0] + fct = fct[0][1] try: fct(superadmin, row, options) except Conflict: diff --git a/modoboa/admin/models/base.py b/modoboa/admin/models/base.py index 9e55e8533..7b1e7c5e7 100644 --- a/modoboa/admin/models/base.py +++ b/modoboa/admin/models/base.py @@ -3,7 +3,6 @@ from django.db import models from django.utils import timezone -from modoboa.lib import events from modoboa.lib.permissions import ( grant_access_to_object, ungrant_access_to_object ) @@ -32,7 +31,6 @@ class AdminObject(models.Model): def post_create(self, creator): grant_access_to_object(creator, self, is_owner=True) - events.raiseEvent("%sCreated" % self.objectname, creator, self) def save(self, *args, **kwargs): creator = kwargs.pop("creator", None) @@ -41,6 +39,5 @@ class AdminObject(models.Model): self.post_create(creator) def delete(self): - events.raiseEvent("%sDeleted" % self.objectname, self) ungrant_access_to_object(self) super(AdminObject, self).delete() diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index c4b233c5c..c17e87bcf 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -15,10 +15,10 @@ from reversion import revisions as reversion from modoboa.core import signals as core_signals from modoboa.core.models import User, ObjectAccess -from modoboa.lib import events from modoboa.lib.exceptions import BadRequest, Conflict from modoboa.parameters import tools as param_tools +from .. import signals from .base import AdminObject @@ -89,7 +89,9 @@ class Domain(AdminObject): def tags(self): if self.type == "domain": return [{"name": "domain", "label": _("Domain"), "type": "dom"}] - return events.raiseQueryEvent("GetTagsForDomain", self) + results = signals.get_domain_tags.send( + sender=self.__class__, domain=self) + return reduce(lambda a, b: a + b, [result[1] for result in results]) @property def admins(self): @@ -153,11 +155,8 @@ class Domain(AdminObject): :param User account: administrator to remove """ - from modoboa.lib.permissions import \ - ungrant_access_to_object, get_object_owner + from modoboa.lib.permissions import ungrant_access_to_object - if get_object_owner(self) == account: - events.raiseEvent('DomainOwnershipRemoved', account, self) ungrant_access_to_object(self, account) for mb in self.mailbox_set.all(): if mb.user.has_perm("admin.add_domain"): @@ -181,17 +180,14 @@ class Domain(AdminObject): from .mailbox import Quota if self.domainalias_set.count(): - events.raiseEvent("DomainAliasDeleted", self.domainalias_set.all()) ungrant_access_to_objects(self.domainalias_set.all()) if self.alias_set.count(): - events.raiseEvent("MailboxAliasDeleted", self.alias_set.all()) ungrant_access_to_objects(self.alias_set.all()) if param_tools.get_global_parameter("auto_account_removal"): for account in User.objects.filter(mailbox__domain=self): account.delete(fromuser, keepdir) elif self.mailbox_set.count(): Quota.objects.filter(username__contains="@%s" % self.name).delete() - events.raiseEvent("MailboxDeleted", self.mailbox_set.all()) ungrant_access_to_objects(self.mailbox_set.all()) super(Domain, self).delete() diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 14bc1d18e..9605fc77a 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -13,7 +13,6 @@ from django.utils.translation import ugettext as _, ugettext_lazy from reversion import revisions as reversion from modoboa.core.models import User -from modoboa.lib import events from modoboa.lib import exceptions as lib_exceptions from modoboa.lib.email_utils import split_mailbox from modoboa.lib.sysutils import exec_cmd @@ -305,7 +304,6 @@ class Mailbox(AdminObject): if newaddress: self.rename(local_part, domain) self.save() - events.raiseEvent("MailboxModified", self) def save(self, *args, **kwargs): """Custom save. diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py index dae12a7e0..f145c5bd6 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -2,14 +2,30 @@ import django.dispatch +check_extra_account_form = django.dispatch.Signal( + providing_args=["account", "form"]) +extra_account_dashboard_widgets = django.dispatch.Signal( + providing_args=["user", "account"]) +extra_account_forms = django.dispatch.Signal( + providing_args=["user", "account"]) extra_admin_content = django.dispatch.Signal( providing_args=["user", "location", "currentpage"]) extra_domain_actions = django.dispatch.Signal( providing_args=["user", "domain"]) extra_domain_dashboard_widgets = django.dispatch.Signal( providing_args=["user", "domain"]) -extra_domain_menu_entries = django.dispatch.Signal( - providing_args=["user"]) -extra_account_dashboard_widgets = django.dispatch.Signal( +extra_domain_filters = django.dispatch.Signal() +extra_domain_forms = django.dispatch.Signal(providing_args=["user", "domain"]) +extra_domain_import_help = django.dispatch.Signal() +extra_domain_menu_entries = django.dispatch.Signal(providing_args=["user"]) +extra_domain_qset_filters = django.dispatch.Signal( + providing_args=["domfilter", "extrafilters"]) +extra_domain_types = django.dispatch.Signal() +extra_domain_wizard_steps = django.dispatch.Signal() +get_account_form_instances = django.dispatch.Signal( providing_args=["user", "account"]) +get_domain_form_instances = django.dispatch.Signal( + providing_args=["user", "domain"]) +get_domain_tags = django.dispatch.Signal(providing_args=["domain"]) +import_object = django.dispatch.Signal(providing_args=["objtype"]) use_external_recipients = django.dispatch.Signal(providing_args=["recipients"]) diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index 3c2b2dea5..b4f33452e 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -16,7 +16,6 @@ from reversion import revisions as reversion from modoboa.core.models import User from modoboa.core import signals as core_signals -from modoboa.lib import events from modoboa.lib.web_utils import ( _render_to_string, render_to_json_response ) @@ -45,10 +44,13 @@ def index(request): ) def _domains(request): sort_order, sort_dir = get_sort_order(request.GET, "name") + extra_filters = reduce( + lambda a, b: a + b, + [result for result in signals.extra_domain_filters(sender="_domains")] + ) filters = dict( (flt, request.GET.get(flt, None)) - for flt in ['domfilter', 'searchquery'] + - events.raiseQueryEvent('ExtraDomainFilters') + for flt in ["domfilter", "searchquery"] + extra_filters ) request.session['domains_filters'] = filters domainlist = get_domains(request.user, **filters) @@ -126,7 +128,10 @@ def editdomain(request, dom_id): raise PermDeniedException instances = dict(general=domain) - events.raiseEvent("FillDomainInstances", request.user, domain, instances) + results = signals.get_domain_form_instances.send( + sender="editdomain", user=request.user, domain=domain) + for result in results: + instances.update(result[1]) return DomainForm(request, instances=instances).process() diff --git a/modoboa/admin/views/identity.py b/modoboa/admin/views/identity.py index f2401bceb..605df0ea4 100644 --- a/modoboa/admin/views/identity.py +++ b/modoboa/admin/views/identity.py @@ -13,7 +13,6 @@ from django.contrib.auth.decorators import ( from reversion import revisions as reversion from modoboa.core.models import User -from modoboa.lib import events from modoboa.lib.exceptions import ( PermDeniedException, BadRequest ) @@ -181,7 +180,10 @@ def editaccount(request, pk): mb = account.mailbox if hasattr(account, "mailbox") else None instances = dict(general=account, mail=mb, perms=account) - events.raiseEvent("FillAccountInstances", request.user, account, instances) + results = signals.get_account_form_instances.send( + sender="editaccount", user=request.user, account=account) + for result in results: + instances.update(result[1]) return AccountForm(request, instances=instances).process() diff --git a/modoboa/admin/views/import_.py b/modoboa/admin/views/import_.py index 4be0fa67f..f29b72cb9 100644 --- a/modoboa/admin/views/import_.py +++ b/modoboa/admin/views/import_.py @@ -12,11 +12,11 @@ from django.utils.translation import ugettext as _ from reversion import revisions as reversion -from modoboa.lib import events from modoboa.lib.exceptions import ModoboaException, Conflict from ..forms import ImportIdentitiesForm, ImportDataForm from .. import lib +from .. import signals @reversion.create_revision() @@ -48,12 +48,11 @@ def importdata(request, formclass=ImportDataForm): try: fct = getattr(lib, "import_%s" % row[0].strip()) except AttributeError: - fct = events.raiseQueryEvent( - 'ImportObject', row[0].strip() - ) + fct = signals.import_object.send( + sender="importdata", objtype=row[0].strip()) if not fct: continue - fct = fct[0] + fct = fct[0][1] with transaction.atomic(): try: fct(request.user, row, form.cleaned_data) @@ -83,6 +82,8 @@ def import_domains(request): if request.method == "POST": return importdata(request) + results = signals.extra_domain_import_help.send(sender="import_domains") + extra_help = reduce(lambda a, b: a + b, [result[1] for result in results]) helptext = _("""Provide a CSV file where lines respect one of the following formats:

The first element of each line is mandatory and must be equal to one of the previous values.

You can use a different character as separator.

-""" % ''.join([unicode(hlp) for hlp in - events.raiseQueryEvent('ExtraDomainImportHelp')])) +""") % "".join(extra_help) ctx = dict( title=_("Import domains"), diff --git a/modoboa/core/apps.py b/modoboa/core/apps.py index 0dca0a28f..a3d6e5dab 100644 --- a/modoboa/core/apps.py +++ b/modoboa/core/apps.py @@ -4,12 +4,6 @@ from django.apps import AppConfig from django.db.models import signals from django.utils.translation import ugettext_lazy -BASE_EVENTS = [ - "AccountCreated", - - "AccountModified", -] - def load_core_settings(): """Load core settings. @@ -17,13 +11,11 @@ def load_core_settings(): This function must be manually called (see :file:`urls.py`) in order to load base settings. """ - from modoboa.lib import events from modoboa.parameters import tools as param_tools from .app_settings import GeneralParametersForm param_tools.registry.add( "global", GeneralParametersForm, ugettext_lazy("General")) - events.declare(BASE_EVENTS) class CoreConfig(AppConfig): diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 29617c4ed..5a987623e 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -20,7 +20,6 @@ import jsonfield from reversion import revisions as reversion from modoboa.core.password_hashers import get_password_hasher -from modoboa.lib import events from modoboa.lib.exceptions import ( PermDeniedException, InternalError, BadRequest, Conflict ) @@ -330,7 +329,6 @@ class User(PermissionsMixin): """Grant permission on this user to creator.""" from modoboa.lib.permissions import grant_access_to_object grant_access_to_object(creator, self, is_owner=True) - events.raiseEvent("AccountCreated", self) def save(self, *args, **kwargs): creator = kwargs.pop("creator", None) diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py index c6d406739..0a215c8a1 100644 --- a/modoboa/core/signals.py +++ b/modoboa/core/signals.py @@ -36,3 +36,4 @@ user_can_set_role = django.dispatch.Signal( providing_args=["user", "role", "account"]) user_login = django.dispatch.Signal( providing_args=["username", "password"]) +user_logout = django.dispatch.Signal() diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index 9ed5dce65..474c353a0 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -12,7 +12,6 @@ from django.views.decorators.cache import never_cache from django.contrib.auth import authenticate, login, logout from modoboa.core.forms import LoginForm -from modoboa.lib import events from modoboa.lib.web_utils import _render_to_string from .base import find_nextlocation diff --git a/modoboa/lib/events.py b/modoboa/lib/events.py deleted file mode 100644 index 57996db07..000000000 --- a/modoboa/lib/events.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -:mod:`EVENTS` --- simple EVENTS handling ----------------------------------------- - -This module provides a simple way of managing EVENTS between Modoboa -core application and additional components. - -""" - -EVENTS = [] -CALLBACKS = {} - - -def declare(nevents): - """Declare new events. - - :param list nevents: a list of event names - """ - for evt in nevents: - if evt not in EVENTS: - EVENTS.append(evt) - - -def register(event, callback): - """Register a plugin callback for a specific event - - The event must be declared in this module. - - :param event: the event to listen to - :param callback: a function to execute when the event is raised - """ - # if not event in EVENTS: - # print "Event %s not registered" % event - # return 0 - if event not in CALLBACKS.keys(): - CALLBACKS[event] = {} - fullname = "%s.%s" % (callback.__module__, callback.__name__) - if fullname not in CALLBACKS[event]: - CALLBACKS[event][fullname] = callback - return 1 - - -class observe(object): - """Event observing decorator. - - Automatically register the decorated function to observe the given - event. - - :param evtnames: a list of event names - """ - def __init__(self, *evtnames, **kwargs): - self.evtnames = evtnames - - def __call__(self, f): - for evt in self.evtnames: - register(evt, f) - return f - - -def unregister(event, callback): - """Unregister a callback for a specific event - - :param event: the targeted event - :param callback: the callback to remove - """ - if event not in EVENTS: - return False - if event not in CALLBACKS: - return False - fullname = "%s.%s" % (callback.__module__, callback.__name__) - try: - del CALLBACKS[event][fullname] - except KeyError: - pass - - -def raiseEvent(event, *args, **kwargs): - """Raise a specific event - - Any additional keyword argument will be passed to registered - CALLBACKS. - - :param event: the event to raise - """ - if event not in EVENTS or event not in CALLBACKS: - return 0 - for callback in CALLBACKS[event].values(): - callback(*args, **kwargs) - return 1 - - -def raiseQueryEvent(event, *args, **kwargs): - """Raise a specific event and wait for answers from CALLBACKS - - Any additional keyword argument will be passed to registered - CALLBACKS. Callback answers are returned as a list. - - :param event: the event to raise - """ - result = [] - if event not in EVENTS or event not in CALLBACKS: - return result - for callback in CALLBACKS[event].values(): - tmp = callback(*args, **kwargs) - if tmp is None: - # Callback is registered but associated extension is - # disabled. - continue - result += tmp - return result - - -def raiseDictEvent(event, *args): - """Raise a specific event and return result as a dictionnary - - Any additional keyword argument will be passed to registered - CALLBACKS. Callback answers must be dictionnaries. - - :param event: the event to raise - :return: a dictionnary - """ - result = {} - if event not in EVENTS or event not 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/limits/apps.py b/modoboa/limits/apps.py index 117c92a5f..f91abd69a 100644 --- a/modoboa/limits/apps.py +++ b/modoboa/limits/apps.py @@ -3,20 +3,13 @@ from django.apps import AppConfig from django.utils.translation import ugettext as _ -EVENTS = [ - "GetExtraLimitTemplates" -] - def load_limits_settings(): """Load settings.""" - from modoboa.lib import events from modoboa.parameters import tools as param_tools from .app_settings import ParametersForm param_tools.registry.add("global", ParametersForm, _("Limits")) - events.declare(EVENTS) - from . import general_callbacks class LimitsConfig(AppConfig): diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py deleted file mode 100644 index e17fd2cc3..000000000 --- a/modoboa/limits/general_callbacks.py +++ /dev/null @@ -1,75 +0,0 @@ -"""General event callbacks.""" - -from django.template.loader import render_to_string -from django.utils.translation import ugettext as _ - -from modoboa.lib import events -from modoboa.parameters import tools as param_tools - -from . import forms -from . import utils - - -@events.observe("ExtraAccountForm") -def extra_account_form(user, account=None): - if not param_tools.get_global_parameter("enable_admin_limits"): - return [] - if user.role not in ["SuperAdmins", "Resellers"]: - return [] - condition = ( - (account is not None and - account.role not in ["Resellers", "DomainAdmins"]) or - user == account - ) - if condition: - return [] - - return [{ - "id": "resources", "title": _("Resources"), - "cls": forms.ResourcePoolForm - }] - - -@events.observe("ExtraDomainForm") -def extra_domain_form(user, domain): - """Include domain limits form.""" - if not param_tools.get_global_parameter("enable_domain_limits"): - return [] - if not user.has_perm("admin.change_domain"): - return [] - return [{ - "id": "resources", "title": _("Resources"), - "cls": forms.DomainLimitsForm - }] - - -@events.observe("FillDomainInstances") -def fill_domain_instances(user, domain, instances): - """Set domain instance for resources form.""" - if not param_tools.get_global_parameter("enable_domain_limits"): - return - if not user.has_perm("admin.change_domain"): - return - instances["resources"] = domain - - -@events.observe("CheckExtraAccountForm") -def check_form_access(account, form): - if form["id"] != "resources": - return [True] - if account.role not in ["Resellers", "DomainAdmins"]: - return [False] - return [True] - - -@events.observe("FillAccountInstances") -def fill_account_instances(user, account, instances): - condition = ( - not param_tools.get_global_parameter("enable_admin_limits") or - (not user.is_superuser and user.role != "Resellers") - ) - if condition: - return - if account.role not in ["Resellers", "DomainAdmins"]: - return - instances["resources"] = account diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index 75657ed29..9e7d8ef2f 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -5,6 +5,7 @@ from django.dispatch import receiver from django.template.loader import render_to_string from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext as _ from modoboa.admin import models as admin_models from modoboa.admin import signals as admin_signals @@ -14,6 +15,7 @@ from modoboa.lib import signals as lib_signals from modoboa.lib import permissions from modoboa.parameters import tools as param_tools +from . import forms from . import lib from . import models from . import utils @@ -103,6 +105,74 @@ def display_admin_limits(sender, user, account, **kwargs): }] +@receiver(admin_signals.extra_account_forms) +def extra_account_form(sender, user, account=None, **kwargs): + """Add limits form.""" + if not param_tools.get_global_parameter("enable_admin_limits"): + return [] + if user.role not in ["SuperAdmins", "Resellers"]: + return [] + condition = ( + (account is not None and + account.role not in ["Resellers", "DomainAdmins"]) or + user == account + ) + if condition: + return [] + return [{ + "id": "resources", "title": _("Resources"), + "cls": forms.ResourcePoolForm + }] + + +@receiver(admin_signals.check_extra_account_form) +def check_form_access(sender, account, form, **kwargs): + """Check if form must be used for account.""" + if form["id"] != "resources": + return True + if account.role not in ["Resellers", "DomainAdmins"]: + return False + return True + + +@receiver(admin_signals.get_account_form_instances) +def fill_account_instances(sender, user, account, **kwargs): + """Set account instance for resources form.""" + condition = ( + not param_tools.get_global_parameter("enable_admin_limits") or + (not user.is_superuser and user.role != "Resellers") or + account.role not in ["Resellers", "DomainAdmins"] + ) + if condition: + return {} + return {"resources": account} + + +@receiver(admin_signals.extra_domain_forms) +def extra_domain_form(sender, user, domain, **kwargs): + """Include domain limits form.""" + if not param_tools.get_global_parameter("enable_domain_limits"): + return [] + if not user.has_perm("admin.change_domain"): + return [] + return [{ + "id": "resources", "title": _("Resources"), + "cls": forms.DomainLimitsForm + }] + + +@receiver(admin_signals.get_domain_form_instances) +def fill_domain_instances(sender, user, domain, **kwargs): + """Set domain instance for resources form.""" + condition = ( + not param_tools.get_global_parameter("enable_domain_limits") or + not user.has_perm("admin.change_domain") + ) + if condition: + return {} + return {"resources": domain} + + @receiver(core_signals.account_deleted) def move_resource(sender, user, **kwargs): """Move remaining resource to another user.""" diff --git a/modoboa/limits/utils.py b/modoboa/limits/utils.py index 25cb45629..64b3358ba 100644 --- a/modoboa/limits/utils.py +++ b/modoboa/limits/utils.py @@ -5,7 +5,6 @@ from . import constants def get_user_limit_templates(): """Return defined templates.""" - # FIXME: change GetExtraLimitTemplates return constants.DEFAULT_USER_LIMITS.items() diff --git a/modoboa/relaydomains/apps.py b/modoboa/relaydomains/apps.py index 433f5d770..34b229a03 100644 --- a/modoboa/relaydomains/apps.py +++ b/modoboa/relaydomains/apps.py @@ -3,26 +3,13 @@ from django.apps import AppConfig from django.utils.translation import ugettext as _ -EVENTS = [ - "RelayDomainCreated", - "RelayDomainDeleted", - "RelayDomainModified", - "RelayDomainAliasCreated", - "RelayDomainAliasDeleted", - "ExtraRelayDomainForm", - "FillRelayDomainInstances" -] - def load_relaydomains_settings(): """Load application settings.""" from modoboa.parameters import tools as param_tools - from modoboa.lib import events from .app_settings import AdminParametersForm param_tools.registry.add("global", AdminParametersForm, _("Relay domains")) - events.declare(EVENTS) - from . import general_callbacks class RelayDomainsConfig(AppConfig): diff --git a/modoboa/relaydomains/general_callbacks.py b/modoboa/relaydomains/general_callbacks.py deleted file mode 100644 index 56f3e7de3..000000000 --- a/modoboa/relaydomains/general_callbacks.py +++ /dev/null @@ -1,85 +0,0 @@ -"""General event callbacks.""" - -from django.utils.translation import ugettext_lazy, ugettext as _ - -from modoboa.lib import events - -from .forms import RelayDomainWizardStep, RelayDomainFormGeneral - - -@events.observe('ExtraDomainFilters') -def extra_domain_filters(): - return ['srvfilter'] - - -@events.observe("ExtraDomainTypes") -def extra_domain_types(): - """Declare the relay domain type.""" - return [("relaydomain", _("Relay domain"))] - - -@events.observe("ExtraDomainWizardSteps") -def extra_wizard_step(): - """Return a step to configure the relay settings.""" - return [RelayDomainWizardStep( - "relay", RelayDomainFormGeneral, _("Relay domain"), - "relaydomains/relaydomain_form.html" - )] - - -@events.observe("ExtraDomainForm") -def extra_domain_form(user, domain): - """Return relay settings for domain edition.""" - if not user.has_perm("relaydomains.change_relaydomain"): - return [] - if domain.type != "relaydomain": - return [] - return [{ - "id": "relaydomain", "title": _("Relay settings"), - "cls": RelayDomainFormGeneral, - "formtpl": "relaydomains/relaydomain_form.html" - }] - - -@events.observe("FillDomainInstances") -def fill_domain_instances(user, domain, instances): - """Fill the relaydomain form with the right instance.""" - if not user.has_perm("relaydomains.change_relaydomain"): - return - if domain.type != "relaydomain": - return - instances["relaydomain"] = domain.relaydomain - - -@events.observe("ExtraDomainQsetFilters") -def extra_domain_entries(domfilter, extrafilters): - """Return extra queryset filters.""" - if domfilter is not None and domfilter and domfilter != 'relaydomain': - return {} - if "srvfilter" in extrafilters and extrafilters["srvfilter"]: - return {"relaydomain__service__name": extrafilters["srvfilter"]} - return {} - - -@events.observe("GetTagsForDomain") -def get_tags_for_domain(domain): - """Return relay domain custom tags.""" - if domain.type != "relaydomain": - return [] - return domain.relaydomain.tags - - -@events.observe('ExtraDomainImportHelp') -def extra_domain_import_help(): - return [ugettext_lazy(""" -
  • relaydomain; name; target host; service; enabled; verify recipients
  • -""")] - - -@events.observe('ImportObject') -def get_import_func(objtype): - from .lib import import_relaydomain - - if objtype == 'relaydomain': - return [import_relaydomain] - return [] diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py index c2d5adcee..533b047dc 100644 --- a/modoboa/relaydomains/handlers.py +++ b/modoboa/relaydomains/handlers.py @@ -4,18 +4,21 @@ from django.conf import settings from django.db.models import signals from django.dispatch import receiver from django.template import Template, Context +from django.utils.translation import ugettext as _ from modoboa.admin import models as admin_models -from modoboa.admin.signals import use_external_recipients +from modoboa.admin import signals as admin_signals from modoboa.core import signals as core_signals from modoboa.lib.email_utils import split_mailbox from . import constants +from . import forms +from . import lib from . import models from . import postfix_maps -@receiver(use_external_recipients) +@receiver(admin_signals.use_external_recipients) def check_relaydomain_alias(sender, **kwargs): """Allow the creation of an alias on a relaydomain.""" recipient = kwargs.get("recipients") @@ -78,3 +81,85 @@ def static_content(sender, caller, st_type, user, **kwargs): """) return [t.render(Context({"STATIC_URL": settings.STATIC_URL}))] + + +@receiver(admin_signals.extra_domain_filters) +def extra_domain_filters(sender, **kwargs): + """Return relaydomain filters.""" + return ["srvfilter"] + + +@receiver(admin_signals.extra_domain_forms) +def extra_domain_form(sender, user, **kwargs): + """Return relay settings for domain edition.""" + if not user.has_perm("relaydomains.change_relaydomain"): + return [] + domain = kwargs.get("domain") + if not domain or domain.type != "relaydomain": + return [] + return [{ + "id": "relaydomain", "title": _("Relay settings"), + "cls": forms.RelayDomainFormGeneral, + "formtpl": "relaydomains/relaydomain_form.html" + }] + + +@receiver(admin_signals.get_domain_form_instances) +def fill_domain_instances(sender, user, domain, **kwargs): + """Fill the relaydomain form with the right instance.""" + condition = ( + not user.has_perm("relaydomains.change_relaydomain") or + domain.type != "relaydomain" + ) + if condition: + return {} + return {"relaydomain": domain.relaydomain} + + +@receiver(admin_signals.extra_domain_import_help) +def extra_domain_import_help(sender, **kwargs): + """Add extra help lines.""" + return [_(""" +
  • relaydomain; name; target host; service; enabled; verify recipients
  • +""")] + + +@receiver(admin_signals.extra_domain_qset_filters) +def extra_domain_entries(sender, domfilter, extrafilters, **kwargs): + """Return extra queryset filters.""" + if domfilter is not None and domfilter and domfilter != "relaydomain": + return {} + if "srvfilter" in extrafilters and extrafilters["srvfilter"]: + return {"relaydomain__service__name": extrafilters["srvfilter"]} + return {} + + +@receiver(admin_signals.extra_domain_types) +def extra_domain_types(sender, **kwargs): + """Declare the relay domain type.""" + return [("relaydomain", _("Relay domain"))] + + +@receiver(admin_signals.extra_domain_wizard_steps) +def extra_wizard_step(sender, **kwargs): + """Return a step to configure the relay settings.""" + return [forms.RelayDomainWizardStep( + "relay", forms.RelayDomainFormGeneral, _("Relay domain"), + "relaydomains/relaydomain_form.html" + )] + + +@receiver(admin_signals.get_domain_tags) +def get_tags_for_domain(sender, domain, **kwargs): + """Return relay domain custom tags.""" + if domain.type != "relaydomain": + return [] + return domain.relaydomain.tags + + +@receiver(admin_signals.import_object) +def get_import_func(sender, objtype, **kwargs): + """Return function used to import objtype.""" + if objtype == "relaydomain": + return [lib.import_relaydomain] + return [] From 0dd31c26a21ae28a12b097ee3a29b0d7a67610ff Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 23 Nov 2016 13:47:48 +0100 Subject: [PATCH 03/15] Remove useless import. --- modoboa/core/views/user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/modoboa/core/views/user.py b/modoboa/core/views/user.py index b57c791a2..86a02d099 100644 --- a/modoboa/core/views/user.py +++ b/modoboa/core/views/user.py @@ -8,7 +8,6 @@ from django.contrib.auth.decorators import login_required, user_passes_test from rest_framework.authtoken.models import Token -from modoboa.lib import events from modoboa.lib.cryptutils import encrypt from modoboa.lib.web_utils import ( _render_to_string, render_to_json_response @@ -22,7 +21,6 @@ from .. import signals @login_required def index(request, tplname="core/user_index.html"): """Render user index page.""" - # FIXME: replace ExtraUprefsJS in autoreply return render(request, tplname, {"selection": "user"}) From 8dfbca46c2d762c42d82e52655dc1065bdee93d6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 23 Nov 2016 14:18:39 +0100 Subject: [PATCH 04/15] Remove deprecated signals. --- modoboa/admin/forms/account.py | 12 ------------ modoboa/core/signals.py | 4 ---- 2 files changed, 16 deletions(-) diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 417a293ed..03c91ecc0 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -228,15 +228,6 @@ class AccountFormMail(forms.Form, DynamicForm): self.field_widths = { "quota": 3 } - self.extra_fields = [] - results = core_signals.extra_form_fields.send( - sender=self.__class__, form="mailform", instance=self.mb) - if results: - results = reduce( - lambda a, b: a + b, [result for result in results]) - for fname, field in results: - self.fields[fname] = field - self.extra_fields.append(fname) if self.mb is not None: self.fields["email"].required = True qset = self.mb.aliasrecipient_set.filter(alias__internal=False) @@ -384,9 +375,6 @@ class AccountFormMail(forms.Form, DynamicForm): self.cleaned_data["use_domain_quota"] = ( self.cleaned_data["quota_act"]) self.mb.update_from_dict(user, self.cleaned_data) - core_signals.extra_form_fields.send( - sender=self.__class__, form="mailform", instance=self.mb, - cleaned_data=self.cleaned_data) account.email = self.cleaned_data["email"] account.save() diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py index 0a215c8a1..6bce716f5 100644 --- a/modoboa/core/signals.py +++ b/modoboa/core/signals.py @@ -17,8 +17,6 @@ can_create_object = django.dispatch.Signal( extra_account_actions = django.dispatch.Signal(providing_args=["account"]) extra_admin_menu_entries = django.dispatch.Signal( providing_args=["location", "user"]) -extra_form_fields = django.dispatch.Signal( - providing_args=["form", "instance"]) extra_role_permissions = django.dispatch.Signal(providing_args=["role"]) extra_static_content = django.dispatch.Signal( providing_args=["caller", "st_type", "user"]) @@ -30,8 +28,6 @@ get_top_notifications = django.dispatch.Signal( providing_args=["include_all"]) initial_data_loaded = django.dispatch.Signal(providing_args=["extname"]) register_postfix_maps = django.dispatch.Signal() -save_extra_form_fields = django.dispatch.Signal( - providing_args=["form", "instance", "cleaned_data"]) user_can_set_role = django.dispatch.Signal( providing_args=["user", "role", "account"]) user_login = django.dispatch.Signal( From 5eb0f57c7599307a65da29893a224bebf70d45ec Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 23 Nov 2016 14:46:30 +0100 Subject: [PATCH 05/15] Remove deprecated template tag. --- modoboa/admin/templates/admin/mailform.html | 1 - modoboa/lib/templatetags/form_tags.py | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/modoboa/admin/templates/admin/mailform.html b/modoboa/admin/templates/admin/mailform.html index 929d34674..c82ad0a6d 100644 --- a/modoboa/admin/templates/admin/mailform.html +++ b/modoboa/admin/templates/admin/mailform.html @@ -2,6 +2,5 @@ {% render_field form.email %} {% include "common/generic_field.html" with field=form.quota help_display_mode="tooltip" label_width="col-sm-4" appended_text=_("MB") activator=True activator_value=form.quota_act.value deactivate_if_empty=False activator_label=_("Use domain default value") activator_size="col-sm-5" %} -{% render_extra_fields form %} {% render_fields_group form "aliases" %} {% render_fields_group form "senderaddress" %} diff --git a/modoboa/lib/templatetags/form_tags.py b/modoboa/lib/templatetags/form_tags.py index 5d6d542c9..213dd17ad 100644 --- a/modoboa/lib/templatetags/form_tags.py +++ b/modoboa/lib/templatetags/form_tags.py @@ -79,17 +79,6 @@ def render_fields_group(form, pattern): )) -@register.simple_tag -def render_extra_fields(form): - result = '' - for fname in form.extra_fields: - result += render_to_string("common/generic_field.html", { - 'field': form[fname], 'help_display_mode': 'tooltip', - 'label_width': 'col-sm-4' - }) - return result - - @register.simple_tag def render_field_width(field): """.""" From e0d9e8061c4acabfc40a053eda664b0b1e24de42 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 23 Nov 2016 16:42:14 +0100 Subject: [PATCH 06/15] Increase coverage. --- modoboa/admin/tests/test_import_.py | 8 ++++++++ modoboa/relaydomains/handlers.py | 4 ++-- modoboa/relaydomains/models.py | 2 +- modoboa/relaydomains/tests.py | 18 ++++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index 4fe2adf88..24b50733d 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -21,6 +21,10 @@ class ImportTestCase(ModoTestCase): factories.populate_database() def test_domains_import(self): + response = self.client.get(reverse("admin:domain_import")) + self.assertEqual(response.status_code, 200) + self.assertIn("Provide a CSV", response.content) + f = ContentFile(b"""domain; domain1.com; 100; True domain; domain2.com; 200; False domainalias; domalias1.com; domain1.com; True @@ -56,6 +60,10 @@ domainalias;test.alias;test.com;True self.assertIn('Object already exists: domainalias', resp.content) def test_identities_import(self): + response = self.client.get(reverse("admin:identity_import")) + self.assertEqual(response.status_code, 200) + self.assertIn("Provide a CSV", response.content) + f = ContentFile(b""" account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0 account; truc@test.com; toto; RenĂ©; Truc; True; DomainAdmins; truc@test.com; 5; test.com diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py index 533b047dc..a9bd873c0 100644 --- a/modoboa/relaydomains/handlers.py +++ b/modoboa/relaydomains/handlers.py @@ -161,5 +161,5 @@ def get_tags_for_domain(sender, domain, **kwargs): def get_import_func(sender, objtype, **kwargs): """Return function used to import objtype.""" if objtype == "relaydomain": - return [lib.import_relaydomain] - return [] + return lib.import_relaydomain + return None diff --git a/modoboa/relaydomains/models.py b/modoboa/relaydomains/models.py index b10b8eaf0..b72f17829 100644 --- a/modoboa/relaydomains/models.py +++ b/modoboa/relaydomains/models.py @@ -113,7 +113,7 @@ class RelayDomain(admin_models.AdminObject): if len(row) != 6: raise BadRequest(_("Invalid line")) self.domain = admin_models.Domain( - name=row[1].strip(), type="relaydomain", + name=row[1].strip(), type="relaydomain", quota=0, enabled=(row[4].strip() in ["True", "1", "yes", "y"]) ) self.domain.save(creator=user) diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index 695e62a6c..ed17a2db1 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -1,5 +1,6 @@ """modoboa-admin-relaydomains unit tests.""" +from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from django.test import TestCase @@ -226,6 +227,23 @@ class RelayDomainsTestCase(ModoTestCase, Operations): address="alias2@relaydomain.tld").exists()) +class ImportTestCase(ModoTestCase): + """Test import.""" + + def test_webui_import(self): + """Check if import from webui works.""" + f = ContentFile("relaydomain;relay.com;127.0.0.1;relay;True;True", + name="domains.csv") + self.client.post( + reverse("admin:domain_import"), { + "sourcefile": f + } + ) + self.assertTrue( + admin_models.Domain.objects.filter( + name="relay.com", type="relaydomain").exists()) + + class LimitsTestCase(ModoTestCase, Operations): @classmethod From 475fe589466c2814aeb2a3b9d267cdb6f1e143b4 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 23 Nov 2016 17:35:18 +0100 Subject: [PATCH 07/15] Increase coverage. --- modoboa/admin/views/domain.py | 3 ++- modoboa/relaydomains/factories.py | 7 ++++--- modoboa/relaydomains/tests.py | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index b4f33452e..81083ff93 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -46,7 +46,8 @@ def _domains(request): sort_order, sort_dir = get_sort_order(request.GET, "name") extra_filters = reduce( lambda a, b: a + b, - [result for result in signals.extra_domain_filters(sender="_domains")] + [result[1] for result in + signals.extra_domain_filters.send(sender="_domains")] ) filters = dict( (flt, request.GET.get(flt, None)) diff --git a/modoboa/relaydomains/factories.py b/modoboa/relaydomains/factories.py index f46b71d0a..b6626d4d8 100644 --- a/modoboa/relaydomains/factories.py +++ b/modoboa/relaydomains/factories.py @@ -17,7 +17,7 @@ class ServiceFactory(factory.DjangoModelFactory): model = models.Service django_get_or_create = ('name', ) - name = 'dummy' + name = "dummy" class RelayDomainFactory(PermissionFactory): @@ -26,8 +26,9 @@ class RelayDomainFactory(PermissionFactory): class Meta: model = models.RelayDomain + django_get_or_create = ("domain", ) - domain = factory.SubFactory(DomainFactory) - target_host = 'external.host.tld' + domain = factory.SubFactory(DomainFactory, type="relaydomain") + target_host = "external.host.tld" verify_recipients = True service = factory.SubFactory(ServiceFactory) diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index ed17a2db1..b815d945a 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -1,5 +1,7 @@ """modoboa-admin-relaydomains unit tests.""" +import json + from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from django.test import TestCase @@ -62,15 +64,24 @@ class RelayDomainsTestCase(ModoTestCase, Operations): """Create test data.""" super(RelayDomainsTestCase, cls).setUpTestData() admin_factories.populate_database() - cls.rdom = RelayDomainFactory(domain__name='relaydomain.tld') + cls.rdom = RelayDomainFactory(domain__name="relaydomain.tld") admin_factories.DomainAliasFactory( - name='relaydomainalias.tld', target=cls.rdom.domain) + name="relaydomainalias.tld", target=cls.rdom.domain) admin_factories.MailboxFactory( domain=cls.rdom.domain, address="local", user__username="local@relaydomain.tld", user__groups=("SimpleUsers", ) ) + def test_domain_list_view(self): + """Make sure relaydomain is listed.""" + url = reverse("admin:_domain_list") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + content = json.loads(response.content) + self.assertIn("relaydomain.tld", content["rows"]) + self.assertIn("Relay Domain", content["rows"]) + def test_create_relaydomain(self): """Test the creation of a relay domain. From 68ae27e1ec7b22311d424386e39e9db319388e88 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 23 Nov 2016 18:31:12 +0100 Subject: [PATCH 08/15] Fixed tests. --- modoboa/relaydomains/factories.py | 2 +- modoboa/relaydomains/forms.py | 3 +++ modoboa/relaydomains/handlers.py | 2 +- modoboa/relaydomains/tests.py | 33 +++++++++++++++++-------------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/modoboa/relaydomains/factories.py b/modoboa/relaydomains/factories.py index b6626d4d8..ad5182e23 100644 --- a/modoboa/relaydomains/factories.py +++ b/modoboa/relaydomains/factories.py @@ -28,7 +28,7 @@ class RelayDomainFactory(PermissionFactory): model = models.RelayDomain django_get_or_create = ("domain", ) - domain = factory.SubFactory(DomainFactory, type="relaydomain") target_host = "external.host.tld" + domain = factory.SubFactory(DomainFactory) verify_recipients = True service = factory.SubFactory(ServiceFactory) diff --git a/modoboa/relaydomains/forms.py b/modoboa/relaydomains/forms.py index a656011ce..7312cc329 100644 --- a/modoboa/relaydomains/forms.py +++ b/modoboa/relaydomains/forms.py @@ -45,6 +45,9 @@ class RelayDomainFormGeneral(forms.ModelForm): """Custom save method.""" domain = kwargs.get("domain") instance = super(RelayDomainFormGeneral, self).save(commit=False) + if instance.domain and instance.domain.type == "domain": + # Avoid new creation if domain type has been changed. + return None if domain: instance.domain = domain instance.save() diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py index a9bd873c0..badc45634 100644 --- a/modoboa/relaydomains/handlers.py +++ b/modoboa/relaydomains/handlers.py @@ -41,7 +41,7 @@ def clean_domain(sender, instance, **kwargs): return has_relaydom = hasattr(instance, "relaydomain") if instance.type == "domain" and has_relaydom: - models.RelayDomain.objects.filter(domain=instance).delete() + instance.relaydomain.delete() elif instance.type == "relaydomain" and not has_relaydom: # Make sure to create a RelayDomain instance since we can't do it # at form level... diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index b815d945a..5805299cf 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -14,13 +14,13 @@ from modoboa.lib.test_utils import MapFilesTestCaseMixin from modoboa.limits import utils as limits_utils from .factories import RelayDomainFactory -from .models import RelayDomain, Service +from . import models class Operations(object): def _create_relay_domain(self, name, status=200): - srv, created = Service.objects.get_or_create(name='dummy') + srv, created = models.Service.objects.get_or_create(name='dummy') values = { "name": name, "create_dom_admin": False, "type": "relaydomain", "target_host": "external.host.tld", "service": srv.id, @@ -32,7 +32,7 @@ class Operations(object): ) def _relay_domain_alias_operation(self, optype, rdomain, name, status=200): - rdom = RelayDomain.objects.get(domain__name=rdomain) + rdom = models.RelayDomain.objects.get(domain__name=rdomain) values = { "name": rdom.domain.name, "target_host": rdom.target_host, "type": "relaydomain", "service": rdom.service.id @@ -65,6 +65,8 @@ class RelayDomainsTestCase(ModoTestCase, Operations): super(RelayDomainsTestCase, cls).setUpTestData() admin_factories.populate_database() cls.rdom = RelayDomainFactory(domain__name="relaydomain.tld") + cls.rdom.domain.type = "relaydomain" + cls.rdom.domain.save() admin_factories.DomainAliasFactory( name="relaydomainalias.tld", target=cls.rdom.domain) admin_factories.MailboxFactory( @@ -91,7 +93,7 @@ class RelayDomainsTestCase(ModoTestCase, Operations): FIXME: add a check for domain alias. """ self._create_relay_domain('relaydomain1.tld') - rdom = RelayDomain.objects.get(domain__name='relaydomain1.tld') + rdom = models.RelayDomain.objects.get(domain__name='relaydomain1.tld') self.assertEqual(rdom.target_host, 'external.host.tld') self.assertEqual(rdom.service.name, 'dummy') self.assertEqual(rdom.domain.enabled, True) @@ -138,16 +140,16 @@ class RelayDomainsTestCase(ModoTestCase, Operations): Rename 'relaydomain.tld' domain to 'relaydomain.org' """ values = { - 'name': "relaydomain.org", "target_host": self.rdom.target_host, + "name": "relaydomain.org", "target_host": self.rdom.target_host, "type": "relaydomain", "enabled": True, "service": self.rdom.service.id } self.ajax_post( - reverse('admin:domain_change', + reverse("admin:domain_change", args=[self.rdom.domain.id]), values ) - RelayDomain.objects.get(domain__name='relaydomain.org') + models.RelayDomain.objects.get(domain__name='relaydomain.org') def test_relaydomain_domain_switch(self): """Check domain <-> relaydomain transitions.""" @@ -156,12 +158,13 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "name": "relaydomain.tld", "type": "domain", "enabled": True, + "target_host": self.rdom.target_host, + "service": self.rdom.service.pk } self.ajax_post( - reverse('admin:domain_change', args=[domain.pk]), values) + reverse("admin:domain_change", args=[domain.pk]), values) self.assertFalse( - RelayDomain.objects.filter( - domain__name="relaydomain.tld").exists()) + models.RelayDomain.objects.filter(domain=domain).exists()) self.assertEqual( admin_models.Domain.objects.get(name="relaydomain.tld").type, "domain") @@ -176,7 +179,7 @@ class RelayDomainsTestCase(ModoTestCase, Operations): admin_models.Domain.objects.get(name="relaydomain.tld").type, "relaydomain") self.assertTrue( - RelayDomain.objects.filter( + models.RelayDomain.objects.filter( domain__name="relaydomain.tld").exists()) def test_edit_relaydomainalias(self): @@ -205,16 +208,16 @@ class RelayDomainsTestCase(ModoTestCase, Operations): args=[self.rdom.domain.id]), {} ) - with self.assertRaises(RelayDomain.DoesNotExist): - RelayDomain.objects.get(domain__name='relaydomain.tld') + with self.assertRaises(models.RelayDomain.DoesNotExist): + models.RelayDomain.objects.get(domain__name='relaydomain.tld') def test_delete_relaydomainalias(self): """Test the removal of a relay domain alias.""" self._relay_domain_alias_operation( 'del', self.rdom.domain.name, 'relaydomainalias.tld' ) - with self.assertRaises(RelayDomain.DoesNotExist): - RelayDomain.objects.get(domain__name='relaydomainalias.tld') + with self.assertRaises(models.RelayDomain.DoesNotExist): + models.RelayDomain.objects.get(domain__name='relaydomainalias.tld') def test_alias_on_relaydomain(self): """Create an alias on a relay domain.""" From 225c87e3d8402fa82eef7087b5fde4d65b7fd370 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 24 Nov 2016 17:09:28 +0100 Subject: [PATCH 09/15] Documentation updated. --- doc/contributing.rst | 1 - doc/contributing/events.rst | 907 -------------------------------- doc/contributing/plugin_api.rst | 115 ++-- 3 files changed, 66 insertions(+), 957 deletions(-) delete mode 100644 doc/contributing/events.rst diff --git a/doc/contributing.rst b/doc/contributing.rst index 5ef78dfb8..490f897b9 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -21,4 +21,3 @@ Table of contents contributing/getting_started contributing/plugin_api - contributing/events diff --git a/doc/contributing/events.rst b/doc/contributing/events.rst deleted file mode 100644 index 26d5eb707..000000000 --- a/doc/contributing/events.rst +++ /dev/null @@ -1,907 +0,0 @@ -.. _events: - -################ -Available events -################ - -************ -Introduction -************ - -Modoboa provides a simple API to interact with events. It understands -two kinds of events: - -* Those returning a value -* Those returning nothing - -Listening to a specific event is achieved as follows:: - - from modoboa.lib import events - - def callback(*args): - pass - - events.register('event', callback) - -You can also use the custom decorator ``events.observe``:: - - @events.observe('event') - def callback(*args): - pass - -``event`` is the event's name you want to listen to, ``callback`` is -the function that will be called each time this event is raised. Each -event impose to callbacks a specific prototype to respect. See below -for a detailled list. - -To stop listening to as specific event, you must use the -``unregister`` function:: - - events.unregister('event', callback) - -The parameters are the same than those used with ``register``. - -To unregister all events declared by a specific extension, use the -``unregister_extension`` function:: - - events.unregister_extension([name]) - -``name`` is the extension's name but it is optional. Leave it empty to -let the function guess the name. - -Read further to get a complete list and description of all available events. - -**************** -Supported events -**************** - -AccountAutoCreated -================== - -Raised when a new account is automatically created (example: LDAP -synchronization). - -*Callback prototype*:: - - def callback(account): pass - -* ``account`` is the newly created account (``User`` instance) - -AccountCreated -============== - -Raised when a new account is created. - -*Callback prototype*:: - - def callback(account): pass - -* ``account`` is the newly created account (``User`` instance) - -AccountDeleted -============== - -Raised when an existing account is deleted. - -*Callback prototype*:: - - def callback(account, byuser, **options): pass - -* ``account`` is the account that is going to be deleted -* ``byuser`` is the adminstrator deleting ``account`` - -AccountExported -=============== - -Raised when an account is exported to CSV. - -*Callback prototype*:: - - def callback(account): pass - -* ``account`` is the account being exported - -Must return a list of values to include in the export. - -AccountImported -=============== - -Raised when an account is imported from CSV. - -*Callback prototype*:: - - def callback(user, account, row): pass - -* ``user`` is the user importing the account -* ``account`` is the account being imported -* ``row`` is a list containing what remains from the CSV definition - -AccountModified -=============== - -Raised when an existing account is modified. - -*Callback prototype*:: - - def callback(oldaccount, newaccount): pass - -* ``oldaccount`` is the account before it is modified - -* ``newaccount`` is the account after the modification - -AdminMenuDisplay -================ - -Raised when an admin menu is about to be displayed. - -*Callback prototype*:: - - def callback(target, user): pass - -The ``target`` argument indicates which menu is being -displayed. Possible values are: - -* ``admin_menu_box`` : corresponds to the menu bar available inside administration pages -* ``top_menu`` : corresponds to the top black bar - -See :ref:`usermenudisplay` for a description of what callbacks that -listen to this event must return. - -CheckDomainName -=============== - -Raised before the unicity of a domain name is checked. By default, -modoboa prevents duplicate names between domains and domain aliases -but extensions have the possibility to extend this rule using this -event. - -*Callback prototype*:: - - def callback(): pass - -Must return a list of 2uple, each one containing a model class and an -associated label. - -.. _event_checkextraaccountform: - -CheckExtraAccountForm -===================== - -When an account is being modified, this event lets extensions check if -this account is concerned by a specific form. - -*Callback prototype*:: - - def callback(account, form): pass - -* ``account`` is the ``User`` instance beeing modified - -* ``form`` is a dictionnary (same content as for ``ExtraAccountForm``) - -Callbacks listening to this event must return a list containing one -Boolean. - -DomainAliasCreated -================== - -Raised when a new domain alias is created. - -*Callback prototype*:: - - def callback(user, domain_alias): pass - -* ``user`` is the new domain alias owner (``User`` instance) -* ``domain_alias`` is the new domain alias (``DomainAlias`` instance) - -DomainAliasDeleted -================== - -Raised when an existing domain alias is about to be deleted. - -*Callback prototype*:: - - def callback(domain_alias): pass - -* ``domain_alias`` is a ``DomainAlias`` instance - -DomainCreated -============= - -Raised when a new domain is created. - -*Callback prototype*:: - - def callback(user, domain): pass - -* ``user`` corresponds to the ``User`` object creating the domain (its owner) -* ``domain`` is a ``Domain`` instance - -DomainDeleted -============= - -Raised when an existing domain is about to be deleted. - -*Callback prototype*:: - - def callback(domain): pass - -* ``domain`` is a ``Domain`` instance - -DomainModified -============== - -Raised when a domain has been modified. - -*Callback prototype*:: - - def callback(domain): pass - -* ``domain`` is the modified ``Domain`` instance, it contains an extra - ``oldname`` field which contains the old name - -DomainOwnershipRemoved -====================== - -Raised before the ownership of a domain is removed from its original -creator. - -*Callback prototype*:: - - def callback(owner, domain): pass - -* ``owner`` is the original creator -* ``domain`` is the ``Domain`` instance being modified - -ExtraAccountActions -=================== - -Raised when the account list is displayed. Let developers define new -actions to act on a specific user. - -*Callback prototype*:: - - def callback(account): pass - -* ``account`` is the account being listed - -.. _event_extraaccountform: - -ExtraAccountForm -================ - -Let extensions add new forms to the account edition form (the one with -tabs). - -*Callback prototype*:: - - def callback(user, account): pass - -* ``user`` is a ``User`` instance corresponding to the currently - logged in user - -* ``account`` is the account beeing modified (``User`` instance) - -Callbacks listening to the event must return a list of dictionnaries, -each one must contain at least three keys:: - - {"id" : "", - "title" : "", - "cls" : TheFormClassName} - -ExtraAdminContent -================= - -Let extensions add extra content into the admin panel. - -*Callback prototype*:: - - def callback(user, target, currentpage): pass - -* ``user`` is a ``User`` instance corresponding to the currently - logged in user - -* ``target`` is a string indicating the place where the content will - be displayed. Possible values are : ``leftcol`` - -* ``currentpage`` is a string indicating which page (or section) is - displayed. Possible values are : ``domains``, ``identities`` - -Callbacks listening to this event must return a list of string. - -ExtraDomainEntries -================== - -Raised to request extra entries to display inside the *domains* -listing. - -*Callback prototype*:: - - def callback(user, domfilter, searchquery, **extrafilters): pass - -* ``user`` is the ``User`` instance corresponding to the currently - logged in user -* ``domfilter`` is a string indicating which domain type the user needs -* ``searchquery`` is a string containing a search query -* ``extrafilters`` is a set of keyword arguments that may contain additional filters - -Must return a valid ``QuerySet``. - -ExtraDomainFilters -================== - -Raised to request extra filters for the *domains* listing page. For -example, the *postfix_relay_domains* extension let users filter -entries based on service types. - -*Callback prototype*:: - - def callback(): pass - -Must return a list of valid filter names (string). - -.. _event_extradomainform: - -ExtraDomainForm -=============== - -Let extensions add new forms to the domain edition form (the one with -tabs). - -*Callback prototype*:: - - def callback(user, domain): pass - -* ``user`` is a ``User`` instance corresponding to the currently - logged in user - -* ``domain`` is the domain beeing modified (``Domain`` instance) - -Callbacks listening to the event must return a list of dictionnaries, -each one must contain at least three keys:: - - {"id" : "", - "title" : "", - "cls" : TheFormClassName} - -ExtraDomainImportHelp -===================== - -Raised to request extra help text to display inside the domain import -form. - -*Callback prototype*:: - - def callback(): pass - -Must return a list a string. - -ExtraDomainMenuEntries -====================== - -Raised to request extra entries to include in the left menu of the -*domains* listing page. - -*Callback prototype*:: - - def callback(user): pass - -* ``user`` is the ``User`` instance corresponding to the currently - logged in user - -Must return a list of dictionaries. Each dictionary must contain at -least three keys:: - - {"name": "", - "label": "", - "url": ""} - -.. _extraformfields: - -ExtraFormFields -=============== - -Raised to request extra fields to include in a django form. - -*Callback prototype*:: - - def callback(form_name, instance=None): pass - -* ``form_name`` is a string used to distinguish a specific form -* ``instance`` is a django model instance related to ``form_name`` - -Must return a list of 2uple, each one containing the following -information:: - - ('field name', ) - -ExtraRelayDomainForm -==================== - -Let extensions add new forms to the relay domain edition form (the one -with tabs). - -*Callback prototype*:: - - def callback(user, rdomain): pass - -* ``user`` is the ``User`` instance corresponding to the currently - logged in user -* ``rdomain`` is the relay domain being modified (``RelayDomain`` instance) - -Callbacks listening to the event must return a list of dictionnaries, -each one must contain at least three keys:: - - {"id" : "", - "title" : "", - "cls" : TheFormClassName} - -.. _event_fillaccountinstances: - -FillAccountInstances -==================== - -When an account is beeing modified, this event is raised to fill extra -forms. - -*Callback prototype*:: - - def callback(user, account, instances): pass - -* ``user`` is a ``User`` instance corresponding to the currently - logged in user - -* ``account`` is the ``User`` instance beeing modified - -* ``instances`` is a dictionnary where the callback will add - information needed to fill a specific form - -.. _event_filldomaininstances: - -FillDomainInstances -=================== - -When a domain is beeing modified, this event is raised to fill extra -forms. - -*Callback prototype*:: - - def callback(user, domain, instances): pass - -* ``user`` is a ``User`` instance corresponding to the currently - logged in user - -* ``domain`` is the ``Domain`` instance beeing modified - -* ``instances`` is a dictionnary where the callback will add - information needed to fill a specific form - -FillRelayDomainInstances -======================== - -When a relay domain is being modified, this event is raised to fill extra -forms. - -*Callback prototype*:: - - def callback(user, rdomain, instances): pass - -* ``user`` is the ``User`` instance corresponding to the currently - logged in user - -* ``rdomain`` is the ``RelayDomain`` instance being modified - -* ``instances`` is a dictionnary where the callback will add - information needed to fill a specific form - -GetAnnouncement -=============== - -Some places in the interface let plugins add their own announcement -(ie. message). - -*Callback prototype*:: - - def callback(target): pass - -* ``target`` is a string indicating the place where the announcement - will appear: - -* ``loginpage`` : corresponds to the login page - -Callbacks listening to this event must return a list of string. - -GetDomainActions -================ - -Raised to request the list of actions available for the *domains* -listing entry being displayed. - -*Callback prototype*:: - - def callback(user, rdomain): pass - -* ``user`` is the ``User`` instance corresponding to the currently - logged in user -* ``rdomain`` is the ``RelayDomain`` instance being displayed - -Must return a list of dictionaries, each dictionary containing at -least the following entries:: - - {"name": "", - "url": "", - "title": "", - "img": ""} - -GetDomainModifyLink -=================== - -Raised to request the modification url of the *domains* listing entry -being displayed. - -*Callback prototype*:: - - def callback(domain): pass - -* ``domain`` is a model instance (``RelayDomain`` for example) - -Must return a dictionary containing at least the following entry:: - - {'url': ''} - -GetExtraLimitTemplates -====================== - -Raised to request extra limit templates. For example, the -*postfix_relay_domains* extension define a template to limit the -number of relay domains an administrator can create. - -*Callback prototype*:: - - def callback(): pass - -Must return a list of set. Each set must contain at least three entries:: - - [('', '', '')] - -.. _getextrarolepermissions: - -GetExtraRolePermissions -======================= - -Let extensions define new permissions for a given role. - -*Callback prototype*:: - - def callback(rolename): pass - -* ``rolename`` is the role's name (str) - -Callbacks listening to this event must return a list of list. The -second list level must contain exactly 3 strings: the application -name, the model name and the permission name. Example:: - - [ - ["core", "user", "add_user"], - ["core", "user", "change_user"], - ["core", "user", "delete_user"], - ] - -.. _getextraroles: - -GetExtraRoles -============= - -Let extensions define new administrative roles (will be used to create -or modify an account). - -*Callback prototype*:: - - def callback(user, account): pass - -* ``user`` is a ``User`` instance corresponding to the currently - logged in user -* ``account`` is the account being modified (None on creation) - -Callbacks listening to this event must return a list of 2uple (two -strings) which respect the following format: ``(value, label)``. - -GetStaticContent -================ - -Let extensions add static content (ie. CSS or javascript) to default -pages. It is pretty useful for functionalities that don't need a -template but need javascript stuff. - -*Callback prototype*:: - - def callback(caller, st_type, user): pass - -* ``caller`` is name of the application (or the location) responsible - for the call - -* ``st_type`` is the expected static content type (``css`` or ``js``) - -* ``user`` is a ``User`` instance corresponding to the currently - logged in user - -Callbacks listening to this event must return a list of string. - -ImportObject -============ - -Raised to request the function handling an object being imported from CSV. - -*Callback prototype*:: - - def callback(objtype): pass - -``objtype`` is the type of object being imported - -Must return a list of function. A valid import function must respect -the following prototype:: - - def import_function(user, row, formopts): pass - -* ``user`` is the ``User`` instance corresponding to the currently - logged in user -* ``row`` is a string containing the object's definition (CSV format) -* ``formopts`` is a dictionary that may contain options - -InitialDataLoaded -================= - -Raised a initial data has been loaded for a given extension. - -*Callback prototype*:: - - def callback(extname); pass - - ``extname`` is the extension name (str) - -MailboxAliasCreated -=================== - -Raised when a new mailbox alias is created. - -*Callback prototype*:: - - def callback(user, mailbox_alias): pass - -* ``user`` is the new domain alias owner (``User`` instance) -* ``mailbox_alias`` is the new mailbox alias (``Alias`` instance) - -MailboxAliasDeleted -=================== - -Raised when an existing mailbox alias is about to be deleted. - -*Callback prototype*:: - - def callback(mailbox_alias): pass - -* ``mailbox_alias`` is an ``Alias`` instance - -MailboxCreated -============== - -Raised when a new mailbox is created. - -*Callback prototype*:: - - def callback(user, mailbox): pass - -* ``user`` is the new mailbox's owner (``User`` instance) -* ``mailbox`` is the new mailbox (``Mailbox`` instance) - -MailboxDeleted -============== - -Raised when an existing mailbox is about to be deleted. - -*Callback prototype*:: - - def callback(mailbox): pass - -* ``mailbox`` is a ``Mailbox`` instance - -MailboxModified -=============== - -Raised when an existing mailbox is modified. - -*Callback prototype*:: - - def callback(mailbox): pass - -* ``mailbox`` is the ``Mailbox`` modified instance. It contains a - ``old_full_address`` extra field to check if the address was - modified. - -PasswordChange -============== - -Raised just before a *password change* action. - -*Callback prototype*:: - - def callback(user): pass - -* ``user`` is a ``User`` instance - -Callbacks listening to this event must return a list containing either -``True`` or ``False``. If at least one ``True`` is returned, the -*password change* will be cancelled (ie. changing the password for -this user is disabled). - -TopNotifications -================ - -Lets extensions subscribe to the global notification service (located -inside the top bar). - -*Callback prototype*:: - - def callback(user, include_all): pass - -* ``request`` is a ``Request`` instance -* ``include_all`` is a boolean indicating if empty notifications must - be included into the result or not - -Callbacks listening to this event must return a list of dictionary, -each dictionary containing at least the following entries:: - - {"id": "", - "url": "", - "text": ""} - -If your notification needs a counter, you can specify it by adding the -two following entries in the dictionary: - - {"counter": , - "level": ""} - -UserLogin -========= - -Raised when a user logs in. - -*Callback prototype*:: - - def callback(request, username, password): pass - -UserLogout -========== - -Raised when a user logs out. - -*Callback prototype*:: - - def callback(request): pass - -.. _usermenudisplay: - -UserMenuDisplay -=============== - -Raised when a user menu is about to be displayed. - -*Callback prototype*:: - - def callback(target, user): pass - -The ``target`` argument indicates which menu is being -displayed. Possible values are: - -* ``options_menu``: corresponds to the top-right user menu -* ``uprefs_menu``: corresponds to the menu bar available inside the - *User preferences* page -* ``top_menu``: corresponds to the top black bar - -All the callbacks that listen to this event must return a list of -dictionnaries (corresponding to menu entries). Each dictionnary must -contain at least the following keys:: - - {"name" : "a_name_without_spaces", - "label" : _("The menu label"), - "url" : reverse("your_view")} - -RelayDomainAliasCreated -======================= - -Raised when a new relay domain alias is created. - -*Callback prototype*:: - - def callback(user, rdomain_alias): pass - -* ``user`` is the new relay domain alias owner (``User`` instance) -* ``rdomain_alias`` is the new relay domain alias (``DomainAlias`` instance) - -RelayDomainAliasDeleted -======================= - -Raised when an existing relay domain alias is about to be deleted. - -*Callback prototype*:: - - def callback(rdomain_alias): pass - -* ``rdomain_alias`` is a ``RelayDomainAlias`` instance - -RelayDomainCreated -================== - -Raised when a new relay domain is created. - -*Callback prototype*:: - - def callback(user, rdomain): pass - -* ``user`` corresponds to the ``User`` object creating the relay domain (its owner) -* ``rdomain`` is a ``RelayDomain`` instance - -RelayDomainDeleted -================== - -Raised when an existing relay domain is about to be deleted. - -*Callback prototype*:: - - def callback(rdomain): pass - -* ``rdomain`` is a ``RelayDomain`` instance - -RelayDomainModified -=================== - -Raised when a relay domain has been modified. - -*Callback prototype*:: - - def callback(rdomain): pass - -* ``rdomain`` is the modified ``RelayDomain`` instance, it contains an - extra ``oldname`` field which contains the old name - -RoleChanged -=========== - -Raised when the role of an account is about to be changed. - -*Callback prototype*:: - - def callback(account, role): pass - -* ``account`` is the account being modified -* ``role`` is the new role (string) - -SaveExtraFormFields -=================== - -Raised to save extra fields declared using :ref:`extraformfields`. - -*Callback prototype*:: - - def callback(form_name, instance, values): pass - -* ``form_name`` is a string used to distinguish a specific form -* ``instance`` is a django model instance related to ``form_name`` -* ``values`` is a dictionary containing the form's values - -UserCanSetRole -============== - -Raised to check if a user is allowed to set a given role to an -account. - -*Callback prototype*:: - - def callback(user, role, account): pass - -* ``user`` is the ``User`` instance corresponding to the currently - logged in user -* ``role`` is the role ``user`` tries to set -* ``account`` is the account being modified (None on creation) - -Must return a list containing ``True`` or ``False`` to indicate if -this user can is allowed to set ``role``. diff --git a/doc/contributing/plugin_api.rst b/doc/contributing/plugin_api.rst index 5d2ff90e3..188de537d 100644 --- a/doc/contributing/plugin_api.rst +++ b/doc/contributing/plugin_api.rst @@ -19,12 +19,11 @@ of code that integrates them into Modoboa. The the plugin: * Admin and user parameters -* Observed events * Custom menu entries -The communication between both applications is provided by -:ref:`events`. Modoboa offers some kind of hooks to let plugins add -custom actions. +The communication between both applications is provided by `Django +signals +`_. The following subsections describe the plugin architecture and explain how you can create your own. @@ -81,7 +80,7 @@ Parameters A plugin can declare its own parameters. There are two levels available: -* 'Administration' parameters : used to configure the plugin, editable +* 'Global' parameters : used to configure the plugin, editable inside the *Admin > Settings > Parameters* page * 'User' parameters : per-user parameters (or preferences), editable inside the *Options > Preferences* page @@ -89,100 +88,118 @@ A plugin can declare its own parameters. There are two levels available: Playing with parameters ======================= -To declare a new administration parameter, use the following function:: +Parameters are defined using `Django forms +`_ and Modoboa +adds two special forms you can inherit depending on the level of +parameter(s) you want to add: - from modoboa.lib import parameters +* ``modoboa.parameters.forms.AdminParametersForm``: for general parameters - parameters.register_admin(name, **kwargs) +* ``modoboa.parameters.forms.UserParametersForm``: for user parameters -To declare a new user parameter, use the following function:: +To register new parameters, add the following line into the ``load`` +method of your plugin class:: - parameter.register_user(name, **kwargs) + from modoboa.parameters import tools as param_tools + param_tools.registry.add( + LEVEL, YourForm, ugettext_lazy("Title")) -Both functions accept extra arguments listed here: +Replace ``LEVEL`` by ``"global"`` or ``"user"``. -* ``type`` : parameter's type, possible values are : ``int``, ``string``, ``list``, ``list_yesno``, -* ``deflt`` : default value, -* ``help`` : help text, -* ``values`` : list of possible values if ``type`` is ``list``. - -*************************** -Custom administrative roles -*************************** +*********************** +Custom role permissions +*********************** Modoboa uses Django's internal permission system. Administrative roles are nothing more than groups (``Group`` instances). -If an extension needs to add new roles, the following steps are required: +An extension can add new permissions to a group by listening to the +``extra_role_permissions`` signal. Here is an example: -#. Listen to the :ref:`getextraroles` event that will return - the group's name +.. sourcecode:: python -#. Listen to the :ref:`getextrarolepermissions` event that will return - the new group's permissions + from django.dispatch import receiver + from modoboa.core import signals as core_signals -The group will automatically be created the next time you run the -``load_initial_data`` command. + PERMISSIONS = { + "Resellers": [ + ("relaydomains", "relaydomain", "add_relaydomain"), + ("relaydomains", "relaydomain", "change_relaydomain"), + ("relaydomains", "relaydomain", "delete_relaydomain"), + ("relaydomains", "service", "add_service"), + ("relaydomains", "service", "change_service"), + ("relaydomains", "service", "delete_service") + ] + } + + @receiver(core_signals.extra_role_permissions) + def extra_role_permissions(sender, role, **kwargs): + """Add permissions to the Resellers group.""" + return constants.PERMISSIONS.get(role, []) ********************* Extending admin forms ********************* -the forms used to edit objects (account, domain, etc.) through the admin -panel are composed of tabs. You can extend those forms (ie. add new -tabs) in a pretty easy way by defining events. +The forms used to edit objects (account, domain, etc.) through the admin +panel are composed of tabs. You can extend them (ie. add new +tabs) in a pretty easy way thanks to custom signals. Account ======= To add a new tab to the account edition form, define new listeners -(handlers) for the following events: +(handlers) for the following signals: -* :ref:`event_extraaccountform` +* ``modoboa.admin.signals.extra_account_forms`` -* :ref:`event_fillaccountinstances` +* ``modoboa.admin.signals.get_account_form_instances`` -* :ref:`event_checkextraaccountform` (optional) +* ``modoboa.admin.signals.check_extra_account_form`` (optional) Example: .. sourcecode:: python - from modoboa.lib import events + from django.dispatch import receiver + from modoboa.admin import signals as admin_signals - @events.observe("ExtraAccountForm") - def extra_account_form(user, account=None): + + @receiver(admin_signals.extra_account_forms) + def extra_account_form(sender, user, account, **kwargs): return [ {"id": "tabid", "title": "Title", "cls": MyFormClass} ] - @events.observe("FillAccountInstances") - def fill_my_tab(user, account, instances): - instances["id"] = my_instance - + @receiver(admin_signals.get_account_form_instances) + def fill_my_tab(sender, user, account, **kwargs): + return {"id": my_instance} + Domain ====== To add a new tab to the domain edition form, define new listeners -(handlers) for the following events: +(handlers) for the following signals: -* :ref:`event_extradomainform` +* ``modoboa.admin.signals.extra_domain_forms`` -* :ref:`event_filldomaininstances` +* ``modoboa.admin.signals.get_domain_form_instances`` Example: .. sourcecode:: python - from modoboa.lib import events + from django.dispatch import receiver + from modoboa.admin import signals as admin_signals - @events.observe("ExtraDomainForm") - def extra_domain_form(user, domain): + + @receiver(admin_signals.extra_domain_forms) + def extra_account_form(sender, user, domain, **kwargs): return [ {"id": "tabid", "title": "Title", "cls": MyFormClass} ] - @events.observe("FillDomainInstances") - def fill_my_tab(user, domain, instances): - instances["id"] = my_instance + @receiver(admin_signals.get_domain_form_instances) + def fill_my_tab(sender, user, domain, **kwargs): + return {"id": my_instance} From 2393b8a0d88177d005abda0e2d7ae1198d25bf21 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 25 Nov 2016 17:08:19 +0100 Subject: [PATCH 10/15] New unit test. --- modoboa/core/tests/test_views.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index e50dbe12c..de7689b04 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -11,6 +11,23 @@ from modoboa.parameters import tools as param_tools from .. import factories from .. import models +from .. import signals + + +def announcement(sender, location, **kwargs): + """Simpler handler.""" + return "This is an annoucement!" + + +class LoginTestCase(ModoTestCase): + """Login page test cases.""" + + def test_announcements(self): + """Check if announcements are displayed.""" + signals.get_announcements.connect(announcement) + response = self.client.get(reverse("core:login")) + self.assertEqual(response.status_code, 200) + self.assertIn("This is an annoucement!", response.content) class DashboardTestCase(ModoTestCase): From 3f4551b7f3a3aa43c240bc1c2af1ba17f517cbc9 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 25 Nov 2016 17:17:38 +0100 Subject: [PATCH 11/15] New unit test. --- modoboa/core/tests/test_views.py | 9 +++++++++ modoboa/core/views/admin.py | 4 +--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index de7689b04..61fa17125 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -1,5 +1,7 @@ """Core views test cases.""" +import json + from testfixtures import compare from django import forms @@ -67,6 +69,13 @@ class DashboardTestCase(ModoTestCase): self.assertEqual(response.status_code, 302) self.assertTrue(response.url.endswith(reverse("core:dashboard"))) + def test_top_notifications(self): + """Check top notifications service.""" + url = reverse("core:top_notifications_check") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(json.loads(response.content)), 1) + class SettingsTestCase(ModoTestCase): """Settings tests.""" diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index e69d7e87b..9371341d2 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -114,9 +114,7 @@ def logs_page(request, tplname="core/logs_page.html"): @login_required def check_top_notifications(request): - """ - AJAX service to check for new top notifications to display. - """ + """AJAX service to check for new top notifications to display.""" notifications = signals.get_top_notifications.send( sender="top_notifications", include_all=True) notifications = reduce( From 31fa537f02e83787db29edd31329fcd53bdc2398 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 25 Nov 2016 17:48:17 +0100 Subject: [PATCH 12/15] Cleanup. --- modoboa/admin/signals.py | 1 - .../templates/admin/import_domains_form.html | 16 ++++++++++ .../admin/import_identities_form.html | 15 ++++++++++ modoboa/admin/templates/admin/importform.html | 2 +- modoboa/admin/views/import_.py | 29 ++----------------- modoboa/relaydomains/handlers.py | 8 ----- 6 files changed, 35 insertions(+), 36 deletions(-) create mode 100644 modoboa/admin/templates/admin/import_domains_form.html create mode 100644 modoboa/admin/templates/admin/import_identities_form.html diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py index f145c5bd6..c915f0c18 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -16,7 +16,6 @@ extra_domain_dashboard_widgets = django.dispatch.Signal( providing_args=["user", "domain"]) extra_domain_filters = django.dispatch.Signal() extra_domain_forms = django.dispatch.Signal(providing_args=["user", "domain"]) -extra_domain_import_help = django.dispatch.Signal() extra_domain_menu_entries = django.dispatch.Signal(providing_args=["user"]) extra_domain_qset_filters = django.dispatch.Signal( providing_args=["domfilter", "extrafilters"]) diff --git a/modoboa/admin/templates/admin/import_domains_form.html b/modoboa/admin/templates/admin/import_domains_form.html new file mode 100644 index 000000000..ef729b4a9 --- /dev/null +++ b/modoboa/admin/templates/admin/import_domains_form.html @@ -0,0 +1,16 @@ +{% extends "admin/importform.html" %} + +{% load i18n %} + +{% block helptext %} + {% blocktrans trimmed %} + Provide a CSV file where lines respect one of the following formats: +
      +
    • domain; name; quota; enabled
    • +
    • domainalias; name; targeted domain; enabled
    • +
    • relaydomain; name; target host; service; enabled; verify recipients
    • +
    +

    The first element of each line is mandatory and must be equal to one of the previous values.

    +

    You can use a different character as separator.

    + {% endblocktrans %} +{% endblock %} diff --git a/modoboa/admin/templates/admin/import_identities_form.html b/modoboa/admin/templates/admin/import_identities_form.html new file mode 100644 index 000000000..71bd7a9aa --- /dev/null +++ b/modoboa/admin/templates/admin/import_identities_form.html @@ -0,0 +1,15 @@ +{% extends "admin/importform.html" %} + +{% load i18n %} + +{% block helptext %} + {% blocktrans trimmed %} + Provide a CSV file where lines respect one of the following formats: +
      +
    • account; loginname; password; first name; last name; enabled; group; address; quota; [, domain, ...]
    • +
    • alias; address; enabled; recipient; recipient; ...
    • +
    +

    The first element of each line is mandatory and must be equal to one of the previous values.

    +

    You can use a different character as separator.

    + {% endblocktrans %} +{% endblock %} diff --git a/modoboa/admin/templates/admin/importform.html b/modoboa/admin/templates/admin/importform.html index 8e15acf69..f34af0e9e 100644 --- a/modoboa/admin/templates/admin/importform.html +++ b/modoboa/admin/templates/admin/importform.html @@ -4,7 +4,7 @@ {% block formbody %}

    {% trans "Help" %}

    - {{ helptext|safe }} + {% block helptext %}{% endblock %}
    {% render_form form %}