diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js index 671af9bd78..aa12f8cf01 100644 --- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js @@ -14,6 +14,8 @@ var DateTimeShortcuts = { clockDivName: 'clockbox', // name of clock
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 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 = $(''); + $warning.attr('class', warningClass); + $warning.text(message); + + $(inp).parent() + .append($('
')) + .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(); diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 63d4419fae..9c79a4e698 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -7,6 +7,7 @@ {% if LANGUAGE_BIDI %}{% endif %} + {% block extrahead %}{% endblock %} {% block blockbots %}{% endblock %} diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 6955ef2030..b5e0b27508 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -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 +`. 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 ===================================== diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 8edb2f5904..870aa6d455 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -1,7 +1,7 @@ # 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):