[Python-modules-commits] [python-django-celery-beat] 03/16: New upstream version 1.1.0

Michael Fladischer fladi at moszumanska.debian.org
Fri Nov 24 11:09:07 UTC 2017


This is an automated email from the git hooks/post-receive script.

fladi pushed a commit to branch debian/master
in repository python-django-celery-beat.

commit 2ec8b7e702fa7a48ad01ab3a06ffed2db0460f89
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date:   Mon Nov 13 12:39:37 2017 +0100

    New upstream version 1.1.0
---
 .bumpversion.cfg                                   |   2 +-
 .travis.yml                                        |   1 +
 AUTHORS                                            |   3 +
 Changelog                                          |  21 +++
 README.rst                                         |  39 ++--
 django_celery_beat/__init__.py                     |   4 +-
 django_celery_beat/admin.py                        |  67 ++++++-
 django_celery_beat/apps.py                         |   4 +-
 .../migrations/0002_auto_20161118_0346.py          |  52 ++++++
 .../migrations/0003_auto_20161209_0049.py          |  26 +++
 .../migrations/0004_auto_20170221_0000.py          |  22 +++
 django_celery_beat/models.py                       | 109 +++++++++--
 django_celery_beat/schedulers.py                   |  19 +-
 django_celery_beat/utils.py                        |   5 +-
 docs/includes/installation.txt                     |   6 -
 docs/includes/introduction.txt                     |  16 +-
 requirements/default.txt                           |   1 +
 requirements/test-django111.txt                    |   1 +
 setup.py                                           |   6 +-
 t/proj/celery.py                                   |   2 +-
 t/proj/settings.py                                 |   9 +-
 t/unit/test_schedulers.py                          | 200 ++++++++++++++++++++-
 tox.ini                                            |   7 +-
 23 files changed, 549 insertions(+), 73 deletions(-)

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index f97eab0..4bf3197 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 1.0.1
+current_version = 1.1.0
 commit = True
 tag = True
 parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?P<releaselevel>[a-z]+)?
diff --git a/.travis.yml b/.travis.yml
index 7128256..30aba81 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,6 +27,7 @@ env:
     - TOXENV=pydocstyle
     - TOXENV=cov
 install: travis_retry pip install -U tox
+services: rabbitmq
 script: tox -v -- -v
 after_success:
   - .tox/$TRAVIS_PYTHON_VERSION/bin/coverage xml
diff --git a/AUTHORS b/AUTHORS
index e8a5c4d..ef97528 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -46,6 +46,8 @@ Jeffrey Hu <zhiwehu at gmail.com>
 Jens Alm <jens.alm at mac.com>
 Jerzy Kozera <jerzy.kozera at gmail.com>
 Jesper Noehr <jesper at noehr.org>
+Jimmy Bradshaw <james.g.bradshaw at gmail.com>
+Joey Wilhelm <tarkatronic at gmail.com>
 John Andrews <johna at stjit011.(none)>
 John Watson <johnw at mahalo.com>
 Jonas Haag <jonas at lophus.org>
@@ -57,6 +59,7 @@ Justin Quick <justquick at gmail.com>
 Keith Perkins <keith at tasteoftheworld.us>
 Kirill Panshin <kipanshi at gmail.com>
 Mark Hellewell <mark.hellewell at gmail.com>
+Mark Heppner <mheppner at users.noreply.github.com>
 Mark Lavin <markdlavin at gmail.com>
 Mark Stover <stovenator at gmail.com>
 Maxim Bodyansky <bodyansky at gmail.com>
diff --git a/Changelog b/Changelog
index 5ec084a..235c483 100644
--- a/Changelog
+++ b/Changelog
@@ -4,6 +4,27 @@
  Change history
 ================
 
