wip: Big refacto

The admin application is now a true extension.
This commit is contained in:
Antoine Nguyen
2013-08-16 16:34:15 +02:00
parent 937e09c723
commit 1155532365
100 changed files with 1795 additions and 2731 deletions
-32
View File
@@ -1,32 +0,0 @@
# coding: utf-8
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.extensions import ModoExtension, exts_pool
from modoboa.lib import parameters, events
class AdminConsole(ModoExtension):
name = "admin"
label = ugettext_lazy("Administration console")
version = "1.0"
description = ugettext_lazy("Web based console to manage domains, accounts and aliases")
always_active = True
def load(self):
from app_settings import GeneralParametersForm
parameters.register(GeneralParametersForm, ugettext_lazy("General"))
def destroy(self):
parameters.unregister()
exts_pool.register_extension(AdminConsole, show=False)
@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")
-291
View File
@@ -1,291 +0,0 @@
from django import forms
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 import parameters
from modoboa.auth.lib import random_key
def enabled_applications():
"""Return the list of currently enabled extensions
We check if the table exists before trying to fetch activated
extensions because the admin module is always imported by Django,
even before the database exists (example: the first ``syncdb``).
:return: a list
"""
from modoboa.admin.models import Extension
from modoboa.lib.dbutils import db_table_exists
result = [("userprefs", "userprefs")]
if db_table_exists("admin_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"
sep1 = SeparatorField(label=ugettext_lazy("Authentication"))
authentication_type = forms.ChoiceField(
label=ugettext_lazy("Authentication type"),
choices=[('local', ugettext_lazy("Local")),
('ldap', "LDAP")],
initial="local",
help_text=ugettext_lazy("The backend used for authentication"),
widget=InlineRadioSelect
)
password_scheme = forms.ChoiceField(
label=ugettext_lazy("Default password scheme"),
choices=[("crypt", "crypt"),
("md5", "md5"),
("md5crypt", "md5crypt"),
("sha256", "sha256"),
("plain", "plain")],
initial="md5crypt",
help_text=ugettext_lazy("Scheme used to crypt mailbox passwords")
)
secret_key = forms.CharField(
label=ugettext_lazy("Secret key"),
initial=random_key(),
help_text=ugettext_lazy("Key used to encrypt/decrypt passwords")
)
# LDAP specific settings
ldap_sep = SeparatorField(label=ugettext_lazy("LDAP settings"))
ldap_server_address = forms.CharField(
label=ugettext_lazy("Server address"),
initial="localhost",
help_text=ugettext_lazy("The IP address of the DNS name of the LDAP server")
)
ldap_server_port = forms.IntegerField(
label=ugettext_lazy("Server port"),
initial=389,
help_text=ugettext_lazy("The TCP port number used by the LDAP server")
)
ldap_secured = YesNoField(
label=ugettext_lazy("Use a secured connection"),
initial="no",
help_text=ugettext_lazy("Use an SSL/TLS connection to access the LDAP server")
)
ldap_auth_method = forms.ChoiceField(
label=ugettext_lazy("Authentication method"),
choices=[('searchbind', ugettext_lazy("Search and bind")),
('directbind', ugettext_lazy("Direct bind"))],
initial='searchbind',
help_text=ugettext_lazy("Choose the authentication method to use"),
widget=InlineRadioSelect
)
ldap_bind_dn = forms.CharField(
label=ugettext_lazy("Bind DN"),
initial='',
help_text=ugettext_lazy("The distinguished name to use when binding to the LDAP server. Leave empty for an anonymous bind"),
required=False
)
ldap_bind_password = forms.CharField(
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),
required=False
)
ldap_search_base = forms.CharField(
label=ugettext_lazy("Search base"),
initial="",
help_text=ugettext_lazy("The distinguished name of the search base"),
required=False
)
ldap_search_filter = forms.CharField(
label=ugettext_lazy("Search filter"),
initial="(mail=%(user)s)",
help_text=ugettext_lazy("An optional filter string (e.g. '(objectClass=person)'). In order to be valid, it must be enclosed in parentheses."),
required=False
)
ldap_user_dn_template = forms.CharField(
label=ugettext_lazy("User DN template"),
initial="",
help_text=ugettext_lazy("The template used to construct a user's DN. It should contain one placeholder (ie. %(user)s)"),
required=False
)
ldap_password_attribute = forms.CharField(
label=ugettext_lazy("Password attribute"),
initial="userPassword",
help_text=ugettext_lazy("The attribute used to store user passwords")
)
ldap_is_active_directory = YesNoField(
label=ugettext_lazy("Active Directory"),
initial="no",
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(
label=ugettext_lazy("Maximum log record age"),
initial=365,
help_text=ugettext_lazy("The maximum age in days of a log record")
)
items_per_page = forms.IntegerField(
label=ugettext_lazy("Items per page"),
initial=30,
help_text=ugettext_lazy("Number of displayed items per page")
)
default_top_redirection = forms.ChoiceField(
label=ugettext_lazy("Default top redirection"),
choices=[],
initial="userprefs",
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",
"ldap_secured": "authentication_type=ldap",
"ldap_auth_method": "authentication_type=ldap",
"ldap_bind_dn": "ldap_auth_method=searchbind",
"ldap_bind_password": "ldap_auth_method=searchbind",
"ldap_search_base": "ldap_auth_method=searchbind",
"ldap_search_filter": "ldap_auth_method=searchbind",
"ldap_user_dn_template": "ldap_auth_method=directbind",
"ldap_password_attribute": "authentication_type=ldap",
"ldap_is_active_directory": "authentication_type=ldap"
}
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"]
try:
test = tpl % {"user": "toto"}
except ValueError:
raise forms.ValidationError(_("Invalid syntax"))
return tpl
def clean(self):
"""Custom validation method
Depending on 'ldap_auth_method' value, we check for different
required parameters.
"""
super(GeneralParametersForm, self).clean()
if len(self._errors):
raise forms.ValidationError(self._errors)
cleaned_data = self.cleaned_data
if cleaned_data["authentication_type"] != "ldap":
return cleaned_data
if cleaned_data["ldap_auth_method"] == "searchbind":
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")])
return cleaned_data
def to_django_settings(self):
"""Apply LDAP related parameters to Django settings
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
ldap_available = True
except ImportError:
ldap_available = False
values = self.get_current_values()
if not ldap_available or values["authentication_type"] != "ldap":
return
if not hasattr(settings, "AUTH_LDAP_USER_ATTR_MAP"):
setattr(settings, "AUTH_LDAP_USER_ATTR_MAP", {
"first_name": "givenName",
"email": "mail",
"last_name": "sn"
})
ldap_uri = 'ldaps://' if values["ldap_secured"] == "yes" else "ldap://"
ldap_uri += "%s:%s" % (values["ldap_server_address"], values["ldap_server_port"])
setattr(settings, "AUTH_LDAP_SERVER_URI", ldap_uri)
if values["ldap_auth_method"] == "searchbind":
setattr(settings, "AUTH_LDAP_BIND_DN", values["ldap_bind_dn"])
setattr(settings, "AUTH_LDAP_BIND_PASSWORD", values["ldap_bind_password"])
search = LDAPSearch(
values["ldap_search_base"], ldap.SCOPE_SUBTREE,
values["ldap_search_filter"]
)
setattr(settings, "AUTH_LDAP_USER_SEARCH", search)
else:
setattr(settings, "AUTH_LDAP_USER_DN_TEMPLATE", values["ldap_user_dn_template"])
if values["ldap_is_active_directory"] == "yes":
if not hasattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS"):
setattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS", {
ldap.OPT_REFERRALS: False
})
else:
settings.AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_REFERRALS] = False
File diff suppressed because it is too large Load Diff
-889
View File
@@ -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:
<ul>
<li><em>account; loginname; password; first name; last name; enabled; group; address[, domain, ...]</em></li>
<li><em>alias; address; enabled; internal recipient</em></li>
<li><em>forward; address; enabled; external recipient</em></li>
<li><em>dlist; address; enabled; recipient; recipient; ...</em></li>
</ul>
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
<p>You can use a different character as separator.</p>
""")
ctx = dict(
title=_("Import identities"),
action_label=_("Import"),
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:
<ul>
<li><em>domain; name; quota; enabled</em></li>
<li><em>domainalias; name; targeted domain; enabled</em></li>
</ul>
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
<p>You can use a different character as separator.</p>
""")
ctx = dict(
title=_("Import 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)
+12 -1
View File
@@ -1,8 +1,19 @@
from django.utils.translation import ugettext_lazy
from modoboa.lib import parameters
from modoboa.lib import parameters, events
def load_settings():
from app_settings import GeneralParametersForm, UserSettings
parameters.register(GeneralParametersForm, ugettext_lazy("General"))
parameters.register(UserSettings, ugettext_lazy("General"))
@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")
+1 -1
View File
@@ -153,7 +153,7 @@ class GeneralParametersForm(parameters.AdminParametersForm):
default_top_redirection = forms.ChoiceField(
label=ugettext_lazy("Default top redirection"),
choices=[],
initial="admin",
initial="core",
help_text=ugettext_lazy("The default redirection used when no application is specified")
)
+7 -3
View File
@@ -12,15 +12,19 @@ class ModoExtension(object):
considered as valid.
"""
name = None
version = 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)
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
+1
View File
@@ -50,6 +50,7 @@ class User(AbstractBaseUser, PermissionsMixin):
REQUIRED_FIELDS = ['email']
class Meta:
db_table = "admin_user"
ordering = ["username"]
password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)')
+30
View File
@@ -42,6 +42,36 @@ def core_menu(selection, user):
)
@register.simple_tag
def settings_menu(selection, user):
entries = [
{"name": "extensions",
"class": "ajaxlink",
"url": "extensions/",
"label": _("Extensions"),
"img": ""},
{"name": "info",
"class": "ajaxlink",
"url": "info/",
"label": _("Information")},
{"name": "logs",
"class": "ajaxlink",
"url": "logs/",
"label": _("Logs")},
{"name": "parameters",
"class": "ajaxlink",
"url": "parameters/",
"img": "",
"label": _("Parameters")},
]
return render_to_string('common/menu.html', {
"entries": entries,
"css": "nav nav-list",
"selection": selection,
"user": user
})
@register.simple_tag
def extensions_menu(selection, user):
menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user)
+21
View File
@@ -0,0 +1,21 @@
# coding: utf-8
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.core.extensions import ModoExtension, exts_pool
from modoboa.lib import parameters, events
class AdminConsole(ModoExtension):
name = "admin"
label = ugettext_lazy("Administration console")
version = "1.0"
description = ugettext_lazy("Web based console to manage domains, accounts and aliases")
always_active = True
def load(self):
from app_settings import AdminParametersForm
parameters.register(AdminParametersForm, ugettext_lazy("Administration"))
def destroy(self):
parameters.unregister()
exts_pool.register_extension(AdminConsole, show=False)
+48
View File
@@ -0,0 +1,48 @@
from django import forms
from django.utils.translation import ugettext_lazy
from modoboa.lib.formutils import YesNoField, SeparatorField, InlineRadioSelect
from modoboa.lib.sysutils import exec_cmd
from modoboa.lib import parameters
class AdminParametersForm(parameters.AdminParametersForm):
app = "admin"
mbsep = 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"),
)
# Visibility rules
visibility_rules = {
"mailboxes_owner": "handle_mailboxes=yes",
}
def __init__(self, *args, **kwargs):
super(AdminParametersForm, self).__init__(*args, **kwargs)
hide_fields = False
try:
code, version = exec_cmd("dovecot --version")
except OSError:
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"]
@@ -3,13 +3,16 @@ from django import forms
from django.contrib.auth.models import Group
from django.utils.translation import ugettext as _, ugettext_lazy
from django.http import QueryDict
from modoboa.admin.templatetags.admin_extras import gender
from modoboa.admin.exceptions import AdminError
from modoboa.lib import events, parameters
from modoboa.lib.emailutils import split_mailbox
from modoboa.lib.permissions import get_account_roles
from modoboa.lib.formutils import *
from modoboa.admin.models import User, Domain, DomainAlias, Mailbox, Alias
from modoboa.core.models import User
from modoboa.extensions.admin.templatetags.admin_tags import gender
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, DomainAlias, Mailbox, Alias
)
class DomainFormGeneral(forms.ModelForm, DynamicForm):
@@ -0,0 +1,8 @@
from .domain import Domain
from .domain_alias import DomainAlias
from .mailbox import Mailbox, Quota
from .alias import Alias
__all__ = [
'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias'
]
+195
View File
@@ -0,0 +1,195 @@
import reversion
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events
from modoboa.lib.emailutils import split_mailbox
from modoboa.lib.exceptions import PermDeniedException
from modoboa.extensions.admin.exceptions import AdminError
from .base import DatesAware
from .domain import Domain
from .mailbox import Mailbox
class Alias(DatesAware):
address = models.CharField(
ugettext_lazy('address'), max_length=254,
help_text=ugettext_lazy("The alias address (without the domain part). For a 'catch-all' address, just enter an * character.")
)
domain = models.ForeignKey(Domain)
mboxes = models.ManyToManyField(
Mailbox, verbose_name=ugettext_lazy('mailboxes'),
help_text=ugettext_lazy("The mailboxes this alias points to")
)
aliases = models.ManyToManyField(
'Alias',
help_text=ugettext_lazy("The aliases this alias points to")
)
extmboxes = models.TextField(blank=True)
enabled = models.BooleanField(
ugettext_lazy('enabled'),
help_text=ugettext_lazy("Check to activate this alias")
)
class Meta:
permissions = (
("view_aliases", "View aliases"),
)
unique_together = (("address", "domain"),)
ordering = ["domain__name", "address"]
def __unicode__(self):
return self.full_address
@property
def full_address(self):
return "%s@%s" % (self.address, self.domain.name)
@property
def identity(self):
return self.full_address
@property
def name_or_rcpt(self):
rcpts_count = self.get_recipients_count()
if not rcpts_count:
return "---"
rcpts = self.get_recipients()
if rcpts_count > 1:
return "%s, ..." % rcpts[0]
return rcpts[0]
@property
def type(self):
cpt = self.get_recipients_count()
if cpt > 1:
return "dlist"
if self.extmboxes != "":
return "forward"
return "alias"
@property
def tags(self):
labels = {
"dlist": _("distribution list"),
"forward": _("forward"),
"alias": _("alias")
}
altype = self.type
return [{"name": altype, "label": labels[altype], "type": "idt"}]
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("MailboxAliasCreated", creator, self)
if creator.is_superuser:
for admin in self.domain.admins:
grant_access_to_object(admin, self)
def save(self, int_rcpts, ext_rcpts, *args, **kwargs):
if len(ext_rcpts):
self.extmboxes = ",".join(ext_rcpts)
else:
self.extmboxes = ""
if "creator" in kwargs:
creator = kwargs["creator"]
del kwargs["creator"]
else:
creator = None
super(Alias, self).save(*args, **kwargs)
if creator is not None:
self.post_create(creator)
curaliases = self.aliases.all()
curmboxes = self.mboxes.all()
for t in int_rcpts:
if isinstance(t, Alias):
if not t in curaliases:
self.aliases.add(t)
continue
if not t in curmboxes:
self.mboxes.add(t)
for t in curaliases:
if not t in int_rcpts:
self.aliases.remove(t)
for t in curmboxes:
if not t in int_rcpts:
self.mboxes.remove(t)
def delete(self):
from modoboa.lib.permissions import ungrant_access_to_object
events.raiseEvent("MailboxAliasDeleted", self)
ungrant_access_to_object(self)
super(Alias, self).delete()
def get_recipients(self):
"""Return the recipients list
Internal and external addresses are mixed into a single list.
"""
result = [al.full_address for al in self.aliases.all()]
result += [mb.full_address for mb in self.mboxes.all()]
if self.extmboxes != "":
result += self.extmboxes.split(',')
return result
def get_recipients_count(self):
total = 0
if self.extmboxes != "":
total += len(self.extmboxes.split(','))
return total + self.aliases.count() + self.mboxes.count()
def ui_disabled(self, user):
if user.is_superuser:
return False
for mb in self.mboxes.all():
if not user.is_owner(mb.domain):
return True
return False
def from_csv(self, user, row, expected_elements=5):
"""Create a new alias from a CSV file entry
"""
if len(row) < expected_elements:
raise AdminError(_("Invalid line: %s" % row))
localpart, domname = split_mailbox(row[1].strip())
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise AdminError(_("Domain '%s' does not exist" % domname))
if not user.can_access(domain):
raise PermDeniedException
self.address = localpart
self.domain = domain
self.enabled = (row[2].strip() == 'True')
int_rcpts = []
ext_rcpts = []
for rcpt in row[3:]:
rcpt = rcpt.strip()
localpart, domname = split_mailbox(rcpt)
try:
Domain.objects.get(name=domname)
except Domain.DoesNotExist:
ext_rcpts += [rcpt]
continue
try:
target = Alias.objects.get(domain__name=domname, address=localpart)
if target.full_address == self.full_address:
target = None
except Alias.DoesNotExist:
target = None
if target is None:
try:
target = Mailbox.objects.get(address=localpart,
domain__name=domname)
except Mailbox.DoesNotExist:
raise AdminError(_("Local recipient %s not found" % rcpt))
int_rcpts += [target]
self.save(int_rcpts, ext_rcpts, creator=user)
def to_csv(self, csvwriter):
row = [self.type, self.full_address, self.enabled]
row += self.get_recipients()
csvwriter.writerow(row)
reversion.register(Alias)
+49
View File
@@ -0,0 +1,49 @@
from django.db import models
class ObjectDates(models.Model):
"""Dates recording for admin objects
This table keeps creation and last modification dates for Domains,
domain aliases, mailboxes and aliases objects.
"""
creation = models.DateTimeField(auto_now_add=True)
last_modification = models.DateTimeField(auto_now=True)
@staticmethod
def set_for_object(obj):
"""Initialize or update dates for a given object.
:param obj: an admin object (Domain, Mailbox, etc)
"""
try:
dates = getattr(obj, "dates")
except ObjectDates.DoesNotExist:
dates = ObjectDates()
dates.save()
obj.dates = dates
class DatesAware(models.Model):
"""Abstract model to support dates
Inherit from this model to automatically add the "dates" feature
to another model. It defines the appropriate field and handles
saves.
"""
dates = models.ForeignKey(ObjectDates)
class Meta:
abstract = True
def save(self, *args, **kwargs):
ObjectDates.set_for_object(self)
super(DatesAware, self).save(*args, **kwargs)
@property
def creation(self):
return self.dates.creation
@property
def last_modification(self):
return self.dates.last_modification
+133
View File
@@ -0,0 +1,133 @@
import reversion
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from django.contrib.contenttypes import generic
from modoboa.lib import events, parameters
from modoboa.core.models import ObjectAccess
from .base import DatesAware
class Domain(DatesAware):
name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True,
help_text=ugettext_lazy("The domain name"))
quota = models.IntegerField(
help_text=ugettext_lazy("Default quota in MB applied to mailboxes")
)
enabled = models.BooleanField(
ugettext_lazy('enabled'),
help_text=ugettext_lazy("Check to activate this domain")
)
owners = generic.GenericRelation(ObjectAccess)
class Meta:
permissions = (
("view_domain", "View domain"),
("view_domains", "View domains"),
)
ordering = ["name"]
@property
def domainalias_count(self):
return self.domainalias_set.count()
@property
def mailbox_count(self):
return self.mailbox_set.count()
@property
def mbalias_count(self):
return self.alias_set.count()
@property
def admins(self):
"""Return the domain administrators of this domain
:return: a list of User objects
"""
return [oa.user for oa in self.owners.filter(user__is_superuser=False)]
def add_admin(self, account):
"""Add a new administrator for this domain
:param User account: the administrotor to add
"""
from modoboa.lib.permissions import grant_access_to_object
grant_access_to_object(account, self)
for mb in self.mailbox_set.all():
if mb.user.has_perm("admin.add_domain"):
continue
grant_access_to_object(account, mb)
grant_access_to_object(account, mb.user)
for al in self.alias_set.all():
grant_access_to_object(account, al)
def remove_admin(self, account):
"""Remove an administrator for this domain
:param User account: the administrotor to remove
"""
from modoboa.lib.permissions import ungrant_access_to_object
ungrant_access_to_object(self, account)
for mb in self.mailbox_set.all():
if mb.user.has_perm("admin.add_domain"):
continue
ungrant_access_to_object(mb, account)
ungrant_access_to_object(mb.user, account)
for al in self.alias_set.all():
ungrant_access_to_object(al, account)
def delete(self, fromuser, keepdir=False):
from modoboa.lib.permissions import \
ungrant_access_to_object, ungrant_access_to_objects
if self.domainalias_set.count():
events.raiseEvent("DomainAliasDeleted", self.domainalias_set.all())
ungrant_access_to_objects(self.domainalias_set.all())
if self.mailbox_set.count():
Quota.objects.filter(username__contains='@%s' % self.name).delete()
events.raiseEvent("DeleteMailbox", self.mailbox_set.all())
ungrant_access_to_objects(self.mailbox_set.all())
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm == "yes" and not keepdir:
for mb in self.mailbox_set.all():
mb.delete_dir()
if self.alias_set.count():
events.raiseEvent("MailboxAliasDelete", self.alias_set.all())
ungrant_access_to_objects(self.alias_set.all())
if parameters.get_admin("AUTO_ACCOUNT_REMOVAL") == "yes":
for account in User.objects.filter(mailbox__domain__name=self.name):
account.delete(fromuser, keepdir)
events.raiseEvent("DeleteDomain", self)
ungrant_access_to_object(self)
super(Domain, self).delete()
def __str__(self):
return self.name
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("CreateDomain", creator, self)
def save(self, *args, **kwargs):
if "creator" in kwargs:
creator = kwargs["creator"]
del kwargs["creator"]
else:
creator = None
super(Domain, self).save(*args, **kwargs)
if creator is not None:
self.post_create(creator)
def from_csv(self, user, row):
if len(row) < 4:
raise AdminError(_("Invalid line"))
self.name = row[1].strip()
self.quota = int(row[2].strip())
self.enabled = (row[3].strip() == 'True')
self.save(creator=user)
def to_csv(self, csvwriter):
csvwriter.writerow(["domain", self.name, self.quota, self.enabled])
reversion.register(Domain)
@@ -0,0 +1,78 @@
import reversion
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events
from modoboa.extensions.admin.exceptions import AdminError
from .base import DatesAware
from .domain import Domain
class DomainAlias(DatesAware):
name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True,
help_text=ugettext_lazy("The alias name"))
target = models.ForeignKey(
Domain, verbose_name=ugettext_lazy('target'),
help_text=ugettext_lazy("The domain this alias points to")
)
enabled = models.BooleanField(
ugettext_lazy('enabled'),
help_text=ugettext_lazy("Check to activate this alias")
)
class Meta:
permissions = (
("view_domaliases", "View domain aliases"),
)
def __unicode__(self):
return self.name
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("DomainAliasCreated", creator, self)
def save(self, *args, **kwargs):
if "creator" in kwargs:
creator = kwargs["creator"]
del kwargs["creator"]
else:
creator = None
super(DomainAlias, self).save(*args, **kwargs)
if creator is not None:
self.post_create(creator)
def delete(self):
from modoboa.lib.permissions import ungrant_access_to_object
events.raiseEvent("DomainAliasDeleted", self)
ungrant_access_to_object(self)
super(DomainAlias, self).delete()
def from_csv(self, user, row):
"""Create a domain alias from a CSV row
Expected format: ["domainalias", domain alias name, targeted domain, enabled]
:param user: a ``User`` object
:param row: a list containing the alias definition
"""
if len(row) < 4:
raise AdminError(_("Invalid line"))
self.name = row[1].strip()
domname = row[2].strip()
try:
self.target = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise AdminError(_("Unknown domain %s" % domname))
self.enabled = row[3].strip() == 'True'
self.save(creator=user)
def to_csv(self, csvwriter):
"""Export a domain alias using CSV format
:param csvwriter: a ``csv.writer`` object
"""
csvwriter.writerow(["domainalias", self.name,
self.target.name, self.enabled])
reversion.register(DomainAlias)
+213
View File
@@ -0,0 +1,213 @@
import os
import reversion
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from modoboa.lib import parameters, events
from modoboa.lib.sysutils import exec_cmd
from modoboa.core.models import User
from modoboa.extensions.admin.exceptions import AdminError
from .base import DatesAware
from .domain import Domain
class Mailbox(DatesAware):
address = models.CharField(
ugettext_lazy('address'), max_length=252,
help_text=ugettext_lazy("Mailbox address (without the @domain.tld part)")
)
quota = models.PositiveIntegerField()
use_domain_quota = models.BooleanField(default=False)
domain = models.ForeignKey(Domain)
user = models.ForeignKey(User)
class Meta:
permissions = (
("view_mailboxes", "View mailboxes"),
)
def __init__(self, *args, **kwargs):
super(Mailbox, self).__init__(*args, **kwargs)
self.__mail_home = None
def __str__(self):
return self.full_address
def __full_address(self, localpart):
return "%s@%s" % (localpart, self.domain.name)
@property
def full_address(self):
return self.__full_address(self.address)
@property
def enabled(self):
return self.user.is_active
@property
def alias_count(self):
return self.alias_set.count()
@property
def mail_home(self):
"""
"""
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm is None or hm == "no":
return None
if self.__mail_home is None:
code, output = exec_cmd("doveadm user %s -f home" % self.full_address,
sudo_user=parameters.get_admin("MAILBOXES_OWNER"))
if code:
raise AdminError(_("Failed to retrieve mailbox location (%s)" % output))
self.__mail_home = output.strip()
return self.__mail_home
def rename_dir(self, old_mail_home):
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm is None or hm == "no":
return
self.__mail_home = None
if not os.path.exists(old_mail_home):
return
code, output = exec_cmd(
"mv %s %s" % (old_mail_home, self.mail_home),
sudo_user=parameters.get_admin("MAILBOXES_OWNER")
)
if code:
raise AdminError(_("Failed to rename mailbox: %s" % output))
def rename(self, address, domain):
"""Rename the mailbox
:param string address: the new mailbox's address (local part)
:param Domain domain: the new mailbox's domain
"""
old_mail_home = self.mail_home
qs = Quota.objects.filter(username=self.full_address)
self.address = address
self.domain = domain
self.rename_dir(old_mail_home)
qs.update(username=self.full_address)
def delete_dir(self):
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
if hm is None or hm == "no":
return
if not os.path.exists(self.mail_home):
return
code, output = exec_cmd(
"rm -r %s" % self.mail_home,
sudo_user=parameters.get_admin("MAILBOXES_OWNER")
)
if code:
raise AdminError(_("Failed to remove mailbox: %s" % output))
def set_quota(self, value=None, override_rules=False):
"""Set or update quota's value for this mailbox.
:param integer value: the quota's value
:param bool override_rules: allow to override defined quota rules
"""
if value is None:
if self.use_domain_quota:
self.quota = self.domain.quota
else:
self.quota = 0
elif int(value) > self.domain.quota and not override_rules:
raise AdminError(
_("Quota is greater than the allowed domain's limit (%dM)" % self.domain.quota)
)
else:
self.quota = value
if not self.quota and not override_rules:
raise AdminError(_("A quota is required"))
def get_quota(self):
q = Quota.objects.get(username=self.full_address)
return int(q.bytes / 1048576)
def get_quota_in_percent(self):
if not self.quota:
return 0
q = Quota.objects.get(username=self.full_address)
return int(q.bytes / float(self.quota * 1048576) * 100)
def post_create(self, creator):
from modoboa.lib.permissions import grant_access_to_object
grant_access_to_object(creator, self, True)
events.raiseEvent("CreateMailbox", creator, self)
if creator.is_superuser and not self.user.has_perm("admin.add_domain"):
# A super user is creating a new mailbox. Give
# access to that mailbox (and the associated
# account) to the appropriate domain admins,
# except if the new account has a more important
# role (SuperAdmin, Reseller)
for admin in self.domain.admins:
grant_access_to_object(admin, self)
grant_access_to_object(admin, self.user)
def save(self, *args, **kwargs):
if "creator" in kwargs:
creator = kwargs["creator"]
del kwargs["creator"]
else:
creator = None
super(Mailbox, self).save(*args, **kwargs)
if creator is not None:
self.post_create(creator)
try:
q = self.quota_value
except Quota.DoesNotExist:
Quota.objects.create(mbox=self, username=self.full_address)
def delete(self, keepdir=False):
"""Custom delete method
We try to delete the associated quota in the same time (it may
has already been removed if we're deleting a domain).
:param bool keepdir: delete the mailbox home dir on the filesystem or not
"""
try:
q = Quota.objects.get(username=self.full_address)
except Quota.DoesNotExist:
pass
else:
q.delete()
if not keepdir:
self.delete_dir()
super(Mailbox, self).delete()
reversion.register(Mailbox)
@receiver(pre_delete, sender=Mailbox)
def mailbox_deleted_handler(sender, **kwargs):
"""``Mailbox`` pre_delete signal receiver
In order to properly handle deletions (ie. we don't want to leave
orphan records into the db), we define this custom receiver.
It manually removes the mailbox from the aliases it is linked to
and then remove all empty aliases.
"""
from modoboa.lib.permissions import ungrant_access_to_object
mb = kwargs['instance']
events.raiseEvent("DeleteMailbox", mb)
ungrant_access_to_object(mb)
for alias in mb.alias_set.all():
alias.mboxes.remove(mb)
if alias.mboxes.count() == 0:
alias.delete()
class Quota(models.Model):
username = models.EmailField(primary_key=True, max_length=254)
bytes = models.BigIntegerField(default=0)
messages = models.IntegerField(default=0)
mbox = models.OneToOneField(Mailbox, related_name="quota_value", null=True)
@@ -1,7 +1,8 @@
# coding: utf-8
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import tables
from templatetags.admin_extras import *
from templatetags.admin_tags import domain_actions
class DomainsTable(tables.Table):
tableid = "objects_table"
@@ -9,8 +10,8 @@ class DomainsTable(tables.Table):
styles = "table"
name = tables.LinkColumn(
"name", label=ugettext_lazy("Name"),
urlpattern="modoboa.admin.views.editdomain",
"name", label=ugettext_lazy("Name"),
urlpattern="modoboa.extensions.admin.views.domain.editdomain",
title=_("Edit domain"), modal=True, modalcb="admin.domainform_cb",
sortable=True, sort_order="name"
)
@@ -18,7 +19,7 @@ class DomainsTable(tables.Table):
"domainalias_set", label=ugettext_lazy("Alias(es)"), safe=True,
sortable=True, sort_order="domainalias__name"
)
actions = tables.ActionColumn("actions", label=ugettext_lazy("Actions"),
actions = tables.ActionColumn("actions", label=ugettext_lazy("Actions"),
defvalue=domain_actions)
cols_order = ["name", "domaliases", "actions"]
@@ -27,7 +28,6 @@ class DomainsTable(tables.Table):
super(DomainsTable, self).__init__(request)
self.populate(self._rows_from_model(doms))
def parse_domainalias_set(self, aliases):
if not len(aliases.all()):
return "---"
@@ -41,11 +41,14 @@ class DomainsTable(tables.Table):
return "muted"
return ""
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%")
version = tables.Column(
"version", label=ugettext_lazy("Version"), width="6%"
)
descr = tables.Column("description", label=ugettext_lazy("Description"))
cols_order = ["selection", "label", "version", "descr"]
@@ -1,4 +1,4 @@
{% extends "admin/listing.html" %}{% load i18n admin_extras libextras %}
{% extends "admin/listing.html" %}{% load i18n admin_tags libextras %}
{% block pagetitle %}{% trans "Domains" %}{% endblock %}
{% block html_head %}
{{ block.super }}
@@ -1,23 +1,23 @@
{% extends "admin/listing.html" %}{% load admin_extras libextras i18n %}{% load url from future %}
{% extends "admin/listing.html" %}{% load admin_tags libextras i18n %}{% load url from future %}
{% block pagetitle %}{% trans "Identities" %}{% endblock %}
{% block html_head %}
{{ block.super }}
<script type="text/javascript" src="{{ STATIC_URL }}js/autocompleter.js"></script>
<script type="text/javascript">
function get_domains_list(container) {
$.getJSON("{% url 'modoboa.admin.views.domains_list' %}", function(data) {
$.getJSON("{% url 'modoboa.extensions.admin.views.domain.domains_list' %}", function(data) {
container.value = data;
});
}
function get_account_list(container) {
$.getJSON("{% url 'modoboa.admin.views.accounts_list' %}", function(data) {
$.getJSON("{% url 'modoboa.extensions.admin.views.identity.accounts_list' %}", function(data) {
container.value = data;
});
}
function get_allowed_recipients(container) {
$.getJSON("{% url 'modoboa.admin.views.allowed_recipients_list' %}", function(data) {
$.getJSON("{% url 'modoboa.extensions.admin.views.alias.allowed_recipients_list' %}", function(data) {
container.value = data;
});
}
@@ -1,4 +1,4 @@
{% extends "admin/header.html" %}{% load i18n admin_extras libextras %}
{% extends "admin/header.html" %}{% load i18n admin_tags libextras %}
{% block html_head %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/jquery.sortable.css" />
@@ -10,66 +10,9 @@ from modoboa.lib.templatetags.libextras import render_link
register = template.Library()
genders = {
"Enabled" : (ugettext_lazy("enabled_m"), ugettext_lazy("enabled_f"))
"Enabled": (ugettext_lazy("enabled_m"), ugettext_lazy("enabled_f"))
}
@register.simple_tag
def admin_menu(selection, user):
entries = []
if user.has_perm("admin.view_domains"):
entries += [
{"name" : "domains",
"url" : reverse("modoboa.admin.views.domains"),
"label" : _("Domains")}
]
entries += \
events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user)
if user.has_perm("admin.add_user") or user.has_perm("admin.add_alias"):
entries += [
{"name" : "identities",
"url" : reverse("modoboa.admin.views.identities"),
"label" : _("Identities")},
]
if user.is_superuser:
entries += [
{"name" : "settings",
"label" : _("Modoboa"),
"url" : reverse("modoboa.admin.views.viewsettings")}
]
if not len(entries):
return ""
return render_to_string("common/menulist.html",
{"entries" : entries, "selection" : selection, "user" : user})
@register.simple_tag
def settings_menu(selection, user):
entries = [
{"name" : "extensions",
"class" : "ajaxlink",
"url" : "extensions/",
"label" : _("Extensions"),
"img" : ""},
{"name" : "info",
"class" : "ajaxlink",
"url" : "info/",
"label" : _("Information")},
{"name" : "logs",
"class" : "ajaxlink",
"url" : "logs/",
"label" : _("Logs")},
{"name" : "parameters",
"class" : "ajaxlink",
"url" : "parameters/",
"img" : "",
"label" : _("Parameters")},
]
return render_to_string('common/menu.html', {
"entries" : entries,
"css" : "nav nav-list",
"selection" : selection,
"user" : user
})
@register.simple_tag
def domains_menu(selection, user):
@@ -77,31 +20,32 @@ def domains_menu(selection, user):
return ""
entries = [
{"name" : "newdomain",
"label" : _("Add domain"),
"img" : "icon-plus",
"modal" : True,
"modalcb" : "admin.newdomain_cb",
"url" : reverse("modoboa.admin.views.newdomain")},
{"name" : "import",
"label" : _("Import"),
"img" : "icon-folder-open",
"url" : reverse("modoboa.admin.views.import_domains"),
"modal" : True,
"modalcb" : "admin.importform_cb"},
{"name" : "export",
"label" : _("Export"),
"img" : "icon-share-alt",
"url" : reverse("modoboa.admin.views.export_domains"),
"modal" : True,
"modalcb" : "admin.exportform_cb"}
]
{"name": "newdomain",
"label": _("Add domain"),
"img": "icon-plus",
"modal": True,
"modalcb": "admin.newdomain_cb",
"url": reverse("modoboa.extensions.admin.views.domain.newdomain")},
{"name": "import",
"label": _("Import"),
"img": "icon-folder-open",
"url": reverse("modoboa.extensions.admin.views.import.import_domains"),
"modal": True,
"modalcb": "admin.importform_cb"},
{"name": "export",
"label": _("Export"),
"img": "icon-share-alt",
"url": reverse("modoboa.extensions.admin.views.export.export_domains"),
"modal": True,
"modalcb": "admin.exportform_cb"}
]
return render_to_string('common/menulist.html', {
"entries" : entries,
"selection" : selection,
"user" : user
})
"entries": entries,
"selection": selection,
"user": user
})
@register.simple_tag
def identities_menu(user):
@@ -116,48 +60,48 @@ def identities_menu(user):
"img": "icon-hdd",
"class": "ajaxlink",
"url": "quotas/"},
{"name" : "newaccount",
"label" : _("Add account"),
"img" : "icon-plus",
"modal" : True,
"modalcb" : "admin.newaccount_cb",
"url" : reverse("modoboa.admin.views.newaccount")},
{"name" : "newalias",
"label" : _("Add alias"),
"img" : "icon-plus",
"modal" : True,
"modalcb" : "admin.aliasform_cb",
"url" : reverse("modoboa.admin.views.newalias")},
{"name" : "newforward",
"label" : _("Add forward"),
"img" : "icon-plus",
"modal" : True,
"modalcb" : "admin.aliasform_cb",
"url" : reverse("modoboa.admin.views.newforward")},
{"name" : "newdlist",
"label" : _("Add distribution list"),
"img" : "icon-plus",
"modal" : True,
"modalcb" : "admin.aliasform_cb",
"url" : reverse("modoboa.admin.views.newdlist")},
{"name" : "import",
"label" : _("Import"),
"img" : "icon-folder-open",
"url" : reverse("modoboa.admin.views.import_identities"),
"modal" : True,
"modalcb" : "admin.importform_cb"},
{"name" : "export",
"label" : _("Export"),
"img" : "icon-share-alt",
"url" : reverse("modoboa.admin.views.export_identities"),
"modal" : True,
"modalcb" : "admin.exportform_cb"}
]
{"name": "newaccount",
"label": _("Add account"),
"img": "icon-plus",
"modal": True,
"modalcb": "admin.newaccount_cb",
"url": reverse("modoboa.extensions.admin.views.identity.newaccount")},
{"name": "newalias",
"label": _("Add alias"),
"img": "icon-plus",
"modal": True,
"modalcb": "admin.aliasform_cb",
"url": reverse("modoboa.extensions.admin.views.alias.newalias")},
{"name": "newforward",
"label": _("Add forward"),
"img": "icon-plus",
"modal": True,
"modalcb": "admin.aliasform_cb",
"url": reverse("modoboa.extensions.admin.views.alias.newforward")},
{"name": "newdlist",
"label": _("Add distribution list"),
"img": "icon-plus",
"modal": True,
"modalcb": "admin.aliasform_cb",
"url": reverse("modoboa.extensions.admin.views.alias.newdlist")},
{"name": "import",
"label": _("Import"),
"img": "icon-folder-open",
"url": reverse("modoboa.extensions.admin.views.import.import_identities"),
"modal": True,
"modalcb": "admin.importform_cb"},
{"name": "export",
"label": _("Export"),
"img": "icon-share-alt",
"url": reverse("modoboa.extensions.admin.views.export.export_identities"),
"modal": True,
"modalcb": "admin.exportform_cb"}
]
return render_to_string('common/menulist.html', {
"entries" : entries,
"user" : user
})
"entries": entries,
"user": user
})
@register.simple_tag
@@ -217,6 +161,7 @@ def identity_actions(user, ident):
]
return render_actions(actions)
@register.simple_tag
def identity_modify_link(identity, active_tab='default'):
linkdef = {"label" : identity.identity, "modal" : True}
@@ -229,6 +174,7 @@ def identity_modify_link(identity, active_tab='default'):
linkdef["modalcb"] = "admin.aliasform_cb"
return render_link(linkdef)
@register.simple_tag
def domadmin_actions(daid, domid):
actions = [dict(
@@ -239,12 +185,14 @@ def domadmin_actions(daid, domid):
)]
return render_actions(actions)
@register.simple_tag
def loadadminextmenu(user):
menu = events.raiseQueryEvent("AdminMenuDisplay", "admin_menu_box", user)
return render_to_string('common/menulist.html',
{"entries" : menu, "user" : user})
@register.simple_tag
def param(app, definition):
name = "%s.%s" % (app, definition["name"])
@@ -313,6 +261,7 @@ def param(app, definition):
"""
return result
@register.filter
def gender(value, target):
if genders.has_key(value):
@@ -321,24 +270,8 @@ def gender(value, target):
return trans
return value
@register.simple_tag
def get_extra_admin_content(user, target, currentpage):
res = events.raiseQueryEvent("ExtraAdminContent", user, target, currentpage)
return "".join(res)
@register.filter
def colorize_level(level):
classes = {
"INFO": "text-info",
"WARNING": "text-warning",
"CRITICAL": "text-error"
}
if not level in classes:
return level
return "<p class='%s'>%s</p>" % (classes[level], level)
@register.filter
def tohtml(message):
return re.sub("'(.*?)'", "<strong>\g<1></strong>", message)
@@ -2,38 +2,31 @@ from django.conf.urls import patterns, url
urlpatterns = patterns(
'modoboa.admin.views',
url(r'^$', 'index', name="index"),
'modoboa.extensions.admin.views.domain',
url(r'^domains/$', 'domains', name="domains"),
(r'^domains/list/$', '_domains'),
(r'^domains/flatlist/$', 'domains_list'),
(r'^domains/new/', 'newdomain'),
(r'^domains/(?P<dom_id>\d+)/edit/$', 'editdomain'),
(r'^domains/(?P<dom_id>\d+)/delete/$', 'deldomain'),
(r'^domains/import/$', 'import_domains'),
(r'^domains/export/$', 'export_domains'),
(r'^settings/$', 'viewsettings'),
(r'^settings/parameters/$', 'viewparameters'),
(r'^settings/parameters/save/$', 'saveparameters'),
(r'^settings/extensions/$', 'viewextensions'),
(r'^settings/extensions/save/$', 'saveextensions'),
(r'^settings/info/$', 'information'),
(r'^settings/logs/$', 'logs'),
)
urlpatterns += patterns(
'modoboa.extensions.admin.views.identity',
(r'^permissions/remove/$', 'remove_permission'),
(r'^identities/$', 'identities'),
(r'^identities/list/$', '_identities'),
(r'^identities/quotas/$', 'list_quotas'),
(r'^identities/import/$', 'import_identities'),
(r'^identities/export/$', 'export_identities'),
(r'^accounts/list/$', 'accounts_list'),
(r'^accounts/new/$', 'newaccount'),
(r'^accounts/edit/(?P<accountid>\d+)/$', 'editaccount'),
(r'^accounts/delete/(?P<accountid>\d+)/$', 'delaccount'),
)
urlpatterns += patterns(
'modoboa.extensions.admin.views.alias',
(r'^distriblists/new/$', 'newdlist'),
(r'^distriblists/delete/$', 'deldlist'),
@@ -45,3 +38,15 @@ urlpatterns = patterns(
(r'^aliases/delete/$', 'delalias'),
(r'^aliases/allowed_recipients/$', 'allowed_recipients_list'),
)
urlpatterns += patterns(
'modoboa.extensions.admin.views.import',
(r'^domains/import/$', 'import_domains'),
(r'^identities/import/$', 'import_identities'),
)
urlpatterns += patterns(
'modoboa.extensions.admin.views.export',
(r'^domains/export/$', 'export_domains'),
(r'^identities/export/$', 'export_identities'),
)
+163
View File
@@ -0,0 +1,163 @@
from django.db import transaction, IntegrityError
from django.utils.translation import ugettext as _, ungettext
from django.shortcuts import render
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from modoboa.lib import events
from modoboa.lib.webutils import ajax_response, ajax_simple_response
from modoboa.lib.exceptions import PermDeniedException
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.forms import AliasForm
from modoboa.extensions.admin.models import Alias
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
@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_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")
+184
View File
@@ -0,0 +1,184 @@
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.translation import ugettext as _, ungettext
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Q
from django.shortcuts import render
from django.views.decorators.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from modoboa.lib import parameters, events
from modoboa.lib.webutils import ajax_simple_response, ajax_response
from modoboa.lib.formutils import CreationWizard
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.templatetags.libextras import pagination_bar
from modoboa.extensions.admin.tables import DomainsTable
from modoboa.extensions.admin.lib import get_sort_order, get_listing_page
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import Domain, Mailbox
from modoboa.extensions.admin.forms import (
DomainForm, DomainFormGeneral, DomainFormOptions
)
@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 = [u for u in domain.admins
if request.user.can_access(u) and not u.is_superuser]
if not request.user.is_superuser:
domadmins = [u for u in domadmins if u.group == "DomainAdmins"]
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})
+83
View File
@@ -0,0 +1,83 @@
import csv
import cStringIO
from django.http import HttpResponse
from django.shortcuts import render
from django.core.urlresolvers import reverse
from django.db import transaction
from django.utils.translation import ugettext as _, ungettext
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from modoboa.extensions.admin.forms import (
ExportIdentitiesForm, ExportDomainsForm
)
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")
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)
+244
View File
@@ -0,0 +1,244 @@
from django.shortcuts import render
from django.db import transaction
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.views.decorators.csrf import ensure_csrf_cookie
from modoboa.lib import parameters, events
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.webutils import ajax_simple_response, _render_to_string
from modoboa.lib.formutils import CreationWizard
from modoboa.lib.templatetags.libextras import pagination_bar
from modoboa.core.models import User
from modoboa.extensions.admin.lib import get_sort_order, get_listing_page
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.forms import (
AccountForm, AccountFormGeneral, AccountFormMail
)
@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
@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({"status": "ok"})
+165
View File
@@ -0,0 +1,165 @@
import csv
from django.utils.translation import ugettext as _, ungettext
from django.shortcuts import render
from django.core.urlresolvers import reverse
from django.db import transaction, IntegrityError
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from modoboa.lib.exceptions import ModoboaException
from modoboa.core.models import User
from modoboa.extensions.admin.models import (
Domain, DomainAlias, Alias
)
from modoboa.extensions.admin.forms import ImportIdentitiesForm, ImportDataForm
@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)
@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:
<ul>
<li><em>domain; name; quota; enabled</em></li>
<li><em>domainalias; name; targeted domain; enabled</em></li>
</ul>
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
<p>You can use a different character as separator.</p>
""")
ctx = dict(
title=_("Import 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)
@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:
<ul>
<li><em>account; loginname; password; first name; last name; enabled; group; address[, domain, ...]</em></li>
<li><em>alias; address; enabled; internal recipient</em></li>
<li><em>forward; address; enabled; external recipient</em></li>
<li><em>dlist; address; enabled; recipient; recipient; ...</em></li>
</ul>
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
<p>You can use a different character as separator.</p>
""")
ctx = dict(
title=_("Import identities"),
action_label=_("Import"),
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)
+8 -3
View File
@@ -2,6 +2,7 @@
import re
from django.http import Http404, HttpResponseRedirect
from modoboa.core.models import Extension
from modoboa.core.extensions import exts_pool
from modoboa.lib.webutils import _render_error, ajax_response
from modoboa.lib.exceptions import ModoboaException
@@ -11,9 +12,13 @@ class ExtControlMiddleware(object):
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
try:
ext = Extension.objects.get(name=m.group(1))
except Extension.DoesNotExist:
extdef = exts_pool.get_extension(m.group(1))
if extdef.always_active:
return None
raise Http404
if ext.enabled:
return None
raise Http404
+1 -1
View File
@@ -3,7 +3,7 @@ from functools import wraps
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _
from django.db import IntegrityError
from modoboa.admin.models import ObjectAccess
from modoboa.core.models import ObjectAccess
import events
from exceptions import ModoboaException
+31 -12
View File
@@ -3,7 +3,8 @@
"""
This module contains extra functions/shortcuts used to render HTML.
"""
import os, sys
import os
import sys
import re
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
@@ -13,14 +14,18 @@ from django import template
from django.utils import simplejson
from django.conf import settings
def _render(request, tpl, user_context):
"""Custom rendering function
Just a wrapper which automatically adds a RequestContext instance
(useful to use settings variables like STATIC_URL inside templates)
"""
return render_to_response(tpl, user_context,
context_instance=template.RequestContext(request))
return render_to_response(
tpl, user_context,
context_instance=template.RequestContext(request)
)
def _render_to_string(request, tpl, user_context):
"""Custom rendering function
@@ -30,9 +35,15 @@ def _render_to_string(request, tpl, user_context):
return render_to_string(tpl, user_context,
context_instance=template.RequestContext(request))
def _render_error(request, errortpl="error", user_context={}):
return render_to_response("common/%s.html" % errortpl, user_context,
context_instance=template.RequestContext(request))
def _render_error(request, errortpl="error", user_context=None):
if user_context is None:
user_context = {}
return render_to_response(
"common/%s.html" % errortpl, user_context,
context_instance=template.RequestContext(request)
)
def render_actions(actions):
t = template.Template("""{% load libextras %}
@@ -40,22 +51,26 @@ def render_actions(actions):
""")
return t.render(template.Context(dict(actions=actions)))
def _ctx_ok(url):
return {"status" : "ok", "url" : url}
return {"status": "ok", "url": url}
def _ctx_ko(tpl, ctx):
return {"status" : "ko", "content" : render_to_string(tpl, ctx)}
return {"status": "ko", "content": render_to_string(tpl, ctx)}
def getctx(status, level=1, callback=None, **kwargs):
if not callback:
callername = sys._getframe(level).f_code.co_name
else:
callername = callback
ctx = {"status" : status, "callback" : callername}
ctx = {"status": status, "callback": callername}
for kw, v in kwargs.iteritems():
ctx[kw] = v
return ctx
def ajax_response(request, status="ok", respmsg=None,
url=None, ajaxnav=False, norefresh=False,
template=None, **kwargs):
@@ -83,7 +98,7 @@ def ajax_response(request, status="ok", respmsg=None,
content = kwargs["content"]
else:
content = ""
jsonctx = {"status" : status, "content" : content}
jsonctx = {"status": status, "content": content}
if respmsg is not None:
jsonctx["respmsg"] = respmsg
if ajaxnav:
@@ -93,6 +108,7 @@ def ajax_response(request, status="ok", respmsg=None,
jsonctx["norefresh"] = norefresh
return HttpResponse(simplejson.dumps(jsonctx), mimetype="application/json")
def ajax_simple_response(content, **response_kwargs):
"""Simple AJAX response
@@ -103,6 +119,7 @@ def ajax_simple_response(content, **response_kwargs):
response_kwargs["content_type"] = "application/json"
return HttpResponse(simplejson.dumps(content), **response_kwargs)
def static_url(path):
"""Returns the correct static url for a given file
@@ -112,11 +129,12 @@ def static_url(path):
path = path[1:]
return "%s%s" % (settings.STATIC_URL, path)
def size2integer(value):
"""Try to convert a string representing a size to an integer value
in bytes.
Supported formats:
Supported formats:
* K|k for KB
* M|m for MB
* G|g for GB
@@ -137,6 +155,7 @@ def size2integer(value):
return int(m.group(1)) * 2 ** 30
return 0
@login_required
def topredirection(request):
"""Simple view to redirect the request when no application is specified
@@ -156,7 +175,7 @@ def topredirection(request):
infos = exts_pool.get_extension_infos(topredir)
path = infos["url"] if infos["url"] else infos["name"]
else:
path = "" # topredir
path = "admin" # topredir
if topredir in ["core", "stats"] and \
request.user.belongs_to_group('SimpleUsers'):
+7 -7
View File
@@ -14,13 +14,13 @@ urlpatterns = patterns('',
(r'^accounts/login/$', 'modoboa.core.views.auth.dologin'),
(r'^accounts/logout/$', 'modoboa.core.views.auth.dologout'),
(r'^admin/$', 'modoboa.core.views.admin.viewsettings'),
(r'^admin/parameters/$', 'modoboa.core.views.admin.viewparameters'),
(r'^admin/parameters/save/$', 'modoboa.core.views.admin.saveparameters'),
(r'^admin/extensions/$', 'modoboa.core.views.admin.viewextensions'),
(r'^admin/extensions/save/$', 'modoboa.core.views.admin.saveextensions'),
(r'^admin/info/$', 'modoboa.core.views.admin.information'),
(r'^admin/logs/$', 'modoboa.core.views.admin.logs'),
(r'^core/$', 'modoboa.core.views.admin.viewsettings'),
(r'^core/parameters/$', 'modoboa.core.views.admin.viewparameters'),
(r'^core/parameters/save/$', 'modoboa.core.views.admin.saveparameters'),
(r'^core/extensions/$', 'modoboa.core.views.admin.viewextensions'),
(r'^core/extensions/save/$', 'modoboa.core.views.admin.saveextensions'),
(r'^core/info/$', 'modoboa.core.views.admin.information'),
(r'^core/logs/$', 'modoboa.core.views.admin.logs'),
(r'^user/$', 'modoboa.core.views.user.index'),
(r'^user/preferences/$', 'modoboa.core.views.user.preferences'),