Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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.
  • Loading branch information...
commit 7e6d852bac4de2d5ed2d5ddeabf71482d644ef51 1 parent 404870e
Loic Bistuer authored aaugustin committed
86  django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
@@ -14,6 +14,8 @@ var DateTimeShortcuts = {
14 14
     clockDivName: 'clockbox',        // name of clock <div> that gets toggled
15 15
     clockLinkName: 'clocklink',      // name of the link that is used to toggle
16 16
     shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts
  17
+    timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch
  18
+    timezoneOffset: 0,
17 19
     admin_media_prefix: '',
18 20
     init: function() {
19 21
         // Get admin_media_prefix by grabbing it off the window object. It's
@@ -26,17 +28,77 @@ var DateTimeShortcuts = {
26 28
             DateTimeShortcuts.admin_media_prefix = '/missing-admin-media-prefix/';
27 29
         }
28 30
 
  31
+        if (window.__admin_utc_offset__ != undefined) {
  32
+            var serverOffset = window.__admin_utc_offset__;
  33
+            var localOffset = new Date().getTimezoneOffset() * -60;
  34
+            DateTimeShortcuts.timezoneOffset = localOffset - serverOffset;
  35
+        }
  36
+
29 37
         var inputs = document.getElementsByTagName('input');
30 38
         for (i=0; i<inputs.length; i++) {
31 39
             var inp = inputs[i];
32 40
             if (inp.getAttribute('type') == 'text' && inp.className.match(/vTimeField/)) {
33 41
                 DateTimeShortcuts.addClock(inp);
  42
+                DateTimeShortcuts.addTimezoneWarning(inp);
34 43
             }
35 44
             else if (inp.getAttribute('type') == 'text' && inp.className.match(/vDateField/)) {
36 45
                 DateTimeShortcuts.addCalendar(inp);
  46
+                DateTimeShortcuts.addTimezoneWarning(inp);
37 47
             }
38 48
         }
39 49
     },
  50
+    // Return the current time while accounting for the server timezone.
  51
+    now: function() {
  52
+        if (window.__admin_utc_offset__ != undefined) {
  53
+            var serverOffset = window.__admin_utc_offset__;
  54
+            var localNow = new Date();
  55
+            var localOffset = localNow.getTimezoneOffset() * -60;
  56
+            localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset));
  57
+            return localNow;
  58
+        } else {
  59
+            return new Date();
  60
+        }
  61
+    },
  62
+    // Add a warning when the time zone in the browser and backend do not match.
  63
+    addTimezoneWarning: function(inp) {
  64
+        var $ = django.jQuery;
  65
+        var warningClass = DateTimeShortcuts.timezoneWarningClass;
  66
+        var timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600;
  67
+
  68
+        // Only warn if there is a time zone mismatch.
  69
+        if (!timezoneOffset)
  70
+            return;
  71
+
  72
+        // Check if warning is already there.
  73
+        if ($(inp).siblings('.' + warningClass).length)
  74
+            return;
  75
+
  76
+        var message;
  77
+        if (timezoneOffset > 0) {
  78
+            message = ngettext(
  79
+                'Note: You are %s hour ahead of server time.',
  80
+                'Note: You are %s hours ahead of server time.',
  81
+                timezoneOffset
  82
+            );
  83
+        }
  84
+        else {
  85
+            timezoneOffset *= -1
  86
+            message = ngettext(
  87
+                'Note: You are %s hour behind server time.',
  88
+                'Note: You are %s hours behind server time.',
  89
+                timezoneOffset
  90
+            );
  91
+        }
  92
+        message = interpolate(message, [timezoneOffset]);
  93
+
  94
+        var $warning = $('<span>');
  95
+        $warning.attr('class', warningClass);
  96
+        $warning.text(message);
  97
+
  98
+        $(inp).parent()
  99
+            .append($('<br>'))
  100
+            .append($warning)
  101
+    },
40 102
     // Add clock widget to a given field
