From 9281c53fd1259cde0c9a306ae06e531b963cd28e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 3 Jul 2016 16:50:17 +0200 Subject: [PATCH 1/7] Detail views for Account and Alias. see #894 --- modoboa/admin/models/alias.py | 5 + modoboa/admin/models/mailbox.py | 8 +- modoboa/admin/signals.py | 2 + .../admin/templates/admin/account_detail.html | 91 +++++++++++++++++++ .../admin/templates/admin/alias_detail.html | 39 ++++++++ .../admin/templates/admin/domain_detail.html | 29 ++++-- .../templates/admin/identities_table.html | 5 +- modoboa/admin/templatetags/admin_tags.py | 42 +++++---- modoboa/admin/urls.py | 10 +- modoboa/admin/views/__init__.py | 4 + modoboa/admin/views/alias.py | 30 +++++- modoboa/admin/views/identity.py | 47 +++++++++- modoboa/core/models.py | 31 +++++-- modoboa/core/password_hashers/advanced.py | 3 +- modoboa/core/templatetags/core_tags.py | 1 - modoboa/limits/general_callbacks.py | 14 +-- modoboa/limits/handlers.py | 21 ++++- modoboa/limits/models.py | 7 +- ...rces_widget.html => resources_widget.html} | 0 modoboa/templates/common/progressbar.html | 5 + modoboa/templates/common/tabforms.html | 2 + 21 files changed, 331 insertions(+), 65 deletions(-) create mode 100644 modoboa/admin/templates/admin/account_detail.html create mode 100644 modoboa/admin/templates/admin/alias_detail.html rename modoboa/limits/templates/limits/{domain_resources_widget.html => resources_widget.html} (100%) create mode 100644 modoboa/templates/common/progressbar.html diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py index cc5e8f4f3..c31147e56 100644 --- a/modoboa/admin/models/alias.py +++ b/modoboa/admin/models/alias.py @@ -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) diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 31d6adfce..107e8f939 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -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 diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py index db0f98c35..7d9be0803 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -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"]) diff --git a/modoboa/admin/templates/admin/account_detail.html b/modoboa/admin/templates/admin/account_detail.html new file mode 100644 index 000000000..476d92b95 --- /dev/null +++ b/modoboa/admin/templates/admin/account_detail.html @@ -0,0 +1,91 @@ +{% extends "admin/header.html" %} + +{% load i18n %} +{% load admin_tags %} +{% load lib_tags %} + +{% block leftcol %} + + + {% get_extra_admin_content user "leftcol" selection %} +{% endblock %} + +{% block apparea %} +

{{ object }}{% if not object.is_active %} {% trans "(disabled)" %}{% endif %}

+
+
+
+
+
+

{% trans "Summary" %}

+
+ + + + + + + + + + + + + + + + + + + + {% if user.mailbox %} + + + + + {% endif %} +
{% trans "Full name" %}{{ object.fullname }}
{% trans "Role" %}{{ object.role }}
{% trans "Created" %}{{ object.date_joined|date:"SHORT_DATETIME_FORMAT" }}
{% trans "Last login" %}{{ object.last_login|date:"SHORT_DATETIME_FORMAT" }}
{% trans "Secondary email" %}{{ object.secondary_email|default:_("Not defined") }}
{% trans "Phone number" %}{{ object.phone_number|default:_("Not defined") }}
{% trans "Quota" %}{% include "common/progressbar.html" with id='quotabar' value=user.mailbox.get_quota_in_percent %}
+
+ + {% if domains %} +
+
+

{% trans "Administered domains" %}

+
+ + {% for domain in domains %} + + {% empty %} + + {% endfor %} +
{{ domain }}
{% trans "No domain defined." %}
+
+ {% endif %} + + {% for template in templates.left %} + {% include template %} + {% endfor %} +
+
+ {% if user.mailbox %} +
+
+

{% trans "Aliases" %}

+
+ + {% for alias in user.mailbox.alias_addresses %} + + {% empty %} + + {% endfor %} +
{{ alias }}
{% trans "No alias defined" %}
+
+ {% endif %} + {% for template in templates.right %} + {% include template %} + {% endfor %} +
+
+{% endblock %} diff --git a/modoboa/admin/templates/admin/alias_detail.html b/modoboa/admin/templates/admin/alias_detail.html new file mode 100644 index 000000000..01d49a6dc --- /dev/null +++ b/modoboa/admin/templates/admin/alias_detail.html @@ -0,0 +1,39 @@ +{% extends "admin/header.html" %} + +{% load i18n %} +{% load admin_tags %} +{% load lib_tags %} + +{% block leftcol %} + + + {% get_extra_admin_content user "leftcol" selection %} +{% endblock %} + +{% block apparea %} +

