From e43ea7974790c56061fb341a76883917fc065c90 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 9 Apr 2018 18:56:10 +1200 Subject: [PATCH] Add a per-user timezone setting --- CHANGELOG.txt | 1 + CONTRIBUTORS.rst | 1 + docs/advanced_topics/settings.rst | 16 +++++ docs/releases/2.1.rst | 7 ++ setup.py | 1 + wagtail/admin/decorators.py | 8 ++- .../account/current_time_zone.html | 20 ++++++ .../admin/tests/test_account_management.py | 66 ++++++++++++++++++- wagtail/admin/urls/__init__.py | 5 ++ wagtail/admin/utils.py | 5 ++ wagtail/admin/views/account.py | 19 +++++- wagtail/admin/wagtail_hooks.py | 14 +++- wagtail/users/forms.py | 21 +++++- .../0007_userprofile_current_time_zone.py | 18 +++++ wagtail/users/models.py | 10 +++ 15 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 wagtail/admin/templates/wagtailadmin/account/current_time_zone.html create mode 100644 wagtail/users/migrations/0007_userprofile_current_time_zone.py diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cd4255481..d8c32ae5b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -6,6 +6,7 @@ Changelog * Add `HelpPanel` to add HTML within an edit form (Keving Chung) * Added API endpoint for finding pages by HTML path (Karl Hobley) + * Added time zone setting to account preferences (David Moore) * Persist tab hash in URL to allow direct navigation to tabs in the admin interface (Ben Weatherman) * Animate the chevron icon when opening sub-menus in the admin (Carlo Ascani) * Look through the target link and target page slug (in addition to the old slug) when searching for redirects in the admin (Michael Harrison) diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index f7c1d0c25..4a09d426a 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -296,6 +296,7 @@ Contributors * Tim Kamanin * Sergey Fedoseev * Harm Zeinstra +* David Moore Translators =========== diff --git a/docs/advanced_topics/settings.rst b/docs/advanced_topics/settings.rst index f9dd28135..e0a3e29c7 100644 --- a/docs/advanced_topics/settings.rst +++ b/docs/advanced_topics/settings.rst @@ -426,6 +426,22 @@ Date and DateTime inputs Specifies the date and datetime format to be used in input fields in the Wagtail admin. The format is specified in `Python datetime module syntax `_, and must be one of the recognised formats listed in the ``DATE_INPUT_FORMATS`` or ``DATETIME_INPUT_FORMATS`` setting respectively (see `DATE_INPUT_FORMATS `_). +.. _WAGTAIL_USER_TIME_ZONES: + +Time zones +---------- + +Logged-in users can choose their current time zone for the admin interface in the account settings. If is no time zone selected by the user, then ``TIME_ZONE`` will be used. +(Note that time zones are only applied to datetime fields, not to plain time or date fields. This is a Django design decision.) + +The list of time zones is by default the common_timezones list from pytz. +It is possible to override this list via the ``WAGTAIL_USER_TIME_ZONES`` setting. +If there is zero or one time zone permitted, the account settings form will be hidden. + +.. code-block:: python + + WAGTAIL_USER_TIME_ZONES = ['America/Chicago', 'Australia/Sydney', 'Europe/Rome'] + .. _WAGTAILADMIN_PERMITTED_LANGUAGES: Admin languages diff --git a/docs/releases/2.1.rst b/docs/releases/2.1.rst index 37c619877..467902333 100644 --- a/docs/releases/2.1.rst +++ b/docs/releases/2.1.rst @@ -22,6 +22,13 @@ API lookup by page path The API now includes an endpoint for finding pages by path; see :ref:`apiv2_finding_pages_by_path`. This feature was developed by Karl Hobley. + +User time zone setting +~~~~~~~~~~~~~~~~~~~~~~ + +Users can now set their current time zone through the Account Settings menu, which will then be reflected in date / time fields throughout the admin (such as go-live / expiry dates). The list of available time zones can be configured via the :ref:`WAGTAIL_USER_TIME_ZONES ` setting. This feature was developed by David Moore. + + Other features ~~~~~~~~~~~~~~ diff --git a/setup.py b/setup.py index ce7ca28bc..0693ec0c1 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ install_requires = [ "Unidecode>=0.04.14,<1.0", "Willow>=1.1,<1.2", "requests>=2.11.1,<3.0", + "l18n", ] # Testing dependencies diff --git a/wagtail/admin/decorators.py b/wagtail/admin/decorators.py index 64397fa6f..f6e09af94 100644 --- a/wagtail/admin/decorators.py +++ b/wagtail/admin/decorators.py @@ -1,9 +1,11 @@ from django.contrib.auth.views import redirect_to_login as auth_redirect_to_login from django.core.exceptions import PermissionDenied from django.urls import reverse +from django.utils.timezone import activate as activate_tz from django.utils.translation import activate as activate_lang from django.utils.translation import ugettext as _ +import l18n from wagtail.admin import messages @@ -24,7 +26,11 @@ def require_admin_access(view_func): if user.has_perms(['wagtailadmin.access_admin']): if hasattr(user, 'wagtail_userprofile'): - activate_lang(user.wagtail_userprofile.get_preferred_language()) + language = user.wagtail_userprofile.get_preferred_language() + l18n.set_language(language) + activate_lang(language) + time_zone = user.wagtail_userprofile.get_current_time_zone() + activate_tz(time_zone) return view_func(request, *args, **kwargs) if not request.is_ajax(): diff --git a/wagtail/admin/templates/wagtailadmin/account/current_time_zone.html b/wagtail/admin/templates/wagtailadmin/account/current_time_zone.html new file mode 100644 index 000000000..32653c4b7 --- /dev/null +++ b/wagtail/admin/templates/wagtailadmin/account/current_time_zone.html @@ -0,0 +1,20 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} + +{% block titletag %}{% trans "Set Time Zone" %}{% endblock %} +{% block content %} + {% trans "Set Time Zone" as prefs_str %} + {% include "wagtailadmin/shared/header.html" with title=prefs_str %} + +
+
+ {% csrf_token %} +
    + {% for field in form %} + {% include "wagtailadmin/shared/field_as_li.html" with field=field %} + {% endfor %} +
  • +
