mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2025-08-19 04:06:23 +02:00
Big bugfix release - addresses a number of issues introduced in recent Django
updates, and other bugs in the codebase. Many thanks to David Clymer and Chris Etcp for reporting these bugs and then providing fixes. Tickets closed: #3: BUG E-Mail Script Incompatible with Python 2.5 #4: BUG Failure on empty attachments #5: ENHANCEMENT Run scripts as command extensions [Backwards Compatible] #7: BUG Cannot view tickets when not logged in #8: BUG Overly broad error handling Note that #5 is backwards-incompatible, as you need to change any CRON or scheduler entries for the 'get_email.py', 'escalate_tickets.py' or 'create_escalation_exclusions.py' scripts. See the README file for the new commands.
This commit is contained in:
0
management/commands/__init__.py
Normal file
0
management/commands/__init__.py
Normal file
148
management/commands/create_escalation_exclusions.py
Normal file
148
management/commands/create_escalation_exclusions.py
Normal file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
scripts/create_escalation_exclusion.py - Easy way to routinely add particular
|
||||
days to the list of days on which no
|
||||
escalation should take place.
|
||||
"""
|
||||
from datetime import datetime, timedelta, date
|
||||
from django.db.models import Q
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
import sys, getopt
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from optparse import make_option
|
||||
|
||||
class Command(BaseCommand):
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
self.option_list += (
|
||||
make_option(
|
||||
'--days', '-d',
|
||||
help='Days of week (monday, tuesday, etc)'),
|
||||
make_option(
|
||||
'--occurrences', '-o',
|
||||
type='int',
|
||||
default=1,
|
||||
help='Occurrences: How many weeks ahead to exclude this day'),
|
||||
make_option(
|
||||
'--queues', '-q',
|
||||
help='Queues to include (default: all). Use queue slugs'),
|
||||
make_option(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Display a list of dates excluded'),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
days = options['days']
|
||||
occurrences = options['occurrences']
|
||||
verbose = False
|
||||
queue_slugs = options['queues']
|
||||
queues = []
|
||||
|
||||
if options['verbose']:
|
||||
verbose = True
|
||||
|
||||
# this should already be handled by optparse
|
||||
if not occurrences: occurrences = 1
|
||||
if not (days and occurrences):
|
||||
raise CommandError('One or more occurrences must be specified.')
|
||||
|
||||
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 Queue.DoesNotExist:
|
||||
raise CommandError("Queue %s does not exist." % queue)
|
||||
queues.append(q)
|
||||
|
||||
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
|
||||
|
||||
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 Queue.DoesNotExist:
|
||||
print "Queue %s does not exist." % queue
|
||||
sys.exit(2)
|
||||
queues.append(q)
|
||||
|
||||
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
|
153
management/commands/escalate_tickets.py
Normal file
153
management/commands/escalate_tickets.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
|
||||
designed to be run from Cron or similar.
|
||||
"""
|
||||
from datetime import datetime, timedelta, date
|
||||
import sys, getopt
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
|
||||
from helpdesk.lib import send_templated_mail
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from optparse import make_option
|
||||
|
||||
class Command(BaseCommand):
|
||||
def __init__(self):
|
||||
BaseCommand.__init__(self)
|
||||
|
||||
self.option_list += (
|
||||
make_option(
|
||||
'--queues', '-q',
|
||||
help='Queues to include (default: all). Use queue slugs'),
|
||||
make_option(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Display a list of dates excluded'),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbose = False
|
||||
queue_slugs = None
|
||||
queues = []
|
||||
|
||||
if options['verbose']:
|
||||
verbose = True
|
||||
if options['queues']:
|
||||
queue_slugs = options['queues']
|
||||
|
||||
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 Queue.DoesNotExist:
|
||||
raise CommandError("Queue %s does not exist." % queue)
|
||||
queues.append(queue)
|
||||
|
||||
escalate_tickets(queues=queues, verbose=verbose)
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
context = {
|
||||
'ticket': t,
|
||||
'queue': q,
|
||||
}
|
||||
|
||||
if t.submitter_email:
|
||||
send_templated_mail('escalated_submitter', context, recipients=t.submitter_email, sender=t.queue.from_address, fail_silently=True)
|
||||
|
||||
if t.queue.updated_ticket_cc:
|
||||
send_templated_mail('escalated_cc', context, recipients=t.queue.updated_ticket_cc, sender=t.queue.from_address, fail_silently=True)
|
||||
|
||||
if t.assigned_to:
|
||||
send_templated_mail('escalated_owner', context, recipients=t.assigned_to.email, sender=t.queue.from_address, fail_silently=True)
|
||||
|
||||
if verbose:
|
||||
print " - Esclating %s from %s>%s" % (t.ticket, t.priority+1, t.priority)
|
||||
|
||||
f = FollowUp(
|
||||
ticket = t,
|
||||
title = 'Ticket Escalated',
|
||||
date=datetime.now(),
|
||||
public=True,
|
||||
comment=_('Ticket escalated after %s days' % q.escalate_days),
|
||||
)
|
||||
f.save()
|
||||
|
||||
tc = TicketChange(
|
||||
followup = f,
|
||||
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__':
|
||||
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 Queue.DoesNotExist:
|
||||
print "Queue %s does not exist." % queue
|
||||
sys.exit(2)
|
||||
queues.append(queue)
|
||||
|
||||
escalate_tickets(queues=queues, verbose=verbose)
|
190
management/commands/get_email.py
Normal file
190
management/commands/get_email.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
scripts/get_email.py - Designed to be run from cron, this script checks the
|
||||
POP and IMAP boxes defined for the queues within a
|
||||
helpdesk, creating tickets from the new messages (or
|
||||
adding to existing tickets if needed)
|
||||
"""
|
||||
import poplib
|
||||
import imaplib
|
||||
from datetime import datetime, timedelta
|
||||
import email, mimetypes, re
|
||||
from email.Utils import parseaddr
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, Attachment
|
||||
from helpdesk.lib import send_templated_mail
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
process_email()
|
||||
|
||||
def process_email():
|
||||
for q in Queue.objects.filter(email_box_type__isnull=False, allow_email_submission=True):
|
||||
if not q.email_box_last_check: q.email_box_last_check = datetime.now()-timedelta(minutes=30)
|
||||
if not q.email_box_interval: q.email_box_interval = 0
|
||||
|
||||
if (q.email_box_last_check + timedelta(minutes=q.email_box_interval)) > datetime.now():
|
||||
continue
|
||||
|
||||
process_queue(q)
|
||||
|
||||
q.email_box_last_check = datetime.now()
|
||||
q.save()
|
||||
|
||||
def process_queue(q):
|
||||
print "Processing: %s" % q
|
||||
if q.email_box_type == 'pop3':
|
||||
server = poplib.POP3(q.email_box_host)
|
||||
server.getwelcome()
|
||||
server.user(q.email_box_user)
|
||||
server.pass_(q.email_box_pass)
|
||||
|
||||
messagesInfo = server.list()[1]
|
||||
|
||||
for msg in messagesInfo:
|
||||
msgNum = msg.split(" ")[0]
|
||||
msgSize = msg.split(" ")[1]
|
||||
|
||||
full_message = "\n".join(server.retr(msgNum)[1])
|
||||
ticket_from_message(message=full_message, queue=q)
|
||||
|
||||
server.dele(msgNum)
|
||||
server.quit()
|
||||
|
||||
elif q.email_box_type == 'imap':
|
||||
if not q.email_box_port: q.email_box_port = 143
|
||||
|
||||
server = imaplib.IMAP4(q.email_box_host, q.email_box_port)
|
||||
server.login(q.email_box_user, q.email_box_pass)
|
||||
server.select(q.email_box_imap_folder)
|
||||
status, data = server.search(None, 'ALL')
|
||||
for num in data[0].split():
|
||||
status, data = server.fetch(num, '(RFC822)')
|
||||
ticket_from_message(message=data[0][1], queue=q)
|
||||
server.store(num, '+FLAGS', '\\Deleted')
|
||||
server.expunge()
|
||||
server.close()
|
||||
server.logout()
|
||||
|
||||
def ticket_from_message(message, queue):
|
||||
# 'message' must be an RFC822 formatted message.
|
||||
msg = message
|
||||
message = email.message_from_string(msg)
|
||||
subject = message.get('subject', _('Created from e-mail'))
|
||||
subject = subject.replace("Re: ", "").replace("Fw: ", "").strip()
|
||||
|
||||
sender = message.get('from', _('Unknown Sender'))
|
||||
|
||||
sender_email = parseaddr(sender)[1]
|
||||
if sender_email.startswith('postmaster'):
|
||||
sender_email = ''
|
||||
|
||||
regex = re.compile("^\[[A-Za-z0-9]+-\d+\]")
|
||||
if regex.match(subject):
|
||||
# This is a reply or forward.
|
||||
ticket = re.match(r"^\[(?P<queue>[A-Za-z0-9]+)-(?P<id>\d+)\]", subject).group('id')
|
||||
else:
|
||||
ticket = None
|
||||
counter = 0
|
||||
files = []
|
||||
for part in message.walk():
|
||||
if part.get_content_maintype() == 'multipart':
|
||||
continue
|
||||
|
||||
name = part.get_param("name")
|
||||
|
||||
if part.get_content_maintype() == 'text' and name == None:
|
||||
body = part.get_payload()
|
||||
else:
|
||||
if not name:
|
||||
ext = mimetypes.guess_extension(part.get_content_type())
|
||||
name = "part-%i%s" % (counter, ext)
|
||||
files.append({'filename': name, 'content': part.get_payload(decode=True), 'type': part.get_content_type()})
|
||||
|
||||
counter += 1
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
if ticket:
|
||||
try:
|
||||
t = Ticket.objects.get(id=ticket)
|
||||
new = False
|
||||
except Ticket.DoesNotExist:
|
||||
ticket = None
|
||||
|
||||
priority = 3
|
||||
|
||||
smtp_priority = message.get('priority', '')
|
||||
smtp_importance = message.get('importance', '')
|
||||
|
||||
high_priority_types = ('high', 'important', '1', 'urgent')
|
||||
|
||||
if smtp_priority in high_priority_types or smtp_importance in high_priority_types:
|
||||
priority = 2
|
||||
|
||||
if ticket == None:
|
||||
t = Ticket(
|
||||
title=subject,
|
||||
queue=queue,
|
||||
submitter_email=sender_email,
|
||||
created=now,
|
||||
description=body,
|
||||
priority=priority,
|
||||
)
|
||||
t.save()
|
||||
new = True
|
||||
update = ''
|
||||
|
||||
context = {
|
||||
'ticket': t,
|
||||
'queue': queue,
|
||||
}
|
||||
|
||||
if new:
|
||||
|
||||
if sender_email:
|
||||
send_templated_mail('newticket_submitter', context, recipients=sender_email, sender=queue.from_address, fail_silently=True)
|
||||
|
||||
if queue.new_ticket_cc:
|
||||
send_templated_mail('newticket_cc', context, recipients=queue.new_ticket_cc, sender=queue.from_address, fail_silently=True)
|
||||
|
||||
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
|
||||
send_templated_mail('newticket_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True)
|
||||
|
||||
else:
|
||||
update = _(' (Updated)')
|
||||
|
||||
if t.assigned_to:
|
||||
send_templated_mail('updated_owner', context, recipients=t.assigned_to.email, sender=queue.from_address, fail_silently=True)
|
||||
|
||||
if queue.updated_ticket_cc:
|
||||
send_templated_mail('updated_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True)
|
||||
|
||||
f = FollowUp(
|
||||
ticket = t,
|
||||
title = _('E-Mail Received from %s' % sender_email),
|
||||
date = datetime.now(),
|
||||
public = True,
|
||||
comment = body,
|
||||
)
|
||||
f.save()
|
||||
|
||||
print " [%s-%s] %s%s" % (t.queue.slug, t.id, t.title, update)
|
||||
|
||||
for file in files:
|
||||
filename = file['filename'].replace(' ', '_')
|
||||
a = Attachment(followup=f, filename=filename, mime_type=file['type'], size=len(file['content']))
|
||||
#a.save_file_file(file['filename'], file['content'])
|
||||
a.file.save(file['filename'], ContentFile(file['content']))
|
||||
a.save()
|
||||
print " - %s" % file['filename']
|
||||
|
||||
if __name__ == '__main__':
|
||||
process_email()
|
Reference in New Issue
Block a user