From 9fe09050924f06f30dba4b43f446c2f019ea00f4 Mon Sep 17 00:00:00 2001 From: ievgenii vdovenko Date: Thu, 7 Aug 2025 10:54:52 +0200 Subject: [PATCH 1/2] fix: fixes age function where comparing a timezone-aware and a naive datetime object could cause an exception. --- docs/release-notes/release-notes-5.7.0.rst | 1 + rhodecode/lib/utils2.py | 13 +++++++ .../src/plugins/jquery.timeago-extension.js | 5 +++ rhodecode/tests/lib/test_libs.py | 34 ++++++++++++++++--- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/release-notes-5.7.0.rst b/docs/release-notes/release-notes-5.7.0.rst index 48c54346..673ee055 100644 --- a/docs/release-notes/release-notes-5.7.0.rst +++ b/docs/release-notes/release-notes-5.7.0.rst @@ -31,6 +31,7 @@ Fixes ^^^^^ - ssh: Fixed an issue where the app.service_api.token field could be left empty in the configuration. The application will now automatically populate this field if it is missing. +- timezone: Fixed an issue in the age function where comparing a timezone-aware and a naive datetime object could cause an exception. Upgrade notes ^^^^^^^^^^^^^ diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py index 35e56631..6938f7d0 100644 --- a/rhodecode/lib/utils2.py +++ b/rhodecode/lib/utils2.py @@ -330,11 +330,24 @@ def age(prevdate, now=None, show_short_version=False, show_suffix=True, short_fo def get_year(prevdate): return prevdate.year + def _normalize_datetime_pair(prevdate, now): + """ + Ensure both datetime objects are the same kind (either both naive or both timezone-aware). + If not, comparing them will raise a TypeError due to mismatched timezone awareness. + """ + if prevdate.tzinfo or now.tzinfo: + utc = datetime.timezone.utc + return prevdate.replace(tzinfo=utc), now.replace(tzinfo=utc) + + return prevdate, now # no timezone + now = now or datetime.datetime.now() order = ["year", "month", "day", "hour", "minute", "second"] deltas = {} future = False + prevdate, now = _normalize_datetime_pair(prevdate, now) + if prevdate > now: now_old = now now = prevdate diff --git a/rhodecode/public/js/src/plugins/jquery.timeago-extension.js b/rhodecode/public/js/src/plugins/jquery.timeago-extension.js index 59bdd2a2..64d4a271 100644 --- a/rhodecode/public/js/src/plugins/jquery.timeago-extension.js +++ b/rhodecode/public/js/src/plugins/jquery.timeago-extension.js @@ -6,6 +6,11 @@ var AgeModule = (function () { var prevdate = moment(prevdate); var now = now || moment().utc(); + if (prevdate.isUTC() || now.isUTC()) { + prevdate = prevdate.clone().utc(); + now = now.clone().utc(); + } + var show_short_version = show_short_version || false; var show_suffix = show_suffix || true; var short_format = short_format || false; diff --git a/rhodecode/tests/lib/test_libs.py b/rhodecode/tests/lib/test_libs.py index 7b81a821..051f252d 100644 --- a/rhodecode/tests/lib/test_libs.py +++ b/rhodecode/tests/lib/test_libs.py @@ -177,6 +177,32 @@ def test_age(age_args, expected, kw, baseapp): assert translate(age(n + delt(**age_args), now=n, **kw)) == expected +@pytest.mark.parametrize( + "prevdate_fn, now_fn", + [ + (functools.partial(datetime.datetime.now, tz=datetime.timezone.max), datetime.datetime.now), + (datetime.datetime.now, datetime.datetime.now), + (datetime.datetime.now, functools.partial(datetime.datetime.now, tz=datetime.timezone.max)), + ( + functools.partial(datetime.datetime.now, tz=datetime.timezone.max), + functools.partial(datetime.datetime.now, tz=datetime.timezone.max), + ), + (functools.partial(datetime.datetime.now, tz=datetime.timezone.max), lambda: None), + (datetime.datetime.now, lambda: None), + ], +) +def test_age_timezone_aware(now_fn, prevdate_fn): + from rhodecode.lib.utils2 import age + + now = now_fn() + prevdate = prevdate_fn() + + try: + age(prevdate, now) + except Exception as e: + pytest.fail(f"Function raised an exception: {e}") + + @pytest.mark.parametrize( "age_args, expected, kw", [ @@ -326,19 +352,19 @@ def test_metatag_extraction(sample, expected_tags): (("lang", "[lang => .NET]"), '
.NET
'), ( ("license", "[license => BSD 3-clause]"), - '
BSD 3-clause
', + '
BSD 3-clause
', # noqa: W605 ), ( ("license", "[license => GPLv3]"), - '
GPLv3
', + '
GPLv3
', # noqa: W605 ), ( ("license", "[license => MIT]"), - '
MIT
', + '
MIT
', # noqa: W605 ), ( ("license", "[license => AGPLv3]"), - '
AGPLv3
', + '
AGPLv3
', # noqa: W605 ), ( ("ref", "[requires => RepoName]"), From c5fe34aebf2e25b9060538a7bb22739ee2e8e424 Mon Sep 17 00:00:00 2001 From: ievgenii vdovenko Date: Thu, 7 Aug 2025 11:22:09 +0200 Subject: [PATCH 2/2] fix: made js more robust --- .../public/js/src/plugins/jquery.timeago-extension.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/rhodecode/public/js/src/plugins/jquery.timeago-extension.js b/rhodecode/public/js/src/plugins/jquery.timeago-extension.js index 64d4a271..89b3d60c 100644 --- a/rhodecode/public/js/src/plugins/jquery.timeago-extension.js +++ b/rhodecode/public/js/src/plugins/jquery.timeago-extension.js @@ -3,13 +3,8 @@ var AgeModule = (function () { return { age: function(prevdate, now, show_short_version, show_suffix, short_format) { - var prevdate = moment(prevdate); - var now = now || moment().utc(); - - if (prevdate.isUTC() || now.isUTC()) { - prevdate = prevdate.clone().utc(); - now = now.clone().utc(); - } + var prevdate = moment(prevdate).utc(); + var now = now ? moment(now).utc() : moment().utc(); var show_short_version = show_short_version || false; var show_suffix = show_suffix || true;