From 0143454e4a5804f8d93226de82196130c388e894 Mon Sep 17 00:00:00 2001 From: Roberta Takenaka Date: Thu, 1 Sep 2022 18:56:04 -0300 Subject: [PATCH 1/2] Acrescenta django_celery_beat --- django_celery_beat/__init__.py | 35 + django_celery_beat/admin.py | 253 +++++++ django_celery_beat/apps.py | 14 + django_celery_beat/button_helper.py | 36 + django_celery_beat/clockedschedule.py | 41 ++ django_celery_beat/forms.py | 97 +++ .../locale/es/LC_MESSAGES/django.po | 528 ++++++++++++++ .../locale/fr/LC_MESSAGES/django.po | 522 ++++++++++++++ .../locale/ru/LC_MESSAGES/django.po | 451 ++++++++++++ .../locale/zh_hans/LC_MESSAGES/django.po | 486 +++++++++++++ django_celery_beat/managers.py | 30 + django_celery_beat/migrations/0001_initial.py | 129 ++++ .../migrations/0002_auto_20161118_0346.py | 49 ++ .../migrations/0003_auto_20161209_0049.py | 23 + .../migrations/0004_auto_20170221_0000.py | 19 + .../0005_add_solarschedule_events_choices.py | 28 + .../migrations/0006_auto_20180210_1226.py | 30 + .../migrations/0006_auto_20180322_0932.py | 51 ++ .../migrations/0006_periodictask_priority.py | 28 + .../migrations/0007_auto_20180521_0826.py | 25 + .../migrations/0008_auto_20180914_1922.py | 57 ++ .../migrations/0009_periodictask_headers.py | 22 + .../migrations/0010_auto_20190429_0326.py | 174 +++++ .../migrations/0011_auto_20190508_0153.py | 32 + .../0012_periodictask_expire_seconds.py | 18 + .../migrations/0013_auto_20200609_0727.py | 20 + .../0014_remove_clockedschedule_enabled.py | 17 + .../0015_edit_solarschedule_events_choices.py | 18 + django_celery_beat/migrations/__init__.py | 0 django_celery_beat/models.py | 670 ++++++++++++++++++ django_celery_beat/schedulers.py | 370 ++++++++++ .../templates/admin/djcelery/change_list.html | 20 + django_celery_beat/tzcrontab.py | 76 ++ django_celery_beat/urls.py | 8 + django_celery_beat/utils.py | 48 ++ django_celery_beat/validators.py | 106 +++ django_celery_beat/views.py | 30 + django_celery_beat/wagtail_hooks.py | 187 +++++ 38 files changed, 4748 insertions(+) create mode 100644 django_celery_beat/__init__.py create mode 100644 django_celery_beat/admin.py create mode 100644 django_celery_beat/apps.py create mode 100644 django_celery_beat/button_helper.py create mode 100644 django_celery_beat/clockedschedule.py create mode 100644 django_celery_beat/forms.py create mode 100644 django_celery_beat/locale/es/LC_MESSAGES/django.po create mode 100644 django_celery_beat/locale/fr/LC_MESSAGES/django.po create mode 100644 django_celery_beat/locale/ru/LC_MESSAGES/django.po create mode 100644 django_celery_beat/locale/zh_hans/LC_MESSAGES/django.po create mode 100644 django_celery_beat/managers.py create mode 100644 django_celery_beat/migrations/0001_initial.py create mode 100644 django_celery_beat/migrations/0002_auto_20161118_0346.py create mode 100644 django_celery_beat/migrations/0003_auto_20161209_0049.py create mode 100644 django_celery_beat/migrations/0004_auto_20170221_0000.py create mode 100644 django_celery_beat/migrations/0005_add_solarschedule_events_choices.py create mode 100644 django_celery_beat/migrations/0006_auto_20180210_1226.py create mode 100644 django_celery_beat/migrations/0006_auto_20180322_0932.py create mode 100644 django_celery_beat/migrations/0006_periodictask_priority.py create mode 100644 django_celery_beat/migrations/0007_auto_20180521_0826.py create mode 100644 django_celery_beat/migrations/0008_auto_20180914_1922.py create mode 100644 django_celery_beat/migrations/0009_periodictask_headers.py create mode 100644 django_celery_beat/migrations/0010_auto_20190429_0326.py create mode 100644 django_celery_beat/migrations/0011_auto_20190508_0153.py create mode 100644 django_celery_beat/migrations/0012_periodictask_expire_seconds.py create mode 100644 django_celery_beat/migrations/0013_auto_20200609_0727.py create mode 100644 django_celery_beat/migrations/0014_remove_clockedschedule_enabled.py create mode 100644 django_celery_beat/migrations/0015_edit_solarschedule_events_choices.py create mode 100644 django_celery_beat/migrations/__init__.py create mode 100644 django_celery_beat/models.py create mode 100644 django_celery_beat/schedulers.py create mode 100644 django_celery_beat/templates/admin/djcelery/change_list.html create mode 100644 django_celery_beat/tzcrontab.py create mode 100644 django_celery_beat/urls.py create mode 100644 django_celery_beat/utils.py create mode 100644 django_celery_beat/validators.py create mode 100644 django_celery_beat/views.py create mode 100644 django_celery_beat/wagtail_hooks.py diff --git a/django_celery_beat/__init__.py b/django_celery_beat/__init__.py new file mode 100644 index 0000000..f8b23d1 --- /dev/null +++ b/django_celery_beat/__init__.py @@ -0,0 +1,35 @@ +"""Database-backed Periodic Tasks.""" +# :copyright: (c) 2016, Ask Solem. +# All rights reserved. +# :license: BSD (3 Clause), see LICENSE for more details. +import re + +from collections import namedtuple + +import django + +__version__ = '2.2.1' +__author__ = 'Asif Saif Uddin, Ask Solem' +__contact__ = 'auvipy@gmail.com, ask@celeryproject.org' +__homepage__ = 'https://github.com/celery/django-celery-beat' +__docformat__ = 'restructuredtext' + +# -eof meta- + +version_info_t = namedtuple('version_info_t', ( + 'major', 'minor', 'micro', 'releaselevel', 'serial', +)) + +# bumpversion can only search for {current_version} +# so we have to parse the version here. +_temp = re.match( + r'(\d+)\.(\d+).(\d+)(.+)?', __version__).groups() +VERSION = version_info = version_info_t( + int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or '', '') +del(_temp) +del(re) + +__all__ = [] + +if django.VERSION < (3, 2): + default_app_config = 'django_celery_beat.apps.BeatConfig' diff --git a/django_celery_beat/admin.py b/django_celery_beat/admin.py new file mode 100644 index 0000000..3d6d89e --- /dev/null +++ b/django_celery_beat/admin.py @@ -0,0 +1,253 @@ +"""Periodic Task Admin interface.""" +from django import forms +from django.conf import settings +from django.contrib import admin, messages +from django.db.models import When, Value, Case +from django.forms.widgets import Select +from django.template.defaultfilters import pluralize +from django.utils.translation import gettext_lazy as _ + +from celery import current_app +from celery.utils import cached_property +from kombu.utils.json import loads + +from .models import ( + PeriodicTask, PeriodicTasks, + IntervalSchedule, CrontabSchedule, + SolarSchedule, ClockedSchedule +) +from .utils import is_database_scheduler + + +class TaskSelectWidget(Select): + """Widget that lets you choose between task names.""" + + celery_app = current_app + _choices = None + + def tasks_as_choices(self): + _ = self._modules # noqa + tasks = list(sorted(name for name in self.celery_app.tasks + if not name.startswith('celery.'))) + return (('', ''), ) + tuple(zip(tasks, tasks)) + + @property + def choices(self): + if self._choices is None: + self._choices = self.tasks_as_choices() + return self._choices + + @choices.setter + def choices(self, _): + # ChoiceField.__init__ sets ``self.choices = choices`` + # which would override ours. + pass + + @cached_property + def _modules(self): + self.celery_app.loader.import_default_modules() + + +class TaskChoiceField(forms.ChoiceField): + """Field that lets you choose between task names.""" + + widget = TaskSelectWidget + + def valid_value(self, value): + return True + + +class PeriodicTaskForm(forms.ModelForm): + """Form that lets you create and modify periodic tasks.""" + + regtask = TaskChoiceField( + label=_('Task (registered)'), + required=False, + ) + task = forms.CharField( + label=_('Task (custom)'), + required=False, + max_length=200, + ) + + class Meta: + """Form metadata.""" + + model = PeriodicTask + exclude = () + + def clean(self): + data = super().clean() + regtask = data.get('regtask') + if regtask: + data['task'] = regtask + if not data['task']: + exc = forms.ValidationError(_('Need name of task')) + self._errors['task'] = self.error_class(exc.messages) + raise exc + + if data.get('expire_seconds') is not None and data.get('expires'): + raise forms.ValidationError( + _('Only one can be set, in expires and expire_seconds') + ) + return data + + def _clean_json(self, field): + value = self.cleaned_data[field] + try: + loads(value) + except ValueError as exc: + raise forms.ValidationError( + _('Unable to parse JSON: %s') % exc, + ) + return value + + def clean_args(self): + return self._clean_json('args') + + def clean_kwargs(self): + return self._clean_json('kwargs') + + +class PeriodicTaskAdmin(admin.ModelAdmin): + """Admin-interface for periodic tasks.""" + + form = PeriodicTaskForm + model = PeriodicTask + celery_app = current_app + date_hierarchy = 'start_time' + list_display = ('__str__', 'enabled', 'interval', 'start_time', + 'last_run_at', 'one_off') + list_filter = ['enabled', 'one_off', 'task', 'start_time', 'last_run_at'] + actions = ('enable_tasks', 'disable_tasks', 'toggle_tasks', 'run_tasks') + search_fields = ('name',) + fieldsets = ( + (None, { + 'fields': ('name', 'regtask', 'task', 'enabled', 'description',), + 'classes': ('extrapretty', 'wide'), + }), + ('Schedule', { + 'fields': ('interval', 'crontab', 'solar', 'clocked', + 'start_time', 'last_run_at', 'one_off'), + 'classes': ('extrapretty', 'wide'), + }), + ('Arguments', { + 'fields': ('args', 'kwargs'), + 'classes': ('extrapretty', 'wide', 'collapse', 'in'), + }), + ('Execution Options', { + 'fields': ('expires', 'expire_seconds', 'queue', 'exchange', + 'routing_key', 'priority', 'headers'), + 'classes': ('extrapretty', 'wide', 'collapse', 'in'), + }), + ) + readonly_fields = ( + 'last_run_at', + ) + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + scheduler = getattr(settings, 'CELERY_BEAT_SCHEDULER', None) + extra_context['wrong_scheduler'] = not is_database_scheduler(scheduler) + return super(PeriodicTaskAdmin, self).changelist_view( + request, extra_context) + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related('interval', 'crontab', 'solar', 'clocked') + + def _message_user_about_update(self, request, rows_updated, verb): + """Send message about action to user. + + `verb` should shortly describe what have changed (e.g. 'enabled'). + + """ + self.message_user( + request, + _('{0} task{1} {2} successfully {3}').format( + rows_updated, + pluralize(rows_updated), + pluralize(rows_updated, _('was,were')), + verb, + ), + ) + + def enable_tasks(self, request, queryset): + rows_updated = queryset.update(enabled=True) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, 'enabled') + enable_tasks.short_description = _('Enable selected tasks') + + def disable_tasks(self, request, queryset): + rows_updated = queryset.update(enabled=False) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, 'disabled') + disable_tasks.short_description = _('Disable selected tasks') + + def _toggle_tasks_activity(self, queryset): + return queryset.update(enabled=Case( + When(enabled=True, then=Value(False)), + default=Value(True), + )) + + def toggle_tasks(self, request, queryset): + rows_updated = self._toggle_tasks_activity(queryset) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, 'toggled') + toggle_tasks.short_description = _('Toggle activity of selected tasks') + + def run_tasks(self, request, queryset): + self.celery_app.loader.import_default_modules() + tasks = [(self.celery_app.tasks.get(task.task), + loads(task.args), + loads(task.kwargs), + task.queue) + for task in queryset] + + if any(t[0] is None for t in tasks): + for i, t in enumerate(tasks): + if t[0] is None: + break + + # variable "i" will be set because list "tasks" is not empty + not_found_task_name = queryset[i].task + + self.message_user( + request, + _('task "{0}" not found'.format(not_found_task_name)), + level=messages.ERROR, + ) + return + + task_ids = [task.apply_async(args=args, kwargs=kwargs, queue=queue) + if queue and len(queue) + else task.apply_async(args=args, kwargs=kwargs) + for task, args, kwargs, queue in tasks] + tasks_run = len(task_ids) + self.message_user( + request, + _('{0} task{1} {2} successfully run').format( + tasks_run, + pluralize(tasks_run), + pluralize(tasks_run, _('was,were')), + ), + ) + run_tasks.short_description = _('Run selected tasks') + + +class ClockedScheduleAdmin(admin.ModelAdmin): + """Admin-interface for clocked schedules.""" + + fields = ( + 'clocked_time', + ) + list_display = ( + 'clocked_time', + ) + + +admin.site.register(IntervalSchedule) +admin.site.register(CrontabSchedule) +admin.site.register(SolarSchedule) +admin.site.register(ClockedSchedule, ClockedScheduleAdmin) +admin.site.register(PeriodicTask, PeriodicTaskAdmin) diff --git a/django_celery_beat/apps.py b/django_celery_beat/apps.py new file mode 100644 index 0000000..0a783d2 --- /dev/null +++ b/django_celery_beat/apps.py @@ -0,0 +1,14 @@ +"""Django Application configuration.""" +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + +__all__ = ['BeatConfig'] + + +class BeatConfig(AppConfig): + """Default configuration for django_celery_beat app.""" + + name = 'django_celery_beat' + label = 'django_celery_beat' + verbose_name = _('Periodic Tasks') + default_auto_field = 'django.db.models.AutoField' diff --git a/django_celery_beat/button_helper.py b/django_celery_beat/button_helper.py new file mode 100644 index 0000000..7369024 --- /dev/null +++ b/django_celery_beat/button_helper.py @@ -0,0 +1,36 @@ +from django.utils.translation import gettext as _ +from wagtail.contrib.modeladmin.helpers import ButtonHelper + +from django.urls import reverse + + +class PeriodicTaskHelper(ButtonHelper): + + # Define classes for our button, here we can set an icon for example + run_button_classnames = ["button-small", "icon", ] + + def run_button(self, obj): + # Define a label for our button + text = _("Run") + return { + "url": reverse("django_celery_beat:task_run") + "?task_id=%s" % str(obj.id), + "label": text, + "classname": self.finalise_classname(self.run_button_classnames), + "title": text, + } + + + def get_buttons_for_obj( + self, obj, exclude=None, classnames_add=None, classnames_exclude=None + ): + """ + This function is used to gather all available buttons. + We append our custom button to the btns list. + """ + btns = super().get_buttons_for_obj( + obj, exclude, classnames_add, classnames_exclude + ) + if "run" not in (exclude or []): + btns.append(self.run_button(obj)) + + return btns diff --git a/django_celery_beat/clockedschedule.py b/django_celery_beat/clockedschedule.py new file mode 100644 index 0000000..94f9885 --- /dev/null +++ b/django_celery_beat/clockedschedule.py @@ -0,0 +1,41 @@ +"""Clocked schedule Implementation.""" + +from celery import schedules +from celery.utils.time import maybe_make_aware +from .utils import NEVER_CHECK_TIMEOUT + + +class clocked(schedules.BaseSchedule): + """clocked schedule. + + Depends on PeriodicTask one_off=True + """ + + def __init__(self, clocked_time, nowfun=None, app=None): + """Initialize clocked.""" + self.clocked_time = maybe_make_aware(clocked_time) + super().__init__(nowfun=nowfun, app=app) + + def remaining_estimate(self, last_run_at): + return self.clocked_time - self.now() + + def is_due(self, last_run_at): + rem_delta = self.remaining_estimate(None) + remaining_s = max(rem_delta.total_seconds(), 0) + if remaining_s == 0: + return schedules.schedstate(is_due=True, next=NEVER_CHECK_TIMEOUT) + return schedules.schedstate(is_due=False, next=remaining_s) + + def __repr__(self): + return ''.format(self.clocked_time) + + def __eq__(self, other): + if isinstance(other, clocked): + return self.clocked_time == other.clocked_time + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __reduce__(self): + return self.__class__, (self.clocked_time, self.nowfun) diff --git a/django_celery_beat/forms.py b/django_celery_beat/forms.py new file mode 100644 index 0000000..144143b --- /dev/null +++ b/django_celery_beat/forms.py @@ -0,0 +1,97 @@ + +from celery import current_app +from celery.utils import cached_property + +from django import forms +from django.forms.widgets import Select +from django.utils.translation import gettext_lazy as _ +from kombu.utils.json import loads +from wagtail.admin.forms import WagtailAdminModelForm + + +class TaskSelectWidget(Select): + """Widget that lets you choose between task names.""" + + celery_app = current_app + _choices = None + + def tasks_as_choices(self): + _ = self._modules # noqa + tasks = list(sorted(name for name in self.celery_app.tasks + if not name.startswith('celery.'))) + return (('', ''), ) + tuple(zip(tasks, tasks)) + + @property + def choices(self): + if self._choices is None: + self._choices = self.tasks_as_choices() + return self._choices + + @choices.setter + def choices(self, _): + # ChoiceField.__init__ sets ``self.choices = choices`` + # which would override ours. + pass + + @cached_property + def _modules(self): + self.celery_app.loader.import_default_modules() + + +class TaskChoiceField(forms.ChoiceField): + """Field that lets you choose between task names.""" + + widget = TaskSelectWidget + + def valid_value(self, value): + return True + + +class PeriodicTaskForm(WagtailAdminModelForm): + """Form that lets you create and modify periodic tasks.""" + + regtask = TaskChoiceField( + label=_('Task (registered)'), + required=False, + ) + task = forms.CharField( + label=_('Task (custom)'), + required=False, + max_length=200, + ) + + class Meta: + """Form metadata.""" + exclude = () + + def clean(self): + data = super().clean() + regtask = data.get('regtask') + if regtask: + data['task'] = regtask + if not data['task']: + exc = forms.ValidationError(_('Need name of task')) + self._errors['task'] = self.error_class(exc.messages) + raise exc + + if data.get('expire_seconds') is not None and data.get('expires'): + raise forms.ValidationError( + _('Only one can be set, in expires and expire_seconds') + ) + return data + + def _clean_json(self, field): + value = self.cleaned_data[field] + try: + loads(value) + except ValueError as exc: + raise forms.ValidationError( + _('Unable to parse JSON: %s') % exc, + ) + return value + + def clean_args(self): + return self._clean_json('args') + + def clean_kwargs(self): + return self._clean_json('kwargs') diff --git a/django_celery_beat/locale/es/LC_MESSAGES/django.po b/django_celery_beat/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..c3fb01a --- /dev/null +++ b/django_celery_beat/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,528 @@ +# Spanish translation strings for django-celery-beat. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-04 01:30+0000\n" +"PO-Revision-Date: 2021-04-03 22:36-0300\n" +"Last-Translator: Luis Saavedra \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 2.3\n" + +#: django_celery_beat/admin.py:64 +msgid "Task (registered)" +msgstr "Tarea (registrada)" + +#: django_celery_beat/admin.py:68 +msgid "Task (custom)" +msgstr "Tarea (personalizada)" + +#: django_celery_beat/admin.py:85 +msgid "Need name of task" +msgstr "Nombre de tarea necesario" + +#: django_celery_beat/admin.py:91 django_celery_beat/models.py:586 +msgid "Only one can be set, in expires and expire_seconds" +msgstr "" +"Sólo uno de los campos puede ser definido, en expiración y segundos de " +"expiración" + +#: django_celery_beat/admin.py:101 +#, python-format +msgid "Unable to parse JSON: %s" +msgstr "Incapaz de parsear el JSON: %s" + +#: django_celery_beat/admin.py:167 +#, python-brace-format +msgid "{0} task{1} {2} successfully {3}" +msgstr "{0} tarea{1} {2} correctamente {3}" + +#: django_celery_beat/admin.py:170 django_celery_beat/admin.py:232 +msgid "was,were" +msgstr "fue,fueron" + +#: django_celery_beat/admin.py:179 +msgid "Enable selected tasks" +msgstr "Habilitar tareas seleccionadas" + +#: django_celery_beat/admin.py:185 +msgid "Disable selected tasks" +msgstr "Deshabilitar tareas seleccionadas" + +#: django_celery_beat/admin.py:197 +msgid "Toggle activity of selected tasks" +msgstr "Conmutar actividad de las tareas seleccionadas" + +#: django_celery_beat/admin.py:217 +#, python-brace-format +msgid "task \"{0}\" not found" +msgstr "tarea \"{0}\" no encontrada" + +#: django_celery_beat/admin.py:229 +#, python-brace-format +msgid "{0} task{1} {2} successfully run" +msgstr "{0} tarea{1} {2} correctamente ejecutadas" + +#: django_celery_beat/admin.py:235 +msgid "Run selected tasks" +msgstr "Ejecutar tareas seleccionadas" + +#: django_celery_beat/apps.py:13 +msgid "Periodic Tasks" +msgstr "Tareas Periódicas" + +#: django_celery_beat/models.py:26 +msgid "Days" +msgstr "Días" + +#: django_celery_beat/models.py:27 +msgid "Hours" +msgstr "Horas" + +#: django_celery_beat/models.py:28 +msgid "Minutes" +msgstr "Minutos" + +#: django_celery_beat/models.py:29 +msgid "Seconds" +msgstr "Segundos" + +#: django_celery_beat/models.py:30 +msgid "Microseconds" +msgstr "Microsegundos" + +#: django_celery_beat/models.py:34 +msgid "Day" +msgstr "Día" + +#: django_celery_beat/models.py:35 +msgid "Hour" +msgstr "Hora" + +#: django_celery_beat/models.py:36 +msgid "Minute" +msgstr "Minuto" + +#: django_celery_beat/models.py:37 +msgid "Second" +msgstr "Segundo" + +#: django_celery_beat/models.py:38 +msgid "Microsecond" +msgstr "Microsegundo" + +#: django_celery_beat/models.py:42 +msgid "Astronomical dawn" +msgstr "Amanecer astronómico" + +#: django_celery_beat/models.py:43 +msgid "Civil dawn" +msgstr "Amanecer civil" + +#: django_celery_beat/models.py:44 +msgid "Nautical dawn" +msgstr "Amanecer náutico" + +#: django_celery_beat/models.py:45 +msgid "Astronomical dusk" +msgstr "Anochecer astronómico" + +#: django_celery_beat/models.py:46 +msgid "Civil dusk" +msgstr "Anochecer civil" + +#: django_celery_beat/models.py:47 +msgid "Nautical dusk" +msgstr "Anochecer náutico" + +#: django_celery_beat/models.py:48 +msgid "Solar noon" +msgstr "Mediodía solar" + +#: django_celery_beat/models.py:49 +msgid "Sunrise" +msgstr "Amanecer" + +#: django_celery_beat/models.py:50 +msgid "Sunset" +msgstr "Puesta de sol" + +#: django_celery_beat/models.py:84 +msgid "Solar Event" +msgstr "Evento Solar" + +#: django_celery_beat/models.py:85 +msgid "The type of solar event when the job should run" +msgstr "El tipo de evento solar cuando el proceso debe ejecutarse" + +#: django_celery_beat/models.py:89 +msgid "Latitude" +msgstr "Latitud" + +#: django_celery_beat/models.py:90 +msgid "Run the task when the event happens at this latitude" +msgstr "Ejecutar la tarea cuando el evento ocurra a esta latitud" + +#: django_celery_beat/models.py:95 +msgid "Longitude" +msgstr "Longitud" + +#: django_celery_beat/models.py:96 +msgid "Run the task when the event happens at this longitude" +msgstr "Ejecutar la tarea cuando el evento ocurra a esta longitud" + +#: django_celery_beat/models.py:103 +msgid "solar event" +msgstr "evento solar" + +#: django_celery_beat/models.py:104 +msgid "solar events" +msgstr "eventos solares" + +#: django_celery_beat/models.py:153 +msgid "Number of Periods" +msgstr "Número de Períodos" + +#: django_celery_beat/models.py:154 +msgid "Number of interval periods to wait before running the task again" +msgstr "" +"Número de períodos de intervalo a esperar antes de ejecutar esta tarea de " +"nuevo" + +#: django_celery_beat/models.py:160 +msgid "Interval Period" +msgstr "Período de intervalo" + +#: django_celery_beat/models.py:161 +msgid "The type of period between task runs (Example: days)" +msgstr "El tipo de período entre ejecuciones de tarea (Ejemplo: días)" + +#: django_celery_beat/models.py:167 +msgid "interval" +msgstr "intervalo" + +#: django_celery_beat/models.py:168 +msgid "intervals" +msgstr "intervalos" + +#: django_celery_beat/models.py:195 +msgid "every {}" +msgstr "cada {}" + +#: django_celery_beat/models.py:200 +msgid "every {} {}" +msgstr "cada {} {}" + +#: django_celery_beat/models.py:211 +msgid "Clock Time" +msgstr "Hora y día" + +#: django_celery_beat/models.py:212 +msgid "Run the task at clocked time" +msgstr "Ejecuta la tarea en el momento indicado" + +#: django_celery_beat/models.py:218 django_celery_beat/models.py:219 +msgid "clocked" +msgstr "cronometrado" + +#: django_celery_beat/models.py:258 +msgid "Minute(s)" +msgstr "Minuto(s)" + +#: django_celery_beat/models.py:260 +msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" +msgstr "" +"Minutos Cron cuando ejecutar. Usa \"*\" para \"todos\". (Ejemplo: \"0,30\")" + +#: django_celery_beat/models.py:265 +msgid "Hour(s)" +msgstr "Hora(s)" + +#: django_celery_beat/models.py:267 +msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" +msgstr "" +"Horas Cron cuando ejecutar. Usa \"*\" para \"todas\". (Ejemplo: \"8,20\")" + +#: django_celery_beat/models.py:272 +msgid "Day(s) Of The Week" +msgstr "Día(s) de la semana" + +#: django_celery_beat/models.py:274 +msgid "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" +msgstr "" +"Días de la semana Cron cuando ejecutar. Usa \"*\" para \"todos\". (Ejemplo: " +"\"0,5\")" + +#: django_celery_beat/models.py:280 +msgid "Day(s) Of The Month" +msgstr "Día(s) del mes" + +#: django_celery_beat/models.py:282 +msgid "" +"Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" +msgstr "" +"Días del mes Cron cuando ejecutar. Usa \"*\" para \"todos\". (Ejemplo: " +"\"1,15\")" + +#: django_celery_beat/models.py:288 +msgid "Month(s) Of The Year" +msgstr "Mes(es) del año" + +#: django_celery_beat/models.py:290 +msgid "" +"Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" +msgstr "" +"Meses del año Cron cuando ejecutar. Usa \"*\" para \"todos\". (Ejemplo: " +"\"0,6\")" + +#: django_celery_beat/models.py:297 +msgid "Cron Timezone" +msgstr "Zona horaria Cron" + +#: django_celery_beat/models.py:299 +msgid "Timezone to Run the Cron Schedule on. Default is UTC." +msgstr "Zona horaria donde ejecutar la programación Cron. Por defecto UTC." + +#: django_celery_beat/models.py:305 +msgid "crontab" +msgstr "crontab" + +#: django_celery_beat/models.py:306 +msgid "crontabs" +msgstr "crontabs" + +#: django_celery_beat/models.py:390 +msgid "Name" +msgstr "Nombre" + +#: django_celery_beat/models.py:391 +msgid "Short Description For This Task" +msgstr "Descripción corta para esta tarea" + +#: django_celery_beat/models.py:396 +msgid "" +"The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." +"import_contacts\")" +msgstr "" +"Nombre de la tarea Celery que debe ser ejecutada. (Ejemplo: \"proj.tasks." +"import_contacts\")" + +#: django_celery_beat/models.py:404 +msgid "Interval Schedule" +msgstr "Intervalo de programación" + +#: django_celery_beat/models.py:405 +msgid "" +"Interval Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "" +"Intervalo de programación donde ejecutar la tarea. Establece sólo un tipo de " +"programación, deja el resto en blanco." + +#: django_celery_beat/models.py:410 +msgid "Crontab Schedule" +msgstr "Programación Crontab" + +#: django_celery_beat/models.py:411 +msgid "" +"Crontab Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "" +"Programación Crontab con la cual ejecutar la tarea. Establece sólo un tipo " +"de programación, deja el resto en blanco." + +#: django_celery_beat/models.py:416 +msgid "Solar Schedule" +msgstr "Programación solar" + +#: django_celery_beat/models.py:417 +msgid "" +"Solar Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "" +"Programación solar con la cual ejecutar la tarea. Establece sólo un tipo de " +"programación, deja el resto en blanco." + +#: django_celery_beat/models.py:422 +msgid "Clocked Schedule" +msgstr "Programación horaria" + +#: django_celery_beat/models.py:423 +msgid "" +"Clocked Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "" +"Programación horaria con la cual ejecutar la tarea. Establece sólo un tipo " +"de programación, deja el resto en blanco." + +#: django_celery_beat/models.py:429 +msgid "Positional Arguments" +msgstr "Argumentos posicionales" + +#: django_celery_beat/models.py:431 +msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" +msgstr "" +"Argumentos posicionales codificados en formato JSON. (Ejemplo: [\"arg1\", " +"\"arg2\"])" + +#: django_celery_beat/models.py:436 +msgid "Keyword Arguments" +msgstr "Agumentos opcionales" + +#: django_celery_beat/models.py:438 +msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" +msgstr "" +"Argumentos opcionales codificados en formato JSON. (Ejemplo: {\"argument\": " +"\"value\"})" + +#: django_celery_beat/models.py:444 +msgid "Queue Override" +msgstr "Invalidación de cola" + +#: django_celery_beat/models.py:446 +msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." +msgstr "" +"Cola definida en CELERY_TASK_QUEUES. Dejala nula para la cola por defecto." + +#: django_celery_beat/models.py:455 +msgid "Exchange" +msgstr "Intercambio" + +#: django_celery_beat/models.py:456 +msgid "Override Exchange for low-level AMQP routing" +msgstr "Invalida intercambio para enrutamiento de bajo nivel de AMQP" + +#: django_celery_beat/models.py:460 +msgid "Routing Key" +msgstr "Clave de enrutamiento" + +#: django_celery_beat/models.py:461 +msgid "Override Routing Key for low-level AMQP routing" +msgstr "" +"Invalida la clave de enrutamiento para enrutamiento de bajo nivel de AMQP" + +#: django_celery_beat/models.py:465 +msgid "AMQP Message Headers" +msgstr "Cabeceras de mensaje de AMQP" + +#: django_celery_beat/models.py:466 +msgid "JSON encoded message headers for the AMQP message." +msgstr "Cacbeceras de mensaje de AMQP codificadas en formato JSON." + +#: django_celery_beat/models.py:472 +msgid "Priority" +msgstr "Prioridad" + +#: django_celery_beat/models.py:474 +msgid "" +"Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " +"reversed, 0 is highest)." +msgstr "" +"Número de prioridad entre 0 and 255. Soportado por: RabbitMQ, Redis " +"(prioridad invertida, 0 es la más alta)." + +#: django_celery_beat/models.py:479 +msgid "Expires Datetime" +msgstr "Fecha de caducidad" + +#: django_celery_beat/models.py:481 +msgid "" +"Datetime after which the schedule will no longer trigger the task to run" +msgstr "" +"Fecha después de la cual la programación no provocará que la tarea vuelva a " +"ejecutarse" + +#: django_celery_beat/models.py:486 +msgid "Expires timedelta with seconds" +msgstr "Delta de tiempo de expiración en segundos" + +#: django_celery_beat/models.py:488 +msgid "" +"Timedelta with seconds which the schedule will no longer trigger the task to " +"run" +msgstr "" +"Delta de Tiempo en segundos después de los cuales la programación no " +"provocará que la tarea vuelva a ejecutarse" + +#: django_celery_beat/models.py:494 +msgid "One-off Task" +msgstr "Tarea de ejecución única" + +#: django_celery_beat/models.py:496 +msgid "If True, the schedule will only run the task a single time" +msgstr "Si es verdadera, la programación sólo lanzará la tarea una vez" + +#: django_celery_beat/models.py:500 +msgid "Start Datetime" +msgstr "Fecha de comienzo" + +#: django_celery_beat/models.py:502 +msgid "Datetime when the schedule should begin triggering the task to run" +msgstr "" +"Fecha cuando la programación debe comenzar a provocar la ejecución de la " +"tarea" + +#: django_celery_beat/models.py:507 +msgid "Enabled" +msgstr "Habilitada" + +#: django_celery_beat/models.py:508 +msgid "Set to False to disable the schedule" +msgstr "Establece a Falso para deshabilitar la programación" + +#: django_celery_beat/models.py:513 +msgid "Last Run Datetime" +msgstr "Fecha de última ejecución" + +#: django_celery_beat/models.py:515 +msgid "" +"Datetime that the schedule last triggered the task to run. Reset to None if " +"enabled is set to False." +msgstr "" +"Fecha en la cual la programación ejecutó la tarea por última vez. " +"Reinicializa a None si enabled está establecido como falso." + +#: django_celery_beat/models.py:520 +msgid "Total Run Count" +msgstr "Contador de ejecuciones totales" + +#: django_celery_beat/models.py:522 +msgid "Running count of how many times the schedule has triggered the task" +msgstr "Contador de cuentas veces ha sido ejecutada la tarea" + +#: django_celery_beat/models.py:527 +msgid "Last Modified" +msgstr "Última modificación" + +#: django_celery_beat/models.py:528 +msgid "Datetime that this PeriodicTask was last modified" +msgstr "Fecha en la cual esta tarea periódica fue modificada por última vez" + +#: django_celery_beat/models.py:532 +msgid "Description" +msgstr "Descripción" + +#: django_celery_beat/models.py:534 +msgid "Detailed description about the details of this Periodic Task" +msgstr "Descripción detallada sobre los detalles de esta tarea periódica" + +#: django_celery_beat/models.py:543 +msgid "periodic task" +msgstr "tarea periódica" + +#: django_celery_beat/models.py:544 +msgid "periodic tasks" +msgstr "tareas periódicas" + +#: django_celery_beat/templates/admin/djcelery/change_list.html:6 +msgid "Home" +msgstr "Inicio" diff --git a/django_celery_beat/locale/fr/LC_MESSAGES/django.po b/django_celery_beat/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..01c5500 --- /dev/null +++ b/django_celery_beat/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,522 @@ +# French translation strings for django-celery-beat. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# , 2019. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-11-10 14:36+0000\n" +"PO-Revision-Date: 2020-06-09 10:30\n" +"Last-Translator: Álvaro Mondéjar \n" +"Language-Team: n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: django_celery_beat/admin.py:69 +msgid "Task (registered)" +msgstr "Tâche (enregistrée)" + +#: django_celery_beat/admin.py:73 +msgid "Task (custom)" +msgstr "Tâche (personalisée)" + +#: django_celery_beat/admin.py:90 +msgid "Need name of task" +msgstr "Besoin du nom de la tâche" + +#: django_celery_beat/admin.py:96 django_celery_beat/models.py:595 +msgid "Only one can be set, in expires and expire_seconds" +msgstr "Seulement un peu être définie, soit expires ou expire_seconds" + +#: django_celery_beat/admin.py:106 +#, python-format +msgid "Unable to parse JSON: %s" +msgstr "Incapable d'analyser le JSON: %s" + +#: django_celery_beat/admin.py:172 +#, python-brace-format +msgid "{0} task{1} {2} successfully {3}" +msgstr "{0} tâche{1} {2} avec succès {3}" + +#: django_celery_beat/admin.py:175 django_celery_beat/admin.py:237 +msgid "was,were" +msgstr "a été,ont été" + +#: django_celery_beat/admin.py:184 +msgid "Enable selected tasks" +msgstr "Active les tâches sélectionnées" + +#: django_celery_beat/admin.py:190 +msgid "Disable selected tasks" +msgstr "Désactive les tâches sélectionnées" + +#: django_celery_beat/admin.py:202 +msgid "Toggle activity of selected tasks" +msgstr "Bascule l'activité des tâches sélectionnées" + +#: django_celery_beat/admin.py:222 +#, python-brace-format +msgid "task \"{0}\" not found" +msgstr "tâche \"{0}\" introuvable" + +#: django_celery_beat/admin.py:234 +#, python-brace-format +msgid "{0} task{1} {2} successfully run" +msgstr "{0} tâche{1} {2} a fonctionnée avec succès" + +#: django_celery_beat/admin.py:240 +msgid "Run selected tasks" +msgstr "Démarre les tâches sélectionnées" + +#: django_celery_beat/apps.py:13 +msgid "Periodic Tasks" +msgstr "Tâches Périodique" + +#: django_celery_beat/models.py:26 +msgid "Days" +msgstr "Jours" + +#: django_celery_beat/models.py:27 +msgid "Hours" +msgstr "Heures" + +#: django_celery_beat/models.py:28 +msgid "Minutes" +msgstr "Minutes" + +#: django_celery_beat/models.py:29 +msgid "Seconds" +msgstr "Secondes" + +#: django_celery_beat/models.py:30 +msgid "Microseconds" +msgstr "Microsecondes" + +#: django_celery_beat/models.py:34 +msgid "Day" +msgstr "Jour" + +#: django_celery_beat/models.py:35 +msgid "Hour" +msgstr "Heure" + +#: django_celery_beat/models.py:36 +msgid "Minute" +msgstr "Minute" + +#: django_celery_beat/models.py:37 +msgid "Second" +msgstr "Seconde" + +#: django_celery_beat/models.py:38 +msgid "Microsecond" +msgstr "Microseconde" + +#: django_celery_beat/models.py:42 +msgid "Astronomical dawn" +msgstr "Aube astronomique" + +#: django_celery_beat/models.py:43 +msgid "Civil dawn" +msgstr "Aube civile" + +#: django_celery_beat/models.py:44 +msgid "Nautical dawn" +msgstr "Aube nautique" + +#: django_celery_beat/models.py:45 +msgid "Astronomical dusk" +msgstr "Crépuscule astronomique" + +#: django_celery_beat/models.py:46 +msgid "Civil dusk" +msgstr "Crépuscule civil" + +#: django_celery_beat/models.py:47 +msgid "Nautical dusk" +msgstr "Crépuscule nautique" + +#: django_celery_beat/models.py:48 +msgid "Solar noon" +msgstr "Midi solaire" + +#: django_celery_beat/models.py:49 +msgid "Sunrise" +msgstr "Lever du soleil" + +#: django_celery_beat/models.py:50 +msgid "Sunset" +msgstr "Coucher du soleil" + +#: django_celery_beat/models.py:82 +msgid "Solar Event" +msgstr "Évènement Solaire" + +#: django_celery_beat/models.py:83 +msgid "The type of solar event when the job should run" +msgstr "Le type d'évènement solaire pour lequel la tâche devrait démarrer" + +#: django_celery_beat/models.py:87 +msgid "Latitude" +msgstr "Latitude" + +#: django_celery_beat/models.py:88 +msgid "Run the task when the event happens at this latitude" +msgstr "Démarre cette tâche lorsque l'évènement se produit à cette latitude" + +#: django_celery_beat/models.py:93 +msgid "Longitude" +msgstr "Longitude" + +#: django_celery_beat/models.py:94 +msgid "Run the task when the event happens at this longitude" +msgstr "" +"Démarre cette tâche lorsque cette évènement se produit à cette longitude" + +#: django_celery_beat/models.py:101 +msgid "solar event" +msgstr "évènement solaire" + +#: django_celery_beat/models.py:102 +msgid "solar events" +msgstr "évènements solaire" + +#: django_celery_beat/models.py:151 +msgid "Number of Periods" +msgstr "Nombre de Périodes" + +#: django_celery_beat/models.py:152 +msgid "Number of interval periods to wait before running the task again" +msgstr "" +"Nombre d'intervale de périodes à attendre avant de démarrer la tâche à " +"nouveau" + +#: django_celery_beat/models.py:158 +msgid "Interval Period" +msgstr "Période d'Intervale" + +#: django_celery_beat/models.py:159 +msgid "The type of period between task runs (Example: days)" +msgstr "Le type de période entre chaque démarrage de tâche (Exemple: jours)" + +#: django_celery_beat/models.py:165 +msgid "interval" +msgstr "intervale" + +#: django_celery_beat/models.py:166 +msgid "intervals" +msgstr "intervales" + +#: django_celery_beat/models.py:194 +msgid "every {}" +msgstr "chaque {}" + +#: django_celery_beat/models.py:199 +msgid "every {} {}" +msgstr "chaque {} {}" + +#: django_celery_beat/models.py:210 +msgid "Clock Time" +msgstr "Horaire" + +#: django_celery_beat/models.py:211 +msgid "Run the task at clocked time" +msgstr "Démarre la tâche à l'horaire définie" + +#: django_celery_beat/models.py:216 django_celery_beat/models.py:516 +msgid "Enabled" +msgstr "Activée" + +#: django_celery_beat/models.py:217 django_celery_beat/models.py:517 +msgid "Set to False to disable the schedule" +msgstr "Mettre à Faux pour désactiver la planification" + +#: django_celery_beat/models.py:223 django_celery_beat/models.py:224 +msgid "clocked" +msgstr "horaire" + +#: django_celery_beat/models.py:266 +msgid "Minute(s)" +msgstr "Minute⋅s" + +#: django_celery_beat/models.py:268 +msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" +msgstr "" +"Minutes Cron pour Démarrer. Utilisez \"*\" pour \"toutes\". (Exemple: " +"\"0,30\")" + +#: django_celery_beat/models.py:273 +msgid "Hour(s)" +msgstr "Heure⋅s" + +#: django_celery_beat/models.py:275 +msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" +msgstr "" +"Heures Cron pour Démarrer. Utilisez \"*\" pour \"toutes\". (Exemple: " +"\"8,20\")" + +#: django_celery_beat/models.py:280 +msgid "Day(s) Of The Week" +msgstr "Jour⋅s De La Semaine" + +#: django_celery_beat/models.py:282 +msgid "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" +msgstr "" +"Jours De La Semaine Cron pour Démarrer. Utilisez \"*\" pour \"tous\". " +"(Exemple: \"0,5\")" + +#: django_celery_beat/models.py:288 +msgid "Day(s) Of The Month" +msgstr "Jour⋅s Du Mois" + +#: django_celery_beat/models.py:290 +msgid "" +"Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" +msgstr "" +"Jours Du Mois Cron pour Démarrer. Utilisez \"*\" pour \"tous\". (Exemple: " +"\"1,15\")" + +#: django_celery_beat/models.py:296 +msgid "Month(s) Of The Year" +msgstr "Mois De L'Année" + +#: django_celery_beat/models.py:298 +msgid "" +"Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" +msgstr "" +"Mois De L'Année Cron pour Démarrer. Utilisez \"*\" pour \"tous\". (Exemple:" +" ,6\")" + +#: django_celery_beat/models.py:305 +msgid "Cron Timezone" +msgstr "Fuseau Horaire Cron" + +#: django_celery_beat/models.py:307 +msgid "Timezone to Run the Cron Schedule on. Default is UTC." +msgstr "" +"Fuseau Horaire pour lequel démarrer la planification Cron. UTC par défaut." + +#: django_celery_beat/models.py:313 +msgid "crontab" +msgstr "crontab" + +#: django_celery_beat/models.py:314 +msgid "crontabs" +msgstr "crontabs" + +#: django_celery_beat/models.py:399 +msgid "Name" +msgstr "Nom" + +#: django_celery_beat/models.py:400 +msgid "Short Description For This Task" +msgstr "Description Courte Pour Cette Tâche" + +#: django_celery_beat/models.py:405 +msgid "" +"The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." +"import_contacts\")" +msgstr "" +"Le Nom de la Tâche Celery qui devrait être démarrée. (Exemple: \"proj.tasks." +"import_contacts\")" + +#: django_celery_beat/models.py:413 +msgid "Interval Schedule" +msgstr "Planification intervalée" + +#: django_celery_beat/models.py:414 +msgid "" +"Interval Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "" +"Planification intervalée pour démarrer cette tâche. Ne mettez qu'un seul " +"type de planification, laissez les autres vides" + +#: django_celery_beat/models.py:419 +msgid "Crontab Schedule" +msgstr "Planification Crontab" + +#: django_celery_beat/models.py:420 +msgid "" +"Crontab Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "" +"Planification Crontab pour démarrer cette tâche. Ne mettez qu'un seul type " +"de planification, laissez les autres vides" + +#: django_celery_beat/models.py:425 +msgid "Solar Schedule" +msgstr "Planification Solaire" + +#: django_celery_beat/models.py:426 +msgid "" +"Solar Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "" +"Planification Solaire pour démarrer cette tâche. Ne mettez qu'un seul type " +"de planification, laissez les autres vides" + +#: django_celery_beat/models.py:431 +msgid "Clocked Schedule" +msgstr "Planification Horaire" + +#: django_celery_beat/models.py:432 +msgid "" +"Clocked Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "" +"Planification Horaire pour démarrer cette tâche. Ne mettez qu'un seul type " +"de planification, laissez les autres vides" + +#: django_celery_beat/models.py:438 +msgid "Positional Arguments" +msgstr "Arguments Positionnels" + +#: django_celery_beat/models.py:440 +msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" +msgstr "Arguments positionnels encodés en JSON (Exemple: [\"arg1\", \"arg2\"])" + +#: django_celery_beat/models.py:445 +msgid "Keyword Arguments" +msgstr "Arguments Nommés" + +#: django_celery_beat/models.py:447 +msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" +msgstr "Arguments nommés encodés en JSON (Exemple: {\"argument\": \"valeur\"})" + +#: django_celery_beat/models.py:453 +msgid "Queue Override" +msgstr "Surcharge de file d'attente" + +#: django_celery_beat/models.py:455 +msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." +msgstr "" +"File d'attente définie dans CELERY_TASK_QEUEUS. Laissez Vide pour la mise en " +"file d'attente par défaut." + +#: django_celery_beat/models.py:464 +msgid "Exchange" +msgstr "Échange" + +#: django_celery_beat/models.py:465 +msgid "Override Exchange for low-level AMQP routing" +msgstr "Surcharge d'échange pour un routage AMQP bas-niveau" + +#: django_celery_beat/models.py:469 +msgid "Routing Key" +msgstr "Clé de routage" + +#: django_celery_beat/models.py:470 +msgid "Override Routing Key for low-level AMQP routing" +msgstr "Surcharge de clé de route pour un routage AMQP bas-niveau" + +#: django_celery_beat/models.py:474 +msgid "AMQP Message Headers" +msgstr "Message d'en-têtes AMQP" + +#: django_celery_beat/models.py:475 +msgid "JSON encoded message headers for the AMQP message." +msgstr "Message d'en-têtes encodés en JSON pour le message AMQP" + +#: django_celery_beat/models.py:481 +msgid "Priority" +msgstr "Priorité" + +#: django_celery_beat/models.py:483 +msgid "" +"Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " +"reversed, 0 is highest)." +msgstr "" +"Valeur de Priorité entre 0 et 255. Supporté par: RabbitMQ, Redis (priorité " +"inversé, 0 est plus élevé)." + +#: django_celery_beat/models.py:488 +msgid "Expires Datetime" +msgstr "Date et heure d'expiration" + +#: django_celery_beat/models.py:490 +msgid "" +"Datetime after which the schedule will no longer trigger the task to run" +msgstr "" +"Date et heure après laquelle la planification ne déclenchera plus la tâche à " +"démarrer" + +#: django_celery_beat/models.py:495 +msgid "Expires timedelta with seconds" +msgstr "Différence de temps en secondes d'expiration" + +#: django_celery_beat/models.py:497 +msgid "" +"Timedelta with seconds which the schedule will no longer trigger the task to " +"run" +msgstr "" +"Différence de temps en secondes à laquelle la planification ne déclenchera " +"plus la tâche à démarrer" + +#: django_celery_beat/models.py:503 +msgid "One-off Task" +msgstr "Tâche Ponctuelle" + +#: django_celery_beat/models.py:505 +msgid "If True, the schedule will only run the task a single time" +msgstr "Si Vrai, la planification ne démarrera la tâche qu'une seule fois" + +#: django_celery_beat/models.py:509 +msgid "Start Datetime" +msgstr "Date et heure de démarrage" + +#: django_celery_beat/models.py:511 +msgid "Datetime when the schedule should begin triggering the task to run" +msgstr "" +"Date et heure à laquelle la planification devrait commencer à déclencher la " +"tâche à démarrer" + +#: django_celery_beat/models.py:522 +msgid "Last Run Datetime" +msgstr "Date et heure du dernier démarrage" + +#: django_celery_beat/models.py:524 +msgid "" +"Datetime that the schedule last triggered the task to run. Reset to None if " +"enabled is set to False." +msgstr "" +"Date et heure à laquelle la planification à dernièrement déclenchée la tâche " +"à démarrer. Est remis à Vide si activé est mis à Faux" + +#: django_celery_beat/models.py:529 +msgid "Total Run Count" +msgstr "Nombre Total de Démarrage" + +#: django_celery_beat/models.py:531 +msgid "Running count of how many times the schedule has triggered the task" +msgstr "Compte combien de fois la planification a déclenchée la tâche" + +#: django_celery_beat/models.py:536 +msgid "Last Modified" +msgstr "Dernière modification" + +#: django_celery_beat/models.py:537 +msgid "Datetime that this PeriodicTask was last modified" +msgstr "Date et heure de la dernière modification de cette Tâche Périodique" + +#: django_celery_beat/models.py:541 +msgid "Description" +msgstr "Description" + +#: django_celery_beat/models.py:543 +msgid "Detailed description about the details of this Periodic Task" +msgstr "Description détaillée à propos des détails de cette Tâche Périodique" + +#: django_celery_beat/models.py:552 +msgid "periodic task" +msgstr "tâche périodiuqe" + +#: django_celery_beat/models.py:553 +msgid "periodic tasks" +msgstr "tâches périodique" diff --git a/django_celery_beat/locale/ru/LC_MESSAGES/django.po b/django_celery_beat/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000..130ab76 --- /dev/null +++ b/django_celery_beat/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,451 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 1.5.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-06-14 17:06+1000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Daniil Kharkov \n" +"Language-Team: LANGUAGE \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" + +#: django_celery_beat/admin.py:71 +msgid "Task (registered)" +msgstr "Задача (зарегистрированные)" + +#: django_celery_beat/admin.py:75 +msgid "Task (custom)" +msgstr "Задача (пользовательская)" + +#: django_celery_beat/admin.py:92 +msgid "Need name of task" +msgstr "Укажите название задачи" + +#: django_celery_beat/admin.py:103 +#, python-format +msgid "Unable to parse JSON: %s" +msgstr "Невозможно проанализировать JSON: %s" + +#: django_celery_beat/admin.py:165 +#, python-brace-format +msgid "{0} task{1} {2} successfully {3}" +msgstr "{0} задача {1} {2} успешно {3}" + +#: django_celery_beat/admin.py:168 django_celery_beat/admin.py:230 +msgid "was,were" +msgstr "был, были" + +#: django_celery_beat/admin.py:177 +msgid "Enable selected tasks" +msgstr "Включить выбранные задачи" + +#: django_celery_beat/admin.py:183 +msgid "Disable selected tasks" +msgstr "Выключить выбранные задачи" + +#: django_celery_beat/admin.py:195 +msgid "Toggle activity of selected tasks" +msgstr "Переключить активность выбранных задач" + +#: django_celery_beat/admin.py:215 +#, python-brace-format +msgid "task \"{0}\" not found" +msgstr "задача \"{0}\" не найдена" + +#: django_celery_beat/admin.py:227 +#, python-brace-format +msgid "{0} task{1} {2} successfully run" +msgstr "{0} задача{1} {2} успешно выполнена" + +#: django_celery_beat/admin.py:233 +msgid "Run selected tasks" +msgstr "Запустить выбранные задачи" + +#: django_celery_beat/apps.py:15 +msgid "Periodic Tasks" +msgstr "Периодические Задачи" + +#: django_celery_beat/models.py:29 +msgid "Days" +msgstr "Дни" + +#: django_celery_beat/models.py:30 +msgid "Hours" +msgstr "Часы" + +#: django_celery_beat/models.py:31 +msgid "Minutes" +msgstr "Минуты" + +#: django_celery_beat/models.py:32 +msgid "Seconds" +msgstr "Секунды" + +#: django_celery_beat/models.py:33 +msgid "Microseconds" +msgstr "Микросекунды" + +#: django_celery_beat/models.py:38 +msgid "Day" +msgstr "день" + +#: django_celery_beat/models.py:39 +msgid "Hour" +msgstr "время" + +#: django_celery_beat/models.py:40 +msgid "Minute" +msgstr "минут" + +#: django_celery_beat/models.py:41 +msgid "Second" +msgstr "Секунды" + +#: django_celery_beat/models.py:42 +msgid "Microsecond" +msgstr "Микросекунды" + +#: django_celery_beat/models.py:54 +msgid "Solar Event" +msgstr "Астрономическое" + +#: django_celery_beat/models.py:55 +msgid "The type of solar event when the job should run" +msgstr "Тип астрономического события для запуска задачи" + +#: django_celery_beat/models.py:59 +msgid "Latitude" +msgstr "Широта" + +#: django_celery_beat/models.py:60 +msgid "Run the task when the event happens at this latitude" +msgstr "Запуск задачи, когда событие происходит на данной широте" + +#: django_celery_beat/models.py:65 +msgid "Longitude" +msgstr "Долгота" + +#: django_celery_beat/models.py:66 +msgid "Run the task when the event happens at this longitude" +msgstr "Запуск задачи, когда событие происходит на данной долготе" + +#: django_celery_beat/models.py:73 +msgid "solar event" +msgstr "астрономическое событие" + +#: django_celery_beat/models.py:74 +msgid "solar events" +msgstr "астрономические события" + +#: django_celery_beat/models.py:124 +msgid "Number of Periods" +msgstr "Число периодов" + +#: django_celery_beat/models.py:125 +msgid "Number of interval periods to wait before running the task again" +msgstr "Количество периодов интервала перед новым запуском задачи" + +#: django_celery_beat/models.py:131 +msgid "Interval Period" +msgstr "Интервальный период" + +#: django_celery_beat/models.py:132 +msgid "The type of period between task runs (Example: days)" +msgstr "Тип периода между запусками задачи (Например: дни)" + +#: django_celery_beat/models.py:138 +msgid "interval" +msgstr "интервал" + +#: django_celery_beat/models.py:139 +msgid "intervals" +msgstr "интервалы" + +#: django_celery_beat/models.py:162 +#, python-brace-format +msgid "every {}" +msgstr "каждые {}" + +#: django_celery_beat/models.py:163 +#, python-brace-format +msgid "every {} {}" +msgstr "каждые {} {}" + +#: django_celery_beat/models.py:175 +msgid "Clock Time" +msgstr "Время" + +#: django_celery_beat/models.py:176 +msgid "Run the task at clocked time" +msgstr "Запуск задачи в указанное время" + +#: django_celery_beat/models.py:181 django_celery_beat/models.py:475 +msgid "Enabled" +msgstr "Активна" + +#: django_celery_beat/models.py:182 django_celery_beat/models.py:476 +msgid "Set to False to disable the schedule" +msgstr "Выключите для отключения расписания" + +#: django_celery_beat/models.py:188 django_celery_beat/models.py:189 +msgid "clocked" +msgstr "время" + +#: django_celery_beat/models.py:232 +msgid "Minute(s)" +msgstr "Минуты" + +#: django_celery_beat/models.py:234 +msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" +msgstr "Cron минуты. Используйте \"*\" для \"каждую\". (Например: \"0,30\")" + +#: django_celery_beat/models.py:239 +msgid "Hour(s)" +msgstr "Часы" + +#: django_celery_beat/models.py:241 +msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" +msgstr "Cron часы. Используйте \"*\" для \"каждый\". (Например: \"8,20\")" + +#: django_celery_beat/models.py:246 +msgid "Day(s) Of The Week" +msgstr "Дни недели" + +#: django_celery_beat/models.py:248 +msgid "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" +msgstr "Cron дни недели. Используйте \"*\" для \"каждый\". (Например: \"0,5\")" + +#: django_celery_beat/models.py:254 +msgid "Day(s) Of The Month" +msgstr "Дни" + +#: django_celery_beat/models.py:256 +msgid "" +"Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" +msgstr "" +"Cron дни. Используйте \"*\" для \"каждый\". (Например: \"1,15\")" + +#: django_celery_beat/models.py:262 +msgid "Month(s) Of The Year" +msgstr "Месяцы" + +#: django_celery_beat/models.py:264 +msgid "" +"Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" +msgstr "" +"Cron месяцы. Используйте \"*\" для \"каждый\". (Например: \"0,6\")" + +#: django_celery_beat/models.py:271 +msgid "Cron Timezone" +msgstr "Временная зона для Cron" + +#: django_celery_beat/models.py:273 +msgid "Timezone to Run the Cron Schedule on. Default is UTC." +msgstr "Временная зона для Cron расписания. UTC по умолчанию." + +#: django_celery_beat/models.py:279 +msgid "crontab" +msgstr "crontab" + +#: django_celery_beat/models.py:280 +msgid "crontabs" +msgstr "crontab" + +#: django_celery_beat/models.py:366 +msgid "Name" +msgstr "Название" + +#: django_celery_beat/models.py:367 +msgid "Short Description For This Task" +msgstr "Краткое описание для этой задачи" + +#: django_celery_beat/models.py:372 +msgid "" +"The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." +"import_contacts\")" +msgstr "" +"Имя запускаемой Celery задачи. (Например: \"proj.tasks." +"import_contacts\")" + +#: django_celery_beat/models.py:380 +msgid "Interval Schedule" +msgstr "Интервал" + +#: django_celery_beat/models.py:381 +msgid "" +"Interval Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "Интервальное расписание для запуска задачи. Выберите только один тип " +"расписания, остальные оставьте пустыми." + +#: django_celery_beat/models.py:386 +msgid "Crontab Schedule" +msgstr "Crontab" + +#: django_celery_beat/models.py:387 +msgid "" +"Crontab Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "Crontab расписание для запуска задачи. Выберите только один тип " +"расписания, остальные оставьте пустыми." + +#: django_celery_beat/models.py:392 +msgid "Solar Schedule" +msgstr "Астрономическое" + +#: django_celery_beat/models.py:393 +msgid "" +"Solar Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "Астрономическое расписание для запуска задачи. Выберите только один " +"тип расписания, остальные оставьте пустыми." + +#: django_celery_beat/models.py:398 +msgid "Clocked Schedule" +msgstr "Хронометрическое" + +#: django_celery_beat/models.py:399 +msgid "" +"Clocked Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "Хронометрическое расписание для запуска задачи. Выберите только один " +"тип расписания, остальные оставьте пустыми." + +#: django_celery_beat/models.py:405 +msgid "Positional Arguments" +msgstr "Позиционные аргументы" + +#: django_celery_beat/models.py:407 +msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" +msgstr "Закодированные в JSON позиционные аргументы (Например: [\"arg1\", \"arg2\"])" + +#: django_celery_beat/models.py:412 +msgid "Keyword Arguments" +msgstr "Именованные аргументы" + +#: django_celery_beat/models.py:414 +msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" +msgstr "Закодированные в JSON именованные аргументы (Например: {\"argument\": \"value\"})" + +#: django_celery_beat/models.py:420 +msgid "Queue Override" +msgstr "Переопределение очереди" + +#: django_celery_beat/models.py:422 +msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." +msgstr "Очередь задана в CELERY_TASK_QUEUES. Оставьте None для стандартного распределения." + +#: django_celery_beat/models.py:431 +msgid "Exchange" +msgstr "Exchange" + +#: django_celery_beat/models.py:432 +msgid "Override Exchange for low-level AMQP routing" +msgstr "Override Exchange for low-level AMQP routing" + +#: django_celery_beat/models.py:436 +msgid "Routing Key" +msgstr "Ключ маршрутизации" + +#: django_celery_beat/models.py:437 +msgid "Override Routing Key for low-level AMQP routing" +msgstr "Override Routing Key for low-level AMQP routing" + +#: django_celery_beat/models.py:441 +msgid "AMQP Message Headers" +msgstr "Заголовки сообщения AMQP" + +#: django_celery_beat/models.py:442 +msgid "JSON encoded message headers for the AMQP message." +msgstr "Закодированные в JSON заголовки для AMQP сообщения." + +#: django_celery_beat/models.py:448 +msgid "Priority" +msgstr "Приоритет" + +#: django_celery_beat/models.py:450 +msgid "" +"Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " +"reversed, 0 is highest)." +msgstr "" +"Число между 0 и 255. Поддерживается в: RabbitMQ, Redis (приоритет " +"по убыванию, 0 наивысший)." + +#: django_celery_beat/models.py:455 +msgid "Expires Datetime" +msgstr "Истекает" + +#: django_celery_beat/models.py:457 +msgid "" +"Datetime after which the schedule will no longer trigger the task to run" +msgstr "Время, после которого расписание больше не будет запускать задачу" + +#: django_celery_beat/models.py:462 +msgid "One-off Task" +msgstr "Одноразовая задача" + +#: django_celery_beat/models.py:464 +msgid "If True, the schedule will only run the task a single time" +msgstr "Если включено, то задача будет запущена только один раз" + +#: django_celery_beat/models.py:468 +msgid "Start Datetime" +msgstr "Время начала" + +#: django_celery_beat/models.py:470 +msgid "Datetime when the schedule should begin triggering the task to run" +msgstr "Время начала вызовов задачи расписанием" + +#: django_celery_beat/models.py:481 +msgid "Last Run Datetime" +msgstr "Последний запуск" + +#: django_celery_beat/models.py:483 +msgid "" +"Datetime that the schedule last triggered the task to run. Reset to None if " +"enabled is set to False." +msgstr "" +"Время последнего вызова задачи. None если задача выключена." + +#: django_celery_beat/models.py:488 +msgid "Total Run Count" +msgstr "Запусков всего" + +#: django_celery_beat/models.py:490 +msgid "Running count of how many times the schedule has triggered the task" +msgstr "Количество запусков задачи этим расписанием" + +#: django_celery_beat/models.py:495 +msgid "Last Modified" +msgstr "Последнее изменение" + +#: django_celery_beat/models.py:496 +msgid "Datetime that this PeriodicTask was last modified" +msgstr "Время последнего изменения этой задачи" + +#: django_celery_beat/models.py:500 +msgid "Description" +msgstr "Описание" + +#: django_celery_beat/models.py:502 +msgid "Detailed description about the details of this Periodic Task" +msgstr "Подробное описание того, что делает эта задача" + +#: django_celery_beat/models.py:511 +msgid "periodic task" +msgstr "периодическая задача" + +#: django_celery_beat/models.py:512 +msgid "periodic tasks" +msgstr "периодические задачи" diff --git a/django_celery_beat/locale/zh_hans/LC_MESSAGES/django.po b/django_celery_beat/locale/zh_hans/LC_MESSAGES/django.po new file mode 100644 index 0000000..367914d --- /dev/null +++ b/django_celery_beat/locale/zh_hans/LC_MESSAGES/django.po @@ -0,0 +1,486 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-02-19 00:36+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Rainshaw \n" +"Language-Team: x_zhuo \n" +"Language: zh-hans \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\django_celery_beat\admin.py:64 +msgid "Task (registered)" +msgstr "任务 (已注册的)" + +#: .\django_celery_beat\admin.py:68 +msgid "Task (custom)" +msgstr "任务 (自定义)" + +#: .\django_celery_beat\admin.py:85 +msgid "Need name of task" +msgstr "任务需要一个名称" + +#: .\django_celery_beat\admin.py:91 .\django_celery_beat\models.py:589 +msgid "Only one can be set, in expires and expire_seconds" +msgstr "" + +#: .\django_celery_beat\admin.py:101 +#, python-format +msgid "Unable to parse JSON: %s" +msgstr "无法解析 JSON: %s" + +#: .\django_celery_beat\admin.py:167 +#, python-brace-format +msgid "{0} task{1} {2} successfully {3}" +msgstr "{0} 任务{1} {2} 成功 {3}" + +#: .\django_celery_beat\admin.py:170 .\django_celery_beat\admin.py:232 +msgid "was,were" +msgstr "将" + +#: .\django_celery_beat\admin.py:179 +msgid "Enable selected tasks" +msgstr "启用选中的任务" + +#: .\django_celery_beat\admin.py:185 +msgid "Disable selected tasks" +msgstr "禁用选中的任务" + +#: .\django_celery_beat\admin.py:197 +msgid "Toggle activity of selected tasks" +msgstr "切换选中的任务" + +#: .\django_celery_beat\admin.py:217 +#, python-brace-format +msgid "task \"{0}\" not found" +msgstr "" + +#: .\django_celery_beat\admin.py:229 +#, python-brace-format +msgid "{0} task{1} {2} successfully run" +msgstr "{0} 任务{1} {2} 启动成功" + +#: .\django_celery_beat\admin.py:235 +msgid "Run selected tasks" +msgstr "运行选中的任务" + +#: .\django_celery_beat\apps.py:13 +msgid "Periodic Tasks" +msgstr "周期任务" + +#: .\django_celery_beat\models.py:26 +msgid "Days" +msgstr "天" + +#: .\django_celery_beat\models.py:27 +msgid "Hours" +msgstr "小时" + +#: .\django_celery_beat\models.py:28 +msgid "Minutes" +msgstr "分钟" + +#: .\django_celery_beat\models.py:29 +msgid "Seconds" +msgstr "秒" + +#: .\django_celery_beat\models.py:30 +msgid "Microseconds" +msgstr "毫秒" + +#: .\django_celery_beat\models.py:34 +msgid "Day" +msgstr "天" + +#: .\django_celery_beat\models.py:35 +msgid "Hour" +msgstr "小时" + +#: .\django_celery_beat\models.py:36 +msgid "Minute" +msgstr "分钟" + +#: .\django_celery_beat\models.py:37 +msgid "Second" +msgstr "秒" + +#: .\django_celery_beat\models.py:38 +msgid "Microsecond" +msgstr "毫秒" + +#: .\django_celery_beat\models.py:42 +msgid "Astronomical dawn" +msgstr "天文黎明" + +#: .\django_celery_beat\models.py:43 +msgid "Civil dawn" +msgstr "民事黎明" + +#: .\django_celery_beat\models.py:44 +msgid "Nautical dawn" +msgstr "航海黎明" + +#: .\django_celery_beat\models.py:45 +msgid "Astronomical dusk" +msgstr "天文黄昏" + +#: .\django_celery_beat\models.py:46 +msgid "Civil dusk" +msgstr "民事黄昏" + +#: .\django_celery_beat\models.py:47 +msgid "Nautical dusk" +msgstr "航海黄昏" + +#: .\django_celery_beat\models.py:48 +msgid "Solar noon" +msgstr "正午" + +#: .\django_celery_beat\models.py:49 +msgid "Sunrise" +msgstr "日出" + +#: .\django_celery_beat\models.py:50 +msgid "Sunset" +msgstr "日落" + +#: .\django_celery_beat\models.py:84 +msgid "Solar Event" +msgstr "日程事件" + +#: .\django_celery_beat\models.py:85 +msgid "The type of solar event when the job should run" +msgstr "当任务应该执行时的日程事件类型" + +#: .\django_celery_beat\models.py:89 +msgid "Latitude" +msgstr "纬度" + +#: .\django_celery_beat\models.py:90 +msgid "Run the task when the event happens at this latitude" +msgstr "当在此纬度发生事件时执行任务" + +#: .\django_celery_beat\models.py:95 +msgid "Longitude" +msgstr "经度" + +#: .\django_celery_beat\models.py:96 +msgid "Run the task when the event happens at this longitude" +msgstr "当在此经度发生事件时执行任务" + +#: .\django_celery_beat\models.py:103 +msgid "solar event" +msgstr "日程事件" + +#: .\django_celery_beat\models.py:104 +msgid "solar events" +msgstr "日程事件" + +#: .\django_celery_beat\models.py:153 +msgid "Number of Periods" +msgstr "周期数" + +#: .\django_celery_beat\models.py:154 +msgid "Number of interval periods to wait before running the task again" +msgstr "再次执行任务之前要等待的间隔周期数" + +#: .\django_celery_beat\models.py:160 +msgid "Interval Period" +msgstr "间隔周期" + +#: .\django_celery_beat\models.py:161 +msgid "The type of period between task runs (Example: days)" +msgstr "任务每次执行之间的时间间隔类型(例如:天)" + +#: .\django_celery_beat\models.py:167 +msgid "interval" +msgstr "间隔" + +#: .\django_celery_beat\models.py:168 +msgid "intervals" +msgstr "间隔" + +#: .\django_celery_beat\models.py:196 +msgid "every {}" +msgstr "每 {}" + +#: .\django_celery_beat\models.py:201 +msgid "every {} {}" +msgstr "每 {} {}" + +#: .\django_celery_beat\models.py:212 +msgid "Clock Time" +msgstr "定时时间" + +#: .\django_celery_beat\models.py:213 +msgid "Run the task at clocked time" +msgstr "在定时时间执行任务" + +#: .\django_celery_beat\models.py:219 .\django_celery_beat\models.py:220 +msgid "clocked" +msgstr "定时" + +#: .\django_celery_beat\models.py:260 +msgid "Minute(s)" +msgstr "分钟" + +#: .\django_celery_beat\models.py:262 +msgid "Cron Minutes to Run. Use \"*\" for \"all\". (Example: \"0,30\")" +msgstr "计划执行的分钟。 将\"*\"用作\"all\"。(例如:\"0,30\")" + +#: .\django_celery_beat\models.py:267 +msgid "Hour(s)" +msgstr "小时" + +#: .\django_celery_beat\models.py:269 +msgid "Cron Hours to Run. Use \"*\" for \"all\". (Example: \"8,20\")" +msgstr "计划执行的小时。 将\"*\"用作\"all\"。(例如:\"8,20\")" + +#: .\django_celery_beat\models.py:274 +msgid "Day(s) Of The Week" +msgstr "一个星期的第几天" + +#: .\django_celery_beat\models.py:276 +msgid "Cron Days Of The Week to Run. Use \"*\" for \"all\". (Example: \"0,5\")" +msgstr "计划执行的每周的第几天。将\"*\"用作\"all\"。(例如:\"0,5\")" + +#: .\django_celery_beat\models.py:282 +msgid "Day(s) Of The Month" +msgstr "一个月的第几天" + +#: .\django_celery_beat\models.py:284 +msgid "" +"Cron Days Of The Month to Run. Use \"*\" for \"all\". (Example: \"1,15\")" +msgstr "计划执行的每个月的第几天。将\"*\"用作\"all\"。(例如:\"0,5\")" + +#: .\django_celery_beat\models.py:290 +msgid "Month(s) Of The Year" +msgstr "一年的第几个月" + +#: .\django_celery_beat\models.py:292 +msgid "" +"Cron Months Of The Year to Run. Use \"*\" for \"all\". (Example: \"0,6\")" +msgstr "计划执行的每一年的第几个月。将\"*\"用作\"all\"。(例如:\"0,5\")" + +#: .\django_celery_beat\models.py:299 +msgid "Cron Timezone" +msgstr "计划任务的时区" + +#: .\django_celery_beat\models.py:301 +msgid "Timezone to Run the Cron Schedule on. Default is UTC." +msgstr "执行计划任务表的时区。 默认为UTC。" + +#: .\django_celery_beat\models.py:307 +msgid "crontab" +msgstr "计划任务" + +#: .\django_celery_beat\models.py:308 +msgid "crontabs" +msgstr "计划任务" + +#: .\django_celery_beat\models.py:393 +msgid "Name" +msgstr "任务名" + +#: .\django_celery_beat\models.py:394 +msgid "Short Description For This Task" +msgstr "该任务的简短说明" + +#: .\django_celery_beat\models.py:399 +msgid "" +"The Name of the Celery Task that Should be Run. (Example: \"proj.tasks." +"import_contacts\")" +msgstr "被执行的任务的名称。(例如:\"proj.tasks.import_contacts\")" + +#: .\django_celery_beat\models.py:407 +msgid "Interval Schedule" +msgstr "间隔时间表" + +#: .\django_celery_beat\models.py:408 +msgid "" +"Interval Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "执行任务的间隔时间表。 仅设置一种时间表类型,将其他保留为空。" + +#: .\django_celery_beat\models.py:413 +msgid "Crontab Schedule" +msgstr "计划时间表" + +#: .\django_celery_beat\models.py:414 +msgid "" +"Crontab Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "执行任务的计划时间表。 仅设置一种时间表类型,将其他保留为空。" + +#: .\django_celery_beat\models.py:419 +msgid "Solar Schedule" +msgstr "日程时间表" + +#: .\django_celery_beat\models.py:420 +msgid "" +"Solar Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "执行任务的日程时间表。 仅设置一种时间表类型,将其他保留为空。" + +#: .\django_celery_beat\models.py:425 +msgid "Clocked Schedule" +msgstr "定时时间表" + +#: .\django_celery_beat\models.py:426 +msgid "" +"Clocked Schedule to run the task on. Set only one schedule type, leave the " +"others null." +msgstr "执行任务的定时时间表。 仅设置一种时间表类型,将其他保留为空。" + +#: .\django_celery_beat\models.py:432 +msgid "Positional Arguments" +msgstr "位置参数" + +#: .\django_celery_beat\models.py:434 +msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" +msgstr "JSON编码的位置参数(例如: [\"arg1\", \"arg2\"])" + +#: .\django_celery_beat\models.py:439 +msgid "Keyword Arguments" +msgstr "关键字参数" + +#: .\django_celery_beat\models.py:441 +msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" +msgstr "JSON编码的关键字参数(例如: {\"argument\": \"value\"})" + +#: .\django_celery_beat\models.py:447 +msgid "Queue Override" +msgstr "队列覆盖" + +#: .\django_celery_beat\models.py:449 +msgid "Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing." +msgstr "在 CELERY_TASK_QUEUES 定义的队列。保留空以进行默认排队。" + +#: .\django_celery_beat\models.py:458 +msgid "Exchange" +msgstr "交换机" + +#: .\django_celery_beat\models.py:459 +msgid "Override Exchange for low-level AMQP routing" +msgstr "覆盖交换机以进行低层级AMQP路由" + +#: .\django_celery_beat\models.py:463 +msgid "Routing Key" +msgstr "路由键" + +#: .\django_celery_beat\models.py:464 +msgid "Override Routing Key for low-level AMQP routing" +msgstr "覆盖路由键以进行低层级AMQP路由" + +#: .\django_celery_beat\models.py:468 +msgid "AMQP Message Headers" +msgstr "AMQP消息头" + +#: .\django_celery_beat\models.py:469 +msgid "JSON encoded message headers for the AMQP message." +msgstr "AMQP消息的JSON编码消息头。" + +#: .\django_celery_beat\models.py:475 +msgid "Priority" +msgstr "优先级" + +#: .\django_celery_beat\models.py:477 +msgid "" +"Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority " +"reversed, 0 is highest)." +msgstr "优先级数字,介于0和255之间。支持者:RabbitMQ,Redis(优先级颠倒,0是最高)。" + +#: .\django_celery_beat\models.py:482 +msgid "Expires Datetime" +msgstr "过期时刻" + +#: .\django_celery_beat\models.py:484 +msgid "" +"Datetime after which the schedule will no longer trigger the task to run" +msgstr "过期时刻,计划表将在此时刻后不再触发任务执行" + +#: .\django_celery_beat\models.py:489 +msgid "Expires timedelta with seconds" +msgstr "过期时间间隔,以秒为单位" + +#: .\django_celery_beat\models.py:491 +msgid "" +"Timedelta with seconds which the schedule will no longer trigger the task to " +"run" +msgstr "再过该秒后,不再触发任务执行" + +#: .\django_celery_beat\models.py:497 +msgid "One-off Task" +msgstr "一次任务" + +#: .\django_celery_beat\models.py:499 +msgid "If True, the schedule will only run the task a single time" +msgstr "如果为True,则计划将仅运行任务一次" + +#: .\django_celery_beat\models.py:503 +msgid "Start Datetime" +msgstr "开始时间" + +#: .\django_celery_beat\models.py:505 +msgid "Datetime when the schedule should begin triggering the task to run" +msgstr "时间表开始触发任务执行的时刻" + +#: .\django_celery_beat\models.py:510 +msgid "Enabled" +msgstr "已启用" + +#: .\django_celery_beat\models.py:511 +msgid "Set to False to disable the schedule" +msgstr "设置为False可禁用时间表" + +#: .\django_celery_beat\models.py:516 +msgid "Last Run Datetime" +msgstr "上次运行时刻" + +#: .\django_celery_beat\models.py:518 +msgid "" +"Datetime that the schedule last triggered the task to run. Reset to None if " +"enabled is set to False." +msgstr "最后一次触发任务执行的时刻。 如果enabled设置为False,则重置为None。" + +#: .\django_celery_beat\models.py:523 +msgid "Total Run Count" +msgstr "总运行次数" + +#: .\django_celery_beat\models.py:525 +msgid "Running count of how many times the schedule has triggered the task" +msgstr "任务执行多少次的运行计数" + +#: .\django_celery_beat\models.py:530 +msgid "Last Modified" +msgstr "最后修改" + +#: .\django_celery_beat\models.py:531 +msgid "Datetime that this PeriodicTask was last modified" +msgstr "该周期性任务的最后修改时刻" + +#: .\django_celery_beat\models.py:535 +msgid "Description" +msgstr "描述" + +#: .\django_celery_beat\models.py:537 +msgid "Detailed description about the details of this Periodic Task" +msgstr "有关此周期性任务的详细信息" + +#: .\django_celery_beat\models.py:546 +msgid "periodic task" +msgstr "周期性任务" + +#: .\django_celery_beat\models.py:547 +msgid "periodic tasks" +msgstr "周期性任务" diff --git a/django_celery_beat/managers.py b/django_celery_beat/managers.py new file mode 100644 index 0000000..fe3b917 --- /dev/null +++ b/django_celery_beat/managers.py @@ -0,0 +1,30 @@ +"""Model managers.""" +from django.db import models +from django.db.models.query import QuerySet + + +class ExtendedQuerySet(QuerySet): + """Base class for query sets.""" + + def update_or_create(self, defaults=None, **kwargs): + obj, created = self.get_or_create(defaults=defaults, **kwargs) + if not created: + self._update_model_with_dict(obj, dict(defaults or {}, **kwargs)) + return obj + + def _update_model_with_dict(self, obj, fields): + [setattr(obj, attr_name, attr_value) + for attr_name, attr_value in fields.items()] + obj.save() + return obj + + +class ExtendedManager(models.Manager.from_queryset(ExtendedQuerySet)): + """Manager with common utilities.""" + + +class PeriodicTaskManager(ExtendedManager): + """Manager for PeriodicTask model.""" + + def enabled(self): + return self.filter(enabled=True) diff --git a/django_celery_beat/migrations/0001_initial.py b/django_celery_beat/migrations/0001_initial.py new file mode 100644 index 0000000..3c8ae4d --- /dev/null +++ b/django_celery_beat/migrations/0001_initial.py @@ -0,0 +1,129 @@ +# Generated by Django 1.9.5 on 2016-08-04 02:13 +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CrontabSchedule', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID')), + ('minute', models.CharField( + default='*', max_length=64, verbose_name='minute')), + ('hour', models.CharField( + default='*', max_length=64, verbose_name='hour')), + ('day_of_week', models.CharField( + default='*', max_length=64, verbose_name='day of week')), + ('day_of_month', models.CharField( + default='*', max_length=64, verbose_name='day of month')), + ('month_of_year', models.CharField( + default='*', max_length=64, verbose_name='month of year')), + ], + options={ + 'ordering': [ + 'month_of_year', 'day_of_month', + 'day_of_week', 'hour', 'minute', + ], + 'verbose_name': 'crontab', + 'verbose_name_plural': 'crontabs', + }, + ), + migrations.CreateModel( + name='IntervalSchedule', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID')), + ('every', models.IntegerField(verbose_name='every')), + ('period', models.CharField( + choices=[ + ('days', 'Days'), + ('hours', 'Hours'), + ('minutes', 'Minutes'), + ('seconds', 'Seconds'), + ('microseconds', 'Microseconds'), + ], + max_length=24, + verbose_name='period')), + ], + options={ + 'ordering': ['period', 'every'], + 'verbose_name': 'interval', + 'verbose_name_plural': 'intervals', + }, + ), + migrations.CreateModel( + name='PeriodicTask', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID')), + ('name', models.CharField( + help_text='Useful description', max_length=200, + unique=True, verbose_name='name')), + ('task', models.CharField( + max_length=200, verbose_name='task name')), + ('args', models.TextField( + blank=True, default='[]', + help_text='JSON encoded positional arguments', + verbose_name='Arguments')), + ('kwargs', models.TextField( + blank=True, default='{}', + help_text='JSON encoded keyword arguments', + verbose_name='Keyword arguments')), + ('queue', models.CharField( + blank=True, default=None, + help_text='Queue defined in CELERY_TASK_QUEUES', + max_length=200, null=True, verbose_name='queue')), + ('exchange', models.CharField( + blank=True, default=None, max_length=200, + null=True, verbose_name='exchange')), + ('routing_key', models.CharField( + blank=True, default=None, + max_length=200, null=True, verbose_name='routing key')), + ('expires', models.DateTimeField( + blank=True, null=True, verbose_name='expires')), + ('enabled', models.BooleanField( + default=True, verbose_name='enabled')), + ('last_run_at', models.DateTimeField( + blank=True, editable=False, null=True)), + ('total_run_count', models.PositiveIntegerField( + default=0, editable=False)), + ('date_changed', models.DateTimeField(auto_now=True)), + ('description', models.TextField( + blank=True, verbose_name='description')), + ('crontab', models.ForeignKey( + blank=True, help_text='Use one of interval/crontab', + null=True, on_delete=django.db.models.deletion.CASCADE, + to='django_celery_beat.CrontabSchedule', + verbose_name='crontab')), + ('interval', models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.CASCADE, + to='django_celery_beat.IntervalSchedule', + verbose_name='interval')), + ], + options={ + 'verbose_name': 'periodic task', + 'verbose_name_plural': 'periodic tasks', + }, + ), + migrations.CreateModel( + name='PeriodicTasks', + fields=[ + ('ident', models.SmallIntegerField( + default=1, primary_key=True, + serialize=False, unique=True)), + ('last_update', models.DateTimeField()), + ], + ), + ] diff --git a/django_celery_beat/migrations/0002_auto_20161118_0346.py b/django_celery_beat/migrations/0002_auto_20161118_0346.py new file mode 100644 index 0000000..6f7fc3d --- /dev/null +++ b/django_celery_beat/migrations/0002_auto_20161118_0346.py @@ -0,0 +1,49 @@ +# Generated by Django 1.10.3 on 2016-11-18 03:46 +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SolarSchedule', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID')), + ('event', models.CharField( + choices=[('dusk_nautical', 'dusk_nautical'), + ('dawn_astronomical', 'dawn_astronomical'), + ('dawn_nautical', 'dawn_nautical'), + ('dawn_civil', 'dawn_civil'), + ('sunset', 'sunset'), + ('solar_noon', 'solar_noon'), + ('dusk_astronomical', 'dusk_astronomical'), + ('sunrise', 'sunrise'), + ('dusk_civil', 'dusk_civil')], + max_length=24, verbose_name='event')), + ('latitude', models.DecimalField( + decimal_places=6, max_digits=9, verbose_name='latitude')), + ('longitude', models.DecimalField( + decimal_places=6, max_digits=9, verbose_name='latitude')), + ], + options={ + 'ordering': ['event', 'latitude', 'longitude'], + 'verbose_name': 'solar', + 'verbose_name_plural': 'solars', + }, + ), + migrations.AddField( + model_name='periodictask', + name='solar', + field=models.ForeignKey( + blank=True, help_text='Use a solar schedule', + null=True, on_delete=django.db.models.deletion.CASCADE, + to='django_celery_beat.SolarSchedule', verbose_name='solar'), + ), + ] diff --git a/django_celery_beat/migrations/0003_auto_20161209_0049.py b/django_celery_beat/migrations/0003_auto_20161209_0049.py new file mode 100644 index 0000000..3688106 --- /dev/null +++ b/django_celery_beat/migrations/0003_auto_20161209_0049.py @@ -0,0 +1,23 @@ +# Generated by Django 1.9.11 on 2016-12-09 00:49 +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0002_auto_20161118_0346'), + ] + + operations = [ + migrations.AlterModelOptions( + name='solarschedule', + options={ + 'ordering': ('event', 'latitude', 'longitude'), + 'verbose_name': 'solar event', + 'verbose_name_plural': 'solar events'}, + ), + migrations.AlterUniqueTogether( + name='solarschedule', + unique_together=set([('event', 'latitude', 'longitude')]), + ), + ] diff --git a/django_celery_beat/migrations/0004_auto_20170221_0000.py b/django_celery_beat/migrations/0004_auto_20170221_0000.py new file mode 100644 index 0000000..409e9d8 --- /dev/null +++ b/django_celery_beat/migrations/0004_auto_20170221_0000.py @@ -0,0 +1,19 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0003_auto_20161209_0049'), + ] + + operations = [ + migrations.AlterField( + model_name='solarschedule', + name='longitude', + field=models.DecimalField( + verbose_name='longitude', + max_digits=9, + decimal_places=6), + ), + ] diff --git a/django_celery_beat/migrations/0005_add_solarschedule_events_choices.py b/django_celery_beat/migrations/0005_add_solarschedule_events_choices.py new file mode 100644 index 0000000..64897c1 --- /dev/null +++ b/django_celery_beat/migrations/0005_add_solarschedule_events_choices.py @@ -0,0 +1,28 @@ +# Generated by Django 1.9.1 on 2017-11-01 15:53 +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0004_auto_20170221_0000'), + ] + + operations = [ + migrations.AlterField( + model_name='solarschedule', + name='event', + field=models.CharField(choices=[ + ('dawn_astronomical', 'dawn_astronomical'), + ('dawn_civil', 'dawn_civil'), + ('dawn_nautical', 'dawn_nautical'), + ('dusk_astronomical', 'dusk_astronomical'), + ('dusk_civil', 'dusk_civil'), + ('dusk_nautical', 'dusk_nautical'), + ('solar_noon', 'solar_noon'), + ('sunrise', 'sunrise'), + ('sunset', 'sunset') + ], + max_length=24, verbose_name='event'), + ), + ] diff --git a/django_celery_beat/migrations/0006_auto_20180210_1226.py b/django_celery_beat/migrations/0006_auto_20180210_1226.py new file mode 100644 index 0000000..f7552e7 --- /dev/null +++ b/django_celery_beat/migrations/0006_auto_20180210_1226.py @@ -0,0 +1,30 @@ +# Generated by Django 2.0.1 on 2018-02-10 12:26 +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0005_add_solarschedule_events_choices'), + ] + + operations = [ + migrations.AlterField( + model_name='crontabschedule', + name='day_of_month', + field=models.CharField(default='*', max_length=124, + verbose_name='day of month'), + ), + migrations.AlterField( + model_name='crontabschedule', + name='hour', + field=models.CharField(default='*', max_length=96, + verbose_name='hour'), + ), + migrations.AlterField( + model_name='crontabschedule', + name='minute', + field=models.CharField(default='*', max_length=240, + verbose_name='minute'), + ), + ] diff --git a/django_celery_beat/migrations/0006_auto_20180322_0932.py b/django_celery_beat/migrations/0006_auto_20180322_0932.py new file mode 100644 index 0000000..9270b3d --- /dev/null +++ b/django_celery_beat/migrations/0006_auto_20180322_0932.py @@ -0,0 +1,51 @@ +# Generated by Django 1.11.7 on 2018-03-22 16:32 +from django.db import migrations, models +import timezone_field.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0005_add_solarschedule_events_choices'), + # ('django_celery_beat', '0006_auto_20180210_1226'), + ] + + operations = [ + migrations.AlterModelOptions( + name='crontabschedule', + options={ + 'ordering': [ + 'month_of_year', 'day_of_month', + 'day_of_week', 'hour', 'minute', 'timezone' + ], + 'verbose_name': 'crontab', + 'verbose_name_plural': 'crontabs' + }, + ), + migrations.AddField( + model_name='crontabschedule', + name='timezone', + field=timezone_field.fields.TimeZoneField(default='UTC'), + ), + migrations.AlterField( + model_name='crontabschedule', + name='day_of_month', + field=models.CharField( + default='*', max_length=124, verbose_name='day of month' + ), + ), + migrations.AlterField( + model_name='crontabschedule', + name='hour', + field=models.CharField( + default='*', max_length=96, verbose_name='hour' + ), + ), + migrations.AlterField( + model_name='crontabschedule', + name='minute', + field=models.CharField( + default='*', max_length=240, verbose_name='minute' + ), + ), + ] diff --git a/django_celery_beat/migrations/0006_periodictask_priority.py b/django_celery_beat/migrations/0006_periodictask_priority.py new file mode 100644 index 0000000..70c6ee8 --- /dev/null +++ b/django_celery_beat/migrations/0006_periodictask_priority.py @@ -0,0 +1,28 @@ +# Generated by Django 2.0.6 on 2018-10-22 05:20 +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + # depends on higher numbers due to a squashed migration + # that was later removed due to migration issues it caused + ('django_celery_beat', '0005_add_solarschedule_events_choices'), + ('django_celery_beat', '0006_auto_20180210_1226'), + ('django_celery_beat', '0006_auto_20180322_0932'), + ('django_celery_beat', '0007_auto_20180521_0826'), + ('django_celery_beat', '0008_auto_20180914_1922'), + ] + + operations = [ + migrations.AddField( + model_name='periodictask', + name='priority', + field=models.PositiveIntegerField( + blank=True, + default=None, + null=True, + validators=[django.core.validators.MaxValueValidator(255)], + verbose_name='priority'), + ), + ] diff --git a/django_celery_beat/migrations/0007_auto_20180521_0826.py b/django_celery_beat/migrations/0007_auto_20180521_0826.py new file mode 100644 index 0000000..4e234c1 --- /dev/null +++ b/django_celery_beat/migrations/0007_auto_20180521_0826.py @@ -0,0 +1,25 @@ +# Generated by Django 1.10.7 on 2018-05-21 08:26 +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'), + ), + ] diff --git a/django_celery_beat/migrations/0008_auto_20180914_1922.py b/django_celery_beat/migrations/0008_auto_20180914_1922.py new file mode 100644 index 0000000..0990f91 --- /dev/null +++ b/django_celery_beat/migrations/0008_auto_20180914_1922.py @@ -0,0 +1,57 @@ +# Generated by Django 2.0.3 on 2018-09-14 19:22 +from django.db import migrations, models +from django_celery_beat import validators + + +class Migration(migrations.Migration): + dependencies = [ + ('django_celery_beat', '0007_auto_20180521_0826'), + ] + + operations = [ + migrations.AlterField( + model_name='crontabschedule', + name='day_of_month', + field=models.CharField( + default='*', max_length=124, + validators=[validators.day_of_month_validator], + verbose_name='day of month' + ), + ), + migrations.AlterField( + model_name='crontabschedule', + name='day_of_week', + field=models.CharField( + default='*', max_length=64, + validators=[validators.day_of_week_validator], + verbose_name='day of week' + ), + ), + migrations.AlterField( + model_name='crontabschedule', + name='hour', + field=models.CharField( + default='*', max_length=96, + validators=[validators.hour_validator], + verbose_name='hour' + ), + ), + migrations.AlterField( + model_name='crontabschedule', + name='minute', + field=models.CharField( + default='*', max_length=240, + validators=[validators.minute_validator], + verbose_name='minute' + ), + ), + migrations.AlterField( + model_name='crontabschedule', + name='month_of_year', + field=models.CharField( + default='*', max_length=64, + validators=[validators.month_of_year_validator], + verbose_name='month of year' + ), + ), + ] diff --git a/django_celery_beat/migrations/0009_periodictask_headers.py b/django_celery_beat/migrations/0009_periodictask_headers.py new file mode 100644 index 0000000..4c7c664 --- /dev/null +++ b/django_celery_beat/migrations/0009_periodictask_headers.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.5 on 2019-02-09 19:33 +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0006_periodictask_priority'), + ] + + operations = [ + migrations.AddField( + model_name='periodictask', + name='headers', + field=models.TextField( + blank=True, + default='{}', + help_text='JSON encoded message headers', + verbose_name='Message headers' + ), + ), + ] diff --git a/django_celery_beat/migrations/0010_auto_20190429_0326.py b/django_celery_beat/migrations/0010_auto_20190429_0326.py new file mode 100644 index 0000000..ae948dd --- /dev/null +++ b/django_celery_beat/migrations/0010_auto_20190429_0326.py @@ -0,0 +1,174 @@ +# Generated by Django 1.11.20 on 2019-04-29 03:26 + +# this file is auto-generated so don't do flake8 on it +# flake8: noqa +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django_celery_beat.validators +import timezone_field.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0009_periodictask_headers'), + ] + + operations = [ + migrations.AlterField( + model_name='crontabschedule', + name='day_of_month', + field=models.CharField(default='*', help_text='Cron Days Of The Month to Run. Use "*" for "all". (Example: "1,15")', max_length=124, validators=[django_celery_beat.validators.day_of_month_validator], verbose_name='Day(s) Of The Month'), + ), + migrations.AlterField( + model_name='crontabschedule', + name='day_of_week', + field=models.CharField(default='*', help_text='Cron Days Of The Week to Run. Use "*" for "all". (Example: "0,5")', max_length=64, validators=[django_celery_beat.validators.day_of_week_validator], verbose_name='Day(s) Of The Week'), + ), + migrations.AlterField( + model_name='crontabschedule', + name='hour', + field=models.CharField(default='*', help_text='Cron Hours to Run. Use "*" for "all". (Example: "8,20")', max_length=96, validators=[django_celery_beat.validators.hour_validator], verbose_name='Hour(s)'), + ), + migrations.AlterField( + model_name='crontabschedule', + name='minute', + field=models.CharField(default='*', help_text='Cron Minutes to Run. Use "*" for "all". (Example: "0,30")', max_length=240, validators=[django_celery_beat.validators.minute_validator], verbose_name='Minute(s)'), + ), + migrations.AlterField( + model_name='crontabschedule', + name='month_of_year', + field=models.CharField(default='*', help_text='Cron Months Of The Year to Run. Use "*" for "all". (Example: "0,6")', max_length=64, validators=[django_celery_beat.validators.month_of_year_validator], verbose_name='Month(s) Of The Year'), + ), + migrations.AlterField( + model_name='crontabschedule', + name='timezone', + field=timezone_field.fields.TimeZoneField(default='UTC', help_text='Timezone to Run the Cron Schedule on. Default is UTC.', verbose_name='Cron Timezone'), + ), + migrations.AlterField( + model_name='intervalschedule', + name='every', + field=models.IntegerField(help_text='Number of interval periods to wait before running the task again', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Number of Periods'), + ), + migrations.AlterField( + model_name='intervalschedule', + name='period', + field=models.CharField(choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes'), ('seconds', 'Seconds'), ('microseconds', 'Microseconds')], help_text='The type of period between task runs (Example: days)', max_length=24, verbose_name='Interval Period'), + ), + migrations.AlterField( + model_name='periodictask', + name='args', + field=models.TextField(blank=True, default='[]', help_text='JSON encoded positional arguments (Example: ["arg1", "arg2"])', verbose_name='Positional Arguments'), + ), + migrations.AlterField( + model_name='periodictask', + name='crontab', + field=models.ForeignKey(blank=True, help_text='Crontab Schedule to run the task on. Set only one schedule type, leave the others null.', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.CrontabSchedule', verbose_name='Crontab Schedule'), + ), + migrations.AlterField( + model_name='periodictask', + name='date_changed', + field=models.DateTimeField(auto_now=True, help_text='Datetime that this PeriodicTask was last modified', verbose_name='Last Modified'), + ), + migrations.AlterField( + model_name='periodictask', + name='description', + field=models.TextField(blank=True, help_text='Detailed description about the details of this Periodic Task', verbose_name='Description'), + ), + migrations.AlterField( + model_name='periodictask', + name='enabled', + field=models.BooleanField(default=True, help_text='Set to False to disable the schedule', verbose_name='Enabled'), + ), + migrations.AlterField( + model_name='periodictask', + name='exchange', + field=models.CharField(blank=True, default=None, help_text='Override Exchange for low-level AMQP routing', max_length=200, null=True, verbose_name='Exchange'), + ), + migrations.AlterField( + model_name='periodictask', + name='expires', + field=models.DateTimeField(blank=True, help_text='Datetime after which the schedule will no longer trigger the task to run', null=True, verbose_name='Expires Datetime'), + ), + migrations.AlterField( + model_name='periodictask', + name='headers', + field=models.TextField(blank=True, default='{}', help_text='JSON encoded message headers for the AMQP message.', verbose_name='AMQP Message Headers'), + ), + migrations.AlterField( + model_name='periodictask', + name='interval', + field=models.ForeignKey(blank=True, help_text='Interval Schedule to run the task on. Set only one schedule type, leave the others null.', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.IntervalSchedule', verbose_name='Interval Schedule'), + ), + migrations.AlterField( + model_name='periodictask', + name='kwargs', + field=models.TextField(blank=True, default='{}', help_text='JSON encoded keyword arguments (Example: {"argument": "value"})', verbose_name='Keyword Arguments'), + ), + migrations.AlterField( + model_name='periodictask', + name='last_run_at', + field=models.DateTimeField(blank=True, editable=False, help_text='Datetime that the schedule last triggered the task to run. Reset to None if enabled is set to False.', null=True, verbose_name='Last Run Datetime'), + ), + migrations.AlterField( + model_name='periodictask', + name='name', + field=models.CharField(help_text='Short Description For This Task', max_length=200, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='periodictask', + name='one_off', + field=models.BooleanField(default=False, help_text='If True, the schedule will only run the task a single time', verbose_name='One-off Task'), + ), + migrations.AlterField( + model_name='periodictask', + name='priority', + field=models.PositiveIntegerField(blank=True, default=None, help_text='Priority Number between 0 and 255. Supported by: RabbitMQ, Redis (priority reversed, 0 is highest).', null=True, validators=[django.core.validators.MaxValueValidator(255)], verbose_name='Priority'), + ), + migrations.AlterField( + model_name='periodictask', + name='queue', + field=models.CharField(blank=True, default=None, help_text='Queue defined in CELERY_TASK_QUEUES. Leave None for default queuing.', max_length=200, null=True, verbose_name='Queue Override'), + ), + migrations.AlterField( + model_name='periodictask', + name='routing_key', + field=models.CharField(blank=True, default=None, help_text='Override Routing Key for low-level AMQP routing', max_length=200, null=True, verbose_name='Routing Key'), + ), + migrations.AlterField( + model_name='periodictask', + name='solar', + field=models.ForeignKey(blank=True, help_text='Solar Schedule to run the task on. Set only one schedule type, leave the others null.', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.SolarSchedule', verbose_name='Solar Schedule'), + ), + migrations.AlterField( + model_name='periodictask', + name='start_time', + field=models.DateTimeField(blank=True, help_text='Datetime when the schedule should begin triggering the task to run', null=True, verbose_name='Start Datetime'), + ), + migrations.AlterField( + model_name='periodictask', + name='task', + field=models.CharField(help_text='The Name of the Celery Task that Should be Run. (Example: "proj.tasks.import_contacts")', max_length=200, verbose_name='Task Name'), + ), + migrations.AlterField( + model_name='periodictask', + name='total_run_count', + field=models.PositiveIntegerField(default=0, editable=False, help_text='Running count of how many times the schedule has triggered the task', verbose_name='Total Run Count'), + ), + migrations.AlterField( + model_name='solarschedule', + name='event', + field=models.CharField(choices=[('dawn_astronomical', 'dawn_astronomical'), ('dawn_civil', 'dawn_civil'), ('dawn_nautical', 'dawn_nautical'), ('dusk_astronomical', 'dusk_astronomical'), ('dusk_civil', 'dusk_civil'), ('dusk_nautical', 'dusk_nautical'), ('solar_noon', 'solar_noon'), ('sunrise', 'sunrise'), ('sunset', 'sunset')], help_text='The type of solar event when the job should run', max_length=24, verbose_name='Solar Event'), + ), + migrations.AlterField( + model_name='solarschedule', + name='latitude', + field=models.DecimalField(decimal_places=6, help_text='Run the task when the event happens at this latitude', max_digits=9, validators=[django.core.validators.MinValueValidator(-90), django.core.validators.MaxValueValidator(90)], verbose_name='Latitude'), + ), + migrations.AlterField( + model_name='solarschedule', + name='longitude', + field=models.DecimalField(decimal_places=6, help_text='Run the task when the event happens at this longitude', max_digits=9, validators=[django.core.validators.MinValueValidator(-180), django.core.validators.MaxValueValidator(180)], verbose_name='Longitude'), + ), + ] diff --git a/django_celery_beat/migrations/0011_auto_20190508_0153.py b/django_celery_beat/migrations/0011_auto_20190508_0153.py new file mode 100644 index 0000000..d77e7e7 --- /dev/null +++ b/django_celery_beat/migrations/0011_auto_20190508_0153.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2 on 2019-05-08 01:53 +# flake8: noqa +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0010_auto_20190429_0326'), + ] + + operations = [ + migrations.CreateModel( + name='ClockedSchedule', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('clocked_time', models.DateTimeField(help_text='Run the task at clocked time', verbose_name='Clock Time')), + ('enabled', models.BooleanField(default=True, editable=False, help_text='Set to False to disable the schedule', verbose_name='Enabled')), + ], + options={ + 'verbose_name': 'clocked', + 'verbose_name_plural': 'clocked', + 'ordering': ['clocked_time'], + }, + ), + migrations.AddField( + model_name='periodictask', + name='clocked', + field=models.ForeignKey(blank=True, help_text='Clocked Schedule to run the task on. Set only one schedule type, leave the others null.', null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.ClockedSchedule', verbose_name='Clocked Schedule'), + ), + ] diff --git a/django_celery_beat/migrations/0012_periodictask_expire_seconds.py b/django_celery_beat/migrations/0012_periodictask_expire_seconds.py new file mode 100644 index 0000000..aab98bf --- /dev/null +++ b/django_celery_beat/migrations/0012_periodictask_expire_seconds.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-30 00:46 +# flake8: noqa +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0011_auto_20190508_0153'), + ] + + operations = [ + migrations.AddField( + model_name='periodictask', + name='expire_seconds', + field=models.PositiveIntegerField(blank=True, help_text='Timedelta with seconds which the schedule will no longer trigger the task to run', null=True, verbose_name='Expires timedelta with seconds'), + ), + ] diff --git a/django_celery_beat/migrations/0013_auto_20200609_0727.py b/django_celery_beat/migrations/0013_auto_20200609_0727.py new file mode 100644 index 0000000..eb9040d --- /dev/null +++ b/django_celery_beat/migrations/0013_auto_20200609_0727.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.6 on 2020-06-09 07:27 +# flake8: noqa +from django.db import migrations +import django_celery_beat.models +import timezone_field.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0012_periodictask_expire_seconds'), + ] + + operations = [ + migrations.AlterField( + model_name='crontabschedule', + name='timezone', + field=timezone_field.fields.TimeZoneField(default=django_celery_beat.models.crontab_schedule_celery_timezone, help_text='Timezone to Run the Cron Schedule on. Default is UTC.', verbose_name='Cron Timezone'), + ), + ] diff --git a/django_celery_beat/migrations/0014_remove_clockedschedule_enabled.py b/django_celery_beat/migrations/0014_remove_clockedschedule_enabled.py new file mode 100644 index 0000000..0ee02dd --- /dev/null +++ b/django_celery_beat/migrations/0014_remove_clockedschedule_enabled.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2019-08-30 00:46 +# flake8: noqa +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0013_auto_20200609_0727'), + ] + + operations = [ + migrations.RemoveField( + model_name='clockedschedule', + name='enabled', + ), + ] diff --git a/django_celery_beat/migrations/0015_edit_solarschedule_events_choices.py b/django_celery_beat/migrations/0015_edit_solarschedule_events_choices.py new file mode 100644 index 0000000..8ec6ae3 --- /dev/null +++ b/django_celery_beat/migrations/0015_edit_solarschedule_events_choices.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.6 on 2020-12-13 15:00 +# flake8: noqa +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0014_remove_clockedschedule_enabled'), + ] + + operations = [ + migrations.AlterField( + model_name='solarschedule', + name='event', + field=models.CharField(choices=[('dawn_astronomical', 'Astronomical dawn'), ('dawn_civil', 'Civil dawn'), ('dawn_nautical', 'Nautical dawn'), ('dusk_astronomical', 'Astronomical dusk'), ('dusk_civil', 'Civil dusk'), ('dusk_nautical', 'Nautical dusk'), ('solar_noon', 'Solar noon'), ('sunrise', 'Sunrise'), ('sunset', 'Sunset')], help_text='The type of solar event when the job should run', max_length=24, verbose_name='Solar Event'), + ), + ] diff --git a/django_celery_beat/migrations/__init__.py b/django_celery_beat/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_celery_beat/models.py b/django_celery_beat/models.py new file mode 100644 index 0000000..13095ee --- /dev/null +++ b/django_celery_beat/models.py @@ -0,0 +1,670 @@ +"""Database models.""" +from datetime import timedelta + +import timezone_field +from celery import current_app, schedules +from django.conf import settings +from django.core.exceptions import MultipleObjectsReturned, ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django.db.models import signals +from django.utils.translation import gettext_lazy as _ +from wagtail.admin.edit_handlers import ( + HelpPanel, + FieldPanel, + MultiFieldPanel, + ObjectList, + PageChooserPanel, + StreamFieldPanel, + TabbedInterface, +) + +from . import managers, validators +from .clockedschedule import clocked +from .forms import PeriodicTaskForm +from .tzcrontab import TzAwareCrontab +from .utils import make_aware, now + +DAYS = 'days' +HOURS = 'hours' +MINUTES = 'minutes' +SECONDS = 'seconds' +MICROSECONDS = 'microseconds' + +PERIOD_CHOICES = ( + (DAYS, _('Days')), + (HOURS, _('Hours')), + (MINUTES, _('Minutes')), + (SECONDS, _('Seconds')), + (MICROSECONDS, _('Microseconds')), +) + +SINGULAR_PERIODS = ( + (DAYS, _('Day')), + (HOURS, _('Hour')), + (MINUTES, _('Minute')), + (SECONDS, _('Second')), + (MICROSECONDS, _('Microsecond')), +) + +SOLAR_SCHEDULES = [ + ("dawn_astronomical", _("Astronomical dawn")), + ("dawn_civil", _("Civil dawn")), + ("dawn_nautical", _("Nautical dawn")), + ("dusk_astronomical", _("Astronomical dusk")), + ("dusk_civil", _("Civil dusk")), + ("dusk_nautical", _("Nautical dusk")), + ("solar_noon", _("Solar noon")), + ("sunrise", _("Sunrise")), + ("sunset", _("Sunset")), +] + + +def cronexp(field): + """Representation of cron expression.""" + return field and str(field).replace(' ', '') or '*' + + +def crontab_schedule_celery_timezone(): + """Return timezone string from Django settings `CELERY_TIMEZONE` variable. + + If is not defined or is not a valid timezone, return `"UTC"` instead. + """ + try: + CELERY_TIMEZONE = getattr( + settings, '%s_TIMEZONE' % current_app.namespace) + except AttributeError: + return 'UTC' + return CELERY_TIMEZONE if CELERY_TIMEZONE in [ + choice[0].zone for choice in timezone_field. + TimeZoneField.default_choices + ] else 'UTC' + + +class SolarSchedule(models.Model): + """Schedule following astronomical patterns. + + Example: to run every sunrise in New York City: + event='sunrise', latitude=40.7128, longitude=74.0060 + """ + + event = models.CharField( + max_length=24, choices=SOLAR_SCHEDULES, + verbose_name=_('Solar Event'), + help_text=_('The type of solar event when the job should run'), + ) + latitude = models.DecimalField( + max_digits=9, decimal_places=6, + verbose_name=_('Latitude'), + help_text=_('Run the task when the event happens at this latitude'), + validators=[MinValueValidator(-90), MaxValueValidator(90)], + ) + longitude = models.DecimalField( + max_digits=9, decimal_places=6, + verbose_name=_('Longitude'), + help_text=_('Run the task when the event happens at this longitude'), + validators=[MinValueValidator(-180), MaxValueValidator(180)], + ) + + class Meta: + """Table information.""" + + verbose_name = _('solar event') + verbose_name_plural = _('solar events') + ordering = ('event', 'latitude', 'longitude') + unique_together = ('event', 'latitude', 'longitude') + + @property + def schedule(self): + return schedules.solar(self.event, + self.latitude, + self.longitude, + nowfun=lambda: make_aware(now())) + + @classmethod + def from_schedule(cls, schedule): + spec = {'event': schedule.event, + 'latitude': schedule.lat, + 'longitude': schedule.lon} + + # we do not check for MultipleObjectsReturned exception here because + # the unique_together constraint safely prevents from duplicates + try: + return cls.objects.get(**spec) + except cls.DoesNotExist: + return cls(**spec) + + def __str__(self): + return '{0} ({1}, {2})'.format( + self.get_event_display(), + self.latitude, + self.longitude + ) + + +class IntervalSchedule(models.Model): + """Schedule executing on a regular interval. + + Example: execute every 2 days + every=2, period=DAYS + """ + + DAYS = DAYS + HOURS = HOURS + MINUTES = MINUTES + SECONDS = SECONDS + MICROSECONDS = MICROSECONDS + + PERIOD_CHOICES = PERIOD_CHOICES + + every = models.IntegerField( + null=False, + verbose_name=_('Number of Periods'), + help_text=_('Number of interval periods to wait before ' + 'running the task again'), + validators=[MinValueValidator(1)], + ) + period = models.CharField( + max_length=24, choices=PERIOD_CHOICES, + verbose_name=_('Interval Period'), + help_text=_('The type of period between task runs (Example: days)'), + ) + + class Meta: + """Table information.""" + + verbose_name = _('interval') + verbose_name_plural = _('intervals') + ordering = ['period', 'every'] + + @property + def schedule(self): + return schedules.schedule( + timedelta(**{self.period: self.every}), + nowfun=lambda: make_aware(now()) + ) + + @classmethod + def from_schedule(cls, schedule, period=SECONDS): + every = max(schedule.run_every.total_seconds(), 0) + try: + return cls.objects.get(every=every, period=period) + except cls.DoesNotExist: + return cls(every=every, period=period) + except MultipleObjectsReturned: + return cls.objects.filter(every=every, period=period).first() + + def __str__(self): + readable_period = None + if self.every == 1: + for period, _readable_period in SINGULAR_PERIODS: + if period == self.period: + readable_period = _readable_period.lower() + break + return _('every {}').format(readable_period) + for period, _readable_period in PERIOD_CHOICES: + if period == self.period: + readable_period = _readable_period.lower() + break + return _('every {} {}').format(self.every, readable_period) + + @property + def period_singular(self): + return self.period[:-1] + + +class ClockedSchedule(models.Model): + """clocked schedule.""" + + clocked_time = models.DateTimeField( + verbose_name=_('Clock Time'), + help_text=_('Run the task at clocked time'), + ) + + class Meta: + """Table information.""" + + verbose_name = _('clocked') + verbose_name_plural = _('clocked') + ordering = ['clocked_time'] + + def __str__(self): + return '{}'.format(self.clocked_time) + + @property + def schedule(self): + c = clocked(clocked_time=self.clocked_time) + return c + + @classmethod + def from_schedule(cls, schedule): + spec = {'clocked_time': schedule.clocked_time} + try: + return cls.objects.get(**spec) + except cls.DoesNotExist: + return cls(**spec) + except MultipleObjectsReturned: + return cls.objects.filter(**spec).first() + + +class CrontabSchedule(models.Model): + """Timezone Aware Crontab-like schedule. + + Example: Run every hour at 0 minutes for days of month 10-15 + minute="0", hour="*", day_of_week="*", + day_of_month="10-15", month_of_year="*" + """ + + # + # The worst case scenario for day of month is a list of all 31 day numbers + # '[1, 2, ..., 31]' which has a length of 115. Likewise, minute can be + # 0..59 and hour can be 0..23. Ensure we can accomodate these by allowing + # 4 chars for each value (what we save on 0-9 accomodates the []). + # We leave the other fields at their historical length. + # + minute = models.CharField( + max_length=60 * 4, default='*', + verbose_name=_('Minute(s)'), + help_text=_( + 'Cron Minutes to Run. Use "*" for "all". (Example: "0,30")'), + validators=[validators.minute_validator], + ) + hour = models.CharField( + max_length=24 * 4, default='*', + verbose_name=_('Hour(s)'), + help_text=_( + 'Cron Hours to Run. Use "*" for "all". (Example: "8,20")'), + validators=[validators.hour_validator], + ) + day_of_week = models.CharField( + max_length=64, default='*', + verbose_name=_('Day(s) Of The Week'), + help_text=_( + 'Cron Days Of The Week to Run. Use "*" for "all". ' + '(Example: "0,5")'), + validators=[validators.day_of_week_validator], + ) + day_of_month = models.CharField( + max_length=31 * 4, default='*', + verbose_name=_('Day(s) Of The Month'), + help_text=_( + 'Cron Days Of The Month to Run. Use "*" for "all". ' + '(Example: "1,15")'), + validators=[validators.day_of_month_validator], + ) + month_of_year = models.CharField( + max_length=64, default='*', + verbose_name=_('Month(s) Of The Year'), + help_text=_( + 'Cron Months Of The Year to Run. Use "*" for "all". ' + '(Example: "0,6")'), + validators=[validators.month_of_year_validator], + ) + + timezone = timezone_field.TimeZoneField( + default=crontab_schedule_celery_timezone, + verbose_name=_('Cron Timezone'), + help_text=_( + 'Timezone to Run the Cron Schedule on. Default is UTC.'), + ) + + class Meta: + """Table information.""" + + verbose_name = _('crontab') + verbose_name_plural = _('crontabs') + ordering = ['month_of_year', 'day_of_month', + 'day_of_week', 'hour', 'minute', 'timezone'] + + def __str__(self): + return '{0} {1} {2} {3} {4} (m/h/dM/MY/d) {5}'.format( + cronexp(self.minute), cronexp(self.hour), + cronexp(self.day_of_month), cronexp(self.month_of_year), + cronexp(self.day_of_week), str(self.timezone) + ) + + @property + def schedule(self): + crontab = schedules.crontab( + minute=self.minute, + hour=self.hour, + day_of_week=self.day_of_week, + day_of_month=self.day_of_month, + month_of_year=self.month_of_year, + ) + if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): + crontab = TzAwareCrontab( + minute=self.minute, + hour=self.hour, + day_of_week=self.day_of_week, + day_of_month=self.day_of_month, + month_of_year=self.month_of_year, + tz=self.timezone + ) + return crontab + + @classmethod + def from_schedule(cls, schedule): + spec = {'minute': schedule._orig_minute, + 'hour': schedule._orig_hour, + 'day_of_week': schedule._orig_day_of_week, + 'day_of_month': schedule._orig_day_of_month, + 'month_of_year': schedule._orig_month_of_year, + 'timezone': schedule.tz + } + try: + return cls.objects.get(**spec) + except cls.DoesNotExist: + return cls(**spec) + except MultipleObjectsReturned: + return cls.objects.filter(**spec).first() + + +class PeriodicTasks(models.Model): + """Helper table for tracking updates to periodic tasks. + + This stores a single row with ident=1. last_update is updated + via django signals whenever anything is changed in the PeriodicTask model. + Basically this acts like a DB data audit trigger. + Doing this so we also track deletions, and not just insert/update. + """ + + ident = models.SmallIntegerField(default=1, primary_key=True, unique=True) + last_update = models.DateTimeField(null=False) + + objects = managers.ExtendedManager() + + @classmethod + def changed(cls, instance, **kwargs): + if not instance.no_changes: + cls.update_changed() + + @classmethod + def update_changed(cls, **kwargs): + cls.objects.update_or_create(ident=1, defaults={'last_update': now()}) + + @classmethod + def last_change(cls): + try: + return cls.objects.get(ident=1).last_update + except cls.DoesNotExist: + pass + + +class PeriodicTask(models.Model): + """Model representing a periodic task.""" + base_form_class = PeriodicTaskForm + + name = models.CharField( + max_length=200, unique=True, + verbose_name=_('Name'), + help_text=_('Short Description For This Task'), + ) + task = models.CharField( + max_length=200, + verbose_name='Task Name', + help_text=_('The Name of the Celery Task that Should be Run. ' + '(Example: "proj.tasks.import_contacts")'), + ) + + # You can only set ONE of the following schedule FK's + # TODO: Redo this as a GenericForeignKey + interval = models.ForeignKey( + IntervalSchedule, on_delete=models.CASCADE, + null=True, blank=True, verbose_name=_('Interval Schedule'), + help_text=_('Interval Schedule to run the task on. ' + 'Set only one schedule type, leave the others null.'), + ) + crontab = models.ForeignKey( + CrontabSchedule, on_delete=models.CASCADE, null=True, blank=True, + verbose_name=_('Crontab Schedule'), + help_text=_('Crontab Schedule to run the task on. ' + 'Set only one schedule type, leave the others null.'), + ) + solar = models.ForeignKey( + SolarSchedule, on_delete=models.CASCADE, null=True, blank=True, + verbose_name=_('Solar Schedule'), + help_text=_('Solar Schedule to run the task on. ' + 'Set only one schedule type, leave the others null.'), + ) + clocked = models.ForeignKey( + ClockedSchedule, on_delete=models.CASCADE, null=True, blank=True, + verbose_name=_('Clocked Schedule'), + help_text=_('Clocked Schedule to run the task on. ' + 'Set only one schedule type, leave the others null.'), + ) + # TODO: use django's JsonField + args = models.TextField( + blank=True, default='[]', + verbose_name=_('Positional Arguments'), + help_text=_( + 'JSON encoded positional arguments ' + '(Example: ["arg1", "arg2"])'), + ) + kwargs = models.TextField( + blank=True, default='{}', + verbose_name=_('Keyword Arguments'), + help_text=_( + 'JSON encoded keyword arguments ' + '(Example: {"argument": "value"})'), + ) + + queue = models.CharField( + max_length=200, blank=True, null=True, default=None, + verbose_name=_('Queue Override'), + help_text=_( + 'Queue defined in CELERY_TASK_QUEUES. ' + 'Leave None for default queuing.'), + ) + + # you can use low-level AMQP routing options here, + # but you almost certaily want to leave these as None + # http://docs.celeryproject.org/en/latest/userguide/routing.html#exchanges-queues-and-routing-keys + exchange = models.CharField( + max_length=200, blank=True, null=True, default=None, + verbose_name=_('Exchange'), + help_text=_('Override Exchange for low-level AMQP routing'), + ) + routing_key = models.CharField( + max_length=200, blank=True, null=True, default=None, + verbose_name=_('Routing Key'), + help_text=_('Override Routing Key for low-level AMQP routing'), + ) + headers = models.TextField( + blank=True, default='{}', + verbose_name=_('AMQP Message Headers'), + help_text=_('JSON encoded message headers for the AMQP message.'), + ) + + priority = models.PositiveIntegerField( + default=None, validators=[MaxValueValidator(255)], + blank=True, null=True, + verbose_name=_('Priority'), + help_text=_( + 'Priority Number between 0 and 255. ' + 'Supported by: RabbitMQ, Redis (priority reversed, 0 is highest).') + ) + expires = models.DateTimeField( + blank=True, null=True, + verbose_name=_('Expires Datetime'), + help_text=_( + 'Datetime after which the schedule will no longer ' + 'trigger the task to run'), + ) + expire_seconds = models.PositiveIntegerField( + blank=True, null=True, + verbose_name=_('Expires timedelta with seconds'), + help_text=_( + 'Timedelta with seconds which the schedule will no longer ' + 'trigger the task to run'), + + ) + one_off = models.BooleanField( + default=False, + verbose_name=_('One-off Task'), + help_text=_( + 'If True, the schedule will only run the task a single time'), + ) + start_time = models.DateTimeField( + blank=True, null=True, + verbose_name=_('Start Datetime'), + help_text=_( + 'Datetime when the schedule should begin ' + 'triggering the task to run'), + ) + enabled = models.BooleanField( + default=True, + verbose_name=_('Enabled'), + help_text=_('Set to False to disable the schedule'), + ) + last_run_at = models.DateTimeField( + auto_now=False, auto_now_add=False, + editable=False, blank=True, null=True, + verbose_name=_('Last Run Datetime'), + help_text=_( + 'Datetime that the schedule last triggered the task to run. ' + 'Reset to None if enabled is set to False.'), + ) + total_run_count = models.PositiveIntegerField( + default=0, editable=False, + verbose_name=_('Total Run Count'), + help_text=_( + 'Running count of how many times the schedule ' + 'has triggered the task'), + ) + date_changed = models.DateTimeField( + auto_now=True, + verbose_name=_('Last Modified'), + help_text=_('Datetime that this PeriodicTask was last modified'), + ) + description = models.TextField( + blank=True, + verbose_name=_('Description'), + help_text=_( + 'Detailed description about the details of this Periodic Task'), + ) + + content_panels = [ + HelpPanel(_('Essa é a área de configuração de execução de tarefas assíncronas.')), + FieldPanel('name'), + FieldPanel('regtask'), + FieldPanel('task'), + FieldPanel('description'), + FieldPanel('args'), + FieldPanel('kwargs'), + FieldPanel('priority'), + FieldPanel('one_off'), + FieldPanel('enabled'), + ] + scheduler_panels = [ + FieldPanel('interval'), + FieldPanel('crontab'), + FieldPanel('solar'), + FieldPanel('clocked'), + ] + + edit_handler = TabbedInterface( + [ + ObjectList(content_panels, heading=_('Content')), + ObjectList(scheduler_panels, heading=_('Scheduler')), + ] + ) + + objects = managers.PeriodicTaskManager() + no_changes = False + + class Meta: + """Table information.""" + + verbose_name = _('periodic task') + verbose_name_plural = _('periodic tasks') + + def validate_unique(self, *args, **kwargs): + super().validate_unique(*args, **kwargs) + + schedule_types = ['interval', 'crontab', 'solar', 'clocked'] + selected_schedule_types = [s for s in schedule_types + if getattr(self, s)] + + if len(selected_schedule_types) == 0: + raise ValidationError( + 'One of clocked, interval, crontab, or solar ' + 'must be set.' + ) + + err_msg = 'Only one of clocked, interval, crontab, '\ + 'or solar must be set' + if len(selected_schedule_types) > 1: + error_info = {} + for selected_schedule_type in selected_schedule_types: + error_info[selected_schedule_type] = [err_msg] + raise ValidationError(error_info) + + # clocked must be one off task + if self.clocked and not self.one_off: + err_msg = 'clocked must be one off, one_off must set True' + raise ValidationError(err_msg) + + def save(self, *args, **kwargs): + self.exchange = self.exchange or None + self.routing_key = self.routing_key or None + self.queue = self.queue or None + self.headers = self.headers or None + if not self.enabled: + self.last_run_at = None + self._clean_expires() + self.validate_unique() + super().save(*args, **kwargs) + + def _clean_expires(self): + if self.expire_seconds is not None and self.expires: + raise ValidationError( + _('Only one can be set, in expires and expire_seconds') + ) + + @property + def expires_(self): + return self.expires or self.expire_seconds + + def __str__(self): + fmt = '{0.name}: {{no schedule}}' + if self.interval: + fmt = '{0.name}: {0.interval}' + if self.crontab: + fmt = '{0.name}: {0.crontab}' + if self.solar: + fmt = '{0.name}: {0.solar}' + if self.clocked: + fmt = '{0.name}: {0.clocked}' + return fmt.format(self) + + @property + def schedule(self): + if self.interval: + return self.interval.schedule + if self.crontab: + return self.crontab.schedule + if self.solar: + return self.solar.schedule + if self.clocked: + return self.clocked.schedule + + +signals.pre_delete.connect(PeriodicTasks.changed, sender=PeriodicTask) +signals.pre_save.connect(PeriodicTasks.changed, sender=PeriodicTask) +signals.pre_delete.connect( + PeriodicTasks.update_changed, sender=IntervalSchedule) +signals.post_save.connect( + PeriodicTasks.update_changed, sender=IntervalSchedule) +signals.post_delete.connect( + PeriodicTasks.update_changed, sender=CrontabSchedule) +signals.post_save.connect( + PeriodicTasks.update_changed, sender=CrontabSchedule) +signals.post_delete.connect( + PeriodicTasks.update_changed, sender=SolarSchedule) +signals.post_save.connect( + PeriodicTasks.update_changed, sender=SolarSchedule) +signals.post_delete.connect( + PeriodicTasks.update_changed, sender=ClockedSchedule) +signals.post_save.connect( + PeriodicTasks.update_changed, sender=ClockedSchedule) diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py new file mode 100644 index 0000000..e260686 --- /dev/null +++ b/django_celery_beat/schedulers.py @@ -0,0 +1,370 @@ +"""Beat Scheduler Implementation.""" +import datetime +import logging +import math + +from multiprocessing.util import Finalize + +from celery import current_app +from celery import schedules +from celery.beat import Scheduler, ScheduleEntry + +from celery.utils.log import get_logger +from celery.utils.time import maybe_make_aware +from kombu.utils.encoding import safe_str, safe_repr +from kombu.utils.json import dumps, loads + +from django.conf import settings +from django.db import transaction, close_old_connections +from django.db.utils import DatabaseError, InterfaceError +from django.core.exceptions import ObjectDoesNotExist + +from .models import ( + PeriodicTask, PeriodicTasks, + CrontabSchedule, IntervalSchedule, + SolarSchedule, ClockedSchedule +) +from .clockedschedule import clocked +from .utils import NEVER_CHECK_TIMEOUT + +# This scheduler must wake up more frequently than the +# regular of 5 minutes because it needs to take external +# changes to the schedule into account. +DEFAULT_MAX_INTERVAL = 5 # seconds + +ADD_ENTRY_ERROR = """\ +Cannot add entry %r to database schedule: %r. Contents: %r +""" + +logger = get_logger(__name__) +debug, info, warning = logger.debug, logger.info, logger.warning + + +class ModelEntry(ScheduleEntry): + """Scheduler entry taken from database row.""" + + model_schedules = ( + (schedules.crontab, CrontabSchedule, 'crontab'), + (schedules.schedule, IntervalSchedule, 'interval'), + (schedules.solar, SolarSchedule, 'solar'), + (clocked, ClockedSchedule, 'clocked') + ) + save_fields = ['last_run_at', 'total_run_count', 'no_changes'] + + def __init__(self, model, app=None): + """Initialize the model entry.""" + self.app = app or current_app._get_current_object() + self.name = model.name + self.task = model.task + try: + self.schedule = model.schedule + except model.DoesNotExist: + logger.error( + 'Disabling schedule %s that was removed from database', + self.name, + ) + self._disable(model) + try: + self.args = loads(model.args or '[]') + self.kwargs = loads(model.kwargs or '{}') + except ValueError as exc: + logger.exception( + 'Removing schedule %s for argument deseralization error: %r', + self.name, exc, + ) + self._disable(model) + + self.options = {} + for option in ['queue', 'exchange', 'routing_key', 'priority']: + value = getattr(model, option) + if value is None: + continue + self.options[option] = value + + if getattr(model, 'expires_', None): + self.options['expires'] = getattr(model, 'expires_') + + self.options['headers'] = loads(model.headers or '{}') + + self.total_run_count = model.total_run_count + self.model = model + + if not model.last_run_at: + model.last_run_at = self._default_now() + + self.last_run_at = model.last_run_at + + def _disable(self, model): + model.no_changes = True + model.enabled = False + model.save() + + def is_due(self): + if not self.model.enabled: + # 5 second delay for re-enable. + return schedules.schedstate(False, 5.0) + + # START DATE: only run after the `start_time`, if one exists. + if self.model.start_time is not None: + now = self._default_now() + if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): + now = maybe_make_aware(self._default_now()) + + if now < self.model.start_time: + # The datetime is before the start date - don't run. + # send a delay to retry on start_time + delay = math.ceil( + (self.model.start_time - now).total_seconds() + ) + return schedules.schedstate(False, delay) + + # 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.no_changes = False # Mark the model entry as changed + self.model.save() + # Don't recheck + return schedules.schedstate(False, NEVER_CHECK_TIMEOUT) + + # CAUTION: make_aware assumes settings.TIME_ZONE for naive datetimes, + # while maybe_make_aware assumes utc for naive datetimes + tz = self.app.timezone + last_run_at_in_tz = maybe_make_aware(self.last_run_at).astimezone(tz) + return self.schedule.is_due(last_run_at_in_tz) + + def _default_now(self): + # The PyTZ datetime must be localised for the Django-Celery-Beat + # scheduler to work. Keep in mind that timezone arithmatic + # with a localized timezone may be inaccurate. + if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): + now = self.app.now() + now = now.tzinfo.localize(now.replace(tzinfo=None)) + else: + # this ends up getting passed to maybe_make_aware, which expects + # all naive datetime objects to be in utc time. + now = datetime.datetime.utcnow() + return now + + def __next__(self): + self.model.last_run_at = self._default_now() + self.model.total_run_count += 1 + self.model.no_changes = True + return self.__class__(self.model) + next = __next__ # for 2to3 + + def save(self): + # Object may not be synchronized, so only + # change the fields we care about. + obj = type(self.model)._default_manager.get(pk=self.model.pk) + for field in self.save_fields: + setattr(obj, field, getattr(self.model, field)) + + obj.save() + + @classmethod + def to_model_schedule(cls, schedule): + for schedule_type, model_type, model_field in cls.model_schedules: + schedule = schedules.maybe_schedule(schedule) + if isinstance(schedule, schedule_type): + model_schedule = model_type.from_schedule(schedule) + model_schedule.save() + return model_schedule, model_field + raise ValueError( + 'Cannot convert schedule type {0!r} to model'.format(schedule)) + + @classmethod + def from_entry(cls, name, app=None, **entry): + return cls(PeriodicTask._default_manager.update_or_create( + name=name, defaults=cls._unpack_fields(**entry), + ), app=app) + + @classmethod + def _unpack_fields(cls, schedule, + args=None, kwargs=None, relative=None, options=None, + **entry): + model_schedule, model_field = cls.to_model_schedule(schedule) + entry.update( + {model_field: model_schedule}, + args=dumps(args or []), + kwargs=dumps(kwargs or {}), + **cls._unpack_options(**options or {}) + ) + return entry + + @classmethod + def _unpack_options(cls, queue=None, exchange=None, routing_key=None, + priority=None, headers=None, expire_seconds=None, + **kwargs): + return { + 'queue': queue, + 'exchange': exchange, + 'routing_key': routing_key, + 'priority': priority, + 'headers': dumps(headers or {}), + 'expire_seconds': expire_seconds, + } + + def __repr__(self): + return ''.format( + safe_str(self.name), self.task, safe_repr(self.args), + safe_repr(self.kwargs), self.schedule, + ) + + +class DatabaseScheduler(Scheduler): + """Database-backed Beat Scheduler.""" + + Entry = ModelEntry + Model = PeriodicTask + Changes = PeriodicTasks + + _schedule = None + _last_timestamp = None + _initial_read = True + _heap_invalidated = False + + def __init__(self, *args, **kwargs): + """Initialize the database scheduler.""" + self._dirty = set() + Scheduler.__init__(self, *args, **kwargs) + self._finalize = Finalize(self, self.sync, exitpriority=5) + self.max_interval = ( + kwargs.get('max_interval') + or self.app.conf.beat_max_loop_interval + or DEFAULT_MAX_INTERVAL) + + def setup_schedule(self): + self.install_default_entries(self.schedule) + self.update_from_dict(self.app.conf.beat_schedule) + + def all_as_schedule(self): + debug('DatabaseScheduler: Fetching database schedule') + s = {} + for model in self.Model.objects.enabled(): + try: + s[model.name] = self.Entry(model, app=self.app) + except ValueError: + pass + return s + + def schedule_changed(self): + try: + close_old_connections() + + # If MySQL is running with transaction isolation level + # REPEATABLE-READ (default), then we won't see changes done by + # other transactions until the current transaction is + # committed (Issue #41). + try: + transaction.commit() + except transaction.TransactionManagementError: + pass # not in transaction management. + + last, ts = self._last_timestamp, self.Changes.last_change() + except DatabaseError as exc: + logger.exception('Database gave error: %r', exc) + return False + except InterfaceError: + warning( + 'DatabaseScheduler: InterfaceError in schedule_changed(), ' + 'waiting to retry in next call...' + ) + return False + + try: + if ts and ts > (last if last else ts): + return True + finally: + self._last_timestamp = ts + return False + + def reserve(self, entry): + new_entry = next(entry) + # Need to store entry by name, because the entry may change + # in the mean time. + self._dirty.add(new_entry.name) + return new_entry + + def sync(self): + if logger.isEnabledFor(logging.DEBUG): + debug('Writing entries...') + _tried = set() + _failed = set() + try: + close_old_connections() + + while self._dirty: + name = self._dirty.pop() + try: + self.schedule[name].save() + _tried.add(name) + except (KeyError, ObjectDoesNotExist): + _failed.add(name) + except DatabaseError as exc: + logger.exception('Database error while sync: %r', exc) + except InterfaceError: + warning( + 'DatabaseScheduler: InterfaceError in sync(), ' + 'waiting to retry in next call...' + ) + finally: + # retry later, only for the failed ones + self._dirty |= _failed + + def update_from_dict(self, mapping): + s = {} + for name, entry_fields in mapping.items(): + try: + entry = self.Entry.from_entry(name, + app=self.app, + **entry_fields) + if entry.model.enabled: + s[name] = entry + + except Exception as exc: + logger.exception(ADD_ENTRY_ERROR, name, exc, entry_fields) + self.schedule.update(s) + + def install_default_entries(self, data): + entries = {} + if self.app.conf.result_expires: + entries.setdefault( + 'celery.backend_cleanup', { + 'task': 'celery.backend_cleanup', + 'schedule': schedules.crontab('0', '4', '*'), + 'options': {'expire_seconds': 12 * 3600}, + }, + ) + self.update_from_dict(entries) + + def schedules_equal(self, *args, **kwargs): + if self._heap_invalidated: + self._heap_invalidated = False + return False + return super().schedules_equal(*args, **kwargs) + + @property + def schedule(self): + initial = update = False + if self._initial_read: + debug('DatabaseScheduler: initial read') + initial = update = True + self._initial_read = False + elif self.schedule_changed(): + info('DatabaseScheduler: Schedule changed.') + update = True + + if update: + self.sync() + self._schedule = self.all_as_schedule() + # the schedule changed, invalidate the heap in Scheduler.tick + if not initial: + self._heap = [] + self._heap_invalidated = True + if logger.isEnabledFor(logging.DEBUG): + debug('Current schedule:\n%s', '\n'.join( + repr(entry) for entry in self._schedule.values()), + ) + return self._schedule diff --git a/django_celery_beat/templates/admin/djcelery/change_list.html b/django_celery_beat/templates/admin/djcelery/change_list.html new file mode 100644 index 0000000..20b269f --- /dev/null +++ b/django_celery_beat/templates/admin/djcelery/change_list.html @@ -0,0 +1,20 @@ +{% extends "admin/change_list.html" %} +{% load i18n %} + +{% block breadcrumbs %} + + {% if wrong_scheduler %} +
    +
  • + Periodic tasks won't be dispatched unless you set the + CELERYBEAT_SCHEDULER setting to + djcelery.schedulers.DatabaseScheduler, + or specify it using the -S option to celerybeat +
  • +
