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..7a6ddf7 100644 --- a/django_future_tasks/admin.py +++ b/django_future_tasks/admin.py @@ -36,34 +36,19 @@ 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 = [ - "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", ] 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 new file mode 100644 index 0000000..1eab4c6 --- /dev/null +++ b/django_future_tasks/fields.py @@ -0,0 +1,35 @@ +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) + + 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/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..4f68827 --- /dev/null +++ b/django_future_tasks/migrations/0007_alter_periodicfuturetask_cron_string.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.14 on 2024-07-22 11:43 + +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 Second", + max_length=100, + ), + ), + ] diff --git a/django_future_tasks/models.py b/django_future_tasks/models.py index c1ba4fa..07ce091 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..9697fbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,8 @@ 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 -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",