1from datetime import timedelta
2
3from plain import models
4from plain.admin.cards import Card
5from plain.admin.views import (
6 AdminModelDetailView,
7 AdminModelListView,
8 AdminViewset,
9 register_viewset,
10)
11from plain.http import ResponseRedirect
12from plain.runtime import settings
13
14from .models import Job, JobRequest, JobResult
15
16
17def _td_format(td_object):
18 seconds = int(td_object.total_seconds())
19 periods = [
20 ("year", 60 * 60 * 24 * 365),
21 ("month", 60 * 60 * 24 * 30),
22 ("day", 60 * 60 * 24),
23 ("hour", 60 * 60),
24 ("minute", 60),
25 ("second", 1),
26 ]
27
28 strings = []
29 for period_name, period_seconds in periods:
30 if seconds > period_seconds:
31 period_value, seconds = divmod(seconds, period_seconds)
32 has_s = "s" if period_value > 1 else ""
33 strings.append(f"{period_value} {period_name}{has_s}")
34
35 return ", ".join(strings)
36
37
38class SuccessfulJobsCard(Card):
39 title = "Successful Jobs"
40 text = "View"
41
42 def get_number(self):
43 return JobResult.query.successful().count()
44
45 def get_link(self):
46 return JobResultViewset.ListView.get_view_url() + "?display=Successful"
47
48
49class ErroredJobsCard(Card):
50 title = "Errored Jobs"
51 text = "View"
52
53 def get_number(self):
54 return JobResult.query.errored().count()
55
56 def get_link(self):
57 return JobResultViewset.ListView.get_view_url() + "?display=Errored"
58
59
60class LostJobsCard(Card):
61 title = "Lost Jobs"
62 text = "View" # TODO make not required - just an icon?
63
64 def get_description(self):
65 delta = timedelta(seconds=settings.WORKER_JOBS_LOST_AFTER)
66 return f"Jobs are considered lost after {_td_format(delta)}"
67
68 def get_number(self):
69 return JobResult.query.lost().count()
70
71 def get_link(self):
72 return JobResultViewset.ListView.get_view_url() + "?display=Lost"
73
74
75class RetriedJobsCard(Card):
76 title = "Retried Jobs"
77 text = "View" # TODO make not required - just an icon?
78
79 def get_number(self):
80 return JobResult.query.retried().count()
81
82 def get_link(self):
83 return JobResultViewset.ListView.get_view_url() + "?display=Retried"
84
85
86class WaitingJobsCard(Card):
87 title = "Waiting Jobs"
88
89 def get_number(self):
90 return Job.query.waiting().count()
91
92
93class RunningJobsCard(Card):
94 title = "Running Jobs"
95
96 def get_number(self):
97 return Job.query.running().count()
98
99
100@register_viewset
101class JobRequestViewset(AdminViewset):
102 class ListView(AdminModelListView):
103 nav_section = "Worker"
104 nav_icon = "gear"
105 model = JobRequest
106 title = "Job requests"
107 fields = ["id", "job_class", "priority", "created_at", "start_at", "unique_key"]
108 actions = ["Delete"]
109
110 def perform_action(self, action: str, target_ids: list):
111 if action == "Delete":
112 JobRequest.query.filter(id__in=target_ids).delete()
113
114 class DetailView(AdminModelDetailView):
115 model = JobRequest
116 title = "Job Request"
117
118
119@register_viewset
120class JobViewset(AdminViewset):
121 class ListView(AdminModelListView):
122 nav_section = "Worker"
123 nav_icon = "gear"
124 model = Job
125 fields = [
126 "id",
127 "job_class",
128 "priority",
129 "created_at",
130 "started_at",
131 "unique_key",
132 ]
133 actions = ["Delete"]
134 cards = [
135 WaitingJobsCard,
136 RunningJobsCard,
137 ]
138
139 def perform_action(self, action: str, target_ids: list):
140 if action == "Delete":
141 Job.query.filter(id__in=target_ids).delete()
142
143 class DetailView(AdminModelDetailView):
144 model = Job
145
146
147@register_viewset
148class JobResultViewset(AdminViewset):
149 class ListView(AdminModelListView):
150 nav_section = "Worker"
151 nav_icon = "gear"
152 model = JobResult
153 title = "Job results"
154 fields = [
155 "id",
156 "job_class",
157 "priority",
158 "created_at",
159 "status",
160 "retried",
161 "is_retry",
162 ]
163 search_fields = [
164 "uuid",
165 "job_uuid",
166 "job_request_uuid",
167 "job_class",
168 ]
169 cards = [
170 SuccessfulJobsCard,
171 ErroredJobsCard,
172 LostJobsCard,
173 RetriedJobsCard,
174 ]
175 filters = [
176 "Successful",
177 "Errored",
178 "Cancelled",
179 "Lost",
180 "Retried",
181 ]
182 actions = [
183 "Retry",
184 ]
185 allow_global_search = False
186
187 def get_initial_queryset(self):
188 queryset = super().get_initial_queryset()
189 queryset = queryset.annotate(
190 retried=models.Case(
191 models.When(retry_job_request_uuid__isnull=False, then=True),
192 default=False,
193 output_field=models.BooleanField(),
194 ),
195 is_retry=models.Case(
196 models.When(retry_attempt__gt=0, then=True),
197 default=False,
198 output_field=models.BooleanField(),
199 ),
200 )
201 if self.display == "Successful":
202 return queryset.successful()
203 if self.display == "Errored":
204 return queryset.errored()
205 if self.display == "Cancelled":
206 return queryset.cancelled()
207 if self.display == "Lost":
208 return queryset.lost()
209 if self.display == "Retried":
210 return queryset.retried()
211 return queryset
212
213 def get_fields(self):
214 fields = super().get_fields()
215 if self.display == "Retried":
216 fields.append("retries")
217 fields.append("retry_attempt")
218 return fields
219
220 def perform_action(self, action: str, target_ids: list):
221 if action == "Retry":
222 for result in JobResult.query.filter(id__in=target_ids):
223 result.retry_job(delay=0)
224 else:
225 raise ValueError("Invalid action")
226
227 class DetailView(AdminModelDetailView):
228 model = JobResult
229 title = "Job result"
230
231 def post(self):
232 self.object.retry_job(delay=0)
233 return ResponseRedirect(".")