Skip to content

Commit c823413

Browse files
committed
Merge branch 'master' into mypy
# Conflicts: # scheduler/models/task.py # scheduler/tests/test_task_types/test_repeatable_task.py # uv.lock
2 parents 4d622c7 + 4420021 commit c823413

File tree

13 files changed

+179
-305
lines changed

13 files changed

+179
-305
lines changed

docs/changelog.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## v4.0.5 🌈
4+
5+
### 🐛 Bug Fixes
6+
7+
- fix:repeatable task without start date #276
8+
- fix:admin list of tasks showing local datetime #280
9+
- fix:wait for job child process using os.waitpid #281
10+
11+
### 🧰 Maintenance
12+
13+
- refactor some tests
14+
315
## v4.0.4 🌈
416

517
### 🐛 Bug Fixes

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "django-tasks-scheduler"
7-
version = "4.0.4"
7+
version = "4.0.5"
88
description = "An async job scheduler for django using redis/valkey brokers"
99
authors = [{ name = "Daniel Moran", email = "[email protected]" }]
10-
requires-python = "~=3.10"
10+
requires-python = ">=3.10"
1111
readme = "README.md"
1212
license = "MIT"
1313
maintainers = [{ name = "Daniel Moran", email = "[email protected]" }]

scheduler/admin/task_admin.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.contrib.contenttypes.admin import GenericStackedInline
55
from django.db.models import QuerySet
66
from django.http import HttpRequest
7+
from django.utils import timezone, formats
78
from django.utils.translation import gettext_lazy as _
89

910
from scheduler.helpers.queues import get_queue
@@ -120,7 +121,11 @@ class Media:
120121
@admin.display(description="Schedule")
121122
def task_schedule(self, o: Task) -> str:
122123
if o.task_type == TaskType.ONCE.value:
123-
return f"Run once: {o.scheduled_time:%Y-%m-%d %H:%M:%S}"
124+
if timezone.is_naive(o.scheduled_time):
125+
local_time = timezone.make_aware(o.scheduled_time, timezone.get_current_timezone())
126+
else:
127+
local_time = timezone.localtime(o.scheduled_time)
128+
return f"Run once: {formats.date_format(local_time, 'DATETIME_FORMAT')}"
124129
elif o.task_type == TaskType.CRON.value:
125130
return f"Cron: {o.cron_string}"
126131
else: # if o.task_type == TaskType.REPEATABLE.value:

scheduler/models/task.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def failure_callback(job: JobModel, connection, result, *args, **kwargs):
4545
task.job_name = None
4646
task.failed_runs += 1
4747
task.last_failed_run = timezone.now()
48-
task.save(schedule_job=True)
48+
task.save(schedule_job=True, clean=False)
4949

5050

5151
def success_callback(job: JobModel, connection: ConnectionType, result: Any, *args, **kwargs):
@@ -56,7 +56,7 @@ def success_callback(job: JobModel, connection: ConnectionType, result: Any, *ar
5656
task.job_name = None
5757
task.successful_runs += 1
5858
task.last_successful_run = timezone.now()
59-
task.save(schedule_job=True)
59+
task.save(schedule_job=True, clean=False)
6060

6161

6262
def get_queue_choices():
@@ -187,9 +187,9 @@ def is_scheduled(self) -> bool:
187187
return False
188188
# check whether job_id is in scheduled/queued/active jobs
189189
res = (
190-
(self.job_name in self.rqueue.scheduled_job_registry.all())
191-
or (self.job_name in self.rqueue.queued_job_registry.all())
192-
or (self.job_name in self.rqueue.active_job_registry.all())
190+
(self.job_name in self.rqueue.scheduled_job_registry.all())
191+
or (self.job_name in self.rqueue.queued_job_registry.all())
192+
or (self.job_name in self.rqueue.active_job_registry.all())
193193
)
194194
# If the job_id is not scheduled/queued/started,
195195
# update the job_id to None. (The job_id belongs to a previous run which is completed)
@@ -266,7 +266,7 @@ def unschedule(self) -> bool:
266266
if self.job_name is not None:
267267
self.rqueue.delete_job(self.job_name)
268268
self.job_name = None
269-
self.save(schedule_job=False)
269+
self.save(schedule_job=False, clean=False)
270270
return True
271271

272272
def _schedule_time(self) -> datetime:
@@ -360,7 +360,9 @@ def _schedule(self) -> bool:
360360
return True
361361

362362
def save(self, **kwargs):
363-
self.clean()
363+
should_clean = kwargs.pop("clean", True)
364+
if should_clean:
365+
self.clean()
364366
schedule_job = kwargs.pop("schedule_job", True)
365367
update_fields = kwargs.get("update_fields", None)
366368
if update_fields is not None:
@@ -407,12 +409,6 @@ def clean_interval_unit(self):
407409
code="invalid",
408410
params={"queue": self.queue, "interval": config.SCHEDULER_INTERVAL},
409411
)
410-
if self.interval_seconds() <= config.SCHEDULER_INTERVAL:
411-
raise ValidationError(
412-
_("Job interval is not a multiple of rq_scheduler's interval frequency: %(interval)ss"),
413-
code="invalid",
414-
params={"interval": config.SCHEDULER_INTERVAL},
415-
)
416412

