From 75ecddf8791b1eb2722538530d9cb015e225f9a4 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 18 Oct 2013 16:09:29 +0200 Subject: [PATCH 01/32] Initial support of relay domains. see #491 --- modoboa/core/templatetags/core_tags.py | 12 -- modoboa/demo/__init__.py | 9 +- modoboa/extensions/admin/__init__.py | 5 + modoboa/extensions/admin/lib.py | 30 +++- .../admin/migrations/0037_rename_tables.py | 7 + modoboa/extensions/admin/models/__init__.py | 4 +- modoboa/extensions/admin/models/domain.py | 4 + .../extensions/admin/static/admin/js/admin.js | 125 +++++++++++------ .../admin/templates/admin/domains.html | 1 + .../admin/templates/admin/domains_table.html | 21 +++ .../admin/templates/admin/identities.html | 1 + .../admin/templates/admin/listing.html | 2 +- .../admin/templatetags/admin_tags.py | 92 ++++++++++--- modoboa/extensions/admin/views/domain.py | 31 +++-- modoboa/extensions/amavis/__init__.py | 40 +++--- modoboa/extensions/limits/__init__.py | 8 +- .../postfix_relay_domains/__init__.py | 118 ++++++++++++++++ .../postfix_relay_domains/app_settings.py | 14 ++ .../extensions/postfix_relay_domains/forms.py | 12 ++ .../postfix_relay_domains/models.py | 129 ++++++++++++++++++ .../postfix_relay_domains/js/relay_domains.js | 79 +++++++++++ .../relaydomain_form.html | 7 + .../postfix_relay_domains/service_field.html | 2 + .../extensions/postfix_relay_domains/tests.py | 16 +++ .../extensions/postfix_relay_domains/urls.py | 9 ++ .../extensions/postfix_relay_domains/views.py | 87 ++++++++++++ modoboa/lib/events.py | 4 +- modoboa/lib/templatetags/lib_tags.py | 18 +++ modoboa/lib/webutils.py | 11 ++ modoboa/static/js/global.js | 77 ++++++++++- modoboa/templates/common/generic_field.html | 1 + modoboa/templates/nlayout.html | 2 +- 32 files changed, 864 insertions(+), 114 deletions(-) create mode 100644 modoboa/extensions/admin/templates/admin/domains_table.html create mode 100644 modoboa/extensions/postfix_relay_domains/__init__.py create mode 100644 modoboa/extensions/postfix_relay_domains/app_settings.py create mode 100644 modoboa/extensions/postfix_relay_domains/forms.py create mode 100644 modoboa/extensions/postfix_relay_domains/models.py create mode 100644 modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js create mode 100644 modoboa/extensions/postfix_relay_domains/templates/postfix_relay_domains/relaydomain_form.html create mode 100644 modoboa/extensions/postfix_relay_domains/templates/postfix_relay_domains/service_field.html create mode 100644 modoboa/extensions/postfix_relay_domains/tests.py create mode 100644 modoboa/extensions/postfix_relay_domains/urls.py create mode 100644 modoboa/extensions/postfix_relay_domains/views.py diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index 91238b489..b424c1f9f 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -230,18 +230,6 @@ def get_modoboa_logo(): return logo -@register.simple_tag -def extra_head_content(user): - tpl = template.Template( - "{% for sc in static_content %}{{ sc|safe }}{% endfor %}" - ) - return tpl.render( - template.Context({ - 'static_content': events.raiseQueryEvent("GetStaticContent", user) - }) - ) - - @register.simple_tag def load_optionalmenu(user): menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu_middle", user) diff --git a/modoboa/demo/__init__.py b/modoboa/demo/__init__.py index 76f5c05b4..ca8931b96 100755 --- a/modoboa/demo/__init__.py +++ b/modoboa/demo/__init__.py @@ -43,12 +43,13 @@ def password_change(user): @events.observe("GetStaticContent") -def get_static_content(user): - if not user.mailbox_set.count(): +def get_static_content(caller, user): + if caller != 'top' and not user.mailbox_set.count(): return [] - return """""" +"""] diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 79c8fad5d..c567fc059 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -15,6 +15,11 @@ admin_events = [ "CreateDomain", "DomainModified", "DeleteDomain", + "ExtraDomainEntries", + "ExtraDomainMenuEntries", + "ExtraDomainFilters", + "GetDomainActions", + "GetDomainModifyLink", "DomainAliasCreated", "DomainAliasDeleted", diff --git a/modoboa/extensions/admin/lib.py b/modoboa/extensions/admin/lib.py index 758371f13..bf142a297 100644 --- a/modoboa/extensions/admin/lib.py +++ b/modoboa/extensions/admin/lib.py @@ -1,13 +1,16 @@ # coding: utf-8 from functools import wraps +from itertools import chain from django.db.models import Q from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib.contenttypes.models import ContentType from modoboa.core.models import User -from modoboa.lib import parameters +from modoboa.lib import parameters, events from modoboa.lib.exceptions import ModoboaException from modoboa.lib.emailutils import split_mailbox -from modoboa.extensions.admin.models import Alias +from modoboa.extensions.admin.models import ( + Domain, Alias +) def needs_mailbox(): @@ -65,8 +68,6 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None): :param list grpfilter: group names filters :return: a queryset """ - from itertools import chain - accounts = [] if idtfilter is None or not idtfilter or idtfilter == "account": ids = user.objectaccess_set \ @@ -104,3 +105,24 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None): if idtfilter is not None and idtfilter: aliases = [al for al in aliases if al.type == idtfilter] return chain(accounts, aliases) + + +def get_domains(user, domfilter=None, searchquery=None, **extrafilters): + """Return all the domains the user can access. + + :param ``User`` user: user object + :param str searchquery: filter + :rtype: list + :return: a list of domains and/or relay domains + """ + domains = [] + if domfilter is None or not domfilter or domfilter == 'domain': + domains = Domain.objects.get_for_admin(user) + if searchquery is not None: + q = Q(name__contains=searchquery) + q |= Q(domainalias__name__contains=searchquery) + domains = domains.filter(q).distinct() + extra_domain_entries = events.raiseQueryEvent( + 'ExtraDomainEntries', user, domfilter, searchquery, **extrafilters + ) + return chain(domains, extra_domain_entries) diff --git a/modoboa/extensions/admin/migrations/0037_rename_tables.py b/modoboa/extensions/admin/migrations/0037_rename_tables.py index 077918c22..6b08aabd7 100644 --- a/modoboa/extensions/admin/migrations/0037_rename_tables.py +++ b/modoboa/extensions/admin/migrations/0037_rename_tables.py @@ -75,6 +75,13 @@ class Migration(SchemaMigration): 'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) }, + u'admin.mailboxoperation': { + 'Meta': {'object_name': 'MailboxOperation'}, + 'argument': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, 'admin.objectdates': { 'Meta': {'object_name': 'ObjectDates'}, 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), diff --git a/modoboa/extensions/admin/models/__init__.py b/modoboa/extensions/admin/models/__init__.py index 8edb89898..2a34185d3 100644 --- a/modoboa/extensions/admin/models/__init__.py +++ b/modoboa/extensions/admin/models/__init__.py @@ -1,8 +1,10 @@ +from .base import DatesAware from .domain import Domain from .domain_alias import DomainAlias from .mailbox import Mailbox, Quota, MailboxOperation from .alias import Alias __all__ = [ - 'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias', 'MailboxOperation' + 'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias', + 'MailboxOperation', 'DatesAware' ] diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py index c4d7d5a36..d497d8260 100644 --- a/modoboa/extensions/admin/models/domain.py +++ b/modoboa/extensions/admin/models/domain.py @@ -56,6 +56,10 @@ class Domain(DatesAware): def mbalias_count(self): return self.alias_set.count() + @property + def tags(self): + return [{"name": "domain", "label": _("Domain"), "type": "dom"}] + @property def admins(self): """Return the domain administrators of this domain diff --git a/modoboa/extensions/admin/static/admin/js/admin.js b/modoboa/extensions/admin/static/admin/js/admin.js index 0e7c225be..f811d322b 100644 --- a/modoboa/extensions/admin/static/admin/js/admin.js +++ b/modoboa/extensions/admin/static/admin/js/admin.js @@ -1,3 +1,8 @@ +/** + * @module admin + * @desc Utility classes for the admin application + */ + var Admin = function(options) { Listing.call(this, options); }; @@ -12,6 +17,7 @@ Admin.prototype = { Listing.prototype.initialize.call(this, options); this.options = $.extend({}, this.defaults, this.options); this.options.defcallback = $.proxy(this.list_cb, this); + this.tag_handlers = {}; this.navobj = new History(this.options); @@ -37,12 +43,61 @@ Admin.prototype = { list_cb: function(data) { $("#listing").html(data.table); this.update_listing(data); + $("a.filter").click($.proxy(this.filter_by_tag, this)); }, listen: function() { $("#searchform").submit($.proxy(this.do_search, this)); }, + register_tag_handler: function(name, handler) { + this.tag_handlers[name] = handler; + if (this.navobj.getparam(name + "filter") != undefined) { + var text = this.navobj.getparam(name + "filter"); + $("#searchform").parent().after(this.make_tag(text, name)); + } + }, + + generic_tag_handler: function(tag, $link) { + if (this.navobj.getparam(tag + "filter") == undefined && $link.hasClass(tag)) { + var text = $link.attr("name"); + this.navobj.setparam(tag + "filter", text).update(); + $("#searchform").parent().after(this.make_tag(text, tag)); + return true; + } + return false; + }, + + make_tag: function(text, type) { + var $tag = $("", {"name": type, "class" : "btn btn-mini", "html": text}); + var $i = $("", {"class" : "icon-remove"}).prependTo($tag); + + $tag.click($.proxy(this.remove_tag, this)); + return $tag; + }, + + remove_tag: function(e) { + var $tag = $(e.target); + + if ($tag.is("i")) { + $tag = $tag.parent(); + } + e.preventDefault(); + this.navobj.delparam($tag.attr("name") + "filter").update(); + $tag.remove(); + }, + + filter_by_tag: function(e) { + var $link = $(e.target); + e.preventDefault(); + + for (var name in this.tag_handlers) { + if (this.tag_handlers[name].apply(this, [name, $link])) { + break; + } + } + }, + do_search: function(e) { e.preventDefault(); var squery = $("#searchquery").val(); @@ -104,6 +159,11 @@ var Domains = function(options) { }; Domains.prototype = { + initialize: function(options) { + Admin.prototype.initialize.call(this, options); + this.register_tag_handler("dom", this.generic_tag_handler); + }, + list_cb: function(data) { Admin.prototype.list_cb.call(this, data); var deloptions = (data.handle_mailboxes) @@ -120,6 +180,11 @@ Domains.prototype = { checkboxes: deloptions, success_cb: $.proxy(this.reload_listing, this) }); + $("a[name=delrelaydomain]").confirm({ + question: function() { return this.$element.attr('title'); }, + method: "DELETE", + success_cb: $.proxy(this.reload_listing, this) + }); }, change_inputs_state: function(value) { @@ -193,6 +258,17 @@ Domains.prototype = { }); }, this)); $(document).trigger('domform_init'); + }, + + rdomainform_cb: function() { + $(".submit").one('click', $.proxy(function(e) { + simple_ajax_form_post2(e, { + formid: "rdomform", + error_cb: $.proxy(this.rdomainform_cb, this), + reload_on_success: false, + success_cb: $.proxy(this.reload_listing, this) + }); + }, this)); } }; @@ -209,14 +285,8 @@ var Identities = function(options) { Identities.prototype = { initialize: function(options) { Admin.prototype.initialize.call(this, options); - if (this.navobj.getparam("idtfilter") != undefined) { - var text = this.navobj.getparam("idtfilter"); - $("#searchform").parent().after(this.make_tag(text, "idt")); - } - if (this.navobj.getparam("grpfilter") != undefined) { - var text = this.navobj.getparam("grpfilter"); - $("#searchform").parent().after(this.make_tag(text, "grp")); - } + this.register_tag_handler("idt", this.generic_tag_handler); + this.register_tag_handler("grp", this.grp_tag_handler); }, listen: function() { @@ -237,7 +307,6 @@ Identities.prototype = { if (data.handle_mailboxes == "yes") { deloptions = {keepdir: gettext("Do not delete mailbox directory")}; } - $("a.filter").click($.proxy(this.filter_by_tag, this)); $("a[name=delaccount]").confirm({ question: function() { return this.$element.attr('title'); }, @@ -252,46 +321,20 @@ Identities.prototype = { }); }, - make_tag: function(text, type) { - var $tag = $("", {"name": type, "class" : "btn btn-mini", "html": text}); - var $i = $("", {"class" : "icon-remove"}).prependTo($tag); - - $tag.click($.proxy(this.remove_tag, this)); - return $tag; - }, - - remove_tag: function(e) { - var $tag = $(e.target); - - if ($tag.is("i")) { - $tag = $tag.parent(); - } - e.preventDefault(); - this.navobj.delparam($tag.attr("name") + "filter").update(); - $tag.remove(); - }, - - filter_by_tag: function(e) { - var $link = $(e.target); - e.preventDefault(); - - if (this.navobj.getparam("idtfilter") == undefined && $link.hasClass("idt")) { - var text = $link.attr("name"); - this.navobj.setparam("idtfilter", text).update(); - $("#searchform").parent().after(this.make_tag(text, "idt")); - return; - } - if (this.navobj.getparam("grpfilter") == undefined && $link.hasClass("grp")) { + grp_tag_handler: function(tag, $link) { + if (this.navobj.getparam(tag + "filter") == undefined && $link.hasClass(tag)) { var text = $link.attr("name"); this.navobj .setparam("idtfilter", "account") - .setparam("grpfilter", text) + .setparam(tag + "filter", text) .update(); if ($("a[name=idt]").length == 0) { $("#searchform").parent().after(this.make_tag("account", "idt")); } - $("#searchform").parent().after(this.make_tag(text, "grp")); + $("#searchform").parent().after(this.make_tag(text, tag)); + return true; } + return false; }, simpleuser_mode: function() { diff --git a/modoboa/extensions/admin/templates/admin/domains.html b/modoboa/extensions/admin/templates/admin/domains.html index 65163e602..d32690232 100644 --- a/modoboa/extensions/admin/templates/admin/domains.html +++ b/modoboa/extensions/admin/templates/admin/domains.html @@ -7,5 +7,6 @@ $(document).ready(function() { admin = new Domains(); }); +{% extra_static_content "domains" user %} {% endblock %} {% block actions %}{% domains_menu "domains" user %}{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/domains_table.html b/modoboa/extensions/admin/templates/admin/domains_table.html new file mode 100644 index 000000000..b3f16ba9d --- /dev/null +++ b/modoboa/extensions/admin/templates/admin/domains_table.html @@ -0,0 +1,21 @@ +{% load i18n admin_tags lib_tags %} + + + + + + + + + + + {% for domain in domains %} + + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Alias(es)" %}{% trans "Tags" %}{% trans "Actions" %}
{% domain_modify_link domain %}{% domain_aliases domain %} {% render_tags domain.tags %}{% domain_actions user domain %}
diff --git a/modoboa/extensions/admin/templates/admin/identities.html b/modoboa/extensions/admin/templates/admin/identities.html index 82e172a6d..632a6f31c 100644 --- a/modoboa/extensions/admin/templates/admin/identities.html +++ b/modoboa/extensions/admin/templates/admin/identities.html @@ -20,5 +20,6 @@ $(document).ready(function() { admin = new Identities(); }); +{% extra_static_content "identities" user %} {% endblock %} {% block actions %}{% identities_menu user %}{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/listing.html b/modoboa/extensions/admin/templates/admin/listing.html index 513512d17..cbcbbbc86 100644 --- a/modoboa/extensions/admin/templates/admin/listing.html +++ b/modoboa/extensions/admin/templates/admin/listing.html @@ -1,4 +1,4 @@ -{% extends "admin/header.html" %}{% load i18n admin_tags lib_tags %} +{% extends "admin/header.html" %}{% load i18n admin_tags %} {% block html_head %} {{ block.super }} diff --git a/modoboa/extensions/admin/templatetags/admin_tags.py b/modoboa/extensions/admin/templatetags/admin_tags.py index 6736fbbe9..e1fee75c9 100644 --- a/modoboa/extensions/admin/templatetags/admin_tags.py +++ b/modoboa/extensions/admin/templatetags/admin_tags.py @@ -15,6 +15,16 @@ genders = { @register.simple_tag def domains_menu(selection, user): + """Specific menu for domain related operations. + + Corresponds to the menu visible on the left column when you go to + Domains. + + :param str selection: menu entry currently selected + :param ``User`` user: connected user + :rtype: str + :return: rendered menu (as HTML) + """ if not user.has_perm("admin.add_domain"): return "" @@ -25,6 +35,9 @@ def domains_menu(selection, user): "modal": True, "modalcb": "admin.newdomain_cb", "url": reverse("modoboa.extensions.admin.views.domain.newdomain")}, + ] + entries += events.raiseQueryEvent("ExtraDomainMenuEntries", user) + entries += [ {"name": "import", "label": _("Import"), "img": "icon-folder-open", @@ -111,23 +124,28 @@ def identities_menu(user): @register.simple_tag -def domain_actions(user, domid): - from modoboa.extensions.admin.models import Domain +def domain_actions(user, domain): + actions = [] + if domain.__class__.__name__ == 'Domain': + actions = [ + {"name": "listidentities", + "url": reverse("modoboa.extensions.admin.views.identity.identities") + "#list/?searchquery=@%s" % domain.name, + "title": _("View the domain's identities"), + "img": "icon-user"} + ] + if user.has_perm("admin.delete_domain"): + actions.append({ + "name": "deldomain", + "url": reverse( + "modoboa.extensions.admin.views.domain.deldomain", + args=[domain.id] + ), + "title": _("Delete %s?" % domain.name), + "img": "icon-trash" + }) + else: + actions = events.raiseQueryEvent('GetDomainActions', user, domain) - domain = Domain.objects.get(pk=domid) - actions = [ - {"name": "listidentities", - "url": reverse("modoboa.extensions.admin.views.identity.identities") + "#list/?searchquery=@%s" % domain.name, - "title": _("View the domain's identities"), - "img": "icon-user"} - ] - if user.has_perm("admin.delete_domain"): - actions.append({ - "name": "deldomain", - "url": reverse("modoboa.extensions.admin.views.domain.deldomain", args=[domid]), - "title": _("Delete %s?" % domain.name), - "img": "icon-trash" - }) return render_actions(actions) @@ -153,8 +171,50 @@ def identity_actions(user, ident): return render_actions(actions) +@register.simple_tag +def domain_modify_link(domain): + linkdef = {"label": domain.name, "modal": True} + if domain.__class__.__name__ == "Domain": + linkdef["url"] = reverse( + "modoboa.extensions.admin.views.domain.editdomain", + args=[domain.id] + ) + linkdef["modalcb"] = "admin.domainform_cb" + else: + tmp = events.raiseDictEvent('GetDomainModifyLink', domain) + for key in ['url', 'modalcb']: + linkdef[key] = tmp[key] + return render_link(linkdef) + + +@register.simple_tag +def domain_aliases(domain): + """Display domain aliases of this domain. + + :param domain: + :rtype: str + """ + if domain.__class__.__name__ == "Domain": + if not domain.domainalias_set.count(): + return '---' + res = "" + for da in domain.domainalias_set.all(): + res += "%s
" % da.name + return res + return '---' + + @register.simple_tag def identity_modify_link(identity, active_tab='default'): + """Return the appropriate modification link. + + According to the identity type, a specific modification link (URL) + must be used. + + :param identity: a ``User`` or ``Alias`` instance + :param str active_tab: the tab to display + :rtype: str + """ linkdef = {"label": identity.identity, "modal": True} if identity.__class__.__name__ == "User": linkdef["url"] = reverse( diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py index 91f2c1669..0b9d83ab8 100644 --- a/modoboa/extensions/admin/views/domain.py +++ b/modoboa/extensions/admin/views/domain.py @@ -15,13 +15,13 @@ from modoboa.lib.webutils import ( from modoboa.lib.formutils import CreationWizard from modoboa.lib.exceptions import PermDeniedException from modoboa.lib.templatetags.lib_tags import pagination_bar -from modoboa.extensions.admin.tables import DomainsTable from modoboa.extensions.admin.lib import get_sort_order, get_listing_page from modoboa.extensions.admin.exceptions import AdminError from modoboa.extensions.admin.models import Domain, Mailbox from modoboa.extensions.admin.forms import ( DomainForm, DomainFormGeneral, DomainFormOptions ) +from modoboa.extensions.admin.lib import get_domains @login_required @@ -35,17 +35,28 @@ def index(request): ) def _domains(request): sort_order, sort_dir = get_sort_order(request.GET, "name") - domains = Domain.objects.get_for_admin(request.user) - squery = request.GET.get("searchquery", None) - if squery is not None: - q = Q(name__contains=squery) - q |= Q(domainalias__name__contains=squery) - domains = domains.filter(q).distinct() + filters = dict( + (flt, request.GET.get(flt, None)) + for flt in ['domfilter', 'searchquery'] + + events.raiseQueryEvent('ExtraDomainFilters') + ) + domainlist = get_domains( + request.user, **filters + ) if sort_order in ["name", "domainalias__name"]: - domains = domains.order_by("%s%s" % (sort_dir, sort_order)) - page = get_listing_page(domains, request.GET.get("page", 1)) + domainlist = sorted( + domainlist, + key=lambda d: getattr(d, sort_order), reverse=sort_dir == '-' + ) + else: + domainlist = sorted(domainlist, key=lambda d: d.tags[0], + reverse=sort_dir == '-') + page = get_listing_page(domainlist, request.GET.get("page", 1)) return ajax_simple_response({ - "table": DomainsTable(request, page.object_list).render(), + "table": _render_to_string(request, 'admin/domains_table.html', { + 'domains': domainlist, + 'tableid': 'domains' + }), "page": page.number, "paginbar": pagination_bar(page), "handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES", diff --git a/modoboa/extensions/amavis/__init__.py b/modoboa/extensions/amavis/__init__.py index 9abc912ed..a376a9a34 100644 --- a/modoboa/extensions/amavis/__init__.py +++ b/modoboa/extensions/amavis/__init__.py @@ -95,13 +95,14 @@ def on_delete_domain(domain): @events.observe("GetStaticContent") -def extra_static_content(user): +def extra_static_content(caller, user): if user.group == "SimpleUsers": return [] - tpl = Template("""""") + +""") + return [tpl.render( + Context({ + 'url': reverse("modoboa.extensions.amavis.views.nbrequests"), + 'interval': int(parameters.get_admin("CHECK_REQUESTS_INTERVAL")) * 1000, + 'text': _("pending requests"), + }) + )] - return [tpl.render( - Context(dict( - url=reverse("modoboa.extensions.amavis.views.nbrequests"), - interval=int(parameters.get_admin("CHECK_REQUESTS_INTERVAL")) * 1000, - text=_("pending requests"), - user_can_release=parameters.get_admin("USER_CAN_RELEASE") - )) - )] + if caller == 'domains': + tpl = Template(""" +""") + + return [tpl.render(Context({}))] + return [] @events.observe("TopNotifications") diff --git a/modoboa/extensions/limits/__init__.py b/modoboa/extensions/limits/__init__.py index 2a22f37e8..5c991598f 100644 --- a/modoboa/extensions/limits/__init__.py +++ b/modoboa/extensions/limits/__init__.py @@ -116,10 +116,12 @@ def fill_account_instances(user, account, instances): @events.observe("GetStaticContent") -def get_static_content(user): +def get_static_content(caller, user): + if caller not in ['domains', 'identities']: + return [] if user.group == "SimpleUsers": return [] - return """ - -"""] diff --git a/modoboa/extensions/limits/general_callbacks.py b/modoboa/extensions/limits/general_callbacks.py new file mode 100644 index 000000000..0d41c2449 --- /dev/null +++ b/modoboa/extensions/limits/general_callbacks.py @@ -0,0 +1,97 @@ +from django.utils.translation import ugettext as _ +from modoboa.lib import events +from modoboa.extensions.limits.models import LimitTemplates +from modoboa.extensions.limits.forms import ResourcePoolForm + + +@events.observe("GetExtraRoles") +def get_extra_roles(user): + if user.is_superuser: + return [("Resellers", _("Reseller")), ] + return [] + + +@events.observe("ExtraAdminContent") +def display_pool_usage(user, target, currentpage): + from django.template.loader import render_to_string + + if target != "leftcol" or user.is_superuser: + return [] + if currentpage == "identities": + names = ["mailboxes_limit", "mailbox_aliases_limit"] + if user.has_perm("admin.add_domain"): + names += ["domain_admins_limit"] + else: + names = [ + tpl[0] for tpl in LimitTemplates().templates + if tpl[0] not in ["domain_admins_limit", "mailboxes_limit", + "mailbox_aliases_limit"] + and (len(tpl) == 3 or tpl[3] == user.group) + ] + + limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0) + if len(limits) == 0: + return [] + return [render_to_string("limits/poolusage.html", dict(limits=limits))] + + +@events.observe("ExtraAccountForm") +def extra_account_form(user, account=None): + if not user.group in ["SuperAdmins", "Resellers"]: + return [] + if account is not None and \ + not account.group in ["Resellers", "DomainAdmins"]: + return [] + + return [ + dict( + id="resources", title=_("Resources"), cls=ResourcePoolForm + ) + ] + + +@events.observe("CheckExtraAccountForm") +def check_form_access(account, form): + if form["id"] != "resources": + return [True] + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return [False] + return [True] + + +@events.observe("FillAccountInstances") +def fill_account_instances(user, account, instances): + if not user.is_superuser and not user.belongs_to_group("Resellers"): + return + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return + instances["resources"] = account + + +@events.observe("GetStaticContent") +def get_static_content(caller, user): + if caller not in ['domains', 'identities']: + return [] + if user.group == "SimpleUsers": + return [] + return [""" + +"""] diff --git a/modoboa/extensions/postfix_autoreply/__init__.py b/modoboa/extensions/postfix_autoreply/__init__.py index fdbebf511..c28dd67e0 100644 --- a/modoboa/extensions/postfix_autoreply/__init__.py +++ b/modoboa/extensions/postfix_autoreply/__init__.py @@ -6,13 +6,20 @@ This module provides a way to integrate Modoboa auto-reply functionality into Postfix. """ +import sys from django.utils.translation import ugettext_lazy from modoboa.lib import events, parameters from modoboa.core.extensions import ModoExtension, exts_pool -from .models import Transport, Alias +from modoboa.extensions.postfix_autoreply.models import ( + Transport, Alias +) class PostfixAutoreply(ModoExtension): + """ + Auto-reply (vacation) functionality using Postfix. + + """ name = "postfix_autoreply" label = "Postfix autoreply" version = "1.0" @@ -38,14 +45,12 @@ class PostfixAutoreply(ModoExtension): def load(self): from modoboa.extensions.postfix_autoreply.app_settings import ParametersForm parameters.register(ParametersForm, ugettext_lazy("Automatic replies")) + from modoboa.extensions.postfix_autoreply import general_callbacks + if 'modoboa.extensions.postfix_autoreply.general_callbacks' in sys.modules: + reload(general_callbacks) def destroy(self): - events.unregister("DomainCreated", onDomainCreated) - events.unregister("DomainDeleted", onDomainDeleted) - events.unregister("MailboxCreated", onMailboxCreated) - events.unregister("MailboxDeleted", onMailboxDeleted) - events.unregister("MailboxModified", onModifyMailbox) - events.unregister("UserMenuDisplay", menu) + events.unregister_extension() parameters.unregister() exts_pool.register_extension(PostfixAutoreply) @@ -55,87 +60,3 @@ exts_pool.register_extension(PostfixAutoreply) def extra_routes(): return [(r'^user/autoreply/$', 'modoboa.extensions.postfix_autoreply.views.autoreply'), ] - - -@events.observe("ExtraUprefsJS") -def extra_js(user): - return ["""function autoreply_cb() { - $('#id_untildate').datepicker({format: 'yyyy-mm-dd', language: '%s'}); -} -""" % parameters.get_user(user, "LANG", app="core") - ] - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "uprefs_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "autoreply", - "class": "ajaxlink", - "url": "autoreply/", - "label": ugettext_lazy("Auto-reply message")} - ] - - -@events.observe("DomainCreated") -def onDomainCreated(user, domain): - transport = Transport() - transport.domain = "autoreply.%s" % domain.name - transport.method = "autoreply:" - transport.save() - - -@events.observe("DomainModified") -def onDomainModified(domain): - if domain.oldname == domain.name: - return - Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \ - .update(domain="autoreply.%s" % domain.name) - for al in Alias.objects.filter(full_address__contains="@%s" % domain.oldname): - new_address = al.full_address.replace("@%s" % domain.oldname, "@%s" % domain.name) - al.full_address = new_address - al.autoreply_address = "%s@autoreply.%s" % (new_address, domain.name) - al.save() - - -@events.observe("DomainDeleted") -def onDomainDeleted(domain): - Transport.objects.filter(domain="autoreply.%s" % domain.name).delete() - - -@events.observe("MailboxCreated") -def onMailboxCreated(user, mailbox): - alias = Alias() - alias.full_address = mailbox.full_address - alias.autoreply_address = \ - "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) - alias.save() - - -@events.observe("MailboxDeleted") -def onMailboxDeleted(mailboxes): - from modoboa.extensions.admin.models import Mailbox - - if isinstance(mailboxes, Mailbox): - mailboxes = [mailboxes] - for mailbox in mailboxes: - try: - alias = Alias.objects.get(full_address=mailbox.full_address) - except Alias.DoesNotExist: - pass - else: - alias.delete() - - -@events.observe("MailboxModified") -def onModifyMailbox(mailbox, oldmailbox): - if oldmailbox.full_address == mailbox.full_address: - return - alias = Alias.objects.get(full_address=oldmailbox.full_address) - alias.full_address = mailbox.full_address - alias.autoreply_address = \ - "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) - alias.save() diff --git a/modoboa/extensions/postfix_autoreply/general_callbacks.py b/modoboa/extensions/postfix_autoreply/general_callbacks.py new file mode 100644 index 000000000..c28de5e9c --- /dev/null +++ b/modoboa/extensions/postfix_autoreply/general_callbacks.py @@ -0,0 +1,87 @@ +from django.utils.translation import ugettext_lazy +from modoboa.lib import events, parameters +from modoboa.extensions.postfix_autoreply.models import Transport, Alias + + +@events.observe("ExtraUprefsJS") +def extra_js(user): + return ["""function autoreply_cb() { + $('#id_untildate').datepicker({format: 'yyyy-mm-dd', language: '%s'}); +} +""" % parameters.get_user(user, "LANG", app="core") + ] + + +@events.observe("UserMenuDisplay") +def menu(target, user): + if target != "uprefs_menu": + return [] + if not user.mailbox_set.count(): + return [] + return [ + {"name": "autoreply", + "class": "ajaxlink", + "url": "autoreply/", + "label": ugettext_lazy("Auto-reply message")} + ] + + +@events.observe("DomainCreated") +def onDomainCreated(user, domain): + transport = Transport() + transport.domain = "autoreply.%s" % domain.name + transport.method = "autoreply:" + transport.save() + + +@events.observe("DomainModified") +def onDomainModified(domain): + if domain.oldname == domain.name: + return + Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \ + .update(domain="autoreply.%s" % domain.name) + for al in Alias.objects.filter(full_address__contains="@%s" % domain.oldname): + new_address = al.full_address.replace("@%s" % domain.oldname, "@%s" % domain.name) + al.full_address = new_address + al.autoreply_address = "%s@autoreply.%s" % (new_address, domain.name) + al.save() + + +@events.observe("DomainDeleted") +def onDomainDeleted(domain): + Transport.objects.filter(domain="autoreply.%s" % domain.name).delete() + + +@events.observe("MailboxCreated") +def onMailboxCreated(user, mailbox): + alias = Alias() + alias.full_address = mailbox.full_address + alias.autoreply_address = \ + "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) + alias.save() + + +@events.observe("MailboxDeleted") +def onMailboxDeleted(mailboxes): + from modoboa.extensions.admin.models import Mailbox + + if isinstance(mailboxes, Mailbox): + mailboxes = [mailboxes] + for mailbox in mailboxes: + try: + alias = Alias.objects.get(full_address=mailbox.full_address) + except Alias.DoesNotExist: + pass + else: + alias.delete() + + +@events.observe("MailboxModified") +def onModifyMailbox(mailbox, oldmailbox): + if oldmailbox.full_address == mailbox.full_address: + return + alias = Alias.objects.get(full_address=oldmailbox.full_address) + alias.full_address = mailbox.full_address + alias.autoreply_address = \ + "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) + alias.save() diff --git a/modoboa/extensions/postfix_relay_domains/__init__.py b/modoboa/extensions/postfix_relay_domains/__init__.py index 10aba023a..dfe3fff0d 100644 --- a/modoboa/extensions/postfix_relay_domains/__init__.py +++ b/modoboa/extensions/postfix_relay_domains/__init__.py @@ -1,4 +1,5 @@ # coding: utf-8 +import sys from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy @@ -88,10 +89,16 @@ class PostfixRelayDomains(ModoExtension): ) events.declare(extension_events) from modoboa.extensions.postfix_relay_domains import general_callbacks + if 'modoboa.extensions.postfix_relay_domains.general_callbacks' in sys.modules: + reload(general_callbacks) if exts_pool.is_extension_enabled('limits'): import limits_callbacks + if 'modoboa.extensions.postfix_relay_domains.limits_callbacks' in sys.modules: + reload(limits_callbacks) if exts_pool.is_extension_enabled('amavis'): import amavis_callbacks + if 'modoboa.extensions.postfix_relay_domains.amavis_callbacks' in sys.modules: + reload(amavis_callbacks) def destroy(self): events.unregister_extension() diff --git a/modoboa/extensions/sievefilters/__init__.py b/modoboa/extensions/sievefilters/__init__.py index c5b88c95f..76cade1f7 100644 --- a/modoboa/extensions/sievefilters/__init__.py +++ b/modoboa/extensions/sievefilters/__init__.py @@ -1,6 +1,6 @@ # coding: utf-8 -from django.utils.translation import ugettext as _, ugettext_lazy -from django.core.urlresolvers import reverse +import sys +from django.utils.translation import ugettext_lazy from modoboa.lib import events, parameters from modoboa.core.extensions import ModoExtension, exts_pool @@ -16,38 +16,12 @@ class SieveFilters(ModoExtension): from .app_settings import ParametersForm, UserSettings parameters.register(ParametersForm, ugettext_lazy("Sieve filters")) parameters.register(UserSettings, ugettext_lazy("Message filters")) + from modoboa.extensions.sievefilters import general_callbacks + if 'modoboa.extensions.sievefilters.general_callbacks' in sys.modules: + reload(general_callbacks) def destroy(self): - events.unregister("UserMenuDisplay", menu) + events.unregister_extension() parameters.unregister() exts_pool.register_extension(SieveFilters) - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "options_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "sievefilters", - "label": _("Message filters"), - "url": reverse("modoboa.extensions.sievefilters.views.index"), - "img": "icon-check"} - ] - - -@events.observe("UserLogout") -def userlogout(request): - from .lib import SieveClient - - if not request.user.mailbox_set.count(): - return - try: - sc = SieveClient(user=request.user.username, - password=request.session["password"]) - except Exception: - pass - else: - sc.logout() diff --git a/modoboa/extensions/sievefilters/general_callbacks.py b/modoboa/extensions/sievefilters/general_callbacks.py new file mode 100644 index 000000000..0ce7a381b --- /dev/null +++ b/modoboa/extensions/sievefilters/general_callbacks.py @@ -0,0 +1,32 @@ +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from modoboa.lib import events + + +@events.observe("UserMenuDisplay") +def menu(target, user): + if target != "options_menu": + return [] + if not user.mailbox_set.count(): + return [] + return [ + {"name": "sievefilters", + "label": _("Message filters"), + "url": reverse("modoboa.extensions.sievefilters.views.index"), + "img": "icon-check"} + ] + + +@events.observe("UserLogout") +def userlogout(request): + from .lib import SieveClient + + if not request.user.mailbox_set.count(): + return + try: + sc = SieveClient(user=request.user.username, + password=request.session["password"]) + except Exception: + pass + else: + sc.logout() diff --git a/modoboa/extensions/stats/__init__.py b/modoboa/extensions/stats/__init__.py index 7197cc01c..e08b12b55 100644 --- a/modoboa/extensions/stats/__init__.py +++ b/modoboa/extensions/stats/__init__.py @@ -7,8 +7,8 @@ This module provides support to retrieve statistics from postfix log : sent, received, bounced, rejected """ -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _, ugettext_lazy +import sys +from django.utils.translation import ugettext_lazy from modoboa.lib import events, parameters from modoboa.core.extensions import ModoExtension, exts_pool @@ -17,7 +17,9 @@ class Stats(ModoExtension): name = "stats" label = "Statistics" version = "1.0" - description = ugettext_lazy("Graphical statistics about emails traffic using RRDtool") + description = ugettext_lazy( + "Graphical statistics about emails traffic using RRDtool" + ) needs_media = True def load(self): @@ -26,27 +28,12 @@ class Stats(ModoExtension): parameters.register( ParametersForm, ugettext_lazy("Graphical statistics") ) + from modoboa.extensions.stats import general_callbacks + if 'modoboa.extensions.stats.general_callbacks' in sys.modules: + reload(general_callbacks) def destroy(self): - events.unregister("AdminMenuDisplay", menu) + events.unregister_extension() + parameters.unregister() exts_pool.register_extension(Stats) - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "top_menu" or user.group == "SimpleUsers": - return [] - return [ - {"name": "stats", - "label": _("Statistics"), - "url": reverse('fullindex')} - ] - - -@events.observe("GetGraphSets") -def get_default_graph_sets(): - from modoboa.extensions.stats.graph_templates import MailTraffic - - gset = MailTraffic() - return {gset.html_id: gset} diff --git a/modoboa/extensions/stats/general_callbacks.py b/modoboa/extensions/stats/general_callbacks.py new file mode 100644 index 000000000..ced60145c --- /dev/null +++ b/modoboa/extensions/stats/general_callbacks.py @@ -0,0 +1,22 @@ +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +from modoboa.lib import events + + +@events.observe("UserMenuDisplay") +def menu(target, user): + if target != "top_menu" or user.group == "SimpleUsers": + return [] + return [ + {"name": "stats", + "label": _("Statistics"), + "url": reverse('fullindex')} + ] + + +@events.observe("GetGraphSets") +def get_default_graph_sets(): + from modoboa.extensions.stats.graph_templates import MailTraffic + + gset = MailTraffic() + return {gset.html_id: gset} diff --git a/modoboa/extensions/webmail/__init__.py b/modoboa/extensions/webmail/__init__.py index 8b336b8b1..597f85375 100644 --- a/modoboa/extensions/webmail/__init__.py +++ b/modoboa/extensions/webmail/__init__.py @@ -1,4 +1,5 @@ # coding: utf-8 +import sys from django.utils.translation import ugettext as _, ugettext_lazy from django.core.urlresolvers import reverse from modoboa.lib import events, parameters @@ -17,43 +18,12 @@ class Webmail(ModoExtension): parameters.register(ParametersForm, "Webmail") parameters.register(UserSettings, "Webmail") + from modoboa.extensions.webmail import general_callbacks + if 'modoboa.extensions.webmail.general_callbacks' in sys.modules: + reload(general_callbacks) def destroy(self): - events.unregister("UserMenuDisplay", menu) + events.unregister_extension() parameters.unregister() exts_pool.register_extension(Webmail) - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "top_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "webmail", - "label": _("Webmail"), - "url": reverse("modoboa.extensions.webmail.views.index")}, - ] - - -@events.observe("UserLogout") -def userlogout(request): - from .lib import IMAPconnector - from .exceptions import ImapError - - if not request.user.mailbox_set.count(): - return - try: - m = IMAPconnector(user=request.user.username, - password=request.session["password"]) - except Exception: - # TODO silent exception are bad : we should at least log it - return - - # The following statement may fail under Python 2.6... - try: - m.logout() - except ImapError: - pass diff --git a/modoboa/extensions/webmail/general_callbacks.py b/modoboa/extensions/webmail/general_callbacks.py new file mode 100644 index 000000000..affbcf2bf --- /dev/null +++ b/modoboa/extensions/webmail/general_callbacks.py @@ -0,0 +1,37 @@ +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from modoboa.lib import events + + +@events.observe("UserMenuDisplay") +def menu(target, user): + if target != "top_menu": + return [] + if not user.mailbox_set.count(): + return [] + return [ + {"name": "webmail", + "label": _("Webmail"), + "url": reverse("modoboa.extensions.webmail.views.index")}, + ] + + +@events.observe("UserLogout") +def userlogout(request): + from .lib import IMAPconnector + from .exceptions import ImapError + + if not request.user.mailbox_set.count(): + return + try: + m = IMAPconnector(user=request.user.username, + password=request.session["password"]) + except Exception: + # TODO silent exception are bad : we should at least log it + return + + # The following statement may fail under Python 2.6... + try: + m.logout() + except ImapError: + pass From c526a7c90bd6cfab043e1fd5e71b738a511dc28e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 2 Nov 2013 15:36:18 +0100 Subject: [PATCH 13/32] wip: relay domains. * Documentation update * Few fixes see #491 --- doc/extending/events.rst | 455 ++++++++++++++++-- modoboa/extensions/admin/__init__.py | 1 - modoboa/extensions/admin/models/domain.py | 4 + .../admin/templatetags/admin_tags.py | 12 +- .../postfix_relay_domains/__init__.py | 1 + .../general_callbacks.py | 7 - .../postfix_relay_domains/models.py | 4 + modoboa/lib/events.py | 1 - 8 files changed, 414 insertions(+), 71 deletions(-) diff --git a/doc/extending/events.rst b/doc/extending/events.rst index 083ed7750..69987bd59 100644 --- a/doc/extending/events.rst +++ b/doc/extending/events.rst @@ -11,10 +11,10 @@ Introduction Modoboa provides a simple API to interact with events. It understands two kinds of events: -* Those that return a value -* Those that return nothing +* Those returning a value +* Those returning nothing -Listening to a specific event is achieved as follow:: +Listening to a specific event is achieved as follows:: from modoboa.lib import events @@ -41,12 +41,32 @@ To stop listening to as specific event, you must use the 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 ============== @@ -69,6 +89,32 @@ Raised when an existing account is deleted. * ``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 =============== @@ -127,6 +173,21 @@ Raised just before a user tries to create a new object. Return ``True`` or ``False`` to indicate if this user can respectively create or not create a new ``Domain`` object. +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. + CheckExtraAccountForm ===================== @@ -144,52 +205,6 @@ this account is concerned by a specific form. Callbacks listening to this event must return a list containing one Boolean. -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 - -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) - -DomainDeleted -============= - -Raised when an existing domain is about to be deleted. - -*Callback prototype*:: - - def callback(domain): pass - -* ``domain`` is a ``Domain`` instance - -MailboxDeleted -============== - -Raised when an existing mailbox is about to be deleted. - -*Callback prototype*:: - - def callback(mailbox): pass - -* ``mailbox`` is a ``Mailbox`` instance - DomainAliasCreated ================== @@ -213,6 +228,29 @@ Raised when an existing domain alias is about to be deleted. * ``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 ============== @@ -223,7 +261,20 @@ Raised when a domain has been modified. def callback(domain): pass * ``domain`` is the modified ``Domain`` instance, it contains an extra - ``oldname`` field which contains the old domain name + ``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 ExtDisabled =========== @@ -289,6 +340,37 @@ Let extensions add extra content into the admin panel. 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). + ExtraDomainForm =============== @@ -304,6 +386,59 @@ tabs). * ``domain`` is the domain beeing modified (``Domain`` instance) +Callbacks listening to the event must return a list of dictionnaries, +each one must contain at least three keys:: + + {"id" : "", + "title" : "", + "cls" : TheFormClassName} + +ExtraDomainImportHelp +===================== + +Raised to request extra help text to display inside the domain import +form. + +*Callback prototype*:: + + def callback(): pass + +Must return a list a string. + +ExtraDomainMenuEntries +====================== + +Raised to request extra entries to include in the left menu of the +*domains* listing page. + +*Callback prototype*:: + + def callback(user): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user + +Must return a list of dictionaries. Each dictionary must contain at +least three keys:: + + {"name": "", + "label": "", + "url": ""} + +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:: @@ -344,6 +479,24 @@ forms. * ``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 @@ -364,6 +517,73 @@ Some places in the interface let plugins add their own announcement Callbacks listening to this event must return a list of string. +GetDomainActions +================ + +Raised to request the list of actions available for the *domains* +listing entry being displayed. + +*Callback prototype*:: + + def callback(user, rdomain): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user +* ``rdomain`` is the ``RelayDomain`` instance being displayed + +Must return a list of dictionaries, each dictionary containing at +least the following entries:: + + {"name": "", + "url": "", + "title": "", + "img": ""} + +GetDomainModifyLink +=================== + +Raised to request the modification url of the *domains* listing entry +being displayed. + +*Callback prototype*:: + + def callback(domain): pass + +* ``domain`` is a model instance (``RelayDomain`` for example) + +Must return a dictionary containing at least the following entry:: + + {'url': ''} + +GetExtraLimitTemplates +====================== + +Raised to request extra limit templates. For example, the +*postfix_relay_domains* extension define a template to limit the +number of relay domains an administrator can create. + +*Callback prototype*:: + + def callback(): pass + +Must return a list of set. Each set must contain at least three entries:: + + [('', '', '')] + +GetExtraParameters +================== + +Raised to request extra parameters for a given parameters form. + +*Callback prototype*:: + + def callback(application, level): pass + +* ``application`` is the name of the form's application (ie. admin, amavis, etc.) +* ``level`` is the form's level: ``A`` for admin or ``U`` for user + +Must return a dictionary. Each entry must be a valid Django form field. + .. _getextraroles: GetExtraRoles @@ -397,6 +617,27 @@ template but need javascript stuff. 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 + MailboxAliasCreated =================== @@ -420,8 +661,31 @@ Raised when an existing mailbox alias is about to be deleted. * ``mailbox_alias`` is an ``Alias`` instance -ModifyMailbox -============= +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. @@ -506,3 +770,90 @@ 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) + +UserCanSetRole +============== + +Raised to check if a user is allowed to set a given role to an +account. + +*Callback prototype*:: + + def callback(account, role): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user +* ``role`` is the role ``user`` tries to set + +Must return a list containing ``True`` or ``False`` to indicate if +this user can is allowed to set ``role``. diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 0589b630b..8a7629a80 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -19,7 +19,6 @@ admin_events = [ "ExtraDomainFilters", "GetDomainActions", "GetDomainModifyLink", - "GetDomainAliasQuerySet", "CheckDomainName", "DomainAliasCreated", diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py index 70166b9b4..95940dda6 100644 --- a/modoboa/extensions/admin/models/domain.py +++ b/modoboa/extensions/admin/models/domain.py @@ -70,6 +70,10 @@ class Domain(AdminObject): """ return [oa.user for oa in self.owners.filter(user__is_superuser=False)] + @property + def aliases(self): + return self.domainalias_set + def add_admin(self, account): """Add a new administrator for this domain diff --git a/modoboa/extensions/admin/templatetags/admin_tags.py b/modoboa/extensions/admin/templatetags/admin_tags.py index 7b9a7c5f7..3f934ddce 100644 --- a/modoboa/extensions/admin/templatetags/admin_tags.py +++ b/modoboa/extensions/admin/templatetags/admin_tags.py @@ -194,18 +194,10 @@ def domain_aliases(domain): :param domain: :rtype: str """ - if domain.__class__.__name__ == "Domain": - qset = domain.domainalias_set - else: - qset = events.raiseQueryEvent('GetDomainAliasQuerySet', domain) - if not qset: - return '---' - qset = qset[0] - - if not qset.count(): + if not domain.aliases.count(): return '---' res = '' - for alias in qset.all(): + for alias in domain.aliases.all(): res += '%s
' % alias.name return res diff --git a/modoboa/extensions/postfix_relay_domains/__init__.py b/modoboa/extensions/postfix_relay_domains/__init__.py index dfe3fff0d..d3451781e 100644 --- a/modoboa/extensions/postfix_relay_domains/__init__.py +++ b/modoboa/extensions/postfix_relay_domains/__init__.py @@ -10,6 +10,7 @@ from .models import RelayDomain, RelayDomainAlias, Service extension_events = [ "RelayDomainCreated", "RelayDomainDeleted", + "RelayDomainModified", "RelayDomainAliasCreated", "RelayDomainAliasDeleted", "ExtraRelayDomainForm", diff --git a/modoboa/extensions/postfix_relay_domains/general_callbacks.py b/modoboa/extensions/postfix_relay_domains/general_callbacks.py index d7fa11bc6..6c9861d36 100644 --- a/modoboa/extensions/postfix_relay_domains/general_callbacks.py +++ b/modoboa/extensions/postfix_relay_domains/general_callbacks.py @@ -71,13 +71,6 @@ def rdomain_modify_link(domain): } -@events.observe('GetDomainAliasQuerySet') -def get_da_query_set(domain): - if domain.__class__.__name__ != 'RelayDomain': - return [] - return [domain.relaydomainalias_set] - - @events.observe('GetDomainActions') def rdomain_actions(user, domain): if not isinstance(domain, RelayDomain): diff --git a/modoboa/extensions/postfix_relay_domains/models.py b/modoboa/extensions/postfix_relay_domains/models.py index 1c45cbffb..360810cb3 100644 --- a/modoboa/extensions/postfix_relay_domains/models.py +++ b/modoboa/extensions/postfix_relay_domains/models.py @@ -105,6 +105,10 @@ class RelayDomain(AdminObject): "type": "srv", "color": "info"} ] + @property + def aliases(self): + return self.relaydomainalias_set + def __str__(self): return self.name diff --git a/modoboa/lib/events.py b/modoboa/lib/events.py index cf565e370..e5baac544 100644 --- a/modoboa/lib/events.py +++ b/modoboa/lib/events.py @@ -125,7 +125,6 @@ def unregister_extension(extension=None): """ if extension is None: extension = guess_extension_name() - print extension for evt, values in callbacks.items(): for name in values.keys(): if extension in name: From e8f59ecf7b51c81177ef874c5bb09ae70fae6d19 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 2 Nov 2013 15:50:12 +0100 Subject: [PATCH 14/32] wip: relay domains. Documentation update. see #491 --- doc/getting_started/plugins.rst | 14 ++++++++++++++ doc/getting_started/upgrade.rst | 3 +++ 2 files changed, 17 insertions(+) diff --git a/doc/getting_started/plugins.rst b/doc/getting_started/plugins.rst index cb47c879f..1bc6c2def 100644 --- a/doc/getting_started/plugins.rst +++ b/doc/getting_started/plugins.rst @@ -32,6 +32,20 @@ associated value to -1. Default limits applied to new administrators can be changed through the *Modoboa > Parameters > Limits* page. +***************************** +Postfix relay domains support +***************************** + +This plugin adds the support for relay domains using postfix. This +feature is useful when the MTA managed by Modoboa is not the final +destination of one or many domains. + +If activated, two new objects will be available from the *domains* +listing page: *relay domain* and *relay domain alias*. + +This extension is compatible with the *amavis* and *limits* +ones. Resellers will be able to create both new objects. + .. _amavis_frontend: ******************** diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index dfde23e49..d5c6e1583 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -53,6 +53,9 @@ Finally, follow the common procedure:: 1.1.0: FIXME ============ +#. Add ``'modoboa.extensions.postfix_relay_domains'`` to + ``MODOBOA_APPS`` + Due to code refactoring, some modifications need to be done into :file:`settings.py`: From c9103bd0e03e936e74b22293bfc129e76e068e90 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 4 Nov 2013 09:29:59 +0100 Subject: [PATCH 15/32] I forgot those modifications from 1.0.1 :p --- modoboa/core/__init__.py | 2 +- modoboa/core/app_settings.py | 13 ++++++++++--- modoboa/core/views/auth.py | 2 +- modoboa/lib/fixtures/demo.json | 2 +- modoboa/lib/webutils.py | 16 +++++----------- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index 824c49add..544b3a1b3 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -52,4 +52,4 @@ def unset_default_topredirection(extension): """ topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION") if topredirection == extension.name: - parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs") + parameters.save_admin("DEFAULT_TOP_REDIRECTION", "core") diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 9acfafae5..543672102 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -19,7 +19,7 @@ def enabled_applications(): from modoboa.core.models import Extension from modoboa.lib.dbutils import db_table_exists - result = [("core", "core")] + result = [("user", "user")] if db_table_exists("core_extension"): exts = Extension.objects.filter(enabled=True) result += [(ext.name, ext.name) for ext in exts] @@ -98,7 +98,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): label=ugettext_lazy("Bind password"), initial='', help_text=ugettext_lazy("The password to use when binding to the LDAP server (with 'Bind DN')"), - widget=forms.PasswordInput, + widget=forms.PasswordInput(render_value=True), required=False ) @@ -152,7 +152,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): default_top_redirection = forms.ChoiceField( label=ugettext_lazy("Default top redirection"), choices=[], - initial="core", + initial="user", help_text=ugettext_lazy("The default redirection used when no application is specified") ) @@ -176,6 +176,13 @@ class GeneralParametersForm(parameters.AdminParametersForm): super(GeneralParametersForm, self).__init__(*args, **kwargs) self.fields["default_top_redirection"].choices = enabled_applications() + def clean_secret_key(self): + if len(self.cleaned_data["secret_key"]) not in [16, 24, 32]: + raise forms.ValidationError( + _("Key must be either 16, 24, or 32 bytes long") + ) + return self.cleaned_data["secret_key"] + def clean_ldap_user_dn_template(self): tpl = self.cleaned_data["ldap_user_dn_template"] try: diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index f4d132d9e..67eb3f973 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -35,7 +35,7 @@ def dologin(request): nextlocation = request.POST.get("next", None) if nextlocation is None or nextlocation == "None": - if user.group != "SimpleUsers": + if user.group == "SimpleUsers": nextlocation = reverse("modoboa.lib.webutils.topredirection") else: nextlocation = reverse("domains") diff --git a/modoboa/lib/fixtures/demo.json b/modoboa/lib/fixtures/demo.json index d155e29d7..046dd26f0 100644 --- a/modoboa/lib/fixtures/demo.json +++ b/modoboa/lib/fixtures/demo.json @@ -20,7 +20,7 @@ "model": "lib.parameter", "fields": { "name": "general.DEFAULT_TOP_REDIRECTION", - "value": "userprefs" + "value": "user" } }, { diff --git a/modoboa/lib/webutils.py b/modoboa/lib/webutils.py index aa47ea23e..f85c90ecb 100644 --- a/modoboa/lib/webutils.py +++ b/modoboa/lib/webutils.py @@ -138,27 +138,21 @@ def size2integer(value): @login_required def topredirection(request): - """Simple view to redirect the request when no application is specified + """Simple view to redirect the request when no application is specified. The default "top redirection" can be specified in the *Admin > Settings* panel. It is the application that will be launched by default. Users that are not allowed to access this application will be redirected to the "User preferences" application. + This feature only applies to simple users. + :param request: a Request object """ from modoboa.lib import parameters from modoboa.core.extensions import exts_pool topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="core") - if not topredir in ["core"]: - infos = exts_pool.get_extension_infos(topredir) - path = infos["url"] if infos["url"] else infos["name"] - else: - path = "admin" # topredir - - if topredir in ["core", "stats"] and \ - request.user.belongs_to_group('SimpleUsers'): - path = "userprefs" - + infos = exts_pool.get_extension_infos(topredir) + path = infos["url"] if infos["url"] else infos["name"] return HttpResponseRedirect(path) From 91ce9c5f5cd96b8919817b6d65196005aedb41a3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 4 Nov 2013 10:27:28 +0100 Subject: [PATCH 16/32] Don't propose all extensions for top redirection. --- modoboa/core/app_settings.py | 6 +++++- modoboa/core/extensions.py | 1 + modoboa/extensions/amavis/__init__.py | 1 + modoboa/extensions/sievefilters/__init__.py | 1 + modoboa/extensions/webmail/__init__.py | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 543672102..edb9bfe48 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -17,12 +17,16 @@ def enabled_applications(): :return: a list """ from modoboa.core.models import Extension + from modoboa.core.extensions import exts_pool from modoboa.lib.dbutils import db_table_exists result = [("user", "user")] if db_table_exists("core_extension"): exts = Extension.objects.filter(enabled=True) - result += [(ext.name, ext.name) for ext in exts] + for ext in exts: + extclass = exts_pool.get_extension(ext.name) + if extclass.available_for_topredirection: + result.append((ext.name, ext.name)) return sorted(result, key=lambda e: e[0]) diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index 4b399bd24..1eedc56a8 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -15,6 +15,7 @@ class ModoExtension(object): url = None needs_media = False always_active = False + available_for_topredirection = False def infos(self): return dict( diff --git a/modoboa/extensions/amavis/__init__.py b/modoboa/extensions/amavis/__init__.py index 9abc912ed..8a85d646c 100644 --- a/modoboa/extensions/amavis/__init__.py +++ b/modoboa/extensions/amavis/__init__.py @@ -21,6 +21,7 @@ class Amavis(ModoExtension): version = "1.0" description = ugettext_lazy("Simple amavis management frontend") url = "quarantine" + available_for_topredirection = True def init(self): """Init function diff --git a/modoboa/extensions/sievefilters/__init__.py b/modoboa/extensions/sievefilters/__init__.py index c5b88c95f..a6e437c6c 100644 --- a/modoboa/extensions/sievefilters/__init__.py +++ b/modoboa/extensions/sievefilters/__init__.py @@ -11,6 +11,7 @@ class SieveFilters(ModoExtension): version = "1.0" description = ugettext_lazy("Plugin to easily create server-side filters") url = "sfilters" + available_for_topredirection = True def load(self): from .app_settings import ParametersForm, UserSettings diff --git a/modoboa/extensions/webmail/__init__.py b/modoboa/extensions/webmail/__init__.py index 8b336b8b1..d673156f0 100644 --- a/modoboa/extensions/webmail/__init__.py +++ b/modoboa/extensions/webmail/__init__.py @@ -11,6 +11,7 @@ class Webmail(ModoExtension): version = "1.0" description = ugettext_lazy("Simple IMAP webmail") needs_media = True + available_for_topredirection = True def load(self): from .app_settings import ParametersForm, UserSettings From 77e5d754fbf7beb49357a23aa383ce30136234cd Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 4 Nov 2013 10:35:43 +0100 Subject: [PATCH 17/32] Display help messages on 'hover' event. fixes #492 --- modoboa/static/js/global.js | 5 +++-- modoboa/static/js/twocols_nav.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modoboa/static/js/global.js b/modoboa/static/js/global.js index d58a2a1d4..0e7d05a52 100644 --- a/modoboa/static/js/global.js +++ b/modoboa/static/js/global.js @@ -45,7 +45,8 @@ function modalbox(e, css, defhref, defcb, defclosecb) { $div.modal({show: false}); $div.one('shown', function() { $(".help").popover({ - container: "#modalbox" + container: "#modalbox", + trigger: "hover" }).click(function(e) {e.preventDefault();}); if (modalcb != undefined) { if (typeof modalcb === "function") modalcb(); else eval(modalcb + '()'); @@ -253,5 +254,5 @@ $(document).ready(function() { $(document).on('click', 'a[data-toggle="ajaxmodal"]', modalbox); $(document).on('click', 'a[data-toggle="ajaxmodal-autowidth"]', modalbox_autowidth); $(document).on('click', '.activator', activate_widget); - $(".help").popover().click(function(e) {e.preventDefault();}); + $(".help").popover({trigger: 'hover'}).click(function(e) {e.preventDefault();}); }); \ No newline at end of file diff --git a/modoboa/static/js/twocols_nav.js b/modoboa/static/js/twocols_nav.js index d8d3ed337..ba1d994c4 100644 --- a/modoboa/static/js/twocols_nav.js +++ b/modoboa/static/js/twocols_nav.js @@ -75,7 +75,7 @@ TwocolsNav.prototype = { $('#' + this.options.divid + ' input[type=radio]').click($.proxy(this.radio_clicked, this)); $(".help").popover({ placement: 'bottom', - trigger: 'click' + trigger: 'hover' }).click(function(e) {e.preventDefault();}); $('#' + this.options.divid + ' select').change(); $('#' + this.options.divid + ' input[type=radio]:checked').click(); From 14a026e2bf0b4b14189acb767949883eaf953482 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 10:55:22 +0100 Subject: [PATCH 18/32] Properly display sieve filter errors. --- .../templates/sievefilters/index.html | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/modoboa/extensions/sievefilters/templates/sievefilters/index.html b/modoboa/extensions/sievefilters/templates/sievefilters/index.html index 817d6fd51..a8442ffab 100644 --- a/modoboa/extensions/sievefilters/templates/sievefilters/index.html +++ b/modoboa/extensions/sievefilters/templates/sievefilters/index.html @@ -14,6 +14,27 @@ var history = new History({ var cond_templates = null; var action_templates = null; +function display_form_errors(data) { + $.each(data.form_errors, function(id, value) { + var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id; + var $widget = $("#" + formid + " #" + fullid); + var ulid = fullid + "-error"; + var $ul = $("#" + ulid); + + if (!$ul.length) { + $ul = $("", { + "class": "errors", + "id": ulid + }).append($("
  • ", { + html: value[0] + }); + $widget.after($ul); + } else { + $ul.find('li').html(value[0]); + } + }); +} + function filterform_cb() { cond_templates = fetch_templates("{% url 'modoboa.extensions.sievefilters.views.get_templates' 'condition' %}", cond_templates); action_templates = fetch_templates("{% url 'modoboa.extensions.sievefilters.views.get_templates' 'action' %}", action_templates); @@ -40,7 +61,8 @@ function filterform_cb() { simple_ajax_form_post(e, { formid: 'filterform', reload_mode: 'history', - extradata: counters + extradata: counters, + error_cb: display_filter_errors }); }); } From d9650a3dca7373aa9a719f3e8357588a02a905f3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 10:58:29 +0100 Subject: [PATCH 19/32] It seems I pushed the wrong version :p --- .../templates/sievefilters/index.html | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/modoboa/extensions/sievefilters/templates/sievefilters/index.html b/modoboa/extensions/sievefilters/templates/sievefilters/index.html index a8442ffab..63ac35146 100644 --- a/modoboa/extensions/sievefilters/templates/sievefilters/index.html +++ b/modoboa/extensions/sievefilters/templates/sievefilters/index.html @@ -14,24 +14,22 @@ var history = new History({ var cond_templates = null; var action_templates = null; -function display_form_errors(data) { +function display_filter_errors(data) { + $("ul.errors").remove(); $.each(data.form_errors, function(id, value) { - var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id; - var $widget = $("#" + formid + " #" + fullid); - var ulid = fullid + "-error"; - var $ul = $("#" + ulid); - - if (!$ul.length) { - $ul = $("", { - "class": "errors", - "id": ulid - }).append($("
  • ", { - html: value[0] - }); - $widget.after($ul); - } else { - $ul.find('li').html(value[0]); + if (id.indexOf('cond') && id.indexOf('action')) { + return; } + var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id; + var $widget = $("#" + fullid); + var ulid = fullid + "-error"; + var $ul = $("
      ", { + "class": "errors", + "id": ulid + }).append($("
    • ", { + html: value[0] + })); + $widget.after($ul); }); } From e62a4c426dedf562b763864772e0398d5044a4e3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 11:01:05 +0100 Subject: [PATCH 20/32] Replace old occurences of 'admin_user' by 'core_user'. --- doc/integration/imap_and_smtp.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index a8df706ba..2c90a9fe5 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -136,11 +136,11 @@ MySQL users default_pass_scheme = CRYPT - password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active=1 + password_query = SELECT email AS user, password FROM core_user WHERE email='%u' and is_active=1 user_query = SELECT '/%d/%n' AS home, as uid, as gid, concat('*:bytes=', mb.quota, 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d' - iterate_query = SELECT email AS username FROM admin_user + iterate_query = SELECT email AS username FROM core_user .. _dovecot_pg_queries: @@ -155,11 +155,11 @@ PostgreSQL users default_pass_scheme = CRYPT - password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active + password_query = SELECT email AS user, password FROM core_user WHERE email='%u' and is_active user_query = SELECT '/%d/%n' AS home, as uid, as gid, '*:bytes=' || mb.quota || 'M' AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d' - iterate_query = SELECT email AS username FROM admin_user + iterate_query = SELECT email AS username FROM core_user SQLite users ------------ @@ -172,11 +172,11 @@ SQLite users default_pass_scheme = CRYPT - password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active=1 + password_query = SELECT email AS user, password FROM core_user WHERE email='%u' and is_active=1 user_query = SELECT '/%d/%n' AS home, as uid, as gid, ('*:bytes=' || mb.quota || 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d' - iterate_query = SELECT email AS username FROM admin_user + iterate_query = SELECT email AS username FROM core_user .. note:: @@ -323,7 +323,7 @@ work for Modoboa. You should use the following one instead: DECLARE mboxid INTEGER; BEGIN - SELECT admin_mailbox.id INTO STRICT mboxid FROM admin_mailbox INNER JOIN admin_user ON admin_mailbox.user_id=admin_user.id WHERE admin_user.username=NEW.username; + SELECT admin_mailbox.id INTO STRICT mboxid FROM admin_mailbox INNER JOIN core_user ON admin_mailbox.user_id=core_user.id WHERE core_user.username=NEW.username; UPDATE admin_quota SET mbox_id = mboxid WHERE username = NEW.username; RETURN NULL; From e5fba5541d4b509a9969cf2b68d8ce7ea8ebfabe Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 11:09:25 +0100 Subject: [PATCH 21/32] Execute all unit tests. --- tests/install_from_scratch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/install_from_scratch.py b/tests/install_from_scratch.py index 50e03947c..63dec55cd 100644 --- a/tests/install_from_scratch.py +++ b/tests/install_from_scratch.py @@ -23,7 +23,7 @@ class DeployTest(unittest.TestCase): def tearDown(self): path = os.path.join(self.workdir, self.projname) - code, output = exec_cmd("python manage.py test admin", cwd=path) + code, output = exec_cmd("python manage.py test lib admin limits", cwd=path) self.assertEqual(code, 0) if hasattr(self, "workdir"): From 1fc330c28a65ec171008719f04f87f8c4684ed11 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 13:50:45 +0100 Subject: [PATCH 22/32] Adding Travis support. --- .travis.yml | 15 +++++++++++++++ tests/install_from_scratch.py | 16 +++------------- 2 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..df07ef804 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: python +python: + - "2.6" + - "2.7" + +services: mysql + +install: + - python setup.py -q install + +before_script: + - mysql -e 'create database modoboa_test;' + +script: + - PYTHONPATH=tests python -m unittest install_from_scratch diff --git a/tests/install_from_scratch.py b/tests/install_from_scratch.py index 63dec55cd..c166fdb09 100644 --- a/tests/install_from_scratch.py +++ b/tests/install_from_scratch.py @@ -12,27 +12,17 @@ class DeployTest(unittest.TestCase): dbtype = "mysql" dbhost = "localhost" projname = "modoboa_test" - dbuser = "root" - dbpassword = "toto" + dbuser = "travis" + dbpassword = "" def setUp(self): - cmd = "mysqladmin -u %s -p%s create %s" % (self.dbuser, self.dbpassword, self.projname) - code, out = exec_cmd(cmd) - self.assertEqual(code, 0) self.workdir = tempfile.mkdtemp() def tearDown(self): path = os.path.join(self.workdir, self.projname) - code, output = exec_cmd("python manage.py test lib admin limits", cwd=path) + code, output = exec_cmd("python manage.py test lib admin limits postfix_relay_domains", cwd=path) self.assertEqual(code, 0) - if hasattr(self, "workdir"): - shutil.rmtree(self.workdir) - child = pexpect.spawn("mysqladmin -u root -p%s drop %s" % (self.dbpassword, self.projname)) - child.expect("\[y/N\]") - child.sendline("y") - child.expect('Database "%s" dropped' % self.projname) - def test_standard(self): timeout = 2 cmd = "modoboa-admin.py deploy --syncdb --collectstatic %s" % self.projname From bf3fe869f6d915ff90ba7d3ebc0d78776c75351d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 16:29:15 +0100 Subject: [PATCH 23/32] Fixing unit tests. --- .travis.yml | 1 + .../core/commands/templates/settings.py.tpl | 1 + modoboa/core/views/admin.py | 2 +- modoboa/extensions/limits/models.py | 7 +++++-- modoboa/extensions/limits/tests.py | 7 ++----- .../extensions/postfix_relay_domains/tests.py | 19 +++++++++++++++---- modoboa/lib/tests.py | 11 ++++++++--- 7 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index df07ef804..2aba856e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ services: mysql install: - python setup.py -q install + - pip install -q pexpect before_script: - mysql -e 'create database modoboa_test;' diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index f86ad1e39..710c6c96a 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -155,6 +155,7 @@ MODOBOA_APPS = ( # Modoboa extensions here. 'modoboa.extensions.admin', 'modoboa.extensions.limits', + 'modoboa.extensions.postfix_relay_domains', 'modoboa.extensions.postfix_autoreply', 'modoboa.extensions.webmail', 'modoboa.extensions.stats', diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index 071e1091a..26f97e0a9 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -78,7 +78,7 @@ def saveextensions(request): for k in request.POST.keys(): if k.startswith("select_"): parts = k.split("_", 1) - dbext = Extension.objects.get(name=parts[1]) + dbext = Extension.objects.get(name=parts[1]) if not dbext in actived_exts: dbext.on() else: diff --git a/modoboa/extensions/limits/models.py b/modoboa/extensions/limits/models.py index c249aa27b..21f4e7b32 100644 --- a/modoboa/extensions/limits/models.py +++ b/modoboa/extensions/limits/models.py @@ -10,7 +10,7 @@ from modoboa.lib import events class LimitTemplates(Singleton): def __init__(self): - self.templates = [ + self.__templates = [ ("domain_admins_limit", ugettext_lazy("Domain admins"), ugettext_lazy("Maximum number of domain administrators this user can create"), 'Resellers'), @@ -25,7 +25,10 @@ class LimitTemplates(Singleton): ("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases"), ugettext_lazy("Maximum number of mailbox aliases this user can create")) ] - self.templates += events.raiseQueryEvent('GetExtraLimitTemplates') + + @property + def templates(self): + return self.__templates + events.raiseQueryEvent('GetExtraLimitTemplates') class LimitsPool(models.Model): diff --git a/modoboa/extensions/limits/tests.py b/modoboa/extensions/limits/tests.py index ccef3e3b6..cf529f8ae 100644 --- a/modoboa/extensions/limits/tests.py +++ b/modoboa/extensions/limits/tests.py @@ -12,10 +12,10 @@ from modoboa.extensions.limits.models import LimitTemplates class PermissionsTestCase(ExtTestCase): fixtures = ["initial_users.json"] - names = ["limits"] def setUp(self): super(PermissionsTestCase, self).setUp() + self.activate_extensions('limits') populate_database() def test_domainadmin_deletes_reseller(self): @@ -41,7 +41,6 @@ class PermissionsTestCase(ExtTestCase): class ResourceTestCase(ExtTestCase): fixtures = ["initial_users.json"] - names = ["limits"] def setUp(self): """Custom setUp method. @@ -49,10 +48,8 @@ class ResourceTestCase(ExtTestCase): The 'limits' is manually loaded to ensure extra parameters provided by 'postfix_relay_domains' are properly received. """ - from modoboa.core.extensions import exts_pool - super(ResourceTestCase, self).setUp() - exts_pool.get_extension('limits').load() + self.activate_extensions('limits') for tpl in LimitTemplates().templates: parameters.save_admin('DEFLT_%s' % tpl[0].upper(), 2, app='limits') populate_database() diff --git a/modoboa/extensions/postfix_relay_domains/tests.py b/modoboa/extensions/postfix_relay_domains/tests.py index 1361a757a..9598f5a1f 100644 --- a/modoboa/extensions/postfix_relay_domains/tests.py +++ b/modoboa/extensions/postfix_relay_domains/tests.py @@ -1,5 +1,6 @@ from django.core.urlresolvers import reverse from modoboa.core.factories import UserFactory +from modoboa.lib import parameters from modoboa.lib.tests import ExtTestCase from modoboa.extensions.admin import factories from modoboa.extensions.limits.tests import ResourceTestCase @@ -40,13 +41,18 @@ class Operations(object): values, status ) + def _check_limit(self, name, curvalue, maxvalue): + l = self.user.limitspool.get_limit('%s_limit' % name) + self.assertEqual(l.curvalue, curvalue) + self.assertEqual(l.maxvalue, maxvalue) + class RelayDomainsTestCase(ExtTestCase, Operations): fixtures = ['initial_users.json'] - names = ['postfix_relay_domains'] def setUp(self): super(RelayDomainsTestCase, self).setUp() + self.activate_extensions('postfix_relay_domains') factories.populate_database() self.rdom = RelayDomainFactory(name='relaydomain.tld') RelayDomainAliasFactory(name='relaydomainalias.tld', target=self.rdom) @@ -156,12 +162,17 @@ class RelayDomainsTestCase(ExtTestCase, Operations): RelayDomain.objects.get(name='relaydomainalias.tld') -class LimitsTestCase(ResourceTestCase, Operations): - names = ['limits', 'postfix_relay_domains'] +class LimitsTestCase(ExtTestCase, Operations): + fixtures = ['initial_users.json'] def setUp(self): super(LimitsTestCase, self).setUp() - self.user = UserFactory( + from modoboa.extensions.limits.models import LimitTemplates + + self.activate_extensions('limits', 'postfix_relay_domains') + for tpl in LimitTemplates().templates: + parameters.save_admin('DEFLT_%s' % tpl[0].upper(), 2, app='limits') + self.user = UserFactory.create( username='reseller', groups=('Resellers',) ) self.clt.logout() diff --git a/modoboa/lib/tests.py b/modoboa/lib/tests.py index 3ad5f2007..50fef9bc0 100644 --- a/modoboa/lib/tests.py +++ b/modoboa/lib/tests.py @@ -27,15 +27,20 @@ class ModoTestCase(TestCase): class ExtTestCase(ModoTestCase): - names = [] def setUp(self, *args, **kwargs): super(ExtTestCase, self).setUp(*args, **kwargs) self.clt.get(reverse("modoboa.core.views.admin.viewextensions")) - self.clt.post( + + def activate_extensions(self, *names): + from modoboa.core.extensions import exts_pool + + self.ajax_post( reverse("modoboa.core.views.admin.saveextensions"), - dict(("select_%s" % name, "1") for name in self.names) + dict(("select_%s" % name, "1") for name in names) ) + for name in names: + exts_pool.get_extension(name).load() class TestParams(parameters.AdminParametersForm): From 7b636cb95b13e2f3ae50029e444b408fdd537493 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 17:02:46 +0100 Subject: [PATCH 24/32] Try with only one silent test. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2aba856e1..d96e78284 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,4 +13,4 @@ before_script: - mysql -e 'create database modoboa_test;' script: - - PYTHONPATH=tests python -m unittest install_from_scratch + - PYTHONPATH=tests python -m unittest install_from_scratch.DeployTest.test_silent From 2c2e4fc95b97d4f1fb95551d0133fad698cb5f80 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 18:38:21 +0100 Subject: [PATCH 25/32] Adding debug. --- .travis.yml | 2 -- tests/install_from_scratch.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d96e78284..0e31043e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ python: - "2.6" - "2.7" -services: mysql - install: - python setup.py -q install - pip install -q pexpect diff --git a/tests/install_from_scratch.py b/tests/install_from_scratch.py index c166fdb09..bf1ea336d 100644 --- a/tests/install_from_scratch.py +++ b/tests/install_from_scratch.py @@ -21,6 +21,7 @@ class DeployTest(unittest.TestCase): def tearDown(self): path = os.path.join(self.workdir, self.projname) code, output = exec_cmd("python manage.py test lib admin limits postfix_relay_domains", cwd=path) + print output self.assertEqual(code, 0) def test_standard(self): From d295613ae68278691f2bdffd07b381b55bf1386d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 19:18:18 +0100 Subject: [PATCH 26/32] Fix Django version. --- .travis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0e31043e9..b5e95c1cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,13 @@ python: - "2.6" - "2.7" -install: +env: + - DJANGO=1.5 + +install: + - pip install -q Django==$DJANGO --use-mirrors - python setup.py -q install - - pip install -q pexpect + - pip install -q pexpect --use-mirrors before_script: - mysql -e 'create database modoboa_test;' From 623b9fec03aa6f54791dec6d916dc876b05c2fee Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 19:22:46 +0100 Subject: [PATCH 27/32] Domain aliases support for amavis. fixes #494 --- modoboa/extensions/amavis/__init__.py | 14 +- .../extensions/amavis/general_callbacks.py | 13 +- .../amavis/migrations/0002_create_records.py | 9 +- .../0003_create_domalias_records.py | 269 ++++++++++++++++++ 4 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 modoboa/extensions/amavis/migrations/0003_create_domalias_records.py diff --git a/modoboa/extensions/amavis/__init__.py b/modoboa/extensions/amavis/__init__.py index 4378ae222..552e006dd 100644 --- a/modoboa/extensions/amavis/__init__.py +++ b/modoboa/extensions/amavis/__init__.py @@ -29,15 +29,21 @@ class Amavis(ModoExtension): for existing domains to let Amavis consider them local. """ from modoboa.extensions.admin.models import Domain - from .models import Users, Policy + from modoboa.extensions.amavis.models import Users + from modoboa.extensions.amavis.lib import ( + create_user_and_policy, create_user_and_use_policy + ) for dom in Domain.objects.all(): try: Users.objects.get(email="@%s" % dom.name) except Users.DoesNotExist: - p = Policy.objects.create(policy_name=dom.name) - Users.objects.create(email="@%s" % dom.name, fullname=dom.name, - priority=7, policy=p) + create_user_and_policy(dom.name) + for domalias in dom.domainalias_set.all(): + try: + Users.objects.get(email='@%s' % domalias.name) + except Users.DoesNotExist: + create_user_and_use_policy(domalias.name, dom.name) def load(self): from .app_settings import ParametersForm, UserSettings diff --git a/modoboa/extensions/amavis/general_callbacks.py b/modoboa/extensions/amavis/general_callbacks.py index 29366aa3a..558b2355b 100644 --- a/modoboa/extensions/amavis/general_callbacks.py +++ b/modoboa/extensions/amavis/general_callbacks.py @@ -4,7 +4,8 @@ from django.template import Template, Context from modoboa.lib import events, parameters from modoboa.extensions.amavis.lib import ( - create_user_and_policy, update_user_and_policy, delete_user_and_policy + create_user_and_policy, update_user_and_policy, delete_user_and_policy, + create_user_and_use_policy, delete_user ) @@ -34,6 +35,16 @@ def on_domain_deleted(domain): delete_user_and_policy(domain.name) +@events.observe("DomainAliasCreated") +def on_relay_domain_alias_created(user, domainalias): + create_user_and_use_policy(domainalias.name, domainalias.target.name) + + +@events.observe("DomainAliasDeleted") +def on_relay_domain_alias_deleted(domainalias): + delete_user(domainalias.name) + + @events.observe("GetStaticContent") def extra_static_content(caller, user): if user.group == "SimpleUsers": diff --git a/modoboa/extensions/amavis/migrations/0002_create_records.py b/modoboa/extensions/amavis/migrations/0002_create_records.py index a9447f2e3..ba2c3ccf2 100644 --- a/modoboa/extensions/amavis/migrations/0002_create_records.py +++ b/modoboa/extensions/amavis/migrations/0002_create_records.py @@ -10,9 +10,12 @@ class Migration(DataMigration): def forwards(self, orm): "Write your forwards methods here." orm["amavis.Users"].objects.all().delete() - orm["amavis.Policy"].objects.create(id=1, policy_name='default', bypass_virus_checks=None, - bypass_spam_checks=None, bypass_banned_checks=None, - spam_subject_tag2=None) + if not orm['amavis.Policy'].objects.all(): + orm["amavis.Policy"].objects.create( + id=1, policy_name='default', bypass_virus_checks=None, + bypass_spam_checks=None, bypass_banned_checks=None, + spam_subject_tag2=None + ) for dom in orm["admin.Domain"].objects.all(): orm["amavis.Users"].objects.create(email="@%s" % dom.name, fullname=dom.name, priority=7, policy_id=1) diff --git a/modoboa/extensions/amavis/migrations/0003_create_domalias_records.py b/modoboa/extensions/amavis/migrations/0003_create_domalias_records.py new file mode 100644 index 000000000..f5fd72418 --- /dev/null +++ b/modoboa/extensions/amavis/migrations/0003_create_domalias_records.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from django.core.exceptions import ObjectDoesNotExist + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + try: + ext = orm["core.Extension"].objects.get(name="amavis") + except ObjectDoesNotExist: + return + if not ext.enabled: + return + for domalias in orm["admin.DomainAlias"].objects.all(): + u = orm["amavis.Users"]( + email="@%s" % domalias.name, fullname=domalias.name, priority=7 + ) + u.policy = orm["amavis.Policy"].objects.get(policy_name=domalias.target.name) + u.save() + + def backwards(self, orm): + "Write your backwards methods here." + raise RuntimeError("Cannot revert this migration") + + models = { + 'admin.alias': { + 'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'aliases': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Alias']", 'symmetrical': 'False'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'ordering': "['name']", 'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quota': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + 'admin.mailboxoperation': { + 'Meta': {'object_name': 'MailboxOperation'}, + 'argument': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + 'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'admin.quota': { + 'Meta': {'object_name': 'Quota'}, + 'bytes': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'mbox': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'quota_value'", 'unique': 'True', 'null': 'True', 'to': "orm['admin.Mailbox']"}), + 'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'username': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'primary_key': 'True'}) + }, + u'amavis.maddr': { + 'Meta': {'object_name': 'Maddr', 'db_table': "u'maddr'", 'managed': 'False'}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '765'}), + 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True'}), + 'partition_tag': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + u'amavis.mailaddr': { + 'Meta': {'object_name': 'Mailaddr', 'db_table': "u'mailaddr'", 'managed': 'False'}, + 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'id': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}), + 'priority': ('django.db.models.fields.IntegerField', [], {}) + }, + u'amavis.msgrcpt': { + 'Meta': {'unique_together': "(('partition_tag', 'mail', 'rseqnum'),)", 'object_name': 'Msgrcpt', 'db_table': "u'msgrcpt'", 'managed': 'False'}, + 'bl': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'bspam_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'content': ('django.db.models.fields.CharField', [], {'max_length': '3'}), + 'ds': ('django.db.models.fields.CharField', [], {'max_length': '3'}), + 'is_local': ('django.db.models.fields.CharField', [], {'max_length': '3'}), + 'mail': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Msgs']", 'primary_key': 'True'}), + 'partition_tag': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'rid': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Maddr']", 'primary_key': 'True', 'db_column': "'rid'"}), + 'rs': ('django.db.models.fields.CharField', [], {'max_length': '3'}), + 'rseqnum': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'smtp_resp': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}), + 'wl': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}) + }, + u'amavis.msgs': { + 'Meta': {'unique_together': "(('partition_tag', 'mail_id'),)", 'object_name': 'Msgs', 'db_table': "u'msgs'", 'managed': 'False'}, + 'am_id': ('django.db.models.fields.CharField', [], {'max_length': '60'}), + 'client_addr': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}), + 'content': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}), + 'dsn_sent': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'from_addr': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'max_length': '765'}), + 'mail_id': ('django.db.models.fields.CharField', [], {'max_length': '12', 'primary_key': 'True'}), + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}), + 'originating': ('django.db.models.fields.CharField', [], {'max_length': '3'}), + 'partition_tag': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'policy': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}), + 'quar_loc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'quar_type': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}), + 'secret_id': ('django.db.models.fields.CharField', [], {'max_length': '12', 'blank': 'True'}), + 'sid': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Maddr']", 'db_column': "'sid'"}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'spam_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}), + 'time_iso': ('django.db.models.fields.CharField', [], {'max_length': '48'}), + 'time_num': ('django.db.models.fields.IntegerField', [], {}) + }, + u'amavis.policy': { + 'Meta': {'object_name': 'Policy', 'db_table': "u'policy'", 'managed': 'False'}, + 'addr_extension_bad_header': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'addr_extension_banned': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'addr_extension_spam': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'addr_extension_virus': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'archive_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'bad_header_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'bad_header_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'bad_header_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'banned_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'banned_files_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'banned_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'banned_rulenames': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'bypass_banned_checks': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '3', 'null': 'True'}), + 'bypass_header_checks': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'bypass_spam_checks': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '3', 'null': 'True'}), + 'bypass_virus_checks': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '3', 'null': 'True'}), + 'clean_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'disclaimer_options': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'forward_method': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message_size_limit': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'newvirus_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'policy_name': ('django.db.models.fields.CharField', [], {'max_length': '96', 'blank': 'True'}), + 'sa_userconf': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'sa_username': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'spam_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'spam_dsn_cutoff_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'spam_kill_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'spam_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'spam_quarantine_cutoff_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'spam_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'spam_subject_tag': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'spam_subject_tag2': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'spam_subject_tag3': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'spam_tag2_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'spam_tag3_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'spam_tag_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'unchecked_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'unchecked_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'virus_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'virus_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'virus_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}), + 'warnbadhrecip': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'warnbannedrecip': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'warnvirusrecip': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}) + }, + u'amavis.quarantine': { + 'Meta': {'ordering': "['-mail__time_num']", 'unique_together': "(('partition_tag', 'mail', 'chunk_ind'),)", 'object_name': 'Quarantine', 'db_table': "u'quarantine'", 'managed': 'False'}, + 'chunk_ind': ('django.db.models.fields.IntegerField', [], {}), + 'mail': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Msgs']", 'primary_key': 'True'}), + 'mail_text': ('django.db.models.fields.TextField', [], {}), + 'partition_tag': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + u'amavis.users': { + 'Meta': {'object_name': 'Users', 'db_table': "u'users'", 'managed': 'False'}, + 'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'fullname': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'policy': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Policy']"}), + 'priority': ('django.db.models.fields.IntegerField', [], {}) + }, + u'amavis.wblist': { + 'Meta': {'object_name': 'Wblist', 'db_table': "u'wblist'", 'managed': 'False'}, + 'rid': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}), + 'sid': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}), + 'wb': ('django.db.models.fields.CharField', [], {'max_length': '30'}) + }, + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'core.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + u'core.log': { + 'Meta': {'object_name': 'Log'}, + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '15'}), + 'logger': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'core.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + u'core.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + } + } + + complete_apps = ['core', 'admin', 'amavis'] + symmetrical = True From 8826cd6cac5e3b1f6270a538622498facbd897b8 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 19:29:18 +0100 Subject: [PATCH 28/32] Travis build status added. --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 956639ad1..fef637441 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Modoboa ####### -|latest-version| |downloads| +|travis| |latest-version| |downloads| Modoboa is a mail hosting and management platform including a modern and simplified Web User Interface. It provides useful components such @@ -65,9 +65,9 @@ The following external libraries are provided with Modoboa: .. |downloads| image:: https://pypip.in/d/modoboa/badge.png :alt: Downloads from Pypi :target: https://crate.io/packages/modoboa/ - +.. |travis| image:: https://travis-ci.org/tonioo/modoboa.png?branch=master + :target: https://travis-ci.org/tonioo/modoboa .. image:: https://d2weczhvl823v0.cloudfront.net/tonioo/modoboa/trend.png :alt: Bitdeli badge :target: https://bitdeli.com/free - From 9ef83f9db93a314f1883f6c3a4a007e5ccec27a6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 20:43:03 +0100 Subject: [PATCH 29/32] Adding mysql-python as requirement. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b5e95c1cf..5c692ad59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ env: install: - pip install -q Django==$DJANGO --use-mirrors - python setup.py -q install + - pip install -q MySQL-python --use-mirrors - pip install -q pexpect --use-mirrors before_script: From 1c6d9935504524fa82d74984c9bf8b03882ff8f3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 9 Nov 2013 22:24:57 +0100 Subject: [PATCH 30/32] Modoboa is not compatible with django-reversion 1.8. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d7ba96b5f..ff9ceadfb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ django -django-reversion +django-reversion==1.7.1 dj-database-url south lxml From 4e72d70917b43971cd9d42b1a1fe13cb7a2dbbfb Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 12 Nov 2013 21:48:48 +0100 Subject: [PATCH 31/32] Compatibility with reversion >= 1.8. fixes #497 --- .../core/commands/templates/settings.py.tpl | 3 +- modoboa/core/extensions.py | 8 +++-- modoboa/core/models.py | 36 +++++++++++++++---- modoboa/core/templatetags/core_tags.py | 2 +- modoboa/extensions/admin/forms/domain.py | 7 ++-- modoboa/extensions/admin/models/domain.py | 9 +++++ .../extensions/admin/static/admin/js/admin.js | 2 +- modoboa/extensions/admin/views/identity.py | 21 +++++------ .../extensions/amavis/general_callbacks.py | 11 +++--- .../extensions/postfix_relay_domains/forms.py | 8 +++-- .../general_callbacks.py | 26 -------------- .../postfix_relay_domains/limits_callbacks.py | 27 ++++++++++++++ .../postfix_relay_domains/models.py | 9 +++++ .../extensions/postfix_relay_domains/views.py | 4 +-- modoboa/lib/parameters.py | 2 ++ requirements.txt | 2 +- 16 files changed, 114 insertions(+), 63 deletions(-) diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 710c6c96a..b7f36115f 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -106,6 +106,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.transaction.TransactionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -154,8 +155,8 @@ MODOBOA_APPS = ( 'modoboa.lib', # Modoboa extensions here. 'modoboa.extensions.admin', - 'modoboa.extensions.limits', 'modoboa.extensions.postfix_relay_domains', + 'modoboa.extensions.limits', 'modoboa.extensions.postfix_autoreply', 'modoboa.extensions.webmail', 'modoboa.extensions.stats', diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index fe11435c7..07073a9d2 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -76,11 +76,13 @@ class ExtensionsPool(object): """ from modoboa.core.models import Extension + result = [] for ext in settings.MODOBOA_APPS: __import__(ext) - result = [] - for extname in self.extensions.keys(): + extname = ext.split('.')[-1] extinstance = self.get_extension(extname) + if extinstance is None: + continue try: baseurl = extinstance.url \ if extinstance.url is not None else extname @@ -106,7 +108,7 @@ class ExtensionsPool(object): for extname, extdef in self.extensions.iteritems(): if not extdef["show"]: continue - infos = extdef["instance"].infos() + infos = self.get_extension_infos(extname) infos["id"] = extname result += [infos] return sorted(result, key=lambda i: i["name"]) diff --git a/modoboa/core/models.py b/modoboa/core/models.py index db0050044..68ac4bead 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -4,9 +4,11 @@ import re import hashlib import crypt import base64 +import logging from random import Random import reversion from django.db import models +from django.db.models.signals import post_delete from django.dispatch import receiver from django.utils import timezone from django.utils.translation import ugettext as _, ugettext_lazy @@ -463,24 +465,44 @@ class Log(models.Model): @receiver(reversion.post_revision_commit) def post_revision_commit(sender, **kwargs): - import logging - if kwargs["revision"].user is None: return logger = logging.getLogger("modoboa.admin") for version in kwargs["versions"]: - if version.type == reversion.models.VERSION_ADD: + prev_revisions = reversion.get_for_object(version.object) + if prev_revisions.count() == 1: action = _("added") level = "info" - elif version.type == reversion.models.VERSION_CHANGE: + else: action = _("modified") level = "warning" - else: - action = _("deleted") - level = "critical" message = _("%(object)s '%(name)s' %(action)s by user %(user)s") % { "object": unicode(version.content_type).capitalize(), "name": version.object_repr, "action": action, "user": kwargs["revision"].user.username } getattr(logger, level)(message) + + +@receiver(post_delete) +def post_delete_hook(sender, instance, **kwargs): + """Custom post-delete hook. + + We want to know who was responsible for an object deletion. + """ + from reversion.models import Version + + if not reversion.is_registered(sender): + return + del_list = reversion.get_deleted(sender) + try: + version = del_list.get(object_id=instance.id) + except Version.DoesNotExist: + return + logger = logging.getLogger("modoboa.admin") + msg = _("%(object)s '%(name)s' %(action)s by user %(user)s") % { + "object": unicode(version.content_type).capitalize(), + "name": version.object_repr, "action": _("deleted"), + "user": version.revision.user.username + } + logger.critical(msg) diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index b424c1f9f..7b9a08930 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -45,7 +45,7 @@ def settings_menu(selection, user): "label": _("Information")}, {"name": "logs", "class": "ajaxlink", - "url": "logs/", + "url": "logs/&sort_order=-date_created", "label": _("Logs")}, {"name": "parameters", "class": "ajaxlink", diff --git a/modoboa/extensions/admin/forms/domain.py b/modoboa/extensions/admin/forms/domain.py index c3686fdc9..c19b60d68 100644 --- a/modoboa/extensions/admin/forms/domain.py +++ b/modoboa/extensions/admin/forms/domain.py @@ -107,7 +107,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): username = q['username'].replace('@%s' % self.oldname, '@%s' % domain.name) Quota.objects.filter(username=q['username']).update(username=username) - def save(self, user, commit=True): + def save(self, user, commit=True, domalias_post_create=False): """Custom save method Updating a domain may have consequences on other objects @@ -148,7 +148,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): else: continue al = DomainAlias(name=alias, target=d, enabled=d.enabled) - al.save(creator=user) + al.save(creator=user) if domalias_post_create else al.save() if old_mail_homes is not None: self.update_mailbox_quotas(d) @@ -253,5 +253,6 @@ class DomainForm(TabForms): As forms interact with each other, it is easier to make custom code to save them. """ - for f in self.forms: + self.forms[0]['instance'].save(user, domalias_post_create=True) + for f in self.forms[1:]: f["instance"].save(user) diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py index 95940dda6..e7467d592 100644 --- a/modoboa/extensions/admin/models/domain.py +++ b/modoboa/extensions/admin/models/domain.py @@ -148,4 +148,13 @@ class Domain(AdminObject): for dalias in self.domainalias_set.all(): dalias.to_csv(csvwriter) + def post_create(self, creator): + """Post creation actions. + + :param ``User`` creator: user whos created this domain + """ + super(Domain, self).post_create(creator) + for domalias in self.domainalias_set.all(): + domalias.post_create(creator) + reversion.register(Domain) diff --git a/modoboa/extensions/admin/static/admin/js/admin.js b/modoboa/extensions/admin/static/admin/js/admin.js index d0a5ef387..0ed43c300 100644 --- a/modoboa/extensions/admin/static/admin/js/admin.js +++ b/modoboa/extensions/admin/static/admin/js/admin.js @@ -392,7 +392,7 @@ Identities.prototype = { $("#wizard").cwizard({ formid: "newaccount_form", transition_callbacks: { - 2: this.mailform_prefill + 1: this.mailform_prefill }, success_callback: $.proxy(this.reload_listing, this) }); diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py index e44ac9d87..8e8fea69a 100644 --- a/modoboa/extensions/admin/views/identity.py +++ b/modoboa/extensions/admin/views/identity.py @@ -115,6 +115,14 @@ def list_quotas(request, tplname="admin/quotas.html"): @permission_required("core.add_user") @transaction.commit_on_success def newaccount(request, tplname='common/wizard_forms.html'): + """Create a new account. + + .. note:: An issue still remains int this code: if all validation + steps are successful but an error occurs after we call 'save', + the account will be created. It happens transaction management + doesn't work very well with nested functions. Need to wait for + django 1.6 and atomicity. + """ cwizard = CreationWizard() cwizard.add_step(AccountFormGeneral, _("General"), [dict(classes="btn-inverse next", label=_("Next"))], @@ -135,16 +143,9 @@ def newaccount(request, tplname='common/wizard_forms.html'): if retcode == 2: genform = cwizard.steps[0]["form"] account = genform.save() - try: - account.post_create(request.user) - mailform = cwizard.steps[1]["form"] - mailform.save(request.user, account) - except ModoboaException: - # A bit uggly: transaction management doesn't work very - # well with nested functions. Need to wait for django 1.6 - # and atomicity. - account.delete(request.user, False) - raise + account.post_create(request.user) + mailform = cwizard.steps[1]["form"] + mailform.save(request.user, account) return render_to_json_response(_("Account created")) return render_to_json_response({ 'stepid': data, 'form_errors': cwizard.errors diff --git a/modoboa/extensions/amavis/general_callbacks.py b/modoboa/extensions/amavis/general_callbacks.py index 558b2355b..8d9ad4ec6 100644 --- a/modoboa/extensions/amavis/general_callbacks.py +++ b/modoboa/extensions/amavis/general_callbacks.py @@ -1,8 +1,8 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from django.template import Template, Context - from modoboa.lib import events, parameters +from modoboa.extensions.admin.models import DomainAlias from modoboa.extensions.amavis.lib import ( create_user_and_policy, update_user_and_policy, delete_user_and_policy, create_user_and_use_policy, delete_user @@ -36,13 +36,16 @@ def on_domain_deleted(domain): @events.observe("DomainAliasCreated") -def on_relay_domain_alias_created(user, domainalias): +def on_domain_alias_created(user, domainalias): create_user_and_use_policy(domainalias.name, domainalias.target.name) @events.observe("DomainAliasDeleted") -def on_relay_domain_alias_deleted(domainalias): - delete_user(domainalias.name) +def on_domain_alias_deleted(domainaliases): + if isinstance(domainaliases, DomainAlias): + domainaliases = [domainaliases] + for domainalias in domainaliases: + delete_user(domainalias.name) @events.observe("GetStaticContent") diff --git a/modoboa/extensions/postfix_relay_domains/forms.py b/modoboa/extensions/postfix_relay_domains/forms.py index 37a49d646..08cff2059 100644 --- a/modoboa/extensions/postfix_relay_domains/forms.py +++ b/modoboa/extensions/postfix_relay_domains/forms.py @@ -82,7 +82,7 @@ class RelayDomainFormGeneral(forms.ModelForm, DynamicForm): return cleaned_data - def save(self, user, commit=True): + def save(self, user, commit=True, rdomalias_post_create=False): """Custom save method. As relay domain aliases are defined using the same form as @@ -119,7 +119,8 @@ class RelayDomainFormGeneral(forms.ModelForm, DynamicForm): al = RelayDomainAlias( name=alias, target=rd, enabled=rd.enabled ) - al.save(creator=user) + al.save(creator=user) \ + if rdomalias_post_create else al.save() return rd @@ -151,5 +152,6 @@ class RelayDomainForm(TabForms): As forms interact with each other, it is easier to make custom code to save them. """ - for f in self.forms: + self.forms[0]['instance'].save(user, rdomalias_post_create=True) + for f in self.forms[1:]: f["instance"].save(user) diff --git a/modoboa/extensions/postfix_relay_domains/general_callbacks.py b/modoboa/extensions/postfix_relay_domains/general_callbacks.py index 6c9861d36..c51c54be4 100644 --- a/modoboa/extensions/postfix_relay_domains/general_callbacks.py +++ b/modoboa/extensions/postfix_relay_domains/general_callbacks.py @@ -106,32 +106,6 @@ def extra_limit_templates(): ] -@events.observe('GetExtraParameters') -def extra_parameters(app, level): - from django import forms - - if app != 'limits' or level != 'A': - return {} - return { - 'deflt_relay_domains_limit': forms.IntegerField( - label=ugettext_lazy("Relay domains"), - initial=0, - help_text=ugettext_lazy( - "Maximum number of allowed relay domains for a new administrator" - ), - widget=forms.widgets.TextInput(attrs={"class": "span1"}) - ), - 'deflt_relay_domain_aliases_limit': forms.IntegerField( - label=ugettext_lazy("Relay domain aliases"), - initial=0, - help_text=ugettext_lazy( - "Maximum number of allowed relay domain aliases for a new administrator" - ), - widget=forms.widgets.TextInput(attrs={"class": "span1"}) - ) - } - - @events.observe('ExtraDomainImportHelp') def extra_domain_import_help(): return [ugettext_lazy(""" diff --git a/modoboa/extensions/postfix_relay_domains/limits_callbacks.py b/modoboa/extensions/postfix_relay_domains/limits_callbacks.py index 9966945e7..5d50ea28a 100644 --- a/modoboa/extensions/postfix_relay_domains/limits_callbacks.py +++ b/modoboa/extensions/postfix_relay_domains/limits_callbacks.py @@ -1,9 +1,36 @@ +from django.utils.translation import ugettext_lazy from modoboa.lib import events from modoboa.lib.permissions import get_object_owner from modoboa.extensions.limits.lib import inc_limit_usage, dec_limit_usage from .models import RelayDomainAlias +@events.observe('GetExtraParameters') +def extra_parameters(app, level): + from django import forms + + if app != 'limits' or level != 'A': + return {} + return { + 'deflt_relay_domains_limit': forms.IntegerField( + label=ugettext_lazy("Relay domains"), + initial=0, + help_text=ugettext_lazy( + "Maximum number of allowed relay domains for a new administrator" + ), + widget=forms.widgets.TextInput(attrs={"class": "span1"}) + ), + 'deflt_relay_domain_aliases_limit': forms.IntegerField( + label=ugettext_lazy("Relay domain aliases"), + initial=0, + help_text=ugettext_lazy( + "Maximum number of allowed relay domain aliases for a new administrator" + ), + widget=forms.widgets.TextInput(attrs={"class": "span1"}) + ) + } + + @events.observe('RelayDomainCreated') def inc_relaydomains_count(user, rdomain): inc_limit_usage(user, 'relay_domains_limit') diff --git a/modoboa/extensions/postfix_relay_domains/models.py b/modoboa/extensions/postfix_relay_domains/models.py index 360810cb3..088874c2e 100644 --- a/modoboa/extensions/postfix_relay_domains/models.py +++ b/modoboa/extensions/postfix_relay_domains/models.py @@ -139,6 +139,15 @@ class RelayDomain(AdminObject): self.verify_recipients = (row[5].strip() == 'True') self.save(creator=user) + def post_create(self, creator): + """Post creation actions. + + :param ``User`` creator: user whos created this relay domain + """ + super(RelayDomain, self).post_create(creator) + for rdomalias in self.relaydomainalias_set.all(): + rdomalias.post_create(creator) + reversion.register(RelayDomain) diff --git a/modoboa/extensions/postfix_relay_domains/views.py b/modoboa/extensions/postfix_relay_domains/views.py index e59657891..e6df53621 100644 --- a/modoboa/extensions/postfix_relay_domains/views.py +++ b/modoboa/extensions/postfix_relay_domains/views.py @@ -23,9 +23,7 @@ def create(request, tplname="postfix_relay_domains/new_relaydomain_form.html"): if form.is_valid(): rdom = form.save(request.user) rdom.post_create(request.user) - return render_to_json_response( - {"respmsg": _("Relay domain created")} - ) + return render_to_json_response(_("Relay domain created")) return render_to_json_response( {'form_errors': form.errors}, status=400 ) diff --git a/modoboa/lib/parameters.py b/modoboa/lib/parameters.py index a5c7dfa89..42b8294db 100644 --- a/modoboa/lib/parameters.py +++ b/modoboa/lib/parameters.py @@ -17,6 +17,8 @@ _params = {'A': {}, 'U': {}} class NotDefined(ModoboaException): + http_code = 404 + def __init__(self, app, name): self.app = app self.name = name diff --git a/requirements.txt b/requirements.txt index ff9ceadfb..94613aa78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ django -django-reversion==1.7.1 +django-reversion>=1.8.0 dj-database-url south lxml From 8d2a26b97f220a82cb3844b1c2d7297f52c638b6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 12 Nov 2013 22:19:16 +0100 Subject: [PATCH 32/32] Cleanup. --- modoboa/core/models.py | 12 +++++------- modoboa/lib/ldaputils.py | 10 ++++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 68ac4bead..e817906fd 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -26,7 +26,7 @@ from modoboa.lib.sysutils import exec_cmd from modoboa.core.extensions import exts_pool try: - from modoboa.lib.ldaputils import * + from modoboa.lib.ldaputils import LDAPAuthBackend ldap_available = True except ImportError: ldap_available = False @@ -110,6 +110,7 @@ class User(AbstractBaseUser, PermissionsMixin): elif scheme == "md5crypt": # The salt may vary from 12 to 48 bits. (Using all six bytes here # with a subset of characters means we get only 35 random bits.) + import string salt = "".join(Random().sample(string.letters + string.digits, 6)) result = md5crypt(raw_value, salt) prefix = "" # the result already has $1$ prepended to it @@ -141,12 +142,9 @@ class User(AbstractBaseUser, PermissionsMixin): raise InternalError( _("Failed to update password: LDAP module not installed") ) - - ab = LDAPAuthBackend() - try: - ab.update_user_password(self.username, curvalue, raw_value) - except LDAPException as e: - raise InternalError(_("Failed to update password: %s" % str(e))) + LDAPAuthBackend().update_user_password( + self.username, curvalue, raw_value + ) events.raiseEvent( "PasswordUpdated", self, raw_value, self.pk is None ) diff --git a/modoboa/lib/ldaputils.py b/modoboa/lib/ldaputils.py index 28e5e178a..ea4723af1 100644 --- a/modoboa/lib/ldaputils.py +++ b/modoboa/lib/ldaputils.py @@ -40,11 +40,9 @@ import string import crypt from random import Random from django.conf import settings +from django.utils.translation import ugettext as _ from modoboa.lib import parameters - - -class LDAPException(Exception): - pass +from modoboa.lib.exceptions import InternalError class LDAPAuthBackend(object): @@ -126,5 +124,5 @@ class LDAPAuthBackend(object): self.pwd_attr, self._crypt_password(newpassword))] self.conn.modify_s(self.user_dn, ldif) - except ldap.LDAPError, e: - raise LDAPException(str(e)) + except ldap.LDAPError as e: + raise InternalError(_("Failed to update password: %s" % str(e)))