+ {% endif %} +{% endblock %} diff --git a/django_celery_beat/tzcrontab.py b/django_celery_beat/tzcrontab.py new file mode 100644 index 0000000..c60f668 --- /dev/null +++ b/django_celery_beat/tzcrontab.py @@ -0,0 +1,76 @@ +"""Timezone aware Cron schedule Implementation.""" +from celery import schedules + +from collections import namedtuple +from datetime import datetime +import pytz + + +schedstate = namedtuple('schedstate', ('is_due', 'next')) + + +class TzAwareCrontab(schedules.crontab): + """Timezone Aware Crontab.""" + + def __init__( + self, minute='*', hour='*', day_of_week='*', + day_of_month='*', month_of_year='*', tz=pytz.utc, app=None + ): + """Overwrite Crontab constructor to include a timezone argument.""" + self.tz = tz + + nowfun = self.nowfunc + + super().__init__( + minute=minute, hour=hour, day_of_week=day_of_week, + day_of_month=day_of_month, + month_of_year=month_of_year, nowfun=nowfun, app=app + ) + + def nowfunc(self): + return self.tz.normalize( + pytz.utc.localize(datetime.utcnow()) + ) + + def is_due(self, last_run_at): + """Calculate when the next run will take place. + + Return tuple of (is_due, next_time_to_check). + The last_run_at argument needs to be timezone aware. + + """ + # convert last_run_at to the schedule timezone + last_run_at = last_run_at.astimezone(self.tz) + + rem_delta = self.remaining_estimate(last_run_at) + rem = max(rem_delta.total_seconds(), 0) + due = rem == 0 + if due: + rem_delta = self.remaining_estimate(self.now()) + rem = max(rem_delta.total_seconds(), 0) + return schedstate(due, rem) + + # Needed to support pickling + def __repr__(self): + return """ + """.format(self) + + def __reduce__(self): + return (self.__class__, (self._orig_minute, + self._orig_hour, + self._orig_day_of_week, + self._orig_day_of_month, + self._orig_month_of_year, + self.tz), None) + + def __eq__(self, other): + if isinstance(other, schedules.crontab): + return (other.month_of_year == self.month_of_year + and other.day_of_month == self.day_of_month + and other.day_of_week == self.day_of_week + and other.hour == self.hour + and other.minute == self.minute + and other.tz == self.tz) + return NotImplemented diff --git a/django_celery_beat/urls.py b/django_celery_beat/urls.py new file mode 100644 index 0000000..be85495 --- /dev/null +++ b/django_celery_beat/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import task_run + +app_name = "django_celery_beat" +urlpatterns = [ + path("", view=task_run, name="task_run"), +] diff --git a/django_celery_beat/utils.py b/django_celery_beat/utils.py new file mode 100644 index 0000000..c19f4ed --- /dev/null +++ b/django_celery_beat/utils.py @@ -0,0 +1,48 @@ +"""Utilities.""" +# -- XXX This module must not use translation as that causes +# -- a recursive loader import! +from django.conf import settings +from django.utils import timezone + +is_aware = timezone.is_aware +# celery schedstate return None will make it not work +NEVER_CHECK_TIMEOUT = 100000000 + +# see Issue #222 +now_localtime = getattr(timezone, 'template_localtime', timezone.localtime) + + +def make_aware(value): + """Force datatime to have timezone information.""" + if getattr(settings, 'USE_TZ', False): + # naive datetimes are assumed to be in UTC. + if timezone.is_naive(value): + value = timezone.make_aware(value, timezone.utc) + # then convert to the Django configured timezone. + default_tz = timezone.get_default_timezone() + value = timezone.localtime(value, default_tz) + else: + # naive datetimes are assumed to be in local timezone. + if timezone.is_naive(value): + value = timezone.make_aware(value, timezone.get_default_timezone()) + return value + + +def now(): + """Return the current date and time.""" + if getattr(settings, 'USE_TZ', False): + return now_localtime(timezone.now()) + else: + return timezone.now() + + +def is_database_scheduler(scheduler): + """Return true if Celery is configured to use the db scheduler.""" + if not scheduler: + return False + from kombu.utils import symbol_by_name + from .schedulers import DatabaseScheduler + return ( + scheduler == 'django' + or issubclass(symbol_by_name(scheduler), DatabaseScheduler) + ) diff --git a/django_celery_beat/validators.py b/django_celery_beat/validators.py new file mode 100644 index 0000000..71c8fd3 --- /dev/null +++ b/django_celery_beat/validators.py @@ -0,0 +1,106 @@ +"""Validators.""" + +import crontab +from django.core.exceptions import ValidationError + + +class _CronSlices(crontab.CronSlices): + """Cron slices with customized validation.""" + + def __init__(self, *args): + super(crontab.CronSlices, self).__init__( + [_CronSlice(info) for info in crontab.S_INFO] + ) + self.special = None + self.setall(*args) + self.is_valid = self.is_self_valid + + @classmethod + def validate(cls, *args): + try: + cls(*args) + except Exception as e: + raise ValueError(e) + + +class _CronSlice(crontab.CronSlice): + """Cron slice with custom range parser.""" + + def get_range(self, *vrange): + ret = _CronRange(self, *vrange) + if ret.dangling is not None: + return [ret.dangling, ret] + return [ret] + + +class _CronRange(crontab.CronRange): + """Cron range parser class.""" + + # rewrite whole method to raise error on bad range + def parse(self, value): + if value.count('/') == 1: + value, seq = value.split('/') + try: + self.seq = self.slice.parse_value(seq) + except crontab.SundayError: + self.seq = 1 + value = "0-0" + if self.seq < 1 or self.seq > self.slice.max: + raise ValueError("Sequence can not be divided by zero or max") + if value.count('-') == 1: + vfrom, vto = value.split('-') + self.vfrom = self.slice.parse_value(vfrom, sunday=0) + try: + self.vto = self.slice.parse_value(vto) + except crontab.SundayError: + if self.vfrom == 1: + self.vfrom = 0 + else: + self.dangling = 0 + self.vto = self.slice.parse_value(vto, sunday=6) + if self.vto < self.vfrom: + raise ValueError("Bad range '{0.vfrom}-{0.vto}'".format(self)) + elif value == '*': + self.all() + else: + raise ValueError('Unknown cron range value "%s"' % value) + + +def crontab_validator(value): + """Validate crontab.""" + try: + _CronSlices.validate(value) + except ValueError as e: + raise ValidationError(e) + + +def minute_validator(value): + """Validate minutes crontab value.""" + _validate_crontab(value, 0) + + +def hour_validator(value): + """Validate hours crontab value.""" + _validate_crontab(value, 1) + + +def day_of_month_validator(value): + """Validate day of month crontab value.""" + _validate_crontab(value, 2) + + +def month_of_year_validator(value): + """Validate month crontab value.""" + _validate_crontab(value, 3) + + +def day_of_week_validator(value): + """Validate day of week crontab value.""" + _validate_crontab(value, 4) + + +def _validate_crontab(value, index): + tab = ['*'] * 5 + tab[index] = value + tab = ' '.join(tab) + crontab_validator(tab) diff --git a/django_celery_beat/views.py b/django_celery_beat/views.py new file mode 100644 index 0000000..fd09c04 --- /dev/null +++ b/django_celery_beat/views.py @@ -0,0 +1,30 @@ +import json +from celery import current_app +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext as _ +from wagtail.admin import messages + +from django_celery_beat import models + + +def task_run(request): + """ + View funciton to run the task by PeriodicTask id. + """ + + task_id = int(request.GET.get("task_id", None)) + + p_task = get_object_or_404(models.PeriodicTask, pk=task_id) + + current_app.loader.import_default_modules() + + task = current_app.tasks.get(p_task.task) + + task.apply_async(args=json.loads(p_task.args), + kwargs=json.loads(p_task.kwargs), + queue=p_task.queue, + periodic_task_name=p_task.name) + + messages.success(request, _("Task {0} was successfully run").format(p_task.name)) + + return redirect(request.META.get('HTTP_REFERER')) diff --git a/django_celery_beat/wagtail_hooks.py b/django_celery_beat/wagtail_hooks.py new file mode 100644 index 0000000..64be9a8 --- /dev/null +++ b/django_celery_beat/wagtail_hooks.py @@ -0,0 +1,187 @@ + +from celery import current_app +from django.conf import settings +from django.contrib import messages +from django.db.models import Case, Value, When +from django.template.defaultfilters import pluralize +from django.urls import include, path +from django.utils.translation import gettext_lazy as _ +from kombu.utils.json import loads +from wagtail.contrib.modeladmin.options import ( + ModelAdmin, + ModelAdminGroup, + modeladmin_register, +) +from wagtail.core import hooks + +from django_celery_beat.models import ( + ClockedSchedule, + CrontabSchedule, + IntervalSchedule, + PeriodicTask, + PeriodicTasks, + SolarSchedule, +) +from django_celery_beat.utils import is_database_scheduler + +from .button_helper import PeriodicTaskHelper + + + +class PeriodicTaskAdmin(ModelAdmin): + """Admin-interface for periodic tasks.""" + + button_helper_class = PeriodicTaskHelper + model = PeriodicTask + menu_icon = 'cog' + celery_app = current_app + date_hierarchy = 'start_time' + list_display = ('__str__', 'enabled', 'interval', 'start_time', + 'last_run_at', 'one_off') + list_filter = ['enabled', 'one_off', 'task',] + actions = ('enable_tasks', 'disable_tasks', 'toggle_tasks', 'run_tasks') + search_fields = ('name',) + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + scheduler = getattr(settings, 'CELERYBEAT_SCHEDULER', None) + extra_context['wrong_scheduler'] = not is_database_scheduler(scheduler) + return super(PeriodicTaskAdmin, self).changelist_view( + request, extra_context) + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related('interval', 'crontab', 'solar', 'clocked') + + def _message_user_about_update(self, request, rows_updated, verb): + """Send message about action to user. + `verb` should shortly describe what have changed (e.g. 'enabled'). + """ + self.message_user( + request, + _('{0} task{1} {2} successfully {3}').format( + rows_updated, + pluralize(rows_updated), + pluralize(rows_updated, _('was,were')), + verb, + ), + ) + + def enable_tasks(self, request, queryset): + rows_updated = queryset.update(enabled=True) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, 'enabled') + enable_tasks.short_description = _('Enable selected tasks') + + def disable_tasks(self, request, queryset): + rows_updated = queryset.update(enabled=False, last_run_at=None) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, 'disabled') + disable_tasks.short_description = _('Disable selected tasks') + + def _toggle_tasks_activity(self, queryset): + return queryset.update(enabled=Case( + When(enabled=True, then=Value(False)), + default=Value(True), + )) + + def toggle_tasks(self, request, queryset): + rows_updated = self._toggle_tasks_activity(queryset) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, 'toggled') + toggle_tasks.short_description = _('Toggle activity of selected tasks') + + def run_tasks(self, request, queryset): + self.celery_app.loader.import_default_modules() + tasks = [(self.celery_app.tasks.get(task.task), + loads(task.args), + loads(task.kwargs), + task.queue, + task.name) + for task in queryset] + + if any(t[0] is None for t in tasks): + for i, t in enumerate(tasks): + if t[0] is None: + break + + # variable "i" will be set because list "tasks" is not empty + not_found_task_name = queryset[i].task + + self.message_user( + request, + _('task "{0}" not found'.format(not_found_task_name)), + level=messages.ERROR, + ) + return + + task_ids = [ + task.apply_async(args=args, kwargs=kwargs, queue=queue, + periodic_task_name=periodic_task_name) + if queue and len(queue) + else task.apply_async(args=args, kwargs=kwargs, + periodic_task_name=periodic_task_name) + for task, args, kwargs, queue, periodic_task_name in tasks + ] + tasks_run = len(task_ids) + self.message_user( + request, + _('{0} task{1} {2} successfully run').format( + tasks_run, + pluralize(tasks_run), + pluralize(tasks_run, _('was,were')), + ), + ) + run_tasks.short_description = _('Run selected tasks') + + +class ClockedScheduleAdmin(ModelAdmin): + """Admin-interface for clocked schedules.""" + menu_icon = 'time' + model = ClockedSchedule + + fields = ( + 'clocked_time', + ) + list_display = ( + 'clocked_time', + ) + + +class IntervalScheduleAdmin(ModelAdmin): + """Admin-interface for clocked schedules.""" + menu_icon = 'date' + model = IntervalSchedule + + +class CrontabScheduleAdmin(ModelAdmin): + """Admin-interface for clocked schedules.""" + + menu_icon = 'date' + model = CrontabSchedule + + +class SolarScheduleAdmin(ModelAdmin): + """Admin-interface for clocked schedules.""" + + menu_icon = 'date' + model = SolarSchedule + + +class TasksModelsAdminGroup(ModelAdminGroup): + menu_label = _('Tasks') + menu_icon = 'cogs' + menu_order = 1000 + items = (PeriodicTaskAdmin, CrontabScheduleAdmin, IntervalScheduleAdmin, + ClockedScheduleAdmin, SolarScheduleAdmin) + + +modeladmin_register(TasksModelsAdminGroup) + + +@hooks.register('register_admin_urls') +def register_task_url(): + return [ + path('django_celery_beat/tasks/', + include('django_celery_beat.urls', namespace='django_celery_beat')), + ] From a15f863fbed3874c5615c8e3bbfae66a1631aa11 Mon Sep 17 00:00:00 2001 From: Roberta Takenaka Date: Thu, 1 Sep 2022 19:00:37 -0300 Subject: [PATCH 2/2] =?UTF-8?q?Acrescenta=20configura=C3=A7=C3=B5es=20para?= =?UTF-8?q?=20DJANGO=20CELERY=20BEAT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings/base.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/config/settings/base.py b/config/settings/base.py index 9074fad..da5c250 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -111,6 +111,7 @@ LOCAL_APPS = [ "core.users", "core_settings", + "django_celery_beat", # Your stuff: custom apps go here ] @@ -311,9 +312,17 @@ CELERY_TASK_TIME_LIMIT = 5 * 60 # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit # TODO: set to whatever value is adequate in your circumstances -CELERY_TASK_SOFT_TIME_LIMIT = 60 +CELERY_TASK_SOFT_TIME_LIMIT = 36000 # http://docs.celeryproject.org/en/latest/userguide/configuration.html#beat-scheduler CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" + +# Celery Results +# ------------------------------------------------------------------------------ +# https: // django-celery-results.readthedocs.io/en/latest/getting_started.html +CELERY_RESULT_BACKEND = 'django-db' +CELERY_CACHE_BACKEND = 'django-cache' +CELERY_RESULT_EXTENDED = True + # django-allauth # ------------------------------------------------------------------------------ ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True)