From e5fe23594cff831ed2a19f8f5478aefe97b9d524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E5=87=BD?= Date: Thu, 31 Jul 2025 17:38:25 +0800 Subject: [PATCH 1/9] fix: handle timezone correctly when DJANGO_CELERY_BEAT_TZ_AWARE=False (Issue #924) --- django_celery_beat/schedulers.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index 99d98f8c..bb9fd786 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -12,13 +12,14 @@ from celery import current_app, schedules from celery.beat import ScheduleEntry, Scheduler from celery.utils.log import get_logger -from celery.utils.time import maybe_make_aware +from celery.utils.time import make_aware from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db import close_old_connections, transaction from django.db.models import Case, F, IntegerField, Q, When from django.db.models.functions import Cast from django.db.utils import DatabaseError, InterfaceError +from django.utils import timezone from kombu.utils.encoding import safe_repr, safe_str from kombu.utils.json import dumps, loads @@ -117,8 +118,6 @@ def is_due(self): # START DATE: only run after the `start_time`, if one exists. if self.model.start_time is not None: now = self._default_now() - if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): - now = maybe_make_aware(self._default_now()) if now < self.model.start_time: # The datetime is before the start date - don't run. # send a delay to retry on start_time @@ -147,19 +146,19 @@ def is_due(self): # Don't recheck return schedules.schedstate(False, NEVER_CHECK_TIMEOUT) - # CAUTION: make_aware assumes settings.TIME_ZONE for naive datetimes, - # while maybe_make_aware assumes utc for naive datetimes - tz = self.app.timezone - last_run_at_in_tz = maybe_make_aware(self.last_run_at).astimezone(tz) + last_run_at_in_tz = make_aware(self.last_run_at, self.app.timezone) return self.schedule.is_due(last_run_at_in_tz) def _default_now(self): if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): - now = datetime.datetime.now(self.app.timezone) + if getattr(settings, 'USE_TZ', True): + now = datetime.datetime.now(self.app.timezone) + else: + # Remove the time zone information (convert to naive datetime) + now = datetime.datetime.now(self.app.timezone).replace(tzinfo=None) else: - # this ends up getting passed to maybe_make_aware, which expects - # all naive datetime objects to be in utc time. - now = datetime.datetime.utcnow() + # Use the default time zone time of Django + now = timezone.now() return now def __next__(self): From 90eff766233c86b93ba9e69f540a3d1f008cd75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E5=87=BD?= Date: Thu, 31 Jul 2025 20:45:58 +0800 Subject: [PATCH 2/9] fix: handle timezone correctly when DJANGO_CELERY_BEAT_TZ_AWARE=False (Issue #924) --- django_celery_beat/schedulers.py | 10 ++++++++-- t/unit/test_schedulers.py | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index bb9fd786..293e6af4 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -12,7 +12,7 @@ from celery import current_app, schedules from celery.beat import ScheduleEntry, Scheduler from celery.utils.log import get_logger -from celery.utils.time import make_aware +from celery.utils.time import make_aware, is_naive from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db import close_old_connections, transaction @@ -146,7 +146,13 @@ def is_due(self): # Don't recheck return schedules.schedstate(False, NEVER_CHECK_TIMEOUT) - last_run_at_in_tz = make_aware(self.last_run_at, self.app.timezone) + # Handle both naive and timezone-aware last_run_at properly + if is_naive(self.last_run_at): + # Naive datetime - make it aware using app timezone + last_run_at_in_tz = make_aware(self.last_run_at, self.app.timezone) + else: + # Already timezone-aware - convert to app timezone if needed + last_run_at_in_tz = self.last_run_at.astimezone(self.app.timezone) return self.schedule.is_due(last_run_at_in_tz) def _default_now(self): diff --git a/t/unit/test_schedulers.py b/t/unit/test_schedulers.py index f7dfac8b..af833579 100644 --- a/t/unit/test_schedulers.py +++ b/t/unit/test_schedulers.py @@ -333,7 +333,7 @@ def test_entry_is_due__no_use_tz(self): assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = datetime.utcnow() + right_now = timezone.now() m = self.create_model_crontab( crontab(minute='*/10'), @@ -364,7 +364,7 @@ def test_entry_and_model_last_run_at_with_utc_no_use_tz(self, monkeypatch): time.tzset() assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = datetime.utcnow() + right_now = timezone.now() # make sure to use fixed date time monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now) m = self.create_model_crontab( @@ -398,7 +398,7 @@ def test_entry_and_model_last_run_at_when_model_changed(self, monkeypatch): time.tzset() assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = datetime.utcnow() + right_now = timezone.now() # make sure to use fixed date time monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now) m = self.create_model_crontab( @@ -451,7 +451,7 @@ def test_entry_is_due__celery_timezone_doesnt_match_time_zone(self): # simulate last_run_at all none, doing the same thing that # _default_now() would do - right_now = datetime.utcnow() + right_now = timezone.now() m = self.create_model_crontab( crontab(minute='*/10'), From 5fbcd046a09bc812a10aa63076c109cb3229ac3b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:46:55 +0000 Subject: [PATCH 3/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_celery_beat/schedulers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index 293e6af4..b04bbf7d 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -12,7 +12,7 @@ from celery import current_app, schedules from celery.beat import ScheduleEntry, Scheduler from celery.utils.log import get_logger -from celery.utils.time import make_aware, is_naive +from celery.utils.time import is_naive, make_aware from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db import close_old_connections, transaction From cf3a0e5113ac095ed832630e4c146292cb68bd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E5=87=BD?= Date: Fri, 1 Aug 2025 09:30:07 +0800 Subject: [PATCH 4/9] fix: The local time obtained from the test sample should be consistent with the CELERY_TIMEZONE time zone. (Issue #924) --- t/unit/test_schedulers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/t/unit/test_schedulers.py b/t/unit/test_schedulers.py index af833579..6e09da12 100644 --- a/t/unit/test_schedulers.py +++ b/t/unit/test_schedulers.py @@ -333,7 +333,7 @@ def test_entry_is_due__no_use_tz(self): assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = timezone.now() + right_now = timezone.now().astimezone(self.app.timezone) m = self.create_model_crontab( crontab(minute='*/10'), @@ -364,7 +364,7 @@ def test_entry_and_model_last_run_at_with_utc_no_use_tz(self, monkeypatch): time.tzset() assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = timezone.now() + right_now = timezone.now().astimezone(self.app.timezone) # make sure to use fixed date time monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now) m = self.create_model_crontab( @@ -398,7 +398,7 @@ def test_entry_and_model_last_run_at_when_model_changed(self, monkeypatch): time.tzset() assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = timezone.now() + right_now = timezone.now().astimezone(self.app.timezone) # make sure to use fixed date time monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now) m = self.create_model_crontab( @@ -451,7 +451,8 @@ def test_entry_is_due__celery_timezone_doesnt_match_time_zone(self): # simulate last_run_at all none, doing the same thing that # _default_now() would do - right_now = timezone.now() + # Imitate local time + right_now = timezone.now().astimezone(self.app.timezone) m = self.create_model_crontab( crontab(minute='*/10'), From 8b49ecc0f6441c1ba1a4a993d58c468a0bd0bdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E5=87=BD?= Date: Fri, 1 Aug 2025 11:46:27 +0800 Subject: [PATCH 5/9] fix: The issue where the local time obtained does not match the CELERY_TIMEZONE. (Issue #924) --- django_celery_beat/schedulers.py | 12 ++++-------- t/unit/test_schedulers.py | 9 ++++----- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index b04bbf7d..7c59d25f 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -156,15 +156,11 @@ def is_due(self): return self.schedule.is_due(last_run_at_in_tz) def _default_now(self): - if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): - if getattr(settings, 'USE_TZ', True): - now = datetime.datetime.now(self.app.timezone) - else: - # Remove the time zone information (convert to naive datetime) - now = datetime.datetime.now(self.app.timezone).replace(tzinfo=None) + if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True) and getattr(settings, 'USE_TZ', True): + now = timezone.now().astimezone(self.app.timezone) else: - # Use the default time zone time of Django - now = timezone.now() + # The naive datetime of self.app.timezone + now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None) return now def __next__(self): diff --git a/t/unit/test_schedulers.py b/t/unit/test_schedulers.py index 6e09da12..4b9d5031 100644 --- a/t/unit/test_schedulers.py +++ b/t/unit/test_schedulers.py @@ -333,7 +333,7 @@ def test_entry_is_due__no_use_tz(self): assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = timezone.now().astimezone(self.app.timezone) + right_now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None) m = self.create_model_crontab( crontab(minute='*/10'), @@ -364,7 +364,7 @@ def test_entry_and_model_last_run_at_with_utc_no_use_tz(self, monkeypatch): time.tzset() assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = timezone.now().astimezone(self.app.timezone) + right_now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None) # make sure to use fixed date time monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now) m = self.create_model_crontab( @@ -398,7 +398,7 @@ def test_entry_and_model_last_run_at_when_model_changed(self, monkeypatch): time.tzset() assert self.app.timezone.key == 'Europe/Berlin' # simulate last_run_at from DB - not TZ aware but localtime - right_now = timezone.now().astimezone(self.app.timezone) + right_now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None) # make sure to use fixed date time monkeypatch.setattr(self.Entry, '_default_now', lambda o: right_now) m = self.create_model_crontab( @@ -451,8 +451,7 @@ def test_entry_is_due__celery_timezone_doesnt_match_time_zone(self): # simulate last_run_at all none, doing the same thing that # _default_now() would do - # Imitate local time - right_now = timezone.now().astimezone(self.app.timezone) + right_now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None) m = self.create_model_crontab( crontab(minute='*/10'), From f61ce192a73e3dc17f507b2320d5c8bcff2d5d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Fri, 1 Aug 2025 10:51:23 +0600 Subject: [PATCH 6/9] Update django_celery_beat/schedulers.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- django_celery_beat/schedulers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index 7c59d25f..c09d77d4 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -159,7 +159,7 @@ def _default_now(self): if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True) and getattr(settings, 'USE_TZ', True): now = timezone.now().astimezone(self.app.timezone) else: - # The naive datetime of self.app.timezone + # A naive datetime representing local time in the app's timezone now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None) return now From 6933370f8626597ad6ab642e7d19141ce4ac90a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E5=87=BD?= Date: Mon, 4 Aug 2025 15:42:24 +0800 Subject: [PATCH 7/9] fix: Line too long (104 > 88). (Issue #924) --- django_celery_beat/schedulers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index c09d77d4..1cd41a49 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -156,7 +156,8 @@ def is_due(self): return self.schedule.is_due(last_run_at_in_tz) def _default_now(self): - if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True) and getattr(settings, 'USE_TZ', True): + if (getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True) and + getattr(settings, 'USE_TZ', True)): now = timezone.now().astimezone(self.app.timezone) else: # A naive datetime representing local time in the app's timezone From baa65c98b977f2dcde131584bd5df5b5f213e5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E5=87=BD?= Date: Mon, 4 Aug 2025 17:23:43 +0800 Subject: [PATCH 8/9] fix: visually indented line with same indent as next logical line. (Issue #924) --- django_celery_beat/schedulers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index 1cd41a49..85aa392e 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -156,13 +156,13 @@ def is_due(self): return self.schedule.is_due(last_run_at_in_tz) def _default_now(self): + now = timezone.now().astimezone(self.app.timezone) if (getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True) and getattr(settings, 'USE_TZ', True)): - now = timezone.now().astimezone(self.app.timezone) - else: - # A naive datetime representing local time in the app's timezone - now = timezone.now().astimezone(self.app.timezone).replace(tzinfo=None) - return now + return now + + # A naive datetime representing local time in the app's timezone + return now.replace(tzinfo=None) def __next__(self): self.model.last_run_at = self._default_now() From 343d368ecbcf9733fcb97b141834e7c8caef2ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E5=87=BD?= Date: Mon, 4 Aug 2025 17:57:01 +0800 Subject: [PATCH 9/9] fix: visually indented line with same indent as next logical line. (Issue #924) --- django_celery_beat/schedulers.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index 85aa392e..816520ff 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -157,11 +157,15 @@ def is_due(self): def _default_now(self): now = timezone.now().astimezone(self.app.timezone) - if (getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True) and - getattr(settings, 'USE_TZ', True)): + tz_aware = ( + getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True) and + getattr(settings, 'USE_TZ', True) + ) + if tz_aware: return now - - # A naive datetime representing local time in the app's timezone + # Return a naive datetime representing local time in the app's timezone. + # This path is taken when either DJANGO_CELERY_BEAT_TZ_AWARE or USE_TZ + # is set to False in Django settings. return now.replace(tzinfo=None) def __next__(self):