Procházet zdrojové kódy

HUE-8725 [core] Adding Django Celery Beat lib

Romain Rigaux před 6 roky
rodič
revize
4827624f18
72 změnil soubory, kde provedl 5076 přidání a 0 odebrání
  1. 108 0
      desktop/core/ext-py/django-celery-beat-1.4.0/Changelog
  2. 54 0
      desktop/core/ext-py/django-celery-beat-1.4.0/LICENSE
  3. 17 0
      desktop/core/ext-py/django-celery-beat-1.4.0/MANIFEST.in
  4. 292 0
      desktop/core/ext-py/django-celery-beat-1.4.0/PKG-INFO
  5. 261 0
      desktop/core/ext-py/django-celery-beat-1.4.0/README.rst
  6. 36 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/__init__.py
  7. 217 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/admin.py
  8. 15 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/apps.py
  9. 34 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/managers.py
  10. 132 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0001_initial.py
  11. 52 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0002_auto_20161118_0346.py
  12. 26 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0003_auto_20161209_0049.py
  13. 22 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0004_auto_20170221_0000.py
  14. 31 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0005_add_solarschedule_events_choices.py
  15. 103 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0005_add_solarschedule_events_choices_squashed_0009_merge_20181012_1416.py
  16. 32 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0006_auto_20180210_1226.py
  17. 54 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0006_auto_20180322_0932.py
  18. 24 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0006_periodictask_priority.py
  19. 28 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0007_auto_20180521_0826.py
  20. 60 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0008_auto_20180914_1922.py
  21. 0 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/__init__.py
  22. 393 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/models.py
  23. 359 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/schedulers.py
  24. 20 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/templates/admin/djcelery/change_list.html
  25. 79 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/tzcrontab.py
  26. 44 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/utils.py
  27. 107 0
      desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/validators.py
  28. 238 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/Makefile
  29. 0 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/_static/.keep
  30. 0 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/_templates/.keep
  31. 1 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/changelog.rst
  32. 27 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/conf.py
  33. 28 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/copyright.rst
  34. 10 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/glossary.rst
  35. binární
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/images/favicon.ico
  36. binární
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/images/logo.png
  37. 36 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/includes/installation.txt
  38. 185 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/includes/introduction.txt
  39. 32 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/index.rst
  40. 272 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/make.bat
  41. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.admin.rst
  42. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.managers.rst
  43. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.models.rst
  44. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.rst
  45. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.schedulers.rst
  46. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.tzcrontab.rst
  47. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.utils.rst
  48. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.validators.rst
  49. 20 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/index.rst
  50. 32 0
      desktop/core/ext-py/django-celery-beat-1.4.0/docs/templates/readme.txt
  51. 11 0
      desktop/core/ext-py/django-celery-beat-1.4.0/manage.py
  52. 3 0
      desktop/core/ext-py/django-celery-beat-1.4.0/requirements/default.txt
  53. 2 0
      desktop/core/ext-py/django-celery-beat-1.4.0/requirements/docs.txt
  54. 8 0
      desktop/core/ext-py/django-celery-beat-1.4.0/requirements/pkgutils.txt
  55. 2 0
      desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test-ci.txt
  56. 1 0
      desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test-django.txt
  57. 1 0
      desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test-django111.txt
  58. 1 0
      desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test-django20.txt
  59. 4 0
      desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test.txt
  60. 19 0
      desktop/core/ext-py/django-celery-beat-1.4.0/setup.cfg
  61. 160 0
      desktop/core/ext-py/django-celery-beat-1.4.0/setup.py
  62. 0 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/__init__.py
  63. 1 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/__init__.py
  64. 15 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/celery.py
  65. 125 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/settings.py
  66. 8 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/urls.py
  67. 17 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/wsgi.py
  68. 0 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/__init__.py
  69. 36 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/conftest.py
  70. 78 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/test_admin.py
  71. 373 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/test_crontabs.py
  72. 642 0
      desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/test_schedulers.py

+ 108 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/Changelog

@@ -0,0 +1,108 @@
+.. _changelog:
+
+================
+ Change history
+================
+
+.. _version-1.4.0:
+
+1.4.0
+=====
+:release-date: 2018-12-09 1:30 p.m. UTC+2:00
+:release-by: Omer Katz
+
+- Fix migrations dependencies.
+- Added the DJANGO_CELERY_BEAT_TZ_AWARE setting.
+
+.. _version-1.3.0:
+
+1.3.0
+=====
+:release-date: 2018-11-12 17:30 p.m. UTC+2:00
+:release-by: Omer Katz
+
+- Fix transaction handling while syncing the schedule.
+- Fix schedule type validation logic.
+- Scheduler no longer forgets the tasks after first schedule change.
+- Fix race condition for schedule_changed() resulting in erroneously closed connections.
+- Add support for task priorities when using RabbitMQ or Redis as broker.
+- Disabled tasks are now correctly deleted from the schedule.
+- Added name as search filter.
+
+.. _version-1.2.0:
+
+1.2.0
+=====
+:release-date: 2018-10-08 16:00 p.m. UTC+3:00
+:release-by: Omer Katz
+
+- Allow timezone-aware Cron schedules.
+- Retry later in case of InterfaceError in sync.
+- Show Periodic Task Description in panel admin.
+- Fix CrontabSchedule example.
+- Support Periodic Tasks with a start date and one-off tasks.
+- Fixes a problem with beat not reconnecting to MySQL (server restart, network problem, etc.) when checking if schedule has changed.
+- Add toggle admin action which allows to activate disabled tasks or deactivate enabled tasks.
+- Add fields validation for CrontabSchedule.
+- Drop support for Django<1.11.
+- Fix task heap invalidation bug which prevented scheduled tasks from running when syncing tasks from the database.
+- Raise a ValidationError when more than one type (solar, crontab or interval) of schedule is provided.
+
+.. _version-1.1.1:
+
+1.1.1
+=====
+:release-date: 2018-2-18 2:30 p.m. UTC+3:00
+:release-by: Omer Katz
+
+- Fix interval schedules by providing nowfun.
+- Removing code that forced last_run_at to be timezone naive for no reason, made timezone aware. Fixes crontab schedules after celery/celery#4173.
+- Entry.last_run_at is no-longer timezone naive.
+- Use a localized PyTZ timezone object for now() otherwise conversions fail scheduling breaks resulting in constant running of tasks or possibly not running ever.
+- Fix endless migrations creation for solar schedules events.
+- Prevent MySQL has gone away errors.
+- Added support for Django 2.0.
+- Adjust CrontabSchedule's minutes, hour & day_of_month fields max length
+
+.. _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
+=====
+:release-date: 2016-11-07 02:28 p.m. PST
+:release-by: Ask Solem
+
+- Now depends on Celery 4.0.0.
+
+- Migration modules were not included in the distribution.
+
+- Adds documentation: http://django-celery-beat.readthedocs.io/
+
+.. _version-1.0.0:
+
+1.0.0
+=====
+:release-date: 2016-09-08 03:19 p.m. PDT
+:release-by: Ask Solem
+
+- Initial release

+ 54 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/LICENSE

@@ -0,0 +1,54 @@
+Copyright (c) 2015-2016 Ask Solem.  All Rights Reserved.
+Copyright (c) 2012-2014 GoPivotal, Inc.  All Rights Reserved.
+Copyright (c) 2009-2012 Ask Solem.  All Rights Reserved.
+
+django-celery-beat is licensed under The BSD License (3 Clause, also known as
+the new BSD license).  The license is an OSI approved Open Source
+license and is GPL-compatible(1).
+
+The license text can also be found here:
+http://www.opensource.org/licenses/BSD-3-Clause
+
+License
+=======
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of Ask Solem nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+Documentation License
+=====================
+
+The documentation portion of django-celery-beat (the rendered contents of the
+"docs" directory of a software distribution or checkout) is supplied
+under the "Creative Commons Attribution-ShareAlike 4.0
+International" (CC BY-SA 4.0) License as described by
+http://creativecommons.org/licenses/by-sa/4.0/
+
+Footnotes
+=========
+(1) A GPL-compatible license makes it possible to
+    combine django-celery-beat with other software that is released
+    under the GPL, it does not mean that we're distributing
+    django-celery-beat under the GPL license.  The BSD license, unlike the GPL,
+    let you distribute a modified version without making your
+    changes open source.

+ 17 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/MANIFEST.in

