Skip to content

Commit f72b398

Browse files
committed
feat:unify models
1 parent 800dea6 commit f72b398

File tree

5 files changed

+742
-2
lines changed

5 files changed

+742
-2
lines changed

scheduler/admin/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
from .task_models import TaskAdmin # noqa: F401
1+
from .task_models import TaskAdmin as OldTaskAdmin # noqa: F401
22
from .ephemeral_models import QueueAdmin, WorkerAdmin # noqa: F401
3+
from .task_admin import TaskAdmin # noqa: F401

scheduler/admin/task_admin.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import redis
2+
import valkey
3+
from django.contrib import admin, messages
4+
from django.contrib.contenttypes.admin import GenericStackedInline
5+
from django.utils.translation import gettext_lazy as _
6+
7+
from scheduler import tools
8+
from scheduler.models import TaskArg, TaskKwarg, Task
9+
from scheduler.settings import SCHEDULER_CONFIG, logger
10+
from scheduler.tools import get_job_executions_for_task
11+
12+
13+
class HiddenMixin(object):
14+
class Media:
15+
js = [
16+
"admin/js/jquery.init.js",
17+
]
18+
19+
20+
class JobArgInline(HiddenMixin, GenericStackedInline):
21+
model = TaskArg
22+
extra = 0
23+
fieldsets = (
24+
(
25+
None,
26+
{
27+
"fields": (
28+
(
29+
"arg_type",
30+
"val",
31+
),
32+
),
33+
},
34+
),
35+
)
36+
37+
38+
class JobKwargInline(HiddenMixin, GenericStackedInline):
39+
model = TaskKwarg
40+
extra = 0
41+
fieldsets = (
42+
(
43+
None,
44+
{
45+
"fields": (
46+
("key",),
47+
(
48+
"arg_type",
49+
"val",
50+
),
51+
),
52+
},
53+
),
54+
)
55+
56+
57+
_LIST_DISPLAY_EXTRA = dict(
58+
CronTask=(
59+
"cron_string",
60+
"next_run",
61+
"successful_runs",
62+
"last_successful_run",
63+
"failed_runs",
64+
"last_failed_run",
65+
),
66+
ScheduledTask=("scheduled_time",),
67+
RepeatableTask=(
68+
"scheduled_time",
69+
"interval_display",
70+
"successful_runs",
71+
"last_successful_run",
72+
"failed_runs",
73+
"last_failed_run",
74+
),
75+
Task=(
76+
"scheduled_time",
77+
"interval_display",
78+
"cron_string",
79+
"next_run",
80+
"successful_runs",
81+
"last_successful_run",
82+
"failed_runs",
83+
"last_failed_run",
84+
),
85+
)
86+
_FIELDSET_EXTRA = dict(
87+
CronTask=(
88+
"cron_string",
89+
"timeout",
90+
"result_ttl",
91+
(
92+
"successful_runs",
93+
"last_successful_run",
94+
),
95+
(
96+
"failed_runs",
97+
"last_failed_run",
98+
),
99+
),
100+
ScheduledTask=("scheduled_time", "timeout", "result_ttl"),
101+
RepeatableTask=(
102+
"scheduled_time",
103+
(
104+
"interval",
105+
"interval_unit",
106+
),
107+
"repeat",
108+
"timeout",
109+
"result_ttl",
110+
(
111+
"successful_runs",
112+
"last_successful_run",
113+
),
114+
(
115+
"failed_runs",
116+
"last_failed_run",
117+
),
118+
),
119+
Task=(
120+
"scheduled_time",
121+
"cron_string",
122+
(
123+
"interval",
124+
"interval_unit",
125+
),
126+
"repeat",
127+
"timeout",
128+
"result_ttl",
129+
(
130+
"successful_runs",
131+
"last_successful_run",
132+
),
133+
(
134+
"failed_runs",
135+
"last_failed_run",
136+
),
137+
),
138+
)
139+
140+
141+
@admin.register(Task)
142+
class TaskAdmin(admin.ModelAdmin):
143+
"""TaskAdmin admin view for all task models.
144+
Using the _LIST_DISPLAY_EXTRA and _FIELDSET_EXTRA additional data for each model.
145+
"""
146+
147+
save_on_top = True
148+
change_form_template = "admin/scheduler/change_form.html"
149+
actions = [
150+
"disable_selected",
151+
"enable_selected",
152+
"enqueue_job_now",
153+
]
154+
inlines = [
155+
JobArgInline,
156+
JobKwargInline,
157+
]
158+
list_filter = ("enabled",)
159+
list_display = (
160+
"enabled",
161+
"name",
162+
"job_id",
163+
"function_string",
164+
"is_scheduled",
165+
"queue",
166+
)
167+
list_display_links = ("name",)
168+
readonly_fields = ("job_id",)
169+
fieldsets = (
170+
(
171+
None,
172+
{
173+
"fields": (
174+
"name",
175+
"callable",
176+
"enabled",
177+
"at_front",
178+
),
179+
},
180+
),
181+
(
182+
_("RQ Settings"),
183+
{
184+
"fields": (
185+
"queue",
186+
"job_id",
187+
),
188+
},
189+
),
190+
)
191+
192+
def get_list_display(self, request):
193+
if self.model.__name__ not in _LIST_DISPLAY_EXTRA:
194+
raise ValueError(f"Unrecognized model {self.model}")
195+
return TaskAdmin.list_display + _LIST_DISPLAY_EXTRA[self.model.__name__]
196+
197+
def get_fieldsets(self, request, obj=None):
198+
if self.model.__name__ not in _FIELDSET_EXTRA:
199+
raise ValueError(f"Unrecognized model {self.model}")
200+
return TaskAdmin.fieldsets + (
201+
(
202+
_("Scheduling"),
203+
{
204+
"fields": _FIELDSET_EXTRA[self.model.__name__],
205+
},
206+
),
207+
)
208+
209+
@admin.display(description="Next run")
210+
def next_run(self, o: Task):
211+
return tools.get_next_cron_time(o.cron_string)
212+
213+
def change_view(self, request, object_id, form_url="", extra_context=None):
214+
extra = extra_context or {}
215+
obj = self.get_object(request, object_id)
216+
try:
217+
execution_list = get_job_executions_for_task(obj.queue, obj)
218+
except (redis.ConnectionError, valkey.ConnectionError) as e:
219+
logger.warn(f"Could not get job executions: {e}")
220+
execution_list = list()
221+
paginator = self.get_paginator(request, execution_list, SCHEDULER_CONFIG.EXECUTIONS_IN_PAGE)
222+
page_number = request.GET.get("p", 1)
223+
page_obj = paginator.get_page(page_number)
224+
page_range = paginator.get_elided_page_range(page_obj.number)
225+
226+
extra.update(
227+
{
228+
"pagination_required": paginator.count > SCHEDULER_CONFIG.EXECUTIONS_IN_PAGE,
229+
"executions": page_obj,
230+
"page_range": page_range,
231+
"page_var": "p",
232+
}
233+
)
234+
235+
return super(TaskAdmin, self).change_view(request, object_id, form_url, extra_context=extra)
236+
237+
def delete_queryset(self, request, queryset):
238+
for job in queryset:
239+
job.unschedule()
240+
super(TaskAdmin, self).delete_queryset(request, queryset)
241+
242+
def delete_model(self, request, obj):
243+
obj.unschedule()
244+
super(TaskAdmin, self).delete_model(request, obj)
245+
246+
@admin.action(description=_("Disable selected %(verbose_name_plural)s"), permissions=("change",))
247+
def disable_selected(self, request, queryset):
248+
rows_updated = 0
249+
for obj in queryset.filter(enabled=True).iterator():
250+
obj.enabled = False
251+
obj.unschedule()
252+
rows_updated += 1
253+
254+
message_bit = "1 job was" if rows_updated == 1 else f"{rows_updated} jobs were"
255+
256+
level = messages.WARNING if not rows_updated else messages.INFO
257+
self.message_user(request, f"{message_bit} successfully disabled and unscheduled.", level=level)
258+
259+
@admin.action(description=_("Enable selected %(verbose_name_plural)s"), permissions=("change",))
260+
def enable_selected(self, request, queryset):
261+
rows_updated = 0
262+
for obj in queryset.filter(enabled=False).iterator():
263+
obj.enabled = True
264+
obj.save()
265+
rows_updated += 1
266+
267+
message_bit = "1 job was" if rows_updated == 1 else f"{rows_updated} jobs were"
268+
level = messages.WARNING if not rows_updated else messages.INFO
269+
self.message_user(request, f"{message_bit} successfully enabled and scheduled.", level=level)
270+
271+
@admin.action(description="Enqueue now", permissions=("change",))
272+
def enqueue_job_now(self, request, queryset):
273+
task_names = []
274+
for task in queryset:
275+
task.enqueue_to_run()
276+
task_names.append(task.name)
277+
self.message_user(
278+
request,
279+
f"The following jobs have been enqueued: {', '.join(task_names)}",
280+
)

scheduler/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .args import TaskKwarg, TaskArg, BaseTaskArg # noqa: F401
22
from .queue import Queue # noqa: F401
33
from .scheduled_task import BaseTask, ScheduledTask, RepeatableTask, CronTask # noqa: F401
4+
from .task import Task # noqa: F401

scheduler/models/scheduled_task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ class ScheduledTask(ScheduledTimeMixin, BaseTask):
395395

396396
def ready_for_schedule(self) -> bool:
397397
return super(ScheduledTask, self).ready_for_schedule() and (
398-
self.scheduled_time is None or self.scheduled_time >= timezone.now()
398+
self.scheduled_time is None or self.scheduled_time >= timezone.now()
399399
)
400400

401401
class Meta:

0 commit comments

Comments
 (0)