+.. _version-1.1.0:
+
+1.1.0
+=====
+:release-date: 2017-10-31 2:30 p.m. UTC+3:00
+:release-by: Omer Katz
+
+- Adds default_app_config (Issue celery/celery#3567)
+- Adds "run now" admin action for tasks.
+- Adds admin actions to toggle tasks.
+- Add solar schedules (Issue #8)
+- Notify beat of changes when Interval/Crontab models change. (Issue celery/celery#3683)
+- Fix PeriodicTask.enable sync issues
+- Notify beat of changes when Solar model changes.
+- Resolve CSS class conflict with django-adminlte2 package.
+- We now support Django 1.11
+- Deletes are now performed cascadingly.
+- Return schedule for solar periodic tasks so that Celery Beat does not crash when one is scheduled.
+- Adding nowfun to solar and crontab schedulers so that the Django timezone is used.
+
+
 .. _version-1.0.1:
 
 1.0.1
diff --git a/README.rst b/README.rst
index 49e07c5..74cce77 100644
--- a/README.rst
+++ b/README.rst
@@ -4,7 +4,7 @@
 
 |build-status| |coverage| |license| |wheel| |pyversion| |pyimp|
 
-:Version: 1.0.1
+:Version: 1.1.0
 :Web: http://django-celery-beat.readthedocs.io/
 :Download: http://pypi.python.org/pypi/django-celery-beat
 :Source: http://github.com/celery/django-celery-beat
@@ -19,10 +19,10 @@ database.
 The periodic tasks can be managed from the Django Admin interface, where you
 can create, edit and delete periodic tasks and how often they should run.
 
-Installing
-==========
+Using the Extension
+===================
 
-The installation instructions for this extension is available
+Usage and installation instructions for this extension are available
 from the `Celery documentation`_:
 
 http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes
@@ -31,23 +31,24 @@ http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-cust
 .. _`Celery documentation`:
     http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes
 
-Important Warning
-=================
+Important Warning about Time Zones
+==================================
 
-.. admonition:: Timezones
+.. warning::
 
-If you change the Django ``TIME_ZONE`` setting your periodic task schedule
-will still be based on the old timezone.
+    If you change the Django ``TIME_ZONE`` setting your periodic task schedule
+    will still be based on the old timezone.
 
-To fix that you would have to reset the "last run time" for each periodic
-task::
+    To fix that you would have to reset the "last run time" for each periodic
+    task::
 
-    >>> from django_celery_beat import PeriodicTask, PeriodicTasks
-    >>> PeriodicTask.objects.all().update(last_run_at=None)
-    >>> PeriodicTasks.changed()
+        >>> from django_celery_beat.models import PeriodicTask, PeriodicTasks
+        >>> PeriodicTask.objects.all().update(last_run_at=None)
+        >>> for task in PeriodicTask.objects.all():
+        >>>     PeriodicTasks.changed(task)
 
-Note that this will reset the state as if the periodic tasks have never run
-before.
+    Note that this will reset the state as if the periodic tasks have never run
+    before.
 
 Models
 ======
@@ -189,8 +190,6 @@ You can use the ``enabled`` flag to temporarily disable a periodic task::
     >>> periodic_task.enabled = False
     >>> periodic_task.save()
 
-.. _installation:
-
 Installation
 ============
 
@@ -201,8 +200,6 @@ To install using `pip`,::
 
     $ pip install -U django-celery-beat
 
-.. _installing-from-source:
-
 Downloading and installing from source
 --------------------------------------
 
@@ -219,8 +216,6 @@ You can install it by doing the following,::
 The last command must be executed as a privileged user if
 you are not currently using a virtualenv.
 
-.. _installing-from-git:
-
 Using the development version
 -----------------------------
 
diff --git a/django_celery_beat/__init__.py b/django_celery_beat/__init__.py
index 15d021f..b72ffb3 100644
--- a/django_celery_beat/__init__.py
+++ b/django_celery_beat/__init__.py
@@ -10,7 +10,7 @@ import re
 
 from collections import namedtuple
 
-__version__ = '1.0.1'
+__version__ = '1.1.0'
 __author__ = 'Ask Solem'
 __contact__ = 'ask at celeryproject.org'
 __homepage__ = 'https://github.com/celery/django-celery-beat'
@@ -32,3 +32,5 @@ del(_temp)
 del(re)
 
 __all__ = []
+
+default_app_config = 'django_celery_beat.apps.BeatConfig'
diff --git a/django_celery_beat/admin.py b/django_celery_beat/admin.py
index 08b4d85..df67fcb 100644
--- a/django_celery_beat/admin.py
+++ b/django_celery_beat/admin.py
@@ -5,13 +5,18 @@ from django import forms
 from django.conf import settings
 from django.contrib import admin
 from django.forms.widgets import Select
+from django.template.defaultfilters import pluralize
 from django.utils.translation import ugettext_lazy as _
 
 from celery import current_app
 from celery.utils import cached_property
 from kombu.utils.json import loads
 
-from .models import PeriodicTask, IntervalSchedule, CrontabSchedule
+from .models import (
+    PeriodicTask, PeriodicTasks,
+    IntervalSchedule, CrontabSchedule,
+    SolarSchedule
+)
 from .utils import is_database_scheduler
 
 try:
@@ -106,27 +111,29 @@ class PeriodicTaskForm(forms.ModelForm):
 
 
 class PeriodicTaskAdmin(admin.ModelAdmin):
-    """Admin-interface for peridic tasks."""
+    """Admin-interface for periodic tasks."""
 
     form = PeriodicTaskForm
     model = PeriodicTask
+    celery_app = current_app
     list_display = ('__str__', 'enabled')
+    actions = ('enable_tasks', 'disable_tasks', 'run_tasks')
     fieldsets = (
         (None, {
             'fields': ('name', 'regtask', 'task', 'enabled'),
             'classes': ('extrapretty', 'wide'),
         }),
         ('Schedule', {
-            'fields': ('interval', 'crontab'),
+            'fields': ('interval', 'crontab', 'solar'),
             'classes': ('extrapretty', 'wide', ),
         }),
         ('Arguments', {
             'fields': ('args', 'kwargs'),
-            'classes': ('extrapretty', 'wide', 'collapse'),
+            'classes': ('extrapretty', 'wide', 'collapse', 'in'),
         }),
         ('Execution Options', {
             'fields': ('expires', 'queue', 'exchange', 'routing_key'),
-            'classes': ('extrapretty', 'wide', 'collapse'),
+            'classes': ('extrapretty', 'wide', 'collapse', 'in'),
         }),
     )
 
@@ -139,7 +146,55 @@ class PeriodicTaskAdmin(admin.ModelAdmin):
 
     def get_queryset(self, request):
         qs = super(PeriodicTaskAdmin, self).get_queryset(request)
-        return qs.select_related('interval', 'crontab')
+        return qs.select_related('interval', 'crontab', 'solar')
+
+    def enable_tasks(self, request, queryset):
+        rows_updated = queryset.update(enabled=True)
+        PeriodicTasks.update_changed()
+        self.message_user(
+            request,
+            _('{0} task{1} {2} successfully enabled').format(
+                rows_updated,
+                pluralize(rows_updated),
+                pluralize(rows_updated, _('was,were')),
+            ),
+        )
+    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(
+            request,
+            _('{0} task{1} {2} successfully disabled').format(
+                rows_updated,
+                pluralize(rows_updated),
+                pluralize(rows_updated, _('was,were')),
+            ),
+        )
+    disable_tasks.short_description = _('Disable 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))
+                 for task in queryset]
+        task_ids = [task.delay(*args, **kwargs)
+                    for task, args, kwargs 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')
+
+
 admin.site.register(IntervalSchedule)
 admin.site.register(CrontabSchedule)
+admin.site.register(SolarSchedule)
 admin.site.register(PeriodicTask, PeriodicTaskAdmin)
diff --git a/django_celery_beat/apps.py b/django_celery_beat/apps.py
index b4253c3..4856a69 100644
--- a/django_celery_beat/apps.py
+++ b/django_celery_beat/apps.py
@@ -11,5 +11,5 @@ class BeatConfig(AppConfig):
     """Default configuration for django_celery_beat app."""
 
     name = 'django_celery_beat'
-    label = 'beat'
-    verbose_name = _('Beat')
+    label = 'django_celery_beat'
+    verbose_name = _('Periodic Tasks')
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..6560473
--- /dev/null
+++ b/django_celery_beat/migrations/0002_auto_20161118_0346.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2016-11-18 03:46
+from __future__ import absolute_import, unicode_literals
+
+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..d403b35
--- /dev/null
+++ b/django_celery_beat/migrations/0003_auto_20161209_0049.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.11 on 2016-12-09 00:49
+from __future__ import absolute_import, unicode_literals
+
+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..2c6f2a8
--- /dev/null
+++ b/django_celery_beat/migrations/0004_auto_20170221_0000.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+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/models.py b/django_celery_beat/models.py
index 211ec24..ad7d77d 100644
--- a/django_celery_beat/models.py
+++ b/django_celery_beat/models.py
@@ -12,7 +12,7 @@ from celery import schedules
 from celery.five import python_2_unicode_compatible
 
 from . import managers
-from .utils import now
+from .utils import now, make_aware
 
 DAYS = 'days'
 HOURS = 'hours'
@@ -28,6 +28,8 @@ PERIOD_CHOICES = (
     (MICROSECONDS, _('Microseconds')),
 )
 
+SOLAR_SCHEDULES = [(x, _(x)) for x in schedules.solar._all_events]
+
 
 def cronexp(field):
     """Representation of cron expression."""
@@ -35,6 +37,56 @@ def cronexp(field):
 
 
 @python_2_unicode_compatible
+class SolarSchedule(models.Model):
+    """Schedule following astronomical patterns."""
+
+    event = models.CharField(
+        _('event'), max_length=24, choices=SOLAR_SCHEDULES
+    )
+    latitude = models.DecimalField(
+        _('latitude'), max_digits=9, decimal_places=6
+    )
+    longitude = models.DecimalField(
+        _('longitude'), max_digits=9, decimal_places=6
+    )
+
+    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}
+        try:
+            return cls.objects.get(**spec)
+        except cls.DoesNotExist:
+            return cls(**spec)
+        except MultipleObjectsReturned:
+            cls.objects.filter(**spec).delete()
+            return cls(**spec)
+
+    def __str__(self):
+        return '{0} ({1}, {2})'.format(
+            self.get_event_display(),
+            self.latitude,
+            self.longitude
+        )
+
+
+ at python_2_unicode_compatible
 class IntervalSchedule(models.Model):
     """Schedule executing every n seconds."""
 
@@ -122,7 +174,8 @@ class CrontabSchedule(models.Model):
                                  hour=self.hour,
                                  day_of_week=self.day_of_week,
                                  day_of_month=self.day_of_month,
-                                 month_of_year=self.month_of_year)
+                                 month_of_year=self.month_of_year,
+                                 nowfun=lambda: make_aware(now()))
 
     @classmethod
     def from_schedule(cls, schedule):
@@ -151,8 +204,11 @@ class PeriodicTasks(models.Model):
     @classmethod
     def changed(cls, instance, **kwargs):
         if not instance.no_changes:
-            cls.objects.update_or_create(ident=1,
-                                         defaults={'last_update': now()})
+            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):
@@ -172,12 +228,16 @@ class PeriodicTask(models.Model):
     )
     task = models.CharField(_('task name'), max_length=200)
     interval = models.ForeignKey(
-        IntervalSchedule,
+        IntervalSchedule, on_delete=models.CASCADE,
         null=True, blank=True, verbose_name=_('interval'),
     )
     crontab = models.ForeignKey(
-        CrontabSchedule, null=True, blank=True, verbose_name=_('crontab'),
-        help_text=_('Use one of interval/crontab'),
+        CrontabSchedule, on_delete=models.CASCADE, null=True, blank=True,
+        verbose_name=_('crontab'), help_text=_('Use one of interval/crontab'),
+    )
+    solar = models.ForeignKey(
+        SolarSchedule, on_delete=models.CASCADE, null=True, blank=True,
+        verbose_name=_('solar'), help_text=_('Use a solar schedule')
     )
     args = models.TextField(
         _('Arguments'), blank=True, default='[]',
@@ -224,12 +284,18 @@ class PeriodicTask(models.Model):
 
     def validate_unique(self, *args, **kwargs):
         super(PeriodicTask, self).validate_unique(*args, **kwargs)
-        if not self.interval and not self.crontab:
-            raise ValidationError(
-                {'interval': ['One of interval or crontab must be set.']})
-        if self.interval and self.crontab:
-            raise ValidationError(
-                {'crontab': ['Only one of interval or crontab must be set']})
+        if not self.interval and not self.crontab and not self.solar:
+            raise ValidationError({
+                'interval': [
+                    'One of interval, crontab, or solar must be set.'
+                ]
+            })
+        if self.interval and self.crontab and self.solar:
+            raise ValidationError({
+                'crontab': [
+                    'Only one of interval, crontab, or solar must be set'
+                ]
+            })
 
     def save(self, *args, **kwargs):
         self.exchange = self.exchange or None
@@ -245,6 +311,8 @@ class PeriodicTask(models.Model):
             fmt = '{0.name}: {0.interval}'
         if self.crontab:
             fmt = '{0.name}: {0.crontab}'
+        if self.solar:
+            fmt = '{0.name}: {0.solar}'
         return fmt.format(self)
 
     @property
@@ -253,6 +321,21 @@ class PeriodicTask(models.Model):
             return self.interval.schedule
         if self.crontab:
             return self.crontab.schedule
+        if self.solar:
+            return self.solar.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)
diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py
index a285f3e..0a8b0a2 100644
--- a/django_celery_beat/schedulers.py
+++ b/django_celery_beat/schedulers.py
@@ -20,6 +20,7 @@ from django.core.exceptions import ObjectDoesNotExist
 from .models import (
     PeriodicTask, PeriodicTasks,
     CrontabSchedule, IntervalSchedule,
+    SolarSchedule,
 )
 from .utils import make_aware
 
