mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2025-02-09 06:49:26 +01:00
* 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:
parent
0f3c979650
commit
274b9300f4
8
README
8
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'
|
This will run the escalation process hourly, using the 'Escalation Hours'
|
||||||
setting for each queue to determine which tickets to escalate.
|
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!
|
You're now up and running!
|
||||||
|
18
models.py
18
models.py
@ -46,8 +46,7 @@ class Queue(models.Model):
|
|||||||
title = models.CharField(maxlength=100)
|
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.')
|
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.')
|
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.')
|
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.')
|
||||||
last_escalation = models.DateTimeField(blank=True, null=True, editable=False)
|
|
||||||
|
|
||||||
def _from_address(self):
|
def _from_address(self):
|
||||||
if not self.email_address:
|
if not self.email_address:
|
||||||
@ -135,6 +134,8 @@ class Ticket(models.Model):
|
|||||||
|
|
||||||
priority = models.IntegerField(choices=PRIORITY_CHOICES, default=3, blank=3)
|
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):
|
def _get_assigned_to(self):
|
||||||
""" Custom property to allow us to easily print 'Unassigned' if a
|
""" 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
|
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):
|
def __unicode__(self):
|
||||||
return u'%s' % self.name
|
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
|
||||||
|
114
scripts/create_escalation_exclusions.py
Normal file
114
scripts/create_escalation_exclusions.py
Normal 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)
|
@ -26,44 +26,91 @@
|
|||||||
$Id$
|
$Id$
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, date
|
||||||
from django.db.models import Q
|
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
|
from helpdesk.lib import send_multipart_mail
|
||||||
|
import sys, getopt
|
||||||
|
|
||||||
def escalate_tickets():
|
def escalate_tickets(queues, verbose):
|
||||||
for q in Queue.objects.filter(escalate_hours__isnull=False).exclude(escalate_hours=0):
|
""" 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():
|
days = 0
|
||||||
continue
|
|
||||||
|
|
||||||
|
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
|
print "Processing: %s" % q
|
||||||
|
|
||||||
q.last_escalation = datetime.now()
|
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)):
|
||||||
q.save()
|
t.last_escalation = datetime.now()
|
||||||
|
|
||||||
for t in q.ticket_set.filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)).exclude(priority=1).exclude(on_hold=True):
|
|
||||||
t.priority -= 1
|
t.priority -= 1
|
||||||
t.save()
|
t.save()
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print " - Esclating %s from %s>%s" % (t.ticket, t.priority+1, t.priority)
|
||||||
|
|
||||||
f = FollowUp(
|
f = FollowUp(
|
||||||
ticket = t,
|
ticket = t,
|
||||||
title = 'Ticket Escalated',
|
title = 'Ticket Escalated',
|
||||||
date=q.last_escalation,
|
date=datetime.now(),
|
||||||
public=True,
|
public=True,
|
||||||
comment='Ticket escalated after %s hours' % q.escalate_hours,
|
comment='Ticket escalated after %s days' % q.escalate_days,
|
||||||
)
|
)
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
tc = TicketChange(
|
tc = TicketChange(
|
||||||
followup = f,
|
followup = f,
|
||||||
field = 'priority',
|
field = 'Priority',
|
||||||
old_value = t.priority + 1,
|
old_value = t.priority + 1,
|
||||||
new_value = t.priority,
|
new_value = t.priority,
|
||||||
)
|
)
|
||||||
tc.save()
|
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__':
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user