Files
django-q2/django_q/models.py
Stan Triepels e3f959c258 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>
2022-10-14 17:13:48 +02:00

267 lines
8.0 KiB
Python

# Django
from django import get_version
from django.core.exceptions import ValidationError
from django.db import models
from django.template.defaultfilters import truncatechars
from django.urls import reverse
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# External
from picklefield import PickledObjectField
from picklefield.fields import dbsafe_decode
# Local
from django_q.conf import croniter
from django_q.signing import SignedPackage
class Task(models.Model):
id = models.CharField(max_length=32, primary_key=True, editable=False)
name = models.CharField(max_length=100, editable=False)
func = models.CharField(max_length=256)
hook = models.CharField(max_length=256, null=True)
args = PickledObjectField(null=True, protocol=-1)
kwargs = PickledObjectField(null=True, protocol=-1)
result = PickledObjectField(null=True, protocol=-1)
group = models.CharField(max_length=100, editable=False, null=True)
started = models.DateTimeField(editable=False)
stopped = models.DateTimeField(editable=False)
success = models.BooleanField(default=True, editable=False)
attempt_count = models.IntegerField(default=0)
@staticmethod
def get_result(task_id):
if len(task_id) == 32 and Task.objects.filter(id=task_id).exists():
return Task.objects.get(id=task_id).result
elif Task.objects.filter(name=task_id).exists():
return Task.objects.get(name=task_id).result
@staticmethod
def get_result_group(group_id, failures=False):
if failures:
values = Task.objects.filter(group=group_id).values_list(
"result", flat=True
)
else:
values = (
Task.objects.filter(group=group_id)
.exclude(success=False)
.values_list("result", flat=True)
)
return decode_results(values)
def group_result(self, failures=False):
if self.group:
return self.get_result_group(self.group, failures)
@staticmethod
def get_group_count(group_id, failures=False):
if failures:
return Failure.objects.filter(group=group_id).count()
return Task.objects.filter(group=group_id).count()
def group_count(self, failures=False):
if self.group:
return self.get_group_count(self.group, failures)
@staticmethod
def delete_group(group_id, objects=False):
group = Task.objects.filter(group=group_id)
if objects:
return group.delete()
return group.update(group=None)
def group_delete(self, tasks=False):
if self.group:
return self.delete_group(self.group, tasks)
@staticmethod
def get_task(task_id):
if len(task_id) == 32 and Task.objects.filter(id=task_id).exists():
return Task.objects.get(id=task_id)
elif Task.objects.filter(name=task_id).exists():
return Task.objects.get(name=task_id)
@staticmethod
def get_task_group(group_id, failures=True):
if failures:
return Task.objects.filter(group=group_id)
return Task.objects.filter(group=group_id).exclude(success=False)
def time_taken(self):
return (self.stopped - self.started).total_seconds()
@property
def short_result(self):
return truncatechars(self.result, 100)
def __str__(self):
return f"{self.name or self.id}"
class Meta:
app_label = "django_q"
ordering = ["-stopped"]
class SuccessManager(models.Manager):
def get_queryset(self):
return super(SuccessManager, self).get_queryset().filter(success=True)
class Success(Task):
objects = SuccessManager()
class Meta:
app_label = "django_q"
verbose_name = _("Successful task")
verbose_name_plural = _("Successful tasks")
ordering = ["-stopped"]
proxy = True
class FailureManager(models.Manager):
def get_queryset(self):
return super(FailureManager, self).get_queryset().filter(success=False)
class Failure(Task):
objects = FailureManager()
class Meta:
app_label = "django_q"
verbose_name = _("Failed task")
verbose_name_plural = _("Failed tasks")
ordering = ["-stopped"]
proxy = True
# Optional Cron validator
def validate_cron(value):
if not croniter:
raise ImportError(_("Please install croniter to enable cron expressions"))
try:
croniter.expand(value)
except ValueError as e:
raise ValidationError(e)
class Schedule(models.Model):
name = models.CharField(max_length=100, null=True, blank=True)
func = models.CharField(max_length=256, help_text="e.g. module.tasks.function")
hook = models.CharField(
max_length=256,
null=True,
blank=True,
help_text="e.g. module.tasks.result_function",
)
args = models.TextField(null=True, blank=True, help_text=_("e.g. 1, 2, 'John'"))
kwargs = models.TextField(
null=True, blank=True, help_text=_("e.g. x=1, y=2, name='John'")
)
ONCE = "O"
MINUTES = "I"
HOURLY = "H"
DAILY = "D"
WEEKLY = "W"
MONTHLY = "M"
QUARTERLY = "Q"
YEARLY = "Y"
CRON = "C"
TYPE = (
(ONCE, _("Once")),
(MINUTES, _("Minutes")),
(HOURLY, _("Hourly")),
(DAILY, _("Daily")),
(WEEKLY, _("Weekly")),
(MONTHLY, _("Monthly")),
(QUARTERLY, _("Quarterly")),
(YEARLY, _("Yearly")),
(CRON, _("Cron")),
)
schedule_type = models.CharField(
max_length=1, choices=TYPE, default=TYPE[0][0], verbose_name=_("Schedule Type")
)
minutes = models.PositiveSmallIntegerField(
null=True, blank=True, help_text=_("Number of minutes for the Minutes type")
)
repeats = models.IntegerField(
default=-1, verbose_name=_("Repeats"), help_text=_("n = n times, -1 = forever")
)
next_run = models.DateTimeField(
verbose_name=_("Next Run"), default=timezone.now, null=True
)
cron = models.CharField(
max_length=100,
null=True,
blank=True,
validators=[validate_cron],
help_text=_("Cron expression"),
)
task = models.CharField(max_length=100, null=True, editable=False)
cluster = models.CharField(max_length=100, default=None, null=True, blank=True)
def success(self):
if self.task and Task.objects.filter(id=self.task):
return Task.objects.get(id=self.task).success
def last_run(self):
if self.task and Task.objects.filter(id=self.task):
task = Task.objects.get(id=self.task)
if task.success:
url = reverse("admin:django_q_success_change", args=(task.id,))
else:
url = reverse("admin:django_q_failure_change", args=(task.id,))
return format_html(f'<a href="{url}">[{task.name}]</a>')
return None
def __str__(self):
return self.func
success.boolean = True
success.short_description = _("success")
last_run.allow_tags = True
last_run.short_description = _("last_run")
class Meta:
app_label = "django_q"
verbose_name = _("Scheduled task")
verbose_name_plural = _("Scheduled tasks")
ordering = ["next_run"]
class OrmQ(models.Model):
key = models.CharField(max_length=100)
payload = models.TextField()
lock = models.DateTimeField(null=True)
def task(self):
return SignedPackage.loads(self.payload)
def func(self):
return self.task()["func"]
def task_id(self):
return self.task()["id"]
def name(self):
return self.task()["name"]
def group(self):
return self.task().get("group")
class Meta:
app_label = "django_q"
verbose_name = _("Queued task")
verbose_name_plural = _("Queued tasks")
# Backwards compatibility for Django 1.7
def decode_results(values):
if get_version().split(".")[1] == "7":
# decode values in 1.7
return [dbsafe_decode(v) for v in values]
return values