Admin improvements (#11)

* Add the group column to OrmQ admin page

* Add group field to tasks list and search_field

* Add search by name to Schedule admin page

* Limit succesful task history per schedule with SAVE_LIMIT_GROUP config

* Improve ScheduleAdmin list view performance

Co-authored-by: Marc Sabatier <marc@sabatier.online>
This commit is contained in:
Stan Triepels
2022-10-14 17:13:48 +02:00
committed by GitHub
co-authored by Marc Sabatier
parent 4aa6325ae8
commit e3f959c258
4 changed files with 61 additions and 17 deletions
+34 -8
View File
@@ -1,16 +1,19 @@
"""Admin module for Django.""" """Admin module for Django."""
from django.urls import reverse
from django.utils.html import format_html
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db.models.expressions import OuterRef, Subquery
from django_q.conf import Conf, croniter from django_q.conf import Conf, croniter
from django_q.models import Failure, OrmQ, Schedule, Success from django_q.models import Failure, OrmQ, Schedule, Success, Task
from django_q.tasks import async_task from django_q.tasks import async_task
class TaskAdmin(admin.ModelAdmin): class TaskAdmin(admin.ModelAdmin):
"""model admin for success tasks.""" """model admin for success tasks."""
list_display = ("name", "func", "started", "stopped", "time_taken", "group") list_display = ("name", "group", "func", "started", "stopped", "time_taken")
def has_add_permission(self, request): def has_add_permission(self, request):
"""Don't allow adds.""" """Don't allow adds."""
@@ -43,14 +46,14 @@ retry_failed.short_description = _("Resubmit selected tasks to queue")
class FailAdmin(admin.ModelAdmin): class FailAdmin(admin.ModelAdmin):
"""model admin for failed tasks.""" """model admin for failed tasks."""
list_display = ("name", "func", "started", "stopped", "short_result") list_display = ("name", "group", "func", "started", "stopped", "short_result")
def has_add_permission(self, request): def has_add_permission(self, request):
"""Don't allow adds.""" """Don't allow adds."""
return False return False
actions = [retry_failed] actions = [retry_failed]
search_fields = ("name", "func") search_fields = ("name", "func", "group")
list_filter = ("group",) list_filter = ("group",)
readonly_fields = [] readonly_fields = []
@@ -70,8 +73,8 @@ class ScheduleAdmin(admin.ModelAdmin):
"repeats", "repeats",
"cluster", "cluster",
"next_run", "next_run",
"last_run", "get_last_run",
"success", "get_success",
) )
# optional cron strings # optional cron strings
@@ -79,14 +82,37 @@ class ScheduleAdmin(admin.ModelAdmin):
readonly_fields = ("cron",) readonly_fields = ("cron",)
list_filter = ("next_run", "schedule_type", "cluster") list_filter = ("next_run", "schedule_type", "cluster")
search_fields = ("func",) search_fields = ("name", "func",)
list_display_links = ("id", "name") list_display_links = ("id", "name")
def get_queryset(self, request):
qs = super().get_queryset(request)
task_query = Task.objects.filter(id=OuterRef('task')).values('id', 'name', 'success')
qs = qs.annotate(task_id=Subquery(task_query.values('id')), task_name=Subquery(task_query.values('name')),
task_success=Subquery(task_query.values('success')))
return qs
def get_success(self, obj):
return obj.task_success
get_success.boolean = True
get_success.short_description = _("success")
def get_last_run(self, obj):
if obj.task_name is not None:
if obj.task_success:
url = reverse("admin:django_q_success_change", args=(obj.task_id,))
else:
url = reverse("admin:django_q_failure_change", args=(obj.task_id,))
return format_html(f'<a href="{url}">[{obj.task_name}]</a>')
return None
get_last_run.allow_tags = True
get_last_run.short_description = _("last_run")
class QueueAdmin(admin.ModelAdmin): class QueueAdmin(admin.ModelAdmin):
"""queue admin for ORM broker""" """queue admin for ORM broker"""
list_display = ("id", "key", "task_id", "name", "func", "lock") list_display = ("id", "key", "name", "group", "func", "lock", "task_id")
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
obj.save(using=Conf.ORM) obj.save(using=Conf.ORM)
+15 -7
View File
@@ -479,12 +479,21 @@ def save_task(task, broker: Broker):
# SAVE LIMIT > 0: Prune database, SAVE_LIMIT 0: No pruning # SAVE LIMIT > 0: Prune database, SAVE_LIMIT 0: No pruning
close_old_django_connections() close_old_django_connections()
try: try:
with db.transaction.atomic(): if task["success"]:
last = Success.objects.select_for_update().last() # first apply per group success history limit
if task["success"] and 0 < Conf.SAVE_LIMIT <= Success.objects.count(): if "group" in task:
last.delete() with db.transaction.atomic():
qs = Success.objects.filter(group=task["group"])
last = qs.select_for_update().last()
if Conf.SAVE_LIMIT_PER_GROUP <= qs.count():
last.delete()
# then apply global success history limit
with db.transaction.atomic():
last = Success.objects.select_for_update().last()
if Conf.SAVE_LIMIT <= Success.objects.count():
last.delete()
# check if this task has previous results # check if this task has previous results
if Task.objects.filter(id=task["id"], name=task["name"]).exists(): try:
existing_task = Task.objects.get(id=task["id"], name=task["name"]) existing_task = Task.objects.get(id=task["id"], name=task["name"])
# only update the result if it hasn't succeeded yet # only update the result if it hasn't succeeded yet
if not existing_task.success: if not existing_task.success:
@@ -499,8 +508,7 @@ def save_task(task, broker: Broker):
and existing_task.attempt_count >= Conf.MAX_ATTEMPTS and existing_task.attempt_count >= Conf.MAX_ATTEMPTS
): ):
broker.acknowledge(task["ack_id"]) broker.acknowledge(task["ack_id"])
except Task.DoesNotExist:
else:
func = task["func"] func = task["func"]
# convert func to string # convert func to string
if inspect.isfunction(func): if inspect.isfunction(func):
+6 -2
View File
@@ -86,6 +86,10 @@ class Conf:
# Failures are always saved # Failures are always saved
SAVE_LIMIT = conf.get("save_limit", 250) SAVE_LIMIT = conf.get("save_limit", 250)
# Maximum number of successful tasks of the same group kept in the database. 0 saves everything. -1 saves none
# Failures are always saved
SAVE_LIMIT_PER_GROUP = conf.get("save_limit_per_group", 5)
# Guard loop sleep in seconds. Should be between 0 and 60 seconds. # Guard loop sleep in seconds. Should be between 0 and 60 seconds.
GUARD_CYCLE = conf.get("guard_cycle", 0.5) GUARD_CYCLE = conf.get("guard_cycle", 0.5)
@@ -137,8 +141,8 @@ class Conf:
# Verify if retry and timeout settings are correct # Verify if retry and timeout settings are correct
if not TIMEOUT or (TIMEOUT > RETRY): if not TIMEOUT or (TIMEOUT > RETRY):
warn( warn(
"""Retry and timeout are misconfigured. Set retry larger than timeout, """Retry and timeout are misconfigured. Set retry larger than timeout,
failure to do so will cause the tasks to be retriggered before completion. failure to do so will cause the tasks to be retriggered before completion.
See https://django-q.readthedocs.io/en/latest/configure.html#retry for details.""" See https://django-q.readthedocs.io/en/latest/configure.html#retry for details."""
) )
+6
View File
@@ -220,7 +220,10 @@ class Schedule(models.Model):
return self.func return self.func
success.boolean = True success.boolean = True
success.short_description = _("success")
last_run.allow_tags = True last_run.allow_tags = True
last_run.short_description = _("last_run")
class Meta: class Meta:
app_label = "django_q" app_label = "django_q"
@@ -246,6 +249,9 @@ class OrmQ(models.Model):
def name(self): def name(self):
return self.task()["name"] return self.task()["name"]
def group(self):
return self.task().get("group")
class Meta: class Meta:
app_label = "django_q" app_label = "django_q"
verbose_name = _("Queued task") verbose_name = _("Queued task")