Commit 7e6d852b authored by Loic Bistuer's avatar Loic Bistuer Committed by Aymeric Augustin
Browse files

Fixed #20663 -- "Today" and "now" admin shortcuts.

Changed the shortcuts next to date and time intput widgets
to account for the current timezone.

Refs #7717, #14253 and #18768.
parent 404870ee
Loading
Loading
Loading
Loading
+77 −9
Original line number Diff line number Diff line
@@ -14,6 +14,8 @@ var DateTimeShortcuts = {
    clockDivName: 'clockbox',        // name of clock <div> that gets toggled
    clockLinkName: 'clocklink',      // name of the link that is used to toggle
    shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts
    timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch
    timezoneOffset: 0,
    admin_media_prefix: '',
    init: function() {
        // Get admin_media_prefix by grabbing it off the window object. It's
@@ -26,17 +28,77 @@ var DateTimeShortcuts = {
            DateTimeShortcuts.admin_media_prefix = '/missing-admin-media-prefix/';
        }

        if (window.__admin_utc_offset__ != undefined) {
            var serverOffset = window.__admin_utc_offset__;
            var localOffset = new Date().getTimezoneOffset() * -60;
            DateTimeShortcuts.timezoneOffset = localOffset - serverOffset;
        }

        var inputs = document.getElementsByTagName('input');
        for (i=0; i<inputs.length; i++) {
            var inp = inputs[i];
            if (inp.getAttribute('type') == 'text' && inp.className.match(/vTimeField/)) {
                DateTimeShortcuts.addClock(inp);
                DateTimeShortcuts.addTimezoneWarning(inp);
            }
            else if (inp.getAttribute('type') == 'text' && inp.className.match(/vDateField/)) {
                DateTimeShortcuts.addCalendar(inp);
                DateTimeShortcuts.addTimezoneWarning(inp);
            }
        }
    },
    // Return the current time while accounting for the server timezone.
    now: function() {
        if (window.__admin_utc_offset__ != undefined) {
            var serverOffset = window.__admin_utc_offset__;
            var localNow = new Date();
            var localOffset = localNow.getTimezoneOffset() * -60;
            localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset));
            return localNow;
        } else {
            return new Date();
        }
    },
    // Add a warning when the time zone in the browser and backend do not match.
    addTimezoneWarning: function(inp) {
        var $ = django.jQuery;
        var warningClass = DateTimeShortcuts.timezoneWarningClass;
        var timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600;

        // Only warn if there is a time zone mismatch.
        if (!timezoneOffset)
            return;

        // Check if warning is already there.
        if ($(inp).siblings('.' + warningClass).length)
            return;

        var message;
        if (timezoneOffset > 0) {
            message = ngettext(
                'Note: You are %s hour ahead of server time.',
                'Note: You are %s hours ahead of server time.',
                timezoneOffset
            );
        }
        else {
            timezoneOffset *= -1
            message = ngettext(
                'Note: You are %s hour behind server time.',
                'Note: You are %s hours behind server time.',
                timezoneOffset
            );
        }
        message = interpolate(message, [timezoneOffset]);

        var $warning = $('<span>');
        $warning.attr('class', warningClass);
        $warning.text(message);

        $(inp).parent()
            .append($('<br>'))
            .append($warning)
    },
    // Add clock widget to a given field
    addClock: function(inp) {
        var num = DateTimeShortcuts.clockInputs.length;
@@ -48,7 +110,7 @@ var DateTimeShortcuts = {
        shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
        inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
        var now_link = document.createElement('a');
        now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + get_format('TIME_INPUT_FORMATS')[0] + "'));");
        now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);");
        now_link.appendChild(document.createTextNode(gettext('Now')));
        var clock_link = document.createElement('a');
        clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');');
@@ -84,11 +146,10 @@ var DateTimeShortcuts = {
        quickElement('h2', clock_box, gettext('Choose a time'));
        var time_list = quickElement('ul', clock_box, '');
        time_list.className = 'timelist';
        var time_format = get_format('TIME_INPUT_FORMATS')[0];
        quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));");
        quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + time_format + "'));");
        quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + time_format + "'));");
        quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + time_format + "'));");
        quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);");
        quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 0);");
        quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 6);");
        quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 12);");

        var cancel_p = quickElement('p', clock_box, '');
        cancel_p.className = 'calendar-cancel';
@@ -128,7 +189,14 @@ var DateTimeShortcuts = {
       removeEvent(document, 'click', DateTimeShortcuts.dismissClockFunc[num]);
    },
    handleClockQuicklink: function(num, val) {
       DateTimeShortcuts.clockInputs[num].value = val;
       var d;
       if (val == -1) {
           d = DateTimeShortcuts.now();
       }
       else {
           d = new Date(1970, 1, 1, val, 0, 0, 0)
       }
       DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]);
       DateTimeShortcuts.clockInputs[num].focus();
       DateTimeShortcuts.dismissClock(num);
    },
@@ -258,7 +326,7 @@ var DateTimeShortcuts = {
        DateTimeShortcuts.calendars[num].drawNextMonth();
    },
    handleCalendarCallback: function(num) {
        format = get_format('DATE_INPUT_FORMATS')[0];
        var format = get_format('DATE_INPUT_FORMATS')[0];
        // the format needs to be escaped a little
        format = format.replace('\\', '\\\\');
        format = format.replace('\r', '\\r');
@@ -276,7 +344,7 @@ var DateTimeShortcuts = {
               ").style.display='none';}"].join('');
    },
    handleCalendarQuickLink: function(num, offset) {
       var d = new Date();
       var d = DateTimeShortcuts.now();
       d.setDate(d.getDate() + offset)
       DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]);
       DateTimeShortcuts.calendarInputs[num].focus();
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@
<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="{% block stylesheet_ie %}{% static "admin/css/ie.css" %}{% endblock %}" /><![endif]-->
{% if LANGUAGE_BIDI %}<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}" />{% endif %}
<script type="text/javascript">window.__admin_media_prefix__ = "{% filter escapejs %}{% static "admin/" %}{% endfilter %}";</script>
<script type="text/javascript">window.__admin_utc_offset__ = "{% filter escapejs %}{% now "Z" %}{% endfilter %}";</script>
{% block extrahead %}{% endblock %}
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
</head>
+13 −0
Original line number Diff line number Diff line
@@ -17,6 +17,19 @@ deprecation process for some features`_.
What's new in Django 1.7
========================

Admin shortcuts support time zones
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The "today" and "now" shortcuts next to date and time input widgets in the
admin are now operating in the :ref:`current time zone
<default-current-time-zone>`. Previously, they used the browser time zone,
which could result in saving the wrong value when it didn't match the current
time zone on the server.

In addition, the widgets now display a help message when the browser and
server time zone are different, to clarify how the value inserted in the field
will be interpreted.

Backwards incompatible changes in 1.7
=====================================

+59 −1
Original line number Diff line number Diff line
# encoding: utf-8
from __future__ import absolute_import, unicode_literals

from datetime import datetime
from datetime import datetime, timedelta
from unittest import TestCase

from django import forms
@@ -526,6 +526,64 @@ class DateTimePickerSeleniumIETests(DateTimePickerSeleniumFirefoxTests):
    webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'


@override_settings(TIME_ZONE='Asia/Singapore')
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class DateTimePickerShortcutsSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
    available_apps = ['admin_widgets'] + AdminSeleniumWebDriverTestCase.available_apps
    fixtures = ['admin-widgets-users.xml']
    urls = "admin_widgets.urls"
    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'

    def test_date_time_picker_shortcuts(self):
        """
        Ensure that date/time/datetime picker shortcuts work in the current time zone.
        Refs #20663.

        This test case is fairly tricky, it relies on selenium still running the browser
        in the default time zone "America/Chicago" despite `override_settings` changing
        the time zone to "Asia/Singapore".
        """
        self.admin_login(username='super', password='secret', login_url='/')

        now = datetime.now()
        error_margin = timedelta(seconds=10)

        self.selenium.get('%s%s' % (self.live_server_url,
            '/admin_widgets/member/add/'))

        self.selenium.find_element_by_id('id_name').send_keys('test')

        # Click on the "today" and "now" shortcuts.
        shortcuts = self.selenium.find_elements_by_css_selector(
            '.field-birthdate .datetimeshortcuts')

        for shortcut in shortcuts:
            shortcut.find_element_by_tag_name('a').click()

        # Check that there is a time zone mismatch warning.
        # Warning: This would effectively fail if the TIME_ZONE defined in the
        # settings has the same UTC offset as "Asia/Singapore" because the
        # mismatch warning would be rightfully missing from the page.
        self.selenium.find_elements_by_css_selector(
            '.field-birthdate .timezonewarning')

        # Submit the form.
        self.selenium.find_element_by_tag_name('form').submit()
        self.wait_page_loaded()

        # Make sure that "now" in javascript is within 10 seconds
        # from "now" on the server side.
        member = models.Member.objects.get(name='test')
        self.assertGreater(member.birthdate, now - error_margin)
        self.assertLess(member.birthdate, now + error_margin)

class DateTimePickerShortcutsSeleniumChromeTests(DateTimePickerShortcutsSeleniumFirefoxTests):
    webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'

class DateTimePickerShortcutsSeleniumIETests(DateTimePickerShortcutsSeleniumFirefoxTests):
    webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'


@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):