mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge pull request #908 from tonioo/feature/dashboard_and_rss_891
Dashboard and rss
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
"""Core application constants."""
|
||||
|
||||
ADMIN_GROUPS = [
|
||||
"SuperAdmins",
|
||||
"Resellers",
|
||||
"DomainAdmins",
|
||||
]
|
||||
|
||||
LANGUAGES = (
|
||||
("cs", u"čeština"),
|
||||
|
||||
+11
-6
@@ -6,6 +6,7 @@ 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.functional import cached_property
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from django.contrib.auth.hashers import make_password, is_password_usable
|
||||
@@ -321,14 +322,13 @@ class User(PermissionsMixin):
|
||||
def role(self):
|
||||
"""Return user role."""
|
||||
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 = "---"
|
||||
else:
|
||||
try:
|
||||
self._role = self.groups.all()[0].name
|
||||
except IndexError:
|
||||
self._role = "---"
|
||||
return self._role
|
||||
|
||||
@role.setter
|
||||
@@ -357,6 +357,11 @@ class User(PermissionsMixin):
|
||||
self.save()
|
||||
self._role = role
|
||||
|
||||
@cached_property
|
||||
def is_admin(self):
|
||||
"""Shortcut to check if user is administrator."""
|
||||
return self.role in constants.ADMIN_GROUPS
|
||||
|
||||
def post_create(self, creator):
|
||||
"""Grant permission on this user to creator."""
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "fluid.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block container_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-offset-1 col-sm-10">
|
||||
<h1>{% blocktrans with user=user trimmed %}Hello {{ user }}.{% endblocktrans %}</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Latest news" %}</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{% for post in news %}
|
||||
<tr>
|
||||
<td><a href="{{ post.link }}" target="_blank">{{ post.title }}</a></td>
|
||||
<td class="text-right">{{ post.published|date:"DATE_FORMAT" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="panel-body text-center">
|
||||
{% blocktrans trimmed %}
|
||||
Visit to the <a href="https://modoboa.org/blog/" target="_blank">official weblog</a> for more information.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock container_content %}
|
||||
@@ -18,8 +18,17 @@ register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def core_menu(selection, user):
|
||||
entries = \
|
||||
events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user)
|
||||
"""Build the top level menu."""
|
||||
entries = []
|
||||
if user.is_admin:
|
||||
entries += [
|
||||
{"name": "dashboard",
|
||||
"label": "",
|
||||
"img": "fa fa-home fa-lg",
|
||||
"url": reverse("core:dashboard")}
|
||||
]
|
||||
entries += (
|
||||
events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user))
|
||||
if user.is_superuser:
|
||||
entries += [
|
||||
{"name": "settings",
|
||||
|
||||
+39
-1
@@ -41,7 +41,45 @@ class AuthenticationTestCase(ModoTestCase):
|
||||
data = {"username": "admin", "password": "password"}
|
||||
response = self.client.post(reverse("core:login"), data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith(reverse("admin:domain_list")))
|
||||
self.assertTrue(response.url.endswith(reverse("core:dashboard")))
|
||||
|
||||
|
||||
class DashboardTestCase(ModoTestCase):
|
||||
"""Dashboard tests."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create some data."""
|
||||
super(DashboardTestCase, cls).setUpTestData()
|
||||
cls.dadmin = factories.UserFactory(
|
||||
username="admin@test.com", groups=("DomainAdmins",)
|
||||
)
|
||||
cls.user = factories.UserFactory(
|
||||
username="user@test.com", groups=("SimpleUsers",)
|
||||
)
|
||||
|
||||
def test_access(self):
|
||||
"""Load dashboard."""
|
||||
url = reverse("core:dashboard")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Latest news", response.content)
|
||||
self.client.logout()
|
||||
self.client.login(username=self.dadmin.username, password="toto")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Latest news", response.content)
|
||||
self.client.logout()
|
||||
self.client.login(username=self.user.username, password="toto")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_root_dispatch(self):
|
||||
"""Check root dispatching."""
|
||||
url = reverse("core:root")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith(reverse("core:dashboard")))
|
||||
|
||||
|
||||
class LDAPTestCaseMixin(object):
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
"""Core urls."""
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
|
||||
url(r'^$', views.RootDispatchView.as_view(), name="root"),
|
||||
url(r'^dashboard/$', views.DashboardView.as_view(), name="dashboard"),
|
||||
|
||||
url(r'^accounts/login/$', 'modoboa.core.views.auth.dologin',
|
||||
name="login"),
|
||||
url(r'^accounts/logout/$', 'modoboa.core.views.auth.dologout',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Core views."""
|
||||
|
||||
from .base import RootDispatchView
|
||||
from .dashboard import DashboardView
|
||||
|
||||
__all__ = [
|
||||
"DashboardView",
|
||||
"RootDispatchView",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# coding: utf-8
|
||||
"""Core authentication views."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
@@ -12,6 +14,10 @@ from modoboa.core.forms import LoginForm
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.web_utils import _render_to_string
|
||||
|
||||
from .base import find_nextlocation
|
||||
|
||||
logger = logging.getLogger("modoboa.auth")
|
||||
|
||||
|
||||
def dologin(request):
|
||||
"""Try to authenticate."""
|
||||
@@ -23,10 +29,6 @@ def dologin(request):
|
||||
user = authenticate(username=form.cleaned_data["username"],
|
||||
password=form.cleaned_data["password"])
|
||||
if user and user.is_active:
|
||||
nextlocation = None
|
||||
if not user.last_login:
|
||||
# Redirect to profile on first login
|
||||
nextlocation = reverse("core:user_index")
|
||||
login(request, user)
|
||||
if not form.cleaned_data["rememberme"]:
|
||||
request.session.set_expiry(0)
|
||||
@@ -41,15 +43,7 @@ def dologin(request):
|
||||
events.raiseEvent("UserLogin", request,
|
||||
form.cleaned_data["username"],
|
||||
form.cleaned_data["password"])
|
||||
|
||||
if nextlocation is None:
|
||||
nextlocation = request.POST.get("next", None)
|
||||
if nextlocation is None or nextlocation == "None":
|
||||
if user.group == "SimpleUsers":
|
||||
nextlocation = reverse("topredirection")
|
||||
else:
|
||||
nextlocation = reverse("admin:domain_list")
|
||||
return HttpResponseRedirect(nextlocation)
|
||||
return HttpResponseRedirect(find_nextlocation(request, user))
|
||||
error = _(
|
||||
"Your username and password didn't match. Please try again.")
|
||||
logger.warning(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Base core views."""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.views import generic
|
||||
|
||||
from django.contrib.auth import mixins as auth_mixins
|
||||
|
||||
from modoboa.lib import parameters
|
||||
|
||||
from ..extensions import exts_pool
|
||||
|
||||
|
||||
def find_nextlocation(request, user):
|
||||
"""Find next location for given user after login."""
|
||||
if not user.last_login:
|
||||
# Redirect to profile on first login
|
||||
return reverse("core:user_index")
|
||||
nextlocation = request.POST.get("next", None)
|
||||
if nextlocation is None or nextlocation == "None":
|
||||
if request.user.role == "SimpleUsers":
|
||||
topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION")
|
||||
if topredir != "user":
|
||||
infos = exts_pool.get_extension_infos(topredir)
|
||||
nextlocation = infos["url"] if infos["url"] else infos["name"]
|
||||
else:
|
||||
nextlocation = reverse("core:user_index")
|
||||
else:
|
||||
nextlocation = reverse("core:dashboard")
|
||||
return nextlocation
|
||||
|
||||
|
||||
class RootDispatchView(auth_mixins.LoginRequiredMixin, generic.RedirectView):
|
||||
"""Handle root dispatching based on role."""
|
||||
|
||||
def get_redirect_url(self):
|
||||
"""Find proper next hop."""
|
||||
return find_nextlocation(self.request, self.request.user)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Core dashboard views."""
|
||||
|
||||
from dateutil import parser
|
||||
import feedparser
|
||||
|
||||
from django.views import generic
|
||||
|
||||
from django.contrib.auth import mixins as auth_mixins
|
||||
|
||||
|
||||
class DashboardView(auth_mixins.AccessMixin, generic.TemplateView):
|
||||
"""Dashboard view."""
|
||||
|
||||
template_name = "core/dashboard.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""Check if user can access dashboard."""
|
||||
if not request.user.is_admin:
|
||||
return self.handle_no_permission()
|
||||
return super(DashboardView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add context variables."""
|
||||
context = super(DashboardView, self).get_context_data(**kwargs)
|
||||
context.update({"selection": "dashboard"})
|
||||
if self.request.user.language == "fr":
|
||||
lang = "fr"
|
||||
else:
|
||||
lang = "en"
|
||||
posts = feedparser.parse(
|
||||
"https://modoboa.org/{}/weblog/feeds/".format(lang))
|
||||
entries = []
|
||||
for entry in posts["entries"][:5]:
|
||||
entry["published"] = parser.parse(entry["published"])
|
||||
entries.append(entry)
|
||||
context.update({"news": entries})
|
||||
return context
|
||||
@@ -10,9 +10,7 @@ import sys
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
@@ -138,35 +136,6 @@ def size2integer(value):
|
||||
return 0
|
||||
|
||||
|
||||
@login_required
|
||||
def topredirection(request):
|
||||
"""Simple view to redirect the request when no application is specified.
|
||||
|
||||
The default "top redirection" can be specified in the *Admin >
|
||||
Settings* panel. It is the application that will be
|
||||
launched. Those not allowed to access the application will be
|
||||
redirected to their preferences page.
|
||||
|
||||
This feature only applies to simple users.
|
||||
|
||||
:param request: a Request object
|
||||
"""
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.core.extensions import exts_pool
|
||||
|
||||
if request.user.group == 'SimpleUsers':
|
||||
topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="core")
|
||||
if topredir != "user":
|
||||
infos = exts_pool.get_extension_infos(topredir)
|
||||
path = infos["url"] if infos["url"] else infos["name"]
|
||||
else:
|
||||
path = reverse("core:user_index")
|
||||
else:
|
||||
# FIXME
|
||||
path = reverse("admin:domain_list")
|
||||
return HttpResponseRedirect(path)
|
||||
|
||||
|
||||
class NavigationParameters(object):
|
||||
"""
|
||||
Just a simple object to manipulate navigation parameters.
|
||||
|
||||
@@ -7,7 +7,6 @@ from modoboa.lib import events
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^$', 'modoboa.lib.web_utils.topredirection', name="topredirection"),
|
||||
(r'^jsi18n/$', 'django.views.i18n.javascript_catalog',
|
||||
{'packages': ('modoboa', ), }),
|
||||
('', include('modoboa.core.urls', namespace="core")),
|
||||
|
||||
@@ -19,3 +19,5 @@ gevent==1.1.1
|
||||
ipaddress
|
||||
dnspython==1.12.0
|
||||
jsonfield==1.0.3
|
||||
py-dateutil==2.2
|
||||
feedparser==5.2.1
|
||||
|
||||
Reference in New Issue
Block a user