mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge pull request #896 from tonioo/feature/acount_alias_detail_views_894
Feature: Acount alias detail views
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Models related to aliases management."""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
@@ -84,6 +85,10 @@ class Alias(AdminObject):
|
||||
altype = self.type
|
||||
return [{"name": altype, "label": labels[altype], "type": "idt"}]
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return detail url for this alias."""
|
||||
return reverse("admin:alias_detail", args=[self.pk])
|
||||
|
||||
def post_create(self, creator):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
super(Alias, self).post_create(creator)
|
||||
|
||||
@@ -155,13 +155,11 @@ class Mailbox(AdminObject):
|
||||
|
||||
:rtype: list of string
|
||||
"""
|
||||
aliases = []
|
||||
qset = (
|
||||
self.aliasrecipient_set.select_related("alias")
|
||||
.filter(alias__internal=False)
|
||||
)
|
||||
for alr in qset:
|
||||
aliases += [alr.alias.address]
|
||||
aliases = [alr.alias.address for alr in qset]
|
||||
return aliases
|
||||
|
||||
@property
|
||||
@@ -235,8 +233,8 @@ class Mailbox(AdminObject):
|
||||
self.quota = 0
|
||||
elif int(value) > self.domain.quota and not override_rules:
|
||||
raise lib_exceptions.BadRequest(
|
||||
_("Quota is greater than the allowed domain's limit (%dM)")
|
||||
% self.domain.quota
|
||||
_("Quota is greater than the allowed domain's limit (%dM)") %
|
||||
self.domain.quota
|
||||
)
|
||||
else:
|
||||
self.quota = value
|
||||
|
||||
@@ -7,3 +7,5 @@ extra_domain_actions = django.dispatch.Signal(
|
||||
providing_args=["user", "domain"])
|
||||
extra_domain_dashboard_widgets = django.dispatch.Signal(
|
||||
providing_args=["user", "domain"])
|
||||
extra_account_dashboard_widgets = django.dispatch.Signal(
|
||||
providing_args=["user", "account"])
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{% extends "admin/header.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load admin_tags %}
|
||||
{% load lib_tags %}
|
||||
|
||||
{% block leftcol %}
|
||||
<ul class="nav nav-sidebar">
|
||||
{% identities_menu user "identities" False %}
|
||||
</ul>
|
||||
|
||||
{% get_extra_admin_content user "leftcol" selection %}
|
||||
{% endblock %}
|
||||
|
||||
{% block apparea %}
|
||||
<h2>{{ object }}{% if not object.is_active %} <small>{% trans "(disabled)" %}</small>{% endif %}</h2>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Summary" %}</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{% trans "Full name" %}</th><td>{{ object.fullname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Role" %}</th><td>{{ object.role }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Created" %}</th><td>{{ object.date_joined|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Last login" %}</th><td>{{ object.last_login|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Secondary email" %}</th><td>{{ object.secondary_email|default:_("Not defined") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Phone number" %}</th><td>{{ object.phone_number|default:_("Not defined") }}</td>
|
||||
</tr>
|
||||
{% if object.mailbox %}
|
||||
<tr>
|
||||
<th>{% trans "Quota" %}</th>
|
||||
<td>{% include "common/progressbar.html" with id='quotabar' value=object.mailbox.get_quota_in_percent %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if domains %}
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Administered domains" %}</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
{% for domain in domains %}
|
||||
<tr><td>{{ domain }}</td></tr>
|
||||
{% empty %}
|
||||
<tr><td>{% trans "No domain defined." %}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for template in templates.left %}
|
||||
{% include template %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
{% if object.mailbox %}
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Aliases" %}</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
{% for alias in object.mailbox.alias_addresses %}
|
||||
<tr><td>{{ alias }}</td></tr>
|
||||
{% empty %}
|
||||
<tr><td>{% trans "No alias defined" %}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for template in templates.right %}
|
||||
{% include template %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "admin/header.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load admin_tags %}
|
||||
{% load lib_tags %}
|
||||
|
||||
{% block leftcol %}
|
||||
<ul class="nav nav-sidebar">
|
||||
{% identities_menu user "identities" False %}
|
||||
</ul>
|
||||
|
||||
{% get_extra_admin_content user "leftcol" selection %}
|
||||
{% endblock %}
|
||||
|
||||
{% block apparea %}
|
||||
<h2>{% trans "Alias" %} {{ object }}{% if not object.enabled %} <small>{% trans "(disabled)" %}</small>{% endif %}</h2>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Summary" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Created" %}</dt><dd>{{ alias.creation|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
<dt>{% trans "Modified" %}</dt><dd>{{ alias.last_modification|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<table class="table">
|
||||
<tr><th>{% trans "Recipients" %}</th></tr>
|
||||
{% for recipient in object.recipients %}
|
||||
<tr><td>{{ recipient }}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -12,13 +12,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block apparea %}
|
||||
<h2>{{ object }} <small>{% trans "Overview" %}</small></h2>
|
||||
<h2>{{ object }} {% if not object.enabled %} <small>{% trans "(disabled)" %}</small>{% endif %}</h2>
|
||||
<hr>
|
||||
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Information" %}</h3>
|
||||
<h3 class="panel-title">{% trans "Summary" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
@@ -35,12 +35,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for template in templates.left %}
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-body">
|
||||
{% include template %}
|
||||
</div>
|
||||
{% if object.domainalias_set.exists %}
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Domain aliases" %}</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
{% for domainalias in object.domainalias_set.all %}
|
||||
<tr><td>{{ domainalias }}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for template in templates.left %}
|
||||
{% include template %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -54,7 +63,11 @@
|
||||
<tr>
|
||||
<td>{{ da.username }}</td>
|
||||
<td>{{ da.fullname }}</td>
|
||||
<td name="actions">{% if perms.admin.change_domain %}{% domadmin_actions da.id domain.id %}{% endif %}</td>
|
||||
<td name="actions">
|
||||
{% if perms.admin.change_domain %}
|
||||
<a name="removeperm" href="{% url 'admin:permission_remove' %}?domid={{ domain.id }}&daid={{ da.id }}" title="{% trans 'Remove this permission' %}"><span class="fa fa-trash"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3">{% trans "No domain administrator defined yet." %}</td></tr>
|
||||
|
||||
@@ -23,4 +23,16 @@ $(document).ready(function() {
|
||||
{% extra_static_content "identities" "js" user %}
|
||||
{% endblock %}
|
||||
|
||||
{% block toolbar_right %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="fa fa-plus"></span> {% trans "Add" %} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a data-toggle="ajaxmodal" modalcb="admin.newaccount_cb" href="{% url 'admin:account_add' %}"><span class="fa fa-user"></span> {% trans "Account" %}</a></li>
|
||||
<li><a data-toggle="ajaxmodal" modalcb="admin.aliasform_cb" href="{% url 'admin:alias_add' %}">{% trans "Alias" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}{% identities_menu user "identities" %}{% endblock %}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
{% load i18n admin_tags lib_tags %}
|
||||
|
||||
{% for ident in identities %}
|
||||
{% check_identity_status ident as identity_is_enabled %}
|
||||
<tr id="{{ ident.id }}" class="{% if not identity_is_enabled %}text-muted{% endif %}" name="{{ forloop.counter }}">
|
||||
<td name="identity">{% identity_modify_link ident %}{% if not identity_is_enabled %} (disabled){% endif %}</td>
|
||||
<td name="identity">
|
||||
<a href="{{ ident.get_absolute_url }}">{{ ident }}</a>{% if not identity_is_enabled %} {% trans "(disabled)" %}{% endif %}
|
||||
</td>
|
||||
<td name="name_or_rcpt">{{ ident.name_or_rcpt }}</td>
|
||||
<td name="tags">{% render_tags ident.tags %}</td>
|
||||
<td name="actions">{% identity_actions user ident %}</td>
|
||||
|
||||
@@ -30,7 +30,10 @@ var admin;
|
||||
<div class="col-sm-5">
|
||||
{% include "admin/searchform.html" %}
|
||||
</div>
|
||||
<div id="taglist" class="col-sm-5"> </div>
|
||||
<div class="col-sm-3">
|
||||
{% block toolbar_right %}{% endblock %}
|
||||
</div>
|
||||
<div id="taglist" class="col-sm-4"> </div>
|
||||
</div>
|
||||
|
||||
<div id="listing">
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""
|
||||
Admin extension tags.
|
||||
"""
|
||||
"""Admin extension tags."""
|
||||
|
||||
from django import template
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -81,48 +79,32 @@ def domains_menu(selection, user, ajax_mode=True):
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def identities_menu(user, selection=None):
|
||||
def identities_menu(user, selection=None, ajax_mode=True):
|
||||
"""Menu specific to the Identities page.
|
||||
|
||||
:param ``User`` user: the connecter user
|
||||
:rtype: str
|
||||
:return: the rendered menu
|
||||
"""
|
||||
nav_classes = "navigation"
|
||||
if ajax_mode:
|
||||
identity_list_url = "list/"
|
||||
quota_list_url = "quotas/"
|
||||
nav_classes += " ajaxnav"
|
||||
else:
|
||||
identity_list_url = reverse("admin:identity_list")
|
||||
quota_list_url = identity_list_url + "#quotas/"
|
||||
entries = [
|
||||
{"name": "identities",
|
||||
"label": _("List identities"),
|
||||
"img": "fa fa-user",
|
||||
"class": "ajaxnav navigation",
|
||||
"url": "list/"},
|
||||
"class": nav_classes,
|
||||
"url": identity_list_url},
|
||||
{"name": "quotas",
|
||||
"label": _("List quotas"),
|
||||
"img": "fa fa-hdd-o",
|
||||
"class": "ajaxnav navigation",
|
||||
"url": "quotas/"},
|
||||
{"name": "newaccount",
|
||||
"label": _("Add account"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.newaccount_cb",
|
||||
"url": reverse("admin:account_add")},
|
||||
{"name": "newalias",
|
||||
"label": _("Add alias"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.aliasform_cb",
|
||||
"url": reverse("admin:alias_add")},
|
||||
{"name": "newforward",
|
||||
"label": _("Add forward"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.aliasform_cb",
|
||||
"url": reverse("admin:forward_add")},
|
||||
{"name": "newdlist",
|
||||
"label": _("Add distribution list"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.aliasform_cb",
|
||||
"url": reverse("admin:dlist_add")},
|
||||
"class": nav_classes,
|
||||
"url": quota_list_url},
|
||||
{"name": "import",
|
||||
"label": _("Import"),
|
||||
"img": "fa fa-folder-open",
|
||||
@@ -134,8 +116,7 @@ def identities_menu(user, selection=None):
|
||||
"img": "fa fa-share-alt",
|
||||
"url": reverse("admin:identity_export"),
|
||||
"modal": True,
|
||||
"modalcb": "admin.exportform_cb"
|
||||
}
|
||||
"modalcb": "admin.exportform_cb"}
|
||||
]
|
||||
|
||||
return render_to_string('common/menulist.html', {
|
||||
@@ -185,7 +166,17 @@ def identity_actions(user, ident):
|
||||
objid = ident.id
|
||||
if name == "User":
|
||||
actions = events.raiseQueryEvent("ExtraAccountActions", ident)
|
||||
url = (
|
||||
reverse("admin:account_change", args=[objid]) +
|
||||
"?active_tab=default"
|
||||
)
|
||||
actions += [
|
||||
{"name": "changeaccount",
|
||||
"url": url,
|
||||
"img": "fa fa-edit",
|
||||
"modal": True,
|
||||
"modalcb": "admin.editaccount_cb",
|
||||
"title": _("Edit {}").format(ident.username)},
|
||||
{"name": "delaccount",
|
||||
"url": reverse("admin:account_delete", args=[objid]),
|
||||
"img": "fa fa-trash",
|
||||
@@ -193,6 +184,12 @@ def identity_actions(user, ident):
|
||||
]
|
||||
else:
|
||||
actions = [
|
||||
{"name": "changealias",
|
||||
"url": reverse("admin:alias_change", args=[objid]),
|
||||
"img": "fa fa-edit",
|
||||
"modal": True,
|
||||
"modalcb": "admin.aliasform_cb",
|
||||
"title": _("Edit {}").format(ident)},
|
||||
{"name": "delalias",
|
||||
"url": "{}?selection={}".format(
|
||||
reverse("admin:alias_delete"), objid),
|
||||
@@ -216,20 +213,6 @@ def check_identity_status(identity):
|
||||
return True
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domain_modify_link(domain):
|
||||
linkdef = {"label": domain.name, "modal": True}
|
||||
if domain.__class__.__name__ == "Domain":
|
||||
linkdef["url"] = reverse(
|
||||
"admin:domain_change", args=[domain.id])
|
||||
linkdef["modalcb"] = "admin.domainform_cb"
|
||||
else:
|
||||
tmp = events.raiseDictEvent('GetDomainModifyLink', domain)
|
||||
for key in ['url', 'modalcb']:
|
||||
linkdef[key] = tmp[key]
|
||||
return render_link(linkdef)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domain_aliases(domain):
|
||||
"""Display domain aliases of this domain.
|
||||
|
||||
@@ -154,6 +154,22 @@ class AccountTestCase(ModoTestCase):
|
||||
reverse("admin:account_add"), values, status=400
|
||||
)
|
||||
|
||||
def test_account_detail_view(self):
|
||||
"""Test account detail view."""
|
||||
parameters.save_admin(
|
||||
"ENABLE_ADMIN_LIMITS", "no", app="limits")
|
||||
account = User.objects.get(username="admin@test.com")
|
||||
url = reverse("admin:account_detail", args=[account.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Summary", response.content)
|
||||
self.assertIn("Administered domains", response.content)
|
||||
self.assertNotIn("Resources usage", response.content)
|
||||
parameters.save_admin(
|
||||
"ENABLE_ADMIN_LIMITS", "yes", app="limits")
|
||||
response = self.client.get(url)
|
||||
self.assertIn("Resources usage", response.content)
|
||||
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(
|
||||
'modoboa.lib.authbackends.LDAPBackend',
|
||||
@@ -305,7 +321,7 @@ class PermissionsTestCase(ModoTestCase):
|
||||
recipients_1="user@test2.com",
|
||||
enabled=True
|
||||
)
|
||||
self.ajax_post(reverse("admin:dlist_add"), values)
|
||||
self.ajax_post(reverse("admin:alias_add"), values)
|
||||
|
||||
def test_domainadmin_master_user(self):
|
||||
"""Check domain administrator is not allowed to access this feature."""
|
||||
|
||||
@@ -55,7 +55,7 @@ class AliasTestCase(ModoTestCase):
|
||||
"enabled": True
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:dlist_add"), values
|
||||
reverse("admin:alias_add"), values
|
||||
)
|
||||
alias = Alias.objects.get(address="badalias@test.com")
|
||||
self.assertEqual(alias.recipients_count, 1)
|
||||
@@ -132,7 +132,7 @@ class AliasTestCase(ModoTestCase):
|
||||
recipients_2="ext@titi.com",
|
||||
enabled=True)
|
||||
self.ajax_post(
|
||||
reverse("admin:dlist_add"), values
|
||||
reverse("admin:alias_add"), values
|
||||
)
|
||||
user = User.objects.get(username="user@test.com")
|
||||
mb = user.mailbox
|
||||
@@ -164,7 +164,7 @@ class AliasTestCase(ModoTestCase):
|
||||
def test_forward(self):
|
||||
values = dict(address="forward2@test.com", recipients="rcpt@dest.com")
|
||||
self.ajax_post(
|
||||
reverse("admin:forward_add"), values
|
||||
reverse("admin:alias_add"), values
|
||||
)
|
||||
fwd = Alias.objects.get(address="forward2@test.com")
|
||||
self.assertEqual(fwd.recipients_count, 1)
|
||||
@@ -187,7 +187,7 @@ class AliasTestCase(ModoTestCase):
|
||||
def test_forward_and_local_copies(self):
|
||||
values = dict(address="user@test.com", recipients="rcpt@dest.com")
|
||||
self.ajax_post(
|
||||
reverse("admin:forward_add"), values
|
||||
reverse("admin:alias_add"), values
|
||||
)
|
||||
fwd = Alias.objects.get(address="user@test.com", internal=False)
|
||||
self.assertEqual(fwd.recipients_count, 1)
|
||||
@@ -211,18 +211,21 @@ class AliasTestCase(ModoTestCase):
|
||||
"enabled": True
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:dlist_add"), values
|
||||
reverse("admin:alias_add"), values
|
||||
)
|
||||
|
||||
def test_distribution_list_deletion_on_user_update_bug(self):
|
||||
"""This test demonstrates an issue with distribution list being deleted
|
||||
when one of the users which belong to that list is changed."""
|
||||
"""This test demonstrates an issue with distribution list being
|
||||
deleted when one of the users which belong to that list is
|
||||
changed.
|
||||
|
||||
"""
|
||||
values = dict(address="list@test.com",
|
||||
recipients="user@test.com",
|
||||
recipients_1="admin@test.com",
|
||||
enabled=True)
|
||||
self.ajax_post(
|
||||
reverse("admin:dlist_add"), values
|
||||
reverse("admin:alias_add"), values
|
||||
)
|
||||
|
||||
user = User.objects.get(username="user@test.com")
|
||||
@@ -233,4 +236,14 @@ class AliasTestCase(ModoTestCase):
|
||||
)
|
||||
self.ajax_post(reverse("admin:account_change", args=[user.id]), values)
|
||||
|
||||
self.assertEqual(Alias.objects.filter(address="list@test.com").count(), 1)
|
||||
self.assertEqual(
|
||||
Alias.objects.filter(address="list@test.com").count(), 1)
|
||||
|
||||
def test_alias_detail_view(self):
|
||||
"""Test alias detail view."""
|
||||
account = Alias.objects.get(address="postmaster@test.com")
|
||||
url = reverse("admin:alias_detail", args=[account.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Summary", response.content)
|
||||
self.assertIn("Recipients", response.content)
|
||||
|
||||
@@ -155,7 +155,7 @@ class DomainTestCase(ModoTestCase):
|
||||
url = reverse("admin:domain_detail", args=[domain.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Information", response.content)
|
||||
self.assertIn("Summary", response.content)
|
||||
self.assertIn("Administrators", response.content)
|
||||
self.assertNotIn("Resources usage", response.content)
|
||||
parameters.save_admin(
|
||||
|
||||
@@ -37,20 +37,21 @@ urlpatterns += patterns(
|
||||
|
||||
url(r'^accounts/list/$', 'accounts_list', name="account_list"),
|
||||
url(r'^accounts/new/$', 'newaccount', name="account_add"),
|
||||
url(r'^accounts/edit/(?P<accountid>\d+)/$', 'editaccount',
|
||||
url(r'^accounts/(?P<pk>\d+)/$', views.AccountDetailView.as_view(),
|
||||
name="account_detail"),
|
||||
url(r'^accounts/(?P<pk>\d+)/edit/$', 'editaccount',
|
||||
name="account_change"),
|
||||
url(r'^accounts/delete/(?P<accountid>\d+)/$', 'delaccount',
|
||||
url(r'^accounts/(?P<pk>\d+)/delete/$', 'delaccount',
|
||||
name="account_delete"),
|
||||
)
|
||||
|
||||
urlpatterns += patterns(
|
||||
'modoboa.admin.views.alias',
|
||||
|
||||
url(r'^distriblists/new/$', 'newdlist', name="dlist_add"),
|
||||
url(r'^forwards/new/$', 'newforward', name="forward_add"),
|
||||
|
||||
url(r'^aliases/new/$', 'newalias', name="alias_add"),
|
||||
url(r'^aliases/edit/(?P<alid>\d+)/$', 'editalias', name="alias_change"),
|
||||
url(r'^aliases/(?P<pk>\d+)/$', views.AliasDetailView.as_view(),
|
||||
name="alias_detail"),
|
||||
url(r'^aliases/(?P<alid>\d+)/edit/$', 'editalias', name="alias_change"),
|
||||
url(r'^aliases/delete/$', 'delalias', name="alias_delete"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""Shortcuts."""
|
||||
|
||||
from .alias import AliasDetailView
|
||||
from .identity import AccountDetailView
|
||||
from .dnsbl import DNSBLDomainDetailView
|
||||
from .domain import DomainDetailView
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AccountDetailView",
|
||||
"AliasDetailView",
|
||||
"DNSBLDomainDetailView",
|
||||
"DomainDetailView",
|
||||
]
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""Alias related views."""
|
||||
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required
|
||||
)
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.views import generic
|
||||
|
||||
from django.contrib.auth import mixins as auth_mixins
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required
|
||||
)
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
@@ -59,17 +62,6 @@ def _new_alias(request, title, action, successmsg,
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_alias")
|
||||
@reversion.create_revision()
|
||||
def newdlist(request):
|
||||
return _new_alias(
|
||||
request, _("New distribution list"),
|
||||
reverse("admin:dlist_add"),
|
||||
_("Distribution list created")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_alias")
|
||||
@reversion.create_revision()
|
||||
@@ -80,16 +72,6 @@ def newalias(request):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_alias")
|
||||
@reversion.create_revision()
|
||||
def newforward(request):
|
||||
return _new_alias(
|
||||
request, _("New forward"), reverse("admin:forward_add"),
|
||||
_("Forward created")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.change_alias")
|
||||
@reversion.create_revision()
|
||||
@@ -140,3 +122,24 @@ def delalias(request):
|
||||
|
||||
msg = ungettext(msg, msgs, len(selection))
|
||||
return render_to_json_response(msg)
|
||||
|
||||
|
||||
class AliasDetailView(
|
||||
auth_mixins.PermissionRequiredMixin, generic.DetailView):
|
||||
"""DetailView for Alias."""
|
||||
|
||||
model = Alias
|
||||
permission_required = "admin.add_alias"
|
||||
|
||||
def has_permission(self):
|
||||
"""Check object-level access."""
|
||||
result = super(AliasDetailView, self).has_permission()
|
||||
if not result:
|
||||
return result
|
||||
return self.request.user.can_access(self.get_object())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add information to context."""
|
||||
context = super(AliasDetailView, self).get_context_data(**kwargs)
|
||||
context["selection"] = "identities"
|
||||
return context
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Identity related views."""
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.views import generic
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from django.contrib.auth import mixins as auth_mixins
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required, user_passes_test
|
||||
)
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
@@ -24,6 +27,7 @@ from modoboa.lib.web_utils import (
|
||||
from ..forms import AccountForm, AccountWizard
|
||||
from ..lib import get_identities
|
||||
from ..models import Mailbox, Domain
|
||||
from .. import signals
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -170,8 +174,8 @@ def newaccount(request):
|
||||
@login_required
|
||||
@permission_required("core.change_user")
|
||||
@reversion.create_revision()
|
||||
def editaccount(request, accountid, tplname="common/tabforms.html"):
|
||||
account = User.objects.get(pk=accountid)
|
||||
def editaccount(request, pk, tplname="common/tabforms.html"):
|
||||
account = User.objects.get(pk=pk)
|
||||
if not request.user.can_access(account):
|
||||
raise PermDeniedException
|
||||
mb = account.mailbox if hasattr(account, "mailbox") else None
|
||||
@@ -183,9 +187,9 @@ def editaccount(request, accountid, tplname="common/tabforms.html"):
|
||||
|
||||
@login_required
|
||||
@permission_required("core.delete_user")
|
||||
def delaccount(request, accountid):
|
||||
def delaccount(request, pk):
|
||||
keepdir = True if request.POST.get("keepdir", "false") == "true" else False
|
||||
User.objects.get(pk=accountid).delete(request.user, keep_mb_dir=keepdir)
|
||||
User.objects.get(pk=pk).delete(request.user, keep_mb_dir=keepdir)
|
||||
return render_to_json_response(
|
||||
ungettext("Account deleted", "Accounts deleted", 1)
|
||||
)
|
||||
@@ -208,3 +212,36 @@ def remove_permission(request):
|
||||
raise PermDeniedException
|
||||
domain.remove_admin(account)
|
||||
return render_to_json_response({})
|
||||
|
||||
|
||||
class AccountDetailView(
|
||||
auth_mixins.PermissionRequiredMixin, generic.DetailView):
|
||||
"""DetailView for Account."""
|
||||
|
||||
model = User
|
||||
permission_required = "core.add_user"
|
||||
template_name = "admin/account_detail.html"
|
||||
|
||||
def has_permission(self):
|
||||
"""Check object-level access."""
|
||||
result = super(AccountDetailView, self).has_permission()
|
||||
if not result:
|
||||
return result
|
||||
return self.request.user.can_access(self.get_object())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add information to context."""
|
||||
context = super(AccountDetailView, self).get_context_data(**kwargs)
|
||||
del context["user"]
|
||||
result = signals.extra_account_dashboard_widgets.send(
|
||||
self.__class__, user=self.request.user, account=self.object)
|
||||
context["templates"] = {"left": [], "right": []}
|
||||
for receiver, widgets in result:
|
||||
for widget in widgets:
|
||||
context["templates"][widget["column"]].append(
|
||||
widget["template"])
|
||||
context.update(widget["context"])
|
||||
if self.object.role in ["Resellers", "DomainAdmins"]:
|
||||
context["domains"] = Domain.objects.get_for_admin(self.object)
|
||||
context["selection"] = "identities"
|
||||
return context
|
||||
|
||||
+21
-10
@@ -2,16 +2,18 @@
|
||||
|
||||
import re
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from django.contrib.auth.hashers import make_password, is_password_usable
|
||||
from django.contrib.auth.models import (
|
||||
UserManager, Group, PermissionsMixin
|
||||
)
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
import jsonfield
|
||||
from reversion import revisions as reversion
|
||||
@@ -208,6 +210,10 @@ class User(PermissionsMixin):
|
||||
def has_usable_password(self):
|
||||
return is_password_usable(self.password)
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return detail url for this user."""
|
||||
return reverse("admin:account_detail", args=[self.pk])
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return [{"name": "account", "label": _("account"), "type": "idt"},
|
||||
@@ -314,12 +320,16 @@ class User(PermissionsMixin):
|
||||
@property
|
||||
def role(self):
|
||||
"""Return user role."""
|
||||
if self.is_superuser:
|
||||
return "SuperAdmins"
|
||||
try:
|
||||
return self.groups.all()[0].name
|
||||
except IndexError:
|
||||
return "---"
|
||||
if not hasattr(self, "_role"):
|
||||
self._role = None
|
||||
if self._role is None:
|
||||
if self.is_superuser:
|
||||
self._role = "SuperAdmins"
|
||||
try:
|
||||
self._role = self.groups.all()[0].name
|
||||
except IndexError:
|
||||
self._role = "---"
|
||||
return self._role
|
||||
|
||||
@role.setter
|
||||
def role(self, role):
|
||||
@@ -345,6 +355,7 @@ class User(PermissionsMixin):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
grant_access_to_object(self, self)
|
||||
self.save()
|
||||
self._role = role
|
||||
|
||||
def post_create(self, creator):
|
||||
"""Grant permission on this user to creator."""
|
||||
|
||||
@@ -7,6 +7,7 @@ from passlib.hash import bcrypt, md5_crypt, sha256_crypt, sha512_crypt
|
||||
from modoboa.core.password_hashers.base import PasswordHasher
|
||||
from modoboa.lib import parameters
|
||||
|
||||
|
||||
class BLFCRYPTHasher(PasswordHasher):
|
||||
"""
|
||||
BLF-CRYPT password hasher.
|
||||
@@ -25,7 +26,7 @@ class BLFCRYPTHasher(PasswordHasher):
|
||||
# Using the parameter for rounds will generate the error
|
||||
# ValueError: rounds too high (bcrypt requires <= 31 rounds)
|
||||
# when using the bcrypt hasher.
|
||||
#rounds = int(parameters.get_admin('ROUNDS_NUMBER'))
|
||||
# rounds = int(parameters.get_admin('ROUNDS_NUMBER'))
|
||||
# To get around this, I use the default of 12.
|
||||
rounds = 12
|
||||
return bcrypt.encrypt(clearvalue, rounds=rounds)
|
||||
|
||||
@@ -26,7 +26,6 @@ def core_menu(selection, user):
|
||||
"label": _("Modoboa"),
|
||||
"url": reverse("core:index")}
|
||||
]
|
||||
|
||||
if not len(entries):
|
||||
return ""
|
||||
return render_to_string("common/menulist.html", {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""General event callbacks."""
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.lib import events, parameters
|
||||
@@ -10,8 +11,6 @@ from . import utils
|
||||
|
||||
@events.observe("ExtraAdminContent")
|
||||
def display_pool_usage(user, target, currentpage):
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
condition = (
|
||||
parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or
|
||||
target != "leftcol" or user.is_superuser)
|
||||
@@ -103,11 +102,12 @@ def fill_account_instances(user, account, instances):
|
||||
|
||||
@events.observe("GetStaticContent")
|
||||
def get_static_content(caller, st_type, user):
|
||||
if parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no":
|
||||
return []
|
||||
if caller not in ['domains', 'identities']:
|
||||
return []
|
||||
if user.group == "SimpleUsers":
|
||||
condition = (
|
||||
parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or
|
||||
caller not in ["domains", "identities"] or
|
||||
user.role in ["SuperAdmins", "SimpleUsers"]
|
||||
)
|
||||
if condition:
|
||||
return []
|
||||
if st_type == "css":
|
||||
return ["""<style>
|
||||
|
||||
@@ -76,8 +76,27 @@ def display_domain_limits(sender, user, domain, **kwargs):
|
||||
return []
|
||||
return [{
|
||||
"column": "right",
|
||||
"template": "limits/domain_resources_widget.html",
|
||||
"template": "limits/resources_widget.html",
|
||||
"context": {
|
||||
"limits": domain.domainobjectlimit_set.all()
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
@receiver(admin_signals.extra_account_dashboard_widgets)
|
||||
def display_admin_limits(sender, user, account, **kwargs):
|
||||
"""Display resources usage for admin."""
|
||||
condition = (
|
||||
parameters.get_admin("ENABLE_ADMIN_LIMITS") == "yes" and
|
||||
account.role in ["DomainAdmins", "Resellers"]
|
||||
)
|
||||
if not condition:
|
||||
return []
|
||||
return [{
|
||||
"column": "right",
|
||||
"template": "limits/resources_widget.html",
|
||||
"context": {
|
||||
"limits": (
|
||||
account.userobjectlimit_set.select_related("content_type"))
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.core import models as core_models
|
||||
@@ -79,7 +80,7 @@ class UserObjectLimit(ObjectLimitMixin, models.Model):
|
||||
"""Return display name."""
|
||||
return self.definition["label"]
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def usage(self):
|
||||
"""Return current limit usage in %."""
|
||||
if self.max_value < 0:
|
||||
|
||||
@@ -48,6 +48,8 @@ class ResourceTestCase(lib_tests.ModoTestCase):
|
||||
def setUpTestData(cls):
|
||||
"""Custom setUpTestData method."""
|
||||
super(ResourceTestCase, cls).setUpTestData()
|
||||
parameters.save_admin("ENABLE_ADMIN_LIMITS", "yes")
|
||||
parameters.save_admin("ENABLE_DOMAIN_LIMITS", "no")
|
||||
for name, tpl in utils.get_user_limit_templates():
|
||||
parameters.save_admin(
|
||||
"DEFLT_USER_{0}_LIMIT".format(name.upper()), 2,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Global javascript utilities
|
||||
*/
|
||||
|
||||
/* global $ */
|
||||
|
||||
var static_url = "";
|
||||
|
||||
/*
|
||||
@@ -180,7 +182,7 @@ function simple_ajax_form_post(e, options) {
|
||||
return;
|
||||
}
|
||||
if (opts.reload_on_success) {
|
||||
if (opts.reload_mode == 'full') {
|
||||
if (opts.reload_mode === 'full') {
|
||||
window.location.reload();
|
||||
} else {
|
||||
histomanager.update(true);
|
||||
@@ -230,13 +232,13 @@ function gethref(obj) {
|
||||
* http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript
|
||||
*/
|
||||
function parse_qs(raw) {
|
||||
if (raw === "") return {};
|
||||
if (raw === "") { return {}; }
|
||||
var res = {};
|
||||
|
||||
for (var i = 0; i < raw.length; i++) {
|
||||
var p = raw[i].split('=');
|
||||
|
||||
if (p.length != 2) continue;
|
||||
if (p.length !== 2) { continue; }
|
||||
res[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
|
||||
}
|
||||
return res;
|
||||
@@ -289,12 +291,12 @@ function simple_ajax_request(e, uoptions) {
|
||||
var defaults = {};
|
||||
var options = $.extend({}, defaults, uoptions);
|
||||
|
||||
if (e !== undefined) e.preventDefault();
|
||||
if (e !== undefined) { e.preventDefault(); }
|
||||
$.ajax({
|
||||
url: $this.attr("href"),
|
||||
dataType: 'json'
|
||||
}).done(function(data) {
|
||||
if (options.ok_cb) options.ok_cb(data);
|
||||
if (options.ok_cb) { options.ok_cb(data); }
|
||||
if (data) {
|
||||
$("body").notify("success", data, 2000);
|
||||
}
|
||||
@@ -306,7 +308,7 @@ function simple_ajax_request(e, uoptions) {
|
||||
* the status code received with a response is equal to 278.
|
||||
*/
|
||||
function ajax_login_redirect(xhr) {
|
||||
if (xhr.status != 278) {
|
||||
if (xhr.status !== 278) {
|
||||
return;
|
||||
}
|
||||
var params = "?next=" + window.location.pathname;
|
||||
@@ -359,5 +361,5 @@ $(document).ready(function() {
|
||||
$(document).on('click', 'a[data-toggle="ajaxmodal"]', modalbox);
|
||||
$(document).on('click', 'a[data-toggle="ajaxmodal-autowidth"]', modalbox_autowidth);
|
||||
$(document).on('click', '.activator', activate_widget);
|
||||
$(".help").popover({trigger: 'hover'}).click(function(e) {e.preventDefault();});
|
||||
$(".help").popover({trigger: 'hover'}).click(function(e) { e.preventDefault(); });
|
||||
});
|
||||
|
||||
@@ -254,10 +254,10 @@ History.prototype = {
|
||||
check: function() {
|
||||
var rawhash = this.get_raw_hash();
|
||||
|
||||
if (this.serialized == rawhash && this.force === undefined) {
|
||||
if (this.serialized === rawhash && this.force === undefined) {
|
||||
return;
|
||||
}
|
||||
delete(this.force);
|
||||
delete this.force;
|
||||
this.from_string(rawhash);
|
||||
|
||||
if (!this.serialized) {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load lib_tags %}
|
||||
|
||||
<div id="{{ id }}" rel="tooltip" title="{{ value }}%" class="progress">
|
||||
<div class="{% progress_color value %}" style="width: {{ value }%"></div>
|
||||
</div>
|
||||
@@ -1,5 +1,7 @@
|
||||
{% extends "common/generic_modal.html" %}
|
||||
|
||||
{% load i18n form_tags %}
|
||||
|
||||
{% block modalbody %}
|
||||
<form id="{{ formid }}" class="form-horizontal" action="{{ action }}" method="POST">{% csrf_token %}
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
|
||||
Reference in New Issue
Block a user