%s
" % (classes[level], level) - - -@register.filter -def tohtml(message): - return re.sub("'(.*?)'", "\g<1>", message) diff --git a/modoboa/admin/tests.py b/modoboa/admin/tests.py deleted file mode 100644 index 60d380a20..000000000 --- a/modoboa/admin/tests.py +++ /dev/null @@ -1,443 +0,0 @@ -# coding: utf-8 -""" -Basic tests for the 'admin' application -======================================= - -TODO: - -* Add new tests to check domain/mailbox manipulations when - create_directories == yes - -""" -import os -from django.test import TestCase -from django.test.client import Client -from django.utils import simplejson -from django.core.urlresolvers import reverse -from django.core.files.base import ContentFile -from models import * -from modoboa.admin.lib import * -from modoboa.lib import parameters -from modoboa.lib.tests import ModoTestCase - - -class DomainTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def test_create(self): - """Test the creation of a domain - """ - values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": "no", - "stepid": 2 - } - self.check_ajax_post(reverse("modoboa.admin.views.newdomain"), values) - dom = Domain.objects.get(name="pouet.com") - self.assertEqual(dom.name, "pouet.com") - self.assertEqual(dom.quota, 100) - self.assertEqual(dom.enabled, False) - self.assertFalse(dom.admins) - - def test_create_with_template(self): - """Test the creation of a domain with a template - - """ - values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", - "stepid": 2 - } - self.check_ajax_post(reverse("modoboa.admin.views.newdomain"), values) - dom = Domain.objects.get(name="pouet.com") - da = User.objects.get(username="toto@pouet.com") - self.assertIn(da, dom.admins) - al = Alias.objects.get(address="postmaster", domain__name="pouet.com") - self.assertIn(da.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(da.can_access(al)) - - def test_create_with_template_and_empty_quota(self): - """Test the creation of a domain with a template and no quota""" - values = { - "name": "pouet.com", "quota": 0, "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", - "stepid": 2 - } - self.check_ajax_post(reverse("modoboa.admin.views.newdomain"), values) - dom = Domain.objects.get(name="pouet.com") - da = User.objects.get(username="toto@pouet.com") - self.assertIn(da, dom.admins) - al = Alias.objects.get(address="postmaster", domain__name="pouet.com") - self.assertIn(da.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(da.can_access(al)) - - def test_modify(self): - """Test the modification of a domain - - Rename 'test.com' domain to 'pouet.com' - """ - values = { - "name": "pouet.com", "quota": 100, "enabled": True - } - self.check_ajax_post(reverse("modoboa.admin.views.editdomain", args=[1]), values) - dom = Domain.objects.get(name="pouet.com") - self.assertEqual(dom.name, "pouet.com") - self.assertTrue(dom.enabled) - - def test_delete(self): - """Test the removal of a domain - """ - self.check_ajax_get(reverse("modoboa.admin.views.deldomain", args=[1]), {}) - with self.assertRaises(Domain.DoesNotExist): - Domain.objects.get(pk=1) - - -class DomainAliasTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def test_model(self): - dom = Domain.objects.get(name="test.com") - domal = DomainAlias() - domal.name = "domalias.net" - domal.target = dom - domal.save() - self.assertEqual(dom.domainalias_count, 1) - - domal.name = "domalias.org" - domal.save() - - domal.delete() - - def test_form(self): - dom = Domain.objects.get(name="test.com") - values = dict(name=dom.name, quota=dom.quota, enabled=dom.enabled, - aliases="domalias.net", aliases_1="domalias.com") - self.check_ajax_post(reverse("modoboa.admin.views.editdomain", args=[1]), values) - self.assertEqual(dom.domainalias_set.count(), 2) - - del values["aliases_1"] - self.check_ajax_post(reverse("modoboa.admin.views.editdomain", args=[1]), values) - self.assertEqual(dom.domainalias_set.count(), 1) - - -class AccountTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json",] - - def test_crud(self): - values = dict(username="tester@test.com", first_name="Tester", last_name="Toto", - password1="toto", password2="toto", role="SimpleUsers", - quota_act=True, - is_active=True, email="tester@test.com", stepid=2) - self.check_ajax_post(reverse("modoboa.admin.views.newaccount"), values) - - account = User.objects.get(username="tester@test.com") - mb = account.mailbox_set.all()[0] - self.assertEqual(mb.full_address, "tester@test.com") - self.assertEqual(mb.quota, 100) - self.assertEqual(mb.enabled, True) - self.assertEqual(mb.quota_value.username, "tester@test.com") - self.assertEqual(account.username, mb.full_address) - self.assertTrue(account.check_password("toto")) - self.assertEqual(account.first_name, "Tester") - self.assertEqual(account.last_name, "Toto") - self.assertEqual(mb.domain.mailbox_count, 4) - - values["username"] = "pouet@test.com" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[account.id]), values) - - mb = Mailbox.objects.get(pk=mb.id) - self.assertEqual(mb.full_address, "pouet@test.com") - - self.check_ajax_get(reverse("modoboa.admin.views.delaccount", args=[account.id]), {}) - - def test_update_password(self): - """Password update - - Two cases: - * The default admin changes his password (no associated Mailbox) - * A normal user changes his password - """ - self.check_ajax_post(reverse("modoboa.userprefs.views.profile"), - {"oldpassword" : "password", - "newpassword" : "titi", "confirmation" : "titi"}) - self.clt.logout() - - self.assertEqual(self.clt.login(username="admin", password="titi"), True) - self.assertEqual(self.clt.login(username="user@test.com", password="toto"), - True) - - self.check_ajax_post(reverse("modoboa.userprefs.views.profile"), - {"oldpassword" : "toto", - "newpassword" : "tutu", "confirmation" : "tutu"}) - self.clt.logout() - self.assertEqual(self.clt.login(username="user@test.com", password="tutu"), True) - - -class AliasTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def test_alias(self): - user = User.objects.get(username="user@test.com") - values = dict( - username="user@test.com", role=user.group, - is_active=user.is_active, email="user@test.com", - aliases="toto@test.com", aliases_1="titi@test.com" - ) - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[user.id]), values) - self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 2) - - del values["aliases_1"] - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[user.id]), values) - self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 1) - - def test_dlist(self): - values = dict(email="all@test.com", - recipients="user@test.com", - recipients_1="admin@test.com", - recipients_2="ext@titi.com", - enabled=True) - self.check_ajax_post(reverse("modoboa.admin.views.newdlist"), values) - user = User.objects.get(username="user@test.com") - self.assertEqual(len(user.mailbox_set.all()[0].alias_set.all()), 1) - admin = User.objects.get(username="admin@test.com") - self.assertEqual(len(admin.mailbox_set.all()[0].alias_set.all()), 1) - - dlist = Alias.objects.get(address="all", domain__name="test.com") - self.assertEqual(len(dlist.get_recipients()), 3) - del values["recipients_1"] - self.check_ajax_post(reverse("modoboa.admin.views.editalias", args=[dlist.id]), - values) - self.assertEqual(dlist.get_recipients_count(), 2) - - self.check_ajax_get(reverse("modoboa.admin.views.deldlist") + "?selection=%d" \ - % dlist.id, {}) - self.assertRaises(Alias.DoesNotExist, Alias.objects.get, - address="all", domain__name="test.com") - - def test_forward(self): - values = dict(email="forward@test.com", recipients="rcpt@dest.com") - self.check_ajax_post(reverse("modoboa.admin.views.newforward"), values) - fwd = Alias.objects.get(address="forward", domain__name="test.com") - self.assertEqual(fwd.get_recipients_count(), 1) - - values["recipients"] = "rcpt2@dest.com" - self.check_ajax_post(reverse("modoboa.admin.views.editalias", args=[fwd.id]), - values) - self.assertEqual(fwd.get_recipients_count(), 1) - - self.check_ajax_get(reverse("modoboa.admin.views.delforward") + "?selection=%d" \ - % fwd.id, {}) - self.assertRaises(Alias.DoesNotExist, Alias.objects.get, - address="forward", domain__name="test.com") - - def test_forward_and_local_copies(self): - values = dict(email="user@test.com", recipients="rcpt@dest.com") - self.check_ajax_post(reverse("modoboa.admin.views.newforward"), values) - fwd = Alias.objects.get(address="user", domain__name="test.com") - self.assertEqual(fwd.get_recipients_count(), 1) - - values["recipients"] = "rcpt@dest.com" - values["recipients_1"] = "user@test.com" - self.check_ajax_post(reverse("modoboa.admin.views.editalias", args=[fwd.id]), - values) - fwd = Alias.objects.get(pk=fwd.pk) - self.assertEqual(fwd.get_recipients_count(), 2) - self.assertEqual(fwd.aliases.count(), 0) - - -class PermissionsTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def setUp(self): - """ - FIXME: je n'arrive pas à générer des données de test incluant - déjà les permissions (cf. table ObjectAccess). C'est pour - cette raison que j'assigne le domaine test.com ici... - """ - super(PermissionsTestCase, self).setUp() - self.user = User.objects.get(username="user@test.com") - self.values = dict( - username=self.user.username, role="DomainAdmins", - is_active=self.user.is_active, email="user@test.com" - ) - self.admin = User.objects.get(username="admin@test.com") - dom = Domain.objects.get(name="test.com") - dom.add_admin(self.admin) - - def tearDown(self): - self.clt.logout() - - def test_domain_admins(self): - self.values["role"] = "DomainAdmins" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[self.user.id]), - self.values) - self.assertEqual(self.user.group == "DomainAdmins", True) - - self.values["role"] = "SimpleUsers" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[self.user.id]), - self.values) - self.assertEqual(self.user.group == 'DomainAdmins', False) - - def test_superusers(self): - self.values["role"] = "SuperAdmins" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[self.user.id]), - self.values) - self.assertEqual(User.objects.get(username="user@test.com").is_superuser, True) - - self.values["role"] = "SimpleUsers" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[self.user.id]), - self.values) - self.assertEqual(User.objects.get(username="user@test.com").is_superuser, False) - - def test_self_modif(self): - self.clt.logout() - self.assertEqual(self.clt.login(username="admin@test.com", password="toto"), - True) - admin = User.objects.get(username="admin@test.com") - values = dict(username="admin@test.com", first_name="Admin", password1="", password2="", - quota=10, is_active=True, email="admin@test.com") - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[admin.id]), values) - self.assertEqual(admin.group, "DomainAdmins") - self.assertEqual(admin.can_access(Domain.objects.get(name="test.com")), True) - - values["role"] = "SuperAdmins" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[admin.id]), values) - admin = User.objects.get(username="admin@test.com") - self.assertEqual(admin.group, "DomainAdmins") - - def test_domadmin_access(self): - self.clt.logout() - self.assertEqual(self.clt.login(username="admin@test.com", password="toto"), - True) - response = self.clt.get(reverse("modoboa.admin.views.domains")) - self.assertEqual(response.status_code, 200) - - user = User.objects.get(username="user@test.com") - response = self.clt.get(reverse("modoboa.admin.views.editaccount", args=[user.id]), - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertNotEqual(response["Content-Type"], "application/json") - - def test_domainadmin_deletes_superadmin(self): - """Check domain admins restrictions about super admins - - When a super admin owns a mailbox and a domain admin exists - for the associated domain, this domain admin must not be able - to access the super admin. - """ - values = dict(username="superadmin2@test.com", first_name="Super", last_name="Admin", - password1="toto", password2="toto", role="SuperAdmins", - is_active=True, email="superadmin2@test.com", stepid=2) - self.check_ajax_post(reverse("modoboa.admin.views.newaccount"), values) - - account = User.objects.get(username="superadmin2@test.com") - self.clt.logout() - self.clt.login(username="admin@test.com", password="toto") - self.check_ajax_get(reverse("modoboa.admin.views.delaccount", args=[account.id]), {}, - status="ko", respmsg="Permission denied") - self.check_ajax_get(reverse("modoboa.admin.views.delaccount", args=[account.id]), {}, - status="ko", respmsg="Permission denied") - - -class ImportTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def setUp(self): - super(ImportTestCase, self).setUp() - self.admin = User.objects.get(username="admin@test.com") - dom = Domain.objects.get(name="test.com") - dom.add_admin(self.admin) - - def test_domains_import(self): - f = ContentFile(b"""domain; domain1.com; 100; True -domain; domain2.com; 200; False -domainalias; domalias1.com; domain1.com; True -""", name="domains.csv") - self.clt.post(reverse("modoboa.admin.views.import_domains"), { - "sourcefile": f - }) - admin = User.objects.get(username="admin") - dom = Domain.objects.get(name="domain1.com") - self.assertEqual(dom.quota, 100) - self.assertTrue(dom.enabled) - self.assertTrue(admin.is_owner(dom)) - domalias = DomainAlias.objects.get(name="domalias1.com") - self.assertEqual(domalias.target, dom) - self.assertTrue(dom.enabled) - self.assertTrue(admin.is_owner(domalias)) - dom = Domain.objects.get(name="domain2.com") - self.assertEqual(dom.quota, 200) - self.assertFalse(dom.enabled) - self.assertTrue(admin.is_owner(dom)) - - def test_identities_import(self): - f = ContentFile(b""" -account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com -account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; test.com -alias; alias1@test.com; True; user1@test.com -forward; fwd1@test.com; True; user@extdomain.com -dlist; dlist@test.com; True; user1@test.com; user@extdomain.com -""", name="identities.csv") - self.clt.post(reverse("modoboa.admin.views.import_identities"), { - "sourcefile": f, "crypt_password": True - }) - admin = User.objects.get(username="admin") - u1 = User.objects.get(username="user1@test.com") - self.assertTrue(admin.is_owner(u1)) - self.assertEqual(u1.email, "user1@test.com") - self.assertEqual(u1.first_name, "User") - self.assertEqual(u1.last_name, "One") - self.assertTrue(u1.is_active) - self.assertEqual(u1.group, "SimpleUsers") - self.assertTrue(admin.is_owner(u1.mailbox_set.all()[0])) - self.assertEqual(u1.mailbox_set.all()[0].full_address, "user1@test.com") - self.assertTrue(self.clt.login(username="user1@test.com", password="toto")) - - da = User.objects.get(username="truc@test.com") - self.assertEqual(da.first_name, u"René") - self.assertEqual(da.group, "DomainAdmins") - self.assertEqual(da.mailbox_set.all()[0].full_address, "truc@test.com") - dom = Domain.objects.get(name="test.com") - self.assertIn(da, dom.admins) - u = User.objects.get(username="user@test.com") - self.assertTrue(da.can_access(u)) - - al = Alias.objects.get(address="alias1", domain__name="test.com") - self.assertIn(u1.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(admin.is_owner(al)) - - fwd = Alias.objects.get(address="fwd1", domain__name="test.com") - self.assertIn("user@extdomain.com", fwd.extmboxes) - self.assertTrue(admin.is_owner(fwd)) - - dlist = Alias.objects.get(address="dlist", domain__name="test.com") - self.assertIn(u1.mailbox_set.all()[0], dlist.mboxes.all()) - self.assertIn("user@extdomain.com", dlist.extmboxes) - self.assertTrue(admin.is_owner(dlist)) - - def test_import_duplicate(self): - f = ContentFile(b""" -account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; test.com -account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; test.com -""", name="identities.csv") - self.clt.post(reverse("modoboa.admin.views.import_identities"), { - "sourcefile": f, "crypt_password": True, "continue_if_exists": True - }) - - admin = User.objects.get(username="admin") - u1 = User.objects.get(username="truc@test.com") - self.assertTrue(admin.is_owner(u1)) - - def test_import_superadmin(self): - """Check if a domain admin can import a superadmin - - Expected result: no - """ - self.clt.logout() - self.assertTrue(self.clt.login(username="admin@test.com", password="toto")) - f = ContentFile(b""" -account; sa@test.com; toto; Super; Admin; True; SuperAdmins; superadmin@test.com -""", name="identities.csv") - self.clt.post(reverse("modoboa.admin.views.import_identities"), { - "sourcefile": f, "crypt_password": True, "continue_if_exists": True - }) - with self.assertRaises(User.DoesNotExist): - User.objects.get(username="sa@test.com") diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py deleted file mode 100644 index b34136387..000000000 --- a/modoboa/admin/views.py +++ /dev/null @@ -1,889 +0,0 @@ -# coding: utf-8 -from django.http import HttpResponse, HttpResponseRedirect -from django.utils.translation import ugettext as _, ungettext -from django.core.urlresolvers import reverse -from django.contrib.auth.decorators \ - import login_required, permission_required, user_passes_test -from django.db.models import Q -from django.shortcuts import render -from django.template.loader import render_to_string -from django.db import transaction, IntegrityError -from django.views.decorators.csrf import ensure_csrf_cookie -import cStringIO -import csv - -from lib import get_sort_order, get_listing_page -from forms import * -from models import * -from modoboa.admin.tables import DomainsTable, ExtensionsTable -from modoboa.lib import events, parameters -from modoboa.lib.exceptions import ModoboaException, PermDeniedException -from modoboa.lib.webutils \ - import _render, ajax_response, ajax_simple_response, _render_to_string -from modoboa.lib.emailutils import split_mailbox -from modoboa.lib.models import Parameter, Log -from modoboa.lib.formutils import CreationWizard -from modoboa.lib.templatetags.libextras import pagination_bar - - -@login_required -def index(request): - return HttpResponseRedirect(reverse(domains)) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.view_domains") or u.has_perm("admin.view_mailboxes")) -def _domains(request): - sort_order, sort_dir = get_sort_order(request.GET, "name") - domains = request.user.get_domains() - 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() - 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)) - return ajax_simple_response({ - "table": DomainsTable(request, page.object_list).render(), - "page": page.number, - "paginbar": pagination_bar(page), - "handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES", - raise_error=False), - "auto_account_removal": parameters.get_admin("AUTO_ACCOUNT_REMOVAL") - }) - - -@login_required -@ensure_csrf_cookie -def domains(request, tplname="admin/domains.html"): - if not request.user.has_perm("admin.view_domains"): - if request.user.has_perm("admin.view_mailboxes"): - return HttpResponseRedirect(reverse(identities)) - return HttpResponseRedirect(reverse("modoboa.userprefs.views.index")) - return render(request, tplname, {"selection": "domains"}) - - -@login_required -@permission_required("admin.add_user") -def domains_list(request): - doms = [dom.name for dom in request.user.get_domains()] - return ajax_simple_response(doms) - - -@login_required -@permission_required("admin.add_domain") -@transaction.commit_on_success -def newdomain(request, tplname="common/wizard_forms.html"): - events.raiseEvent("CanCreate", request.user, "domains") - - def newdomain_cb(steps): - genform = steps[0]["form"] - genform.is_valid() - domain = genform.save(request.user) - domain.post_create(request.user) - steps[1]["form"].save(request.user, domain) - - commonctx = {"title": _("New domain"), - "action_label": _("Create"), - "action_classes": "submit", - "action": reverse(newdomain), - "formid": "domform"} - cwizard = CreationWizard(newdomain_cb) - cwizard.add_step(DomainFormGeneral, _("General"), - [dict(classes="btn-inverse next", label=_("Next"))], - formtpl="admin/domain_general_form.html") - cwizard.add_step(DomainFormOptions, _("Options"), - [dict(classes="btn-primary submit", label=_("Create")), - dict(classes="btn-inverse prev", label=_("Previous"))], - formtpl="admin/domain_options_form.html") - - if request.method == "POST": - retcode, data = cwizard.validate_step(request) - if retcode == -1: - raise AdminError(data) - if retcode == 1: - return ajax_simple_response(dict( - status="ok", title=cwizard.get_title(data + 1), stepid=data - )) - if retcode == 2: - return ajax_simple_response( - dict(status="ok", respmsg=_("Domain created")) - ) - - from modoboa.lib.templatetags.libextras import render_form - return ajax_simple_response(dict( - status="ko", stepid=data, - form=render_form(cwizard.steps[data]["form"]) - )) - - cwizard.create_forms() - commonctx.update(steps=cwizard.steps) - commonctx.update(subtitle="1. %s" % cwizard.steps[0]['title']) - return render(request, tplname, commonctx) - - -@login_required -@permission_required("admin.view_domains") -def editdomain(request, dom_id, tplname="admin/editdomainform.html"): - domain = Domain.objects.get(pk=dom_id) - if not request.user.can_access(domain): - raise PermDeniedException - - domadmins = filter( - lambda u: request.user.can_access(u) and not u.is_superuser, - domain.admins - ) - if not request.user.is_superuser: - domadmins = filter(lambda u: u.group == "DomainAdmins", domadmins) - - instances = dict(general=domain) - events.raiseEvent("FillDomainInstances", request.user, domain, instances) - - commonctx = {"title": domain.name, - "action_label": _("Update"), - "action_classes": "submit", - "action": reverse(editdomain, args=[dom_id]), - "formid": "domform", - "domain": domain} - if request.method == "POST": - error = None - domain.oldname = domain.name - form = DomainForm(request.user, request.POST, instances=instances) - if form.is_valid(): - try: - form.save(request.user) - except AdminError, e: - error = str(e) - else: - events.raiseEvent("DomainModified", domain) - return ajax_simple_response({"status": "ok", "respmsg": _("Domain modified")}) - - commonctx["tabs"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - commonctx["tabs"] = DomainForm(request.user, instances=instances) - commonctx["domadmins"] = domadmins - return render(request, tplname, commonctx) - - -@login_required -@permission_required("admin.delete_domain") -@transaction.commit_on_success -def deldomain(request, dom_id): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - try: - mb = Mailbox.objects.get(user__id=request.user.id) - except Mailbox.DoesNotExist: - mb = None - - dom = Domain.objects.get(pk=dom_id) - if not request.user.can_access(dom): - raise PermDeniedException - if mb and mb.domain == dom: - raise AdminError(_("You can't delete your own domain")) - dom.delete(request.user, keepdir) - - msg = ungettext("Domain deleted", "Domains deleted", 1) - return ajax_simple_response({"status": "ok", "respmsg": msg}) - - -def _validate_alias(request, form, successmsg, tplname, commonctx, callback=None): - """Alias validation - - Common function shared between creation and modification actions. - """ - error = None - if form.is_valid(): - form.set_recipients() - try: - alias = form.save() - except IntegrityError: - raise AdminError(_("Alias with this name already exists")) - if callback: - callback(request.user, alias) - return ajax_simple_response({"status": "ok", "respmsg": successmsg}) - - if "targets" in request.POST: - targets = request.POST.getlist("targets") - commonctx["targets"] = targets[:-1] - - commonctx["form"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - -def _new_alias(request, title, action, successmsg, - tplname="common/generic_modal_form.html"): - events.raiseEvent("CanCreate", request.user, "mailbox_aliases") - ctx = { - "title": title, - "action": action, - "formid": "aliasform", - "action_label": _("Create"), - "action_classes": "submit" - } - if request.method == "POST": - def callback(user, alias): - alias.post_create(user) - - form = AliasForm(request.user, request.POST) - return _validate_alias( - request, form, successmsg, tplname, ctx, callback - ) - - form = AliasForm(request.user) - ctx["form"] = form - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newdlist(request): - return _new_alias( - request, _("New distribution list"), reverse(newdlist), - _("Distribution list created") - ) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newalias(request): - return _new_alias( - request, _("New alias"), reverse(newalias), - _("Alias created") - ) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newforward(request): - return _new_alias( - request, _("New forward"), reverse(newforward), - _("Forward created") - ) - - -@login_required -@permission_required("admin.change_alias") -def editalias(request, alid, tplname="common/generic_modal_form.html"): - alias = Alias.objects.get(pk=alid) - if not request.user.can_access(alias): - raise PermDeniedException - ctx = dict( - action=reverse(editalias, args=[alias.id]), - formid="aliasform", - title=alias.full_address, - action_label=_("Update"), - action_classes="submit" - ) - if len(alias.get_recipients()) >= 2: - successmsg = _("Distribution list modified") - elif alias.extmboxes != "": - successmsg = _("Forward modified") - else: - successmsg = _("Alias modified") - if request.method == "POST": - form = AliasForm(request.user, request.POST, instance=alias) - return _validate_alias(request, form, successmsg, tplname, ctx) - - form = AliasForm(request.user, instance=alias) - ctx["form"] = form - return render(request, tplname, ctx) - - -def _del_alias(request, msg, msgs): - selection = request.GET["selection"].split(",") - for alid in selection: - alias = Alias.objects.get(pk=alid) - if not request.user.can_access(alias): - raise PermDeniedException - alias.delete() - - msg = ungettext(msg, msgs, len(selection)) - return ajax_simple_response({"status": "ok", "respmsg": msg}) - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delalias(request): - return _del_alias(request, "Alias deleted", "Aliases deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def deldlist(request): - return _del_alias(request, "Distribution list deleted", - "Distribution lists deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delforward(request): - return _del_alias(request, "Forward deleted", "Forwards deleted") - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def _identities(request): - idents_list = request.user.get_identities(request.GET) - sort_order, sort_dir = get_sort_order(request.GET, "identity", - ["identity", "name_or_rcpt", "tags"]) - if sort_order in ["identity", "name_or_rcpt"]: - objects = sorted(idents_list, key=lambda o: getattr(o, sort_order), - reverse=sort_dir == '-') - else: - objects = sorted(idents_list, key=lambda o: o.tags[0], - reverse=sort_dir == '-') - page = get_listing_page(objects, request.GET.get("page", 1)) - return ajax_simple_response({ - "table": _render_to_string(request, "admin/identities_table.html", { - "identities": page.object_list, - "tableid": "objects_table" - }), - "handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES", - raise_error=False), - "page": page.number, - "paginbar": pagination_bar(page) - }) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -@ensure_csrf_cookie -def identities(request, tplname="admin/identities.html"): - return render(request, tplname, { - "selection": "identities", - "deflocation": "list/" - }) - - -@login_required -@permission_required("admin.add_user") -def accounts_list(request): - accs = User.objects.filter(is_superuser=False) \ - .exclude(groups__name='SimpleUsers') - res = [a.username for a in accs.all()] - return ajax_simple_response(res) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") - or u.has_perm("admin.add_alias")) -def allowed_recipients_list(request): - ret = [mb.full_address for mb in request.user.get_mailboxes()] - ret += [al.full_address for al in request.user.get_aliases()] - return ajax_simple_response(ret) - - -@login_required -@permission_required("admin.add_mailbox") -def list_quotas(request, tplname="admin/quotas.html"): - from modoboa.lib.dbutils import db_type - - sort_order, sort_dir = get_sort_order(request.GET, "address") - mboxes = request.user.get_mailboxes(request.GET.get("searchquery", None)) - mboxes = mboxes.exclude(quota=0) - if sort_order in ["address", "quota", "quota_value__bytes"]: - mboxes = mboxes.order_by("%s%s" % (sort_dir, sort_order)) - elif sort_order == "quota_usage": - if db_type() == "postgres": - select = '(admin_quota.bytes::float / (CAST(admin_mailbox.quota AS BIGINT) * 1048576)) * 100' - else: - select = 'admin_quota.bytes / (admin_mailbox.quota * 1048576) * 100' - mboxes = mboxes.extra( - select={'quota_usage': select}, - where=["admin_quota.mbox_id=admin_mailbox.id"], - tables=["admin_quota"], - order_by=["%s%s" % (sort_dir, sort_order)] - ) - else: - raise AdminError(_("Invalid request")) - page = get_listing_page(mboxes, request.GET.get("page", 1)) - return ajax_simple_response({ - "status": "ok", - "page": page.number, - "paginbar": pagination_bar(page), - "table": _render_to_string(request, tplname, { - "mboxes": page - }) - }) - - -@login_required -@permission_required("admin.add_user") -@transaction.commit_on_success -def newaccount(request, tplname='common/wizard_forms.html'): - def create_account(steps): - """Account creation callback - - Called when all creation steps have been validated. - - :param steps: the steps data - """ - genform = steps[0]["form"] - genform.is_valid() - account = genform.save() - account.post_create(request.user) - - mailform = steps[1]["form"] - try: - mailform.save(request.user, account) - except AdminError: - # A bit uggly: transaction management doesn't work very - # well with nested functions. Need to wait for django 1.6 - # and atomicity. - account.delete(request.user, False) - raise - - ctx = dict( - title=_("New account"), - action=reverse(newaccount), - formid="newaccount_form", - submit_label=_("Create") - ) - cwizard = CreationWizard(create_account) - cwizard.add_step(AccountFormGeneral, _("General"), - [dict(classes="btn-inverse next", label=_("Next"))], - new_args=[request.user]) - cwizard.add_step(AccountFormMail, _("Mail"), - [dict(classes="btn-primary submit", label=_("Create")), - dict(classes="btn-inverse prev", label=_("Previous"))], - formtpl="admin/mailform.html") - - if request.method == "POST": - retcode, data = cwizard.validate_step(request) - if retcode == -1: - raise AdminError(data) - if retcode == 1: - return ajax_simple_response(dict( - status="ok", title=cwizard.get_title(data + 1), stepid=data - )) - if retcode == 2: - return ajax_simple_response(dict( - status="ok", respmsg=_("Account created") - )) - - from modoboa.lib.templatetags.libextras import render_form - return ajax_simple_response(dict( - status="ko", stepid=data, - form=render_form(cwizard.steps[data]["form"]) - )) - - cwizard.create_forms() - ctx.update(steps=cwizard.steps) - ctx.update(subtitle="1. %s" % cwizard.steps[0]['title']) - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.change_user") -@transaction.commit_on_success -def editaccount(request, accountid, tplname="common/tabforms.html"): - account = User.objects.get(pk=accountid) - if not request.user.can_access(account): - raise PermDeniedException - mb = None - if account.has_mailbox: - mb = account.mailbox_set.all()[0] - - instances = dict(general=account, mail=mb, perms=account) - events.raiseEvent("FillAccountInstances", request.user, account, instances) - - ctx = dict( - title=account.username, - formid="accountform", - action=reverse(editaccount, args=[accountid]), - action_label=_("Update"), - action_classes="submit" - ) - - if request.method == "POST": - classes = {} - form = AccountForm(request.user, request.POST, - instances=instances, classes=classes) - account.oldgroup = account.group - if form.is_valid(mandatory_only=True): - form.save_general_form() - if form.is_valid(optional_only=True): - events.raiseEvent("AccountModified", account, form.account) - form.save() - return ajax_simple_response( - dict(status="ok", respmsg=_("Account updated")) - ) - transaction.rollback() - - ctx["tabs"] = form - return ajax_response(request, status="ko", template=tplname, **ctx) - - ctx["tabs"] = AccountForm(request.user, instances=instances) - active_tab_id = request.GET.get("active_tab", "default") - if active_tab_id != "default": - ctx["tabs"].active_id = active_tab_id - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.delete_user") -@transaction.commit_on_success -def delaccount(request, accountid): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - - User.objects.get(pk=accountid).delete(request.user, keepdir) - - msg = ungettext("Account deleted", "Accounts deleted", 1) - return ajax_simple_response({"status": "ok", "respmsg": msg}) - - -@login_required -@permission_required("admin.add_domain") -def remove_permission(request): - domid = request.GET.get("domid", None) - daid = request.GET.get("daid", None) - if domid is None or daid is None: - raise AdminError(_("Invalid request")) - try: - account = User.objects.get(pk=daid) - domain = Domain.objects.get(pk=domid) - except (User.DoesNotExist, Domain.DoesNotExist): - raise AdminError(_("Invalid request")) - if not request.user.can_access(account) or not request.user.can_access(domain): - raise PermDeniedException - domain.remove_admin(account) - return ajax_simple_response(dict(status="ok")) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewsettings(request, tplname='admin/settings_header.html'): - return render(request, tplname, { - "selection": "settings" - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewparameters(request, tplname='admin/parameters.html'): - return ajax_simple_response({ - "status": "ok", - "left_selection": "parameters", - "content": _render_to_string(request, tplname, { - "forms": parameters.get_admin_forms - }) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def saveparameters(request): - for formdef in parameters.get_admin_forms(request.POST): - form = formdef["form"] - if form.is_valid(): - form.save() - form.to_django_settings() - continue - return ajax_simple_response({ - "status": "ko", "prefix": form.app, "errors": form.errors - }) - return ajax_simple_response(dict( - status="ok", respmsg=_("Parameters saved") - )) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewextensions(request, tplname='admin/extensions.html'): - from modoboa.extensions import exts_pool - - exts = exts_pool.list_all() - for ext in exts: - try: - dbext = Extension.objects.get(name=ext["id"]) - ext["selection"] = dbext.enabled - except Extension.DoesNotExist: - dbext = Extension() - dbext.name = ext["id"] - dbext.enabled = False - dbext.save() - ext["selection"] = False - - tbl = ExtensionsTable(request, exts) - return ajax_simple_response({ - "status": "ok", - "content": _render_to_string(request, tplname, {"extensions": tbl}) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def saveextensions(request): - actived_exts = Extension.objects.filter(enabled=True) - found = [] - for k in request.POST.keys(): - if k.startswith("select_"): - parts = k.split("_", 1) - dbext = Extension.objects.get(name=parts[1]) - if not dbext in actived_exts: - dbext.on() - else: - found += [dbext] - for ext in actived_exts: - if not ext in found: - ext.off() - - return ajax_simple_response(dict( - status="ok", respmsg=_("Modifications applied.") - )) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def information(request, tplname="admin/information.html"): - return ajax_simple_response({ - "status": "ok", - "content": render_to_string(tplname, {}) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def logs(request, tplname="admin/logs.html"): - sort_order, sort_dir = get_sort_order( - request.GET, "date_created", - allowed_values=['date_created', 'level', 'logger', 'message'] - ) - page = get_listing_page( - Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)), - request.GET.get("page", 1) - ) - return ajax_simple_response({ - "status": "ok", - "content": render_to_string(tplname, { - "logs": page.object_list, - }), - "page": page.number, - "paginbar": pagination_bar(page), - }) - - -@transaction.commit_on_success -def import_domain(user, row, formopts): - """Specific code for domains import""" - dom = Domain() - dom.from_csv(user, row) - - -@transaction.commit_on_success -def import_domainalias(user, row, formopts): - """Specific code for domain aliases import""" - domalias = DomainAlias() - domalias.from_csv(user, row) - - -@transaction.commit_on_success -def import_account(user, row, formopts): - """Specific code for accounts import""" - account = User() - account.from_csv(user, row, formopts["crypt_password"]) - - -@transaction.commit_on_success -def _import_alias(user, row, **kwargs): - """Specific code for aliases import""" - alias = Alias() - alias.from_csv(user, row, **kwargs) - - -def import_alias(user, row, formopts): - _import_alias(user, row, expected_elements=4) - - -def import_forward(user, row, formopts): - _import_alias(user, row, expected_elements=4) - - -def import_dlist(user, row, formopts): - _import_alias(user, row) - -def importdata(request, formclass=ImportDataForm): - """Generic import function - - As the process of importing data from a CSV file is the same - whatever the type, we do a maximum of the work here. - - :param request: a ``Request`` instance - :param typ: a string indicating the object type being imported - :return: a ``Response`` instance - """ - error = None - form = formclass(request.POST, request.FILES) - if form.is_valid(): - try: - reader = csv.reader(request.FILES['sourcefile'], - delimiter=form.cleaned_data['sepchar']) - except csv.Error, e: - error = str(e) - - if error is None: - try: - cpt = 0 - for row in reader: - if not row: - continue - try: - fct = globals()["import_%s" % row[0].strip()] - except KeyError: - continue - try: - fct(request.user, row, form.cleaned_data) - except IntegrityError, e: - if form.cleaned_data["continue_if_exists"]: - continue - raise ModoboaException(_("Object already exists: %s" % row)) - cpt += 1 - msg = _("%d objects imported successfully" % cpt) - return render(request, "admin/import_done.html", { - "status": "ok", "msg": msg - }) - except (ModoboaException), e: - error = str(e) - - return render(request, "admin/import_done.html", { - "status": "ko", "msg": error - }) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def import_identities(request): - if request.method == "POST": - return importdata(request, ImportIdentitiesForm) - - helptext = _("""Provide a CSV file where lines respect one of the following formats: -The first element of each line is mandatory and must be equal to one of the previous values.
- -You can use a different character as separator.
-""") - ctx = dict( - title=_("Import identities"), - action_label=_("Import"), - action_classes="submit", - action=reverse(import_identities), - formid="importform", - enctype="multipart/form-data", - target="import_target", - form=ImportIdentitiesForm(), - helptext=helptext - ) - return render(request, "admin/importform.html", ctx) - - -def _export(content, filename): - """Export a csv file's content - - :param content: the content to export (string) - :param filename: the name that will appear into the response - :return: an ``HttpResponse`` object - """ - resp = HttpResponse(content) - resp["Content-Type"] = "text/csv" - resp["Content-Length"] = len(content) - resp["Content-Disposition"] = 'attachment; filename="%s"' % filename - return resp - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def export_identities(request): - ctx = { - "title": _("Export identities"), - "action_label": _("Export"), - "action_classes": "submit", - "formid": "exportform", - "action": reverse(export_identities), - } - - if request.method == "POST": - form = ExportIdentitiesForm(request.POST) - form.is_valid() - fp = cStringIO.StringIO() - csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for ident in request.user.get_identities(): - ident.to_csv(csvwriter) - content = fp.getvalue() - fp.close() - return _export(content, form.cleaned_data["filename"]) - - ctx["form"] = ExportIdentitiesForm() - return render(request, "common/generic_modal_form.html", ctx) - - -@login_required -@permission_required("admin.add_domain") -@transaction.commit_on_success -def import_domains(request): - if request.method == "POST": - return importdata(request) - - helptext = _("""Provide a CSV file where lines respect one of the following formats: -The first element of each line is mandatory and must be equal to one of the previous values.
-You can use a different character as separator.
-""") - - ctx = dict( - title=_("Import domains"), - action_label=_("Import"), - action_classes="submit", - action=reverse(import_domains), - formid="importform", - enctype="multipart/form-data", - target="import_target", - helptext=helptext, - form=ImportDataForm() - ) - return render(request, "admin/importform.html", ctx) - - -@login_required -@permission_required("admin.add_domain") -def export_domains(request): - ctx = { - "title": _("Export domains"), - "action_label": _("Export"), - "action_classes": "submit", - "formid": "exportform", - "action": reverse(export_domains), - } - - if request.method == "POST": - form = ExportDomainsForm(request.POST) - form.is_valid() - fp = cStringIO.StringIO() - csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for dom in request.user.get_domains(): - dom.to_csv(csvwriter) - for da in dom.domainalias_set.all(): - da.to_csv(csvwriter) - content = fp.getvalue() - fp.close() - return _export(content, form.cleaned_data["filename"]) - - ctx["form"] = ExportDomainsForm() - return render(request, "common/generic_modal_form.html", ctx) diff --git a/modoboa/auth/forms.py b/modoboa/auth/forms.py deleted file mode 100644 index 143299654..000000000 --- a/modoboa/auth/forms.py +++ /dev/null @@ -1,17 +0,0 @@ -# coding: utf-8 -from django import forms -from django.utils.translation import ugettext_lazy as _ - -class LoginForm(forms.Form): - username = forms.CharField( - label=_("Username"), - widget=forms.TextInput(attrs={"class": "input-block-level"}) - ) - password = forms.CharField( - label=_("Password"), - widget=forms.PasswordInput(attrs={"class": "input-block-level"}) - ) - rememberme = forms.BooleanField( - initial=False, - required=False - ) diff --git a/modoboa/auth/models.py b/modoboa/auth/models.py deleted file mode 100755 index 71a836239..000000000 --- a/modoboa/auth/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/modoboa/auth/tests.py b/modoboa/auth/tests.py deleted file mode 100755 index 082b0c52d..000000000 --- a/modoboa/auth/tests.py +++ /dev/null @@ -1,23 +0,0 @@ -# coding: utf-8 -""" -This file demonstrates two different styles of tests (one doctest and one -unittest). These will both pass when you run "manage.py test". - -Replace these with more appropriate tests for your application. -""" - -from django.test import TestCase - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.failUnlessEqual(1 + 1, 2) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index e69de29bb..824c49add 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -0,0 +1,55 @@ +from django.utils.translation import ugettext_lazy +from modoboa.lib import parameters, events + +base_events = [ + "CanCreate", + + "AccountCreated", + "AccountAutoCreated", + "AccountModified", + "AccountDeleted", + "AccountExported", + "AccountImported", + "PasswordUpdated", + "ExtraAccountActions", + "RoleChanged", + "GetExtraRoles", + "PasswordChange", + + "UserMenuDisplay", + "AdminMenuDisplay", + "GetStaticContent", + + "ExtEnabled", + "ExtDisabled", + + "UserLogin", + "UserLogout", + + "GetAnnouncement", + + "TopNotifications", + "ExtraAdminContent", + + "ExtraUprefsRoutes", + "ExtraUprefsJS" +] + + +def load_settings(): + from .app_settings import GeneralParametersForm, UserSettings + + parameters.register(GeneralParametersForm, ugettext_lazy("General")) + parameters.register(UserSettings, ugettext_lazy("General")) + events.declare(base_events) + + +@events.observe("ExtDisabled") +def unset_default_topredirection(extension): + """ + Simple callback to change the default redirection if the + corresponding extension is being disabled. + """ + topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION") + if topredirection == extension.name: + parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs") diff --git a/modoboa/admin/app_settings.py b/modoboa/core/app_settings.py similarity index 83% rename from modoboa/admin/app_settings.py rename to modoboa/core/app_settings.py index 1f02db899..9acfafae5 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/core/app_settings.py @@ -1,9 +1,10 @@ +# coding: utf-8 from django import forms +from django.conf import settings from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib.formutils import YesNoField, SeparatorField, InlineRadioSelect -from modoboa.lib.sysutils import exec_cmd +from modoboa.lib.cryptutils import random_key from modoboa.lib import parameters -from modoboa.auth.lib import random_key def enabled_applications(): @@ -15,18 +16,18 @@ def enabled_applications(): :return: a list """ - from modoboa.admin.models import Extension + from modoboa.core.models import Extension from modoboa.lib.dbutils import db_table_exists - result = [("userprefs", "userprefs")] - if db_table_exists("admin_extension"): + result = [("core", "core")] + if db_table_exists("core_extension"): exts = Extension.objects.filter(enabled=True) result += [(ext.name, ext.name) for ext in exts] return sorted(result, key=lambda e: e[0]) class GeneralParametersForm(parameters.AdminParametersForm): - app = "admin" + app = "core" sep1 = SeparatorField(label=ugettext_lazy("Authentication")) @@ -97,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(render_value=True), + widget=forms.PasswordInput, required=False ) @@ -134,26 +135,6 @@ class GeneralParametersForm(parameters.AdminParametersForm): help_text=ugettext_lazy("Tell if the LDAP server is an Active Directory one") ) - sep2 = SeparatorField(label=ugettext_lazy("Mailboxes")) - - handle_mailboxes = YesNoField( - label=ugettext_lazy("Handle mailboxes on filesystem"), - initial="no", - help_text=ugettext_lazy("Rename or remove mailboxes on the filesystem when they get renamed or removed within Modoboa") - ) - - mailboxes_owner = forms.CharField( - label=ugettext_lazy("Mailboxes ower"), - initial="vmail", - help_text=ugettext_lazy("The UNIX account who owns mailboxes on the filesystem") - ) - - auto_account_removal = YesNoField( - label=ugettext_lazy("Automatic account removal"), - initial="no", - help_text=ugettext_lazy("When a mailbox is removed, also remove the associated account"), - ) - sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous")) log_maximum_age = forms.IntegerField( @@ -171,13 +152,12 @@ class GeneralParametersForm(parameters.AdminParametersForm): default_top_redirection = forms.ChoiceField( label=ugettext_lazy("Default top redirection"), choices=[], - initial="userprefs", + initial="core", help_text=ugettext_lazy("The default redirection used when no application is specified") ) # Visibility rules visibility_rules = { - "mailboxes_owner": "handle_mailboxes=yes", "ldap_sep": "authentication_type=ldap", "ldap_server_address": "authentication_type=ldap", "ldap_server_port": "authentication_type=ldap", @@ -195,24 +175,6 @@ class GeneralParametersForm(parameters.AdminParametersForm): def __init__(self, *args, **kwargs): super(GeneralParametersForm, self).__init__(*args, **kwargs) self.fields["default_top_redirection"].choices = enabled_applications() - hide_fields = False - try: - code, version = exec_cmd("dovecot --version") - except OSError, e: - hide_fields = True - else: - if code or not version.strip().startswith("2"): - hide_fields = True - if hide_fields: - del self.fields["handle_mailboxes"] - del self.fields["mailboxes_owner"] - - 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"] @@ -239,7 +201,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): required_fields = ["ldap_search_base", "ldap_search_filter"] else: required_fields = ["ldap_user_dn_template"] - + for f in required_fields: if not f in cleaned_data or cleaned_data[f] == u'': self._errors[f] = self.error_class([_("This field is required")]) @@ -251,7 +213,6 @@ class GeneralParametersForm(parameters.AdminParametersForm): Doing so, we can use the django_auth_ldap module. """ - from django.conf import settings try: import ldap from django_auth_ldap.config import LDAPSearch @@ -289,3 +250,25 @@ class GeneralParametersForm(parameters.AdminParametersForm): }) else: settings.AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_REFERRALS] = False + + +def translate_language_code(value): + if "-" in value: + return value.split("-")[0] + return value + + +class UserSettings(parameters.UserParametersForm): + app = "core" + + sep = SeparatorField(label=ugettext_lazy("Display")) + + lang = forms.ChoiceField( + initial=translate_language_code(settings.LANGUAGE_CODE), + label=ugettext_lazy("Prefered language"), + choices=[("cs", "čeština"), ("de", "deutsch"), + ("en", "english"), ("es", "español"), + ("fr", "français"), ("it", "italiano"), + ("pt", "português"), ("sv", "svenska")], + help_text=ugettext_lazy("Prefered language to display pages") + ) diff --git a/modoboa/core/commands/__init__.py b/modoboa/core/commands/__init__.py index e69de29bb..9b2b1c9f2 100644 --- a/modoboa/core/commands/__init__.py +++ b/modoboa/core/commands/__init__.py @@ -0,0 +1,90 @@ +# coding: utf-8 +import os +import sys +import argparse +from django.conf import settings +from django.template import Context, Template + + +class Command(object): + """Base command class + + A valid administrative command must inherit from this class. + """ + help = "No help available." + + def __init__(self, commands, verbose=False, **kwargs): + self._commands = commands + self._parser = argparse.ArgumentParser() + self._verbose = verbose + if not settings.configured: + settings.configure() + self._templates_dir = "%s/templates" % os.path.dirname(__file__) + + def _render_template(self, tplfile, env): + fp = open(tplfile) + t = Template(fp.read()) + fp.close() + return t.render(Context(env)) + + def run(self, cmdline): + args = self._parser.parse_args(cmdline) + self.handle(args) + + def handle(self, parsed_args): + """A command must overload this method to be called + + :param parsed_args: + """ + raise NotImplementedError + + +def scan_for_commands(dirname=""): + """Build a dictionnary containing all commands + + :param str dirname: the directory where commands are located + :return: a dict of commands (name : class) + """ + path = os.path.join(os.path.dirname(__file__), dirname) + result = {} + for f in os.listdir(path): + if f in ['.', '..', '__init__.py']: + continue + if not f.endswith('.py'): + continue + if os.path.isfile(f): + continue + cmdname = f.replace('.py', '') + cmdmod = __import__("modoboa.core.commands", globals(), locals(), + [cmdname]) + cmdmod = getattr(cmdmod, cmdname) + if '_' in cmdname: + cmdclassname = ''.join([s.capitalize() for s in cmdname.split('_')]) + else: + cmdclassname = cmdname.capitalize() + try: + cmdclass = getattr(cmdmod, "%sCommand" % cmdclassname) + except AttributeError: + continue + result[cmdname] = cmdclass + return result + + +def handle_command_line(): + commands = scan_for_commands() + parser = argparse.ArgumentParser( + description="A set of utilities to ease the installation of Modoboa.", + epilog="""Available commands: +%s +""" % "\n".join(["\t%s" % c for c in sorted(commands)])) + parser.add_argument('--verbose', action='store_true', + help='Activate verbose output') + parser.add_argument('command', type=str, + help='A valid command name') + (args, remaining) = parser.parse_known_args() + + if not args.command in commands: + print >> sys.stderr, "Unknown command '%s'" % args.command + sys.exit(1) + + commands[args.command](commands, verbose=args.verbose).run(remaining) diff --git a/modoboa/core/commands/deploy.py b/modoboa/core/commands/deploy.py index 3c9c15107..5d10ad69b 100644 --- a/modoboa/core/commands/deploy.py +++ b/modoboa/core/commands/deploy.py @@ -4,18 +4,21 @@ import os import shutil import getpass import subprocess +import dj_database_url + from django.core import management from django.template import Context, Template -from modoboa.core.management import Command +from modoboa.lib.sysutils import exec_cmd +from modoboa.core.commands import Command dbconn_tpl = """ '{{ conn_name }}': { - 'ENGINE': 'django.db.backends.{{ dbtype }}', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': '{{ dbname }}', # Or path to database file if using sqlite3. - 'USER': '{{ username }}', # Not used with sqlite3. - 'PASSWORD': '{{ password }}', # Not used with sqlite3. - 'HOST': '{{ dbhost }}', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3.{% if dbtype == 'mysql' %} + 'ENGINE': '{{ ENGINE }}', + 'NAME': '{{ NAME }}', # Or path to database file if using sqlite3. + 'USER': '{% if USER %}{{ USER }}{% endif %}', # Not used with sqlite3. + 'PASSWORD': '{% if PASSWORD %}{{ PASSWORD }}{% endif %}', # Not used with sqlite3. + 'HOST': '{% if HOST %}{{ HOST }}{% endif %}', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '{% if PORT %}{{ PORT }}{% endif %}', # Set to empty string for default. Not used with sqlite3.{% if ENGINE == 'django.db.backends.mysql' %} 'OPTIONS' : { "init_command" : 'SET foreign_key_checks = 0;', },{% endif %} @@ -42,6 +45,16 @@ class DeployCommand(Command): '--collectstatic', action='store_true', default=False, help='Run django collectstatic command' ) + self._parser.add_argument( + '--dburl', type=str, nargs=1, default=None, + help='The database-url for your modoboa instance') + self._parser.add_argument( + '--amavis_dburl', type=str, nargs=1, default=None, + help='The database-url for your amavis instance') + self._parser.add_argument( + '--domain', type=str, nargs=1, default=None, + help='The domain under which you want to deploy modoboa') + def _exec_django_command(self, name, cwd, *args): """Run a django command for the freshly created project @@ -75,19 +88,29 @@ class DeployCommand(Command): """ print "Configuring database connection: %s" % name info = {'conn_name': name, - 'dbtype': raw_input('Database type (mysql or postgres): ')} - if info['dbtype'] not in ['mysql', 'postgres']: - info['dbtype'] = 'mysql' - if info['dbtype'] == 'postgres': - info['dbtype'] = 'postgresql_psycopg2' - default_host = 'localhost' if info['dbtype'] == 'mysql' else '' - info['dbhost'] = \ - raw_input("Database host (default: '%s'): " % default_host) - if info['dbhost'] == '': - info['dbhost'] = default_host - info['dbname'] = raw_input('Database name: ') - info['username'] = raw_input('Username: ') - info['password'] = getpass.getpass('Password: ') + 'ENGINE': raw_input('Database type (mysql, postgres or sqlite3): ')} + if info['ENGINE'] not in ['mysql', 'postgres', 'sqlite3']: + raise RuntimeError('Unsupported database engine') + + if info['ENGINE'] == 'sqlite3': + info['ENGINE'] = 'django.db.backends.sqlite3' + info['NAME'] = '%s.db' % name + return info + if info['ENGINE'] == 'postgres': + info['ENGINE'] = 'django.db.backends.postgresql_psycopg2' + default_port = 5432 + else: + info['ENGINE'] = 'django.db.backends.mysql' + default_port = 3306 + info['HOST'] = raw_input("Database host (default: 'localhost'): ") + info['PORT'] = raw_input("Database port (default: '%s'): " % default_port) + # leave port setting empty, if default value is supplied and + # leave it to django + if info['PORT'] == default_port: + info['PORT'] = '' + info['NAME'] = raw_input('Database name: ') + info['USER'] = raw_input('Username: ') + info['PASSWORD'] = getpass.getpass('Password: ') return info def handle(self, parsed_args): @@ -105,13 +128,41 @@ class DeployCommand(Command): django14 = False t = Template(dbconn_tpl) - default_conn = t.render(Context(self.ask_db_info())) - amavis_conn = t.render(Context(self.ask_db_info('amavis'))) \ - if parsed_args.with_amavis else None - allowed_host = raw_input( - 'Under which domain do you want to deploy modoboa? ' - ) + if parsed_args.dburl: + info = dj_database_url.config(default=parsed_args.dburl[0]) + # In case the user fails to supply a valid database url, fallback to manual mode + if not info: + print "There was a problem with your database-url. \n" + info = self.ask_db_info() + #If we set this earlier, our fallback method will never be triggered + info['conn_name'] = 'default' + else: + info = self.ask_db_info() + + default_conn = t.render(Context(info)) + + if parsed_args.with_amavis: + if parsed_args.amavis_dburl: + amavis_info = dj_database_url.config(default=parsed_args.amavis_dburl[0]) + # In case the user fails to supply a valid database url, fallback to manual mode + if not amavis_info: + amavis_info = self.ask_db_info('amavis') + #If we set this earlier, our fallback method will never be triggered + amavis_info['conn_name'] = 'amavis' + else: + amavis_info = self.ask_db_info('amavis') + + amavis_conn = t.render(Context(amavis_info)) + else: + amavis_conn = None + + if parsed_args.domain: + allowed_host = parsed_args.domain[0] + else: + allowed_host = raw_input( + 'Under which domain do you want to deploy modoboa? ' + ) mod = __import__(parsed_args.name, globals(), locals(), ['settings']) tpl = self._render_template( @@ -122,17 +173,24 @@ class DeployCommand(Command): 'allowed_host': allowed_host } ) - fp = open("%s/settings.py" % path, "w") - fp.write(tpl) - fp.close() + with open("%s/settings.py" % path, "w") as fp: + fp.write(tpl) shutil.copyfile( "%s/urls.py.tpl" % self._templates_dir, "%s/urls.py" % path ) os.mkdir("%s/media" % path) if parsed_args.syncdb: + os.unlink("%s/settings.pyc" % path) self._exec_django_command( - "syncdb", parsed_args.name, '--migrate', '--noinput' + "syncdb", parsed_args.name, '--noinput' + ) + exec_cmd('sed -ri "s|^#(\s+\'south)|\\1|" %s/settings.py' % path) + self._exec_django_command( + "syncdb", parsed_args.name, + ) + self._exec_django_command( + 'migrate', parsed_args.name, '--fake' ) self._exec_django_command( "loaddata", parsed_args.name, 'initial_users.json' diff --git a/modoboa/core/commands/help.py b/modoboa/core/commands/help.py index cd1ada463..dfe46307c 100644 --- a/modoboa/core/commands/help.py +++ b/modoboa/core/commands/help.py @@ -1,6 +1,7 @@ # coding: utf-8 import sys -from modoboa.core.management import Command +from . import Command + class HelpCommand(Command): help = "Display the help message associated to a specific command" diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index f697db4e2..eda26f4fc 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -3,66 +3,114 @@ import getpass import inspect import os from django.template import Context, Template -from modoboa.core.management import Command +from . import Command -class MapFile(object): - category = 'std' +class MapFilesGenerator(object): def __init__(self): - self.base_tpl = Template("""user = {{ dbuser }} -password = {{ dbpassword }} + self.template = Template("""user = {{ dbuser }} +password = {{ dbpass }} dbname = {{ dbname }} hosts = {{ dbhost }} query = {{ query|safe }} """) + self.tpl_context = { + 'dbhost': raw_input('Database host: '), + 'dbname': raw_input('Database name: '), + 'dbuser': raw_input('Username: '), + 'dbpass': getpass.getpass('Password: ') + } + if not self.tpl_context['dbhost']: + self.tpl_context['dbhost'] = '127.0.0.1' - def render(self, destdir, dbtype, **options): + def render(self, args): + """Render all map files. + + :param args: arguments received from the command line + """ try: - options['query'] = getattr(self, dbtype) - except AttributeError: - return + os.mkdir(args.destdir) + except OSError: + pass - content = self.base_tpl.render(Context(options)) - fp = open("%s/%s" % (destdir, self.filename), "w") - fp.write(content) - fp.close() + for v in globals().values(): + if not inspect.isclass(v) or not issubclass(v, MapFile): + continue + if v is MapFile: + continue + if not v.category in args.categories: + continue + content = self.template.render( + Context(dict(self.tpl_context.items(), + query=getattr(v, args.dbtype))) + ) + with open("%s/%s" % (args.destdir, v.filename), "w") as fp: + print >> fp, content + + +class SQLiteMapFilesGenerator(MapFilesGenerator): + + def __init__(self): + self.template = Template("""dbpath = {{ dbpath }} +query = {{ query|safe }} +""") + self.tpl_context = { + 'dbpath': raw_input('Database path: ') + } + + +class MapFile(object): + category = 'std' class DomainsMap(MapFile): filename = 'sql-domains.cf' mysql = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" postgres = "SELECT name FROM admin_domain WHERE name='%s' AND enabled" + sqlite = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" class DomainsAliasesMap(MapFile): filename = 'sql-domain-aliases.cf' - mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT concat(al.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" - postgres = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND al.address='%u')" + mysql = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" + postgres = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled AND dom.enabled" + sqlite = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" class MailboxesMap(MapFile): filename = 'sql-mailboxes.cf' mysql = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user user ON mb.user_id=user.id WHERE dom.enabled=1 AND dom.name='%d' AND user.is_active=1 AND mb.address='%u'" postgres = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user u ON mb.user_id=u.id WHERE dom.enabled AND dom.name='%d' AND u.is_active AND mb.address='%u'" + sqlite = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user user ON mb.user_id=user.id WHERE dom.enabled=1 AND dom.name='%d' AND user.is_active=1 AND mb.address='%u'" class AliasesMap(MapFile): filename = 'sql-aliases.cf' mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT concat(al.address, '@', dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.address || '@' || dom.name FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled AND al.extmboxes<>'')" + sqlite = "(SELECT (mb.address || '@' || dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT (al.address || '@' || dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" class DomainAliasesMailboxesMap(MapFile): filename = 'sql-domain-aliases-mailboxes.cf' - mysql = "SELECT DISTINCT concat(mb.address, '@', dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON dom.id=al.domain_id INNER JOIN admin_domainalias domal ON domal.target_id=dom.id INNER JOIN (admin_alias_mboxes almb, admin_mailbox mb) ON (almb.alias_id=al.id AND almb.mailbox_id=mb.id) WHERE domal.name='%d' AND domal.enabled=1 AND (al.address='%u' OR mb.address='%u')" - postgres = "SELECT DISTINCT mb.address || '@' || dom.name FROM admin_alias al INNER JOIN admin_domain dom ON dom.id=al.domain_id INNER JOIN admin_domainalias domal ON domal.target_id=dom.id INNER JOIN admin_alias_mboxes almb ON almb.alias_id=al.id INNER JOIN admin_mailbox mb ON almb.mailbox_id=mb.id WHERE domal.name='%d' AND domal.enabled AND (al.address='%u' OR mb.address='%u')" + mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT concat(al.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" + postgres = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND al.address='%u')" + sqlite = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" class CatchallAliasesMap(MapFile): filename = 'sql-catchall-aliases.cf' mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled AND al.address='*' AND dom.name='%d' AND dom.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled)" + sqlite = "(SELECT (mb.address || '@' || dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" + + +class MaintainMap(MapFile): + filename = 'sql-maintain.cf' + mysql = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + postgres = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + sqlite = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" class TransportMap(MapFile): @@ -70,6 +118,7 @@ class TransportMap(MapFile): filename = 'sql-transport.cf' mysql = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" postgres = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" + sqlite = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" class AutoRepliesMap(MapFile): @@ -77,6 +126,7 @@ class AutoRepliesMap(MapFile): filename = 'sql-autoreplies.cf' mysql = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" postgres = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" + sqlite = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" class PostfixMapsCommand(Command): @@ -84,30 +134,23 @@ class PostfixMapsCommand(Command): def __init__(self, *args, **kwargs): super(PostfixMapsCommand, self).__init__(*args, **kwargs) - self._parser.add_argument('destdir', type=str, - help='Directory that will contain generated map files') - self._parser.add_argument('--dbtype', type=str, choices=['mysql', 'postgres'], default='mysql', - help='Used database type') - self._parser.add_argument('--dbhost', type=str, default='127.0.0.1', - help='Database host address') - self._parser.add_argument('--categories', type=str, nargs='*', default=['std'], - help='Map file categories to generate (choices: std, autoreply)') + self._parser.add_argument( + 'destdir', type=str, + help='Directory that will contain generated map files' + ) + self._parser.add_argument( + '--dbtype', type=str, choices=['mysql', 'postgres', 'sqlite'], + default='mysql', + help='Used database type' + ) + self._parser.add_argument( + '--categories', type=str, nargs='*', default=['std'], + help='Map file categories to generate (choices: std, autoreply)' + ) def handle(self, parsed_args): - dbname = raw_input('Database name: ') - dbuser = raw_input('Username: ') - dbpass = getpass.getpass('Password: ') - - try: - os.mkdir(parsed_args.destdir) - except OSError: - pass - - for k, v in globals().iteritems(): - if not inspect.isclass(v) or not issubclass(v, MapFile): - continue - if not v.category in parsed_args.categories: - continue - v().render(parsed_args.destdir, parsed_args.dbtype, - dbname=dbname, dbuser=dbuser, dbpassword=dbpass, - dbhost=parsed_args.dbhost) + if parsed_args.dbtype == 'sqlite': + g = SQLiteMapFilesGenerator() + else: + g = MapFilesGenerator() + g.render(parsed_args) diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl similarity index 96% rename from modoboa/core/templates/settings.py.tpl rename to modoboa/core/commands/templates/settings.py.tpl index 4f926f712..f86ad1e39 100644 --- a/modoboa/core/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -141,10 +141,8 @@ INSTALLED_APPS = ( # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', - 'south', - - # Extra tools - # 'modoboa.tools.pfxadmin_migrate', +# 'south', + 'reversion', ) # A dedicated place to register Modoboa applications @@ -152,26 +150,23 @@ INSTALLED_APPS = ( # Do not change the order. MODOBOA_APPS = ( 'modoboa', - 'modoboa.auth', - 'modoboa.admin', + 'modoboa.core', 'modoboa.lib', - 'modoboa.userprefs', - + # Modoboa extensions here. + 'modoboa.extensions.admin', 'modoboa.extensions.limits', 'modoboa.extensions.postfix_autoreply', 'modoboa.extensions.webmail', 'modoboa.extensions.stats', - {% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis', 'modoboa.extensions.sievefilters', + {% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis', ) INSTALLED_APPS += MODOBOA_APPS -INSTALLED_APPS += ( - 'reversion', -) +AUTH_USER_MODEL = 'core.User' -AUTH_USER_MODEL = 'admin.User' +SOUTH_TESTS_MIGRATE = False #MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") @@ -209,7 +204,7 @@ LOGGING = { 'formatter': 'syslog' }, 'modoboa': { - 'class': 'modoboa.lib.logutils.SQLHandler', + 'class': 'modoboa.core.loggers.SQLHandler', } }, 'loggers': { diff --git a/modoboa/core/templates/urls.py.tpl b/modoboa/core/commands/templates/urls.py.tpl similarity index 100% rename from modoboa/core/templates/urls.py.tpl rename to modoboa/core/commands/templates/urls.py.tpl diff --git a/modoboa/admin/exceptions.py b/modoboa/core/exceptions.py similarity index 95% rename from modoboa/admin/exceptions.py rename to modoboa/core/exceptions.py index 9ee8ced0d..063db7ad9 100644 --- a/modoboa/admin/exceptions.py +++ b/modoboa/core/exceptions.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ :mod:`exceptions` --- Custom exceptions of the admin. application ----------------------------------------------------------------- diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py new file mode 100644 index 000000000..4b399bd24 --- /dev/null +++ b/modoboa/core/extensions.py @@ -0,0 +1,93 @@ +from django.conf import settings +from django.conf.urls import include + + +class ModoExtension(object): + """Base extension class + + Each Modoboa extension must inherit from this class to be + considered as valid. + """ + name = None + label = None + version = "NA" + description = "" + url = None + needs_media = False + always_active = False + + def infos(self): + return dict( + name=self.name, label=self.label, version=self.version, + description=self.description, url=self.url, + always_active=self.always_active + ) + + def init(self): + pass + + def destroy(self): + pass + + def load(self): + pass + + +class ExtensionsPool(object): + """The extensions manager""" + + def __init__(self): + self.extensions = dict() + + def register_extension(self, ext, show=True): + self.extensions[ext.name] = dict(cls=ext, show=show) + + def get_extension(self, name): + if not name in self.extensions: + return None + if not "instance" in self.extensions[name]: + self.extensions[name]["instance"] = self.extensions[name]["cls"]() + return self.extensions[name]["instance"] + + def get_extension_infos(self, name): + instance = self.get_extension(name) + if instance is None: + return None + return instance.infos() + + def load_all(self): + from modoboa.core.models import Extension + + for ext in settings.MODOBOA_APPS: + __import__(ext) + result = [] + for extname in self.extensions.keys(): + extinstance = self.get_extension(extname) + if not extinstance.always_active: + try: + ext = Extension.objects.get(name=extname) + if not ext.enabled: + continue + except Extension.DoesNotExist: + continue + extinstance.load() + try: + baseurl = extinstance.url if extinstance.url is not None else extname + result += [(r'^%s/' % (baseurl), + include("%s.urls" % extinstance.__module__))] + except ImportError: + # No urls for this extension + pass + return result + + def list_all(self): + result = [] + for extname, extdef in self.extensions.iteritems(): + if not extdef["show"]: + continue + infos = extdef["instance"].infos() + infos["id"] = extname + result += [infos] + return sorted(result, key=lambda i: i["name"]) + +exts_pool = ExtensionsPool() diff --git a/modoboa/core/factories.py b/modoboa/core/factories.py new file mode 100644 index 000000000..df37f5a47 --- /dev/null +++ b/modoboa/core/factories.py @@ -0,0 +1,35 @@ +import factory +from django.contrib.auth.models import Group +from . import models + + +class PermissionFactory(factory.DjangoModelFactory): + ABSTRACT_FACTORY = True + + @factory.post_generation + def set_permission(self, create, extracted, **kwargs): + if not create: + return + self.post_create(models.User.objects.get(pk=1)) + + +class GroupFactory(factory.DjangoModelFactory): + FACTORY_FOR = Group + + name = 'DefaultGroup' + + +class UserFactory(PermissionFactory): + FACTORY_FOR = models.User + + email = factory.LazyAttribute(lambda a: a.username) + password = '{PLAIN}toto' + + @factory.post_generation + def groups(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for group in extracted: + self.groups.add(Group.objects.get(name=group)) diff --git a/modoboa/core/fixtures/initial_users.json b/modoboa/core/fixtures/initial_users.json new file mode 100644 index 000000000..bf60843ba --- /dev/null +++ b/modoboa/core/fixtures/initial_users.json @@ -0,0 +1 @@ +[{"pk": 1, "model": "core.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "last_login": "2012-01-26 15:19:25", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26 15:19:25"}},{"pk": 1, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["core", "user"], "object_id": 1}}] \ No newline at end of file diff --git a/modoboa/core/forms.py b/modoboa/core/forms.py new file mode 100644 index 000000000..4dc7d38ae --- /dev/null +++ b/modoboa/core/forms.py @@ -0,0 +1,71 @@ +# coding: utf-8 +from django import forms +from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.core.models import User +from modoboa.lib import parameters + + +class LoginForm(forms.Form): + username = forms.CharField( + label=ugettext_lazy("Username"), + widget=forms.TextInput(attrs={"class": "input-block-level"}) + ) + password = forms.CharField( + label=ugettext_lazy("Password"), + widget=forms.PasswordInput(attrs={"class": "input-block-level"}) + ) + rememberme = forms.BooleanField( + initial=False, + required=False + ) + + +class ProfileForm(forms.ModelForm): + oldpassword = forms.CharField( + label=ugettext_lazy("Old password"), required=False, + widget=forms.PasswordInput + ) + newpassword = forms.CharField( + label=ugettext_lazy("New password"), required=False, + widget=forms.PasswordInput + ) + confirmation = forms.CharField( + label=ugettext_lazy("Confirmation"), required=False, + widget=forms.PasswordInput + ) + + class Meta: + model = User + fields = ("first_name", "last_name") + + def __init__(self, update_password, *args, **kwargs): + super(ProfileForm, self).__init__(*args, **kwargs) + if not update_password: + del self.fields["oldpassword"] + del self.fields["newpassword"] + del self.fields["confirmation"] + + def clean_oldpassword(self): + if self.cleaned_data["oldpassword"] == "": + return self.cleaned_data["oldpassword"] + + if parameters.get_admin("AUTHENTICATION_TYPE") != "local": + return self.cleaned_data["oldpassword"] + + if not self.instance.check_password(self.cleaned_data["oldpassword"]): + raise forms.ValidationError(_("Old password mismatchs")) + return self.cleaned_data["oldpassword"] + + def clean_confirmation(self): + if self.cleaned_data["newpassword"] != self.cleaned_data["confirmation"]: + raise forms.ValidationError(_("Passwords mismatch")) + return self.cleaned_data["confirmation"] + + def save(self, commit=True): + user = super(ProfileForm, self).save(commit=False) + if commit: + if self.cleaned_data.has_key("confirmation") and \ + self.cleaned_data["confirmation"] != "": + user.set_password(self.cleaned_data["confirmation"], self.cleaned_data["oldpassword"]) + user.save() + return user diff --git a/modoboa/lib/logutils.py b/modoboa/core/loggers.py similarity index 100% rename from modoboa/lib/logutils.py rename to modoboa/core/loggers.py diff --git a/modoboa/core/management.py b/modoboa/core/management.py deleted file mode 100644 index 82db79f11..000000000 --- a/modoboa/core/management.py +++ /dev/null @@ -1,88 +0,0 @@ -# coding: utf-8 - -from django.conf import settings -from django.template import Context, Template -import argparse -import os -import sys - -class Command(object): - """Base command class - - A valid administrative command must inherit from this class. - """ - help = "No help available." - - def __init__(self, commands, verbose=False, **kwargs): - self._commands = commands - self._parser = argparse.ArgumentParser() - self._verbose = verbose - if not settings.configured: - settings.configure() - self._templates_dir = "%s/templates" % os.path.dirname(__file__) - - def _render_template(self, tplfile, env): - fp = open(tplfile) - t = Template(fp.read()) - fp.close() - return t.render(Context(env)) - - def run(self, cmdline): - args = self._parser.parse_args(cmdline) - self.handle(args) - - def handle(self, parsed_args): - """A command must overload this method to be called - - :param parsed_args: - """ - raise NotImplemented - -def scan_for_commands(dirname="commands"): - """Build a dictionnary containing all commands - - :param dirname: the directory where commands are located - :return: a dict of commands (name : class) - """ - path = os.path.join(os.path.dirname(__file__), dirname) - result = {} - for f in os.listdir(path): - if f in ['.', '..', '__init__.py']: - continue - if not f.endswith('.py'): - continue - if os.path.isfile(f): - continue - cmdname = f.replace('.py', '') - cmdmod = __import__("modoboa.core.commands", globals(), locals(), - [cmdname]) - cmdmod = getattr(cmdmod, cmdname) - if '_' in cmdname: - cmdclassname = ''.join(map(lambda s: s.capitalize(), cmdname.split('_'))) - else: - cmdclassname = cmdname.capitalize() - try: - cmdclass = getattr(cmdmod, "%sCommand" % cmdclassname) - except AttributeError: - continue - result[cmdname] = cmdclass - return result - -def handle_command_line(): - commands = scan_for_commands() - parser = argparse.ArgumentParser( - description="A set of utilities to ease the installation of Modoboa.", - epilog="""Available commands: -%s -""" % "\n".join(map(lambda c: "\t" + c, sorted(commands)))) - parser.add_argument('--verbose', action='store_true', - help='Activate verbose output') - parser.add_argument('command', type=str, - help='A valid command name') - (args, remaining) = parser.parse_known_args() - - if not args.command in commands: - print >>sys.stderr, "Unknown command '%s'" % args.command - sys.exit(1) - - commands[args.command](commands, verbose=args.verbose).run(remaining) diff --git a/modoboa/admin/migrations/__init__.py b/modoboa/core/management/__init__.py similarity index 100% rename from modoboa/admin/migrations/__init__.py rename to modoboa/core/management/__init__.py diff --git a/modoboa/admin/templatetags/__init__.py b/modoboa/core/management/commands/__init__.py similarity index 100% rename from modoboa/admin/templatetags/__init__.py rename to modoboa/core/management/commands/__init__.py diff --git a/modoboa/lib/management/commands/cleanlogs.py b/modoboa/core/management/commands/cleanlogs.py similarity index 91% rename from modoboa/lib/management/commands/cleanlogs.py rename to modoboa/core/management/commands/cleanlogs.py index e25ba7604..3da28798f 100644 --- a/modoboa/lib/management/commands/cleanlogs.py +++ b/modoboa/core/management/commands/cleanlogs.py @@ -1,13 +1,12 @@ #!/usr/bin/env python # coding: utf-8 - import datetime from optparse import make_option from django.core.management.base import BaseCommand from django.utils import timezone +from modoboa.core import load_settings +from modoboa.core.models import Log from modoboa.lib import parameters -from modoboa.lib.models import Log -from modoboa.admin import AdminConsole class Command(BaseCommand): @@ -38,8 +37,8 @@ class Command(BaseCommand): l.addHandler(logging.StreamHandler()) self.verbose = options["verbose"] - AdminConsole().load() - log_maximum_age = int(parameters.get_admin("LOG_MAXIMUM_AGE", app="admin")) + load_settings() + log_maximum_age = int(parameters.get_admin("LOG_MAXIMUM_AGE")) self.__vprint("Deleting logs older than %d days..." % log_maximum_age) limit = timezone.now() - datetime.timedelta(log_maximum_age) Log.objects.filter(date_created__lt=limit).delete() diff --git a/modoboa/core/migrations/0001_initial.py b/modoboa/core/migrations/0001_initial.py new file mode 100644 index 000000000..4b0bbe9ed --- /dev/null +++ b/modoboa/core/migrations/0001_initial.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'User' + db.create_table(u'core_user', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('password', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('last_login', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('is_superuser', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('username', self.gf('django.db.models.fields.CharField')(unique=True, max_length=254)), + ('first_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)), + ('last_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)), + ('email', self.gf('django.db.models.fields.EmailField')(max_length=254, blank=True)), + ('is_staff', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('date_joined', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('is_local', self.gf('django.db.models.fields.BooleanField')(default=True)), + )) + db.send_create_signal(u'core', ['User']) + + # Adding M2M table for field groups on 'User' + db.create_table(u'core_user_groups', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm[u'core.user'], null=False)), + ('group', models.ForeignKey(orm[u'auth.group'], null=False)) + )) + db.create_unique(u'core_user_groups', ['user_id', 'group_id']) + + # Adding M2M table for field user_permissions on 'User' + db.create_table(u'core_user_user_permissions', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm[u'core.user'], null=False)), + ('permission', models.ForeignKey(orm[u'auth.permission'], null=False)) + )) + db.create_unique(u'core_user_user_permissions', ['user_id', 'permission_id']) + + # Adding model 'ObjectAccess' + db.create_table(u'core_objectaccess', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['core.User'])), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), + ('is_owner', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'core', ['ObjectAccess']) + + # Adding unique constraint on 'ObjectAccess', fields ['user', 'content_type', 'object_id'] + db.create_unique(u'core_objectaccess', ['user_id', 'content_type_id', 'object_id']) + + # Adding model 'Extension' + db.create_table(u'core_extension', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=150)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'core', ['Extension']) + + # Adding model 'Log' + db.create_table(u'core_log', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('message', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('level', self.gf('django.db.models.fields.CharField')(max_length=15)), + ('logger', self.gf('django.db.models.fields.CharField')(max_length=30)), + )) + db.send_create_signal(u'core', ['Log']) + + + def backwards(self, orm): + # Removing unique constraint on 'ObjectAccess', fields ['user', 'content_type', 'object_id'] + db.delete_unique(u'core_objectaccess', ['user_id', 'content_type_id', 'object_id']) + + # Deleting model 'User' + db.delete_table(u'core_user') + + # Removing M2M table for field groups on 'User' + db.delete_table('core_user_groups') + + # Removing M2M table for field user_permissions on 'User' + db.delete_table('core_user_user_permissions') + + # Deleting model 'ObjectAccess' + db.delete_table(u'core_objectaccess') + + # Deleting model 'Extension' + db.delete_table(u'core_extension') + + # Deleting model 'Log' + db.delete_table(u'core_log') + + + models = { + 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'] \ No newline at end of file diff --git a/modoboa/core/migrations/0002_update_permissions.py b/modoboa/core/migrations/0002_update_permissions.py new file mode 100644 index 000000000..31bc46068 --- /dev/null +++ b/modoboa/core/migrations/0002_update_permissions.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + depends_on = ( + ('admin', '0037_rename_tables'), + ) + + def forwards(self, orm): + "Write your forwards methods here." + orm['contenttypes.ContentType'].objects.filter( + app_label='admin', model__in=['user', 'extension', 'objectaccess'] + ).update(app_label='core') + + def backwards(self, orm): + "Write your backwards methods here." + orm['contenttypes.ContentType'].objects.filter( + app_label='core', model__in=['user', 'extension', 'objectaccess'] + ).update(app_label='admin') + + models = { + 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'}) + }, + u'reversion.revision': { + 'Meta': {'object_name': 'Revision'}, + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'manager_slug': ('django.db.models.fields.CharField', [], {'default': "u'default'", 'max_length': '200', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']", 'null': 'True', 'blank': 'True'}) + }, + u'reversion.version': { + 'Meta': {'object_name': 'Version'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + 'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.TextField', [], {}), + 'object_id_int': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'object_repr': ('django.db.models.fields.TextField', [], {}), + 'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['reversion.Revision']"}), + 'serialized_data': ('django.db.models.fields.TextField', [], {}), + 'type': ('django.db.models.fields.PositiveSmallIntegerField', [], {'db_index': 'True'}) + } + } + + complete_apps = ['reversion', 'core'] + symmetrical = True diff --git a/modoboa/auth/__init__.py b/modoboa/core/migrations/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from modoboa/auth/__init__.py rename to modoboa/core/migrations/__init__.py diff --git a/modoboa/core/models.py b/modoboa/core/models.py new file mode 100644 index 000000000..4af548ad3 --- /dev/null +++ b/modoboa/core/models.py @@ -0,0 +1,483 @@ +import sys +import os +import re +import hashlib +import crypt +import base64 +from random import Random +import reversion +from django.db import models +from django.dispatch import receiver +from django.utils import timezone +from django.utils.translation import ugettext as _, ugettext_lazy +from django.utils.crypto import constant_time_compare +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.contrib.auth.models import ( + UserManager, Group, AbstractBaseUser, PermissionsMixin +) +from modoboa.lib import events, md5crypt, parameters +from modoboa.lib.exceptions import PermDeniedException +from modoboa.lib.sysutils import exec_cmd +from modoboa.core.extensions import exts_pool +from modoboa.core.exceptions import AdminError + +try: + from modoboa.lib.ldaputils import * + ldap_available = True +except ImportError: + ldap_available = False + + +class User(AbstractBaseUser, PermissionsMixin): + """Custom User model. + + It overloads the way passwords are stored into the database. The + main reason to change this mechanism is to ensure the + compatibility with the way Dovecot stores passwords. + + It also adds new attributes and methods. + """ + username = models.CharField(max_length=254, unique=True) + first_name = models.CharField(max_length=30, blank=True) + last_name = models.CharField(max_length=30, blank=True) + email = models.EmailField(max_length=254, blank=True) + is_staff = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + date_joined = models.DateTimeField(default=timezone.now) + is_local = models.BooleanField(default=True) + + objects = UserManager() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email'] + + class Meta: + ordering = ["username"] + + password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)') + + def delete(self, fromuser, *args, **kwargs): + """Custom delete method + + To check permissions properly, we need to make a distinction + between 2 cases: + + * If the user owns a mailbox, the check is made on that object + (useful for domain admins) + + * Otherwise, the check is made on the user + """ + from modoboa.lib.permissions import \ + get_object_owner, grant_access_to_object, ungrant_access_to_object + + if fromuser == self: + raise AdminError(_("You can't delete your own account")) + + if not fromuser.can_access(self): + raise PermDeniedException + + owner = get_object_owner(self) + for ooentry in self.objectaccess_set.filter(is_owner=True): + if ooentry.content_object is not None: + grant_access_to_object(owner, ooentry.content_object, True) + + events.raiseEvent("AccountDeleted", self, fromuser, **kwargs) + ungrant_access_to_object(self) + super(User, self).delete() + + def _crypt_password(self, raw_value): + scheme = parameters.get_admin("PASSWORD_SCHEME") + if type(raw_value) is unicode: + raw_value = raw_value.encode("utf-8") + if scheme == "crypt": + salt = "".join(Random().sample(string.letters + string.digits, 2)) + result = crypt.crypt(raw_value, salt) + prefix = "{CRYPT}" + elif scheme == "md5": + obj = hashlib.md5(raw_value) + result = obj.hexdigest() + prefix = "{MD5}" + # The md5crypt scheme is the only supported method that has both: + # (a) a salt ("crypt" has this too), + # (b) supports passwords lengths of more than 8 characters (all except + # "crypt"). + 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.) + salt = "".join(Random().sample(string.letters + string.digits, 6)) + result = md5crypt(raw_value, salt) + prefix = "" # the result already has $1$ prepended to it + # to signify what this is + elif scheme == "sha256": + obj = hashlib.sha256(raw_value) + result = base64.b64encode(obj.digest()) + prefix = "{SHA256}" + else: + scheme = "plain" + result = raw_value + prefix = "{PLAIN}" + return "%s%s" % (prefix, result) + + def set_password(self, raw_value, curvalue=None): + """Password update + + Update the current mailbox's password with the given clear + value. This value is encrypted according to the defined method + before it is saved. + + :param raw_value: the new password's value + :param curvalue: the current password (for LDAP authentication) + """ + if parameters.get_admin("AUTHENTICATION_TYPE") == "local": + self.password = self._crypt_password(raw_value) + else: + if not ldap_available: + raise AdminError( + _("Failed to update password: LDAP module not installed") + ) + + ab = LDAPAuthBackend() + try: + ab.update_user_password(self.username, curvalue, raw_value) + except LDAPException, e: + raise AdminError(_("Failed to update password: %s" % str(e))) + events.raiseEvent( + "PasswordUpdated", self, raw_value, self.pk is None + ) + + def check_password(self, raw_value): + m = self.password_expr.match(self.password) + if m is None: + return False + if type(raw_value) is unicode: + raw_value = raw_value.encode("utf-8") + scheme = (m.group(2) or m.group(3)).lower() + val2 = m.group(4) + if scheme == u"crypt": + val1 = crypt.crypt(raw_value, val2) + elif scheme == u"md5": + val1 = hashlib.md5(raw_value).hexdigest() + elif scheme == u"sha256": + val1 = base64.b64encode(hashlib.sha256(raw_value).digest()) + elif scheme == u"$1$": # md5crypt + salt, hashed = val2.split('$') + val1 = md5crypt(raw_value, str(salt)) + val2 = self.password # re-add scheme for comparison below + else: + val1 = raw_value + return constant_time_compare(val1, val2) + + @property + def tags(self): + return [{"name": "account", "label": _("account"), "type": "idt"}, + {"name": self.group, "label": self.group, + "type": "grp", "color": "info"}] + + @property + def fullname(self): + if self.first_name != u"": + return u"%s %s" % (self.first_name, self.last_name) + return self.username + + @property + def identity(self): + return self.username + + @property + def name_or_rcpt(self): + if self.first_name != "": + return "%s %s" % (self.first_name, self.last_name) + return "----" + + @property + def group(self): + if self.is_superuser: + return "SuperAdmins" + try: + return self.groups.all()[0].name + except IndexError: + return "SimpleUsers" + + @property + def enabled(self): + return self.is_active + + @property + def encoded_address(self): + from email.header import Header + if self.first_name != "" or self.last_name != "": + return "%s <%s>" % \ + (Header(self.fullname, 'utf8').encode(), self.email) + return self.email + + def belongs_to_group(self, name): + """Simple shortcut to check if this user is a member of a + specific group. + + :param name: the group's name + :return: a boolean + """ + try: + self.groups.get(name=name) + except Group.DoesNotExist: + return False + return True + + def is_owner(self, obj): + """Tell is the user is the unique owner of this object + + :param obj: an object inheriting from ``models.Model`` + :return: a boolean + """ + ct = ContentType.objects.get_for_model(obj) + try: + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) + except ObjectAccess.DoesNotExist: + return False + return ooentry.is_owner + + def can_access(self, obj): + """Check if the user can access a specific object + + This function is recursive : if the given user hasn't got direct + access to this object and if he has got access other ``User`` + objects, we check if one of those users owns the object. + + :param obj: a admin object + :return: a boolean + """ + if self.is_superuser: + return True + + ct = ContentType.objects.get_for_model(obj) + try: + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) + except ObjectAccess.DoesNotExist: + pass + else: + return True + if ct.model == "user": + return False + + ct = ContentType.objects.get_for_model(self) + qs = self.objectaccess_set.filter(content_type=ct) + for ooentry in qs.all(): + if ooentry.content_object.is_owner(obj): + return True + return False + + def set_role(self, role): + """Set administrative role for this account + + :param string role: the role to set + """ + if role is None or self.group == role: + return + events.raiseEvent("RoleChanged", self, role) + self.groups.clear() + if role == "SuperAdmins": + self.is_superuser = True + else: + if self.is_superuser: + ObjectAccess.objects.filter(user=self).delete() + self.is_superuser = False + try: + self.groups.add(Group.objects.get(name=role)) + except Group.DoesNotExist: + self.groups.add(Group.objects.get(name="SimpleUsers")) + if self.group != "SimpleUsers" and not self.can_access(self): + from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(self, self) + self.save() + + def post_create(self, creator): + from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(creator, self, is_owner=True) + events.raiseEvent("AccountCreated", self) + + def save(self, *args, **kwargs): + if "creator" in kwargs: + creator = kwargs["creator"] + del kwargs["creator"] + else: + creator = None + super(User, self).save(*args, **kwargs) + if creator is not None: + self.post_create(creator) + + def from_csv(self, user, row, crypt_password=True): + """Create a new account from a CSV file entry + + The expected order is the following:: + + "account", loginname, password, first name, last name, enabled, group, address[, domain, ...] + + :param user: a ``core.User`` instance + :param row: a list containing the expected information + :param crypt_password: + """ + if len(row) < 7: + raise AdminError(_("Invalid line")) + role = row[6].strip() + if not user.is_superuser and not role in ["SimpleUsers", "DomainAdmins"]: + raise PermDeniedException( + _("You can't import an account with a role greater than yours") + ) + self.username = row[1].strip() + if role == "SimpleUsers": + if (len(row) < 8 or not row[7].strip()): + raise AdminError( + _("The simple user '%s' must have a valid email address" % self.username) + ) + if self.username != row[7].strip(): + raise AdminError( + _("username and email fields must not differ for '%s'" % self.username) + ) + + if crypt_password: + self.set_password(row[2].strip()) + else: + self.password = row[2].strip() + self.first_name = row[3].strip() + self.last_name = row[4].strip() + self.is_active = (row[5].strip() == 'True') + self.save(creator=user) + self.set_role(role) + if len(row) < 8: + return + events.raiseEvent("AccountImported", user, self, row[7:]) + + def to_csv(self, csvwriter): + row = ["account", self.username.encode("utf-8"), self.password.encode("utf-8"), + self.first_name.encode("utf-8"), self.last_name.encode("utf-8"), + self.is_active, self.group, self.email.encode("utf-8")] + row += events.raiseQueryEvent("AccountExported", self) + csvwriter.writerow(row) + +reversion.register(User) + + +def populate_callback(user): + """Populate callback + + If the LDAP authentication backend is in use, this callback will + be called each time a new user authenticates succesfuly to + Modoboa. This function is in charge of creating the mailbox + associated to the provided ``User`` object. + + :param user: a ``User`` instance + """ + from modoboa.lib.permissions import grant_access_to_object + + sadmins = User.objects.filter(is_superuser=True) + user.set_role("SimpleUsers") + user.post_create(sadmins[0]) + for su in sadmins[1:]: + grant_access_to_object(su, user) + events.raiseEvent("AccountAutoCreated", user) + + +class ObjectAccess(models.Model): + user = models.ForeignKey(User) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + is_owner = models.BooleanField(default=False) + + class Meta: + unique_together = (("user", "content_type", "object_id"),) + + def __unicode__(self): + return "%s => %s (%s)" % (self.user, self.content_object, self.content_type) + + +class Extension(models.Model): + name = models.CharField(max_length=150) + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to enable this extension") + ) + + def __init__(self, *args, **kwargs): + super(Extension, self).__init__(*args, **kwargs) + + def __unicode__(self): + return self.name + + def __get_ext_instance(self): + if not self.name: + return None + if hasattr(self, "instance") and self.instance: + return + self.instance = exts_pool.get_extension(self.name) + if self.instance: + self.__get_ext_dir() + + def __get_ext_dir(self): + modname = self.instance.__module__ + path = os.path.realpath(sys.modules[modname].__file__) + self.extdir = os.path.dirname(path) + + def on(self): + self.enabled = True + self.save() + + self.__get_ext_instance() + self.instance.load() + self.instance.init() + + if self.instance.needs_media: + path = os.path.join(settings.MEDIA_ROOT, self.name) + exec_cmd("mkdir %s" % path) + + events.raiseEvent("ExtEnabled", self) + + def off(self): + self.__get_ext_instance() + if self.instance is None: + return + self.instance.destroy() + + self.enabled = False + self.save() + + if self.instance.needs_media: + path = os.path.join(settings.MEDIA_ROOT, self.name) + exec_cmd("rm -r %s" % path) + + events.raiseEvent("ExtDisabled", self) + +reversion.register(Extension) + + +class Log(models.Model): + date_created = models.DateTimeField(auto_now_add=True) + message = models.CharField(max_length=255) + level = models.CharField(max_length=15) + logger = models.CharField(max_length=30) + + +@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: + action = _("added") + level = "info" + elif version.type == reversion.models.VERSION_CHANGE: + 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) diff --git a/modoboa/userprefs/static/userprefs/css/userprefs.css b/modoboa/core/static/core/css/userprefs.css similarity index 100% rename from modoboa/userprefs/static/userprefs/css/userprefs.css rename to modoboa/core/static/core/css/userprefs.css diff --git a/modoboa/core/tables.py b/modoboa/core/tables.py new file mode 100644 index 000000000..e35868cab --- /dev/null +++ b/modoboa/core/tables.py @@ -0,0 +1,14 @@ +from django.utils.translation import ugettext_lazy +from modoboa.lib import tables + + +class ExtensionsTable(tables.Table): + idkey = "id" + selection = tables.SelectionColumn("selection", width="4%", header=False) + label = tables.Column("label", label=ugettext_lazy("Name"), width="15%") + version = tables.Column( + "version", label=ugettext_lazy("Version"), width="6%" + ) + descr = tables.Column("description", label=ugettext_lazy("Description")) + + cols_order = ["selection", "label", "version", "descr"] diff --git a/modoboa/admin/templates/admin/extensions.html b/modoboa/core/templates/core/extensions.html similarity index 68% rename from modoboa/admin/templates/admin/extensions.html rename to modoboa/core/templates/core/extensions.html index a419d20ad..276524756 100644 --- a/modoboa/admin/templates/admin/extensions.html +++ b/modoboa/core/templates/core/extensions.html @@ -1,6 +1,6 @@ {% load i18n %}{% load url from future %}