* Fixed svn:keywords

* Added escalation system that allows certain days to be excluded from escalation
* New script to automatically create exclusions on user-defined days, eg easily add a years worth of saturdays & sundays to the exclusion system
This commit is contained in:
Ross Poulton 2008-01-22 05:54:22 +00:00
parent 0f3c979650
commit 274b9300f4
4 changed files with 204 additions and 21 deletions

8
README
View File

@ -122,4 +122,12 @@ LICENSE.JQUERY and LICENSE.NICEDIT for their respective license terms.
This will run the escalation process hourly, using the 'Escalation Hours'
setting for each queue to determine which tickets to escalate.
5. If you wish to exclude some days (eg, weekends) from escalation calculations, enter
the dates manually via the Admin, or setup a cronjob to run
scripts/create_escalation_exclusions.py on a regular basis:
0 0 * * 0 DJANGO_SETTINGS_MODULE='myproject.settings' python /path/to/helpdesk/scripts/create_escalation_exclusions.py --days saturday,sunday --verbose
This will, on a weekly basis, create exclusions for the coming weekend.
You're now up and running!

View File

@ -46,8 +46,7 @@ class Queue(models.Model):
title = models.CharField(maxlength=100)
slug = models.SlugField(help_text='This slug is used when building ticket ID\'s. Once set, try not to change it or e-mailing may get messy.')
email_address = models.EmailField(blank=True, null=True, help_text='All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this shoul be the e-mail address for that mailbox.')
escalate_hours = models.IntegerField(blank=True, null=True, help_text='For tickets which are not held, how often do you wish to increase their priority? Set to 0 for no escalation.')
last_escalation = models.DateTimeField(blank=True, null=True, editable=False)
escalate_days = models.IntegerField(blank=True, null=True, help_text='For tickets which are not held, how often do you wish to increase their priority? Set to 0 for no escalation.')
def _from_address(self):
if not self.email_address:
@ -135,6 +134,8 @@ class Ticket(models.Model):
priority = models.IntegerField(choices=PRIORITY_CHOICES, default=3, blank=3)
last_escalation = models.DateTimeField(blank=True, null=True, editable=False)
def _get_assigned_to(self):
""" Custom property to allow us to easily print 'Unassigned' if a
ticket has no owner, or the users name if it's assigned. If the user
@ -310,3 +311,16 @@ class PreSetReply(models.Model):
def __unicode__(self):
return u'%s' % self.name
class EscalationExclusion(models.Model):
queues = models.ManyToManyField(Queue, blank=True, null=True, help_text='Leave blank for this exclusion to b eaplied to all queues, or select those queues you wish to exclude with this entry.')
name = models.CharField(maxlength=100)
date = models.DateField(help_text='Date on which escalation should not happen')
class Admin:
pass
def __unicode__(self):
return u'%s' % self.name

View File

@ -0,0 +1,114 @@
""" ..
.,::;::::::
..,::::::::,,,,::: Jutda Helpdesk - A Django
.,,::::::,,,,,,,,,,,,,:: powered ticket tracker for
.,::::::,,,,,,,,,,,,,,,,,,:;r. small enterprise
.::::,,,,,,,,,,,,,,,,,,,,,,:;;rr.
.:::,,,,,,,,,,,,,,,,,,,,,,,:;;;;;rr (c) Copyright 2008
.:::,,,,,,,,,,,,,,,,,,,,,,,:;;;:::;;rr
.:::,,,,,,,,,,,,,,,,,,,,. ,;;;::::::;;rr Jutda
.:::,,,,,,,,,,,,,,,,,,. .:;;:::::::::;;rr
.:::,,,,,,,,,,,,,,,. .;r;::::::::::::;r; All Rights Reserved
.:::,,,,,,,,,,,,,,, .;r;;:::::::::::;;:.
.:::,,,,,,,,,,,,,,,. .;r;;::::::::::::;:.
.;:,,,,,,,,,,,,,,, .,;rr;::::::::::::;:. This software is released
.,:,,,,,,,,,,,,,. .,:;rrr;;::::::::::::;;. under a limited-use license that
:,,,,,,,,,,,,,..:;rrrrr;;;::::::::::::;;. allows you to freely download this
:,,,,,,,:::;;;rr;;;;;;:::::::::::::;;, software from it's manufacturer and
::::;;;;;;;;;;;:::::::::::::::::;;, use it yourself, however you may not
.r;;;;:::::::::::::::::::::::;;;, distribute it. For further details, see
.r;::::::::::::::::::::;;;;;:, the enclosed LICENSE file.
.;;::::::::::::::;;;;;:,.
.;;:::::::;;;;;;:,. Please direct people who wish to download this
.r;;;;;;;;:,. software themselves to www.jutda.com.au.
,,,..
$Id$
"""
from datetime import datetime, timedelta, date
from django.db.models import Q
from helpdesk.models import EscalationExclusion, Queue
import sys, getopt
day_names = {
'monday': 0,
'tuesday': 1,
'wednesday': 2,
'thursday': 3,
'friday': 4,
'saturday': 5,
'sunday': 6,
}
def create_exclusions(days, occurrences, verbose, queues):
days = days.split(',')
for day in days:
day_name = day
day = day_names[day]
workdate = date.today()
i = 0
while i < occurrences:
if day == workdate.weekday():
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate)
esc.save()
if verbose:
print "Created exclusion for %s %s" % (day_name, workdate)
for q in queues:
esc.queues.add(q)
if verbose:
print " - for queue %s" % q
i += 1
workdate += timedelta(days=1)
def usage():
print "Options:"
print " --days, -d: Days of week (monday, tuesday, etc)"
print " --occurrences, -o: Occurrences: How many weeks ahead to exclude this day"
print " --queues, -q: Queues to include (default: all). Use queue slugs"
print " --verbose, -v: Display a list of dates excluded"
if __name__ == '__main__':
try:
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
except getopt.GetoptError:
usage()
sys.exit(2)
days = None
occurrences = None
verbose = False
queue_slugs = None
queues = []
for o, a in opts:
if o in ('-v', '--verbose'):
verbose = True
if o in ('-d', '--days'):
days = a
if o in ('-q', '--queues'):
queue_slugs = a
if o in ('-o', '--occurrences'):
occurrences = int(a)
if not occurrences: occurrences = 1
if not (days and occurrences):
usage()
sys.exit(2)
if queue_slugs is not None:
queue_set = queue_slugs.split(',')
for queue in queue_set:
try:
q = Queue.objects.get(slug__exact=queue)
except:
print "Queue %s does not exist." % queue
sys.exit(2)
queues.append(q)
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)

View File

@ -26,44 +26,91 @@
$Id$
"""
from datetime import datetime, timedelta
from datetime import datetime, timedelta, date
from django.db.models import Q
from helpdesk.models import Queue, Ticket, FollowUp
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
from helpdesk.lib import send_multipart_mail
import sys, getopt
def escalate_tickets():
for q in Queue.objects.filter(escalate_hours__isnull=False).exclude(escalate_hours=0):
def escalate_tickets(queues, verbose):
""" Only include queues with escalation configured """
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
if queues:
queryset = queryset.filter(slug__in=queues)
if not q.last_escalation: q.last_escalation = datetime.now()-timedelta(hours=q.escalate_hours)
for q in queryset:
last = date.today() - timedelta(days=q.escalate_days)
today = date.today()
workdate = last
if (q.last_escalation + timedelta(hours=q.escalate_hours) - timedelta(minutes=2)) > datetime.now():
continue
days = 0
while workdate < today:
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
days += 1
workdate = workdate + timedelta(days=1)
req_last_escl_date = date.today() - timedelta(days=days)
if verbose:
print "Processing: %s" % q
q.last_escalation = datetime.now()
q.save()
for t in q.ticket_set.filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)).exclude(priority=1).exclude(on_hold=True):
for t in q.ticket_set.filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)).exclude(priority=1).filter(Q(on_hold__isnull=True) | Q(on_hold=False)).filter(Q(last_escalation__lte=req_last_escl_date) | Q(last_escalation__isnull=True)):
t.last_escalation = datetime.now()
t.priority -= 1
t.save()
if verbose:
print " - Esclating %s from %s>%s" % (t.ticket, t.priority+1, t.priority)
f = FollowUp(
ticket = t,
title = 'Ticket Escalated',
date=q.last_escalation,
date=datetime.now(),
public=True,
comment='Ticket escalated after %s hours' % q.escalate_hours,
comment='Ticket escalated after %s days' % q.escalate_days,
)
f.save()
tc = TicketChange(
followup = f,
field = 'priority',
field = 'Priority',
old_value = t.priority + 1,
new_value = t.priority,
)
tc.save()
def usage():
print "Options:"
print " --queues, -q: Queues to include (default: all). Use queue slugs"
print " --verbose, -v: Display a list of dates excluded"
if __name__ == '__main__':
escalate_tickets()
try:
opts, args = getopt.getopt(sys.argv[1:], 'q:v', ['queues=', 'verbose'])
except getopt.GetoptError:
usage()
sys.exit(2)
verbose = False
queue_slugs = None
queues = []
for o, a in opts:
if o in ('-v', '--verbose'):
verbose = True
if o in ('-q', '--queues'):
queue_slugs = a
if queue_slugs is not None:
queue_set = queue_slugs.split(',')
for queue in queue_set:
try:
q = Queue.objects.get(slug__exact=queue)
except:
print "Queue %s does not exist." % queue
sys.exit(2)
queues.append(queue)
escalate_tickets(queues=queues, verbose=verbose)