Browse Source

Add a per-user timezone setting

David 7 years ago
parent
commit
e43ea79747

+ 1 - 0
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)

+ 1 - 0
CONTRIBUTORS.rst

@@ -296,6 +296,7 @@ Contributors
 * Tim Kamanin
 * Sergey Fedoseev
 * Harm Zeinstra
+* David Moore
 
 Translators
 ===========

+ 16 - 0
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 <https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior>`_, and must be one of the recognised formats listed in the ``DATE_INPUT_FORMATS`` or ``DATETIME_INPUT_FORMATS`` setting respectively (see `DATE_INPUT_FORMATS <https://docs.djangoproject.com/en/1.10/ref/settings/#std:setting-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

+ 7 - 0
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 <WAGTAIL_USER_TIME_ZONES>` setting. This feature was developed by David Moore.
+
+
 Other features
 ~~~~~~~~~~~~~~
 

+ 1 - 0
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

+ 7 - 1
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():

+ 20 - 0
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 %}
+
+    <div class="nice-padding">
+        <form action="{% url 'wagtailadmin_account_current_time_zone' %}" method="POST" novalidate>
+            {% csrf_token %}
+            <ul class="fields">
+                {% for field in form %}
+                    {% include "wagtailadmin/shared/field_as_li.html" with field=field %}
+                {% endfor %}
+                <li class="submit"><input type="submit" value="{% trans 'Update' %}" class="button" /></li>
+            </ul>
+        </form>
+    </div>
+{% endblock %}

+ 65 - 1
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):
     """

+ 5 - 0
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'),
 ]
 

+ 5 - 0
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"
 

+ 18 - 1
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'
 

+ 13 - 1
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

+ 20 - 1
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",)

+ 18 - 0
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'),
+        ),
+    ]

+ 10 - 0
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()