+
+
+{% endblock %} diff --git a/wagtail/admin/tests/test_account_management.py b/wagtail/admin/tests/test_account_management.py index 70fcbece5..b83672c80 100644 --- a/wagtail/admin/tests/test_account_management.py +++ b/wagtail/admin/tests/test_account_management.py @@ -1,3 +1,5 @@ +import pytz + from django.contrib.auth import views as auth_views from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission @@ -6,7 +8,8 @@ from django.core import mail from django.test import TestCase, override_settings from django.urls import reverse -from wagtail.admin.utils import WAGTAILADMIN_PROVIDED_LANGUAGES, get_available_admin_languages +from wagtail.admin.utils import ( + WAGTAILADMIN_PROVIDED_LANGUAGES, get_available_admin_languages, get_available_admin_time_zones) from wagtail.tests.utils import WagtailTestUtils from wagtail.users.models import UserProfile @@ -403,6 +406,67 @@ class TestAccountSection(TestCase, WagtailTestUtils): response = self.client.post(reverse('wagtailadmin_account')) self.assertNotContains(response, 'Language Preferences') + def test_current_time_zone_view(self): + """ + This tests that the current time zone view responds with an index page + """ + # Get account page + response = self.client.get(reverse('wagtailadmin_account_current_time_zone')) + + # Check that the user received an account page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/account/current_time_zone.html') + + # Page should contain a 'Set Time Zone' title + self.assertContains(response, "Set Time Zone") + + def test_current_time_zone_view_post(self): + """ + This posts to the current time zone view and checks that the + user profile is updated + """ + # Post new values to the current time zone page + post_data = { + 'current_time_zone': 'Pacific/Fiji' + } + response = self.client.post(reverse('wagtailadmin_account_current_time_zone'), post_data) + + # Check that the user was redirected to the account page + self.assertRedirects(response, reverse('wagtailadmin_account')) + + profile = UserProfile.get_for_user(get_user_model().objects.get(pk=self.user.pk)) + + # Check that the current time zone is stored + self.assertEqual(profile.current_time_zone, 'Pacific/Fiji') + + def test_unset_current_time_zone(self): + # Post new values to the current time zone page + post_data = { + 'current_time_zone': '' + } + response = self.client.post(reverse('wagtailadmin_account_current_time_zone'), post_data) + + # Check that the user was redirected to the account page + self.assertRedirects(response, reverse('wagtailadmin_account')) + + profile = UserProfile.get_for_user(get_user_model().objects.get(pk=self.user.pk)) + + # Check that the current time zone are stored + self.assertEqual(profile.current_time_zone, '') + + @override_settings(WAGTAIL_USER_TIME_ZONES=['Africa/Addis_Ababa', 'America/Argentina/Buenos_Aires']) + def test_available_admin_time_zones_with_permitted_time_zones(self): + self.assertListEqual(get_available_admin_time_zones(), + ['Africa/Addis_Ababa', 'America/Argentina/Buenos_Aires']) + + def test_available_admin_time_zones_by_default(self): + self.assertListEqual(get_available_admin_time_zones(), pytz.common_timezones) + + @override_settings(WAGTAIL_USER_TIME_ZONES=['Europe/London']) + def test_not_show_options_if_only_one_time_zone_is_permitted(self): + response = self.client.post(reverse('wagtailadmin_account')) + self.assertNotContains(response, 'Set Time Zone') + class TestAccountManagementForNonModerator(TestCase, WagtailTestUtils): """ diff --git a/wagtail/admin/urls/__init__.py b/wagtail/admin/urls/__init__.py index f05c559c8..e0325fce3 100644 --- a/wagtail/admin/urls/__init__.py +++ b/wagtail/admin/urls/__init__.py @@ -55,6 +55,11 @@ urlpatterns = [ account.language_preferences, name='wagtailadmin_account_language_preferences' ), + url( + r'^account/current_time_zone/$', + account.current_time_zone, + name='wagtailadmin_account_current_time_zone' + ), url(r'^logout/$', account.LogoutView.as_view(), name='wagtailadmin_logout'), ] diff --git a/wagtail/admin/utils.py b/wagtail/admin/utils.py index 8bc3f1d72..81ecaa363 100644 --- a/wagtail/admin/utils.py +++ b/wagtail/admin/utils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import logging from functools import wraps +import pytz from django.conf import settings from django.contrib.auth import get_user_model @@ -53,6 +54,10 @@ def get_available_admin_languages(): return getattr(settings, 'WAGTAILADMIN_PERMITTED_LANGUAGES', WAGTAILADMIN_PROVIDED_LANGUAGES) +def get_available_admin_time_zones(): + return getattr(settings, 'WAGTAIL_USER_TIME_ZONES', pytz.common_timezones) + + def get_object_usage(obj): "Returns a queryset of pages that link to a particular object" diff --git a/wagtail/admin/views/account.py b/wagtail/admin/views/account.py index 1c7952fc5..0a271d11b 100644 --- a/wagtail/admin/views/account.py +++ b/wagtail/admin/views/account.py @@ -11,7 +11,8 @@ from django.utils.translation import activate from wagtail.admin import forms from wagtail.core import hooks -from wagtail.users.forms import EmailForm, NotificationPreferencesForm, PreferredLanguageForm +from wagtail.users.forms import ( + CurrentTimeZoneForm, EmailForm, NotificationPreferencesForm, PreferredLanguageForm) from wagtail.users.models import UserProfile from wagtail.utils.loading import get_custom_form @@ -168,6 +169,22 @@ def language_preferences(request): }) +def current_time_zone(request): + if request.method == 'POST': + form = CurrentTimeZoneForm(request.POST, instance=UserProfile.get_for_user(request.user)) + + if form.is_valid(): + form.save() + messages.success(request, _("Your preferences have been updated.")) + return redirect('wagtailadmin_account') + else: + form = CurrentTimeZoneForm(instance=UserProfile.get_for_user(request.user)) + + return render(request, 'wagtailadmin/account/current_time_zone.html', { + 'form': form, + }) + + class LoginView(auth_views.LoginView): template_name = 'wagtailadmin/login.html' diff --git a/wagtail/admin/wagtail_hooks.py b/wagtail/admin/wagtail_hooks.py index 8ca108b66..c709902b9 100644 --- a/wagtail/admin/wagtail_hooks.py +++ b/wagtail/admin/wagtail_hooks.py @@ -15,7 +15,9 @@ from wagtail.admin.rich_text.converters.html_to_contentstate import ( BlockElementHandler, ExternalLinkElementHandler, HorizontalRuleHandler, InlineStyleElementHandler, ListElementHandler, ListItemElementHandler, PageLinkElementHandler) from wagtail.admin.search import SearchArea -from wagtail.admin.utils import get_available_admin_languages, user_has_any_page_permission +from wagtail.admin.utils import ( + get_available_admin_languages, get_available_admin_time_zones, + user_has_any_page_permission) from wagtail.admin.views.account import password_management_enabled from wagtail.admin.viewsets import viewsets from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook, PageListingButton @@ -244,6 +246,16 @@ def register_account_preferred_language_preferences(request): } +@hooks.register('register_account_menu_item') +def register_account_current_time_zone(request): + if len(get_available_admin_time_zones()) > 1: + return { + 'url': reverse('wagtailadmin_account_current_time_zone'), + 'label': _('Current Time Zone'), + 'help_text': _('Choose your current time zone.'), + } + + @hooks.register('register_rich_text_features') def register_core_features(features): # Hallo.js diff --git a/wagtail/users/forms.py b/wagtail/users/forms.py index 5299cd9bb..ae9d39381 100644 --- a/wagtail/users/forms.py +++ b/wagtail/users/forms.py @@ -1,4 +1,5 @@ from itertools import groupby +from operator import itemgetter from django import forms from django.conf import settings @@ -12,7 +13,8 @@ from django.template.loader import render_to_string from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _ -from wagtail.admin.utils import get_available_admin_languages +import l18n +from wagtail.admin.utils import get_available_admin_languages, get_available_admin_time_zones from wagtail.admin.widgets import AdminPageChooser from wagtail.core import hooks from wagtail.core.models import ( @@ -399,3 +401,20 @@ class EmailForm(forms.ModelForm): class Meta: model = User fields = ("email", ) + + +class CurrentTimeZoneForm(forms.ModelForm): + def _get_time_zone_choices(): + time_zones = [(tz, str(l18n.tz_fullnames.get(tz, tz))) + for tz in get_available_admin_time_zones()] + time_zones.sort(key=itemgetter(1)) + return BLANK_CHOICE_DASH + time_zones + + current_time_zone = forms.ChoiceField( + required=False, + choices=_get_time_zone_choices + ) + + class Meta: + model = UserProfile + fields = ("current_time_zone",) diff --git a/wagtail/users/migrations/0007_userprofile_current_time_zone.py b/wagtail/users/migrations/0007_userprofile_current_time_zone.py new file mode 100644 index 000000000..db1545205 --- /dev/null +++ b/wagtail/users/migrations/0007_userprofile_current_time_zone.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.4 on 2018-04-07 01:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailusers', '0006_userprofile_prefered_language'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='current_time_zone', + field=models.CharField(default='', help_text='Select your current time zone', max_length=40, verbose_name='current time zone'), + ), + ] diff --git a/wagtail/users/models.py b/wagtail/users/models.py index c47acadd2..00dc767e4 100644 --- a/wagtail/users/models.py +++ b/wagtail/users/models.py @@ -33,6 +33,13 @@ class UserProfile(models.Model): default='' ) + current_time_zone = models.CharField( + verbose_name=_('current time zone'), + max_length=40, + help_text=_("Select your current time zone"), + default='' + ) + @classmethod def get_for_user(cls, user): return cls.objects.get_or_create(user=user)[0] @@ -40,6 +47,9 @@ class UserProfile(models.Model): def get_preferred_language(self): return self.preferred_language or settings.LANGUAGE_CODE + def get_current_time_zone(self): + return self.current_time_zone or settings.TIME_ZONE + def __str__(self): return self.user.get_username()