From edd06fc02c3c05de1cbd9df50204c8a21e97cd93 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Thu, 29 Feb 2024 17:46:55 +0100 Subject: [PATCH 1/2] Allow seconds in cron string settings --- CHANGELOG.md | 9 ++++++ django_future_tasks/admin.py | 2 -- django_future_tasks/fields.py | 30 +++++++++++++++++++ .../populate_periodic_future_tasks.py | 2 +- ...07_alter_periodicfuturetask_cron_string.py | 23 ++++++++++++++ django_future_tasks/models.py | 14 ++------- requirements.txt | 1 - setup.py | 3 +- 8 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 django_future_tasks/fields.py create mode 100644 django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc4a87..cd5a736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support for seconds in cron-like tasks + +### Removed + +- Human readable cron name due to incompatibility of [cron-descriptor](https://github.com/Salamek/cron-descriptor) and [croniter](https://github.com/kiorky/croniter) for seconds +- `cron-descriptor` from requirements + ## [1.1.2] ### Fixed diff --git a/django_future_tasks/admin.py b/django_future_tasks/admin.py index ab02590..2494082 100644 --- a/django_future_tasks/admin.py +++ b/django_future_tasks/admin.py @@ -51,14 +51,12 @@ class FutureTaskInline(admin.TabularInline): @admin.register(PeriodicFutureTask) class PeriodicFutureTaskAdmin(admin.ModelAdmin): readonly_fields = [ - "cron_humnan_readable", "last_task_creation", "next_planned_execution", ] list_display = [ "periodic_task_id", "cron_string", - "cron_humnan_readable", "is_active", "type", "next_planned_execution", diff --git a/django_future_tasks/fields.py b/django_future_tasks/fields.py new file mode 100644 index 0000000..507b3b1 --- /dev/null +++ b/django_future_tasks/fields.py @@ -0,0 +1,30 @@ +import re + +from cronfield.models import CronField +from django.core.exceptions import ValidationError + + +def _validate_CRON_string(value): + """Validation routine for CRON string in TestingPlan""" + + if value.strip() != value: + raise ValidationError("Leading nor trailing spaces are allowed") + columns = value.split() + if columns != value.split(" "): + raise ValidationError("Use only a single space as a column separator") + + if len(columns) not in [5, 6]: + raise ValidationError("Entry has to consist of 5 or 6 columns") + + pattern = r"^(\*|\d+(-\d+)?(,\d+(-\d+)?)*)(/\d+)?$" + p = re.compile(pattern) + for i, c in enumerate(columns): + if not p.match(c): + raise ValidationError("Incorrect value {} in column {}".format(c, i + 1)) + + +class FuturetaskCronField(CronField): + def validate(self, value, model_instance): + super(CronField, self).validate(value, model_instance) + if self.editable: # Skip validation for non-editable fields. + _validate_CRON_string(value) diff --git a/django_future_tasks/management/commands/populate_periodic_future_tasks.py b/django_future_tasks/management/commands/populate_periodic_future_tasks.py index c3680c7..f0516f3 100644 --- a/django_future_tasks/management/commands/populate_periodic_future_tasks.py +++ b/django_future_tasks/management/commands/populate_periodic_future_tasks.py @@ -60,7 +60,7 @@ def handle_tick(self): p_task.is_active = False break - dt_format = "%Y-%m-%d %H:%M%z" + dt_format = "%Y-%m-%d %H:%M:%S%z" task_id = f"{p_task.periodic_task_id} ({dt.strftime(dt_format)})" FutureTask.objects.create( task_id=task_id, diff --git a/django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py b/django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py new file mode 100644 index 0000000..e65f171 --- /dev/null +++ b/django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.24 on 2024-02-29 15:34 + +from django.db import migrations + +import django_future_tasks.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("django_future_tasks", "0006_periodicfuturetask_end_time"), + ] + + operations = [ + migrations.AlterField( + model_name="periodicfuturetask", + name="cron_string", + field=django_future_tasks.fields.FuturetaskCronField( + default="* * * * *", + help_text="Minute Hour Day Month Weekday", + max_length=100, + ), + ), + ] diff --git a/django_future_tasks/models.py b/django_future_tasks/models.py index c1ba4fa..b467cee 100644 --- a/django_future_tasks/models.py +++ b/django_future_tasks/models.py @@ -1,8 +1,6 @@ import datetime import croniter -from cron_descriptor import CasingTypeEnum, ExpressionDescriptor -from cronfield.models import CronField from django.conf import settings from django.core.exceptions import ValidationError from django.db import models @@ -11,6 +9,8 @@ from django.utils.dateformat import format from django.utils.translation import gettext_lazy as _ +from .fields import FuturetaskCronField + class FutureTask(models.Model): FUTURE_TASK_STATUS_OPEN = "open" @@ -77,7 +77,7 @@ class PeriodicFutureTask(models.Model): blank=True, null=True, ) - cron_string = CronField() + cron_string = FuturetaskCronField() is_active = models.BooleanField(_("Active"), default=True) max_number_of_executions = models.IntegerField( _("Maximal number of executions"), null=True, blank=True @@ -115,14 +115,6 @@ def next_planned_execution(self): settings.DATETIME_FORMAT, ) - def cron_humnan_readable(self): - descriptor = ExpressionDescriptor( - expression=self.cron_string, - casing_type=CasingTypeEnum.Sentence, - use_24hour_time_format=False, - ) - return descriptor.get_description() - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__original_is_active = self.is_active diff --git a/requirements.txt b/requirements.txt index d193776..94bd4be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,4 @@ django>=3.2,<4 # Cron croniter>=1.4.1,<1.5 -cron-descriptor>=1.4.0, <1.5 django-cronfield>=0.2.0,<0.3 diff --git a/setup.py b/setup.py index 8eeca96..d7440aa 100644 --- a/setup.py +++ b/setup.py @@ -10,12 +10,11 @@ setup( name="django-future-tasks", - version=os.getenv("PACKAGE_VERSION", "1.1.2").replace("refs/tags/", ""), + version=os.getenv("PACKAGE_VERSION", "1.2.0").replace("refs/tags/", ""), packages=find_packages(), include_package_data=True, install_requires=[ "croniter>=1.4.1,<1.5", - "cron-descriptor>=1.4.0,<1.5", "django-cronfield>=0.2.0,<0.3", ], license="MIT License", From 5dcbe8e051ae2bfdc364068120450e308e824c44 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Tue, 23 Apr 2024 11:32:52 +0200 Subject: [PATCH 2/2] Remove coresponing single tasks from periodic future task chage view --- django_future_tasks/admin.py | 13 ------------- django_future_tasks/fields.py | 7 ++++++- .../0007_alter_periodicfuturetask_cron_string.py | 8 ++++---- django_future_tasks/models.py | 4 ++-- requirements.txt | 2 +- 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/django_future_tasks/admin.py b/django_future_tasks/admin.py index 2494082..7a6ddf7 100644 --- a/django_future_tasks/admin.py +++ b/django_future_tasks/admin.py @@ -36,18 +36,6 @@ class Media: css = {"all": ("django_future_tasks/cronfield.css",)} -class FutureTaskInline(admin.TabularInline): - verbose_name = "Corresponding single task" - verbose_name_plural = "Corresponding single tasks" - model = FutureTask - fields = ["task_id", "eta", "status"] - readonly_fields = ["task_id", "eta", "status"] - extra = 0 - classes = ["collapse"] - ordering = ["-eta"] - max_num = 100 - - @admin.register(PeriodicFutureTask) class PeriodicFutureTaskAdmin(admin.ModelAdmin): readonly_fields = [ @@ -62,6 +50,5 @@ class PeriodicFutureTaskAdmin(admin.ModelAdmin): "next_planned_execution", ] list_editable = ["cron_string", "is_active"] - inlines = [FutureTaskInline] list_filter = ["type", "is_active"] form = PeriodicFutureTaskAdminForm diff --git a/django_future_tasks/fields.py b/django_future_tasks/fields.py index 507b3b1..1eab4c6 100644 --- a/django_future_tasks/fields.py +++ b/django_future_tasks/fields.py @@ -23,8 +23,13 @@ def _validate_CRON_string(value): raise ValidationError("Incorrect value {} in column {}".format(c, i + 1)) -class FuturetaskCronField(CronField): +class FutureTaskCronField(CronField): def validate(self, value, model_instance): super(CronField, self).validate(value, model_instance) if self.editable: # Skip validation for non-editable fields. _validate_CRON_string(value) + + def __init__(self, *args, **kwargs): + kwargs["default"] = "* * * * * *" + kwargs["help_text"] = "Minute Hour Day Month Weekday Second" + super().__init__(*args, **kwargs) diff --git a/django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py b/django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py index e65f171..4f68827 100644 --- a/django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py +++ b/django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.24 on 2024-02-29 15:34 +# Generated by Django 4.2.14 on 2024-07-22 11:43 from django.db import migrations @@ -14,9 +14,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="periodicfuturetask", name="cron_string", - field=django_future_tasks.fields.FuturetaskCronField( - default="* * * * *", - help_text="Minute Hour Day Month Weekday", + field=django_future_tasks.fields.FutureTaskCronField( + default="* * * * * *", + help_text="Minute Hour Day Month Weekday Second", max_length=100, ), ), diff --git a/django_future_tasks/models.py b/django_future_tasks/models.py index b467cee..07ce091 100644 --- a/django_future_tasks/models.py +++ b/django_future_tasks/models.py @@ -9,7 +9,7 @@ from django.utils.dateformat import format from django.utils.translation import gettext_lazy as _ -from .fields import FuturetaskCronField +from .fields import FutureTaskCronField class FutureTask(models.Model): @@ -77,7 +77,7 @@ class PeriodicFutureTask(models.Model): blank=True, null=True, ) - cron_string = FuturetaskCronField() + cron_string = FutureTaskCronField() is_active = models.BooleanField(_("Active"), default=True) max_number_of_executions = models.IntegerField( _("Maximal number of executions"), null=True, blank=True diff --git a/requirements.txt b/requirements.txt index 94bd4be..9697fbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ coverage>=7.2.3,<7.3 pre-commit>=3.2.2,<3.3 # TestApp dependencies -django>=3.2,<4 +django>=4.2,<5 # Cron croniter>=1.4.1,<1.5