From d30c9ea0768b3f91bd93f8fcb762b75e4f637173 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Tue, 16 Jul 2019 14:59:25 +0200
Subject: [PATCH] cron: add option to force a specific job to run (#34906)

---
 tests/test_publisher.py                | 25 +++++++++++++++++++++++
 wcs/formdef.py                         |  1 +
 wcs/qommon/cron.py                     | 28 +++++++++++++++++---------
 wcs/qommon/management/commands/cron.py |  3 ++-
 4 files changed, 46 insertions(+), 11 deletions(-)

diff --git a/tests/test_publisher.py b/tests/test_publisher.py
index 0894d28ac..b973c6d02 100644
--- a/tests/test_publisher.py
+++ b/tests/test_publisher.py
@@ -16,7 +16,9 @@ from django.core.management.base import CommandError
 from django.http import Http404
 from django.test import override_settings
 from quixote import cleanup
+from wcs.qommon import get_publisher_class
 from wcs.qommon.http_request import HTTPRequest
+from wcs.qommon.cron import CronJob
 
 from utilities import create_temporary_pub
 
@@ -214,3 +216,26 @@ def test_cron_command():
         with mock.patch('wcs.qommon.management.commands.cron.cron_worker') as cron_worker:
             call_command('cron')
             assert cron_worker.call_count == 0
+
+    # run a specific job
+    jobs = []
+    def job1(pub):
+        jobs.append('job1')
+    def job2(pub):
+        jobs.append('job2')
+    def job3(pub):
+        jobs.append('job3')
+
+    @classmethod
+    def register_test_cronjobs(cls):
+        cls.register_cronjob(CronJob(job1, days=[10]))
+        cls.register_cronjob(CronJob(job2, name='job2', days=[10]))
+        cls.register_cronjob(CronJob(job3, name='job3', days=[10]))
+
+    with mock.patch('wcs.publisher.WcsPublisher.register_cronjobs', register_test_cronjobs):
+        get_publisher_class().cronjobs = []
+        call_command('cron', job_name='job1')
+        assert jobs == []
+        get_publisher_class().cronjobs = []
+        call_command('cron', job_name='job2')
+        assert jobs == ['job2']
diff --git a/wcs/formdef.py b/wcs/formdef.py
index d0c459438..86c3ae724 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -1544,4 +1544,5 @@ def clean_drafts(publisher):
 if get_publisher_class():
     # once a month, look for drafts to remove
     get_publisher_class().register_cronjob(CronJob(clean_drafts,
+        name='clean_drafts',
         days=[2], hours=[0], minutes=[0]))
diff --git a/wcs/qommon/cron.py b/wcs/qommon/cron.py
index 5c67cb8ca..e08086aff 100644
--- a/wcs/qommon/cron.py
+++ b/wcs/qommon/cron.py
@@ -19,14 +19,16 @@ import sys
 from django.conf import settings
 
 class CronJob(object):
+    name = None
     hours = None
     minutes = None
     weekdays = None
     days = None
     function = None
 
-    def __init__(self, function, hours=None, minutes=None, weekdays=None, days=None, hourly=False):
+    def __init__(self, function, name=None, hours=None, minutes=None, weekdays=None, days=None, hourly=False):
         self.function = function
+        self.name = name
         self.hours = hours
         self.minutes = minutes
         self.weekdays = weekdays
@@ -36,7 +38,7 @@ class CronJob(object):
             # prevents waking up all jobs at the same time on a server farm.
             self.minutes = [ord(settings.SECRET_KEY[-1]) % 60]
 
-def cron_worker(publisher, now):
+def cron_worker(publisher, now, job_name=None):
     try:
         publisher.set_config()
     except:
@@ -47,14 +49,20 @@ def cron_worker(publisher, now):
         publisher.reindex_sql()
 
     for job in publisher.cronjobs:
-        if job.days and now[2] not in job.days:
-            continue
-        if job.weekdays and now[6] not in job.weekdays:
-            continue
-        if job.hours and not now[3] in job.hours:
-            continue
-        if job.minutes and not now[4] in job.minutes:
-            continue
+        if job_name:
+            # a specific job name is asked, run it whatever
+            # the current time is.
+            if job.name != job_name:
+                continue
+        else:
+            if job.days and now[2] not in job.days:
+                continue
+            if job.weekdays and now[6] not in job.weekdays:
+                continue
+            if job.hours and not now[3] in job.hours:
+                continue
+            if job.minutes and not now[4] in job.minutes:
+                continue
 
         class FakeRequest(object):
             language = publisher.get_site_language()
diff --git a/wcs/qommon/management/commands/cron.py b/wcs/qommon/management/commands/cron.py
index 3cdcf3cba..efdb84d16 100644
--- a/wcs/qommon/management/commands/cron.py
+++ b/wcs/qommon/management/commands/cron.py
@@ -34,6 +34,7 @@ class Command(BaseCommand):
         parser.set_defaults(verbosity=0)
         parser.add_argument('--force-job', dest='force_job', action='store_true',
             help='Run even if DISABLE_CRON_JOBS is set in settings')
+        parser.add_argument('--job', dest='job_name', metavar='NAME')
 
     def handle(self, verbosity, **options):
         if getattr(settings, 'DISABLE_CRON_JOBS', False) and not options['force_job']:
@@ -54,7 +55,7 @@ class Command(BaseCommand):
                     if verbosity > 1:
                         print('cron work on %s' % hostname)
                     publisher.app_dir = os.path.join(app_dir, hostname)
-                    cron_worker(publisher, now)
+                    cron_worker(publisher, now, job_name=options.get('job_name'))
             if verbosity > 2:
                 print('cron end (release lock %s)' % lockfile)
         except locket.LockError:
-- 
2.22.0