{% trans "Alias" %} {{ object }}{% if not object.enabled %} {% trans "(disabled)" %}{% endif %}

+
+
+
+
+
+

{% trans "Summary" %}

+
+
+
+
{% trans "Created" %}
{{ alias.creation|date:"SHORT_DATETIME_FORMAT" }}
+
{% trans "Modified" %}
{{ alias.last_modification|date:"SHORT_DATETIME_FORMAT" }}
+
+
+ + + {% for recipient in object.recipients %} + + {% endfor %} +
{% trans "Recipients" %}
{{ recipient }}
+
+
+
+{% endblock %} diff --git a/modoboa/admin/templates/admin/domain_detail.html b/modoboa/admin/templates/admin/domain_detail.html index cbac67289..3b71ed193 100644 --- a/modoboa/admin/templates/admin/domain_detail.html +++ b/modoboa/admin/templates/admin/domain_detail.html @@ -12,13 +12,13 @@ {% endblock %} {% block apparea %} -

{{ object }} {% trans "Overview" %}

+

{{ object }} {% if not object.enabled %} {% trans "(disabled)" %}{% endif %}


-

{% trans "Information" %}

+

{% trans "Summary" %}

@@ -35,12 +35,21 @@
- {% for template in templates.left %} -
-
- {% include template %} -
+ {% if object.domainalias_set.exists %} +
+
+

{% trans "Domain aliases" %}

+ + {% for domainalias in object.domainalias_set.all %} + + {% endfor %} +
{{ domainalias }}
+
+ {% endif %} + + {% for template in templates.left %} + {% include template %} {% endfor %}
@@ -54,7 +63,11 @@ {{ da.username }} {{ da.fullname }} - {% if perms.admin.change_domain %}{% domadmin_actions da.id domain.id %}{% endif %} + + {% if perms.admin.change_domain %} + + {% endif %} + {% empty %} {% trans "No domain administrator defined yet." %} diff --git a/modoboa/admin/templates/admin/identities_table.html b/modoboa/admin/templates/admin/identities_table.html index 15fad1e00..eb8c96700 100644 --- a/modoboa/admin/templates/admin/identities_table.html +++ b/modoboa/admin/templates/admin/identities_table.html @@ -1,8 +1,11 @@ {% load i18n admin_tags lib_tags %} + {% for ident in identities %} {% check_identity_status ident as identity_is_enabled %} - {% identity_modify_link ident %}{% if not identity_is_enabled %} (disabled){% endif %} + + {{ ident }}{% if not identity_is_enabled %} {% trans "(disabled)" %}{% endif %} + {{ ident.name_or_rcpt }} {% render_tags ident.tags %} {% identity_actions user ident %} diff --git a/modoboa/admin/templatetags/admin_tags.py b/modoboa/admin/templatetags/admin_tags.py index dc9fdbb67..1f6ced634 100644 --- a/modoboa/admin/templatetags/admin_tags.py +++ b/modoboa/admin/templatetags/admin_tags.py @@ -81,24 +81,30 @@ 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 """ + if ajax_mode: + identity_list_url = "list/" + quota_list_url = "quota/" + else: + identity_list_url = reverse("admin:identity_list") + quota_list_url = reverse("admin:quota_list") entries = [ {"name": "identities", "label": _("List identities"), "img": "fa fa-user", "class": "ajaxnav navigation", - "url": "list/"}, + "url": identity_list_url}, {"name": "quotas", "label": _("List quotas"), "img": "fa fa-hdd-o", "class": "ajaxnav navigation", - "url": "quotas/"}, + "url": quota_list_url}, {"name": "newaccount", "label": _("Add account"), "img": "fa fa-plus", @@ -185,7 +191,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 +209,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 +238,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/urls.py b/modoboa/admin/urls.py index fb5ae7376..20a387e78 100644 --- a/modoboa/admin/urls.py +++ b/modoboa/admin/urls.py @@ -37,9 +37,11 @@ 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"), ) @@ -50,7 +52,9 @@ urlpatterns += patterns( 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..c905cde32 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 @@ -140,3 +143,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..3ad51f76b 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 @@ -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 ["""