Skip to content

Fix last task not populated #24

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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,17 @@ def morning_routine():
return f"15 seconds passed !"
```

Schedule's cron can also be set to `manual` in which case it never runs, but can only be triggered manually from the admin :
```python
from django_toosimple_q.decorators import register_task, schedule_task

# A schedule that only runs when manually triggered
@schedule_task(cron="manual")
@register_task()
def for_special_occasions():
return f"this was triggered manually !"
```

### Management comment

Besides standard django management commands arguments, the management command supports following arguments.
Expand Down Expand Up @@ -361,6 +372,7 @@ pre-commit install
- feature: added workerstatus to the admin, allowing to monitor workers
- feature: queue tasks for later (`mytask.queue(due=now()+timedelta(hours=2))`)
- feature: assign queues to schedules (`@schedule_task(queue="schedules")`)
- feature: allow manual schedules that are only run manually through the admin (`@schedule_task(cron="manual")`)
- refactor: removed non-execution related data from the database (clarifying the fact tha the source of truth is the registry)
- refactor: better support for concurrent workers
- refactor: better names for models and decorators
Expand Down
16 changes: 8 additions & 8 deletions django_toosimple_q/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from datetime import datetime

from croniter import croniter
from django.contrib import admin
from django.contrib.messages.constants import SUCCESS
from django.template.defaultfilters import truncatechars
Expand Down Expand Up @@ -191,14 +188,17 @@ def last_due_(self, obj):

@admin.display()
def next_due_(self, obj):
if obj.next_dues:
next_due = obj.next_dues[0]
if len(obj.past_dues) >= 1:
next_due = obj.past_dues[0]
else:
next_due = croniter(obj.schedule.cron, timezone.now()).get_next(datetime)
next_due = obj.upcomming_due

if next_due is None:
return "never"

formatted_next_due = short_naturaltime(next_due)
if len(obj.next_dues) > 1:
formatted_next_due += mark_safe(f" [×{len(obj.next_dues)}]")
if len(obj.past_dues) > 1:
formatted_next_due += mark_safe(f" [×{len(obj.past_dues)}]")
if next_due < timezone.now():
return mark_safe(f"<span style='color: red'>{formatted_next_due}</span>")
return formatted_next_due
Expand Down
23 changes: 18 additions & 5 deletions django_toosimple_q/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from croniter import croniter, croniter_range
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -202,7 +203,11 @@ def icon(self):
return ScheduleExec.States.icon(self.state)

@cached_property
def next_dues(self):
def past_dues(self):
if self.schedule.cron == "manual":
# A manual schedule is never due
return []

if self.last_due is None:
# If the schedule has no last due date (probaby create with run_on_creation), we run it
return [croniter(self.schedule.cron, now()).get_prev(datetime)]
Expand All @@ -217,14 +222,22 @@ def next_dues(self):

return dues

@cached_property
def upcomming_due(self):
if self.schedule.cron == "manual":
# A manual schedule is never due
return None

return croniter(self.schedule.cron, timezone.now()).get_next(datetime)

def execute(self):
did_something = False

if self.next_dues:
logger.info(f"{self} is due ({len(self.next_dues)} occurences)")
self.schedule.execute(self.next_dues)
if self.past_dues:
logger.info(f"{self} is due ({len(self.past_dues)} occurences)")
self.last_task = self.schedule.execute(self.past_dues)
did_something = True
self.last_due = self.next_dues[-1]
self.last_due = self.past_dues[-1]

self.state = ScheduleExec.States.ACTIVE
self.save()
Expand Down
4 changes: 3 additions & 1 deletion django_toosimple_q/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ def execute(self, dues: List[Optional[datetime]]):
"""Enqueues the related tasks at the given due dates"""

# We enqueue the due tasks
last_task = None
for due in dues:
logger.debug(f"{self} is due at {due}")

dt_kwarg = {}
if self.datetime_kwarg:
dt_kwarg = {self.datetime_kwarg: due}

tasks_registry[self.name].enqueue(
last_task = tasks_registry[self.name].enqueue(
*self.args, due=due, **dt_kwarg, **self.kwargs
)
return last_task

def __str__(self):
return f"Schedule {self.name}"
2 changes: 1 addition & 1 deletion django_toosimple_q/tests/demo/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def long_running():
return text


@schedule_task(cron="0 */30 * * * *", run_on_creation=True, queue="demo")
@schedule_task(cron="manual", queue="demo")
@register_task(name="cleanup", queue="demo", priority=-5)
def cleanup():
old_tasks_execs = TaskExec.objects.filter(
Expand Down
28 changes: 28 additions & 0 deletions django_toosimple_q/tests/tests_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,34 @@ def a():
)
self.assertEqual(response.status_code, 200)

def test_manual_schedule_admin(self):
"""Check that manual schedule admin action work"""

@schedule_task(cron="manual")
@register_task(name="a")
def a():
return 2

self.assertSchedule("a", None)
management.call_command("worker", "--until_done")
self.assertQueue(0)

data = {
"action": "action_force_run",
"_selected_action": ScheduleExec.objects.get(name="a").pk,
}
response = self.client.post(
"/admin/toosimpleq/scheduleexec/", data, follow=True
)
self.assertEqual(response.status_code, 200)

self.assertQueue(1, state=TaskExec.States.QUEUED)

management.call_command("worker", "--until_done")

self.assertQueue(1, state=TaskExec.States.SUCCEEDED)
self.assertSchedule("a", ScheduleExec.States.ACTIVE)

def test_schedule_admin_force_action(self):
"""Check if he force execute schedule action works"""

Expand Down
19 changes: 19 additions & 0 deletions django_toosimple_q/tests/tests_schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,25 @@ def d(scheduled_on):
],
)

@freeze_time("2020-01-01", as_kwarg="frozen_datetime")
def test_manual_schedule(self, frozen_datetime):
"""Testing manual schedules"""

@schedule_task(cron="manual", datetime_kwarg="scheduled_on")
@register_task(name="normal")
def a(scheduled_on):
return f"{scheduled_on:%Y-%m-%d %H:%M}"

self.assertEquals(len(schedules_registry), 1)
self.assertEquals(ScheduleExec.objects.count(), 0)
self.assertQueue(0)

# a "manual" schedule never runs
management.call_command("worker", "--until_done")
frozen_datetime.move_to("2050-01-01")
management.call_command("worker", "--until_done")
self.assertQueue(0)

@freeze_time("2020-01-01", as_kwarg="frozen_datetime")
def test_invalid_schedule(self, frozen_datetime):
"""Testing invalid schedules"""
Expand Down