@@ -0,0 +1,17 @@
+include Changelog
+include LICENSE
+include README.rst
+include MANIFEST.in
+include setup.cfg
+include setup.py
+include manage.py
+recursive-include docs *
+recursive-include extra/*
+recursive-include examples *
+recursive-include requirements *.txt *.rst
+recursive-include django_celery_beat *.py *.html
+recursive-include t *.py
+
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
+recursive-exclude * .*.sw[a-z]

+ 292 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/PKG-INFO

@@ -0,0 +1,292 @@
+Metadata-Version: 1.1
+Name: django-celery-beat
+Version: 1.4.0
+Summary: Database-backed Periodic Tasks.
+Home-page: https://github.com/celery/django-celery-beat
+Author: Ask Solem
+Author-email: ask@celeryproject.org
+License: BSD
+Description: =====================================================================
+         Database-backed Periodic Tasks
+        =====================================================================
+        
+        |build-status| |coverage| |license| |wheel| |pyversion| |pyimp|
+        
+        :Version: 1.4.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
+        :Keywords: django, celery, beat, periodic task, cron, scheduling
+        
+        About
+        =====
+        
+        This extension enables you to store the periodic task schedule in the
+        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.
+        
+        Using the Extension
+        ===================
+        
+        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
+        
+        
+        .. _`Celery documentation`:
+            http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes
+        
+        Important Warning about Time Zones
+        ==================================
+        
+        .. warning::
+        
+            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::
+        
+                >>> 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.
+        
+        Models
+        ======
+        
+        - ``django_celery_beat.models.PeriodicTask``
+        
+        This model defines a single periodic task to be run.
+        
+        It must be associated with a schedule, which defines how often the task should
+        run.
+        
+        - ``django_celery_beat.models.IntervalSchedule``
+        
+        A schedule that runs at a specific interval (e.g. every 5 seconds).
+        
+        - ``django_celery_beat.models.CrontabSchedule``
+        
+        A schedule with fields like entries in cron:
+        ``minute hour day-of-week day_of_month month_of_year``.
+        
+        - ``django_celery_beat.models.PeriodicTasks``
+        
+        This model is only used as an index to keep track of when the schedule has
+        changed.
+        
+        Whenever you update a ``PeriodicTask`` a counter in this table is also
+        incremented, which tells the ``celery beat`` service to reload the schedule
+        from the database.
+        
+        If you update periodic tasks in bulk, you will need to update the counter
+        manually::
+        
+            >>> from django_celery_beat.models import PeriodicTasks
+            >>> PeriodicTasks.changed()
+        
+        Example creating interval-based periodic task
+        ---------------------------------------------
+        
+        To create a periodic task executing at an interval you must first
+        create the interval object::
+        
+            >>> from django_celery_beat.models import PeriodicTask, IntervalSchedule
+        
+            # executes every 10 seconds.
+            >>> schedule, created = IntervalSchedule.objects.get_or_create(
+            ...     every=10,
+            ...     period=IntervalSchedule.SECONDS,
+            ... )
+        
+        That's all the fields you need: a period type and the frequency.
+        
+        You can choose between a specific set of periods:
+        
+        
+        - ``IntervalSchedule.DAYS``
+        - ``IntervalSchedule.HOURS``
+        - ``IntervalSchedule.MINUTES``
+        - ``IntervalSchedule.SECONDS``
+        - ``IntervalSchedule.MICROSECONDS``
+        
+        .. note::
+        
+            If you have multiple periodic tasks executing every 10 seconds,
+            then they should all point to the same schedule object.
+        
+        There's also a "choices tuple" available should you need to present this
+        to the user::
+        
+            >>> IntervalSchedule.PERIOD_CHOICES
+        
+        
+        Now that we have defined the schedule object, we can create the periodic task
+        entry::
+        
+            >>> PeriodicTask.objects.create(
+            ...     interval=schedule,                  # we created this above.
+            ...     name='Importing contacts',          # simply describes this periodic task.
+            ...     task='proj.tasks.import_contacts',  # name of task.
+            ... )
+        
+        
+        Note that this is a very basic example, you can also specify the arguments
+        and keyword arguments used to execute the task, the ``queue`` to send it
+        to[*], and set an expiry time.
+        
+        Here's an example specifying the arguments, note how JSON serialization is
+        required::
+        
+            >>> import json
+            >>> from datetime import datetime, timedelta
+        
+            >>> PeriodicTask.objects.create(
+            ...     interval=schedule,                  # we created this above.
+            ...     name='Importing contacts',          # simply describes this periodic task.
+            ...     task='proj.tasks.import_contacts',  # name of task.
+            ...     args=json.dumps(['arg1', 'arg2']),
+            ...     kwargs=json.dumps({
+            ...        'be_careful': True,
+            ...     }),
+            ...     expires=datetime.utcnow() + timedelta(seconds=30)
+            ... )
+        
+        
+        .. [*] you can also use low-level AMQP routing using the ``exchange`` and
+               ``routing_key`` fields.
+        
+        Example creating crontab-based periodic task
+        --------------------------------------------
+        
+        A crontab schedule has the fields: ``minute``, ``hour``, ``day_of_week``,
+        ``day_of_month`` and ``month_of_year`, so if you want the equivalent
+        of a ``30 * * * *`` (execute every 30 minutes) crontab entry you specify::
+        
+            >>> from django_celery_beat.models import CrontabSchedule, PeriodicTask
+            >>> schedule, _ = CrontabSchedule.objects.get_or_create(
+            ...     minute='30',
+            ...     hour='*',
+            ...     day_of_week='*',
+            ...     day_of_month='*',
+            ...     month_of_year='*',
+            ...     timezone=pytz.timezone('Canada/Pacific')
+            ... )
+        
+        The crontab schedule is linked to a specific timezone using the 'timezone' input parameter.
+        
+        Then to create a periodic task using this schedule, use the same approach as
+        the interval-based periodic task earlier in this document, but instead
+        of ``interval=schedule``, specify ``crontab=schedule``::
+        
+            >>> PeriodicTask.objects.create(
+            ...     crontab=schedule,
+            ...     name='Importing contacts',
+            ...     task='proj.tasks.import_contacts',
+            ... )
+        
+        Temporarily disable a periodic task
+        -----------------------------------
+        
+        You can use the ``enabled`` flag to temporarily disable a periodic task::
+        
+            >>> periodic_task.enabled = False
+            >>> periodic_task.save()
+        
+        Installation
+        ============
+        
+        You can install django-celery-beat either via the Python Package Index (PyPI)
+        or from source.
+        
+        To install using `pip`,::
+        
+            $ pip install -U django-celery-beat
+        
+        Downloading and installing from source
+        --------------------------------------
+        
+        Download the latest version of django-celery-beat from
+        http://pypi.python.org/pypi/django-celery-beat
+        
+        You can install it by doing the following,::
+        
+            $ tar xvfz django-celery-beat-0.0.0.tar.gz
+            $ cd django-celery-beat-0.0.0
+            $ python setup.py build
+            # python setup.py install
+        
+        The last command must be executed as a privileged user if
+        you are not currently using a virtualenv.
+        
+        Using the development version
+        -----------------------------
+        
+        With pip
+        ~~~~~~~~
+        
+        You can install the latest snapshot of django-celery-beat using the following
+        pip command::
+        
+            $ pip install https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat
+        
+        
+        TZ Awareness:
+        -------------
+        
+        If you have a project that is time zone naive, you can set `DJANGO_CELERY_BEAT_TZ_AWARE=False` in your settings file.
+        
+        
+        .. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-beat.svg?branch=master
+            :alt: Build status
+            :target: https://travis-ci.org/celery/django-celery-beat
+        
+        .. |coverage| image:: https://codecov.io/github/celery/django-celery-beat/coverage.svg?branch=master
+            :target: https://codecov.io/github/celery/django-celery-beat?branch=master
+        
+        .. |license| image:: https://img.shields.io/pypi/l/django-celery-beat.svg
+            :alt: BSD License
+            :target: https://opensource.org/licenses/BSD-3-Clause
+        
+        .. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-beat.svg
+            :alt: django-celery-beat can be installed via wheel
+            :target: http://pypi.python.org/pypi/django-celery-beat/
+        
+        .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-beat.svg
+            :alt: Supported Python versions.
+            :target: http://pypi.python.org/pypi/django-celery-beat/
+        
+        .. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-beat.svg
+            :alt: Support Python implementations.
+            :target: http://pypi.python.org/pypi/django-celery-beat/
+        
+        
+Keywords: django celery beat periodic task database
+Platform: any
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Framework :: Django
+Classifier: Framework :: Django :: 1.8
+Classifier: Framework :: Django :: 1.9
+Classifier: Framework :: Django :: 1.10
+Classifier: Framework :: Django :: 1.11
+Classifier: Framework :: Django :: 2.0
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Communications
+Classifier: Topic :: System :: Distributed Computing
+Classifier: Topic :: Software Development :: Libraries :: Python Modules

+ 261 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/README.rst

@@ -0,0 +1,261 @@
+=====================================================================
+ Database-backed Periodic Tasks
+=====================================================================
+
+|build-status| |coverage| |license| |wheel| |pyversion| |pyimp|
+
+:Version: 1.4.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
+:Keywords: django, celery, beat, periodic task, cron, scheduling
+
+About
+=====
+
+This extension enables you to store the periodic task schedule in the
+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.
+
+Using the Extension
+===================
+
+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
+
+
+.. _`Celery documentation`:
+    http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes
+
+Important Warning about Time Zones
+==================================
+
+.. warning::
+
+    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::
+
+        >>> 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.
+
+Models
+======
+
+- ``django_celery_beat.models.PeriodicTask``
+
+This model defines a single periodic task to be run.
+
+It must be associated with a schedule, which defines how often the task should
+run.
+
+- ``django_celery_beat.models.IntervalSchedule``
+
+A schedule that runs at a specific interval (e.g. every 5 seconds).
+
+- ``django_celery_beat.models.CrontabSchedule``
+
+A schedule with fields like entries in cron:
+``minute hour day-of-week day_of_month month_of_year``.
+
+- ``django_celery_beat.models.PeriodicTasks``
+
+This model is only used as an index to keep track of when the schedule has
+changed.
+
+Whenever you update a ``PeriodicTask`` a counter in this table is also
+incremented, which tells the ``celery beat`` service to reload the schedule
+from the database.
+
+If you update periodic tasks in bulk, you will need to update the counter
+manually::
+
+    >>> from django_celery_beat.models import PeriodicTasks
+    >>> PeriodicTasks.changed()
+
+Example creating interval-based periodic task
+---------------------------------------------
+
+To create a periodic task executing at an interval you must first
+create the interval object::
+
+    >>> from django_celery_beat.models import PeriodicTask, IntervalSchedule
+
+    # executes every 10 seconds.
+    >>> schedule, created = IntervalSchedule.objects.get_or_create(
+    ...     every=10,
+    ...     period=IntervalSchedule.SECONDS,
+    ... )
+
+That's all the fields you need: a period type and the frequency.
+
+You can choose between a specific set of periods:
+
+
+- ``IntervalSchedule.DAYS``
+- ``IntervalSchedule.HOURS``
+- ``IntervalSchedule.MINUTES``
+- ``IntervalSchedule.SECONDS``
+- ``IntervalSchedule.MICROSECONDS``
+
+.. note::
+
+    If you have multiple periodic tasks executing every 10 seconds,
+    then they should all point to the same schedule object.
+
+There's also a "choices tuple" available should you need to present this
+to the user::
+
+    >>> IntervalSchedule.PERIOD_CHOICES
+
+
+Now that we have defined the schedule object, we can create the periodic task
+entry::
+
+    >>> PeriodicTask.objects.create(
+    ...     interval=schedule,                  # we created this above.
+    ...     name='Importing contacts',          # simply describes this periodic task.
+    ...     task='proj.tasks.import_contacts',  # name of task.
+    ... )
+
+
+Note that this is a very basic example, you can also specify the arguments
+and keyword arguments used to execute the task, the ``queue`` to send it
+to[*], and set an expiry time.
+
+Here's an example specifying the arguments, note how JSON serialization is
+required::
+
+    >>> import json
+    >>> from datetime import datetime, timedelta
+
+    >>> PeriodicTask.objects.create(
+    ...     interval=schedule,                  # we created this above.
+    ...     name='Importing contacts',          # simply describes this periodic task.
+    ...     task='proj.tasks.import_contacts',  # name of task.
+    ...     args=json.dumps(['arg1', 'arg2']),
+    ...     kwargs=json.dumps({
+    ...        'be_careful': True,
+    ...     }),
+    ...     expires=datetime.utcnow() + timedelta(seconds=30)
+    ... )
+
+
+.. [*] you can also use low-level AMQP routing using the ``exchange`` and
+       ``routing_key`` fields.
+
+Example creating crontab-based periodic task
+--------------------------------------------
+
+A crontab schedule has the fields: ``minute``, ``hour``, ``day_of_week``,
+``day_of_month`` and ``month_of_year`, so if you want the equivalent
+of a ``30 * * * *`` (execute every 30 minutes) crontab entry you specify::
+
+    >>> from django_celery_beat.models import CrontabSchedule, PeriodicTask
+    >>> schedule, _ = CrontabSchedule.objects.get_or_create(
+    ...     minute='30',
+    ...     hour='*',
+    ...     day_of_week='*',
+    ...     day_of_month='*',
+    ...     month_of_year='*',
+    ...     timezone=pytz.timezone('Canada/Pacific')
+    ... )
+
+The crontab schedule is linked to a specific timezone using the 'timezone' input parameter.
+
+Then to create a periodic task using this schedule, use the same approach as
+the interval-based periodic task earlier in this document, but instead
+of ``interval=schedule``, specify ``crontab=schedule``::
+
+    >>> PeriodicTask.objects.create(
+    ...     crontab=schedule,
+    ...     name='Importing contacts',
+    ...     task='proj.tasks.import_contacts',
+    ... )
+
+Temporarily disable a periodic task
+-----------------------------------
+
+You can use the ``enabled`` flag to temporarily disable a periodic task::
+
+    >>> periodic_task.enabled = False
+    >>> periodic_task.save()
+
+Installation
+============
+
+You can install django-celery-beat either via the Python Package Index (PyPI)
+or from source.
+
+To install using `pip`,::
+
+    $ pip install -U django-celery-beat
+
+Downloading and installing from source
+--------------------------------------
+
+Download the latest version of django-celery-beat from
+http://pypi.python.org/pypi/django-celery-beat
+
+You can install it by doing the following,::
+
+    $ tar xvfz django-celery-beat-0.0.0.tar.gz
+    $ cd django-celery-beat-0.0.0
+    $ python setup.py build
+    # python setup.py install
+
+The last command must be executed as a privileged user if
+you are not currently using a virtualenv.
+
+Using the development version
+-----------------------------
+
+With pip
+~~~~~~~~
+
+You can install the latest snapshot of django-celery-beat using the following
+pip command::
+
+    $ pip install https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat
+
+
+TZ Awareness:
+-------------
+
+If you have a project that is time zone naive, you can set `DJANGO_CELERY_BEAT_TZ_AWARE=False` in your settings file.
+
+
+.. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-beat.svg?branch=master
+    :alt: Build status
+    :target: https://travis-ci.org/celery/django-celery-beat
+
+.. |coverage| image:: https://codecov.io/github/celery/django-celery-beat/coverage.svg?branch=master
+    :target: https://codecov.io/github/celery/django-celery-beat?branch=master
+
+.. |license| image:: https://img.shields.io/pypi/l/django-celery-beat.svg
+    :alt: BSD License
+    :target: https://opensource.org/licenses/BSD-3-Clause
+
+.. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-beat.svg
+    :alt: django-celery-beat can be installed via wheel
+    :target: http://pypi.python.org/pypi/django-celery-beat/
+
+.. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-beat.svg
+    :alt: Supported Python versions.
+    :target: http://pypi.python.org/pypi/django-celery-beat/
+
+.. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-beat.svg
+    :alt: Support Python implementations.
+    :target: http://pypi.python.org/pypi/django-celery-beat/
+

+ 36 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/__init__.py

@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+"""Database-backed Periodic Tasks."""
+# :copyright: (c) 2016, Ask Solem.
+#             All rights reserved.
+# :license:   BSD (3 Clause), see LICENSE for more details.
+
+from __future__ import absolute_import, unicode_literals
+
+import re
+
+from collections import namedtuple
+
+__version__ = '1.4.0'
+__author__ = 'Ask Solem'
+__contact__ = '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__ = []
+
+default_app_config = 'django_celery_beat.apps.BeatConfig'

+ 217 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/admin.py

@@ -0,0 +1,217 @@
+"""Periodic Task Admin interface."""
+from __future__ import absolute_import, unicode_literals
+
+from django import forms
+from django.conf import settings
+from django.contrib import admin
+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 ugettext_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
+)
+from .utils import is_database_scheduler
+
+try:
+    from django.utils.encoding import force_text
+except ImportError:
+    from django.utils.encoding import force_unicode as force_text  # noqa
+
+
+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(PeriodicTaskForm, self).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
+        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
+    list_display = ('__str__', 'enabled', 'interval', 'start_time', 'one_off')
+    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',
+                       'start_time', 'one_off'),
+            'classes': ('extrapretty', 'wide'),
+        }),
+        ('Arguments', {
+            'fields': ('args', 'kwargs'),
+            'classes': ('extrapretty', 'wide', 'collapse', 'in'),
+        }),
+        ('Execution Options', {
+            'fields': ('expires', 'queue', 'exchange', 'routing_key'),
+            'classes': ('extrapretty', 'wide', 'collapse', 'in'),
+        }),
+    )
+
+    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(PeriodicTaskAdmin, self).get_queryset(request)
+        return qs.select_related('interval', 'crontab', 'solar')
+
+    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))
+                 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)

+ 15 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/apps.py

@@ -0,0 +1,15 @@
+"""Django Application configuration."""
+from __future__ import absolute_import, unicode_literals
+
+from django.apps import AppConfig
+from django.utils.translation import ugettext_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')

+ 34 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/managers.py

@@ -0,0 +1,34 @@
+"""Model managers."""
+from __future__ import absolute_import, unicode_literals
+
+from celery.five import items
+
+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 items(fields)]
+        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)

+ 132 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0001_initial.py

@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2016-08-04 02:13
+from __future__ import absolute_import, unicode_literals
+
+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()),
+            ],
+        ),
+    ]

+ 52 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/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'),
+        ),
+    ]

+ 26 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/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')]),
+        ),
+    ]

+ 22 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/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),
+        ),
+    ]

+ 31 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0005_add_solarschedule_events_choices.py

@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.1 on 2017-11-01 15:53
+from __future__ import absolute_import, unicode_literals
+
+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'),
+        ),
+    ]

+ 103 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0005_add_solarschedule_events_choices_squashed_0009_merge_20181012_1416.py

@@ -0,0 +1,103 @@
+# Generated by Django 2.1.2 on 2018-10-12 14:18
+from __future__ import absolute_import, unicode_literals
+
+from django.db import migrations, models
+import django_celery_beat.validators
+import timezone_field.fields
+
+
+class Migration(migrations.Migration):
+    replaces = [
+        ('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'),
+    ]
+
+    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'),
+        ),
+        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.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.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'),
+        ),
+        migrations.AlterField(
+            model_name='crontabschedule',
+            name='day_of_month',
+            field=models.CharField(default='*', max_length=124, validators=[
+                django_celery_beat.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=[
+                django_celery_beat.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=[
+                django_celery_beat.validators.hour_validator],
+                verbose_name='hour'),
+        ),
+        migrations.AlterField(
+            model_name='crontabschedule',
+            name='minute',
+            field=models.CharField(default='*', max_length=240, validators=[
+                django_celery_beat.validators.minute_validator],
+                verbose_name='minute'),
+        ),
+        migrations.AlterField(
+            model_name='crontabschedule',
+            name='month_of_year',
+            field=models.CharField(default='*', max_length=64, validators=[
+                django_celery_beat.validators.month_of_year_validator],
+                verbose_name='month of year'),
+        ),
+    ]

+ 32 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0006_auto_20180210_1226.py

@@ -0,0 +1,32 @@
+# Generated by Django 2.0.1 on 2018-02-10 12:26
+from __future__ import absolute_import, unicode_literals
+
+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'),
+        ),
+    ]

+ 54 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0006_auto_20180322_0932.py

@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.7 on 2018-03-22 16:32
+from __future__ import absolute_import, unicode_literals
+
+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'
+            ),
+        ),
+    ]

+ 24 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0006_periodictask_priority.py

@@ -0,0 +1,24 @@
+# Generated by Django 2.0.6 on 2018-10-22 05:20
+from __future__ import absolute_import, unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('django_celery_beat', '0005_add_solarschedule_events_choices_squashed_0009_merge_20181012_1416'), # noqa
+    ]
+
+    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'),
+        ),
+    ]

+ 28 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0007_auto_20180521_0826.py

@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2018-05-21 08:26
+from __future__ import absolute_import, unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_celery_beat', '0006_auto_20180322_0932'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='periodictask',
+            name='one_off',
+            field=models.BooleanField(default=False,
+                                      verbose_name='one-off task'),
+        ),
+        migrations.AddField(
+            model_name='periodictask',
+            name='start_time',
+            field=models.DateTimeField(blank=True,
+                                       null=True,
+                                       verbose_name='start_time'),
+        ),
+    ]

+ 60 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/0008_auto_20180914_1922.py

@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 2.0.3 on 2018-09-14 19:22
+from __future__ import absolute_import, unicode_literals
+
+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'
+            ),
+        ),
+    ]

+ 0 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/migrations/__init__.py


+ 393 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/models.py

@@ -0,0 +1,393 @@
+"""Database models."""
+from __future__ import absolute_import, unicode_literals
+
+from datetime import timedelta
+
+import timezone_field
+from celery import schedules
+from celery.five import python_2_unicode_compatible
+from django.conf import settings
+from django.core.exceptions import MultipleObjectsReturned, ValidationError
+from django.core.validators import MaxValueValidator
+from django.db import models
+from django.db.models import signals
+from django.utils.translation import ugettext_lazy as _
+
+from . import managers, validators
+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')),
+)
+
+SOLAR_SCHEDULES = [(x, _(x)) for x in sorted(schedules.solar._all_events)]
+
+
+def cronexp(field):
+    """Representation of cron expression."""
+    return field and str(field).replace(' ', '') or '*'
+
+
+@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
+        )
+
+
+@python_2_unicode_compatible
+class IntervalSchedule(models.Model):
+    """Schedule executing every n seconds."""
+
+    DAYS = DAYS
+    HOURS = HOURS
+    MINUTES = MINUTES
+    SECONDS = SECONDS
+    MICROSECONDS = MICROSECONDS
+
+    PERIOD_CHOICES = PERIOD_CHOICES
+
+    every = models.IntegerField(_('every'), null=False)
+    period = models.CharField(
+        _('period'), max_length=24, choices=PERIOD_CHOICES,
+    )
+
+    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:
+            cls.objects.filter(every=every, period=period).delete()
+            return cls(every=every, period=period)
+
+    def __str__(self):
+        if self.every == 1:
+            return _('every {0.period_singular}').format(self)
+        return _('every {0.every} {0.period}').format(self)
+
+    @property
+    def period_singular(self):
+        return self.period[:-1]
+
+
+@python_2_unicode_compatible
+class CrontabSchedule(models.Model):
+    """Timezone Aware Crontab-like schedule."""
+
+    #
+    # 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(
+        _('minute'), max_length=60 * 4, default='*',
+        validators=[validators.minute_validator],
+    )
+    hour = models.CharField(
+        _('hour'), max_length=24 * 4, default='*',
+        validators=[validators.hour_validator],
+    )
+    day_of_week = models.CharField(
+        _('day of week'), max_length=64, default='*',
+        validators=[validators.day_of_week_validator],
+    )
+    day_of_month = models.CharField(
+        _('day of month'), max_length=31 * 4, default='*',
+        validators=[validators.day_of_month_validator],
+    )
+    month_of_year = models.CharField(
+        _('month of year'), max_length=64, default='*',
+        validators=[validators.month_of_year_validator],
+    )
+
+    timezone = timezone_field.TimeZoneField(default='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/d/dM/MY) {5}'.format(
+            cronexp(self.minute), cronexp(self.hour),
+            cronexp(self.day_of_week), cronexp(self.day_of_month),
+            cronexp(self.month_of_year), 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:
+            cls.objects.filter(**spec).delete()
+            return cls(**spec)
+
+
+class PeriodicTasks(models.Model):
+    """Helper table for tracking updates to periodic tasks."""
+
+    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
+
+
+@python_2_unicode_compatible
+class PeriodicTask(models.Model):
+    """Model representing a periodic task."""
+
+    name = models.CharField(
+        _('name'), max_length=200, unique=True,
+        help_text=_('Useful description'),
+    )
+    task = models.CharField(_('task name'), max_length=200)
+    interval = models.ForeignKey(
+        IntervalSchedule, on_delete=models.CASCADE,
+        null=True, blank=True, verbose_name=_('interval'),
+    )
+    crontab = models.ForeignKey(
+        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='[]',
+        help_text=_('JSON encoded positional arguments'),
+    )
+    kwargs = models.TextField(
+        _('Keyword arguments'), blank=True, default='{}',
+        help_text=_('JSON encoded keyword arguments'),
+    )
+    queue = models.CharField(
+        _('queue'), max_length=200, blank=True, null=True, default=None,
+        help_text=_('Queue defined in CELERY_TASK_QUEUES'),
+    )
+    exchange = models.CharField(
+        _('exchange'), max_length=200, blank=True, null=True, default=None,
+    )
+    routing_key = models.CharField(
+        _('routing key'), max_length=200, blank=True, null=True, default=None,
+    )
+    priority = models.PositiveIntegerField(
+        _('priority'), default=None, validators=[MaxValueValidator(255)],
+        blank=True, null=True
+    )
+    expires = models.DateTimeField(
+        _('expires'), blank=True, null=True,
+    )
+    one_off = models.BooleanField(
+        _('one-off task'), default=False,
+    )
+    start_time = models.DateTimeField(
+        _('start_time'), blank=True, null=True,
+    )
+    enabled = models.BooleanField(
+        _('enabled'), default=True,
+    )
+    last_run_at = models.DateTimeField(
+        auto_now=False, auto_now_add=False,
+        editable=False, blank=True, null=True,
+    )
+    total_run_count = models.PositiveIntegerField(
+        default=0, editable=False,
+    )
+    date_changed = models.DateTimeField(auto_now=True)
+    description = models.TextField(_('description'), blank=True)
+
+    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(PeriodicTask, self).validate_unique(*args, **kwargs)
+
+        schedule_types = ['interval', 'crontab', 'solar']
+        selected_schedule_types = [s for s in schedule_types
+                                   if getattr(self, s)]
+
+        if len(selected_schedule_types) == 0:
+            raise ValidationError({
+                'interval': [
+                    'One of interval, crontab, or solar must be set.'
+                ]
+            })
+
+        err_msg = 'Only one of 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)
+
+    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
+        if not self.enabled:
+            self.last_run_at = None
+        super(PeriodicTask, self).save(*args, **kwargs)
+
+    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}'
+        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
+
+
+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)

+ 359 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/schedulers.py

@@ -0,0 +1,359 @@
+"""Beat Scheduler Implementation."""
+from __future__ import absolute_import, unicode_literals
+
+import datetime
+import logging
+
+from multiprocessing.util import Finalize
+
+from celery import current_app
+from celery import schedules
+from celery.beat import Scheduler, ScheduleEntry
+from celery.five import values, items
+from celery.utils.encoding import safe_str, safe_repr
+from celery.utils.log import get_logger
+from celery.utils.time import maybe_make_aware
+from kombu.utils.json import dumps, loads
+
+from django.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,
+)
+from .utils import make_aware
+
+try:
+    from celery.utils.time import is_naive
+except ImportError:  # pragma: no cover
+    from celery.utils.timeutils import is_naive  # noqa
+
+# 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'),
+    )
+    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', 'expires',
+                       'priority']:
+            value = getattr(model, option)
+            if value is None:
+                continue
+            self.options[option] = value
+
+        self.total_run_count = model.total_run_count
+        self.model = model
+
+        if not model.last_run_at:
+            model.last_run_at = self._default_now()
+
+        last_run_at = model.last_run_at
+
+        if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True):
+            last_run_at = make_aware(last_run_at)
+
+        self.last_run_at = 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:
+            if maybe_make_aware(self._default_now()) < self.model.start_time:
+                # The datetime is before the start date - don't run.
+                _, delay = self.schedule.is_due(self.last_run_at)
+                # use original delay for re-check
+                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()
+            return schedules.schedstate(False, None)  # Don't recheck
+
+        return self.schedule.is_due(self.last_run_at)
+
+    def _default_now(self):
+        now = self.app.now()
+        # 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 = now.tzinfo.localize(now.replace(tzinfo=None))
+        return now
+
+    def __next__(self):
+        self.model.last_run_at = self.app.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))
+
+        if not getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True):
+            obj.last_run_at = datetime.datetime.now()
+
+        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, **kwargs):
+        return {
+            'queue': queue,
+            'exchange': exchange,
+            'routing_key': routing_key,
+            'priority': priority
+        }
+
+    def __repr__(self):
+        return '<ModelEntry: {0} {1}(*{2}, **{3}) {4}>'.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):
+        info('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) as exc:
+                    _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 items(mapping):
+            try:
+                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_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': {'expires': 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(DatabaseScheduler, self).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 values(self._schedule)),
+                )
+        return self._schedule

+ 20 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/templates/admin/djcelery/change_list.html

@@ -0,0 +1,20 @@
+{% extends "admin/change_list.html" %}
+{% load i18n %}
+
+{% block breadcrumbs %}
+  <div class="breadcrumbs">
+    <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
+    &rsaquo; <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
+    &rsaquo; {{ cl.opts.verbose_name_plural|capfirst }}
+  </div>
+  {% if wrong_scheduler %}
+    <ul class="messagelist">
+      <li class="warning">
+      Periodic tasks won't be dispatched unless you set the
+      <code>CELERYBEAT_SCHEDULER</code> setting to
+      <code>djcelery.schedulers.DatabaseScheduler</code>,
+      or specify it using the <code>-S</code> option to celerybeat
+      </li>
+    </ul>
+  {% endif %}
+{% endblock %}

+ 79 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/tzcrontab.py

@@ -0,0 +1,79 @@
+"""Timezone aware Cron schedule Implementation."""
+from __future__ import absolute_import, unicode_literals
+
+
+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(TzAwareCrontab, self).__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 """<crontab: {0._orig_minute} {0._orig_hour}
+         {0._orig_day_of_week} {0._orig_day_of_month}
+          {0._orig_month_of_year} (m/h/d/dM/MY), {0.tz}>
+        """.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

+ 44 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/utils.py

@@ -0,0 +1,44 @@
+"""Utilities."""
+# -- XXX This module must not use translation as that causes
+# -- a recursive loader import!
+from __future__ import absolute_import, unicode_literals
+
+from django.conf import settings
+from django.utils import timezone
+
+is_aware = timezone.is_aware
+
+# 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)
+    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)
+    )

+ 107 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/django_celery_beat/validators.py

@@ -0,0 +1,107 @@
+"""Validators."""
+from __future__ import absolute_import, unicode_literals
+
+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)

+ 238 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/Makefile

@@ -0,0 +1,238 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+	$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  applehelp  to make an Apple Help Book"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  epub3      to make an epub3"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  xml        to make Docutils-native XML files"
+	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+	@echo "  coverage   to run coverage check of the documentation (if enabled)"
+	@echo "  apicheck   to verify that all modules are present in autodoc"
+	@echo "  configcheck to verify that all modules are present in autodoc"
+	@echo "  spelling   to run a spell checker on the documentation"
+
+.PHONY: clean
+clean:
+	rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PROJ.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PROJ.qhc"
+
+.PHONY: applehelp
+applehelp:
+	$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+	@echo
+	@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+	@echo "N.B. You won't be able to view it unless you put it in" \
+	      "~/Library/Documentation/Help or install it in your application" \
+	      "bundle."
+
+.PHONY: devhelp
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/PROJ"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PROJ"
+	@echo "# devhelp"
+
+.PHONY: epub
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: epub3
+epub3:
+	$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
+	@echo
+	@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
+
+.PHONY: latex
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through platex and dvipdfmx..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+	$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+	@echo "Testing of coverage in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: apicheck
+apicheck:
+	$(SPHINXBUILD) -b apicheck $(ALLSPHINXOPTS) $(BUILDDIR)/apicheck
+
+.PHONY: configcheck
+configcheck:
+	$(SPHINXBUILD) -b configcheck $(ALLSPHINXOPTS) $(BUILDDIR)/configcheck
+
+.PHONY: spelling
+spelling:
+	SPELLCHECK=1 $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling
+
+.PHONY: xml
+xml:
+	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+	@echo
+	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+	@echo
+	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

+ 0 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/_static/.keep


+ 0 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/_templates/.keep


+ 1 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/changelog.rst

@@ -0,0 +1 @@
+.. include:: ../Changelog

+ 27 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/conf.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+import os
+
+from sphinx_celery import conf
+
+globals().update(conf.build_config(
+    'django_celery_beat', __file__,
+    project='django_celery_beat',
+    # version_dev='2.0',
+    # version_stable='1.4',
+    canonical_url='http://django-celery-beat.readthedocs.io',
+    webdomain='',
+    github_project='celery/django-celery-beat',
+    copyright='2016',
+    django_settings='proj.settings',
+    include_intersphinx={'python', 'sphinx', 'django', 'celery'},
+    path_additions=[os.path.join(os.pardir, 't')],
+    html_logo='images/logo.png',
+    html_favicon='images/favicon.ico',
+    html_prepend_sidebars=[],
+    apicheck_ignore_modules=[
+        'django_celery_beat.apps',
+        r'django_celery_beat.migrations.*',
+    ],
+))

+ 28 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/copyright.rst

@@ -0,0 +1,28 @@
+Copyright
+=========
+
+*django-celery-beat User Manual*
+
+by Ask Solem
+
+.. |copy|   unicode:: U+000A9 .. COPYRIGHT SIGN
+
+Copyright |copy| 2016, Ask Solem
+
+All rights reserved.  This material may be copied or distributed only
+subject to the terms and conditions set forth in the `Creative Commons
+Attribution-ShareAlike 4.0 International`
+<http://creativecommons.org/licenses/by-sa/4.0/legalcode>`_ license.
+
+You may share and adapt the material, even for commercial purposes, but
+you must give the original author credit.
+If you alter, transform, or build upon this
+work, you may distribute the resulting work only under the same license or
+a license compatible to this one.
+
+.. note::
+
+   While the django-celery-beat *documentation* is offered under the
+   Creative Commons *Attribution-ShareAlike 4.0 International* license
+   the django-celery-beat *software* is offered under the
+   `BSD License (3 Clause) <http://www.opensource.org/licenses/BSD-3-Clause>`_

+ 10 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/glossary.rst

@@ -0,0 +1,10 @@
+.. _glossary:
+
+Glossary
+========
+
+.. glossary::
+    :sorted:
+
+    term
+        Description of term

binární
desktop/core/ext-py/django-celery-beat-1.4.0/docs/images/favicon.ico


binární
desktop/core/ext-py/django-celery-beat-1.4.0/docs/images/logo.png


+ 36 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/includes/installation.txt

@@ -0,0 +1,36 @@
+Installation
+============
+
+You can install django-celery-beat either via the Python Package Index (PyPI)
+or from source.
+
+To install using `pip`,::
+
+    $ pip install -U django-celery-beat
+
+Downloading and installing from source
+--------------------------------------
+
+Download the latest version of django-celery-beat from
+http://pypi.python.org/pypi/django-celery-beat
+
+You can install it by doing the following,::
+
+    $ tar xvfz django-celery-beat-0.0.0.tar.gz
+    $ cd django-celery-beat-0.0.0
+    $ python setup.py build
+    # python setup.py install
+
+The last command must be executed as a privileged user if
+you are not currently using a virtualenv.
+
+Using the development version
+-----------------------------
+
+With pip
+~~~~~~~~
+
+You can install the latest snapshot of django-celery-beat using the following
+pip command::
+
+    $ pip install https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat

+ 185 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/includes/introduction.txt

@@ -0,0 +1,185 @@
+:Version: 1.4.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
+:Keywords: django, celery, beat, periodic task, cron, scheduling
+
+About
+=====
+
+This extension enables you to store the periodic task schedule in the
+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.
+
+Using the Extension
+===================
+
+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
+
+
+.. _`Celery documentation`:
+    http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#using-custom-scheduler-classes
+
+Important Warning about Time Zones
+==================================
+
+.. warning::
+
+    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::
+
+        >>> from django_celery_beat.models import PeriodicTask, PeriodicTasks
+        >>> PeriodicTask.objects.all().update(last_run_at=None)
+        >>> PeriodicTasks.changed()
+
+    Note that this will reset the state as if the periodic tasks have never run
+    before.
+
+Models
+======
+
+- ``django_celery_beat.models.PeriodicTask``
+
+This model defines a single periodic task to be run.
+
+It must be associated with a schedule, which defines how often the task should
+run.
+
+- ``django_celery_beat.models.IntervalSchedule``
+
+A schedule that runs at a specific interval (e.g. every 5 seconds).
+
+- ``django_celery_beat.models.CrontabSchedule``
+
+A schedule with fields like entries in cron:
+``minute hour day-of-week day_of_month month_of_year``.
+
+- ``django_celery_beat.models.PeriodicTasks``
+
+This model is only used as an index to keep track of when the schedule has
+changed.
+
+Whenever you update a ``PeriodicTask`` a counter in this table is also
+incremented, which tells the ``celery beat`` service to reload the schedule
+from the database.
+
+If you update periodic tasks in bulk, you will need to update the counter
+manually::
+
+    >>> from django_celery_beat.models import PeriodicTasks
+    >>> PeriodicTasks.changed()
+
+Example creating interval-based periodic task
+---------------------------------------------
+
+To create a periodic task executing at an interval you must first
+create the interval object::
+
+    >>> from django_celery_beat.models import PeriodicTask, IntervalSchedule
+
+    # executes every 10 seconds.
+    >>> schedule, created = IntervalSchedule.objects.get_or_create(
+    ...     every=10,
+    ...     period=IntervalSchedule.SECONDS,
+    ... )
+
+That's all the fields you need: a period type and the frequency.
+
+You can choose between a specific set of periods:
+
+
+- ``IntervalSchedule.DAYS``
+- ``IntervalSchedule.HOURS``
+- ``IntervalSchedule.MINUTES``
+- ``IntervalSchedule.SECONDS``
+- ``IntervalSchedule.MICROSECONDS``
+
+.. note::
+
+    If you have multiple periodic tasks executing every 10 seconds,
+    then they should all point to the same schedule object.
+
+There's also a "choices tuple" available should you need to present this
+to the user::
+
+    >>> IntervalSchedule.PERIOD_CHOICES
+
+
+Now that we have defined the schedule object, we can create the periodic task
+entry::
+
+    >>> PeriodicTask.objects.create(
+    ...     interval=schedule,                  # we created this above.
+    ...     name='Importing contacts',          # simply describes this periodic task.
+    ...     task='proj.tasks.import_contacts',  # name of task.
+    ... )
+
+
+Note that this is a very basic example, you can also specify the arguments
+and keyword arguments used to execute the task, the ``queue`` to send it
+to[*], and set an expiry time.
+
+Here's an example specifying the arguments, note how JSON serialization is
+required::
+
+    >>> import json
+    >>> from datetime import datetime, timedelta
+
+    >>> PeriodicTask.objects.create(
+    ...     interval=schedule,                  # we created this above.
+    ...     name='Importing contacts',          # simply describes this periodic task.
+    ...     task='proj.tasks.import_contacts',  # name of task.
+    ...     args=json.dumps(['arg1', 'arg2']),
+    ...     kwargs=json.dumps({
+    ...        'be_careful': True,
+    ...     }),
+    ...     expires=datetime.utcnow() + timedelta(seconds=30)
+    ... )
+
+
+.. [*] you can also use low-level AMQP routing using the ``exchange`` and
+       ``routing_key`` fields.
+
+Example creating crontab-based periodic task
+--------------------------------------------
+
+A crontab schedule has the fields: ``minute``, ``hour``, ``day_of_week``,
+``day_of_month`` and ``month_of_year`, so if you want the equivalent
+of a ``30 * * * *`` (execute at 30 minutes past the hour every hour) crontab 
+entry you specify::
+
+    >>> from django_celery_beat.models import CrontabSchedule, PeriodicTask
+    >>> schedule, _ = CrontabSchedule.objects.get_or_create(
+    ...     minute='30',
+    ...     hour='*',
+    ...     day_of_week='*',
+    ...     day_of_month='*',
+    ...     month_of_year='*',
+    ... )
+
+
+Then to create a periodic task using this schedule, use the same approach as
+the interval-based periodic task earlier in this document, but instead
+of ``interval=schedule``, specify ``crontab=schedule``::
+
+    >>> PeriodicTask.objects.create(
+    ...     crontab=schedule,
+    ...     name='Importing contacts',
+    ...     task='proj.tasks.import_contacts',
+    ... )
+
+Temporarily disable a periodic task
+-----------------------------------
+
+You can use the ``enabled`` flag to temporarily disable a periodic task::
+
+    >>> periodic_task.enabled = False
+    >>> periodic_task.save()

+ 32 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/index.rst

@@ -0,0 +1,32 @@
+=======================================================================
+ django-celery-beat - Database-backed Periodic Tasks
+=======================================================================
+
+.. include:: includes/introduction.txt
+
+Contents
+========
+
+.. toctree::
+    :maxdepth: 1
+
+    copyright
+
+.. toctree::
+    :maxdepth: 2
+
+    reference/index
+
+.. toctree::
+    :maxdepth: 1
+
+    changelog
+    glossary
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+

+ 272 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/make.bat

@@ -0,0 +1,272 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	:help
+	echo.Please use `make ^<target^>` where ^<target^> is one of
+	echo.  html       to make standalone HTML files
+	echo.  dirhtml    to make HTML files named index.html in directories
+	echo.  singlehtml to make a single large HTML file
+	echo.  pickle     to make pickle files
+	echo.  json       to make JSON files
+	echo.  htmlhelp   to make HTML files and a HTML help project
+	echo.  qthelp     to make HTML files and a qthelp project
+	echo.  devhelp    to make HTML files and a Devhelp project
+	echo.  epub       to make an epub
+	echo.  epub3      to make an epub3
+	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	echo.  text       to make text files
+	echo.  man        to make manual pages
+	echo.  texinfo    to make Texinfo files
+	echo.  gettext    to make PO message catalogs
+	echo.  changes    to make an overview over all changed/added/deprecated items
+	echo.  xml        to make Docutils-native XML files
+	echo.  pseudoxml  to make pseudoxml-XML files for display purposes
+	echo.  linkcheck  to check all external links for integrity
+	echo.  doctest    to run all doctests embedded in the documentation if enabled
+	echo.  coverage   to run coverage check of the documentation if enabled
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+	del /q /s %BUILDDIR%\*
+	goto end
+)
+
+
+REM Check if sphinx-build is available and fallback to Python version if any
+%SPHINXBUILD% 1>NUL 2>NUL
+if errorlevel 9009 goto sphinx_python
+goto sphinx_ok
+
+:sphinx_python
+
+set SPHINXBUILD=python -m sphinx.__init__
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+:sphinx_ok
+
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+	goto end
+)
+
+if "%1" == "singlehtml" (
+	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PROJ.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PROJ.ghc
+	goto end
+)
+
+if "%1" == "devhelp" (
+	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished.
+	goto end
+)
+
+if "%1" == "epub" (
+	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The epub file is in %BUILDDIR%/epub.
+	goto end
+)
+
+if "%1" == "epub3" (
+	%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "latexpdf" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	cd %BUILDDIR%/latex
+	make all-pdf
+	cd %~dp0
+	echo.
+	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "latexpdfja" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	cd %BUILDDIR%/latex
+	make all-pdf-ja
+	cd %~dp0
+	echo.
+	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "text" (
+	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The text files are in %BUILDDIR%/text.
+	goto end
+)
+
+if "%1" == "man" (
+	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The manual pages are in %BUILDDIR%/man.
+	goto end
+)
+
+if "%1" == "texinfo" (
+	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+	goto end
+)
+
+if "%1" == "gettext" (
+	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.The overview file is in %BUILDDIR%/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+	goto end
+)
+
+if "%1" == "coverage" (
+	%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Testing of coverage in the sources finished, look at the ^
+results in %BUILDDIR%/coverage/python.txt.
+	goto end
+)
+
+if "%1" == "xml" (
+	%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The XML files are in %BUILDDIR%/xml.
+	goto end
+)
+
+if "%1" == "pseudoxml" (
+	%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+	goto end
+)
+
+:end

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.admin.rst

@@ -0,0 +1,11 @@
+=====================================================
+ ``django_celery_beat.admin``
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: django_celery_beat.admin
+
+.. automodule:: django_celery_beat.admin
+    :members:
+    :undoc-members:

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.managers.rst

@@ -0,0 +1,11 @@
+=====================================================
+ ``django_celery_beat.managers``
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: django_celery_beat.managers
+
+.. automodule:: django_celery_beat.managers
+    :members:
+    :undoc-members:

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.models.rst

@@ -0,0 +1,11 @@
+=====================================================
+ ``django_celery_beat.models``
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: django_celery_beat.models
+
+.. automodule:: django_celery_beat.models
+    :members:
+    :undoc-members:

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.rst

@@ -0,0 +1,11 @@
+=====================================================
+ ``django_celery_beat``
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: django_celery_beat
+
+.. automodule:: django_celery_beat
+    :members:
+    :undoc-members:

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.schedulers.rst

@@ -0,0 +1,11 @@
+=====================================================
+ ``django_celery_beat.schedulers``
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: django_celery_beat.schedulers
+
+.. automodule:: django_celery_beat.schedulers
+    :members:
+    :undoc-members:

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.tzcrontab.rst

@@ -0,0 +1,11 @@
+=====================================================
+ ``django_celery_beat.tzcrontab``
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: django_celery_beat.tzcrontab
+
+.. automodule:: django_celery_beat.tzcrontab
+    :members:
+    :undoc-members:

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.utils.rst

@@ -0,0 +1,11 @@
+=====================================================
+ ``django_celery_beat.utils``
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: django_celery_beat.utils
+
+.. automodule:: django_celery_beat.utils
+    :members:
+    :undoc-members:

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/django-celery-beat.validators.rst

@@ -0,0 +1,11 @@
+=====================================================
+ ``django_celery_beat.validators``
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: django_celery_beat.validators
+
+.. automodule:: django_celery_beat.validators
+    :members:
+    :undoc-members:

+ 20 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/reference/index.rst

@@ -0,0 +1,20 @@
+.. _apiref:
+
+===============
+ API Reference
+===============
+
+:Release: |version|
+:Date: |today|
+
+.. toctree::
+    :maxdepth: 1
+
+    django-celery-beat
+    django-celery-beat.models
+    django-celery-beat.tzcrontab
+    django-celery-beat.managers
+    django-celery-beat.schedulers
+    django-celery-beat.admin
+    django-celery-beat.utils
+    django-celery-beat.validators

+ 32 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/docs/templates/readme.txt

@@ -0,0 +1,32 @@
+=====================================================================
+ Database-backed Periodic Tasks
+=====================================================================
+
+|build-status| |coverage| |license| |wheel| |pyversion| |pyimp|
+
+.. include:: ../includes/introduction.txt
+
+.. include:: ../includes/installation.txt
+
+.. |build-status| image:: https://secure.travis-ci.org/celery/django-celery-beat.svg?branch=master
+    :alt: Build status
+    :target: https://travis-ci.org/celery/django-celery-beat
+
+.. |coverage| image:: https://codecov.io/github/celery/django-celery-beat/coverage.svg?branch=master
+    :target: https://codecov.io/github/celery/django-celery-beat?branch=master
+
+.. |license| image:: https://img.shields.io/pypi/l/django-celery-beat.svg
+    :alt: BSD License
+    :target: https://opensource.org/licenses/BSD-3-Clause
+
+.. |wheel| image:: https://img.shields.io/pypi/wheel/django-celery-beat.svg
+    :alt: django-celery-beat can be installed via wheel
+    :target: http://pypi.python.org/pypi/django-celery-beat/
+
+.. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-celery-beat.svg
+    :alt: Supported Python versions.
+    :target: http://pypi.python.org/pypi/django-celery-beat/
+
+.. |pyimp| image:: https://img.shields.io/pypi/implementation/django-celery-beat.svg
+    :alt: Support Python implementations.
+    :target: http://pypi.python.org/pypi/django-celery-beat/

+ 11 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/manage.py

@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from __future__ import absolute_import, unicode_literals
+import os
+import sys
+
+if __name__ == '__main__':
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings')
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)

+ 3 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/requirements/default.txt

@@ -0,0 +1,3 @@
+celery>=4.0,<5.0
+django-timezone-field>=2.0
+python-crontab>=2.3.4

+ 2 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/requirements/docs.txt

@@ -0,0 +1,2 @@
+https://github.com/celery/sphinx_celery/archive/master.zip
+Django>=1.10

+ 8 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/requirements/pkgutils.txt

@@ -0,0 +1,8 @@
+setuptools>=20.6.7
+wheel>=0.29.0
+flake8>=2.5.4
+flakeplus>=1.1
+tox>=2.3.1
+sphinx2rst>=1.0
+bumpversion
+pydocstyle

+ 2 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test-ci.txt

@@ -0,0 +1,2 @@
+pytest-cov
+codecov

+ 1 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test-django.txt

@@ -0,0 +1 @@
+django

+ 1 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test-django111.txt

@@ -0,0 +1 @@
+django>=1.11,<2.0

+ 1 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test-django20.txt

@@ -0,0 +1 @@
+django>=2.0,<2.1

+ 4 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/requirements/test.txt

@@ -0,0 +1,4 @@
+case>=1.3.1
+pytest>=3.0
+pytest-django
+pytz>dev

+ 19 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/setup.cfg

@@ -0,0 +1,19 @@
+[tool:pytest]
+testpaths = t/unit/
+python_classes = test_*
+django_settings_module = t.proj.settings
+
+[flake8]
+ignore = N806, N802, N801, N803, W503, W504
+
+[pep257]
+ignore = D102,D104,D203,D105,D213
+match-dir = [^migrations]
+
+[wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 160 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/setup.py

@@ -0,0 +1,160 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import re
+import sys
+import codecs
+
+import setuptools
+import setuptools.command.test
+
+try:
+    import platform
+    _pyimp = platform.python_implementation
+except (AttributeError, ImportError):
+    def _pyimp():
+        return 'Python'
+
+NAME = 'django-celery-beat'
+PACKAGE = 'django_celery_beat'
+
+E_UNSUPPORTED_PYTHON = '%s 1.0 requires %%s %%s or later!' % (NAME,)
+
+PYIMP = _pyimp()
+PY26_OR_LESS = sys.version_info < (2, 7)
+PY3 = sys.version_info[0] == 3
+PY33_OR_LESS = PY3 and sys.version_info < (3, 4)
+PYPY_VERSION = getattr(sys, 'pypy_version_info', None)
+PYPY = PYPY_VERSION is not None
+PYPY24_ATLEAST = PYPY_VERSION and PYPY_VERSION >= (2, 4)
+
+if PY26_OR_LESS:
+    raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '2.7'))
+elif PY33_OR_LESS and not PYPY24_ATLEAST:
+    raise Exception(E_UNSUPPORTED_PYTHON % (PYIMP, '3.4'))
+
+# -*- Classifiers -*-
+
+classes = """
+    Development Status :: 5 - Production/Stable
+    License :: OSI Approved :: BSD License
+    Programming Language :: Python
+    Programming Language :: Python :: 2
+    Programming Language :: Python :: 2.7
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3.4
+    Programming Language :: Python :: 3.5
+    Programming Language :: Python :: Implementation :: CPython
+    Programming Language :: Python :: Implementation :: PyPy
+    Framework :: Django
+    Framework :: Django :: 1.8
+    Framework :: Django :: 1.9
+    Framework :: Django :: 1.10
+    Framework :: Django :: 1.11
+    Framework :: Django :: 2.0
+    Operating System :: OS Independent
+    Topic :: Communications
+    Topic :: System :: Distributed Computing
+    Topic :: Software Development :: Libraries :: Python Modules
+"""
+classifiers = [s.strip() for s in classes.split('\n') if s]
+
+# -*- Distribution Meta -*-
+
+re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)')
+re_doc = re.compile(r'^"""(.+?)"""')
+
+
+def add_default(m):
+    attr_name, attr_value = m.groups()
+    return ((attr_name, attr_value.strip("\"'")),)
+
+
+def add_doc(m):
+    return (('doc', m.groups()[0]),)
+
+pats = {re_meta: add_default,
+        re_doc: add_doc}
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, PACKAGE, '__init__.py')) as meta_fh:
+    meta = {}
+    for line in meta_fh:
+        if line.strip() == '# -eof meta-':
+            break
+        for pattern, handler in pats.items():
+            m = pattern.match(line.strip())
+            if m:
+                meta.update(handler(m))
+
+# -*- Installation Requires -*-
+
+def strip_comments(l):
+    return l.split('#', 1)[0].strip()
+
+
+def _pip_requirement(req):
+    if req.startswith('-r '):
+        _, path = req.split()
+        return reqs(*path.split('/'))
+    return [req]
+
+
+def _reqs(*f):
+    return [
+        _pip_requirement(r) for r in (
+            strip_comments(l) for l in open(
+                os.path.join(os.getcwd(), 'requirements', *f)).readlines()
+        ) if r]
+
+
+def reqs(*f):
+    return [req for subreq in _reqs(*f) for req in subreq]
+
+# -*- Long Description -*-
+
+if os.path.exists('README.rst'):
+    long_description = codecs.open('README.rst', 'r', 'utf-8').read()
+else:
+    long_description = 'See http://pypi.python.org/pypi/%s' % (NAME,)
+
+# -*- %%% -*-
+
+
+class pytest(setuptools.command.test.test):
+    user_options = [('pytest-args=', 'a', 'Arguments to pass to py.test')]
+
+    def initialize_options(self):
+        setuptools.command.test.test.initialize_options(self)
+        self.pytest_args = []
+
+    def run_tests(self):
+        import pytest
+        sys.exit(pytest.main(self.pytest_args))
+
+setuptools.setup(
+    name=NAME,
+    packages=setuptools.find_packages(exclude=[
+        'ez_setup', 't', 't.*',
+    ]),
+    version=meta['version'],
+    description=meta['doc'],
+    long_description=long_description,
+    keywords='django celery beat periodic task database',
+    author=meta['author'],
+    author_email=meta['contact'],
+    url=meta['homepage'],
+    platforms=['any'],
+    license='BSD',
+    install_requires=reqs('default.txt'),
+    tests_require=reqs('test.txt') + reqs('test-django.txt'),
+    cmdclass={'test': pytest},
+    classifiers=classifiers,
+    entry_points={
+        'celery.beat_schedulers': [
+            'django = django_celery_beat.schedulers:DatabaseScheduler',
+        ],
+    },
+    include_package_data=False,
+    zip_safe=False,
+)

+ 0 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/__init__.py


+ 1 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/__init__.py

@@ -0,0 +1 @@
+from .celery import app as celery_app  # noqa

+ 15 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/celery.py

@@ -0,0 +1,15 @@
+from __future__ import absolute_import, unicode_literals
+
+import os
+
+from celery import Celery
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't.proj.settings')
+
+app = Celery('proj')
+
+# Using a string here means the worker doesn't have to serialize
+# the configuration object.
+app.config_from_object('django.conf:settings', namespace='CELERY')
+
+app.autodiscover_tasks()

+ 125 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/settings.py

@@ -0,0 +1,125 @@
+"""
+Django settings for Test project.
+
+Generated by 'django-admin startproject' using Django 1.9.1.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.9/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.9/ref/settings/
+"""
+from __future__ import absolute_import, unicode_literals
+
+import os
+import sys
+
+CELERY_DEFAULT_EXCHANGE = 'testcelery'
+CELERY_DEFAULT_ROUTING_KEY = 'testcelery'
+CELERY_DEFAULT_QUEUE = 'testcelery'
+
+CELERY_QUEUES = {'testcelery': {'binding_key': 'testcelery'}}
+
+CELERY_ACCEPT_CONTENT = ['pickle', 'json']
+CELERY_TASK_SERIALIZER = 'pickle'
+CELERY_RESULT_SERIALIZER = 'pickle'
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+sys.path.insert(0, os.path.abspath(os.path.join(BASE_DIR, os.pardir)))
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'u($kbs9$irs0)436gbo9%!b&#zyd&70tx!n7!i&fl6qun@z1_l'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'django_celery_beat',
+]
+
+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 = 't.proj.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 't.proj.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+        'OPTIONS': {
+            'timeout': 1000,
+        },
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
+
+django_auth = 'django.contrib.auth.password_validation.'
+
+AUTH_PASSWORD_VALIDATORS = [
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.9/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.9/howto/static-files/
+
+STATIC_URL = '/static/'
+DJANGO_CELERY_BEAT_TZ_AWARE = True

+ 8 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/urls.py

@@ -0,0 +1,8 @@
+from __future__ import absolute_import, unicode_literals
+
+from django.conf.urls import url
+from django.contrib import admin
+
+urlpatterns = [
+    url(r'^admin/', admin.site.urls),
+]

+ 17 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/proj/wsgi.py

@@ -0,0 +1,17 @@
+"""
+WSGI config for Test project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
+"""
+from __future__ import absolute_import, unicode_literals
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "proj.settings")
+
+application = get_wsgi_application()

+ 0 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/__init__.py


+ 36 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/conftest.py

@@ -0,0 +1,36 @@
+from __future__ import absolute_import, unicode_literals
+
+import pytest
+
+from celery.contrib.testing.app import TestApp, Trap
+from celery.contrib.pytest import depends_on_current_app
+
+__all__ = ['app', 'depends_on_current_app']
+
+
+@pytest.fixture(scope='session', autouse=True)
+def setup_default_app_trap():
+    from celery._state import set_default_app
+    set_default_app(Trap())
+
+
+@pytest.fixture()
+def app(celery_app):
+    return celery_app
+
+
+@pytest.fixture(autouse=True)
+def test_cases_shortcuts(request, app, patching):
+    if request.instance:
+        @app.task
+        def add(x, y):
+            return x + y
+
+        # IMPORTANT: We set an .app attribute for every test case class.
+        request.instance.app = app
+        request.instance.Celery = TestApp
+        request.instance.add = add
+        request.instance.patching = patching
+    yield
+    if request.instance:
+        request.instance.app = None

+ 78 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/test_admin.py

@@ -0,0 +1,78 @@
+from __future__ import absolute_import, unicode_literals
+
+from itertools import combinations
+from django.test import TestCase
+
+from django_celery_beat.admin import PeriodicTaskAdmin
+from django_celery_beat.models import \
+    PeriodicTask, \
+    CrontabSchedule, \
+    IntervalSchedule, \
+    SolarSchedule
+from django.core.exceptions import ValidationError
+
+
+class ActionsTests(TestCase):
+    def test_toggle_action(self):
+        PeriodicTask.objects.create(name='name1', task='task1', enabled=False)
+        PeriodicTask.objects.create(name='name2', task='task2', enabled=True)
+        PeriodicTask.objects.create(name='name3', task='task3', enabled=False)
+
+        qs = PeriodicTask.objects.all()
+        PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs)
+
+        e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled
+        e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled
+        e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled
+        self.assertTrue(e1)
+        self.assertFalse(e2)
+        self.assertTrue(e3)
+
+    def test_toggle_action_all_enabled(self):
+        PeriodicTask.objects.create(name='name1', task='task1', enabled=True)
+        PeriodicTask.objects.create(name='name2', task='task2', enabled=True)
+        PeriodicTask.objects.create(name='name3', task='task3', enabled=True)
+
+        qs = PeriodicTask.objects.all()
+        PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs)
+
+        e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled
+        e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled
+        e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled
+        self.assertFalse(e1)
+        self.assertFalse(e2)
+        self.assertFalse(e3)
+
+    def test_toggle_action_all_disabled(self):
+        PeriodicTask.objects.create(name='name1', task='task1', enabled=False)
+        PeriodicTask.objects.create(name='name2', task='task2', enabled=False)
+        PeriodicTask.objects.create(name='name3', task='task3', enabled=False)
+
+        qs = PeriodicTask.objects.all()
+        PeriodicTaskAdmin(PeriodicTask, None)._toggle_tasks_activity(qs)
+
+        e1 = PeriodicTask.objects.get(name='name1', task='task1').enabled
+        e2 = PeriodicTask.objects.get(name='name2', task='task2').enabled
+        e3 = PeriodicTask.objects.get(name='name3', task='task3').enabled
+        self.assertTrue(e1)
+        self.assertTrue(e2)
+        self.assertTrue(e3)
+
+    def test_validate_unique_raises_if_schedule_not_set(self):
+        with self.assertRaises(ValidationError):
+            PeriodicTask().validate_unique()
+
+    def test_validate_unique_raises_for_multiple_schedules(self):
+        schedules = [
+            ('crontab', CrontabSchedule()),
+            ('interval', IntervalSchedule()),
+            ('solar', SolarSchedule()),
+        ]
+        for options in combinations(schedules, 2):
+            with self.assertRaises(ValidationError):
+                PeriodicTask(**dict(options)).validate_unique()
+
+    def test_validate_unique_not_raises(self):
+        PeriodicTask(crontab=CrontabSchedule()).validate_unique()
+        PeriodicTask(interval=IntervalSchedule()).validate_unique()
+        PeriodicTask(solar=SolarSchedule()).validate_unique()

+ 373 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/test_crontabs.py

@@ -0,0 +1,373 @@
+from __future__ import absolute_import, unicode_literals
+
+from unittest import TestCase
+
+from django.core.exceptions import ValidationError
+
+from django_celery_beat import validators
+
+
+class MinuteTests(TestCase):
+    def test_good(self):
+        try:
+            validators.minute_validator('*')
+            validators.minute_validator('0')
+            validators.minute_validator('1')
+            validators.minute_validator('54')
+            validators.minute_validator('59')
+
+            validators.minute_validator('1,2,59')
+            validators.minute_validator('43,2')
+            validators.minute_validator('5,20,25,43')
+
+            validators.minute_validator('1-4')
+            validators.minute_validator('1-29')
+
+            validators.minute_validator('45-59')
+            validators.minute_validator('*/4')
+            validators.minute_validator('*/43')
+            validators.minute_validator('1-2/43')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_space(self):
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('1, 2')
+
+    def test_big_number(self):
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('60')
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('420')
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('100500')
+
+    def test_text(self):
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('fsd')
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('.')
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('432a')
+
+    def test_out_range(self):
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('0-432')
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('342-432')
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('4-60')
+
+    def test_bad_range(self):
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('10-4')
+
+    def test_bad_slice(self):
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('*/100')
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('10/30')
+        with self.assertRaises(ValidationError):
+            validators.minute_validator('10-20/100')
+
+
+class HourTests(TestCase):
+    def test_good(self):
+        try:
+            validators.hour_validator('*')
+            validators.hour_validator('0')
+            validators.hour_validator('1')
+            validators.hour_validator('22')
+            validators.hour_validator('23')
+
+            validators.hour_validator('1,2,23')
+            validators.hour_validator('23,2')
+            validators.hour_validator('5,20,21,22')
+
+            validators.hour_validator('1-4')
+            validators.hour_validator('1-23')
+
+            validators.hour_validator('*/4')
+            validators.hour_validator('*/22')
+            validators.hour_validator('1-2/5')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_space(self):
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('1, 2')
+
+    def test_big_number(self):
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('24')
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('420')
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('100500')
+
+    def test_text(self):
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('fsd')
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('.')
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('432a')
+
+    def test_out_range(self):
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('0-24')
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('342-432')
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('4-25')
+
+    def test_bad_range(self):
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('10-4')
+
+    def test_bad_slice(self):
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('*/100')
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('10/30')
+        with self.assertRaises(ValidationError):
+            validators.hour_validator('10-20/100')
+
+
+class DayOfMonthTests(TestCase):
+    def test_good(self):
+        try:
+            validators.day_of_month_validator('*')
+            validators.day_of_month_validator('1')
+            validators.day_of_month_validator('29')
+            validators.day_of_month_validator('31')
+
+            validators.day_of_month_validator('1,2,31')
+            validators.day_of_month_validator('30,2')
+            validators.day_of_month_validator('5,20,25,31')
+
+            validators.day_of_month_validator('1-4')
+            validators.day_of_month_validator('1-30')
+
+            validators.day_of_month_validator('*/4')
+            validators.day_of_month_validator('*/22')
+            validators.day_of_month_validator('1-2/5')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_space(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('1, 2')
+
+    def test_zero(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('0')
+
+    def test_big_number(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('32')
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('420')
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('100500')
+
+    def test_text(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('fsd')
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('.')
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('432a')
+
+    def test_out_range(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('0-32')
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('342-432')
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('4-33')
+
+    def test_bad_range(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('10-4')
+
+    def test_bad_slice(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('*/100')
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('10/30')
+        with self.assertRaises(ValidationError):
+            validators.day_of_month_validator('10-20/100')
+
+
+class MonthTests(TestCase):
+    def test_good(self):
+        try:
+            validators.month_of_year_validator('*')
+            validators.month_of_year_validator('1')
+            validators.month_of_year_validator('10')
+            validators.month_of_year_validator('12')
+
+            validators.month_of_year_validator('1,2,12')
+            validators.month_of_year_validator('12,2')
+            validators.month_of_year_validator('5,10,11,12')
+
+            validators.month_of_year_validator('1-4')
+            validators.month_of_year_validator('1-12')
+
+            validators.month_of_year_validator('*/4')
+            validators.month_of_year_validator('*/12')
+            validators.month_of_year_validator('1-2/12')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_good_month_name(self):
+        try:
+            validators.month_of_year_validator('jan')
+            validators.month_of_year_validator('feb')
+            validators.month_of_year_validator('mar')
+            validators.month_of_year_validator('apr')
+            validators.month_of_year_validator('may')
+            validators.month_of_year_validator('jun')
+            validators.month_of_year_validator('jul')
+            validators.month_of_year_validator('aug')
+            validators.month_of_year_validator('sep')
+            validators.month_of_year_validator('oct')
+            validators.month_of_year_validator('nov')
+            validators.month_of_year_validator('dec')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_good_month_name_case(self):
+        try:
+            validators.month_of_year_validator('jan')
+            validators.month_of_year_validator('JAN')
+            validators.month_of_year_validator('JaN')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_space(self):
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('1, 2')
+
+    def test_zero(self):
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('0')
+
+    def test_big_number(self):
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('13')
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('420')
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('100500')
+
+    def test_text(self):
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('fsd')
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('.')
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('432a')
+
+    def test_out_range(self):
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('0-13')
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('342-432')
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('4-14')
+
+    def test_bad_range(self):
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('10-4')
+
+    def test_bad_slice(self):
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('*/13')
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('10/30')
+        with self.assertRaises(ValidationError):
+            validators.month_of_year_validator('10-20/100')
+
+
+class DayOfWeekTests(TestCase):
+    def test_good(self):
+        try:
+            validators.day_of_week_validator('*')
+            validators.day_of_week_validator('1')
+            validators.day_of_week_validator('6')
+            validators.day_of_week_validator('7')
+
+            validators.day_of_week_validator('1,2,6')
+            validators.day_of_week_validator('6,2')
+            validators.day_of_week_validator('5,6,4,6')
+
+            validators.day_of_week_validator('1-4')
+            validators.day_of_week_validator('1-7')
+
+            validators.day_of_week_validator('*/4')
+            validators.day_of_week_validator('*/6')
+            validators.day_of_week_validator('2-7/5')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_good_month_name(self):
+        try:
+            validators.day_of_week_validator('sun')
+            validators.day_of_week_validator('mon')
+            validators.day_of_week_validator('tue')
+            validators.day_of_week_validator('wed')
+            validators.day_of_week_validator('thu')
+            validators.day_of_week_validator('fri')
+            validators.day_of_week_validator('sat')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_good_month_name_case(self):
+        try:
+            validators.day_of_week_validator('mon')
+            validators.day_of_week_validator('MoN')
+            validators.day_of_week_validator('MON')
+        except ValidationError as e:
+            self.fail(e)
+
+    def test_space(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('1, 2')
+
+    def test_big_number(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('8')
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('420')
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('100500')
+
+    def test_text(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('fsd')
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('.')
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('432a')
+
+    def test_out_range(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('0-32')
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('342-432')
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('4-33')
+
+    def test_bad_range(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('10-4')
+
+    def test_bad_slice(self):
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('*/8')
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('10/30')
+        with self.assertRaises(ValidationError):
+            validators.day_of_week_validator('10-20/100')

+ 642 - 0
desktop/core/ext-py/django-celery-beat-1.4.0/t/unit/test_schedulers.py

@@ -0,0 +1,642 @@
+from __future__ import absolute_import, unicode_literals
+
+import time
+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, 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)
+
+
+@pytest.fixture(autouse=True)
+def no_multiprocessing_finalizers(patching):
+    patching('multiprocessing.util.Finalize')
+    patching('django_celery_beat.schedulers.Finalize')
+
+
+class EntryTrackSave(schedulers.ModelEntry):
+
+    def __init__(self, *args, **kwargs):
+        self.saved = 0
+        super(EntryTrackSave, self).__init__(*args, **kwargs)
+
+    def save(self):
+        self.saved += 1
+        super(EntryTrackSave, self).save()
+
+
+class EntrySaveRaises(schedulers.ModelEntry):
+
+    def save(self):
+        raise RuntimeError('this is expected')
+
+
+class TrackingScheduler(schedulers.DatabaseScheduler):
+    Entry = EntryTrackSave
+
+    def __init__(self, *args, **kwargs):
+        self.flushed = 0
+        schedulers.DatabaseScheduler.__init__(self, *args, **kwargs)
+
+    def sync(self):
+        self.flushed += 1
+        schedulers.DatabaseScheduler.sync(self)
+
+
+@pytest.mark.django_db()
+class SchedulerCase:
+
+    def create_model_interval(self, schedule, **kwargs):
+        interval = IntervalSchedule.from_schedule(schedule)
+        interval.save()
+        return self.create_model(interval=interval, **kwargs)
+
+    def create_model_crontab(self, schedule, **kwargs):
+        crontab = CrontabSchedule.from_schedule(schedule)
+        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)),
+            task='djcelery.unittest.add{0}'.format(next(_ids)),
+            args='[2, 2]',
+            kwargs='{"callback": "foo"}',
+            queue='xaz',
+            routing_key='cpu',
+            exchange='foo',
+        )
+        return Model(**dict(entry, **kwargs))
+
+
+@pytest.mark.django_db()
+class test_ModelEntry(SchedulerCase):
+    Entry = EntryTrackSave
+
+    def test_entry(self):
+        m = self.create_model_interval(schedule(timedelta(seconds=10)))
+        e = self.Entry(m, app=self.app)
+
+        assert e.args == [2, 2]
+        assert e.kwargs == {'callback': 'foo'}
+        assert e.schedule
+        assert e.total_run_count == 0
+        assert isinstance(e.last_run_at, datetime)
+        assert e.options['queue'] == 'xaz'
+        assert e.options['exchange'] == 'foo'
+        assert e.options['routing_key'] == 'cpu'
+
+        right_now = self.app.now()
+        m2 = self.create_model_interval(
+            schedule(timedelta(seconds=10)),
+            last_run_at=right_now,
+        )
+
+        assert m2.last_run_at
+        e2 = self.Entry(m2, app=self.app)
+        assert e2.last_run_at is right_now
+
+        e3 = e2.next()
+        assert e3.last_run_at > e2.last_run_at
+        assert e3.total_run_count == 1
+
+    def test_task_with_start_time(self):
+        interval = 10
+        right_now = self.app.now()
+        one_interval_ago = right_now - timedelta(seconds=interval)
+        m = self.create_model_interval(schedule(timedelta(seconds=interval)),
+                                       start_time=right_now,
+                                       last_run_at=one_interval_ago)
+        e = self.Entry(m, app=self.app)
+        isdue, delay = e.is_due()
+        assert isdue
+        assert delay == interval
+
+        tomorrow = right_now + timedelta(days=1)
+        m2 = self.create_model_interval(schedule(timedelta(seconds=interval)),
+                                        start_time=tomorrow,
+                                        last_run_at=one_interval_ago)
+        e2 = self.Entry(m2, app=self.app)
+        isdue, delay = e2.is_due()
+        assert not isdue
+        assert delay == interval
+
+    def test_one_off_task(self):
+        interval = 10
+        right_now = self.app.now()
+        one_interval_ago = right_now - timedelta(seconds=interval)
+        m = self.create_model_interval(schedule(timedelta(seconds=interval)),
+                                       one_off=True,
+                                       last_run_at=one_interval_ago,
+                                       total_run_count=0)
+        e = self.Entry(m, app=self.app)
+        isdue, delay = e.is_due()
+        assert isdue
+        assert delay == interval
+
+        m2 = self.create_model_interval(schedule(timedelta(seconds=interval)),
+                                        one_off=True,
+                                        last_run_at=one_interval_ago,
+                                        total_run_count=1)
+        e2 = self.Entry(m2, app=self.app)
+        isdue, delay = e2.is_due()
+        assert not isdue
+        assert delay is None
+
+
+@pytest.mark.django_db()
+class test_DatabaseSchedulerFromAppConf(SchedulerCase):
+    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
+
+
+@pytest.mark.django_db()
+class test_DatabaseScheduler(SchedulerCase):
+    Scheduler = TrackingScheduler
+
+    @pytest.mark.django_db()
+    @pytest.fixture(autouse=True)
+    def setup_scheduler(self, app):
+        self.app = app
+        self.app.conf.beat_schedule = {}
+
+        self.m1 = self.create_model_interval(
+            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(
+            crontab(minute='2,4,5'))
+        self.m3.save()
+        self.m3.refresh_from_db()
+
+        self.m4 = self.create_model_solar(
+            solar('solar_noon', 48.06, 12.86))
+        self.m4.save()
+        self.m4.refresh_from_db()
+
+        # disabled, should not be in schedule
+        m5 = self.create_model_interval(
+            schedule(timedelta(seconds=1)))
+        m5.enabled = False
+        m5.save()
+
+        self.s = self.Scheduler(app=self.app)
+
+    def test_constructor(self):
+        assert isinstance(self.s._dirty, set)
+        assert self.s._last_sync is None
+        assert self.s.sync_every
+
+    def test_all_as_schedule(self):
+        sched = self.s.schedule
+        assert sched
+        assert len(sched) == 5
+        assert 'celery.backend_cleanup' in sched
+        for n, e in sched.items():
+            assert isinstance(e, self.s.Entry)
+
+    def test_schedule_changed(self):
+        self.m2.args = '[16, 16]'
+        self.m2.save()
+        e2 = self.s.schedule[self.m2.name]
+        assert e2.args == [16, 16]
+
+        self.m1.args = '[32, 32]'
+        self.m1.save()
+        e1 = self.s.schedule[self.m1.name]
+        assert e1.args == [32, 32]
+        e1 = self.s.schedule[self.m1.name]
+        assert e1.args == [32, 32]
+
+        self.m3.delete()
+        with pytest.raises(KeyError):
+            self.s.schedule.__getitem__(self.m3.name)
+
+    def test_should_sync(self):
+        assert self.s.should_sync()
+        self.s._last_sync = monotonic()
+        assert not self.s.should_sync()
+        self.s._last_sync -= self.s.sync_every
+        assert self.s.should_sync()
+
+    def test_reserve(self):
+        e1 = self.s.schedule[self.m1.name]
+        self.s.schedule[self.m1.name] = self.s.reserve(e1)
+        assert self.s.flushed == 1
+
+        e2 = self.s.schedule[self.m2.name]
+        self.s.schedule[self.m2.name] = self.s.reserve(e2)
+        assert self.s.flushed == 1
+        assert self.m2.name in self.s._dirty
+
+    def test_sync_saves_last_run_at(self):
+        e1 = self.s.schedule[self.m2.name]
+        last_run = e1.last_run_at
+        last_run2 = last_run - timedelta(days=1)
+        e1.model.last_run_at = last_run2
+        self.s._dirty.add(self.m2.name)
+        self.s.sync()
+
+        e2 = self.s.schedule[self.m2.name]
+        assert e2.last_run_at == last_run2
+
+    def test_sync_syncs_before_save(self):
+        # Get the entry for m2
+        e1 = self.s.schedule[self.m2.name]
+
+        # Increment the entry (but make sure it doesn't sync)
+        self.s._last_sync = monotonic()
+        e2 = self.s.schedule[e1.name] = self.s.reserve(e1)
+        assert self.s.flushed == 1
+
+        # Fetch the raw object from db, change the args
+        # and save the changes.
+        m2 = PeriodicTask.objects.get(pk=self.m2.pk)
+        m2.args = '[16, 16]'
+        m2.save()
+
+        # get_schedule should now see the schedule has changed.
+        # and also sync the dirty objects.
+        e3 = self.s.schedule[self.m2.name]
+        assert self.s.flushed == 2
+        assert e3.last_run_at == e2.last_run_at
+        assert e3.args == [16, 16]
+
+    def test_periodic_task_disabled_and_enabled(self):
+        # Get the entry for m2
+        e1 = self.s.schedule[self.m2.name]
+
+        # Increment the entry (but make sure it doesn't sync)
+        self.s._last_sync = monotonic()
+        self.s.schedule[e1.name] = self.s.reserve(e1)
+        assert self.s.flushed == 1
+
+        # Fetch the raw object from db, change the args
+        # and save the changes.
+        m2 = PeriodicTask.objects.get(pk=self.m2.pk)
+        m2.enabled = False
+        m2.save()
+
+        # get_schedule should now see the schedule has changed.
+        # and remove entry for m2
+        assert self.m2.name not in self.s.schedule
+        assert self.s.flushed == 2
+
+        m2.enabled = True
+        m2.save()
+
+        # get_schedule should now see the schedule has changed.
+        # and add entry for m2
+        assert self.m2.name in self.s.schedule
+        assert self.s.flushed == 3
+
+    def test_periodic_task_disabled_while_reserved(self):
+        # Get the entry for m2
+        e1 = self.s.schedule[self.m2.name]
+
+        # Increment the entry (but make sure it doesn't sync)
+        self.s._last_sync = monotonic()
+        e2 = self.s.schedule[e1.name] = self.s.reserve(e1)
+        assert self.s.flushed == 1
+
+        # Fetch the raw object from db, change the args
+        # and save the changes.
+        m2 = PeriodicTask.objects.get(pk=self.m2.pk)
+        m2.enabled = False
+        m2.save()
+
+        # reserve is called because the task gets called from
+        # tick after the database change is made
+        self.s.reserve(e2)
+
+        # get_schedule should now see the schedule has changed.
+        # and remove entry for m2
+        assert self.m2.name not in self.s.schedule
+        assert self.s.flushed == 2
+
+    def test_sync_not_dirty(self):
+        self.s._dirty.clear()
+        self.s.sync()
+
+    def test_sync_object_gone(self):
+        self.s._dirty.add('does-not-exist')
+        self.s.sync()
+
+    def test_sync_rollback_on_save_error(self):
+        self.s.schedule[self.m1.name] = EntrySaveRaises(self.m1, app=self.app)
+        self.s._dirty.add(self.m1.name)
+        with pytest.raises(RuntimeError):
+            self.s.sync()
+
+    def test_update_scheduler_heap_invalidation(self, monkeypatch):
+        # mock "schedule_changed" to always trigger update for
+        # all calls to schedule, as a change may occur at any moment
+        monkeypatch.setattr(self.s, 'schedule_changed', lambda: True)
+        self.s.tick()
+
+    def test_heap_size_is_constant(self, monkeypatch):
+        # heap size is constant unless the schedule changes
+        monkeypatch.setattr(self.s, 'schedule_changed', lambda: True)
+        expected_heap_size = len(self.s.schedule.values())
+        self.s.tick()
+        assert len(self.s._heap) == expected_heap_size
+        self.s.tick()
+        assert len(self.s._heap) == expected_heap_size
+
+    def test_scheduler_schedules_equality_on_change(self, monkeypatch):
+        monkeypatch.setattr(self.s, 'schedule_changed', lambda: False)
+        assert self.s.schedules_equal(self.s.schedule, self.s.schedule)
+
+        monkeypatch.setattr(self.s, 'schedule_changed', lambda: True)
+        assert not self.s.schedules_equal(self.s.schedule, self.s.schedule)
+
+    def test_heap_always_return_the_first_item(self):
+        interval = 10
+
+        s1 = schedule(timedelta(seconds=interval))
+        m1 = self.create_model_interval(s1, enabled=False)
+        m1.last_run_at = self.app.now() - timedelta(seconds=interval + 2)
+        m1.save()
+        m1.refresh_from_db()
+
+        s2 = schedule(timedelta(seconds=interval))
+        m2 = self.create_model_interval(s2, enabled=True)
+        m2.last_run_at = self.app.now() - timedelta(seconds=interval + 1)
+        m2.save()
+        m2.refresh_from_db()
+
+        e1 = EntryTrackSave(m1, self.app)
+        # because the disabled task e1 runs first, e2 will never be executed
+        e2 = EntryTrackSave(m2, self.app)
+
+        s = self.Scheduler(app=self.app)
+        s.schedule.clear()
+        s.schedule[e1.name] = e1
+        s.schedule[e2.name] = e2
+
+        tried = set()
+        for _ in range(len(s.schedule) * 8):
+            tick_interval = s.tick()
+            if tick_interval and tick_interval > 0.0:
+                tried.add(s._heap[0].entry.name)
+                time.sleep(tick_interval)
+                if s.should_sync():
+                    s.sync()
+        assert len(tried) == 1 and tried == set([e1.name])
+
+
+@pytest.mark.django_db()
+class test_models(SchedulerCase):
+
+    def test_IntervalSchedule_unicode(self):
+        assert (text_t(IntervalSchedule(every=1, period='seconds'))
+                == 'every second')
+        assert (text_t(IntervalSchedule(every=10, period='seconds'))
+                == 'every 10 seconds')
+
+    def test_CrontabSchedule_unicode(self):
+        assert text_t(CrontabSchedule(
+            minute=3,
+            hour=3,
+            day_of_week=None,
+        )) == '3 3 * * * (m/h/d/dM/MY) UTC'
+        assert text_t(CrontabSchedule(
+            minute=3,
+            hour=3,
+            day_of_week='tue',
+            day_of_month='*/2',
+            month_of_year='4,6',
+        )) == '3 3 tue */2 4,6 (m/h/d/dM/MY) UTC'
+
+    def test_PeriodicTask_unicode_interval(self):
+        p = self.create_model_interval(schedule(timedelta(seconds=10)))
+        assert text_t(p) == '{0}: every 10.0 seconds'.format(p.name)
+
+    def test_PeriodicTask_unicode_crontab(self):
+        p = self.create_model_crontab(crontab(
+            hour='4, 5',
+            day_of_week='4, 5',
+        ))
+        assert text_t(p) == """{0}: * 4,5 4,5 * * (m/h/d/dM/MY) UTC""".format(
+            p.name
+        )
+
+    def test_PeriodicTask_unicode_solar(self):
+        p = self.create_model_solar(
+            solar('solar_noon', 48.06, 12.86), name='solar_event'
+        )
+        assert text_t(p) == 'solar_event: {0} ({1}, {2})'.format(
+            'solar_noon', '48.06', '12.86'
+        )
+
+    def test_PeriodicTask_schedule_property(self):
+        p1 = self.create_model_interval(schedule(timedelta(seconds=10)))
+        s1 = p1.schedule
+        assert s1.run_every.total_seconds() == 10
+
+        p2 = self.create_model_crontab(crontab(
+            hour='4, 5',
+            minute='10,20,30',
+            day_of_month='1-7',
+            month_of_year='*/3',
+        ))
+        s2 = p2.schedule
+        assert s2.hour == {4, 5}
+        assert s2.minute == {10, 20, 30}
+        assert s2.day_of_week == {0, 1, 2, 3, 4, 5, 6}
+        assert s2.day_of_month == {1, 2, 3, 4, 5, 6, 7}
+        assert s2.month_of_year == {1, 4, 7, 10}
+
+    def test_PeriodicTask_unicode_no_schedule(self):
+        p = self.create_model()
+        assert text_t(p) == '{0}: {{no schedule}}'.format(p.name)
+
+    def test_CrontabSchedule_schedule(self):
+        s = CrontabSchedule(
+            minute='3, 7',
+            hour='3, 4',
+            day_of_week='*',
+            day_of_month='1, 16',
+            month_of_year='1, 7',
+        )
+        assert s.schedule.minute == {3, 7}
+        assert s.schedule.hour == {3, 4}
+        assert s.schedule.day_of_week == {0, 1, 2, 3, 4, 5, 6}
+        assert s.schedule.day_of_month == {1, 16}
+        assert s.schedule.month_of_year == {1, 7}
+
+    def test_CrontabSchedule_long_schedule(self):
+        s = CrontabSchedule(
+            minute=str(list(range(60)))[1:-1],
+            hour=str(list(range(24)))[1:-1],
+            day_of_week=str(list(range(7)))[1:-1],
+            day_of_month=str(list(range(1, 32)))[1:-1],
+            month_of_year=str(list(range(1, 13)))[1:-1]
+        )
+        assert s.schedule.minute == set(range(60))
+        assert s.schedule.hour == set(range(24))
+        assert s.schedule.day_of_week == set(range(7))
+        assert s.schedule.day_of_month == set(range(1, 32))
+        assert s.schedule.month_of_year == set(range(1, 13))
+        fields = [
+            'minute', 'hour', 'day_of_week', 'day_of_month', 'month_of_year'
+        ]
+        for field in fields:
+            str_length = len(str(getattr(s.schedule, field)))
+            field_length = s._meta.get_field(field).max_length
+            assert str_length <= field_length
+
+    def test_SolarSchedule_schedule(self):
+        s = SolarSchedule(event='solar_noon', latitude=48.06, longitude=12.86)
+        dt = datetime(day=26, month=7, year=2050, hour=1, minute=0)
+        dt_lastrun = make_aware(dt)
+
+        assert s.schedule is not None
+        isdue, nextcheck = s.schedule.is_due(dt_lastrun)
+        assert isdue is False  # False means task isn't due, but keep checking.
+        assert (nextcheck > 0) and (isdue is False) or \
+            (nextcheck == s.max_interval) and (isdue is True)
+
+        s2 = SolarSchedule(event='solar_noon', latitude=48.06, longitude=12.86)
+        dt2 = datetime(day=26, month=7, year=2000, hour=1, minute=0)
+        dt2_lastrun = make_aware(dt2)
+
+        assert s2.schedule is not None
+        isdue2, nextcheck2 = s2.schedule.is_due(dt2_lastrun)
+        assert isdue2 is True  # True means task is due and should run.
+        assert (nextcheck2 > 0) and (isdue2 is True) or \
+            (nextcheck2 == s2.max_interval) and (isdue2 is False)
+
+
+@pytest.mark.django_db()
+class test_model_PeriodicTasks(SchedulerCase):
+
+    def test_track_changes(self):
+        assert PeriodicTasks.last_change() is None
+        m1 = self.create_model_interval(schedule(timedelta(seconds=10)))
+        m1.save()
+        x = PeriodicTasks.last_change()
+        assert x
+        m1.args = '(23, 24)'
+        m1.save()
+        y = PeriodicTasks.last_change()
+        assert y
+        assert y > x
+
+
+@pytest.mark.django_db()
+class test_modeladmin_PeriodicTaskAdmin(SchedulerCase):
+    @pytest.mark.django_db()
+    @pytest.fixture(autouse=True)
+    def setup_scheduler(self, app):
+        self.app = app
+        self.site = AdminSite()
+        self.request_factory = RequestFactory()
+
+        entry_name, entry = self.create_conf_entry()
+        self.app.conf.beat_schedule = {entry_name: entry}
+        self.m1 = PeriodicTask(name=entry_name)
+        self.m1.task = 'celery.backend_cleanup'
+        self.m1.save()
+
+        entry_name, entry = self.create_conf_entry()
+        self.app.conf.beat_schedule = {entry_name: entry}
+        self.m2 = PeriodicTask(name=entry_name)
+        self.m2.task = 'celery.backend_cleanup'
+        self.m2.save()
+
+    def patch_request(self, request):
+        """patch request to allow for django messages storage"""
+        setattr(request, 'session', 'session')
+        messages = FallbackStorage(request)
+        setattr(request, '_messages', messages)
+        return request
+
+    def test_run_task(self):
+        ma = PeriodicTaskAdmin(PeriodicTask, self.site)
+        self.request = self.patch_request(self.request_factory.get('/'))
+        ma.run_tasks(self.request, PeriodicTask.objects.filter(id=self.m1.id))
+        assert len(self.request._messages._queued_messages) == 1
+        queued_message = self.request._messages._queued_messages[0].message
+        assert queued_message == '1 task was successfully run'
+
+    def test_run_tasks(self):
+        ma = PeriodicTaskAdmin(PeriodicTask, self.site)
+        self.request = self.patch_request(self.request_factory.get('/'))
+        ma.run_tasks(self.request, PeriodicTask.objects.all())
+        assert len(self.request._messages._queued_messages) == 1
+        queued_message = self.request._messages._queued_messages[0].message
+        assert queued_message == '2 tasks were successfully run'