41 103
     addClock: function(inp) {
42 104
         var num = DateTimeShortcuts.clockInputs.length;
@@ -48,7 +110,7 @@ var DateTimeShortcuts = {
48 110
         shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
49 111
         inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
50 112
         var now_link = document.createElement('a');
51  
-        now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + get_format('TIME_INPUT_FORMATS')[0] + "'));");
  113
+        now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);");
52 114
         now_link.appendChild(document.createTextNode(gettext('Now')));
53 115
         var clock_link = document.createElement('a');
54 116
         clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');');
@@ -84,11 +146,10 @@ var DateTimeShortcuts = {
84 146
         quickElement('h2', clock_box, gettext('Choose a time'));
85 147
         var time_list = quickElement('ul', clock_box, '');
86 148
         time_list.className = 'timelist';
87  
-        var time_format = get_format('TIME_INPUT_FORMATS')[0];
88  
-        quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));");
89  
-        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 + "'));");
90  
-        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 + "'));");
91  
-        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 + "'));");
  149
+        quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);");
  150
+        quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 0);");
  151
+        quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 6);");
  152
+        quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 12);");
92 153
 
93 154
         var cancel_p = quickElement('p', clock_box, '');
94 155
         cancel_p.className = 'calendar-cancel';
@@ -128,7 +189,14 @@ var DateTimeShortcuts = {
128 189
        removeEvent(document, 'click', DateTimeShortcuts.dismissClockFunc[num]);
129 190
     },
130 191
     handleClockQuicklink: function(num, val) {
131  
-       DateTimeShortcuts.clockInputs[num].value = val;
  192
+       var d;
  193
+       if (val == -1) {
  194
+           d = DateTimeShortcuts.now();
  195
+       }
  196
+       else {
  197
+           d = new Date(1970, 1, 1, val, 0, 0, 0)
  198
+       }
  199
+       DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]);
132 200
        DateTimeShortcuts.clockInputs[num].focus();
133 201
        DateTimeShortcuts.dismissClock(num);
134 202
     },
@@ -258,7 +326,7 @@ var DateTimeShortcuts = {
258 326
         DateTimeShortcuts.calendars[num].drawNextMonth();
259 327
     },
260 328
     handleCalendarCallback: function(num) {
261  
-        format = get_format('DATE_INPUT_FORMATS')[0];
  329
+        var format = get_format('DATE_INPUT_FORMATS')[0];
262 330
         // the format needs to be escaped a little
263 331
         format = format.replace('\\', '\\\\');
264 332
         format = format.replace('\r', '\\r');
@@ -276,7 +344,7 @@ var DateTimeShortcuts = {
276 344
                ").style.display='none';}"].join('');
277 345
     },
278 346
     handleCalendarQuickLink: function(num, offset) {
279  
-       var d = new Date();
  347
+       var d = DateTimeShortcuts.now();
280 348
        d.setDate(d.getDate() + offset)
281 349
        DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]);
282 350
        DateTimeShortcuts.calendarInputs[num].focus();
1  django/contrib/admin/templates/admin/base.html
@@ -7,6 +7,7 @@
7 7
 <!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="{% block stylesheet_ie %}{% static "admin/css/ie.css" %}{% endblock %}" /><![endif]-->
8 8
 {% if LANGUAGE_BIDI %}<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}" />{% endif %}
9 9
 <script type="text/javascript">window.__admin_media_prefix__ = "{% filter escapejs %}{% static "admin/" %}{% endfilter %}";</script>
  10
+<script type="text/javascript">window.__admin_utc_offset__ = "{% filter escapejs %}{% now "Z" %}{% endfilter %}";</script>
10 11
 {% block extrahead %}{% endblock %}