417413
def clean_result_ttl(self) -> None:
418414
"""Throws an error if there are repeats left to run and the result_ttl won't last until the next scheduled time.

scheduler/tests/test_internals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class TestInternals(SchedulerBaseCase):
1313
def test_get_scheduled_job(self):
14-
task = task_factory(TaskType.ONCE, scheduled_time=timezone.now() - timedelta(hours=1))
14+
task = task_factory(TaskType.ONCE, scheduled_time=timezone.now() + timedelta(hours=1))
1515
self.assertEqual(task, get_scheduled_task(TaskType.ONCE, task.id))
1616
with self.assertRaises(ValueError):
1717
get_scheduled_task(task.task_type, task.id + 1)

scheduler/tests/test_mgmt_commands/test_import.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@ def tearDown(self) -> None:
2121
os.remove(self.tmpfile.name)
2222

2323
def test_import__should_schedule_job(self):
24-
jobs = list()
25-
jobs.append(task_factory(TaskType.ONCE, enabled=True, instance_only=True))
26-
jobs.append(task_factory(TaskType.REPEATABLE, enabled=True, instance_only=True))
27-
res = json.dumps([j.to_dict() for j in jobs])
24+
tasks = list()
25+
tasks.append(task_factory(TaskType.ONCE, enabled=True, instance_only=True))
26+
tasks.append(task_factory(TaskType.REPEATABLE, enabled=True, instance_only=True))
27+
res = json.dumps([j.to_dict() for j in tasks])
2828
self.tmpfile.write(res)
2929
self.tmpfile.flush()
3030
# act
3131
call_command("import", filename=self.tmpfile.name)
3232
# assert
3333
self.assertEqual(1, Task.objects.filter(task_type=TaskType.ONCE).count())
3434
self.assertEqual(1, Task.objects.filter(task_type=TaskType.REPEATABLE).count())
35-
db_job = Task.objects.filter(task_type=TaskType.ONCE).first()
35+
db_task = Task.objects.filter(task_type=TaskType.ONCE).first()
3636
attrs = ["name", "queue", "callable", "enabled", "timeout"]
3737
for attr in attrs:
38-
self.assertEqual(getattr(jobs[0], attr), getattr(db_job, attr))
38+
self.assertEqual(getattr(tasks[0], attr), getattr(db_task, attr))
3939

4040
def test_import__should_schedule_job_yaml(self):
4141
tasks = list()
@@ -49,16 +49,16 @@ def test_import__should_schedule_job_yaml(self):
4949
# assert
5050
self.assertEqual(1, Task.objects.filter(task_type=TaskType.ONCE).count())
5151
self.assertEqual(1, Task.objects.filter(task_type=TaskType.REPEATABLE).count())
52-
db_job = Task.objects.filter(task_type=TaskType.ONCE).first()
52+
task = Task.objects.filter(task_type=TaskType.ONCE).first()
5353
attrs = ["name", "queue", "callable", "enabled", "timeout"]
5454
for attr in attrs:
55-
self.assertEqual(getattr(tasks[0], attr), getattr(db_job, attr))
55+
self.assertEqual(getattr(tasks[0], attr), getattr(task, attr))
5656

5757
def test_import__should_schedule_job_yaml_without_yaml_lib(self):
58-
jobs = list()
59-
jobs.append(task_factory(TaskType.ONCE, enabled=True, instance_only=True))
60-
jobs.append(task_factory(TaskType.REPEATABLE, enabled=True, instance_only=True))
61-
res = yaml.dump([j.to_dict() for j in jobs], default_flow_style=False)
58+
tasks = list()
59+
tasks.append(task_factory(TaskType.ONCE, enabled=True, instance_only=True))
60+
tasks.append(task_factory(TaskType.REPEATABLE, enabled=True, instance_only=True))
61+
res = yaml.dump([j.to_dict() for j in tasks], default_flow_style=False)
6262
self.tmpfile.write(res)
6363
self.tmpfile.flush()
6464
# act
@@ -68,27 +68,27 @@ def test_import__should_schedule_job_yaml_without_yaml_lib(self):
6868
self.assertEqual(cm.exception.code, 1)
6969

7070
def test_import__should_schedule_job_reset(self):
71-
jobs = list()
71+
tasks = list()
7272
task_factory(TaskType.ONCE, enabled=True)
7373
task_factory(TaskType.ONCE, enabled=True)
74-
jobs.append(task_factory(TaskType.ONCE, enabled=True))
75-
jobs.append(task_factory(TaskType.REPEATABLE, enabled=True, instance_only=True))
76-
res = json.dumps([j.to_dict() for j in jobs])
74+
tasks.append(task_factory(TaskType.ONCE, enabled=True))
75+
tasks.append(task_factory(TaskType.REPEATABLE, enabled=True, instance_only=True))
76+
res = json.dumps([j.to_dict() for j in tasks])
7777
self.tmpfile.write(res)
7878
self.tmpfile.flush()
7979
# act
8080
call_command("import", filename=self.tmpfile.name, reset=True)
8181
# assert
8282
self.assertEqual(1, Task.objects.filter(task_type=TaskType.ONCE).count())
83-
db_job = Task.objects.filter(task_type=TaskType.ONCE).first()
83+
task = Task.objects.filter(task_type=TaskType.ONCE).first()
8484
attrs = ["name", "queue", "callable", "enabled", "timeout"]
8585
for attr in attrs:
86-
self.assertEqual(getattr(jobs[0], attr), getattr(db_job, attr))
86+
self.assertEqual(getattr(tasks[0], attr), getattr(task, attr))
8787
self.assertEqual(1, Task.objects.filter(task_type=TaskType.REPEATABLE).count())
88-
db_job = Task.objects.filter(task_type=TaskType.REPEATABLE).first()
88+
task = Task.objects.filter(task_type=TaskType.REPEATABLE).first()
8989
attrs = ["name", "queue", "callable", "enabled", "timeout"]
9090
for attr in attrs:
91-
self.assertEqual(getattr(jobs[1], attr), getattr(db_job, attr))
91+
self.assertEqual(getattr(tasks[1], attr), getattr(task, attr))
9292

9393
def test_import__should_schedule_job_update_existing(self):
9494
tasks = list()
@@ -101,10 +101,10 @@ def test_import__should_schedule_job_update_existing(self):
101101
call_command("import", filename=self.tmpfile.name, update=True)
102102
# assert
103103
self.assertEqual(2, Task.objects.filter(task_type=TaskType.ONCE).count())
104-
db_job = Task.objects.filter(task_type=TaskType.ONCE).get(name=tasks[0].name)
104+
task = Task.objects.filter(task_type=TaskType.ONCE).get(name=tasks[0].name)
105105
attrs = ["name", "queue", "callable", "enabled", "timeout"]
106106
for attr in attrs:
107-
self.assertEqual(getattr(tasks[0], attr), getattr(db_job, attr))
107+
self.assertEqual(getattr(tasks[0], attr), getattr(task, attr))
108108

109109
def test_import__should_schedule_job_without_update_existing(self):
110110
tasks = list()
@@ -117,7 +117,7 @@ def test_import__should_schedule_job_without_update_existing(self):
117117
call_command("import", filename=self.tmpfile.name)
118118
# assert
119119
self.assertEqual(2, Task.objects.filter(task_type=TaskType.ONCE).count())
120-
db_job = Task.objects.get(name=tasks[0].name)
120+
task = Task.objects.get(name=tasks[0].name)
121121
attrs = ["id", "name", "queue", "callable", "enabled", "timeout"]
122122
for attr in attrs:
123-
self.assertEqual(getattr(tasks[0], attr), getattr(db_job, attr))
123+
self.assertEqual(getattr(tasks[0], attr), getattr(task, attr))

scheduler/tests/test_task_types/test_once_task.py

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

3+
import time_machine
34
from django.core.exceptions import ValidationError
5+
from django.urls import reverse
46
from django.utils import timezone
57

68
from scheduler import settings
@@ -9,15 +11,26 @@
911
from scheduler.tests.testtools import task_factory
1012

1113

12-
class TestScheduledTask(BaseTestCases.TestSchedulableTask):
14+
class TestScheduledOnceTask(BaseTestCases.TestSchedulableTask):
1315
task_type = TaskType.ONCE
1416
queue_name = settings.get_queue_names()[0]
1517

1618
def test_clean(self):
17-
job = task_factory(self.task_type)
18-
job.queue = self.queue_name
19-
job.callable = "scheduler.tests.jobs.test_job"
20-
self.assertIsNone(job.clean())
19+
task = task_factory(self.task_type)
20+
task.queue = self.queue_name
21+
task.callable = "scheduler.tests.jobs.test_job"
22+
self.assertIsNone(task.clean())
23+
24+
@time_machine.travel(datetime(2016, 12, 25))
25+
def test_admin_changelist_view__has_timezone_data(self):
26+
# arrange
27+
self.client.login(username="admin", password="admin")
28+
task_factory(self.task_type)
29+
url = reverse("admin:scheduler_task_changelist")
30+
# act
31+
res = self.client.get(url)
32+
# assert
33+
self.assertContains(res, "Run once: Dec. 26, 2016, midnight", count=1, status_code=200)
2134

2235
def test_create_without_date__fail(self):
2336
task = task_factory(self.task_type, scheduled_time=None, instance_only=True)
@@ -35,5 +48,6 @@ def test_create_with_date_in_the_past__fail(self):
3548
self.assertEqual(str(cm.exception), "{'scheduled_time': ['Scheduled time must be in the future']}")
3649

3750
def test_unschedulable_old_job(self):
38-
job = task_factory(self.task_type, scheduled_time=timezone.now() - timedelta(hours=1))
39-
self.assertFalse(job.is_scheduled())
51+
task = task_factory(self.task_type, scheduled_time=timezone.now() - timedelta(hours=1), instance_only=True)
52+
task.save(clean=False)
53+
self.assertFalse(task.is_scheduled())

0 commit comments

Comments
 (0)