Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SIANXKE-342: Deactivate periodic task after end time #7

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def handle_tick(self):
p_task.max_number_of_executions is not None
and self.number_of_corresponding_single_tasks(p_task)
>= p_task.max_number_of_executions
):
) or (p_task.end_time is not None and p_task.end_time < dt):
p_task.is_active = False
break

Expand Down
30 changes: 30 additions & 0 deletions django_future_tasks/migrations/0006_periodicfuturetask_end_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 3.2.22 on 2023-10-20 15:42

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("django_future_tasks", "0005_periodicfuturetask_max_number_of_executions"),
]

operations = [
migrations.AddField(
model_name="periodicfuturetask",
name="end_time",
field=models.DateTimeField(
blank=True, null=True, verbose_name="Executions until"
),
),
migrations.AddConstraint(
model_name="periodicfuturetask",
constraint=models.CheckConstraint(
check=models.Q(
("end_time__isnull", True),
("max_number_of_executions__isnull", True),
_connector="OR",
),
name="not_both_not_null",
),
),
]
61 changes: 50 additions & 11 deletions django_future_tasks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
)
from cronfield.models import CronField
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import JSONField
from django.db.models import JSONField, Q
from django.utils.dateformat import format
from django.utils.timezone import utc
from django.utils.translation import gettext_lazy as _


Expand Down Expand Up @@ -86,6 +88,7 @@ class PeriodicFutureTask(models.Model):
max_number_of_executions = models.IntegerField(
_("Maximal number of executions"), null=True, blank=True
)
end_time = models.DateTimeField(_("Executions until"), null=True, blank=True)
__original_is_active = None
last_task_creation = models.DateTimeField(
_("Last single task creation"),
Expand All @@ -94,18 +97,31 @@ class PeriodicFutureTask(models.Model):
)

def next_planned_execution(self):
if not self.is_active or (
self.max_number_of_executions is not None
and FutureTask.objects.filter(periodic_parent_task=self.pk).count()
>= self.max_number_of_executions
now = datetime.datetime.now()
next_planned_execution = utc.localize(
croniter.croniter(self.cron_string, now).get_next(datetime.datetime)
)
if (
not self.is_active
or (
self.max_number_of_executions is not None
and FutureTask.objects.filter(periodic_parent_task=self.pk).count()
>= self.max_number_of_executions
)
or (
self.end_time is not None
and self.end_time
< utc.localize(
croniter.croniter(self.cron_string, now).get_next(datetime.datetime)
)
)
):
return None
else:
now = datetime.datetime.now()
return format(
croniter.croniter(self.cron_string, now).get_next(datetime.datetime),
settings.DATETIME_FORMAT,
)

return format(
next_planned_execution,
settings.DATETIME_FORMAT,
)

def cron_humnan_readable(self):
descriptor = ExpressionDescriptor(
Expand All @@ -125,8 +141,31 @@ def save(
if self.is_active and not self.__original_is_active:
self.last_task_creation = datetime.datetime.now()

self.clean()
super().save()
self.__original_is_active = self.is_active

def __str__(self):
return f"{self.periodic_task_id} ({self.cron_string})"

def clean(self):
if self.end_time is not None and self.max_number_of_executions is not None:
raise ValidationError(
{
"end_time": _(
"Cannot be set together with maximal number of executions, at least one must be empty."
),
"max_number_of_executions": _(
"Cannot be set together with execution end time, at least one must be empty."
),
}
)

class Meta:
constraints = [
models.CheckConstraint(
check=Q(end_time__isnull=True)
| Q(max_number_of_executions__isnull=True),
name="not_both_not_null",
)
]
42 changes: 40 additions & 2 deletions tests/testapp/tests/test_periodic_future_tasks.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import time
from datetime import timedelta

from django.core.exceptions import ValidationError
from django.test import TransactionTestCase
from django.utils import timezone

from django_future_tasks.models import FutureTask, PeriodicFutureTask
from tests.core import settings
from tests.testapp.mixins import PopulatePeriodicTaskCommandMixin

SLEEP_TIME = 1.5
SLEEP_TIME = 1.8


class TestPeriodicFutureTasks(PopulatePeriodicTaskCommandMixin, TransactionTestCase):
def setUp(self):
super().setUp()

self.original_last_task_creation = timezone.now() - timedelta(hours=2)
now = timezone.now()
self.original_last_task_creation = now - timedelta(hours=2)

self.task_active = PeriodicFutureTask.objects.create(
periodic_task_id="periodic task",
Expand Down Expand Up @@ -45,6 +47,24 @@ def setUp(self):
p_task.last_task_creation = self.original_last_task_creation
p_task.save()

end_time = now - timedelta(hours=1)
self.task_with_end_time = PeriodicFutureTask.objects.create(
periodic_task_id="periodic task with end time",
type=settings.FUTURE_TASK_TYPE_ONE,
cron_string="42 * * * *",
end_time=end_time,
)

p_task = PeriodicFutureTask.objects.get(pk=self.task_with_end_time.pk)
p_task.last_task_creation = self.original_last_task_creation
p_task.save()

self.task_for_validation_test = PeriodicFutureTask.objects.create(
periodic_task_id="periodic task for validation test",
type=settings.FUTURE_TASK_TYPE_ONE,
cron_string="42 * * * *",
)

def test_periodic_future_task_populate_active_task(self):
p_task = PeriodicFutureTask.objects.get(pk=self.task_active.pk)

Expand Down Expand Up @@ -96,3 +116,21 @@ def test_periodic_future_task_max_one_exection(self):
FutureTask.objects.filter(periodic_parent_task_id=p_task.pk).count(), 1
)
self.assertFalse(p_task.is_active)

def test_periodic_future_task_end_time(self):
p_task = PeriodicFutureTask.objects.get(pk=self.task_with_end_time.pk)

# Make sure that task population has been processed.
time.sleep(SLEEP_TIME)

p_task.refresh_from_db()
self.assertEqual(
FutureTask.objects.filter(periodic_parent_task_id=p_task.pk).count(), 1
)
self.assertFalse(p_task.is_active)

def test_end_time_and_max_number_of_executions_validation(self):
p_task = PeriodicFutureTask.objects.get(pk=self.task_for_validation_test.pk)
p_task.max_number_of_executions = 42
p_task.end_time = timezone.now()
self.assertRaises(ValidationError, p_task.save)
Loading