mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge pull request #969 from modoboa/refactor/replace_events
Refactor/replace events
This commit is contained in:
@@ -21,4 +21,3 @@ Table of contents
|
||||
|
||||
contributing/getting_started
|
||||
contributing/plugin_api
|
||||
contributing/events
|
||||
|
||||
@@ -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" : "<the form's id>",
|
||||
"title" : "<the title used to present the form>",
|
||||
"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" : "<the form's id>",
|
||||
"title" : "<the title used to present the form>",
|
||||
"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": "<menu name>",
|
||||
"label": "<menu label>",
|
||||
"url": "<menu 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', <Django form field instance>)
|
||||
|
||||
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" : "<the form's id>",
|
||||
"title" : "<the title used to present the form>",
|
||||
"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": "<action name>",
|
||||
"url": "<action url>",
|
||||
"title": "<action title>",
|
||||
"img": "<action icon>"}
|
||||
|
||||
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': '<modification 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::
|
||||
|
||||
[('<limit_name>', '<limit label>', '<limit help text>')]
|
||||
|
||||
.. _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": "<notification entry ID>",
|
||||
"url": "<associated URL>",
|
||||
"text": "<text to display>"}
|
||||
|
||||
If your notification needs a counter, you can specify it by adding the
|
||||
two following entries in the dictionary:
|
||||
|
||||
{"counter": <associated counter>,
|
||||
"level": "<info|success|warning|error>"}
|
||||
|
||||
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``.
|
||||
@@ -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
|
||||
<https://docs.djangoproject.com/en/1.9/topics/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
|
||||
<https://docs.djangoproject.com/en/1.9/topics/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}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+186
-4
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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:
|
||||
<ul>
|
||||
<li><em>domain; name; quota; enabled</em></li>
|
||||
<li><em>domainalias; name; targeted domain; enabled</em></li>
|
||||
<li><em>relaydomain; name; target host; service; enabled; verify recipients</em></li>
|
||||
</ul>
|
||||
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
|
||||
<p>You can use a different character as separator.</p>
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
@@ -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:
|
||||
<ul>
|
||||
<li><em>account; loginname; password; first name; last name; enabled; group; address; quota; [, domain, ...]</em></li>
|
||||
<li><em>alias; address; enabled; recipient; recipient; ...</em></li>
|
||||
</ul>
|
||||
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
|
||||
<p>You can use a different character as separator.</p>
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block formbody %}
|
||||
<div class="alert alert-info">
|
||||
<h4>{% trans "Help" %}</h4>
|
||||
{{ helptext|safe }}
|
||||
{% block helptext %}{% endblock %}
|
||||
</div>
|
||||
{% render_form form %}
|
||||
<iframe id="import_target" name="import_target" src="#"
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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 += action[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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,14 @@ 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[1] for result in
|
||||
signals.extra_domain_filters.send(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 +129,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()
|
||||
|
||||
|
||||
|
||||
@@ -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,15 +180,17 @@ 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()
|
||||
|
||||
|
||||
@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)
|
||||
)
|
||||
|
||||
@@ -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,17 +82,6 @@ def import_domains(request):
|
||||
if request.method == "POST":
|
||||
return importdata(request)
|
||||
|
||||
helptext = _("""Provide a CSV file where lines respect one of the following formats:
|
||||
<ul>
|
||||
<li><em>domain; name; quota; enabled</em></li>
|
||||
<li><em>domainalias; name; targeted domain; enabled</em></li>
|
||||
%s
|
||||
</ul>
|
||||
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
|
||||
<p>You can use a different character as separator.</p>
|
||||
""" % ''.join([unicode(hlp) for hlp in
|
||||
events.raiseQueryEvent('ExtraDomainImportHelp')]))
|
||||
|
||||
ctx = dict(
|
||||
title=_("Import domains"),
|
||||
action_label=_("Import"),
|
||||
@@ -102,10 +90,9 @@ def import_domains(request):
|
||||
formid="importform",
|
||||
enctype="multipart/form-data",
|
||||
target="import_target",
|
||||
helptext=helptext,
|
||||
form=ImportDataForm()
|
||||
)
|
||||
return render(request, "admin/importform.html", ctx)
|
||||
return render(request, "admin/import_domains_form.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -117,15 +104,6 @@ def import_identities(request):
|
||||
if request.method == "POST":
|
||||
return importdata(request, ImportIdentitiesForm)
|
||||
|
||||
helptext = _("""Provide a CSV file where lines respect one of the following formats:
|
||||
<ul>
|
||||
<li><em>account; loginname; password; first name; last name; enabled; group; address; quota; [, domain, ...]</em></li>
|
||||
<li><em>alias; address; enabled; recipient; recipient; ...</em></li>
|
||||
</ul>
|
||||
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
|
||||
|
||||
<p>You can use a different character as separator.</p>
|
||||
""")
|
||||
ctx = dict(
|
||||
title=_("Import identities"),
|
||||
action_label=_("Import"),
|
||||
@@ -134,7 +112,6 @@ def import_identities(request):
|
||||
formid="importform",
|
||||
enctype="multipart/form-data",
|
||||
target="import_target",
|
||||
form=ImportIdentitiesForm(),
|
||||
helptext=helptext
|
||||
form=ImportIdentitiesForm()
|
||||
)
|
||||
return render(request, "admin/importform.html", ctx)
|
||||
return render(request, "admin/import_identities_form.html", ctx)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,42 +4,6 @@ from django.apps import AppConfig
|
||||
from django.db.models import signals
|
||||
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",
|
||||
]
|
||||
|
||||
|
||||
def load_core_settings():
|
||||
"""Load core settings.
|
||||
@@ -47,17 +11,14 @@ 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):
|
||||
|
||||
"""App configuration."""
|
||||
|
||||
name = "modoboa.core"
|
||||
|
||||
@@ -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"],
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}]
|
||||
|
||||
@@ -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
|
||||
|
||||
+15
-45
@@ -20,13 +20,13 @@ 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
|
||||
)
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from . import constants
|
||||
from . import signals
|
||||
|
||||
|
||||
try:
|
||||
@@ -103,41 +103,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 +142,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 +294,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
|
||||
@@ -363,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)
|
||||
@@ -378,7 +343,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 +396,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 +416,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 +442,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):
|
||||
|
||||
@@ -2,6 +2,34 @@
|
||||
|
||||
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_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()
|
||||
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()
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script type="text/javascript" src="{% static 'js/listing.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/twocols_nav.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
{{ extrajs|safe }}
|
||||
{% extra_static_content "user_index" "js" user %}
|
||||
$(document).ready(function() {
|
||||
var userprefs = new TwocolsNav({
|
||||
deflocation: "profile/",
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
"""Custom tags for Core application."""
|
||||
|
||||
import os
|
||||
import pkg_resources
|
||||
import re
|
||||
|
||||
from django import template
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.lib import events
|
||||
from django.contrib.sessions.models import Session
|
||||
|
||||
from .. import signals
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -19,8 +22,9 @@ register = template.Library()
|
||||
@register.simple_tag
|
||||
def core_menu(selection, user):
|
||||
"""Build the top level menu."""
|
||||
entries = (
|
||||
events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user))
|
||||
entries = signals.extra_admin_menu_entries.send(
|
||||
sender="core_menu", location="top_menu", user=user)
|
||||
entries = reduce(lambda a, b: a + b, [entry[1] for entry in entries])
|
||||
if user.is_superuser:
|
||||
entries += [
|
||||
{"name": "settings",
|
||||
@@ -38,8 +42,10 @@ def core_menu(selection, user):
|
||||
|
||||
@register.simple_tag
|
||||
def extensions_menu(selection, user):
|
||||
menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user)
|
||||
return render_to_string('common/menulist.html', {
|
||||
menu = signals.extra_user_menu_entries.send(
|
||||
sender="core_menu", location="top_menu", user=user)
|
||||
menu = reduce(lambda a, b: a + b, [entry[1] for entry in menu])
|
||||
return render_to_string("common/menulist.html", {
|
||||
"selection": selection, "entries": menu, "user": user
|
||||
})
|
||||
|
||||
@@ -83,13 +89,16 @@ def user_menu(user, selection):
|
||||
]}
|
||||
]
|
||||
|
||||
entries[0]["menu"] += \
|
||||
events.raiseQueryEvent("UserMenuDisplay", "options_menu", user) \
|
||||
+ [{"name": "logout",
|
||||
"url": reverse("core:logout"),
|
||||
"label": _("Logout"),
|
||||
"img": "fa fa-sign-out"}]
|
||||
|
||||
extra_entries = signals.extra_user_menu_entries.send(
|
||||
sender="user_menu", location="options_menu", user=user)
|
||||
extra_entries = reduce(
|
||||
lambda a, b: a + b, [entry[1] for entry in extra_entries])
|
||||
entries[0]["menu"] += (
|
||||
extra_entries + [{"name": "logout",
|
||||
"url": reverse("core:logout"),
|
||||
"label": _("Logout"),
|
||||
"img": "fa fa-sign-out"}]
|
||||
)
|
||||
return render_to_string("common/menulist.html", {
|
||||
"selection": selection, "entries": entries, "user": user
|
||||
})
|
||||
@@ -114,9 +123,13 @@ def uprefs_menu(selection, user):
|
||||
"url": "api/",
|
||||
"label": _("API"),
|
||||
})
|
||||
entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu", user)
|
||||
extra_entries = signals.extra_user_menu_entries.send(
|
||||
sender="user_menu", location="uprefs_menu", user=user)
|
||||
extra_entries = reduce(
|
||||
lambda a, b: a + b, [entry[1] for entry in extra_entries])
|
||||
entries += extra_entries
|
||||
entries = sorted(entries, key=lambda e: e["label"])
|
||||
return render_to_string('common/menu.html', {
|
||||
return render_to_string("common/menu.html", {
|
||||
"entries": entries,
|
||||
"css": "nav nav-sidebar",
|
||||
"selection": selection,
|
||||
@@ -157,7 +170,6 @@ def visirule(field):
|
||||
|
||||
@register.simple_tag
|
||||
def get_version():
|
||||
import pkg_resources
|
||||
return pkg_resources.get_distribution("modoboa").version
|
||||
|
||||
|
||||
@@ -201,9 +213,6 @@ def connected_users(parser, token):
|
||||
|
||||
@register.simple_tag
|
||||
def get_modoboa_logo():
|
||||
import os
|
||||
from django.conf import settings
|
||||
|
||||
try:
|
||||
logo = settings.MODOBOA_CUSTOM_LOGO
|
||||
except AttributeError:
|
||||
@@ -215,9 +224,12 @@ def get_modoboa_logo():
|
||||
|
||||
@register.simple_tag
|
||||
def load_optionalmenu(user):
|
||||
menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu_middle", user)
|
||||
menu = signals.extra_user_menu_entries.send(
|
||||
sender="user_menu", location="top_menu_middle", user=user)
|
||||
menu = reduce(
|
||||
lambda a, b: a + b, [entry[1] for entry in menu])
|
||||
return template.loader.render_to_string(
|
||||
'common/menulist.html',
|
||||
"common/menulist.html",
|
||||
{"entries": menu, "user": user}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Core views test cases."""
|
||||
|
||||
import json
|
||||
|
||||
from testfixtures import compare
|
||||
|
||||
from django import forms
|
||||
@@ -11,6 +13,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):
|
||||
@@ -50,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."""
|
||||
@@ -131,6 +157,16 @@ class UserSettings(param_forms.UserParametersForm):
|
||||
test = forms.CharField()
|
||||
|
||||
|
||||
def extra_user_menu(sender, location, user, **kwargs):
|
||||
"""Return extra menu entry."""
|
||||
return [
|
||||
{"name": "test_menu_entry",
|
||||
"class": "ajaxnav",
|
||||
"url": "toto/",
|
||||
"label": "Test"}
|
||||
]
|
||||
|
||||
|
||||
class PreferencesTestCase(ModoTestCase):
|
||||
"""Test user preferences."""
|
||||
|
||||
@@ -146,6 +182,13 @@ class PreferencesTestCase(ModoTestCase):
|
||||
super(PreferencesTestCase, cls).tearDownClass()
|
||||
del param_tools.registry._registry["user"]["core"]
|
||||
|
||||
def test_get_user_index(self):
|
||||
"""Retrieve index page."""
|
||||
signals.extra_user_menu_entries.connect(extra_user_menu)
|
||||
url = reverse("core:user_index")
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, 'name="test_menu_entry"')
|
||||
|
||||
def test_get_preferences(self):
|
||||
"""Test preferences display."""
|
||||
url = reverse("core:user_preferences")
|
||||
|
||||
+10
-10
@@ -1,6 +1,5 @@
|
||||
"""
|
||||
Views available to super administrators only.
|
||||
"""
|
||||
"""Views available to super administrators only."""
|
||||
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, user_passes_test
|
||||
)
|
||||
@@ -10,13 +9,14 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.core.models import Log
|
||||
from modoboa.core.utils import check_for_updates
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.listing import get_sort_order, get_listing_page
|
||||
from modoboa.lib.web_utils import (
|
||||
_render_to_string, render_to_json_response
|
||||
)
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from .. import signals
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
@@ -114,9 +114,9 @@ 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.
|
||||
"""
|
||||
return render_to_json_response(
|
||||
events.raiseQueryEvent("TopNotifications", request, True)
|
||||
)
|
||||
"""AJAX service to check for new top notifications to display."""
|
||||
notifications = signals.get_top_notifications.send(
|
||||
sender="top_notifications", include_all=True)
|
||||
notifications = reduce(
|
||||
lambda a, b: a + b, [notif[1] for notif in notifications])
|
||||
return render_to_json_response(notifications)
|
||||
|
||||
+14
-10
@@ -3,18 +3,19 @@
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ugettext as _
|
||||
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
|
||||
from .. import signals
|
||||
|
||||
logger = logging.getLogger("modoboa.auth")
|
||||
|
||||
@@ -40,9 +41,10 @@ def dologin(request):
|
||||
logger.info(
|
||||
_("User '%s' successfully logged in") % user.username
|
||||
)
|
||||
events.raiseEvent("UserLogin", request,
|
||||
form.cleaned_data["username"],
|
||||
form.cleaned_data["password"])
|
||||
signals.user_login.send(
|
||||
sender="dologin",
|
||||
username=form.cleaned_data["username"],
|
||||
password=form.cleaned_data["password"])
|
||||
return HttpResponseRedirect(find_nextlocation(request, user))
|
||||
error = _(
|
||||
"Your username and password didn't match. Please try again.")
|
||||
@@ -59,20 +61,22 @@ def dologin(request):
|
||||
nextlocation = request.GET.get("next", None)
|
||||
httpcode = 200
|
||||
|
||||
announcements = signals.get_announcements.send(
|
||||
sender="login", location="loginpage")
|
||||
announcements = [announcement[1] for announcement in announcements]
|
||||
return HttpResponse(_render_to_string(request, "registration/login.html", {
|
||||
"form": form, "error": error, "next": nextlocation,
|
||||
"annoucements": events.raiseQueryEvent("GetAnnouncement", "loginpage")
|
||||
"annoucements": announcements
|
||||
}), status=httpcode)
|
||||
|
||||
dologin = never_cache(dologin)
|
||||
|
||||
|
||||
def dologout(request):
|
||||
"""Logout the current user.
|
||||
"""
|
||||
"""Logout current user."""
|
||||
if not request.user.is_anonymous():
|
||||
events.raiseEvent("UserLogout", request)
|
||||
signals.user_logout.send(sender="dologout")
|
||||
logger = logging.getLogger("modoboa.auth")
|
||||
logger.info(_("User '%s' logged out") % request.user.username)
|
||||
logger.info(_("User {} logged out").format(request.user.username))
|
||||
logout(request)
|
||||
return HttpResponseRedirect(reverse("core:login"))
|
||||
|
||||
@@ -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
|
||||
@@ -16,22 +15,22 @@ from modoboa.lib.web_utils import (
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from ..forms import ProfileForm, APIAccessForm
|
||||
from .. import signals
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request, tplname="core/user_index.html"):
|
||||
extrajs = events.raiseQueryEvent("ExtraUprefsJS", request.user)
|
||||
return render(request, tplname, {
|
||||
"selection": "user",
|
||||
"extrajs": "".join(extrajs)
|
||||
})
|
||||
"""Render user index page."""
|
||||
return render(request, tplname, {"selection": "user"})
|
||||
|
||||
|
||||
@login_required
|
||||
def profile(request, tplname='core/user_profile.html'):
|
||||
"""Profile detail/update view."""
|
||||
update_password = True
|
||||
if True in events.raiseQueryEvent("PasswordChange", request.user):
|
||||
results = signals.allow_password_change.send(
|
||||
sender="profile", user=request.user)
|
||||
if True in [result[1] for result in results]:
|
||||
update_password = False
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
@@ -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
|
||||
@@ -5,31 +5,31 @@ from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from modoboa.core import constants as core_constants
|
||||
from modoboa.core import signals as core_signals
|
||||
from modoboa.core.models import ObjectAccess, User
|
||||
from modoboa.lib import events
|
||||
|
||||
|
||||
def get_account_roles(user, account=None):
|
||||
"""Return the list of available account roles.
|
||||
|
||||
This function is used to create or modify an account. The returned
|
||||
list can be extended by listening to the ``GetExtraRoles`` event.
|
||||
This function is used to create or modify an account.
|
||||
|
||||
:param ``User`` user: connected user
|
||||
:param ``User`` account: account beeing modified (None on creation)
|
||||
:return: list of strings
|
||||
"""
|
||||
result = [core_constants.SIMPLEUSERS_ROLE]
|
||||
filters = events.raiseQueryEvent(
|
||||
"UserCanSetRole", user, "DomainAdmins", account
|
||||
)
|
||||
if user.has_perm("admin.add_domain") and \
|
||||
(not filters or True in filters):
|
||||
filters = core_signals.user_can_set_role.send(
|
||||
sender="get_account_roles", user=user, role="DomainAdmins",
|
||||
account=account)
|
||||
condition = (
|
||||
user.has_perm("admin.add_domain") and
|
||||
(not filters or True in [flt[1] for flt in filters]))
|
||||
if condition:
|
||||
result += [core_constants.DOMAINADMINS_ROLE]
|
||||
if user.is_superuser:
|
||||
result += [
|
||||
core_constants.RESELLERS_ROLE, core_constants.SUPERADMINS_ROLE]
|
||||
result += events.raiseQueryEvent("GetExtraRoles", user, account)
|
||||
return sorted(result, key=lambda role: role[1])
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""."""
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.template import Template, Context
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.lib import events
|
||||
from modoboa.core import signals as core_signals
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -89,16 +89,22 @@ def extra_static_content(caller, st_type, user):
|
||||
tpl = template.Template(
|
||||
"{% for sc in static_content %}{{ sc|safe }}{% endfor %}"
|
||||
)
|
||||
static_content = core_signals.extra_static_content.send(
|
||||
sender="extra_static_content",
|
||||
caller=caller, st_type=st_type, user=user)
|
||||
static_content = [result[1] for result in static_content]
|
||||
return tpl.render(
|
||||
template.Context({
|
||||
'static_content': events.raiseQueryEvent(
|
||||
"GetStaticContent", caller, st_type, user)
|
||||
})
|
||||
template.Context({"static_content": static_content})
|
||||
)
|
||||
|
||||
|
||||
@register.filter(name='localize_header_name')
|
||||
def localize_header_name(headername):
|
||||
""" Localizes the header names """
|
||||
names = {"From": _("From"), "To": _("To"), "Date": _("Date"), "Subject": _("Subject")}
|
||||
names = {
|
||||
"From": _("From"),
|
||||
"To": _("To"),
|
||||
"Date": _("Date"),
|
||||
"Subject": _("Subject")
|
||||
}
|
||||
return names.get(headername, headername)
|
||||
|
||||
@@ -3,21 +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 controls
|
||||
from . import general_callbacks
|
||||
|
||||
|
||||
class LimitsConfig(AppConfig):
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""Event handlers that check if limits are reached."""
|
||||
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.permissions import get_object_owner
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
if not owner.is_superuser:
|
||||
for name, ltpl in utils.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()
|
||||
|
||||
|
||||
@events.observe("UserCanSetRole")
|
||||
def user_can_set_role(user, role, account=None):
|
||||
"""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 ``User`` account: account modified (None on creation)
|
||||
:param str newrole: role to check
|
||||
"""
|
||||
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]
|
||||
|
||||
|
||||
@events.observe("AccountModified")
|
||||
def on_account_modified(old, new):
|
||||
"""Update limits when roles are updated"""
|
||||
owner = get_object_owner(old)
|
||||
if owner.role not in ["SuperAdmins", "Resellers"]:
|
||||
# Domain admins can't change the role so nothing to check.
|
||||
return
|
||||
|
||||
if new.role not in ["DomainAdmins", "Resellers"]:
|
||||
move_pool_resource(owner, new)
|
||||
|
||||
|
||||
@events.observe("AccountDeleted")
|
||||
def on_account_deleted(account, byuser, **kwargs):
|
||||
owner = get_object_owner(account)
|
||||
if owner.role not in ["SuperAdmins", "Resellers"]:
|
||||
return
|
||||
|
||||
move_pool_resource(owner, account)
|
||||
@@ -1,137 +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("ExtraAdminContent")
|
||||
def display_pool_usage(user, target, currentpage):
|
||||
condition = (
|
||||
not param_tools.get_global_parameter("enable_admin_limits") or
|
||||
target != "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))
|
||||
]
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@events.observe("GetStaticContent")
|
||||
def get_static_content(caller, st_type, user):
|
||||
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 ["""<style>
|
||||
.resource {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.resource .progress {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.resource .progress .bar {
|
||||
color: #000000;
|
||||
}
|
||||
</style>
|
||||
"""]
|
||||
return ["""
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$(".progress").tooltip();
|
||||
});
|
||||
</script>
|
||||
"""]
|
||||
@@ -2,16 +2,20 @@
|
||||
|
||||
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
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
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 forms
|
||||
from . import lib
|
||||
from . import models
|
||||
from . import utils
|
||||
@@ -99,3 +103,185 @@ def display_admin_limits(sender, user, account, **kwargs):
|
||||
account.userobjectlimit_set.select_related("content_type"))
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
@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."""
|
||||
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 """<style>
|
||||
.resource {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.resource .progress {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.resource .progress .bar {
|
||||
color: #000000;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
return """
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$(".progress").tooltip();
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
+15
-1
@@ -5,10 +5,24 @@ from . import constants
|
||||
|
||||
def get_user_limit_templates():
|
||||
"""Return defined templates."""
|
||||
# FIXME: change GetExtraLimitTemplates
|
||||
return constants.DEFAULT_USER_LIMITS.items()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
]
|
||||
}
|
||||
@@ -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", )
|
||||
|
||||
target_host = "external.host.tld"
|
||||
domain = factory.SubFactory(DomainFactory)
|
||||
target_host = 'external.host.tld'
|
||||
verify_recipients = True
|
||||
service = factory.SubFactory(ServiceFactory)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
"""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("""<script src="{{ STATIC_URL }}relaydomains/js/relay_domains.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
var rdomain;
|
||||
$(document).ready(function() {
|
||||
rdomain = new RelayDomains({});
|
||||
});
|
||||
</script>
|
||||
""")
|
||||
return [t.render(Context({'STATIC_URL': settings.STATIC_URL}))]
|
||||
|
||||
|
||||
@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("""
|
||||
<li><em>relaydomain; name; target host; service; enabled; verify recipients</em></li>
|
||||
""")]
|
||||
|
||||
|
||||
@events.observe('ImportObject')
|
||||
def get_import_func(objtype):
|
||||
from .lib import import_relaydomain
|
||||
|
||||
if objtype == 'relaydomain':
|
||||
return [import_relaydomain]
|
||||
return []
|
||||
@@ -1,18 +1,24 @@
|
||||
"""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 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")
|
||||
@@ -35,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...
|
||||
@@ -52,3 +58,100 @@ 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("""<script src="{{ STATIC_URL }}relaydomains/js/relay_domains.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
var rdomain;
|
||||
$(document).ready(function() {
|
||||
rdomain = new RelayDomains({});
|
||||
});
|
||||
</script>
|
||||
""")
|
||||
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_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 None
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""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
|
||||
|
||||
@@ -11,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,
|
||||
@@ -29,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
|
||||
@@ -61,15 +64,26 @@ 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")
|
||||
cls.rdom.domain.type = "relaydomain"
|
||||
cls.rdom.domain.save()
|
||||
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.
|
||||
|
||||
@@ -79,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)
|
||||
@@ -126,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."""
|
||||
@@ -144,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")
|
||||
@@ -164,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):
|
||||
@@ -193,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."""
|
||||
@@ -226,6 +241,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
|
||||
|
||||
+9
-5
@@ -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[1] for route in extra_routes])
|
||||
urlpatterns += patterns(
|
||||
'',
|
||||
*extra_routes
|
||||
)
|
||||
|
||||
# API urls
|
||||
urlpatterns += patterns(
|
||||
|
||||
Reference in New Issue
Block a user