@@ -34,7 +35,7 @@ except ImportError:  # pragma: no cover
 DEFAULT_MAX_INTERVAL = 5  # seconds
 
 ADD_ENTRY_ERROR = """\
-Couldn't add entry %r to database schedule: %r. Contents: %r
+Cannot add entry %r to database schedule: %r. Contents: %r
 """
 
 logger = get_logger(__name__)
@@ -47,6 +48,7 @@ class ModelEntry(ScheduleEntry):
     model_schedules = (
         (schedules.crontab, CrontabSchedule, 'crontab'),
         (schedules.schedule, IntervalSchedule, 'interval'),
+        (schedules.solar, SolarSchedule, 'solar'),
     )
     save_fields = ['last_run_at', 'total_run_count', 'no_changes']
 
@@ -220,7 +222,7 @@ class DatabaseScheduler(Scheduler):
         return False
 
     def reserve(self, entry):
-        new_entry = Scheduler.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)
@@ -245,11 +247,16 @@ class DatabaseScheduler(Scheduler):
 
     def update_from_dict(self, mapping):
         s = {}
-        for name, entry in items(mapping):
+        for name, entry_fields in items(mapping):
             try:
-                s[name] = self.Entry.from_entry(name, app=self.app, **entry)
+                entry = self.Entry.from_entry(name,
+                                              app=self.app,
+                                              **entry_fields)
+                if entry.model.enabled:
+                    s[name] = entry
+
             except Exception as exc:
