From 274b9300f40fe0789381c386d873f9b51da6f128 Mon Sep 17 00:00:00 2001 From: Ross Poulton Date: Tue, 22 Jan 2008 05:54:22 +0000 Subject: [PATCH] * 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 --- README | 8 ++ models.py | 18 +++- scripts/create_escalation_exclusions.py | 114 ++++++++++++++++++++++++ scripts/escalate_tickets.py | 85 ++++++++++++++---- 4 files changed, 204 insertions(+), 21 deletions(-) create mode 100644 scripts/create_escalation_exclusions.py diff --git a/README b/README index a14579e4..12da5a25 100644 --- a/README +++ b/README @@ -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! diff --git a/models.py b/models.py index da1e1f15..66d51fee 100644 --- a/models.py +++ b/models.py @@ -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: @@ -134,6 +133,8 @@ class Ticket(models.Model): resolution = models.TextField(blank=True, null=True) 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 @@ -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 diff --git a/scripts/create_escalation_exclusions.py b/scripts/create_escalation_exclusions.py new file mode 100644 index 00000000..452656b2 --- /dev/null +++ b/scripts/create_escalation_exclusions.py @@ -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) diff --git a/scripts/escalate_tickets.py b/scripts/escalate_tickets.py index 3997c474..318c187c 100644 --- a/scripts/escalate_tickets.py +++ b/scripts/escalate_tickets.py @@ -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) + + for q in queryset: + last = date.today() - timedelta(days=q.escalate_days) + today = date.today() + workdate = last + + 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 - if not q.last_escalation: q.last_escalation = datetime.now()-timedelta(hours=q.escalate_hours) - - if (q.last_escalation + timedelta(hours=q.escalate_hours) - timedelta(minutes=2)) > datetime.now(): - continue - - 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)