Skip to content

Commit dcb8867

Browse files
committed
fix:admin view shows next run time properly #288
1 parent 3660f24 commit dcb8867

File tree

8 files changed

+185
-141
lines changed

8 files changed

+185
-141
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### 🐛 Bug Fixes
66

77
- Issue on task admin showing list of jobs where jobs have been deleted from broker #285
8+
- Task admin view shows next run currently #288
89

910
### 🧰 Maintenance
1011

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ dependencies = [
4141
"django>=5",
4242
"croniter>=2.0",
4343
"click~=8.2",
44-
"fakeredis",
4544
]
4645

4746
[project.optional-dependencies]
@@ -57,13 +56,14 @@ Funding = "https://github.com/sponsors/cunla"
5756

5857
[dependency-groups]
5958
dev = [
60-
"time-machine>=2.16.0,<3",
59+
"time-machine>=2.16.0",
6160
"ruff>=0.11",
62-
"coverage~=7.6",
63-
"fakeredis~=2.28",
61+
"coverage>=7.6",
62+
"fakeredis>=2.28",
6463
"pyyaml>=6,<7",
6564
"mypy>=1.16.0",
6665
"types-croniter>=6.0.0.20250411",
66+
"beautifulsoup4>=4.13.4"
6767
]
6868

6969
[tool.hatch.build.targets.sdist]

scheduler/admin/task_admin.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from typing import List
1+
from datetime import datetime
2+
from typing import List, Optional, Union
23

34
from django.contrib import admin, messages
45
from django.contrib.contenttypes.admin import GenericStackedInline
56
from django.db.models import QuerySet
67
from django.http import HttpRequest, HttpResponse
78
from django.utils import timezone, formats
9+
from django.utils.timezone import is_naive
810
from django.utils.translation import gettext_lazy as _
911

1012
from scheduler.helpers.queues import get_queue
@@ -137,8 +139,16 @@ def task_schedule(self, o: Task) -> str:
137139
return f"Repeatable: {o.interval} {o.get_interval_unit_display()}"
138140

139141
@admin.display(description="Next run")
140-
def next_run(self, o: Task) -> str:
141-
return get_next_cron_time(o.cron_string)
142+
def next_run(self, o: Task) -> Union[str, datetime]:
143+
res = o.scheduled_time
144+
if res < timezone.now():
145+
o.save(clean=False)
146+
res = o.scheduled_time
147+
if res is None:
148+
return _("Not scheduled")
149+
if is_naive(res):
150+
res = timezone.make_aware(res, timezone.get_current_timezone())
151+
return res
142152

143153
def change_view(self, request: HttpRequest, object_id, form_url="", extra_context=None) -> HttpResponse:
144154
extra = extra_context or {}

scheduler/models/task.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,6 @@ def _schedule(self) -> bool:
333333
"""Schedule the next execution for the task to run.
334334
:returns: True if a job was scheduled, False otherwise.
335335
"""
336-
self.refresh_from_db()
337336
if self.is_scheduled():
338337
logger.debug(f"Task {self.name} already scheduled")
339338
return False
@@ -358,10 +357,10 @@ def save(self, **kwargs: Any) -> None:
358357
should_clean = kwargs.pop("clean", True)
359358
if should_clean:
360359
self.clean()
361-
schedule_job = kwargs.pop("schedule_job", True)
362360
update_fields = kwargs.get("update_fields", None)
363361
if update_fields is not None:
364362
kwargs["update_fields"] = set(update_fields).union({"updated_at"})
363+
schedule_job = kwargs.pop("schedule_job", True)
365364
super(Task, self).save(**kwargs)
366365
if schedule_job:
367366
self._schedule()
@@ -443,13 +442,13 @@ def clean(self) -> None:
443442
)
444443

445444

446-
def get_next_cron_time(cron_string: Optional[str]) -> Optional[timezone.datetime]:
445+
def get_next_cron_time(cron_string: Optional[str]) -> Optional[datetime]:
447446
"""Calculate the next scheduled time by creating a crontab object with a cron string"""
448447
if cron_string is None:
449448
return None
450449
now = timezone.now()
451450
itr = croniter.croniter(cron_string, now)
452-
next_itr = itr.get_next(timezone.datetime)
451+
next_itr = itr.get_next(datetime)
453452
return next_itr
454453

455454

scheduler/tests/test_internals.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from datetime import timedelta
1+
from datetime import timedelta, datetime, timezone as dt_timezone
22

33
from django.core.exceptions import ImproperlyConfigured
44
from django.test import override_settings
55
from django.utils import timezone
66

77
from scheduler.helpers.callback import Callback, CallbackSetupError
8-
from scheduler.models import TaskType, get_scheduled_task
8+
from scheduler.models import TaskType, get_scheduled_task, get_next_cron_time
99
from scheduler.tests.testtools import SchedulerBaseCase, task_factory
1010

1111

@@ -80,3 +80,10 @@ def test_conf_settings__unknown_setting(self):
8080
settings.conf_settings()
8181

8282
self.assertEqual(str(cm.exception), "Unknown setting UNKNOWN_SETTING in SCHEDULER_CONFIG")
83+
84+
@override_settings(USE_TZ=True, TIME_ZONE="EST")
85+
def test_get_next_cron_time(self):
86+
next_cron_time = get_next_cron_time("0 0 * * *")
87+
self.assertIsNotNone(next_cron_time)
88+
self.assertTrue(next_cron_time > timezone.now())
89+
self.assertEqual(dt_timezone.utc, next_cron_time.tzinfo)

scheduler/tests/test_task_types/test_once_task.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import timedelta, datetime
22

3+
import bs4
34
import time_machine
45
from django.core.exceptions import ValidationError
56
from django.urls import reverse
@@ -43,13 +44,16 @@ def test_admin_change_view__has_execution_list(self):
4344
# act
4445
res = self.client.get(url)
4546
# assert
47+
self.assertEqual(200, res.status_code)
4648
self.assertContains(res, "Job executions")
47-
self.assertContains(res, """<table id="result_list">""")
48-
self.assertContains(res, task.job_name, status_code=200)
49-
self.assertContains(res, "Scheduled", status_code=200)
50-
self.assertContains(res, """<span id="counter">1""", status_code=200)
5149
self.assertFalse(res.context["pagination_required"])
5250
self.assertEqual(res.context["executions"].paginator.count, 1)
51+
soup = bs4.BeautifulSoup(res.content, "html.parser")
52+
self.assertEqual(1, len(soup.find_all("table", {"id": "result_list"})))
53+
counter_element_list = soup.find_all("span", {"id": "counter"})
54+
self.assertEqual(1, len(counter_element_list))
55+
counter_element = counter_element_list[0]
56+
self.assertEqual("1 entry", counter_element.text.strip())
5357

5458
@time_machine.travel(datetime(2016, 12, 25))
5559
def test_admin_change_view__has_empty_execution_list(self):

scheduler/tests/test_task_types/test_task_model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import zoneinfo
22
from datetime import datetime, timedelta
33

4+
import bs4
45
import time_machine
56
from django.contrib.messages import get_messages
67
from django.core.exceptions import ValidationError

0 commit comments

Comments
 (0)