Detail views for Account and Alias.

see #894
This commit is contained in:
Antoine Nguyen
2016-07-03 16:50:17 +02:00
parent 9d0b4e8533
commit 9281c53fd1
21 changed files with 331 additions and 65 deletions
+5
View File
@@ -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)
+3 -5
View File
@@ -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
+2
View File
@@ -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 user.mailbox %}
<tr>
<th>{% trans "Quota" %}</th>
<td>{% include "common/progressbar.html" with id='quotabar' value=user.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 user.mailbox %}
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Aliases" %}</h3>
</div>
<table class="table">
{% for alias in user.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>
@@ -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>
+25 -17
View File
@@ -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.
+7 -3
View File
@@ -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<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"),
)
@@ -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<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"),
)
+4
View File
@@ -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",
]
+27 -3
View File
@@ -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
+42 -5
View File
@@ -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
+21 -10
View File
@@ -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."""
+2 -1
View File
@@ -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)
-1
View File
@@ -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", {
+7 -7
View File
@@ -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>
+20 -1
View File
@@ -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"))
}
}]
+4 -3
View File
@@ -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
@@ -60,7 +61,7 @@ class UserObjectLimit(ObjectLimitMixin, models.Model):
return tpl
return None
@property
@cached_property
def current_value(self):
"""Return the current number of objects."""
if "extra_filters" not in self.definition:
@@ -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:
@@ -117,7 +118,7 @@ class DomainObjectLimit(ObjectLimitMixin, models.Model):
return tpl
return None
@property
@cached_property
def current_value(self):
"""Return the current number of objects."""
definition = self.definition
@@ -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>
+2
View File
@@ -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">