mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
wip: Big refacto
The admin application is now a true extension.
This commit is contained in:
@@ -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")
|
||||
@@ -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
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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\$))(.+)')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
-1
@@ -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 }}
|
||||
+4
-4
@@ -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
-1
@@ -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" />
|
||||
+71
-138
@@ -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'),
|
||||
)
|
||||
@@ -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")
|
||||
@@ -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})
|
||||
@@ -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)
|
||||
@@ -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"})
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user