Skip to content

Commit

Permalink
Support Periodic Tasks with a start date and one-off tasks (#78)
Browse files Browse the repository at this point in the history
* support periodic tasks with a start date and also one-off tasks. Change the list display in PeriodicTaskAdmin to show interval, start_date, one_off additionally - easier/quicker identifcation of task properties in admin list

* add migration

* rename start_date to start_time and add tests

* fix unit test

* update migrations and fix tests
  • Loading branch information
oubeichen authored and auvipy committed May 23, 2018
1 parent 3b3b142 commit 27f73a5
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 2 deletions.
5 changes: 3 additions & 2 deletions django_celery_beat/admin.py
Expand Up @@ -116,15 +116,16 @@ class PeriodicTaskAdmin(admin.ModelAdmin):
form = PeriodicTaskForm
model = PeriodicTask
celery_app = current_app
list_display = ('__str__', 'enabled')
list_display = ('__str__', 'enabled', 'interval', 'start_time', 'one_off')
actions = ('enable_tasks', 'disable_tasks', 'run_tasks')
fieldsets = (
(None, {
'fields': ('name', 'regtask', 'task', 'enabled', 'description',),
'classes': ('extrapretty', 'wide'),
}),
('Schedule', {
'fields': ('interval', 'crontab', 'solar'),
'fields': ('interval', 'crontab', 'solar',
'start_time', 'one_off'),
'classes': ('extrapretty', 'wide', ),
}),
('Arguments', {
Expand Down
28 changes: 28 additions & 0 deletions django_celery_beat/migrations/0007_auto_20180521_0826.py
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-05-21 08:26
from __future__ import absolute_import, unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('django_celery_beat', '0006_auto_20180322_0932'),
]

operations = [
migrations.AddField(
model_name='periodictask',
name='one_off',
field=models.BooleanField(default=False,
verbose_name='one-off task'),
),
migrations.AddField(
model_name='periodictask',
name='start_time',
field=models.DateTimeField(blank=True,
null=True,
verbose_name='start_time'),
),
]
6 changes: 6 additions & 0 deletions django_celery_beat/models.py
Expand Up @@ -276,6 +276,12 @@ class PeriodicTask(models.Model):
expires = models.DateTimeField(
_('expires'), blank=True, null=True,
)
one_off = models.BooleanField(
_('one-off task'), default=False,
)
start_time = models.DateTimeField(
_('start_time'), blank=True, null=True,
)
enabled = models.BooleanField(
_('enabled'), default=True,
)
Expand Down
17 changes: 17 additions & 0 deletions django_celery_beat/schedulers.py
Expand Up @@ -11,6 +11,7 @@
from celery.five import values, items
from celery.utils.encoding import safe_str, safe_repr
from celery.utils.log import get_logger
from celery.utils.time import maybe_make_aware
from kombu.utils.json import dumps, loads

from django.db import transaction, close_old_connections
Expand Down Expand Up @@ -96,6 +97,22 @@ def _disable(self, model):
def is_due(self):
if not self.model.enabled:
return False, 5.0 # 5 second delay for re-enable.

# START DATE: only run after the `start_time`, if one exists.
if self.model.start_time is not None:
if maybe_make_aware(self._default_now()) < self.model.start_time:
# The datetime is before the start date - don't run.
_, delay = self.schedule.is_due(self.last_run_at)
return False, delay # use original delay for re-check

# ONE OFF TASK: Disable one off tasks after they've ran once
if self.model.one_off and self.model.enabled \
and self.model.total_run_count > 0:
self.model.enabled = False
self.model.total_run_count = 0 # Reset
self.model.save()
return False, None # Don't recheck

return self.schedule.is_due(self.last_run_at)

def _default_now(self):
Expand Down
43 changes: 43 additions & 0 deletions t/unit/test_schedulers.py
Expand Up @@ -131,6 +131,49 @@ def test_entry(self):
assert e3.last_run_at > e2.last_run_at
assert e3.total_run_count == 1

def test_task_with_start_time(self):
interval = 10
right_now = self.app.now()
one_interval_ago = right_now - timedelta(seconds=interval)
m = self.create_model_interval(schedule(timedelta(seconds=interval)),
start_time=right_now,
last_run_at=one_interval_ago)
e = self.Entry(m, app=self.app)
isdue, delay = e.is_due()
assert isdue
assert delay == interval

tomorrow = right_now + timedelta(days=1)
m2 = self.create_model_interval(schedule(timedelta(seconds=interval)),
start_time=tomorrow,
last_run_at=one_interval_ago)
e2 = self.Entry(m2, app=self.app)
isdue, delay = e2.is_due()
assert not isdue
assert delay == interval

def test_one_off_task(self):
interval = 10
right_now = self.app.now()
one_interval_ago = right_now - timedelta(seconds=interval)
m = self.create_model_interval(schedule(timedelta(seconds=interval)),
one_off=True,
last_run_at=one_interval_ago,
total_run_count=0)
e = self.Entry(m, app=self.app)
isdue, delay = e.is_due()
assert isdue
assert delay == interval

m2 = self.create_model_interval(schedule(timedelta(seconds=interval)),
one_off=True,
last_run_at=one_interval_ago,
total_run_count=1)
e2 = self.Entry(m2, app=self.app)
isdue, delay = e2.is_due()
assert not isdue
assert delay is None


@pytest.mark.django_db()
class test_DatabaseSchedulerFromAppConf(SchedulerCase):
Expand Down

0 comments on commit 27f73a5

Please sign in to comment.