diff --git a/admin/migrations/0003_auto__add_extension.py b/admin/migrations/0003_auto__add_extension.py new file mode 100644 index 000000000..d47308463 --- /dev/null +++ b/admin/migrations/0003_auto__add_extension.py @@ -0,0 +1,100 @@ +# encoding: 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 'Extension' + db.create_table('admin_extension', ( + ('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, blank=True)), + )) + db.send_create_signal('admin', ['Extension']) + + + def backwards(self, orm): + + # Deleting model 'Extension' + db.delete_table('admin_extension') + + + models = { + 'admin.alias': { + 'Meta': {'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'full_address': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'object_name': 'Domain'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'full_address': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'gid': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}), + 'uid': ('django.db.models.fields.IntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + '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'}) + } + } + + complete_apps = ['admin'] diff --git a/admin/models.py b/admin/models.py index 92c2b1618..0639b7596 100644 --- a/admin/models.py +++ b/admin/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ +from django.conf import settings from modoboa.lib import parameters from modoboa.lib import exec_cmd, exec_as_vuser import os @@ -112,3 +113,56 @@ class Alias(models.Model): ("view_aliases", "View aliases"), ) +class Extension(models.Model): + name = models.CharField(max_length=150) + enabled = models.BooleanField(_('enabled'), + help_text=_("Check to enable this extension")) + + def __getmodule(self): + extname = "modoboa.extensions.%s" % self.name + return __import__(extname, globals(), locals(), ['main']) + + def load(self): + self.__getmodule().main.init() + + def unload(self): + self.__getmodule().main.destroy() + + def on(self): + self.load() + + extdir = "%s/extensions/%s" % (settings.MODOBOA_DIR, self.name) + scriptdir = "%s/%s" % (extdir, "scripts") + if os.path.exists(scriptdir): + targetdir = os.path.join(settings.MODOBOA_DIR, "scripts") + exec_cmd("ln -s %s %s/%s" % (scriptdir, targetdir, self.name)) + + staticpath = "%s/%s" % (extdir, "static") + if os.path.exists(staticpath): + for d in ['css', 'js']: + subpath = "%s/%s" % (staticpath, d) + if not os.path.exists(subpath): + continue + exec_cmd("ln -s %s %s/static/%s/%s" % (subpath, settings.MODOBOA_DIR, + d, self.name)) + + def off(self): + self.unload() + + extdir = "%s/extensions/%s" % (settings.MODOBOA_DIR, self.name) + scriptdir = "%s/%s" % (extdir, "scripts") + if os.path.exists(scriptdir): + target = "%s/scripts/%s" % (settings.MODOBOA_DIR, self.name) + exec_cmd("rm %s" % target) + + staticpath = "%s/%s" % (extdir, "static") + if os.path.exists(staticpath): + for d in ['css', 'js']: + subpath = "%s/static/%s/%s" % (settings.MODOBOA_DIR, d, self.name) + exec_cmd("rm -f %s" % subpath) + + + + + + diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index fcba18626..675294762 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -62,6 +62,9 @@ def settings_menu(selection, user): "url" : reverse(admin.views.viewparameters), "img" : static_url("pics/domains.png"), "label" : _("Parameters")}, + {"name" : "extensions", + "url" : reverse(admin.views.viewextensions), + "label" : _("Extensions")}, ] return render_to_string('common/menu.html', {"selection" : selection, "entries" : entries, diff --git a/admin/urls.py b/admin/urls.py index c0a362d26..8ffe45754 100644 --- a/admin/urls.py +++ b/admin/urls.py @@ -21,4 +21,6 @@ urlpatterns = patterns('modoboa.admin.views', (r'^settings/delperm/(?P\d+)/(?P\w+)/$', 'deletepermission'), (r'^settings/parameters/$', 'viewparameters'), (r'^settings/parameters/save/$', 'saveparameters'), + (r'^settings/extensions/$', 'viewextensions'), + (r'^settings/extensions/save/$', 'saveextensions'), ) diff --git a/admin/views.py b/admin/views.py index 65edc96c3..81226fdf0 100644 --- a/admin/views.py +++ b/admin/views.py @@ -14,7 +14,7 @@ from django.contrib.auth.models import User, Group from django.contrib import messages from modoboa import admin, userprefs -from modoboa.admin.models import Domain, Mailbox, Alias +from modoboa.admin.models import * from forms import MailboxForm, DomainForm, AliasForm, PermissionForm from modoboa.lib.authbackends import crypt_password from modoboa.lib import _render, _ctx_ok, _ctx_ko, getctx @@ -205,7 +205,7 @@ def newmailbox(request, dom_id=None): return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") form = MailboxForm() - return render_to_response('admin/newmailbox.html', { + return _render(request, "admin/newmailbox.html", { "domain" : domain, "form" : form }) @@ -445,3 +445,58 @@ def saveparameters(request): parameters.save_admin(name, v, app=app) ctx = getctx("ok") return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def viewextensions(request): + from extensions import list_extensions + from lib import tables + + class ExtensionsTable(tables.Table): + idkey = "id" + selection = tables.SelectionColumn("selection", width="4%", first=True) + _1_name = tables.Column("name", label=_("Name"), width="15%") + _2_version = tables.Column("version", label=_("Version"), width="6%") + _3_descr = tables.Column("description", label=_("Description")) + + exts = list_extensions() + 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(exts) + return _render(request, 'admin/extensions.html', { + "extensions" : tbl.render(request) + }) + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def saveextensions(request): + import extensions + + actived_exts = Extension.objects.filter(enabled=True) + found = [] + for k, v in request.POST.iteritems(): + if k.startswith("select_"): + parts = k.split("_", 1) + dbext = Extension.objects.get(name=parts[1]) + if not dbext in actived_exts: + dbext.on() + dbext.enabled = True + dbext.save() + else: + found += [dbext] + for ext in actived_exts: + if not ext in found: + ext.off() + ext.enabled = False + ext.save() + messages.info(request, _("Modifications applied."), fail_silently=True) + return HttpResponseRedirect(reverse(admin.views.viewextensions)) diff --git a/extensions/__init__.py b/extensions/__init__.py index 49eba9bf7..1f3eedac0 100644 --- a/extensions/__init__.py +++ b/extensions/__init__.py @@ -2,6 +2,10 @@ import os import re from django.conf import settings +from modoboa.admin.models import Extension + +def isinstalled(ext): + return "modoboa.extensions.%s" % ext in settings.INSTALLED_APPS def loadextensions(): basedir = "%s/extensions" % settings.MODOBOA_DIR @@ -9,15 +13,53 @@ def loadextensions(): for f in os.listdir(basedir): if not os.path.isdir("%s/%s" % (basedir, f)): continue - if not isenabled(f): + if not isinstalled(f): continue module = __import__(f, globals(), locals(), ['main']) - module.main.init() + try: + ext = Extension.objects.get(name=f) + if ext.enabled: + module.main.init() + except Extension.DoesNotExist: + pass u = module.main.urls() if u == (): continue result += [u] return result -def isenabled(ext): - return "modoboa.extensions.%s" % ext in settings.INSTALLED_APPS +def list_extensions(): + basedir = "extensions" + result = [] + for d in os.listdir(basedir): + if not os.path.isdir("%s/%s" % (basedir, d)): + continue + if not isinstalled(d): + continue + isvalid = True + for f in ['urls.py', 'main.py', '__init__.py', 'views.py']: + if not os.path.exists("%s/%s/%s" % (basedir, d, f)): + isvalid = False + break + if isvalid: + module = __import__(d, globals(), locals(), ['main']) + try: + infos = module.main.infos() + infos["id"] = d + if os.path.isdir("%s/%s/templates" % (basedir, d)): + infos["templates"] = True + else: + infos["templates"] = False + result += [infos] + except AttributeError: + pass + return result + +def loadmenus(): + result = [] + for name, mod in extensions.iteritems(): + u = mod.main.urls() + if u == (): + continue + result += (u,) + return result diff --git a/extensions/amavis_quarantine/templates/amavis_quarantine/index.html b/extensions/amavis_quarantine/templates/amavis_quarantine/index.html index d401cec12..b926d19d6 100644 --- a/extensions/amavis_quarantine/templates/amavis_quarantine/index.html +++ b/extensions/amavis_quarantine/templates/amavis_quarantine/index.html @@ -1,7 +1,7 @@ {% extends "common/email_listing.html" %} {% load i18n %} {% block css %} - + {% endblock %} {% block js %} mysubmit = function(event, name) { diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index e3c142ec6..2852c37d2 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -1,7 +1,7 @@ {% extends "common/email_listing.html" %} {% load i18n %} {% block css %} - + {% endblock %} {% block js %} quota_pb = new ProgressBar({ diff --git a/lib/__init__.py b/lib/__init__.py index bb8dd1c3f..b851d81af 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -77,6 +77,11 @@ def is_not_localadmin(errortpl="error"): % {"user" : request.user.username}) }) return f(request, *args, **kwargs) + + wrapped_f.__name__ = f.__name__ + wrapped_f.__dict__ = f.__dict__ + wrapped_f.__doc__ = f.__doc__ + wrapped_f.__module__ = f.__module__ return wrapped_f return dec diff --git a/lib/middleware.py b/lib/middleware.py new file mode 100644 index 000000000..49f7c534c --- /dev/null +++ b/lib/middleware.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +import re +from django.http import Http404 +from modoboa.admin.models import Extension + +class ExtControlMiddleware(object): + def process_view(self, request, view, args, kwargs): + m = re.match("modoboa\.extensions\.(\w+)", view.__module__) + if m is None: + return None + ext = Extension.objects.get(name=m.group(1)) + if ext is None: + return None + if ext.enabled: + return None + raise Http404 + + diff --git a/settings_sample.py b/settings_sample.py index 30ba6b717..6ebd25ed1 100644 --- a/settings_sample.py +++ b/settings_sample.py @@ -66,7 +66,8 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware' + 'django.contrib.messages.middleware.MessageMiddleware', + 'modoboa.lib.middleware.ExtControlMiddleware', ) ROOT_URLCONF = 'modoboa.urls' @@ -91,7 +92,10 @@ INSTALLED_APPS = ( 'modoboa.userprefs', # Modoboa extensions here. - # 'modoboa.extensions.postfix_autoreply' + 'modoboa.extensions.postfix_autoreply', + 'modoboa.extensions.webmail', + 'modoboa.extensions.stats', + 'modoboa.extensions.amavis_quarantine', ) AUTHENTICATION_BACKENDS = ( diff --git a/templates/admin/extensions.html b/templates/admin/extensions.html new file mode 100644 index 000000000..7b86890c7 --- /dev/null +++ b/templates/admin/extensions.html @@ -0,0 +1,13 @@ +{% extends "admin/settings_header.html" %} +{% block menu %} +{% load admin_extras %} +{% settings_menu "extensions" user %} +{% endblock %} +{% block setcontent %} +{% load i18n %} +
+ {{ extensions }} +
+ +
+{% endblock %} diff --git a/urls.py b/urls.py index 74a1f22eb..c74fea294 100644 --- a/urls.py +++ b/urls.py @@ -19,12 +19,5 @@ if settings.DEBUG: (r'^static/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}) ) - # Stats - if isenabled('stats'): - urlpatterns += patterns( - '', - (r'^img/stats/(?P.*)/$', 'django.views.static.serve', - {'document_root': parameters.get_admin("IMG_ROOTDIR", app="stats")}) - )