11 12
 {% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
12 13
 </head>
13  docs/releases/1.7.txt
@@ -17,6 +17,19 @@ deprecation process for some features`_.
17 17
 What's new in Django 1.7
18 18
 ========================
19 19
 
  20
+Admin shortcuts support time zones
  21
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  22
+
  23
+The "today" and "now" shortcuts next to date and time input widgets in the
  24
+admin are now operating in the :ref:`current time zone
  25
+<default-current-time-zone>`. Previously, they used the browser time zone,
  26
+which could result in saving the wrong value when it didn't match the current
  27
+time zone on the server.
  28
+
  29
+In addition, the widgets now display a help message when the browser and
  30
+server time zone are different, to clarify how the value inserted in the field
  31
+will be interpreted.
  32
+
20 33
 Backwards incompatible changes in 1.7
21 34
 =====================================
22 35
 
60  tests/admin_widgets/tests.py
... ...
@@ -1,7 +1,7 @@
1 1
 # encoding: utf-8
2 2
 from __future__ import absolute_import, unicode_literals
3 3
 
4  
-from datetime import datetime
  4
+from datetime import datetime, timedelta
5 5
 from unittest import TestCase
6 6
 
7 7
 from django import forms
@@ -526,6 +526,64 @@ class DateTimePickerSeleniumIETests(DateTimePickerSeleniumFirefoxTests):
526 526
     webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
527 527
 
528 528
 
  529
+@override_settings(TIME_ZONE='Asia/Singapore')
  530
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
  531
+class DateTimePickerShortcutsSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
  532
+    available_apps = ['admin_widgets'] + AdminSeleniumWebDriverTestCase.available_apps
  533
+    fixtures = ['admin-widgets-users.xml']
  534
+    urls = "admin_widgets.urls"
  535
+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
  536
+
  537
+    def test_date_time_picker_shortcuts(self):
  538
+        """
  539
+        Ensure that date/time/datetime picker shortcuts work in the current time zone.
  540
+        Refs #20663.
  541
+
  542
+        This test case is fairly tricky, it relies on selenium still running the browser
  543
+        in the default time zone "America/Chicago" despite `override_settings` changing
  544
+        the time zone to "Asia/Singapore".
  545
+        """
  546
+        self.admin_login(username='super', password='secret', login_url='/')
  547
+
  548
+        now = datetime.now()
  549
+        error_margin = timedelta(seconds=10)
  550
+
  551
+        self.selenium.get('%s%s' % (self.live_server_url,
  552
+            '/admin_widgets/member/add/'))
  553
+
  554
+        self.selenium.find_element_by_id('id_name').send_keys('test')
  555
+
  556
+        # Click on the "today" and "now" shortcuts.
  557
+        shortcuts = self.selenium.find_elements_by_css_selector(
  558
+            '.field-birthdate .datetimeshortcuts')
  559
+
  560
+        for shortcut in shortcuts:
  561
+            shortcut.find_element_by_tag_name('a').click()
  562
+
  563
+        # Check that there is a time zone mismatch warning.
  564
+        # Warning: This would effectively fail if the TIME_ZONE defined in the
  565
+        # settings has the same UTC offset as "Asia/Singapore" because the
  566
+        # mismatch warning would be rightfully missing from the page.
  567
+        self.selenium.find_elements_by_css_selector(
  568
+            '.field-birthdate .timezonewarning')
  569
+
  570
+        # Submit the form.
  571
+        self.selenium.find_element_by_tag_name('form').submit()
  572
+        self.wait_page_loaded()
  573
+
  574
+        # Make sure that "now" in javascript is within 10 seconds
  575
+        # from "now" on the server side.
  576
+        member = models.Member.objects.get(name='test')
  577
+        self.assertGreater(member.birthdate, now - error_margin)
  578
+        self.assertLess(member.birthdate, now + error_margin)
  579
+
  580
+class DateTimePickerShortcutsSeleniumChromeTests(DateTimePickerShortcutsSeleniumFirefoxTests):
  581
+    webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'
  582
+
  583
+class DateTimePickerShortcutsSeleniumIETests(DateTimePickerShortcutsSeleniumFirefoxTests):
  584
+    webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
  585
+
  586
+
529 587
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
530 588
 class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
531 589
 

0 notes on commit 7e6d852

Please sign in to comment.
Something went wrong with that request. Please try again.