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 %} + + + + + {% endfor %} + +
{{ post.title }}{{ post.published|date:"DATE_FORMAT" }}
+ +
+ {% 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