diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 99ed48344cd7bdea19fb27982552b07431eb12cc..234cdb8de9e7b9b8519a217c1a3f19118f152856 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,11 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- +Added +~~~~~ + +* Periodic tasks can now have a default schedule, which is automatically created + Fixed ~~~~~ diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py index ab264c53a91edac303e8cb574e7d7a56fdca00ac..77e4b2a6327d9b88254b160375b1ce85be73ed68 100644 --- a/aleksis/core/apps.py +++ b/aleksis/core/apps.py @@ -17,7 +17,12 @@ from .registries import ( site_preferences_registry, ) from .util.apps import AppConfig -from .util.core_helpers import get_or_create_favicon, get_site_preferences, has_person +from .util.core_helpers import ( + create_default_celery_schedule, + get_or_create_favicon, + get_site_preferences, + has_person, +) from .util.sass_helpers import clean_scss @@ -140,6 +145,9 @@ class CoreConfig(AppConfig): for name, default in settings.DEFAULT_FAVICON_PATHS.items(): get_or_create_favicon(name, default, is_favicon=name == "favicon") + # Create default periodic tasks + create_default_celery_schedule() + def user_logged_in( self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs ) -> None: diff --git a/aleksis/core/tasks.py b/aleksis/core/tasks.py index 731c4356c20bba915bd5ad0811307c3877810631..13eb76444b77e84ca7509958f02517c98b678296 100644 --- a/aleksis/core/tasks.py +++ b/aleksis/core/tasks.py @@ -1,3 +1,5 @@ +from datetime import timedelta + from django.conf import settings from django.core import management @@ -15,7 +17,7 @@ def send_notification(notification: int, resend: bool = False) -> None: _send_notification(notification, resend) -@app.task +@app.task(run_every=timedelta(days=1)) def backup_data() -> None: """Backup database and media using django-dbbackup.""" # Assemble command-line options for dbbackup management command diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index 3b3c934a85341e75092b6ef1e8df77bdc838687e..148d33594d2cc0a2619a1c1a64c3b8fa69dad79c 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -332,3 +332,60 @@ def process_custom_context_processors(context_processors: list) -> Dict[str, Any for processor in processors: context.update(processor(None)) return context + + +def create_default_celery_schedule(): + """Create default periodic tasks in database for tasks that have a schedule defined.""" + from celery import current_app + from celery.schedules import BaseSchedule, crontab, schedule, solar + from django_celery_beat.clockedschedule import clocked + from django_celery_beat.models import ( + ClockedSchedule, + CrontabSchedule, + IntervalSchedule, + PeriodicTask, + SolarSchedule, + ) + + defined_periodic_tasks = PeriodicTask.objects.values_list("task", flat=True).all() + + for name, task in current_app.tasks.items(): + if name in defined_periodic_tasks: + # Task is already known in database, skip + continue + + run_every = getattr(task, "run_every", None) + if not run_every: + # Task has no default schedule, skip + continue + + if isinstance(run_every, (float, int, timedelta)): + # Schedule is defined as a raw seconds value or timedelta, convert to schedule class + run_every = schedule(run_every) + elif not isinstance(run_every, BaseSchedule): + raise ValueError(f"Task {name} has an invalid schedule defined.") + + # Find matching django-celery-beat schedule model + if isinstance(run_every, clocked): + Schedule = ClockedSchedule + attr = "clocked" + elif isinstance(run_every, crontab): + Schedule = CrontabSchedule + attr = "crontab" + elif isinstance(run_every, schedule): + Schedule = IntervalSchedule + attr = "interval" + elif isinstance(run_every, solar): + Schedule = SolarSchedule + attr = "solar" + else: + raise ValueError(f"Task {name} has an unknown schedule class defined.") + + # Get or create schedule in database + db_schedule = Schedule.from_schedule(run_every) + db_schedule.save() + + # Create periodic task + PeriodicTask.objects.create( + name=f"{name} (default schedule)", task=name, **{attr: db_schedule} + )