-                logger.error(ADD_ENTRY_ERROR, name, exc, entry)
+                logger.error(ADD_ENTRY_ERROR, name, exc, entry_fields)
         self.schedule.update(s)
 
     def install_default_entries(self, data):
@@ -278,6 +285,8 @@ class DatabaseScheduler(Scheduler):
         if update:
             self.sync()
             self._schedule = self.all_as_schedule()
+            # the schedule changed, invalidate the heap in Scheduler.tick
+            self._heap = None
             if logger.isEnabledFor(logging.DEBUG):
                 debug('Current schedule:\n%s', '\n'.join(
                     repr(entry) for entry in values(self._schedule)),
diff --git a/django_celery_beat/utils.py b/django_celery_beat/utils.py
index ba20a8b..c38226f 100644
--- a/django_celery_beat/utils.py
+++ b/django_celery_beat/utils.py
@@ -38,4 +38,7 @@ def is_database_scheduler(scheduler):
         return False
     from kombu.utils import symbol_by_name
     from .schedulers import DatabaseScheduler
-    return issubclass(symbol_by_name(scheduler), DatabaseScheduler)
+    return (
+        scheduler == 'django' or
+        issubclass(symbol_by_name(scheduler), DatabaseScheduler)
+    )
diff --git a/docs/includes/installation.txt b/docs/includes/installation.txt
index ddf011d..ac16b11 100644
--- a/docs/includes/installation.txt
+++ b/docs/includes/installation.txt
@@ -1,5 +1,3 @@
-.. _installation:
-
 Installation
 ============
 
@@ -10,8 +8,6 @@ To install using `pip`,::
 
     $ pip install -U django-celery-beat
 
-.. _installing-from-source:
-
 Downloading and installing from source
 --------------------------------------
 
@@ -28,8 +24,6 @@ You can install it by doing the following,::
 The last command must be executed as a privileged user if
 you are not currently using a virtualenv.
 
-.. _installing-from-git:
-
 Using the development version
 -----------------------------
 
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index 216cd00..dae02ff 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 1.0.1
+:Version: 1.1.0
 :Web: http://django-celery-beat.readthedocs.io/
 :Download: http://pypi.python.org/pypi/django-celery-beat
 :Source: http://github.com/celery/django-celery-beat
@@ -13,10 +13,10 @@ database.
 The periodic tasks can be managed from the Django Admin interface, where you
 can create, edit and delete periodic tasks and how often they should run.
 
-Installing
-==========
+Using the Extension
+===================
 
-The installation instructions for this extension is available
+Usage and installation instructions for this extension are available
 from the `Celery documentation`_:
 
 http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes
@@ -25,10 +25,10 @@ http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-cust
 .. _`Celery documentation`:
     http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes
 
-Important Warning
-=================
+Important Warning about Time Zones
+==================================
 
-.. admonition:: Timezones
+.. warning::
 
     If you change the Django ``TIME_ZONE`` setting your periodic task schedule
     will still be based on the old timezone.
@@ -36,7 +36,7 @@ Important Warning
     To fix that you would have to reset the "last run time" for each periodic
     task::
 
-        >>> from django_celery_beat import PeriodicTask, PeriodicTasks
+        >>> from django_celery_beat.models import PeriodicTask, PeriodicTasks
         >>> PeriodicTask.objects.all().update(last_run_at=None)
         >>> PeriodicTasks.changed()
 
diff --git a/requirements/default.txt b/requirements/default.txt
index 01be9da..8d9f878 100644
--- a/requirements/default.txt
+++ b/requirements/default.txt
@@ -1 +1,2 @@
 celery>=4.0,<5.0
+ephem>=3.7.6.0
diff --git a/requirements/test-django111.txt b/requirements/test-django111.txt
new file mode 100644
index 0000000..4891c35
--- /dev/null
+++ b/requirements/test-django111.txt
@@ -0,0 +1 @@
+django>=1.11,<2.0
diff --git a/setup.py b/setup.py
index e8db5dd..265387d 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,8 @@ except (AttributeError, ImportError):
     def _pyimp():
         return 'Python'
 
-NAME = 'django_celery_beat'
+NAME = 'django-celery-beat'
+PACKAGE = 'django_celery_beat'
 
 E_UNSUPPORTED_PYTHON = '%s 1.0 requires %%s %%s or later!' % (NAME,)
 
@@ -50,6 +51,7 @@ classes = """
     Framework :: Django :: 1.8
     Framework :: Django :: 1.9
     Framework :: Django :: 1.10
+    Framework :: Django :: 1.11
     Operating System :: OS Independent
     Topic :: Communications
     Topic :: System :: Distributed Computing
@@ -74,7 +76,7 @@ def add_doc(m):
 pats = {re_meta: add_default,
         re_doc: add_doc}
 here = os.path.abspath(os.path.dirname(__file__))
-with open(os.path.join(here, NAME, '__init__.py')) as meta_fh:
+with open(os.path.join(here, PACKAGE, '__init__.py')) as meta_fh:
     meta = {}
     for line in meta_fh:
         if line.strip() == '# -eof meta-':
diff --git a/t/proj/celery.py b/t/proj/celery.py
index 8324e6f..6ef18d4 100644
--- a/t/proj/celery.py
+++ b/t/proj/celery.py
@@ -4,7 +4,7 @@ import os
 
 from celery import Celery
 
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings')
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings')
 
 app = Celery('proj')
 
diff --git a/t/proj/settings.py b/t/proj/settings.py
index 84f86cf..ed76492 100644
--- a/t/proj/settings.py
+++ b/t/proj/settings.py
@@ -53,9 +53,14 @@ INSTALLED_APPS = [
 ]
 
 MIDDLEWARE_CLASSES = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
 ]
 
-ROOT_URLCONF = 'proj.urls'
+ROOT_URLCONF = 't.proj.urls'
 
 TEMPLATES = [
     {
@@ -73,7 +78,7 @@ TEMPLATES = [
     },
 ]
 
-WSGI_APPLICATION = 'proj.wsgi.application'
+WSGI_APPLICATION = 't.proj.wsgi.application'
 
 
 # Database
diff --git a/t/unit/test_schedulers.py b/t/unit/test_schedulers.py
index 3df420f..3b1538b 100644
--- a/t/unit/test_schedulers.py
+++ b/t/unit/test_schedulers.py
@@ -5,13 +5,20 @@ import pytest
 from datetime import datetime, timedelta
 from itertools import count
 
+from django.contrib.admin.sites import AdminSite
+from django.contrib.messages.storage.fallback import FallbackStorage
+from django.test import RequestFactory
+
 from celery.five import monotonic, text_t
-from celery.schedules import schedule, crontab
+from celery.schedules import schedule, crontab, solar
 
 from django_celery_beat import schedulers
+from django_celery_beat.admin import PeriodicTaskAdmin
 from django_celery_beat.models import (
     PeriodicTask, PeriodicTasks, IntervalSchedule, CrontabSchedule,
+    SolarSchedule
 )
+from django_celery_beat.utils import make_aware
 
 _ids = count(0)
 
@@ -64,6 +71,22 @@ class SchedulerCase:
         crontab.save()
         return self.create_model(crontab=crontab, **kwargs)
 
+    def create_model_solar(self, schedule, **kwargs):
+        solar = SolarSchedule.from_schedule(schedule)
+        solar.save()
+        return self.create_model(solar=solar, **kwargs)
+
+    def create_conf_entry(self):
+        name = 'thefoo{0}'.format(next(_ids))
+        return name, dict(
+            task='djcelery.unittest.add{0}'.format(next(_ids)),
+            schedule=timedelta(0, 600),
+            args=(),
+            relative=False,
+            kwargs={},
+            options={'queue': 'extra_queue'}
+        )
+
     def create_model(self, Model=PeriodicTask, **kwargs):
         entry = dict(
             name='thefoo{0}'.format(next(_ids)),
@@ -95,6 +118,8 @@ class test_ModelEntry(SchedulerCase):
         assert e.options['routing_key'] == 'cpu'
 
         right_now = self.app.now()
+        # Entry.last_run_at returns naive tz, so make this naive for comparison
+        right_now = right_now.replace(tzinfo=None)
         m2 = self.create_model_interval(
             schedule(timedelta(seconds=10)),
             last_run_at=right_now,
@@ -109,6 +134,47 @@ class test_ModelEntry(SchedulerCase):
 
 
 @pytest.mark.django_db()
+class test_DatabaseSchedulerFromAppConf(SchedulerCase):
+    Scheduler = TrackingScheduler
+
+    @pytest.mark.django_db()
+    @pytest.fixture(autouse=True)
+    def setup_scheduler(self, app):
+        self.app = app
+
+        self.entry_name, entry = self.create_conf_entry()
+        self.app.conf.beat_schedule = {self.entry_name: entry}
+        self.m1 = PeriodicTask(name=self.entry_name)
+
+    def test_constructor(self):
+        s = self.Scheduler(app=self.app)
+
+        assert isinstance(s._dirty, set)
+        assert s._last_sync is None
+        assert s.sync_every
+
+    def test_periodic_task_model_enabled_schedule(self):
+        s = self.Scheduler(app=self.app)
+        sched = s.schedule
+        assert len(sched) == 2
+        assert 'celery.backend_cleanup' in sched
+        assert self.entry_name in sched
+        for n, e in sched.items():
+            assert isinstance(e, s.Entry)
+
+    def test_periodic_task_model_disabled_schedule(self):
+        self.m1.enabled = False
+        self.m1.save()
+
+        s = self.Scheduler(app=self.app)
+        sched = s.schedule
+        assert sched
+        assert len(sched) == 1
+        assert 'celery.backend_cleanup' in sched
+        assert self.entry_name not in sched
+
+
+ at pytest.mark.django_db()
 class test_DatabaseScheduler(SchedulerCase):
     Scheduler = TrackingScheduler
 
@@ -122,15 +188,28 @@ class test_DatabaseScheduler(SchedulerCase):
             schedule(timedelta(seconds=10)))
         self.m1.save()
         self.m1.refresh_from_db()
+
         self.m2 = self.create_model_interval(
             schedule(timedelta(minutes=20)))
         self.m2.save()
         self.m2.refresh_from_db()
+
         self.m3 = self.create_model_crontab(
... 202 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django-celery-beat.git



More information about the Python-modules-commits mailing list