├── 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 | --------------------------------------------------------------------------------