├── django_cron
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── cron_reset.py
│ │ ├── cron_requeue.py
│ │ └── cronjobs.py
├── admin_urls.py
├── cron_settings.py
├── admin_views.py
├── signals.py
├── notifications.py
├── models.py
├── __init__.py
└── base.py
├── setup.py
├── LICENCE.txt
└── README.rst
/django_cron/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_cron/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_cron/admin_urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 |
3 | urlpatterns = patterns('django_cron.admin_views',
4 | url(r'^restart/$', 'restart'),
5 | )
6 |
--------------------------------------------------------------------------------
/django_cron/cron_settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.conf import settings
4 |
5 | PID_FILE = getattr(settings, 'CRON_PID_FILE', os.path.join(settings.PROJECT_DIR, 'cron_pid.pid'))
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(name="django-cron",
4 | version="0.1",
5 | description="Django application automating tasks.",
6 | author="Reavis Sutphin-Gray",
7 | author_email="reavis-django-cron@sutphin-gray.com",
8 | packages=find_packages(),
9 | include_package_data=True,
10 | )
--------------------------------------------------------------------------------
/django_cron/admin_views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.admin.views.decorators import staff_member_required
2 | from django.core.urlresolvers import reverse
3 | from django.http import HttpResponseRedirect
4 |
5 |
6 | from models import Job
7 |
8 | @staff_member_required
9 | def restart(request):
10 | for j in Job.objects.all():
11 | j.queued = True
12 | j.save()
13 | return HttpResponseRedirect('%s' % reverse('admin:index'))
14 |
--------------------------------------------------------------------------------
/django_cron/management/commands/cron_reset.py:
--------------------------------------------------------------------------------
1 | #
2 | # run the cron service (intended to be executed from a cron job)
3 | #
4 | # usage: manage.py cronjobs
5 |
6 | from django.core.management.base import NoArgsCommand
7 | from django_cron.models import Cron
8 |
9 | class Command(NoArgsCommand):
10 | help = "reset the django cron status"
11 |
12 | def handle_noargs(self, **options):
13 | print 'Django Cron status reset'
14 | status, created = Cron.objects.get_or_create(pk=1)
15 | status.executing = False
16 | status.save()
17 | print 'Done'
--------------------------------------------------------------------------------
/django_cron/management/commands/cron_requeue.py:
--------------------------------------------------------------------------------
1 | """Requeue all the cronjobs.
2 |
3 | Usage: ./manage.py cron_requeue
4 | """
5 |
6 | from django.core.management.base import NoArgsCommand
7 | from django_cron.models import Job
8 |
9 |
10 | class Command(NoArgsCommand):
11 | help = "Set all jobs to be queued again."
12 |
13 | def handle_noargs(self, **options):
14 | print 'Django Cron requeueing all jobs.'
15 | for job in Job.objects.all():
16 | job.queued = True
17 | job.save()
18 | print "Requeued ", job
19 | print 'Done'
20 |
--------------------------------------------------------------------------------
/django_cron/management/commands/cronjobs.py:
--------------------------------------------------------------------------------
1 | #
2 | # run the cron service (intended to be executed from a cron job)
3 | #
4 | # usage: manage.py cronjobs
5 |
6 | from datetime import datetime
7 |
8 | from django.conf import settings
9 | from django.core.management.base import NoArgsCommand
10 | import django_cron
11 |
12 | class Command(NoArgsCommand):
13 | help = "run the cron services (intended to be executed from a cron job)"
14 |
15 | def handle_noargs(self, **options):
16 | django_cron.autodiscover(start_timer=False, registering=False)
17 | print "%s: Cronjobs for %s finished" % (datetime.now(), settings.SITE_NAME)
18 |
--------------------------------------------------------------------------------
/LICENCE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008 Dj Gilcrease
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/django_cron/signals.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2007-2008, Dj Gilcrease
3 | All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | """
23 | from django.dispatch import Signal
24 |
25 | cron_queued = Signal()
26 | cron_done = Signal(providing_args=["job"])
--------------------------------------------------------------------------------
/django_cron/notifications.py:
--------------------------------------------------------------------------------
1 | ######################################################################
2 | # For use with https://github.com/andybak/django-admin-notifications #
3 | ######################################################################
4 |
5 | from datetime import datetime
6 | from datetime import timedelta
7 |
8 | from django.utils.html import escape
9 |
10 | import admin_notifications
11 |
12 | from models import Job
13 |
14 | def delta_display(delta):
15 | s = ''
16 | if delta.days:
17 | s += '%s days ' % delta.days
18 | s += '%s minutes' % str(delta.seconds/60)
19 | return s
20 |
21 | def notification():
22 | msgs = []
23 | jobs = Job.objects.all()
24 | for job in jobs:
25 | msg = ""
26 | miss = datetime.now() - job.last_run
27 | has_missed = (miss - timedelta(seconds=job.run_frequency*60)) > timedelta(seconds=3*60) # If a job is late by at least the cron interval+fudge factor
28 | if not(job.queued):
29 | msg += "Job: %s isn't currently queued. " % escape(job.name)
30 | if has_missed:
31 | msg += "Job: %s has missed it's schedule by %s" % (escape(job.name), delta_display(miss))
32 | if job.queued:
33 | msg += " but has been re-scheduled."
34 | else:
35 | msg += "
Click here to re-queue all jobs"
36 | if msg:
37 | msgs.append(msg)
38 | if msgs:
39 | return "
".join(msgs)
40 |
41 | admin_notifications.register(notification)
--------------------------------------------------------------------------------
/django_cron/models.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2007-2008, Dj Gilcrease
3 | All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | """
23 | from django.db import models
24 | from datetime import datetime
25 |
26 | class Job(models.Model):
27 | name = models.CharField(max_length=100)
28 |
29 | # time between job runs (in minutes) // default: 1 day
30 | run_frequency = models.PositiveIntegerField(default=1440)
31 | last_run = models.DateTimeField(default=datetime.now())
32 |
33 | instance = models.TextField()
34 | args = models.TextField()
35 | kwargs = models.TextField()
36 | queued = models.BooleanField(default=True)
37 |
38 | def __unicode__(self):
39 | return self.name
40 |
41 | class Cron(models.Model):
42 | executing = models.BooleanField(default=False)
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | How to install django-cron
2 | ==========================
3 |
4 | 1. Put 'django_cron' into your python path
5 | 2. Add 'django_cron' to INSTALLED_APPS in your settings.py file
6 | 3. Add the following code to the beginning of your urls.py file (just after the imports)::
7 |
8 | import django_cron
9 | django_cron.autodiscover()
10 |
11 | 4. Create a file called 'cron.py' inside each installed app that you want to add a recurring job to. The app must be installed via the INSTALLED_APPS in your settings.py or the autodiscover will not find it.
12 |
13 | Example cron.py::
14 |
15 | from django_cron import cronScheduler, Job, HOUR, DAY, WEEK, MONTH
16 |
17 | # This is a function I wrote to check a feedback email address
18 | # and add it to our database. Replace with your own imports
19 | from MyMailFunctions import check_feedback_mailbox
20 |
21 | class CheckMail(Job):
22 | """
23 | Cron Job that checks the lgr users mailbox and adds any
24 | approved senders' attachments to the db
25 | """
26 |
27 | # run every hours
28 | run_every = HOUR
29 |
30 | def job(self):
31 | # This will be executed every 5 minutes
32 | check_feedback_mailbox()
33 |
34 | cronScheduler.register(CheckMail)
35 |
36 | Notes on andybak's fork
37 | -----------------------
38 |
39 | - Feature: Run via normal system cron using a management command rather than the original request based triggering. Makes it usable on low-traffic sites where there might not be enough requests to reliably run your cron jobs.
40 | - Feature: Changed time units for run_every to be minutes rather than seconds. This makes the numbers a bit less unwieldy and I couldn't see a need for the higher resolution
41 | - Feature: Added some convenient time constants: HOUR, DAY, WEEK, MONTH (last one is actually 52/12 weeks)
42 | - Feature: Email notifications of exceptions while running a job.
43 | - Feature: Admin messages to all staff when jobs fail.
44 | - Feature: Allow restarting jobs via a GET to [admin_url]/cron/restart/ (I know... I know... GET's shouldn't do this...)
45 | - Feature: Support notifications and restarting jobs via the app django-admin-notifications if jobs have missed their schedule.
46 | - Fixed: Registering jobs used to also run them - this tends to block the dev server if there are any long processes like a backup. Added a argument 'registering=False' to the method execute().
47 |
48 | Extra installation steps
49 | ~~~~~~~~~~~~~~~~~~~~~~~~
50 |
51 | 1. Add an entry to your crontab. I use
52 | ::
53 |
54 | \*/1 \* \* \* \* /path/to/python /path/to/manage.py cronjobs >> $HOME/cron.log 2>>cron_error.log
55 |
56 | (If you do PYTHONPATH wrangling in your .bash_profile then this might need moving into your manage.py or a similar wrapper script)
57 |
58 | 2. Add this to your root urls if you want to support restarting cron jobs via a GET
59 | ::
60 |
61 | url(r'^admin/cron/', include('django_cron.admin_urls')),
62 |
--------------------------------------------------------------------------------
/django_cron/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2007-2008, Dj Gilcrease
3 | All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | """
23 | from base import Job, cronScheduler, HOUR, DAY, WEEK, MONTH
24 |
25 | def autodiscover(start_timer=True, registering=True):
26 | """
27 | Auto-discover INSTALLED_APPS cron.py modules and fail silently when
28 | not present. This forces an import on them to register any cron jobs they
29 | may want.
30 | """
31 | import imp
32 | from django.conf import settings
33 |
34 | for app in settings.INSTALLED_APPS:
35 | # For each app, we need to look for an cron.py inside that app's
36 | # package. We can't use os.path here -- recall that modules may be
37 | # imported different ways (think zip files) -- so we need to get
38 | # the app's __path__ and look for cron.py on that path.
39 |
40 | # Step 1: find out the app's __path__ Import errors here will (and
41 | # should) bubble up, but a missing __path__ (which is legal, but weird)
42 | # fails silently -- apps that do weird things with __path__ might
43 | # need to roll their own cron registration.
44 | try:
45 | app_path = __import__(app, {}, {}, [app.split('.')[-1]]).__path__
46 | except AttributeError:
47 | continue
48 |
49 | # Step 2: use imp.find_module to find the app's admin.py. For some
50 | # reason imp.find_module raises ImportError if the app can't be found
51 | # but doesn't actually try to import the module. So skip this app if
52 | # its admin.py doesn't exist
53 | try:
54 | imp.find_module('cron', app_path)
55 | except ImportError:
56 | continue
57 |
58 | # Step 3: import the app's cron file. If this has errors we want them
59 | # to bubble up.
60 | __import__("%s.cron" % app)
61 |
62 | # Step 4: once we find all the cron jobs, start the cronScheduler
63 | cronScheduler.execute(start_timer=start_timer, registering=registering)
64 |
--------------------------------------------------------------------------------
/django_cron/base.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2007-2008, Dj Gilcrease
3 | All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | """
23 |
24 | import os
25 | import cPickle
26 | from threading import Timer
27 | from datetime import datetime
28 | from datetime import timedelta
29 | import sys
30 |
31 | from django.dispatch import dispatcher
32 | from django.conf import settings
33 | from django.contrib.auth.models import User
34 | from django.core.mail import mail_admins
35 | from signals import cron_done
36 | import models
37 | import cron_settings
38 |
39 | HOUR = 60
40 | DAY = HOUR*24
41 | WEEK = DAY*7
42 | MONTH = int(WEEK*4.333) # well sorta
43 |
44 | # how often to check if jobs are ready to be run (in seconds)
45 | # in reality if you have a multithreaded server, it may get checked
46 | # more often that this number suggests, so keep an eye on it...
47 | # default value: 300 seconds == 5 min
48 | polling_frequency = getattr(settings, "CRON_POLLING_FREQUENCY", 300)
49 |
50 | cron_pid_file = cron_settings.PID_FILE
51 |
52 | class Job(object):
53 | run_every = DAY
54 |
55 | def run(self, *args, **kwargs):
56 | self.job()
57 | cron_done.send(sender=self, *args, **kwargs)
58 |
59 | def job(self):
60 | """
61 | Should be overridden (this way is cleaner, but the old way - overriding run() - will still work)
62 | """
63 | pass
64 |
65 | class CronScheduler(object):
66 | def register(self, job_class, *args, **kwargs):
67 | """
68 | Register the given Job with the scheduler class
69 | """
70 |
71 | job_instance = job_class()
72 |
73 | if not isinstance(job_instance, Job):
74 | raise TypeError("You can only register a Job not a %r" % job_class)
75 |
76 | job, created = models.Job.objects.get_or_create(name=str(job_instance.__class__))
77 | if created:
78 | job.instance = cPickle.dumps(job_instance)
79 | job.args = cPickle.dumps(args)
80 | job.kwargs = cPickle.dumps(kwargs)
81 | job.run_frequency = job_instance.run_every
82 | job.save()
83 |
84 |
85 | def unregister(self, job_class, *args, **kwargs):
86 |
87 | job_instance = job_class()
88 | if not isinstance(job_instance, Job):
89 | raise TypeError("You can only unregister a Job not a %r" % job_class)
90 | try:
91 | job = models.Job.objects.get(name=str(job_instance.__class__))
92 | job.delete()
93 | except models.Job.DoesNotExist:
94 | pass
95 |
96 |
97 | def execute(self, start_timer=True, registering=False):
98 | """
99 | Queue all Jobs for execution
100 | """
101 | if not registering:
102 | status, created = models.Cron.objects.get_or_create(pk=1)
103 |
104 | ###PID code
105 | if cron_pid_file:
106 | if not os.path.exists(cron_pid_file):
107 | f = open(cron_pid_file, 'w') #create the file if it doesn't exist yet.
108 | f.close()
109 |
110 | # This is important for 2 reasons:
111 | # 1. It keeps us for running more than one instance of the
112 | # same job at a time
113 | # 2. It reduces the number of polling threads because they
114 | # get killed off if they happen to check while another
115 | # one is already executing a job (only occurs with
116 | # multi-threaded servers)
117 |
118 | if status.executing:
119 | print "Already executing"
120 | ###PID code
121 | ###check if django_cron is stuck
122 | if cron_pid_file:
123 | pid_file = open(cron_pid_file, 'r')
124 | pid_content = pid_file.read()
125 | pid_file.close()
126 | if not pid_content:
127 | pass#File is empty, do nothing
128 | else:
129 | pid = int(pid_content)
130 | if os.path.exists('/proc/%s' % pid):
131 | print 'Verified! Process with pid %s is running.' % pid
132 | else:
133 | print 'Oops! process with pid %s is not running.' % pid
134 | print 'Fixing status in db. '
135 | status.executing = False
136 | status.save()
137 | subject = 'Fixed cron job for %s' % settings.SITE_NAME
138 | body = 'django_cron was stuck as we found process #%s is not running, yet status.executing==True.' % pid
139 | mail_admins(subject, body, fail_silently=True)
140 | return
141 |
142 | status.executing = True
143 | ###PID code
144 | if cron_pid_file:
145 | pid_file = open(cron_pid_file, 'w')
146 | pid_file.write(str(os.getpid()))
147 | pid_file.close()
148 | try:
149 | status.save()
150 | except:
151 | # this will fail if you're debugging, so we want it
152 | # to fail silently and start the timer again so we
153 | # can pick up where we left off once debugging is done
154 | if start_timer:
155 | # Set up for this function to run again
156 | Timer(polling_frequency, self.execute).start()
157 | return
158 |
159 | jobs = models.Job.objects.all()
160 | for job in jobs:
161 | if job.queued:
162 |
163 | # Discard the seconds to prevent drift. Thanks to Josh Cartmell
164 | now = datetime.now()
165 | now = datetime(now.year, now.month, now.day, now.hour, now.minute)
166 | last_run = datetime(job.last_run.year, job.last_run.month, job.last_run.day, job.last_run.hour, job.last_run.minute)
167 |
168 | if (now - last_run) >= timedelta(minutes=job.run_frequency):
169 | inst = cPickle.loads(str(job.instance))
170 | args = cPickle.loads(str(job.args))
171 | kwargs = cPickle.loads(str(job.kwargs))
172 |
173 | try:
174 | inst.run(*args, **kwargs)
175 | job.last_run = datetime.now()
176 | job.save()
177 |
178 | except Exception, err:
179 | # if the job throws an error, just remove it from
180 | # the queue. That way we can find/fix the error and
181 | # requeue the job manually
182 | for u in User.objects.filter(is_staff=True):
183 | u.message_set.create(message="Error running job: %s: %s Please notify the administrator." % (job.name, err))
184 | job.queued = False
185 | job.save()
186 | import traceback
187 | exc_info = sys.exc_info()
188 | stack = ''.join(traceback.format_tb(exc_info[2]))
189 | if not settings.LOCAL_DEV:
190 | self.mail_exception(job.name, inst.__module__, err, stack)
191 | else:
192 | print stack
193 | status.executing = False
194 | status.save()
195 |
196 | if start_timer:
197 | # Set up for this function to run again
198 | Timer(polling_frequency, self.execute).start()
199 |
200 |
201 | def mail_exception(self, job, module, err, stack=None):
202 | subject = 'Cron job failed for %s' % settings.SITE_NAME
203 | body = '''
204 | Cron job failed for %s
205 | Job: %s
206 | Module: %s
207 | Error message: %s
208 | Time: %s
209 |
210 | Stack Trace: %s
211 | ''' % (settings.SITE_NAME, job, module, str(err), datetime.now().strftime("%d %b %Y"), ''.join(stack))
212 |
213 | mail_admins(subject, body, fail_silently=True)
214 |
215 |
216 | cronScheduler = CronScheduler()
217 |
218 |
--------------------------------------------------------------------------------