dj-queue 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {dj_queue-0.2.0 → dj_queue-0.2.2}/PKG-INFO +4 -3
  2. {dj_queue-0.2.0 → dj_queue-0.2.2}/README.md +3 -2
  3. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/admin.py +133 -7
  4. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/apps.py +1 -1
  5. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/dashboard.py +12 -5
  6. dj_queue-0.2.2/dj_queue/templates/admin/dj_queue/change_form.html +69 -0
  7. dj_queue-0.2.2/dj_queue/templates/admin/dj_queue/change_list.html +11 -0
  8. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/templates/admin/dj_queue/dashboard.html +47 -33
  9. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/templates/admin/dj_queue/queue_jobs.html +107 -10
  10. {dj_queue-0.2.0 → dj_queue-0.2.2}/pyproject.toml +1 -1
  11. {dj_queue-0.2.0 → dj_queue-0.2.2}/LICENSE +0 -0
  12. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/__init__.py +0 -0
  13. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/api.py +0 -0
  14. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/backend.py +0 -0
  15. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/config.py +0 -0
  16. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/contrib/__init__.py +0 -0
  17. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/contrib/asgi.py +0 -0
  18. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/contrib/gunicorn.py +0 -0
  19. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/db.py +0 -0
  20. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/exceptions.py +0 -0
  21. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/hooks.py +0 -0
  22. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/log.py +0 -0
  23. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/management/__init__.py +0 -0
  24. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/management/commands/__init__.py +0 -0
  25. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/management/commands/dj_queue.py +0 -0
  26. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/management/commands/dj_queue_health.py +0 -0
  27. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  28. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/migrations/0001_initial.py +0 -0
  29. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  30. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  31. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/migrations/0004_dashboard.py +0 -0
  32. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/migrations/__init__.py +0 -0
  33. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/models/__init__.py +0 -0
  34. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/models/jobs.py +0 -0
  35. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/models/recurring.py +0 -0
  36. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/models/runtime.py +0 -0
  37. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/operations/__init__.py +0 -0
  38. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/operations/cleanup.py +0 -0
  39. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/operations/concurrency.py +0 -0
  40. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/operations/jobs.py +0 -0
  41. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/operations/recurring.py +0 -0
  42. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/routers.py +0 -0
  43. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/__init__.py +0 -0
  44. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/base.py +0 -0
  45. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/dispatcher.py +0 -0
  46. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/errors.py +0 -0
  47. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/interruptible.py +0 -0
  48. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/notify.py +0 -0
  49. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/pidfile.py +0 -0
  50. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/pool.py +0 -0
  51. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/procline.py +0 -0
  52. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/scheduler.py +0 -0
  53. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/supervisor.py +0 -0
  54. {dj_queue-0.2.0 → dj_queue-0.2.2}/dj_queue/runtime/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Database-backed task queue backend for Django's django.tasks framework
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -28,6 +28,7 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  [![CI](https://github.com/coriocactus/dj_queue/actions/workflows/ci.yml/badge.svg)](https://github.com/coriocactus/dj_queue/actions/workflows/ci.yml)
30
30
  ![PyPI](https://img.shields.io/pypi/v/dj-queue.svg)
31
+ [![Latest on Django Packages](https://img.shields.io/badge/pypi/dj-queue-tags.svg)](https://djangopackages.org/packages/p/dj-queue/)
31
32
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dj-queue.svg)
32
33
  ![PyPI - Status](https://img.shields.io/pypi/status/dj-queue.svg)
33
34
  ![PyPI - License](https://img.shields.io/pypi/l/dj-queue.svg)
@@ -414,8 +415,8 @@ pauses, and semaphores.
414
415
  When a task raises, `dj_queue` keeps the job and its failed execution row in the
415
416
  queue database, including the exception class, message, and traceback.
416
417
 
417
- You can retry failed jobs through Django admin, or retry and discard them
418
- through the operations layer:
418
+ You can retry and discard failed jobs through Django admin, or call the same
419
+ operations directly through the operations layer:
419
420
 
420
421
  ```python
421
422
  from dj_queue.operations.jobs import discard_failed_job, retry_failed_job
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/coriocactus/dj_queue/actions/workflows/ci.yml/badge.svg)](https://github.com/coriocactus/dj_queue/actions/workflows/ci.yml)
4
4
  ![PyPI](https://img.shields.io/pypi/v/dj-queue.svg)
5
+ [![Latest on Django Packages](https://img.shields.io/badge/pypi/dj-queue-tags.svg)](https://djangopackages.org/packages/p/dj-queue/)
5
6
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dj-queue.svg)
6
7
  ![PyPI - Status](https://img.shields.io/pypi/status/dj-queue.svg)
7
8
  ![PyPI - License](https://img.shields.io/pypi/l/dj-queue.svg)
@@ -388,8 +389,8 @@ pauses, and semaphores.
388
389
  When a task raises, `dj_queue` keeps the job and its failed execution row in the
389
390
  queue database, including the exception class, message, and traceback.
390
391
 
391
- You can retry failed jobs through Django admin, or retry and discard them
392
- through the operations layer:
392
+ You can retry and discard failed jobs through Django admin, or call the same
393
+ operations directly through the operations layer:
393
394
 
394
395
  ```python
395
396
  from dj_queue.operations.jobs import discard_failed_job, retry_failed_job
@@ -29,10 +29,25 @@ from dj_queue.models import (
29
29
 
30
30
 
31
31
  class DjQueueFirstAdminSite(admin.AdminSite):
32
+ def _dashboard_app_url(self):
33
+ return reverse("admin:dj_queue_dashboard_changelist", current_app=self.name)
34
+
32
35
  def get_app_list(self, request, app_label=None):
33
36
  app_list = super().get_app_list(request, app_label=app_label)
37
+ for app in app_list:
38
+ if app["app_label"] == "dj_queue":
39
+ app["app_url"] = self._dashboard_app_url()
34
40
  return sorted(app_list, key=lambda app: app["app_label"] != "dj_queue")
35
41
 
42
+ def app_index(self, request, app_label, extra_context=None):
43
+ if app_label == "dj_queue":
44
+ url = self._dashboard_app_url()
45
+ query = request.GET.urlencode()
46
+ if query:
47
+ url = f"{url}?{query}"
48
+ return HttpResponseRedirect(url)
49
+ return super().app_index(request, app_label, extra_context=extra_context)
50
+
36
51
 
37
52
  admin.site.__class__ = DjQueueFirstAdminSite
38
53
 
@@ -64,6 +79,7 @@ class DashboardAdmin(admin.ModelAdmin):
64
79
  context = {
65
80
  **self.admin_site.each_context(request),
66
81
  **dashboard.dashboard_context(backend_alias=backend_alias, query_params=request.GET),
82
+ "title": "dj_queue",
67
83
  }
68
84
  if extra_context:
69
85
  context.update(extra_context)
@@ -96,16 +112,19 @@ class DashboardAdmin(admin.ModelAdmin):
96
112
  backend_alias = dashboard.resolve_backend_alias(request.GET.get("backend"))
97
113
  state = request.GET.get("state", "ready")
98
114
  page_number = request.GET.get("page", 1)
115
+ queue_context = dashboard.queue_page_context(
116
+ backend_alias=backend_alias,
117
+ queue_name=queue_name,
118
+ state=state,
119
+ page_number=page_number,
120
+ query_params=request.GET,
121
+ )
99
122
  context = {
100
123
  **self.admin_site.each_context(request),
101
- **dashboard.queue_page_context(
102
- backend_alias=backend_alias,
103
- queue_name=queue_name,
104
- state=state,
105
- page_number=page_number,
106
- query_params=request.GET,
107
- ),
124
+ **queue_context,
108
125
  "job_actions": dashboard.job_actions_for_state(state),
126
+ "title": "dj_queue",
127
+ "subtitle": queue_name,
109
128
  }
110
129
  return TemplateResponse(request, "admin/dj_queue/queue_jobs.html", context)
111
130
 
@@ -175,10 +194,35 @@ class HiddenSidebarAdminMixin:
175
194
  backend_query_param = "backend"
176
195
  backend_filter_field = None
177
196
  ignored_filter_params = ()
197
+ change_list_template = "admin/dj_queue/change_list.html"
198
+ change_form_template = "admin/dj_queue/change_form.html"
178
199
 
179
200
  def get_list_filter(self, request):
180
201
  return (BackendListFilter, *tuple(super().get_list_filter(request)))
181
202
 
203
+ def changelist_view(self, request, extra_context=None):
204
+ extra_context = {**(extra_context or {}), "dashboard_url": self._dashboard_url(request)}
205
+ return super().changelist_view(request, extra_context=extra_context)
206
+
207
+ def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
208
+ obj = self.get_object(request, object_id) if object_id is not None else None
209
+ if request.method == "POST":
210
+ action = request.POST.get("_djq_object_action")
211
+ if action and obj is not None:
212
+ return self.handle_change_action(request, obj, action)
213
+
214
+ extra_context = {
215
+ **(extra_context or {}),
216
+ "dashboard_url": self._dashboard_url(request),
217
+ "change_actions": self.get_change_actions(request, obj),
218
+ }
219
+ return super().changeform_view(
220
+ request,
221
+ object_id=object_id,
222
+ form_url=form_url,
223
+ extra_context=extra_context,
224
+ )
225
+
182
226
  def has_add_permission(self, request):
183
227
  return False
184
228
 
@@ -239,6 +283,26 @@ class HiddenSidebarAdminMixin:
239
283
  def _backend_database_alias(self, request):
240
284
  return get_database_alias(self._backend_alias(request))
241
285
 
286
+ def _dashboard_url(self, request):
287
+ return f"{reverse('admin:dj_queue_dashboard_changelist')}?{urlencode({'backend': self._backend_alias(request)})}"
288
+
289
+ def get_change_actions(self, request, obj):
290
+ return ()
291
+
292
+ def handle_change_action(self, request, obj, action):
293
+ return HttpResponseRedirect(request.get_full_path())
294
+
295
+ def _change_url(self, *, object_id, backend_alias):
296
+ url = reverse(
297
+ f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_change",
298
+ args=[object_id],
299
+ )
300
+ return f"{url}?{urlencode({'backend': backend_alias})}"
301
+
302
+ def _changelist_url(self, *, backend_alias):
303
+ url = reverse(f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist")
304
+ return f"{url}?{urlencode({'backend': backend_alias})}"
305
+
242
306
 
243
307
  class JobStatusListFilter(admin.SimpleListFilter):
244
308
  title = "status"
@@ -409,6 +473,39 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
409
473
  url = f"{reverse('admin:dj_queue_dashboard_queue', args=[obj.queue_name])}?{urlencode(query)}"
410
474
  return format_html('<a href="{}">{}</a>', url, obj.queue_name)
411
475
 
476
+ def get_change_actions(self, request, obj):
477
+ if obj is None or obj.status != "failed":
478
+ return ()
479
+ return (
480
+ {"name": "retry", "label": "Retry failed job", "css_class": "djq-object-action-retry"},
481
+ {
482
+ "name": "discard",
483
+ "label": "Discard failed job",
484
+ "css_class": "djq-object-action-discard",
485
+ },
486
+ )
487
+
488
+ def handle_change_action(self, request, obj, action):
489
+ if obj.status != "failed":
490
+ self.message_user(request, "This job is not failed", level=messages.ERROR)
491
+ return HttpResponseRedirect(
492
+ self._change_url(object_id=obj.pk, backend_alias=obj.backend_name)
493
+ )
494
+
495
+ if action == "retry":
496
+ obj.failed_execution.retry()
497
+ self.message_user(request, "Retried failed job", level=messages.SUCCESS)
498
+ return HttpResponseRedirect(
499
+ self._change_url(object_id=obj.pk, backend_alias=obj.backend_name)
500
+ )
501
+
502
+ if action == "discard":
503
+ obj.failed_execution.discard()
504
+ self.message_user(request, "Discarded failed job", level=messages.SUCCESS)
505
+ return HttpResponseRedirect(self._changelist_url(backend_alias=obj.backend_name))
506
+
507
+ return HttpResponseRedirect(self._change_url(object_id=obj.pk, backend_alias=obj.backend_name))
508
+
412
509
 
413
510
  @admin.register(FailedExecution)
414
511
  class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
@@ -436,6 +533,35 @@ class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
436
533
  discarded += execution.discard()
437
534
  self.message_user(request, f"Discarded {discarded} failed jobs", level=messages.SUCCESS)
438
535
 
536
+ def get_change_actions(self, request, obj):
537
+ if obj is None:
538
+ return ()
539
+ return (
540
+ {"name": "retry", "label": "Retry failed job", "css_class": "djq-object-action-retry"},
541
+ {
542
+ "name": "discard",
543
+ "label": "Discard failed job",
544
+ "css_class": "djq-object-action-discard",
545
+ },
546
+ )
547
+
548
+ def handle_change_action(self, request, obj, action):
549
+ backend_alias = obj.job.backend_name
550
+
551
+ if action == "retry":
552
+ job_id = obj.job_id
553
+ obj.retry()
554
+ self.message_user(request, "Retried failed job", level=messages.SUCCESS)
555
+ url = reverse("admin:dj_queue_job_change", args=[job_id])
556
+ return HttpResponseRedirect(f"{url}?{urlencode({'backend': backend_alias})}")
557
+
558
+ if action == "discard":
559
+ obj.discard()
560
+ self.message_user(request, "Discarded failed job", level=messages.SUCCESS)
561
+ return HttpResponseRedirect(self._changelist_url(backend_alias=backend_alias))
562
+
563
+ return HttpResponseRedirect(self._change_url(object_id=obj.pk, backend_alias=backend_alias))
564
+
439
565
 
440
566
  @admin.register(Process)
441
567
  class ProcessAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
@@ -3,4 +3,4 @@ from django.apps import AppConfig
3
3
 
4
4
  class DjQueueConfig(AppConfig):
5
5
  name = "dj_queue"
6
- verbose_name = "dj queue"
6
+ verbose_name = "dj_queue"
@@ -550,7 +550,7 @@ def _summary_cards(*, backend_alias, queue_rows, process_rows, recurring_rows, s
550
550
  "detail": f"{live_processes} live, {stale_processes} stale",
551
551
  },
552
552
  {
553
- "label": "control plane",
553
+ "label": "control-plane",
554
554
  "value": len(recurring_rows) + len(semaphore_rows),
555
555
  "detail": f"{len(recurring_rows)} recurring and {len(semaphore_rows)} semaphores",
556
556
  },
@@ -962,6 +962,7 @@ def _overview_headers(
962
962
  explicit_sort=explicit_sort,
963
963
  page_param=page_param,
964
964
  anchor=anchor,
965
+ preserve_anchor=True,
965
966
  )
966
967
 
967
968
 
@@ -974,11 +975,12 @@ def _queue_page_headers(*, state, query_params, sort, explicit_sort, page_param,
974
975
  explicit_sort=explicit_sort,
975
976
  page_param=page_param,
976
977
  anchor=anchor,
978
+ preserve_anchor=False,
977
979
  )
978
980
 
979
981
 
980
982
  def _sortable_headers(
981
- *, fields, query_params, sort_param, sort, explicit_sort, page_param, anchor
983
+ *, fields, query_params, sort_param, sort, explicit_sort, page_param, anchor, preserve_anchor
982
984
  ):
983
985
  sort_fields = _parse_sort_fields(sort) if explicit_sort else ()
984
986
 
@@ -1021,6 +1023,7 @@ def _sortable_headers(
1021
1023
  sort_value=primary_sort,
1022
1024
  page_param=page_param,
1023
1025
  anchor=anchor,
1026
+ preserve_anchor=preserve_anchor,
1024
1027
  )
1025
1028
  toggle_url = (
1026
1029
  _overview_sort_url(
@@ -1029,6 +1032,7 @@ def _sortable_headers(
1029
1032
  sort_value=toggle_sort,
1030
1033
  page_param=page_param,
1031
1034
  anchor=anchor,
1035
+ preserve_anchor=preserve_anchor,
1032
1036
  )
1033
1037
  if is_sorted
1034
1038
  else primary_url
@@ -1040,6 +1044,7 @@ def _sortable_headers(
1040
1044
  sort_value=remove_sort,
1041
1045
  page_param=page_param,
1042
1046
  anchor=anchor,
1047
+ preserve_anchor=preserve_anchor,
1043
1048
  )
1044
1049
  if is_sorted
1045
1050
  else None
@@ -1073,7 +1078,9 @@ def _queue_result_count_text(*, page_obj, total_count):
1073
1078
  return f"{page_obj.start_index()}-{page_obj.end_index()} of {total_count} jobs"
1074
1079
 
1075
1080
 
1076
- def _overview_sort_url(*, query_params, sort_param, sort_value, page_param, anchor):
1081
+ def _overview_sort_url(
1082
+ *, query_params, sort_param, sort_value, page_param, anchor, preserve_anchor
1083
+ ):
1077
1084
  params = query_params.copy()
1078
1085
  if sort_value:
1079
1086
  params[sort_param] = sort_value
@@ -1082,8 +1089,8 @@ def _overview_sort_url(*, query_params, sort_param, sort_value, page_param, anch
1082
1089
  params.pop(page_param, None)
1083
1090
  url = params.urlencode() if hasattr(params, "urlencode") else urlencode(params, doseq=True)
1084
1091
  if not url:
1085
- return f"?#{anchor}"
1086
- return f"?{url}#{anchor}"
1092
+ return f"?#{anchor}" if preserve_anchor else "?"
1093
+ return f"?{url}#{anchor}" if preserve_anchor else f"?{url}"
1087
1094
 
1088
1095
 
1089
1096
  def _queue_rows(*, backend_alias, now, process_cutoff):
@@ -0,0 +1,69 @@
1
+ {% extends "admin/change_form.html" %}
2
+
3
+ {% load i18n admin_urls %}
4
+
5
+ {% block extrastyle %}
6
+ {{ block.super }}
7
+ <style>
8
+ .submit-row form {
9
+ display: inline;
10
+ }
11
+
12
+ .submit-row .djq-object-action {
13
+ border: none;
14
+ border-radius: 4px;
15
+ min-height: 2.1875rem;
16
+ padding: 0.625rem 0.9375rem;
17
+ color: var(--button-fg);
18
+ font-size: 0.75rem;
19
+ line-height: 0.9375rem;
20
+ text-transform: uppercase;
21
+ letter-spacing: 0.5px;
22
+ cursor: pointer;
23
+ }
24
+
25
+ .submit-row .djq-object-action-retry {
26
+ background: var(--button-bg);
27
+ }
28
+
29
+ .submit-row .djq-object-action-retry:hover,
30
+ .submit-row .djq-object-action-retry:focus {
31
+ background: var(--button-hover-bg);
32
+ }
33
+
34
+ .submit-row .djq-object-action-discard {
35
+ background: var(--delete-button-bg);
36
+ }
37
+
38
+ .submit-row .djq-object-action-discard:hover,
39
+ .submit-row .djq-object-action-discard:focus {
40
+ background: var(--delete-button-hover-bg);
41
+ }
42
+ </style>
43
+ {% endblock extrastyle %}
44
+
45
+ {% block breadcrumbs %}
46
+ <div class="breadcrumbs">
47
+ <a href='{% url "admin:index" %}'>{% translate "Home" %}</a>
48
+ › <a href="{{ dashboard_url }}">dj_queue</a>
49
+ › {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
50
+ › {% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}
51
+ </div>
52
+ {% endblock breadcrumbs %}
53
+
54
+ {% block object-tools %}{% endblock object-tools %}
55
+
56
+ {% block submit_buttons_top %}{% endblock submit_buttons_top %}
57
+
58
+ {% block submit_buttons_bottom %}
59
+ {% if change_actions %}
60
+ <div class="submit-row">
61
+ {% for action in change_actions %}
62
+ <form method="post" action="{{ request.get_full_path }}">
63
+ {% csrf_token %}
64
+ <button type="submit" name="_djq_object_action" value="{{ action.name }}" class="djq-object-action {{ action.css_class }}">{{ action.label }}</button>
65
+ </form>
66
+ {% endfor %}
67
+ </div>
68
+ {% endif %}
69
+ {% endblock submit_buttons_bottom %}
@@ -0,0 +1,11 @@
1
+ {% extends "admin/change_list.html" %}
2
+
3
+ {% load i18n %}
4
+
5
+ {% block breadcrumbs %}
6
+ <div class="breadcrumbs">
7
+ <a href='{% url "admin:index" %}'>{% translate "Home" %}</a>
8
+ › <a href="{{ dashboard_url }}">dj_queue</a>
9
+ › {{ cl.opts.verbose_name_plural|capfirst }}
10
+ </div>
11
+ {% endblock breadcrumbs %}
@@ -7,6 +7,8 @@
7
7
  <link rel="stylesheet" type="text/css" href="{% static 'admin/css/changelists.css' %}">
8
8
  <style>
9
9
  #content-main.djq-dashboard {
10
+ --djq-status-live-fg: #0b6b2d;
11
+ --djq-status-stale-fg: #8a4b00;
10
12
  width: 100%;
11
13
  max-width: 100%;
12
14
  min-width: 0;
@@ -16,6 +18,18 @@
16
18
  box-sizing: border-box;
17
19
  }
18
20
 
21
+ @media (prefers-color-scheme: dark) {
22
+ html:not([data-theme="light"]) #content-main.djq-dashboard {
23
+ --djq-status-live-fg: #7ad7a2;
24
+ --djq-status-stale-fg: #ffbf69;
25
+ }
26
+ }
27
+
28
+ html[data-theme="dark"] #content-main.djq-dashboard {
29
+ --djq-status-live-fg: #7ad7a2;
30
+ --djq-status-stale-fg: #ffbf69;
31
+ }
32
+
19
33
  .djq-hero,
20
34
  .djq-summary-grid,
21
35
  .djq-section {
@@ -30,8 +44,8 @@
30
44
  display: grid;
31
45
  gap: 1rem;
32
46
  padding: 1.25rem;
33
- border: 1px solid rgba(38, 75, 94, 0.14);
34
- background: linear-gradient(180deg, rgba(64, 110, 135, 0.06), rgba(64, 110, 135, 0.02));
47
+ border: 1px solid var(--hairline-color);
48
+ background: var(--darkened-bg);
35
49
  }
36
50
 
37
51
  .djq-hero-main {
@@ -43,7 +57,7 @@
43
57
  p.djq-eyebrow {
44
58
  padding: 0;
45
59
  margin: 0;
46
- color: #406e87;
60
+ color: var(--link-fg);
47
61
  font-size: 0.78rem;
48
62
  font-weight: 700;
49
63
  letter-spacing: 0.08em;
@@ -60,7 +74,7 @@
60
74
  max-width: 44rem;
61
75
  padding: 0;
62
76
  margin: 0;
63
- color: #4c5b66;
77
+ color: var(--body-quiet-color);
64
78
  font-size: 0.95rem;
65
79
  line-height: 1.5;
66
80
  }
@@ -69,8 +83,8 @@
69
83
  display: grid;
70
84
  gap: 0.9rem;
71
85
  padding: 1rem;
72
- border: 1px solid rgba(38, 75, 94, 0.12);
73
- background: rgba(255, 255, 255, 0.84);
86
+ border: 1px solid var(--hairline-color);
87
+ background: var(--body-bg);
74
88
  min-width: 0;
75
89
  }
76
90
 
@@ -88,7 +102,7 @@
88
102
  }
89
103
 
90
104
  .djq-backend-switch label {
91
- color: #264b5e;
105
+ color: var(--link-selected-fg);
92
106
  font-size: 0.78rem;
93
107
  font-weight: 700;
94
108
  letter-spacing: 0.06em;
@@ -107,7 +121,7 @@
107
121
  p.djq-backend-caption {
108
122
  padding: 0;
109
123
  margin: 0;
110
- color: #4c5b66;
124
+ color: var(--body-quiet-color);
111
125
  font-size: 0.86rem;
112
126
  line-height: 1.45;
113
127
  }
@@ -126,7 +140,7 @@
126
140
 
127
141
  .djq-backend-facts dt {
128
142
  margin: 0;
129
- color: #4c5b66;
143
+ color: var(--body-quiet-color);
130
144
  font-size: 0.76rem;
131
145
  font-weight: 700;
132
146
  letter-spacing: 0.06em;
@@ -135,7 +149,7 @@
135
149
 
136
150
  .djq-backend-facts dd {
137
151
  margin: 0.2rem 0 0;
138
- color: #1f1f1f;
152
+ color: var(--body-fg);
139
153
  font-size: 0.95rem;
140
154
  line-height: 1.3;
141
155
  word-break: break-word;
@@ -143,8 +157,8 @@
143
157
 
144
158
  .djq-raw-panel {
145
159
  padding: 0.95rem 1rem;
146
- border: 1px solid rgba(38, 75, 94, 0.12);
147
- background: rgba(255, 255, 255, 0.82);
160
+ border: 1px solid var(--hairline-color);
161
+ background: var(--body-bg);
148
162
  min-width: 0;
149
163
  }
150
164
 
@@ -182,7 +196,7 @@
182
196
  .djq-raw-group h3 {
183
197
  padding: 0;
184
198
  margin: 0;
185
- color: #4c5b66;
199
+ color: var(--body-quiet-color);
186
200
  font-size: 0.78rem;
187
201
  font-weight: 700;
188
202
  letter-spacing: 0.08em;
@@ -232,8 +246,8 @@
232
246
  }
233
247
 
234
248
  .djq-summary-card {
235
- border: 1px solid rgba(38, 75, 94, 0.12);
236
- background: rgba(255, 255, 255, 0.9);
249
+ border: 1px solid var(--hairline-color);
250
+ background: var(--body-bg);
237
251
  min-width: 0;
238
252
  }
239
253
 
@@ -250,12 +264,12 @@
250
264
  font-size: 1.7rem;
251
265
  font-weight: 700;
252
266
  line-height: 1.1;
253
- color: #1f1f1f;
267
+ color: var(--body-fg);
254
268
  }
255
269
 
256
270
  .djq-summary-detail {
257
271
  margin-top: 0.35rem;
258
- color: #4c5b66;
272
+ color: var(--body-quiet-color);
259
273
  font-size: 0.88rem;
260
274
  }
261
275
 
@@ -276,18 +290,18 @@
276
290
  display: block;
277
291
  width: 100%;
278
292
  padding: 0.5rem;
279
- background: #4f82a0;
280
- color: #fff;
293
+ background: var(--primary);
294
+ color: var(--primary-fg);
281
295
  box-sizing: border-box;
282
296
  }
283
297
 
284
298
  .djq-section-copy {
285
299
  margin: 0;
286
300
  padding: 0.5rem;
287
- color: #4c5b66;
301
+ color: var(--body-quiet-color);
288
302
  font-size: 0.9rem;
289
303
  line-height: 1.45;
290
- border-bottom: 1px solid rgba(38, 75, 94, 0.08);
304
+ border-bottom: 1px solid var(--hairline-color);
291
305
  }
292
306
 
293
307
  .djq-table-wrap {
@@ -354,7 +368,7 @@
354
368
  }
355
369
 
356
370
  .djq-process-row-group {
357
- background: rgba(38, 75, 94, 0.04);
371
+ background: var(--selected-bg);
358
372
  }
359
373
 
360
374
  .djq-process-row-child th:first-child {
@@ -369,25 +383,25 @@
369
383
  top: 0.7rem;
370
384
  bottom: 0.7rem;
371
385
  width: 2px;
372
- background: rgba(38, 75, 94, 0.16);
386
+ background: var(--border-color);
373
387
  }
374
388
 
375
389
  .djq-process-meta {
376
390
  display: block;
377
391
  margin-top: 0.15rem;
378
- color: #4c5b66;
392
+ color: var(--body-quiet-color);
379
393
  font-size: 0.78rem;
380
394
  font-weight: 400;
381
395
  white-space: normal;
382
396
  }
383
397
 
384
398
  .djq-status-live {
385
- color: #0b6b2d;
399
+ color: var(--djq-status-live-fg);
386
400
  font-weight: 700;
387
401
  }
388
402
 
389
403
  .djq-status-stale {
390
- color: #8a4b00;
404
+ color: var(--djq-status-stale-fg);
391
405
  font-weight: 700;
392
406
  }
393
407
 
@@ -416,7 +430,7 @@
416
430
  .djq-empty {
417
431
  margin: 0;
418
432
  padding: 1rem;
419
- color: #4c5b66;
433
+ color: var(--body-quiet-color);
420
434
  font-size: 0.86rem;
421
435
  line-height: 1.45;
422
436
  }
@@ -796,9 +810,9 @@
796
810
  <p>Job-centric rows scoped to the selected backend where possible.</p>
797
811
  </div>
798
812
  <div class="djq-raw-links">
799
- <a href="{% url 'admin:dj_queue_job_changelist' %}?backend={{ backend_alias }}">jobs</a>
800
- <a href="{% url 'admin:dj_queue_failedexecution_changelist' %}?backend={{ backend_alias }}">failed executions</a>
801
- <a href="{% url 'admin:dj_queue_pause_changelist' %}?backend={{ backend_alias }}">pause rows</a>
813
+ <a href="{% url 'admin:dj_queue_job_changelist' %}?backend={{ backend_alias }}">Jobs</a>
814
+ <a href="{% url 'admin:dj_queue_failedexecution_changelist' %}?backend={{ backend_alias }}">Failed executions</a>
815
+ <a href="{% url 'admin:dj_queue_pause_changelist' %}?backend={{ backend_alias }}">Pauses</a>
802
816
  </div>
803
817
  </div>
804
818
 
@@ -808,9 +822,9 @@
808
822
  <p>Control-plane rows scoped by the selected backend's queue database alias.</p>
809
823
  </div>
810
824
  <div class="djq-raw-links">
811
- <a href="{% url 'admin:dj_queue_process_changelist' %}?backend={{ backend_alias }}">processes</a>
812
- <a href="{% url 'admin:dj_queue_recurringtask_changelist' %}?backend={{ backend_alias }}">recurring tasks</a>
813
- <a href="{% url 'admin:dj_queue_semaphore_changelist' %}?backend={{ backend_alias }}">semaphores</a>
825
+ <a href="{% url 'admin:dj_queue_process_changelist' %}?backend={{ backend_alias }}">Processes</a>
826
+ <a href="{% url 'admin:dj_queue_recurringtask_changelist' %}?backend={{ backend_alias }}">Recurring tasks</a>
827
+ <a href="{% url 'admin:dj_queue_semaphore_changelist' %}?backend={{ backend_alias }}">Semaphores</a>
814
828
  </div>
815
829
  </div>
816
830
  </div>
@@ -6,6 +6,29 @@
6
6
  {{ block.super }}
7
7
  <link rel="stylesheet" type="text/css" href="{% static 'admin/css/changelists.css' %}">
8
8
  <style>
9
+ #content-main {
10
+ --djq-tab-selected-bg: var(--primary);
11
+ --djq-tab-selected-border: var(--primary);
12
+ --djq-tab-selected-fg: var(--primary-fg);
13
+ --djq-tab-selected-count-fg: var(--breadcrumbs-link-fg);
14
+ }
15
+
16
+ @media (prefers-color-scheme: dark) {
17
+ html:not([data-theme="light"]) #content-main {
18
+ --djq-tab-selected-bg: var(--selected-row);
19
+ --djq-tab-selected-border: var(--link-selected-fg);
20
+ --djq-tab-selected-fg: var(--body-fg);
21
+ --djq-tab-selected-count-fg: var(--body-quiet-color);
22
+ }
23
+ }
24
+
25
+ html[data-theme="dark"] #content-main {
26
+ --djq-tab-selected-bg: var(--selected-row);
27
+ --djq-tab-selected-border: var(--link-selected-fg);
28
+ --djq-tab-selected-fg: var(--body-fg);
29
+ --djq-tab-selected-count-fg: var(--body-quiet-color);
30
+ }
31
+
9
32
  #toolbar .queue-toolbar-row,
10
33
  #toolbar .queue-toolbar-meta,
11
34
  #toolbar .queue-toolbar-links,
@@ -78,12 +101,71 @@
78
101
  margin-top: 0.5rem;
79
102
  }
80
103
 
81
- .queue-state-tabs {
82
- padding-bottom: 5px;
104
+ .toplinks.queue-state-tabs {
105
+ gap: 1rem;
106
+ margin-top: 0.75rem;
107
+ padding: 0.45rem;
108
+ border: 1px solid var(--hairline-color);
109
+ background: var(--darkened-bg);
83
110
  }
84
111
 
85
- .queue-state-current {
86
- color: var(--link-selected-fg);
112
+ .queue-state-tab {
113
+ display: inline-flex;
114
+ gap: 0.4rem;
115
+ align-items: center;
116
+ min-height: 2.15rem;
117
+ padding: 0 0.9rem;
118
+ border: 1px solid var(--border-color);
119
+ border-radius: 999px;
120
+ background: var(--body-bg);
121
+ color: var(--link-fg);
122
+ font-weight: 600;
123
+ text-decoration: none;
124
+ transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
125
+ }
126
+
127
+ #changelist .queue-state-tabs a.queue-state-tab,
128
+ #changelist .queue-state-tabs a.queue-state-tab:hover,
129
+ #changelist .queue-state-tabs a.queue-state-tab:focus,
130
+ #changelist .queue-state-tabs a.queue-state-tab:visited {
131
+ text-decoration: none;
132
+ }
133
+
134
+ .queue-state-tab:not(.queue-state-tab-current):hover,
135
+ .queue-state-tab:not(.queue-state-tab-current):focus {
136
+ background: var(--selected-bg);
137
+ border-color: var(--primary);
138
+ color: var(--link-hover-color);
139
+ text-decoration: none;
140
+ }
141
+
142
+ .queue-state-tab-current {
143
+ background: var(--djq-tab-selected-bg);
144
+ border-color: var(--djq-tab-selected-border);
145
+ color: var(--djq-tab-selected-fg);
146
+ box-shadow: none;
147
+ }
148
+
149
+ .queue-state-tab-current:hover,
150
+ .queue-state-tab-current:focus {
151
+ background: var(--djq-tab-selected-bg);
152
+ border-color: var(--djq-tab-selected-border);
153
+ color: var(--djq-tab-selected-fg);
154
+ }
155
+
156
+ .queue-state-tab-count {
157
+ color: var(--body-quiet-color);
158
+ font-size: 0.82rem;
159
+ font-weight: 700;
160
+ }
161
+
162
+ .queue-state-tab-current .queue-state-tab-count {
163
+ color: var(--djq-tab-selected-count-fg);
164
+ }
165
+
166
+ .queue-state-tab-current:hover .queue-state-tab-count,
167
+ .queue-state-tab-current:focus .queue-state-tab-count {
168
+ color: var(--djq-tab-selected-count-fg);
87
169
  }
88
170
 
89
171
  #result_list {
@@ -104,15 +186,22 @@
104
186
  {% block breadcrumbs %}
105
187
  <div class="breadcrumbs">
106
188
  <a href="{% url 'admin:index' %}">Home</a>
107
- › <a href="{% url 'admin:dj_queue_dashboard_changelist' %}?backend={{ backend_alias }}">dj queue</a>
189
+ › <a href="{% url 'admin:dj_queue_dashboard_changelist' %}?backend={{ backend_alias }}">dj_queue</a>
190
+ › <a href="{% url 'admin:dj_queue_dashboard_changelist' %}?backend={{ backend_alias }}#queue-summary">queues</a>
108
191
  › {{ queue_name }}
109
192
  </div>
110
193
  {% endblock breadcrumbs %}
111
194
 
112
195
  {% block content_title %}
113
- <h1>{{ queue_name }} {{ state_label }} jobs</h1>
196
+ <h1>
197
+ <a href="{% url 'admin:dj_queue_dashboard_changelist' %}?backend={{ backend_alias }}">dj_queue</a>
198
+ › <a href="{% url 'admin:dj_queue_dashboard_changelist' %}?backend={{ backend_alias }}#queue-summary">queues</a>
199
+ › {{ queue_name }}
200
+ </h1>
114
201
  {% endblock content_title %}
115
202
 
203
+ {% block content_subtitle %}{% endblock content_subtitle %}
204
+
116
205
  {% block content %}
117
206
  <div id="content-main">
118
207
  <div class="module" id="changelist">
@@ -150,12 +239,18 @@
150
239
  {% endif %}
151
240
  </div>
152
241
 
153
- <div class="toplinks queue-state-tabs">
242
+ <div class="toplinks queue-state-tabs" aria-label="Job state navigation">
154
243
  {% for tab in state_tabs %}
155
244
  {% if tab.selected %}
156
- <span class="queue-state-current">{{ tab.label }} ({{ tab.count }})</span>
245
+ <span class="queue-state-tab queue-state-tab-current" aria-current="page">
246
+ <span>{{ tab.label }}</span>
247
+ <span class="queue-state-tab-count">{{ tab.count }}</span>
248
+ </span>
157
249
  {% else %}
158
- <a href="{% url 'admin:dj_queue_dashboard_queue' queue_name %}?backend={{ backend_alias }}&state={{ tab.name }}">{{ tab.label }} ({{ tab.count }})</a>
250
+ <a class="queue-state-tab" href="{% url 'admin:dj_queue_dashboard_queue' queue_name %}?backend={{ backend_alias }}&state={{ tab.name }}">
251
+ <span>{{ tab.label }}</span>
252
+ <span class="queue-state-tab-count">{{ tab.count }}</span>
253
+ </a>
159
254
  {% endif %}
160
255
  {% endfor %}
161
256
  </div>
@@ -212,7 +307,9 @@
212
307
  {% if job_actions %}
213
308
  <td class="action-checkbox"><input type="checkbox" class="action-select" name="job_ids" value="{{ job.id }}"></td>
214
309
  {% endif %}
215
- <th scope="row"><code>{{ job.id }}</code></th>
310
+ <th scope="row">
311
+ <a href="{% url 'admin:dj_queue_job_change' job.id %}?backend={{ backend_alias }}"><code>{{ job.id }}</code></a>
312
+ </th>
216
313
  <td>{{ job.task_path }}</td>
217
314
  <td>{{ job.priority }}</td>
218
315
  <td>{{ job.created_at|date:"Y-m-d H:i:s" }}</td>
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "dj-queue"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Database-backed task queue backend for Django's django.tasks framework"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes