diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js index 8e3548ce5225..f6c61010d5e9 100644 --- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js @@ -52,17 +52,7 @@ }, // Return the current time while accounting for the server timezone. now: function () { - const serverOffset = document.body.dataset.adminUtcOffset; - if (serverOffset) { - const localNow = new Date(); - const localOffset = localNow.getTimezoneOffset() * -60; - localNow.setTime( - localNow.getTime() + 1000 * (serverOffset - localOffset), - ); - return localNow; - } else { - return new Date(); - } + return CalendarNamespace.serverToday(); }, // Add a warning when the time zone in the browser and backend do not match. addTimezoneWarning: function (inp) { diff --git a/django/contrib/admin/static/admin/js/calendar.js b/django/contrib/admin/static/admin/js/calendar.js index 9cef8d0c81cc..b9b45b26186b 100644 --- a/django/contrib/admin/static/admin/js/calendar.js +++ b/django/contrib/admin/static/admin/js/calendar.js @@ -103,9 +103,21 @@ depends on core.js for utility functions like removeChildren or quickElement true, ); }, + // Return the current date adjusted for the server timezone. + serverToday: function () { + const today = new Date(); + const serverOffset = document.body.dataset.adminUtcOffset; + if (serverOffset) { + const localOffset = today.getTimezoneOffset() * -60; + today.setTime( + today.getTime() + 1000 * (serverOffset - localOffset), + ); + } + return today; + }, draw: function (month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999 - const today = new Date(); + const today = CalendarNamespace.serverToday(); const todayDay = today.getDate(); const todayMonth = today.getMonth() + 1; const todayYear = today.getFullYear(); @@ -267,7 +279,7 @@ depends on core.js for utility functions like removeChildren or quickElement // calendar is clicked this.div_id = div_id; this.callback = callback; - this.today = new Date(); + this.today = CalendarNamespace.serverToday(); this.currentMonth = this.today.getMonth() + 1; this.currentYear = this.today.getFullYear(); if (typeof selected !== "undefined") { diff --git a/django/contrib/admin/templatetags/base.py b/django/contrib/admin/templatetags/base.py index c0474135ea0e..e23e45798071 100644 --- a/django/contrib/admin/templatetags/base.py +++ b/django/contrib/admin/templatetags/base.py @@ -1,8 +1,6 @@ -from inspect import getfullargspec - from django.template.exceptions import TemplateSyntaxError from django.template.library import InclusionNode, parse_bits -from django.utils.inspect import lazy_annotations +from django.utils.inspect import getfullargspec class InclusionAdminNode(InclusionNode): @@ -13,10 +11,9 @@ class InclusionAdminNode(InclusionNode): def __init__(self, name, parser, token, func, template_name, takes_context=True): self.template_name = template_name - with lazy_annotations(): - params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = ( - getfullargspec(func) - ) + params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec( + func + ) if takes_context: if params and params[0] == "context": del params[0] diff --git a/django/template/base.py b/django/template/base.py index 8c6390de33fe..ddfb150cd9a0 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -60,7 +60,7 @@ from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes from django.utils.formats import localize from django.utils.html import conditional_escape -from django.utils.inspect import lazy_annotations, signature +from django.utils.inspect import getfullargspec, signature from django.utils.regex_helper import _lazy_re_compile from django.utils.safestring import SafeData, SafeString, mark_safe from django.utils.text import get_text_list, smart_split, unescape_string_literal @@ -843,8 +843,7 @@ def args_check(name, func, provided): # Check to see if a decorator is providing the real function. func = inspect.unwrap(func) - with lazy_annotations(): - args, _, _, defaults, _, _, _ = inspect.getfullargspec(func) + args, _, _, defaults, _, _, _ = getfullargspec(func) alen = len(args) dlen = len(defaults or []) # Not enough OR Too many diff --git a/django/template/library.py b/django/template/library.py index 0a459c049787..8b2ec609eea7 100644 --- a/django/template/library.py +++ b/django/template/library.py @@ -1,10 +1,10 @@ from collections.abc import Iterable from functools import wraps from importlib import import_module -from inspect import getfullargspec, unwrap +from inspect import unwrap from django.utils.html import conditional_escape -from django.utils.inspect import lazy_annotations +from django.utils.inspect import getfullargspec from .base import Node, Template, token_kwargs from .exceptions import TemplateSyntaxError @@ -111,16 +111,15 @@ def hello(*args, **kwargs): """ def dec(func): - with lazy_annotations(): - ( - params, - varargs, - varkw, - defaults, - kwonly, - kwonly_defaults, - _, - ) = getfullargspec(unwrap(func)) + ( + params, + varargs, + varkw, + defaults, + kwonly, + kwonly_defaults, + _, + ) = getfullargspec(unwrap(func)) function_name = name or func.__name__ if takes_context: @@ -175,16 +174,15 @@ def hello(content): def dec(func): nonlocal end_name - with lazy_annotations(): - ( - params, - varargs, - varkw, - defaults, - kwonly, - kwonly_defaults, - _, - ) = getfullargspec(unwrap(func)) + ( + params, + varargs, + varkw, + defaults, + kwonly, + kwonly_defaults, + _, + ) = getfullargspec(unwrap(func)) function_name = name or func.__name__ if end_name is None: @@ -264,16 +262,15 @@ def show_results(poll): """ def dec(func): - with lazy_annotations(): - ( - params, - varargs, - varkw, - defaults, - kwonly, - kwonly_defaults, - _, - ) = getfullargspec(unwrap(func)) + ( + params, + varargs, + varkw, + defaults, + kwonly, + kwonly_defaults, + _, + ) = getfullargspec(unwrap(func)) function_name = name or func.__name__ if takes_context: diff --git a/django/utils/inspect.py b/django/utils/inspect.py index a04669fc116a..1b4d5733fd87 100644 --- a/django/utils/inspect.py +++ b/django/utils/inspect.py @@ -3,16 +3,17 @@ import threading from contextlib import contextmanager -from django.utils.version import PY314 +from django.utils.version import PY314, PY315 if PY314: import annotationlib - lock = threading.Lock() - safe_signature_from_callable = functools.partial( - inspect._signature_from_callable, - annotation_format=annotationlib.Format.FORWARDREF, - ) + if not PY315: + lock = threading.Lock() + safe_signature_from_callable = functools.partial( + inspect._signature_from_callable, + annotation_format=annotationlib.Format.FORWARDREF, + ) @functools.lru_cache(maxsize=512) @@ -106,12 +107,12 @@ def lazy_annotations(): compatibility with Python 3.14+ deferred evaluation, patch the module-level helper to provide the annotation_format that we are using elsewhere. - This private helper could be removed when there is an upstream solution for - https://github.com/python/cpython/issues/141560. + This private helper should only be used for Python 3.14, as + https://github.com/python/cpython/issues/141560 was fixed in 3.15. This context manager is not reentrant. """ - if not PY314: + if PY315 or not PY314: yield return with lock: @@ -123,6 +124,22 @@ def lazy_annotations(): inspect._signature_from_callable = original_helper +def getfullargspec(*args, annotation_format=None, **kwargs): + """ + A wrapper around inspect.getfullargspec that leaves deferred annotations + unevaluated on Python 3.14+, since they are not used in our case. + """ + if PY315: + return inspect.getfullargspec( + *args, **kwargs, annotation_format=annotationlib.Format.FORWARDREF + ) + if PY314: + with lazy_annotations(): + return inspect.getfullargspec(*args, **kwargs) + else: + return inspect.getfullargspec(*args, **kwargs) + + def signature(obj): """ A wrapper around inspect.signature that leaves deferred annotations diff --git a/js_tests/admin/DateTimeShortcuts.test.js b/js_tests/admin/DateTimeShortcuts.test.js index bf09b711d4e3..fae84b72c29c 100644 --- a/js_tests/admin/DateTimeShortcuts.test.js +++ b/js_tests/admin/DateTimeShortcuts.test.js @@ -137,3 +137,34 @@ QUnit.test("today link has aria-label with current date", function (assert) { const expectedAriaLabel = `Today (${formattedDate})`; assert.equal(todayLink.attr("aria-label"), expectedAriaLabel); }); + +QUnit.test("calendar today highlight with server offset", function (assert) { + const $ = django.jQuery; + const calDiv = $('
'); + $("#qunit-fixture").append(calDiv); + + // Simulate a server timezone that is 24 hours ahead of the browser. + const localOffset = new Date().getTimezoneOffset() * -60; + const serverOffset = localOffset + 86400; + $("body").attr("data-admin-utc-offset", serverOffset); + + const expectedDate = new Date(); + expectedDate.setTime( + expectedDate.getTime() + 1000 * (serverOffset - localOffset), + ); + + CalendarNamespace.draw( + expectedDate.getMonth() + 1, + expectedDate.getFullYear(), + "test-calendar", + function () {}, + ); + + const todayCells = calDiv.find("td.today"); + assert.equal(todayCells.length, 1, "Exactly one cell marked as today"); + assert.equal( + todayCells.find("a").text(), + String(expectedDate.getDate()), + "Today cell matches server-adjusted date", + ); +}); diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 923fac06aec4..49bda86d1f8f 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -3097,7 +3097,7 @@ def test_alter_field_reloads_state_fk_with_to_field_related_name_target_type_cha def test_alter_field_reloads_state_on_transitive_attname_to_field_type_change( self, ): - app_label = "test_alflrstfattnamettc" + app_label = "test_alflrstatftc" project_state = self.apply_operations( app_label, ProjectState(),