diff --git a/modoboa/admin/templatetags/admin_tags.py b/modoboa/admin/templatetags/admin_tags.py
index dc9fdbb67..be7e380d9 100644
--- a/modoboa/admin/templatetags/admin_tags.py
+++ b/modoboa/admin/templatetags/admin_tags.py
@@ -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.
diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py
index 2c47ea9f4..84787ff0c 100644
--- a/modoboa/admin/tests/test_account.py
+++ b/modoboa/admin/tests/test_account.py
@@ -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."""
diff --git a/modoboa/admin/tests/test_alias.py b/modoboa/admin/tests/test_alias.py
index ad9ceec7a..3a7a4b162 100644
--- a/modoboa/admin/tests/test_alias.py
+++ b/modoboa/admin/tests/test_alias.py
@@ -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)
diff --git a/modoboa/admin/tests/test_domain.py b/modoboa/admin/tests/test_domain.py
index 57c11c2ac..cef7c0f54 100644
--- a/modoboa/admin/tests/test_domain.py
+++ b/modoboa/admin/tests/test_domain.py
@@ -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(
diff --git a/modoboa/admin/urls.py b/modoboa/admin/urls.py
index fb5ae7376..58dbee490 100644
--- a/modoboa/admin/urls.py
+++ b/modoboa/admin/urls.py
@@ -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
\d+)/$', 'editaccount',
+ url(r'^accounts/(?P\d+)/$', views.AccountDetailView.as_view(),
+ name="account_detail"),
+ url(r'^accounts/(?P\d+)/edit/$', 'editaccount',
name="account_change"),
- url(r'^accounts/delete/(?P\d+)/$', 'delaccount',
+ url(r'^accounts/(?P\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\d+)/$', 'editalias', name="alias_change"),
+ url(r'^aliases/(?P\d+)/$', views.AliasDetailView.as_view(),
+ name="alias_detail"),
+ url(r'^aliases/(?P\d+)/edit/$', 'editalias', name="alias_change"),
url(r'^aliases/delete/$', 'delalias', name="alias_delete"),
)
diff --git a/modoboa/admin/views/__init__.py b/modoboa/admin/views/__init__.py
index 008fb6858..9fa96c833 100644
--- a/modoboa/admin/views/__init__.py
+++ b/modoboa/admin/views/__init__.py
@@ -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",
]
diff --git a/modoboa/admin/views/alias.py b/modoboa/admin/views/alias.py
index 571eada92..7332822d1 100644
--- a/modoboa/admin/views/alias.py
+++ b/modoboa/admin/views/alias.py
@@ -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
diff --git a/modoboa/admin/views/identity.py b/modoboa/admin/views/identity.py
index 093a25dce..9aebe1050 100644
--- a/modoboa/admin/views/identity.py
+++ b/modoboa/admin/views/identity.py
@@ -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
diff --git a/modoboa/core/models.py b/modoboa/core/models.py
index a7f782f02..a7bac652e 100644
--- a/modoboa/core/models.py
+++ b/modoboa/core/models.py
@@ -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."""
diff --git a/modoboa/core/password_hashers/advanced.py b/modoboa/core/password_hashers/advanced.py
index 55915e6db..e4c4c54b9 100644
--- a/modoboa/core/password_hashers/advanced.py
+++ b/modoboa/core/password_hashers/advanced.py
@@ -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)
diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py
index fa836c41c..b1ef9afc5 100644
--- a/modoboa/core/templatetags/core_tags.py
+++ b/modoboa/core/templatetags/core_tags.py
@@ -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", {
diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py
index 3d7ab6780..88bc51dd5 100644
--- a/modoboa/limits/general_callbacks.py
+++ b/modoboa/limits/general_callbacks.py
@@ -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 ["""