diff --git a/modoboa/core/constants.py b/modoboa/core/constants.py
index f9a27a871..4130866ed 100644
--- a/modoboa/core/constants.py
+++ b/modoboa/core/constants.py
@@ -2,6 +2,11 @@
"""Core application constants."""
+ADMIN_GROUPS = [
+ "SuperAdmins",
+ "Resellers",
+ "DomainAdmins",
+]
LANGUAGES = (
("cs", u"čeština"),
diff --git a/modoboa/core/models.py b/modoboa/core/models.py
index a7bac652e..870be90c9 100644
--- a/modoboa/core/models.py
+++ b/modoboa/core/models.py
@@ -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
diff --git a/modoboa/core/templates/core/dashboard.html b/modoboa/core/templates/core/dashboard.html
new file mode 100644
index 000000000..24a87524e
--- /dev/null
+++ b/modoboa/core/templates/core/dashboard.html
@@ -0,0 +1,40 @@
+{% extends "fluid.html" %}
+
+{% load i18n %}
+
+{% block container_content %}
+
+
+
+
{% blocktrans with user=user trimmed %}Hello {{ user }}.{% endblocktrans %}
+
+
+
+
+
+
{% trans "Latest news" %}
+
+
+
+ {% for post in news %}
+
+ | {{ post.title }} |
+ {{ post.published|date:"DATE_FORMAT" }} |
+
+ {% endfor %}
+
+
+
+
+ {% blocktrans trimmed %}
+ Visit to the
official weblog for more information.
+ {% endblocktrans %}
+
+
+
+
+
+
+
+
+{% endblock container_content %}
diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py
index b1ef9afc5..1f508ab7d 100644
--- a/modoboa/core/templatetags/core_tags.py
+++ b/modoboa/core/templatetags/core_tags.py
@@ -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",
diff --git a/modoboa/core/tests.py b/modoboa/core/tests.py
index 16871539f..58093eb23 100644
--- a/modoboa/core/tests.py
+++ b/modoboa/core/tests.py
@@ -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):
diff --git a/modoboa/core/urls.py b/modoboa/core/urls.py
index 61c56ceca..f2050f9f4 100644
--- a/modoboa/core/urls.py
+++ b/modoboa/core/urls.py
@@ -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',
diff --git a/modoboa/core/views/__init__.py b/modoboa/core/views/__init__.py
index e69de29bb..d674802fe 100644
--- a/modoboa/core/views/__init__.py
+++ b/modoboa/core/views/__init__.py
@@ -0,0 +1,9 @@
+"""Core views."""
+
+from .base import RootDispatchView
+from .dashboard import DashboardView
+
+__all__ = [
+ "DashboardView",
+ "RootDispatchView",
+]
diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py
index ad115044f..ea9c69fd8 100644
--- a/modoboa/core/views/auth.py
+++ b/modoboa/core/views/auth.py
@@ -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(
diff --git a/modoboa/core/views/base.py b/modoboa/core/views/base.py
new file mode 100644
index 000000000..bb61722e9
--- /dev/null
+++ b/modoboa/core/views/base.py
@@ -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)
diff --git a/modoboa/core/views/dashboard.py b/modoboa/core/views/dashboard.py
new file mode 100644
index 000000000..a375a65c5
--- /dev/null
+++ b/modoboa/core/views/dashboard.py
@@ -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
diff --git a/modoboa/lib/web_utils.py b/modoboa/lib/web_utils.py
index eb06c157d..e83136c52 100644
--- a/modoboa/lib/web_utils.py
+++ b/modoboa/lib/web_utils.py
@@ -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.
diff --git a/modoboa/urls.py b/modoboa/urls.py
index 0d5069c95..8568c5558 100644
--- a/modoboa/urls.py
+++ b/modoboa/urls.py
@@ -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")),
diff --git a/requirements.txt b/requirements.txt
index bb39671ba..c11bde24b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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