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} 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..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", - "ExtraDomainMenuEntries", - "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 59e59313a..000000000 --- a/modoboa/admin/callbacks.py +++ /dev/null @@ -1,208 +0,0 @@ -"""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..03c91ecc0 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): @@ -228,11 +228,6 @@ class AccountFormMail(forms.Form, DynamicForm): self.field_widths = { "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) if self.mb is not None: self.fields["email"].required = True qset = self.mb.aliasrecipient_set.filter(alias__internal=False) @@ -380,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) - events.raiseEvent( - 'SaveExtraFormFields', 'mailform', self.mb, self.cleaned_data - ) account.email = self.cleaned_data["email"] account.save() @@ -456,11 +448,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): @@ -489,9 +483,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 @@ -509,9 +504,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 bad0668d7..381ca41b4 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... """ @@ -193,8 +196,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 @@ -282,11 +286,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) @@ -329,8 +336,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")) @@ -346,9 +351,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 b7b6554e3..4da7e6304 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. @@ -82,17 +94,24 @@ 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) + mb = kwargs["instance"] ungrant_access_to_object(mb) for ralias in mb.aliasrecipient_set.select_related("alias"): alias = ralias.alias 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 +130,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/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 3e7c10f8f..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 @@ -75,7 +74,6 @@ class MailboxManager(Manager): @python_2_unicode_compatible class Mailbox(AdminObject): - """User mailbox.""" address = models.CharField( @@ -306,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. @@ -326,21 +323,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..c915f0c18 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -2,10 +2,29 @@ import django.dispatch -use_external_recipients = django.dispatch.Signal(providing_args=["recipients"]) +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_account_dashboard_widgets = django.dispatch.Signal( +extra_domain_filters = django.dispatch.Signal() +extra_domain_forms = django.dispatch.Signal(providing_args=["user", "domain"]) +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/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 %}