* Large change to clean up the codebase: Decrease excess whitespace at ends

of lines; Increase line-wrapping of commands to limit code to 80 columns 
  wherever possible
* Re-built 'en' locale to match some new strings
* Clean up import statements somewhat
This commit is contained in:
Ross Poulton 2008-08-19 08:50:38 +00:00
parent ef25b571db
commit 5040d3d243
16 changed files with 1462 additions and 565 deletions

247
forms.py
View File

@ -1,50 +1,73 @@
""" .. """
Jutda Helpdesk - A Django powered ticket tracker for small enterprise. Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
forms.py - Definitions of newforms-based forms for creating and maintaining forms.py - Definitions of newforms-based forms for creating and maintaining
tickets. tickets.
""" """
from django import forms
from helpdesk.models import Ticket, Queue, FollowUp
from django.contrib.auth.models import User
from datetime import datetime from datetime import datetime
from django import forms
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from helpdesk.lib import send_templated_mail
from helpdesk.models import Ticket, Queue, FollowUp
class TicketForm(forms.Form): class TicketForm(forms.Form):
queue = forms.ChoiceField(label=_('Queue'), required=True, choices=()) queue = forms.ChoiceField(
label=_('Queue'),
required=True,
choices=()
)
title = forms.CharField(max_length=100, required=True, title = forms.CharField(
widget=forms.TextInput(), max_length=100,
label=_('Summary of the problem')) required=True,
widget=forms.TextInput(),
submitter_email = forms.EmailField(required=False, label=_('Summary of the problem'),
label=_('Submitter E-Mail Address'), )
help_text=_('This e-mail address will receive copies of all public updates to this ticket.'))
submitter_email = forms.EmailField(
body = forms.CharField(widget=forms.Textarea(), required=False,
label=_('Description of Issue'), required=True) label=_('Submitter E-Mail Address'),
help_text=_('This e-mail address will receive copies of all public '
assigned_to = forms.ChoiceField(choices=(), required=False, 'updates to this ticket.'),
label=_('Case owner'), )
help_text=_('If you select an owner other than yourself, they\'ll be e-mailed details of this ticket immediately.'))
body = forms.CharField(
widget=forms.Textarea(),
label=_('Description of Issue'),
required=True,
)
assigned_to = forms.ChoiceField(
choices=(),
required=False,
label=_('Case owner'),
help_text=_('If you select an owner other than yourself, they\'ll be '
'e-mailed details of this ticket immediately.'),
)
priority = forms.ChoiceField(
choices=Ticket.PRIORITY_CHOICES,
required=False,
initial='3',
label=_('Priority'),
help_text=_('Please select a priority carefully. If unsure, leave it '
'as \'3\'.'),
)
priority = forms.ChoiceField(choices=Ticket.PRIORITY_CHOICES,
required=False,
initial='3',
label=_('Priority'),
help_text=_('Please select a priority carefully. If unsure, leave it as \'3\'.'))
def save(self, user): def save(self, user):
""" """
Writes and returns a Ticket() object Writes and returns a Ticket() object
""" """
q = Queue.objects.get(id=int(self.cleaned_data['queue'])) q = Queue.objects.get(id=int(self.cleaned_data['queue']))
t = Ticket( title = self.cleaned_data['title'], t = Ticket( title = self.cleaned_data['title'],
submitter_email = self.cleaned_data['submitter_email'], submitter_email = self.cleaned_data['submitter_email'],
created = datetime.now(), created = datetime.now(),
status = Ticket.OPEN_STATUS, status = Ticket.OPEN_STATUS,
@ -52,7 +75,7 @@ class TicketForm(forms.Form):
description = self.cleaned_data['body'], description = self.cleaned_data['body'],
priority = self.cleaned_data['priority'], priority = self.cleaned_data['priority'],
) )
if self.cleaned_data['assigned_to']: if self.cleaned_data['assigned_to']:
try: try:
u = User.objects.get(id=self.cleaned_data['assigned_to']) u = User.objects.get(id=self.cleaned_data['assigned_to'])
@ -69,91 +92,151 @@ class TicketForm(forms.Form):
user = user, user = user,
) )
if self.cleaned_data['assigned_to']: if self.cleaned_data['assigned_to']:
f.title = _('Ticket Opened & Assigned to %(name)s') % {'name': t.get_assigned_to} f.title = _('Ticket Opened & Assigned to %(name)s') % {
'name': t.get_assigned_to
}
f.save() f.save()
context = { context = {
'ticket': t, 'ticket': t,
'queue': q, 'queue': q,
} }
from helpdesk.lib import send_templated_mail
if t.submitter_email: if t.submitter_email:
send_templated_mail('newticket_submitter', context, recipients=t.submitter_email, sender=q.from_address, fail_silently=True) send_templated_mail(
'newticket_submitter',
context,
recipients=t.submitter_email,
sender=q.from_address,
fail_silently=True,
)
if t.assigned_to and t.assigned_to != user: if t.assigned_to and t.assigned_to != user:
send_templated_mail('assigned_owner', context, recipients=t.assigned_to.email, sender=q.from_address, fail_silently=True) send_templated_mail(
'assigned_owner',
context,
recipients=t.assigned_to.email,
sender=q.from_address,
fail_silently=True,
)
if q.new_ticket_cc: if q.new_ticket_cc:
send_templated_mail('newticket_cc', context, recipients=q.new_ticket_cc, sender=q.from_address, fail_silently=True) send_templated_mail(
'newticket_cc',
context,
recipients=q.new_ticket_cc,
sender=q.from_address,
fail_silently=True,
)
if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc: if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc:
send_templated_mail('newticket_cc', context, recipients=q.updated_ticket_cc, sender=q.from_address, fail_silently=True) send_templated_mail(
'newticket_cc',
context,
recipients=q.updated_ticket_cc,
sender=q.from_address,
fail_silently=True,
)
return t return t
class PublicTicketForm(forms.Form):
queue = forms.ChoiceField(label=_('Queue'), required=True, choices=())
title = forms.CharField(max_length=100, required=True, class PublicTicketForm(forms.Form):
widget=forms.TextInput(), queue = forms.ChoiceField(
label=_('Summary of your query')) label=_('Queue'),
required=True,
submitter_email = forms.EmailField(required=True, choices=()
label=_('Your E-Mail Address'), )
help_text=_('We will e-mail you when your ticket is updated.'))
title = forms.CharField(
body = forms.CharField(widget=forms.Textarea(), max_length=100,
label=_('Description of your issue'), required=True, required=True,
help_text=_('Please be as descriptive as possible, including any details we may need to address your query.')) widget=forms.TextInput(),
label=_('Summary of your query'),
priority = forms.ChoiceField(choices=Ticket.PRIORITY_CHOICES, )
required=True,
initial='3', submitter_email = forms.EmailField(
label=_('Urgency'), required=True,
help_text=_('Please select a priority carefully.')) label=_('Your E-Mail Address'),
help_text=_('We will e-mail you when your ticket is updated.'),
)
body = forms.CharField(
widget=forms.Textarea(),
label=_('Description of your issue'),
required=True,
help_text=_('Please be as descriptive as possible, including any '
'details we may need to address your query.'),
)
priority = forms.ChoiceField(
choices=Ticket.PRIORITY_CHOICES,
required=True,
initial='3',
label=_('Urgency'),
help_text=_('Please select a priority carefully.'),
)
def save(self): def save(self):
""" """
Writes and returns a Ticket() object Writes and returns a Ticket() object
""" """
q = Queue.objects.get(id=int(self.cleaned_data['queue'])) q = Queue.objects.get(id=int(self.cleaned_data['queue']))
t = Ticket( title = self.cleaned_data['title'], t = Ticket(
submitter_email = self.cleaned_data['submitter_email'], title = self.cleaned_data['title'],
created = datetime.now(), submitter_email = self.cleaned_data['submitter_email'],
status = Ticket.OPEN_STATUS, created = datetime.now(),
queue = q, status = Ticket.OPEN_STATUS,
description = self.cleaned_data['body'], queue = q,
priority = self.cleaned_data['priority'], description = self.cleaned_data['body'],
) priority = self.cleaned_data['priority'],
)
t.save() t.save()
f = FollowUp( ticket = t, f = FollowUp(
title = _('Ticket Opened Via Web'), ticket = t,
date = datetime.now(), title = _('Ticket Opened Via Web'),
public = True, date = datetime.now(),
comment = self.cleaned_data['body'], public = True,
) comment = self.cleaned_data['body'],
)
f.save() f.save()
context = { context = {
'ticket': t, 'ticket': t,
'queue': q, 'queue': q,
} }
from helpdesk.lib import send_templated_mail send_templated_mail(
send_templated_mail('newticket_submitter', context, recipients=t.submitter_email, sender=q.from_address, fail_silently=True) 'newticket_submitter',
context,
recipients=t.submitter_email,
sender=q.from_address,
fail_silently=True,
)
if q.new_ticket_cc: if q.new_ticket_cc:
send_templated_mail('newticket_cc', context, recipients=q.new_ticket_cc, sender=q.from_address, fail_silently=True) send_templated_mail(
'newticket_cc',
context,
recipients=q.new_ticket_cc,
sender=q.from_address,
fail_silently=True,
)
if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc: if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc:
send_templated_mail('newticket_cc', context, recipients=q.updated_ticket_cc, sender=q.from_address, fail_silently=True) send_templated_mail(
'newticket_cc',
context,
recipients=q.updated_ticket_cc,
sender=q.from_address,
fail_silently=True,
)
return t return t

98
lib.py
View File

@ -1,15 +1,46 @@
""" .. """
Jutda Helpdesk - A Django powered ticket tracker for small enterprise. Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
lib.py - Common functions (eg multipart e-mail) lib.py - Common functions (eg multipart e-mail)
""" """
chart_colours = ('80C65A', '990066', 'FF9900', '3399CC', 'BBCCED', '3399CC', 'FFCC33')
def send_templated_mail(template_name, email_context, recipients, sender=None, bcc=None, fail_silently=False, files=None): def send_templated_mail(template_name, email_context, recipients, sender=None, bcc=None, fail_silently=False, files=None):
from helpdesk.models import EmailTemplate """
send_templated_mail() is a warpper around Django's e-mail routines that
allows us to easily send multipart (text/plain & text/html) e-mails using
templates that are stored in the database. This lets the admin provide
both a text and a HTML template for each message.
template_name is the slug of the template to use for this message (see
models.EmailTemplate)
email_context is a dictionary to be used when rendering the template
recipients can be either a string, eg 'a@b.com', or a list of strings.
sender should contain a string, eg 'My Site <me@z.com>'. If you leave it
blank, it'll use settings.DEFAULT_FROM_EMAIL as a fallback.
bcc is an optional list of addresses that will receive this message as a
blind carbon copy.
fail_silently is passed to Django's mail routine. Set to 'True' to ignore
any errors at send time.
files can be a list of file paths to be attached, or it can be left blank.
eg ('/tmp/file1.txt', '/tmp/image.png')
"""
from django.conf import settings
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template import loader, Context from django.template import loader, Context
from django.conf import settings
from helpdesk.models import EmailTemplate
t = EmailTemplate.objects.get(template_name__iexact=template_name) t = EmailTemplate.objects.get(template_name__iexact=template_name)
@ -17,15 +48,27 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
sender = settings.DEFAULT_FROM_EMAIL sender = settings.DEFAULT_FROM_EMAIL
context = Context(email_context) context = Context(email_context)
text_part = loader.get_template_from_string("%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text).render(context) text_part = loader.get_template_from_string(
html_part = loader.get_template_from_string("{%% extends 'helpdesk/email_html_base.html' %%}{%% block title %%}%s{%% endblock %%}{%% block content %%}%s{%% endblock %%}" % (t.heading, t.html)).render(context) "%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text
subject_part = loader.get_template_from_string("{{ ticket.ticket }} {{ ticket.title }} %s" % t.subject).render(context) ).render(context)
html_part = loader.get_template_from_string(
"{%% extends 'helpdesk/email_html_base.html' %%}{%% block title %%}%s{%% endblock %%}{%% block content %%}%s{%% endblock %%}" % (t.heading, t.html)
).render(context)
subject_part = loader.get_template_from_string(
"{{ ticket.ticket }} {{ ticket.title }} %s" % t.subject
).render(context)
if type(recipients) != list: if type(recipients) != list:
recipients = [recipients,] recipients = [recipients,]
msg = EmailMultiAlternatives(subject_part, text_part, sender, recipients, bcc=bcc) msg = EmailMultiAlternatives( subject_part,
text_part,
sender,
recipients,
bcc=bcc)
msg.attach_alternative(html_part, "text/html") msg.attach_alternative(html_part, "text/html")
if files: if files:
@ -34,18 +77,18 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
for file in files: for file in files:
msg.attach_file(file) msg.attach_file(file)
return msg.send(fail_silently) return msg.send(fail_silently)
def send_multipart_mail(template_name, email_context, subject, recipients, sender=None, bcc=None, fail_silently=False, files=None): def send_multipart_mail(template_name, email_context, subject, recipients, sender=None, bcc=None, fail_silently=False, files=None):
""" """
This function will send a multi-part e-mail with both HTML and This function will send a multi-part e-mail with both HTML and
Text parts. Text parts. Note we don't use this any more; wsee send_templated_mail
instead.
template_name must NOT contain an extension. Both HTML (.html) and TEXT template_name must NOT contain an extension. Both HTML (.html) and TEXT
(.txt) versions must exist, eg 'emails/public_submit' will use both (.txt) versions must exist, eg 'emails/public_submit' will use both
public_submit.html and public_submit.txt. public_submit.html and public_submit.txt.
email_context should be a plain python dictionary. It is applied against email_context should be a plain python dictionary. It is applied against
@ -57,7 +100,7 @@ def send_multipart_mail(template_name, email_context, subject, recipients, sende
recipients can be either a string, eg 'a@b.com' or a list, eg: recipients can be either a string, eg 'a@b.com' or a list, eg:
['a@b.com', 'c@d.com']. Type conversion is done if needed. ['a@b.com', 'c@d.com']. Type conversion is done if needed.
sender can be an e-mail, 'Name <email>' or None. If unspecified, the sender can be an e-mail, 'Name <email>' or None. If unspecified, the
DEFAULT_FROM_EMAIL will be used. DEFAULT_FROM_EMAIL will be used.
Originally posted on my blog at http://www.rossp.org/ Originally posted on my blog at http://www.rossp.org/
@ -70,7 +113,7 @@ def send_multipart_mail(template_name, email_context, subject, recipients, sende
sender = settings.DEFAULT_FROM_EMAIL sender = settings.DEFAULT_FROM_EMAIL
context = Context(email_context) context = Context(email_context)
text_part = loader.get_template('%s.txt' % template_name).render(context) text_part = loader.get_template('%s.txt' % template_name).render(context)
html_part = loader.get_template('%s.html' % template_name).render(context) html_part = loader.get_template('%s.html' % template_name).render(context)
subject_part = loader.get_template_from_string(subject).render(context) subject_part = loader.get_template_from_string(subject).render(context)
@ -87,13 +130,16 @@ def send_multipart_mail(template_name, email_context, subject, recipients, sende
for file in files: for file in files:
msg.attach_file(file) msg.attach_file(file)
return msg.send(fail_silently) return msg.send(fail_silently)
def normalise_data(data, to=100): def normalise_data(data, to=100):
""" """
Used for normalising data prior to graphing with Google charting API Used for normalising data prior to graphing with Google charting API. EG:
[1, 4, 10] becomes [10, 40, 100]
[36, 54, 240] becomes [15, 23, 100]
""" """
max_value = max(data) max_value = max(data)
if max_value > to: if max_value > to:
@ -103,7 +149,6 @@ def normalise_data(data, to=100):
data = new_data data = new_data
return data return data
chart_colours = ('80C65A', '990066', 'FF9900', '3399CC', 'BBCCED', '3399CC', 'FFCC33')
def line_chart(data): def line_chart(data):
""" """
@ -138,7 +183,7 @@ def line_chart(data):
first = True first = True
for row in range(rows): for row in range(rows):
# Set maximum data ranges to '0:x' where 'x' is the maximum number in use. # Set maximum data ranges to '0:x' where 'x' is the maximum number in use.
if not first: if not first:
chart_url += ',' chart_url += ','
chart_url += '0,%s' % max chart_url += '0,%s' % max
first = False first = False
@ -149,6 +194,7 @@ def line_chart(data):
return chart_url return chart_url
def bar_chart(data): def bar_chart(data):
""" """
'data' is a list of lists making a table. 'data' is a list of lists making a table.
@ -185,12 +231,16 @@ def bar_chart(data):
return chart_url return chart_url
def query_to_dict(results, descriptions): def query_to_dict(results, descriptions):
""" Replacement method for cursor.dictfetchall() as that method no longer """
Replacement method for cursor.dictfetchall() as that method no longer
exists in psycopg2, and I'm guessing in other backends too. exists in psycopg2, and I'm guessing in other backends too.
Converts the results of a raw SQL query into a list of dictionaries, suitable Converts the results of a raw SQL query into a list of dictionaries, suitable
for use in templates etc. """ for use in templates etc.
"""
output = [] output = []
for data in results: for data in results:
row = {} row = {}

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-08-13 23:32+0000\n" "POT-Creation-Date: 2008-08-19 07:01+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: forms.py:17 forms.py:98 templates/helpdesk/dashboard.html:10 #: forms.py:21 forms.py:147 templates/helpdesk/dashboard.html:10
#: templates/helpdesk/dashboard.html:26 templates/helpdesk/dashboard.html:41 #: templates/helpdesk/dashboard.html:26 templates/helpdesk/dashboard.html:41
#: templates/helpdesk/rss_list.html:23 templates/helpdesk/ticket_list.html:14 #: templates/helpdesk/rss_list.html:23 templates/helpdesk/ticket_list.html:14
#: templates/helpdesk/ticket_list.html:25 #: templates/helpdesk/ticket_list.html:25
@ -24,520 +24,564 @@ msgstr ""
msgid "Queue" msgid "Queue"
msgstr "" msgstr ""
#: forms.py:21 #: forms.py:30
msgid "Summary of the problem" msgid "Summary of the problem"
msgstr "" msgstr ""
#: forms.py:24 #: forms.py:35
msgid "Submitter E-Mail Address" msgid "Submitter E-Mail Address"
msgstr "" msgstr ""
#: forms.py:25 #: forms.py:36
msgid "" msgid ""
"This e-mail address will receive copies of all public updates to this ticket." "This e-mail address will receive copies of all public updates to this ticket."
msgstr "" msgstr ""
#: forms.py:28 #: forms.py:42
msgid "Description of Issue" msgid "Description of Issue"
msgstr "" msgstr ""
#: forms.py:31 #: forms.py:49
msgid "Case owner" msgid "Case owner"
msgstr "" msgstr ""
#: forms.py:32 #: forms.py:50
msgid "" msgid ""
"If you select an owner other than yourself, they'll be e-mailed details of " "If you select an owner other than yourself, they'll be e-mailed details of "
"this ticket immediately." "this ticket immediately."
msgstr "" msgstr ""
#: forms.py:37 models.py:116 scripts/escalate_tickets.py:76 #: forms.py:58 models.py:289 management/commands/escalate_tickets.py:152
#: templates/helpdesk/public_view_ticket.html:29 #: templates/helpdesk/public_view_ticket.html:29
#: templates/helpdesk/ticket.html:67 templates/helpdesk/ticket.html.py:159 #: templates/helpdesk/ticket.html:67 templates/helpdesk/ticket.html.py:159
#: templates/helpdesk/ticket_list.html:27 views/staff.py:146 #: templates/helpdesk/ticket_list.html:27 views/staff.py:182
msgid "Priority" msgid "Priority"
msgstr "" msgstr ""
#: forms.py:38 #: forms.py:59
msgid "Please select a priority carefully. If unsure, leave it as '3'." msgid "Please select a priority carefully. If unsure, leave it as '3'."
msgstr "" msgstr ""
#: forms.py:65 #: forms.py:88
msgid "Ticket Opened" msgid "Ticket Opened"
msgstr "" msgstr ""
#: forms.py:72 #: forms.py:95
#, python-format #, python-format
msgid "Ticket Opened & Assigned to %(name)s" msgid "Ticket Opened & Assigned to %(name)s"
msgstr "" msgstr ""
#: forms.py:102 #: forms.py:156
msgid "Summary of your query" msgid "Summary of your query"
msgstr "" msgstr ""
#: forms.py:105 #: forms.py:161
msgid "Your E-Mail Address" msgid "Your E-Mail Address"
msgstr "" msgstr ""
#: forms.py:106 #: forms.py:162
msgid "We will e-mail you when your ticket is updated." msgid "We will e-mail you when your ticket is updated."
msgstr "" msgstr ""
#: forms.py:109 #: forms.py:167
msgid "Description of your issue" msgid "Description of your issue"
msgstr "" msgstr ""
#: forms.py:110 #: forms.py:169
msgid "" msgid ""
"Please be as descriptive as possible, including any details we may need to " "Please be as descriptive as possible, including any details we may need to "
"address your query." "address your query."
msgstr "" msgstr ""
#: forms.py:115 #: forms.py:177
msgid "Urgency" msgid "Urgency"
msgstr "" msgstr ""
#: forms.py:116 #: forms.py:178
msgid "Please select a priority carefully." msgid "Please select a priority carefully."
msgstr "" msgstr ""
#: forms.py:137 #: forms.py:202
msgid "Ticket Opened Via Web" msgid "Ticket Opened Via Web"
msgstr "" msgstr ""
#: models.py:28 models.py:103 models.py:216 models.py:360 models.py:380 #: models.py:29 models.py:227 models.py:434 models.py:728 models.py:759
#: templates/helpdesk/dashboard.html:26 templates/helpdesk/dashboard.html:41 #: templates/helpdesk/dashboard.html:26 templates/helpdesk/dashboard.html:41
#: templates/helpdesk/ticket.html:153 templates/helpdesk/ticket_list.html:24 #: templates/helpdesk/ticket.html:153 templates/helpdesk/ticket_list.html:24
#: templates/helpdesk/ticket_list.html:59 views/staff.py:141 #: templates/helpdesk/ticket_list.html:59 views/staff.py:172
msgid "Title" msgid "Title"
msgstr "" msgstr ""
#: models.py:29 models.py:361 #: models.py:34 models.py:733
msgid "Slug" msgid "Slug"
msgstr "" msgstr ""
#: models.py:29 #: models.py:35
msgid "" msgid ""
"This slug is used when building ticket ID's. Once set, try not to change it " "This slug is used when building ticket ID's. Once set, try not to change it "
"or e-mailing may get messy." "or e-mailing may get messy."
msgstr "" msgstr ""
#: models.py:30 #: models.py:40
msgid "E-Mail Address" msgid "E-Mail Address"
msgstr "" msgstr ""
#: models.py:30 #: models.py:43
msgid "" msgid ""
"All outgoing e-mails for this queue will use this e-mail address. If you use " "All outgoing e-mails for this queue will use this e-mail address. If you use "
"IMAP or POP3, this should be the e-mail address for that mailbox." "IMAP or POP3, this should be the e-mail address for that mailbox."
msgstr "" msgstr ""
#: models.py:31 #: models.py:49
msgid "Allow Public Submission?" msgid "Allow Public Submission?"
msgstr "" msgstr ""
#: models.py:31 #: models.py:52
msgid "Should this queue be listed on the public submission form?" msgid "Should this queue be listed on the public submission form?"
msgstr "" msgstr ""
#: models.py:32 #: models.py:57
msgid "Allow E-Mail Submission?" msgid "Allow E-Mail Submission?"
msgstr "" msgstr ""
#: models.py:32 #: models.py:60
msgid "Do you want to poll the e-mail box below for new tickets?" msgid "Do you want to poll the e-mail box below for new tickets?"
msgstr "" msgstr ""
#: models.py:33 #: models.py:65
msgid "Escalation Days" msgid "Escalation Days"
msgstr "" msgstr ""
#: models.py:33 #: models.py:68
msgid "" msgid ""
"For tickets which are not held, how often do you wish to increase their " "For tickets which are not held, how often do you wish to increase their "
"priority? Set to 0 for no escalation." "priority? Set to 0 for no escalation."
msgstr "" msgstr ""
#: models.py:42 #: models.py:73
msgid "New Ticket CC Address" msgid "New Ticket CC Address"
msgstr "" msgstr ""
#: models.py:42 #: models.py:76
msgid "" msgid ""
"If an e-mail address is entered here, then it will receive notification of " "If an e-mail address is entered here, then it will receive notification of "
"all new tickets created for this queue" "all new tickets created for this queue"
msgstr "" msgstr ""
#: models.py:43 #: models.py:81
msgid "Updated Ticket CC Address" msgid "Updated Ticket CC Address"
msgstr "" msgstr ""
#: models.py:43 #: models.py:84
msgid "" msgid ""
"If an e-mail address is entered here, then it will receive notification of " "If an e-mail address is entered here, then it will receive notification of "
"all activity (new tickets, closed tickets, updates, reassignments, etc) for " "all activity (new tickets, closed tickets, updates, reassignments, etc) for "
"this queue" "this queue"
msgstr "" msgstr ""
#: models.py:45 #: models.py:90
msgid "E-Mail Box Type" msgid "E-Mail Box Type"
msgstr "" msgstr ""
#: models.py:45 #: models.py:92
msgid "POP 3" msgid "POP 3"
msgstr "" msgstr ""
#: models.py:45 #: models.py:92
msgid "IMAP" msgid "IMAP"
msgstr "" msgstr ""
#: models.py:45 #: models.py:95
msgid "" msgid ""
"E-Mail server type for creating tickets automatically from a mailbox - both " "E-Mail server type for creating tickets automatically from a mailbox - both "
"POP3 and IMAP are supported." "POP3 and IMAP are supported."
msgstr "" msgstr ""
#: models.py:46 #: models.py:100
msgid "E-Mail Hostname" msgid "E-Mail Hostname"
msgstr "" msgstr ""
#: models.py:46 #: models.py:104
msgid "" msgid ""
"Your e-mail server address - either the domain name or IP address. May be " "Your e-mail server address - either the domain name or IP address. May be "
"\"localhost\"." "\"localhost\"."
msgstr "" msgstr ""
#: models.py:47 #: models.py:109
msgid "E-Mail Port" msgid "E-Mail Port"
msgstr "" msgstr ""
#: models.py:47 #: models.py:112
msgid "" msgid ""
"Port number to use for accessing e-mail. Default for POP3 is \"110\", and " "Port number to use for accessing e-mail. Default for POP3 is \"110\", and "
"for IMAP is \"143\". This may differ on some servers. Leave it blank to use " "for IMAP is \"143\". This may differ on some servers. Leave it blank to use "
"the defaults." "the defaults."
msgstr "" msgstr ""
#: models.py:48 #: models.py:118
msgid "E-Mail Username" msgid "E-Mail Username"
msgstr "" msgstr ""
#: models.py:48 #: models.py:122
msgid "Username for accessing this mailbox." msgid "Username for accessing this mailbox."
msgstr "" msgstr ""
#: models.py:49 #: models.py:126
msgid "E-Mail Password" msgid "E-Mail Password"
msgstr "" msgstr ""
#: models.py:49 #: models.py:130
msgid "Password for the above username" msgid "Password for the above username"
msgstr "" msgstr ""
#: models.py:50 #: models.py:134
msgid "IMAP Folder" msgid "IMAP Folder"
msgstr "" msgstr ""
#: models.py:50 #: models.py:138
msgid "" msgid ""
"If using IMAP, what folder do you wish to fetch messages from? This allows " "If using IMAP, what folder do you wish to fetch messages from? This allows "
"you to use one IMAP account for multiple queues, by filtering messages on " "you to use one IMAP account for multiple queues, by filtering messages on "
"your IMAP server into separate folders. Default: INBOX." "your IMAP server into separate folders. Default: INBOX."
msgstr "" msgstr ""
#: models.py:51 #: models.py:145
msgid "E-Mail Check Interval" msgid "E-Mail Check Interval"
msgstr "" msgstr ""
#: models.py:51 #: models.py:146
msgid "How often do you wish to check this mailbox? (in Minutes)" msgid "How often do you wish to check this mailbox? (in Minutes)"
msgstr "" msgstr ""
#: models.py:89 templates/helpdesk/dashboard.html:10 #: models.py:212 templates/helpdesk/dashboard.html:10
#: templates/helpdesk/ticket.html:123 #: templates/helpdesk/ticket.html:123
msgid "Open" msgid "Open"
msgstr "" msgstr ""
#: models.py:90 templates/helpdesk/ticket.html:128 #: models.py:213 templates/helpdesk/ticket.html:128
#: templates/helpdesk/ticket.html.py:133 templates/helpdesk/ticket.html:138 #: templates/helpdesk/ticket.html.py:133 templates/helpdesk/ticket.html:138
msgid "Reopened" msgid "Reopened"
msgstr "" msgstr ""
#: models.py:91 templates/helpdesk/dashboard.html:10 #: models.py:214 templates/helpdesk/dashboard.html:10
#: templates/helpdesk/ticket.html:124 templates/helpdesk/ticket.html.py:129 #: templates/helpdesk/ticket.html:124 templates/helpdesk/ticket.html.py:129
#: templates/helpdesk/ticket.html:134 #: templates/helpdesk/ticket.html:134
msgid "Resolved" msgid "Resolved"
msgstr "" msgstr ""
#: models.py:92 templates/helpdesk/ticket.html:125 #: models.py:215 templates/helpdesk/ticket.html:125
#: templates/helpdesk/ticket.html.py:130 templates/helpdesk/ticket.html:135 #: templates/helpdesk/ticket.html.py:130 templates/helpdesk/ticket.html:135
#: templates/helpdesk/ticket.html.py:139 #: templates/helpdesk/ticket.html.py:139
msgid "Closed" msgid "Closed"
msgstr "" msgstr ""
#: models.py:96 #: models.py:219
msgid "1. Critical" msgid "1. Critical"
msgstr "" msgstr ""
#: models.py:97 #: models.py:220
msgid "2. High" msgid "2. High"
msgstr "" msgstr ""
#: models.py:98 #: models.py:221
msgid "3. Normal" msgid "3. Normal"
msgstr "" msgstr ""
#: models.py:99 #: models.py:222
msgid "4. Low" msgid "4. Low"
msgstr "" msgstr ""
#: models.py:100 #: models.py:223
msgid "5. Very Low" msgid "5. Very Low"
msgstr "" msgstr ""
#: models.py:105 templates/helpdesk/dashboard.html:41 #: models.py:234 templates/helpdesk/dashboard.html:41
#: templates/helpdesk/ticket_list.html:23 #: templates/helpdesk/ticket_list.html:23
#: templates/helpdesk/ticket_list.html:59 #: templates/helpdesk/ticket_list.html:59
msgid "Created" msgid "Created"
msgstr "" msgstr ""
#: models.py:106 #: models.py:236
msgid "Date this ticket was first created"
msgstr ""
#: models.py:240
msgid "Modified" msgid "Modified"
msgstr "" msgstr ""
#: models.py:107 templates/helpdesk/public_view_ticket.html:24 #: models.py:242
msgid "Date this ticket was most recently changed."
msgstr ""
#: models.py:246 templates/helpdesk/public_view_ticket.html:24
#: templates/helpdesk/ticket.html:62 #: templates/helpdesk/ticket.html:62
msgid "Submitter E-Mail" msgid "Submitter E-Mail"
msgstr "" msgstr ""
#: models.py:107 #: models.py:249
msgid "" msgid ""
"The submitter will receive an email for all public follow-ups left for this " "The submitter will receive an email for all public follow-ups left for this "
"task." "task."
msgstr "" msgstr ""
#: models.py:109 templates/helpdesk/dashboard.html:26 #: models.py:261 templates/helpdesk/dashboard.html:26
#: templates/helpdesk/ticket_list.html:15 #: templates/helpdesk/ticket_list.html:15
#: templates/helpdesk/ticket_list.html:26 #: templates/helpdesk/ticket_list.html:26
#: templates/helpdesk/ticket_list.html:59 #: templates/helpdesk/ticket_list.html:59
msgid "Status" msgid "Status"
msgstr "" msgstr ""
#: models.py:111 #: models.py:267
msgid "On Hold" msgid "On Hold"
msgstr "" msgstr ""
#: models.py:113 models.py:362 templates/helpdesk/public_view_ticket.html:34 #: models.py:270
msgid "If a ticket is on hold, it will not automatically be escalated."
msgstr ""
#: models.py:275 models.py:737 templates/helpdesk/public_view_ticket.html:34
#: templates/helpdesk/ticket.html:72 #: templates/helpdesk/ticket.html:72
msgid "Description" msgid "Description"
msgstr "" msgstr ""
#: models.py:114 templates/helpdesk/public_view_ticket.html:41 #: models.py:278
msgid "The content of the customers query."
msgstr ""
#: models.py:282 templates/helpdesk/public_view_ticket.html:41
#: templates/helpdesk/ticket.html:79 #: templates/helpdesk/ticket.html:79
msgid "Resolution" msgid "Resolution"
msgstr "" msgstr ""
#: models.py:125 templates/helpdesk/ticket.html:58 views/feeds.py:55 #: models.py:285
#: views/feeds.py:77 views/feeds.py:120 views/staff.py:120 msgid "The resolution provided to the customer by our staff."
msgstr ""
#: models.py:293
msgid "1 = Highest Priority, 5 = Low Priority"
msgstr ""
#: models.py:300
msgid ""
"The date this ticket was last escalated - updated automatically by "
"management/commands/escalate_tickets.py."
msgstr ""
#: models.py:309 templates/helpdesk/ticket.html:58 views/feeds.py:91
#: views/feeds.py:117 views/feeds.py:171 views/staff.py:149
msgid "Unassigned" msgid "Unassigned"
msgstr "" msgstr ""
#: models.py:156 #: models.py:348
msgid " - On Hold" msgid " - On Hold"
msgstr "" msgstr ""
#: models.py:215 models.py:330 #: models.py:430 models.py:662
msgid "Date" msgid "Date"
msgstr "" msgstr ""
#: models.py:217 views/staff.py:134 #: models.py:441 views/staff.py:163
msgid "Comment" msgid "Comment"
msgstr "" msgstr ""
#: models.py:218 #: models.py:447
msgid "Public" msgid "Public"
msgstr "" msgstr ""
#: models.py:221 templates/helpdesk/ticket.html:121 #: models.py:450
msgid ""
"Public tickets are viewable by the submitter and all staff, but non-public "
"tickets can only be seen by staff."
msgstr ""
#: models.py:461 templates/helpdesk/ticket.html:121
msgid "New Status" msgid "New Status"
msgstr "" msgstr ""
#: models.py:246 #: models.py:465
msgid "If the status was changed, what was it changed to?"
msgstr ""
#: models.py:496
msgid "Field" msgid "Field"
msgstr "" msgstr ""
#: models.py:247 #: models.py:501
msgid "Old Value" msgid "Old Value"
msgstr "" msgstr ""
#: models.py:248 #: models.py:507
msgid "New Value" msgid "New Value"
msgstr "" msgstr ""
#: models.py:253 #: models.py:515
msgid "removed" msgid "removed"
msgstr "" msgstr ""
#: models.py:255 #: models.py:517
#, python-format #, python-format
msgid "set to %s" msgid "set to %s"
msgstr "" msgstr ""
#: models.py:257 #: models.py:519
#, python-format #, python-format
msgid "changed from \"%(old_value)s\" to \"%(new_value)s\"" msgid "changed from \"%(old_value)s\" to \"%(new_value)s\""
msgstr "" msgstr ""
#: models.py:289 #: models.py:562
msgid "File" msgid "File"
msgstr "" msgstr ""
#: models.py:290 #: models.py:567
msgid "Filename" msgid "Filename"
msgstr "" msgstr ""
#: models.py:291 #: models.py:572
msgid "MIME Type" msgid "MIME Type"
msgstr "" msgstr ""
#: models.py:292 #: models.py:577
msgid "Size" msgid "Size"
msgstr "" msgstr ""
#: models.py:292 #: models.py:578
msgid "Size of this file in bytes" msgid "Size of this file in bytes"
msgstr "" msgstr ""
#: models.py:315 #: models.py:611
msgid "" msgid ""
"Leave blank to allow this reply to be used for all queues, or select those " "Leave blank to allow this reply to be used for all queues, or select those "
"queues you wish to limit this reply to." "queues you wish to limit this reply to."
msgstr "" msgstr ""
#: models.py:316 models.py:328 #: models.py:616 models.py:657
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#: models.py:316 #: models.py:618
msgid "" msgid ""
"Only used to assist users with selecting a reply - not shown to the user." "Only used to assist users with selecting a reply - not shown to the user."
msgstr "" msgstr ""
#: models.py:317 #: models.py:623
msgid "Body" msgid "Body"
msgstr "" msgstr ""
#: models.py:317 #: models.py:624
msgid "" msgid ""
"Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); " "Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); "
"{{ queue }} - The queue; and {{ user }} - the current user." "{{ queue }} - The queue; and {{ user }} - the current user."
msgstr "" msgstr ""
#: models.py:326 #: models.py:651
msgid "" msgid ""
"Leave blank for this exclusion to be applied to all queues, or select those " "Leave blank for this exclusion to be applied to all queues, or select those "
"queues you wish to exclude with this entry." "queues you wish to exclude with this entry."
msgstr "" msgstr ""
#: models.py:330 #: models.py:663
msgid "Date on which escalation should not happen" msgid "Date on which escalation should not happen"
msgstr "" msgstr ""
#: models.py:341 #: models.py:680
msgid "Template Name" msgid "Template Name"
msgstr "" msgstr ""
#: models.py:343 #: models.py:686
msgid "Subject" msgid "Subject"
msgstr "" msgstr ""
#: models.py:343 #: models.py:688
msgid "" msgid ""
"This will be prefixed with \"[ticket.ticket] ticket.title\". We recommend " "This will be prefixed with \"[ticket.ticket] ticket.title\". We recommend "
"something simple such as \"(Updated\") or \"(Closed)\" - the same context is " "something simple such as \"(Updated\") or \"(Closed)\" - the same context is "
"available as in plain_text, below." "available as in plain_text, below."
msgstr "" msgstr ""
#: models.py:344 #: models.py:694
msgid "Heading" msgid "Heading"
msgstr "" msgstr ""
#: models.py:344 #: models.py:696
msgid "" msgid ""
"In HTML e-mails, this will be the heading at the top of the email - the same " "In HTML e-mails, this will be the heading at the top of the email - the same "
"context is available as in plain_text, below." "context is available as in plain_text, below."
msgstr "" msgstr ""
#: models.py:345 #: models.py:702
msgid "Plain Text" msgid "Plain Text"
msgstr "" msgstr ""
#: models.py:345 #: models.py:703
msgid "" msgid ""
"The context available to you includes {{ ticket }}, {{ queue }}, and " "The context available to you includes {{ ticket }}, {{ queue }}, and "
"depending on the time of the call: {{ resolution }} or {{ comment }}." "depending on the time of the call: {{ resolution }} or {{ comment }}."
msgstr "" msgstr ""
#: models.py:346 #: models.py:709
msgid "HTML" msgid "HTML"
msgstr "" msgstr ""
#: models.py:346 #: models.py:710
msgid "The same context is available here as in plain_text, above." msgid "The same context is available here as in plain_text, above."
msgstr "" msgstr ""
#: models.py:381 #: models.py:764
msgid "Question" msgid "Question"
msgstr "" msgstr ""
#: models.py:382 #: models.py:768
msgid "Answer" msgid "Answer"
msgstr "" msgstr ""
#: models.py:384 #: models.py:772
msgid "Votes" msgid "Votes"
msgstr "" msgstr ""
#: models.py:384 #: models.py:773
msgid "Total number of votes cast for this item" msgid "Total number of votes cast for this item"
msgstr "" msgstr ""
#: models.py:385 #: models.py:777
msgid "Positive Votes" msgid "Positive Votes"
msgstr "" msgstr ""
#: models.py:385 #: models.py:778
msgid "Number of votes for this item which were POSITIVE." msgid "Number of votes for this item which were POSITIVE."
msgstr "" msgstr ""
#: models.py:387 #: models.py:782
msgid "Last Updated" msgid "Last Updated"
msgstr "" msgstr ""
#: models.py:397 #: models.py:783
msgid "The date on which this question was most recently changed."
msgstr ""
#: models.py:795
msgid "Unrated" msgid "Unrated"
msgstr "" msgstr ""
#: scripts/escalate_tickets.py:70 #: management/commands/escalate_tickets.py:146
#, python-format #, python-format
msgid "Ticket escalated after %s days" msgid "Ticket escalated after %s days"
msgstr "" msgstr ""
#: scripts/get_email.py:74 #: management/commands/get_email.py:97
msgid "Created from e-mail" msgid "Created from e-mail"
msgstr "" msgstr ""
#: scripts/get_email.py:77 #: management/commands/get_email.py:100
msgid "Unknown Sender" msgid "Unknown Sender"
msgstr "" msgstr ""
#: scripts/get_email.py:156 #: management/commands/get_email.py:206
msgid " (Updated)" msgid " (Updated)"
msgstr "" msgstr ""
#: scripts/get_email.py:166 #: management/commands/get_email.py:228
#, python-format #, python-format
msgid "E-Mail Received from %s" msgid "E-Mail Received from %(sender_email)s"
msgstr "" msgstr ""
#: templates/helpdesk/base.html:4 #: templates/helpdesk/base.html:4
@ -806,7 +850,7 @@ msgstr ""
#: templates/helpdesk/public_view_ticket.html:16 #: templates/helpdesk/public_view_ticket.html:16
#, python-format #, python-format
msgid "Queue: %(ticket.queue)s" msgid "Queue: %(queue_name)s"
msgstr "" msgstr ""
#: templates/helpdesk/public_view_ticket.html:19 #: templates/helpdesk/public_view_ticket.html:19
@ -1087,75 +1131,75 @@ msgstr ""
msgid "Password" msgid "Password"
msgstr "" msgstr ""
#: views/feeds.py:26 #: views/feeds.py:35
#, python-format #, python-format
msgid "Helpdesk: Open Tickets in queue %(queue)s for %(username)s" msgid "Helpdesk: Open Tickets in queue %(queue)s for %(username)s"
msgstr "" msgstr ""
#: views/feeds.py:28 #: views/feeds.py:40
#, python-format #, python-format
msgid "Helpdesk: Open Tickets for %(username)s" msgid "Helpdesk: Open Tickets for %(username)s"
msgstr "" msgstr ""
#: views/feeds.py:32 #: views/feeds.py:46
#, python-format #, python-format
msgid "Open and Reopened Tickets in queue %(queue)s for %(username)s" msgid "Open and Reopened Tickets in queue %(queue)s for %(username)s"
msgstr "" msgstr ""
#: views/feeds.py:34 #: views/feeds.py:51
#, python-format #, python-format
msgid "Open and Reopened Tickets for %(username)s" msgid "Open and Reopened Tickets for %(username)s"
msgstr "" msgstr ""
#: views/feeds.py:62 #: views/feeds.py:98
msgid "Helpdesk: Unassigned Tickets" msgid "Helpdesk: Unassigned Tickets"
msgstr "" msgstr ""
#: views/feeds.py:63 #: views/feeds.py:99
msgid "Unassigned Open and Reopened tickets" msgid "Unassigned Open and Reopened tickets"
msgstr "" msgstr ""
#: views/feeds.py:84 #: views/feeds.py:124
msgid "Helpdesk: Recent Followups" msgid "Helpdesk: Recent Followups"
msgstr "" msgstr ""
#: views/feeds.py:85 #: views/feeds.py:125
msgid "" msgid ""
"Recent FollowUps, such as e-mail replies, comments, attachments and " "Recent FollowUps, such as e-mail replies, comments, attachments and "
"resolutions" "resolutions"
msgstr "" msgstr ""
#: views/feeds.py:102 #: views/feeds.py:142
#, python-format #, python-format
msgid "Helpdesk: Open Tickets in queue %(queue)s" msgid "Helpdesk: Open Tickets in queue %(queue)s"
msgstr "" msgstr ""
#: views/feeds.py:105 #: views/feeds.py:147
#, python-format #, python-format
msgid "Open and Reopened Tickets in queue %(queue)s" msgid "Open and Reopened Tickets in queue %(queue)s"
msgstr "" msgstr ""
#: views/public.py:53 #: views/public.py:58
msgid "Invalid ticket ID or e-mail address. Please try again." msgid "Invalid ticket ID or e-mail address. Please try again."
msgstr "" msgstr ""
#: views/staff.py:82 #: views/staff.py:107
msgid "Accepted resolution and closed ticket" msgid "Accepted resolution and closed ticket"
msgstr "" msgstr ""
#: views/staff.py:116 #: views/staff.py:143
#, python-format #, python-format
msgid "Assigned to %(username)s" msgid "Assigned to %(username)s"
msgstr "" msgstr ""
#: views/staff.py:136 #: views/staff.py:165
msgid "Updated" msgid "Updated"
msgstr "" msgstr ""
#: views/staff.py:298 #: views/staff.py:369
msgid "Ticket taken off hold" msgid "Ticket taken off hold"
msgstr "" msgstr ""
#: views/staff.py:301 #: views/staff.py:372
msgid "Ticket placed on hold" msgid "Ticket placed on hold"
msgstr "" msgstr ""

View File

@ -8,13 +8,17 @@ scripts/create_escalation_exclusion.py - Easy way to routinely add particular
days to the list of days on which no days to the list of days on which no
escalation should take place. escalation should take place.
""" """
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
from django.db.models import Q import getopt
from helpdesk.models import EscalationExclusion, Queue from optparse import make_option
import sys, getopt import sys
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from optparse import make_option from django.db.models import Q
from helpdesk.models import EscalationExclusion, Queue
class Command(BaseCommand): class Command(BaseCommand):
def __init__(self): def __init__(self):
@ -22,15 +26,15 @@ class Command(BaseCommand):
self.option_list += ( self.option_list += (
make_option( make_option(
'--days', '-d', '--days', '-d',
help='Days of week (monday, tuesday, etc)'), help='Days of week (monday, tuesday, etc)'),
make_option( make_option(
'--occurrences', '-o', '--occurrences', '-o',
type='int', type='int',
default=1, default=1,
help='Occurrences: How many weeks ahead to exclude this day'), help='Occurrences: How many weeks ahead to exclude this day'),
make_option( make_option(
'--queues', '-q', '--queues', '-q',
help='Queues to include (default: all). Use queue slugs'), help='Queues to include (default: all). Use queue slugs'),
make_option( make_option(
'--verbose', '-v', '--verbose', '-v',
@ -53,7 +57,7 @@ class Command(BaseCommand):
if not occurrences: occurrences = 1 if not occurrences: occurrences = 1
if not (days and occurrences): if not (days and occurrences):
raise CommandError('One or more occurrences must be specified.') raise CommandError('One or more occurrences must be specified.')
if queue_slugs is not None: if queue_slugs is not None:
queue_set = queue_slugs.split(',') queue_set = queue_slugs.split(',')
for queue in queue_set: for queue in queue_set:
@ -65,6 +69,7 @@ class Command(BaseCommand):
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues) create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
day_names = { day_names = {
'monday': 0, 'monday': 0,
'tuesday': 1, 'tuesday': 1,
@ -75,6 +80,7 @@ day_names = {
'sunday': 6, 'sunday': 6,
} }
def create_exclusions(days, occurrences, verbose, queues): def create_exclusions(days, occurrences, verbose, queues):
days = days.split(',') days = days.split(',')
for day in days: for day in days:
@ -87,10 +93,10 @@ def create_exclusions(days, occurrences, verbose, queues):
if EscalationExclusion.objects.filter(date=workdate).count() == 0: if EscalationExclusion.objects.filter(date=workdate).count() == 0:
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate) esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate)
esc.save() esc.save()
if verbose: if verbose:
print "Created exclusion for %s %s" % (day_name, workdate) print "Created exclusion for %s %s" % (day_name, workdate)
for q in queues: for q in queues:
esc.queues.add(q) esc.queues.add(q)
if verbose: if verbose:
@ -107,13 +113,15 @@ def usage():
print " --queues, -q: Queues to include (default: all). Use queue slugs" print " --queues, -q: Queues to include (default: all). Use queue slugs"
print " --verbose, -v: Display a list of dates excluded" print " --verbose, -v: Display a list of dates excluded"
if __name__ == '__main__': if __name__ == '__main__':
# This script can be run from the command-line or via Django's manage.py.
try: try:
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues=']) opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
except getopt.GetoptError: except getopt.GetoptError:
usage() usage()
sys.exit(2) sys.exit(2)
days = None days = None
occurrences = None occurrences = None
verbose = False verbose = False
@ -134,7 +142,7 @@ if __name__ == '__main__':
if not (days and occurrences): if not (days and occurrences):
usage() usage()
sys.exit(2) sys.exit(2)
if queue_slugs is not None: if queue_slugs is not None:
queue_set = queue_slugs.split(',') queue_set = queue_slugs.split(',')
for queue in queue_set: for queue in queue_set:

View File

@ -4,20 +4,22 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
scripts/escalate_tickets.py - Easy way to escalate tickets based on their age, scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
designed to be run from Cron or similar. designed to be run from Cron or similar.
""" """
from datetime import datetime, timedelta, date
import sys, getopt
from datetime import datetime, timedelta, date
import getopt
from optparse import make_option
import sys
from django.core.management.base import BaseCommand
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
from helpdesk.lib import send_templated_mail from helpdesk.lib import send_templated_mail
from django.core.management.base import BaseCommand
from optparse import make_option
class Command(BaseCommand): class Command(BaseCommand):
def __init__(self): def __init__(self):
@ -25,7 +27,7 @@ class Command(BaseCommand):
self.option_list += ( self.option_list += (
make_option( make_option(
'--queues', '-q', '--queues', '-q',
help='Queues to include (default: all). Use queue slugs'), help='Queues to include (default: all). Use queue slugs'),
make_option( make_option(
'--verbose', '-v', '--verbose', '-v',
@ -33,17 +35,17 @@ class Command(BaseCommand):
default=False, default=False,
help='Display a list of dates excluded'), help='Display a list of dates excluded'),
) )
def handle(self, *args, **options): def handle(self, *args, **options):
verbose = False verbose = False
queue_slugs = None queue_slugs = None
queues = [] queues = []
if options['verbose']: if options['verbose']:
verbose = True verbose = True
if options['queues']: if options['queues']:
queue_slugs = options['queues'] queue_slugs = options['queues']
if queue_slugs is not None: if queue_slugs is not None:
queue_set = queue_slugs.split(',') queue_set = queue_slugs.split(',')
for queue in queue_set: for queue in queue_set:
@ -55,12 +57,13 @@ class Command(BaseCommand):
escalate_tickets(queues=queues, verbose=verbose) escalate_tickets(queues=queues, verbose=verbose)
def escalate_tickets(queues, verbose): def escalate_tickets(queues, verbose):
""" Only include queues with escalation configured """ """ Only include queues with escalation configured """
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0) queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
if queues: if queues:
queryset = queryset.filter(slug__in=queues) queryset = queryset.filter(slug__in=queues)
for q in queryset: for q in queryset:
last = date.today() - timedelta(days=q.escalate_days) last = date.today() - timedelta(days=q.escalate_days)
today = date.today() today = date.today()
@ -78,28 +81,62 @@ def escalate_tickets(queues, verbose):
if verbose: if verbose:
print "Processing: %s" % q 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)): 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.last_escalation = datetime.now()
t.priority -= 1 t.priority -= 1
t.save() t.save()
context = { context = {
'ticket': t, 'ticket': t,
'queue': q, 'queue': q,
} }
if t.submitter_email: if t.submitter_email:
send_templated_mail('escalated_submitter', context, recipients=t.submitter_email, sender=t.queue.from_address, fail_silently=True) send_templated_mail(
'escalated_submitter',
context,
recipients=t.submitter_email,
sender=t.queue.from_address,
fail_silently=True,
)
if t.queue.updated_ticket_cc: 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) send_templated_mail(
'escalated_cc',
context,
recipients=t.queue.updated_ticket_cc,
sender=t.queue.from_address,
fail_silently=True,
)
if t.assigned_to: if t.assigned_to:
send_templated_mail('escalated_owner', context, recipients=t.assigned_to.email, sender=t.queue.from_address, fail_silently=True) send_templated_mail(
'escalated_owner',
context,
recipients=t.assigned_to.email,
sender=t.queue.from_address,
fail_silently=True,
)
if verbose: if verbose:
print " - Esclating %s from %s>%s" % (t.ticket, t.priority+1, t.priority) print " - Esclating %s from %s>%s" % (
t.ticket,
t.priority+1,
t.priority
)
f = FollowUp( f = FollowUp(
ticket = t, ticket = t,
@ -118,11 +155,13 @@ def escalate_tickets(queues, verbose):
) )
tc.save() tc.save()
def usage(): def usage():
print "Options:" print "Options:"
print " --queues, -q: Queues to include (default: all). Use queue slugs" print " --queues, -q: Queues to include (default: all). Use queue slugs"
print " --verbose, -v: Display a list of dates excluded" print " --verbose, -v: Display a list of dates excluded"
if __name__ == '__main__': if __name__ == '__main__':
try: try:
opts, args = getopt.getopt(sys.argv[1:], 'q:v', ['queues=', 'verbose']) opts, args = getopt.getopt(sys.argv[1:], 'q:v', ['queues=', 'verbose'])
@ -133,13 +172,13 @@ if __name__ == '__main__':
verbose = False verbose = False
queue_slugs = None queue_slugs = None
queues = [] queues = []
for o, a in opts: for o, a in opts:
if o in ('-v', '--verbose'): if o in ('-v', '--verbose'):
verbose = True verbose = True
if o in ('-q', '--queues'): if o in ('-q', '--queues'):
queue_slugs = a queue_slugs = a
if queue_slugs is not None: if queue_slugs is not None:
queue_set = queue_slugs.split(',') queue_set = queue_slugs.split(',')
for queue in queue_set: for queue in queue_set:

View File

@ -5,8 +5,8 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (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 scripts/get_email.py - Designed to be run from cron, this script checks the
POP and IMAP boxes defined for the queues within a POP and IMAP boxes defined for the queues within a
helpdesk, creating tickets from the new messages (or helpdesk, creating tickets from the new messages (or
adding to existing tickets if needed) adding to existing tickets if needed)
""" """
@ -25,16 +25,27 @@ from django.utils.translation import ugettext as _
from helpdesk.lib import send_templated_mail from helpdesk.lib import send_templated_mail
from helpdesk.models import Queue, Ticket, FollowUp, Attachment from helpdesk.models import Queue, Ticket, FollowUp, Attachment
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
process_email() 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(): 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
queue_time_delta = timedelta(minutes=q.email_box_interval)
if (q.email_box_last_check + queue_time_delta) > datetime.now():
continue continue
process_queue(q) process_queue(q)
@ -42,6 +53,7 @@ def process_email():
q.email_box_last_check = datetime.now() q.email_box_last_check = datetime.now()
q.save() q.save()
def process_queue(q): def process_queue(q):
print "Processing: %s" % q print "Processing: %s" % q
if q.email_box_type == 'pop3': if q.email_box_type == 'pop3':
@ -55,16 +67,16 @@ def process_queue(q):
for msg in messagesInfo: for msg in messagesInfo:
msgNum = msg.split(" ")[0] msgNum = msg.split(" ")[0]
msgSize = msg.split(" ")[1] msgSize = msg.split(" ")[1]
full_message = "\n".join(server.retr(msgNum)[1]) full_message = "\n".join(server.retr(msgNum)[1])
ticket_from_message(message=full_message, queue=q) ticket_from_message(message=full_message, queue=q)
server.dele(msgNum) server.dele(msgNum)
server.quit() server.quit()
elif q.email_box_type == 'imap': elif q.email_box_type == 'imap':
if not q.email_box_port: q.email_box_port = 143 if not q.email_box_port: q.email_box_port = 143
server = imaplib.IMAP4(q.email_box_host, q.email_box_port) server = imaplib.IMAP4(q.email_box_host, q.email_box_port)
server.login(q.email_box_user, q.email_box_pass) server.login(q.email_box_user, q.email_box_pass)
server.select(q.email_box_imap_folder) server.select(q.email_box_imap_folder)
@ -77,15 +89,16 @@ def process_queue(q):
server.close() server.close()
server.logout() server.logout()
def ticket_from_message(message, queue): def ticket_from_message(message, queue):
# 'message' must be an RFC822 formatted message. # 'message' must be an RFC822 formatted message.
msg = message msg = message
message = email.message_from_string(msg) message = email.message_from_string(msg)
subject = message.get('subject', _('Created from e-mail')) subject = message.get('subject', _('Created from e-mail'))
subject = subject.replace("Re: ", "").replace("Fw: ", "").strip() subject = subject.replace("Re: ", "").replace("Fw: ", "").strip()
sender = message.get('from', _('Unknown Sender')) sender = message.get('from', _('Unknown Sender'))
sender_email = parseaddr(sender)[1] sender_email = parseaddr(sender)[1]
if sender_email.startswith('postmaster'): if sender_email.startswith('postmaster'):
sender_email = '' sender_email = ''
@ -96,24 +109,31 @@ def ticket_from_message(message, queue):
ticket = re.match(r"^\[(?P<queue>[A-Za-z0-9]+)-(?P<id>\d+)\]", subject).group('id') ticket = re.match(r"^\[(?P<queue>[A-Za-z0-9]+)-(?P<id>\d+)\]", subject).group('id')
else: else:
ticket = None ticket = None
counter = 0 counter = 0
files = [] files = []
for part in message.walk(): for part in message.walk():
if part.get_content_maintype() == 'multipart': if part.get_content_maintype() == 'multipart':
continue continue
name = part.get_param("name") name = part.get_param("name")
if part.get_content_maintype() == 'text' and name == None: if part.get_content_maintype() == 'text' and name == None:
body = part.get_payload() body = part.get_payload()
else: else:
if not name: if not name:
ext = mimetypes.guess_extension(part.get_content_type()) ext = mimetypes.guess_extension(part.get_content_type())
name = "part-%i%s" % (counter, ext) name = "part-%i%s" % (counter, ext)
files.append({'filename': name, 'content': part.get_payload(decode=True), 'type': part.get_content_type()})
files.append({
'filename': name,
'content': part.get_payload(decode=True),
'type': part.get_content_type()},
)
counter += 1 counter += 1
now = datetime.now() now = datetime.now()
if ticket: if ticket:
@ -130,7 +150,8 @@ def ticket_from_message(message, queue):
high_priority_types = ('high', 'important', '1', 'urgent') high_priority_types = ('high', 'important', '1', 'urgent')
if smtp_priority in high_priority_types or smtp_importance in high_priority_types: if smtp_priority in high_priority_types
or smtp_importance in high_priority_types:
priority = 2 priority = 2
if ticket == None: if ticket == None:
@ -145,7 +166,7 @@ def ticket_from_message(message, queue):
t.save() t.save()
new = True new = True
update = '' update = ''
context = { context = {
'ticket': t, 'ticket': t,
'queue': queue, 'queue': queue,
@ -154,26 +175,57 @@ def ticket_from_message(message, queue):
if new: if new:
if sender_email: if sender_email:
send_templated_mail('newticket_submitter', context, recipients=sender_email, sender=queue.from_address, fail_silently=True) send_templated_mail(
'newticket_submitter',
context,
recipients=sender_email,
sender=queue.from_address,
fail_silently=True,
)
if queue.new_ticket_cc: if queue.new_ticket_cc:
send_templated_mail('newticket_cc', context, recipients=queue.new_ticket_cc, sender=queue.from_address, fail_silently=True) send_templated_mail(
'newticket_cc',
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc: context,
send_templated_mail('newticket_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True) 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: else:
update = _(' (Updated)') update = _(' (Updated)')
if t.assigned_to: if t.assigned_to:
send_templated_mail('updated_owner', context, recipients=t.assigned_to.email, sender=queue.from_address, fail_silently=True) send_templated_mail(
'updated_owner',
context,
recipients=t.assigned_to.email,
sender=queue.from_address,
fail_silently=True,
)
if queue.updated_ticket_cc: if queue.updated_ticket_cc:
send_templated_mail('updated_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True) send_templated_mail(
'updated_cc',
context,
recipients=queue.updated_ticket_cc,
sender=queue.from_address,
fail_silently=True,
)
f = FollowUp( f = FollowUp(
ticket = t, ticket = t,
title = _('E-Mail Received from %s' % sender_email), title = _('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
date = datetime.now(), date = datetime.now(),
public = True, public = True,
comment = body, comment = body,
@ -184,11 +236,17 @@ def ticket_from_message(message, queue):
for file in files: for file in files:
filename = file['filename'].replace(' ', '_') filename = file['filename'].replace(' ', '_')
a = Attachment(followup=f, filename=filename, mime_type=file['type'], size=len(file['content'])) a = Attachment(
#a.save_file_file(file['filename'], file['content']) followup=f,
filename=filename,
mime_type=file['type'],
size=len(file['content']),
)
a.file.save(file['filename'], ContentFile(file['content'])) a.file.save(file['filename'], ContentFile(file['content']))
a.save() a.save()
print " - %s" % file['filename'] print " - %s" % file['filename']
if __name__ == '__main__': if __name__ == '__main__':
process_email() process_email()

629
models.py
View File

@ -14,69 +14,192 @@ from django.db import models
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class Queue(models.Model): class Queue(models.Model):
""" """
A queue is a collection of tickets into what would generally be business A queue is a collection of tickets into what would generally be business
areas or departments. areas or departments.
For example, a company may have a queue for each Product they provide, or For example, a company may have a queue for each Product they provide, or
a queue for each of Accounts, Pre-Sales, and Support. a queue for each of Accounts, Pre-Sales, and Support.
TODO: Add e-mail inboxes (either using piped e-mail or IMAP/POP3) so we
can automatically get tickets via e-mail.
""" """
title = models.CharField(_('Title'), max_length=100)
slug = models.SlugField(_('Slug'), 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(_('E-Mail Address'), 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 should be the e-mail address for that mailbox.'))
allow_public_submission = models.BooleanField(_('Allow Public Submission?'), blank=True, null=True, help_text=_('Should this queue be listed on the public submission form?'))
allow_email_submission = models.BooleanField(_('Allow E-Mail Submission?'), blank=True, null=True, help_text=_('Do you want to poll the e-mail box below for new tickets?'))
escalate_days = models.IntegerField(_('Escalation Days'), 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): title = models.CharField(
if not self.email_address: _('Title'),
return u'NO QUEUE EMAIL ADDRESS DEFINED <%s>' % settings.DEFAULT_FROM_EMAIL max_length=100,
else: )
return u'%s <%s>' % (self.title, self.email_address)
from_address = property(_from_address)
new_ticket_cc = models.EmailField(_('New Ticket CC Address'), blank=True, null=True, help_text=_('If an e-mail address is entered here, then it will receive notification of all new tickets created for this queue')) slug = models.SlugField(
updated_ticket_cc = models.EmailField(_('Updated Ticket CC Address'), blank=True, null=True, help_text=_('If an e-mail address is entered here, then it will receive notification of all activity (new tickets, closed tickets, updates, reassignments, etc) for this queue')) _('Slug'),
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_box_type = models.CharField(_('E-Mail Box Type'), max_length=5, choices=(('pop3', _('POP 3')),('imap', _('IMAP'))), blank=True, null=True, help_text=_('E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported.')) email_address = models.EmailField(
email_box_host = models.CharField(_('E-Mail Hostname'), max_length=200, blank=True, null=True, help_text=_('Your e-mail server address - either the domain name or IP address. May be "localhost".')) _('E-Mail Address'),
email_box_port = models.IntegerField(_('E-Mail Port'), blank=True, null=True, help_text=_('Port number to use for accessing e-mail. Default for POP3 is "110", and for IMAP is "143". This may differ on some servers. Leave it blank to use the defaults.')) blank=True,
email_box_user = models.CharField(_('E-Mail Username'), max_length=200, blank=True, null=True, help_text=_('Username for accessing this mailbox.')) null=True,
email_box_pass = models.CharField(_('E-Mail Password'), max_length=200, blank=True, null=True, help_text=_('Password for the above username')) help_text=_('All outgoing e-mails for this queue will use this e-mail '
email_box_imap_folder = models.CharField(_('IMAP Folder'), max_length=100, blank=True, null=True, help_text=_('If using IMAP, what folder do you wish to fetch messages from? This allows you to use one IMAP account for multiple queues, by filtering messages on your IMAP server into separate folders. Default: INBOX.')) 'address. If you use IMAP or POP3, this should be the e-mail '
email_box_interval = models.IntegerField(_('E-Mail Check Interval'), help_text=_('How often do you wish to check this mailbox? (in Minutes)'), blank=True, null=True, default='5') 'address for that mailbox.'),
email_box_last_check = models.DateTimeField(blank=True, null=True, editable=False) # Updated by the auto-pop3-and-imap-checker )
allow_public_submission = models.BooleanField(
_('Allow Public Submission?'),
blank=True,
null=True,
help_text=_('Should this queue be listed on the public submission '
'form?'),
)
allow_email_submission = models.BooleanField(
_('Allow E-Mail Submission?'),
blank=True,
null=True,
help_text=_('Do you want to poll the e-mail box below for new '
'tickets?'),
)
escalate_days = models.IntegerField(
_('Escalation Days'),
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.'),
)
new_ticket_cc = models.EmailField(
_('New Ticket CC Address'),
blank=True,
null=True,
help_text=_('If an e-mail address is entered here, then it will '
'receive notification of all new tickets created for this queue'),
)
updated_ticket_cc = models.EmailField(
_('Updated Ticket CC Address'),
blank=True,
null=True,
help_text=_('If an e-mail address is entered here, then it will '
'receive notification of all activity (new tickets, closed '
'tickets, updates, reassignments, etc) for this queue'),
)
email_box_type = models.CharField(
_('E-Mail Box Type'),
max_length=5,
choices=(('pop3', _('POP 3')), ('imap', _('IMAP'))),
blank=True,
null=True,
help_text=_('E-Mail server type for creating tickets automatically '
'from a mailbox - both POP3 and IMAP are supported.'),
)
email_box_host = models.CharField(
_('E-Mail Hostname'),
max_length=200,
blank=True,
null=True,
help_text=_('Your e-mail server address - either the domain name or '
'IP address. May be "localhost".'),
)
email_box_port = models.IntegerField(
_('E-Mail Port'),
blank=True,
null=True,
help_text=_('Port number to use for accessing e-mail. Default for '
'POP3 is "110", and for IMAP is "143". This may differ on some '
'servers. Leave it blank to use the defaults.'),
)
email_box_user = models.CharField(
_('E-Mail Username'),
max_length=200,
blank=True,
null=True,
help_text=_('Username for accessing this mailbox.'),
)
email_box_pass = models.CharField(
_('E-Mail Password'),
max_length=200,
blank=True,
null=True,
help_text=_('Password for the above username'),
)
email_box_imap_folder = models.CharField(
_('IMAP Folder'),
max_length=100,
blank=True,
null=True,
help_text=_('If using IMAP, what folder do you wish to fetch messages '
'from? This allows you to use one IMAP account for multiple '
'queues, by filtering messages on your IMAP server into separate '
'folders. Default: INBOX.'),
)
email_box_interval = models.IntegerField(
_('E-Mail Check Interval'),
help_text=_('How often do you wish to check this mailbox? (in Minutes)'),
blank=True,
null=True,
default='5',
)
email_box_last_check = models.DateTimeField(
blank=True,
null=True,
editable=False,
# This is updated by management/commands/get_mail.py.
)
def __unicode__(self): def __unicode__(self):
return u"%s" % self.title return u"%s" % self.title
class Meta: class Meta:
ordering = ('title',) ordering = ('title',)
def _from_address(self):
"""
Short property to provide a sender address in SMTP format,
eg 'Name <email>'. We do this so we can put a simple error message
in the sender name field, so hopefully the admin can see and fix it.
"""
if not self.email_address:
return u'NO QUEUE EMAIL ADDRESS DEFINED <%s>' % settings.DEFAULT_FROM_EMAIL
else:
return u'%s <%s>' % (self.title, self.email_address)
from_address = property(_from_address)
def save(self): def save(self):
if self.email_box_type == 'imap' and not self.email_box_imap_folder: if self.email_box_type == 'imap' and not self.email_box_imap_folder:
self.email_box_imap_folder = 'INBOX' self.email_box_imap_folder = 'INBOX'
if not self.email_box_port:
if self.email_box_type == 'imap':
self.email_box_port = 143
else:
self.email_box_port = 110
super(Queue, self).save() super(Queue, self).save()
class Ticket(models.Model): class Ticket(models.Model):
""" """
To allow a ticket to be entered as quickly as possible, only the To allow a ticket to be entered as quickly as possible, only the
bare minimum fields are required. These basically allow us to bare minimum fields are required. These basically allow us to
sort and manage the ticket. The user can always go back and sort and manage the ticket. The user can always go back and
enter more information later. enter more information later.
A good example of this is when a customer is on the phone, and A good example of this is when a customer is on the phone, and
you want to give them a ticket ID as quickly as possible. You can you want to give them a ticket ID as quickly as possible. You can
enter some basic info, save the ticket, give the customer the ID enter some basic info, save the ticket, give the customer the ID
and get off the phone, then add in further detail at a later time and get off the phone, then add in further detail at a later time
(once the customer is not on the line). (once the customer is not on the line).
Note that assigned_to is optional - unassigned tickets are displayed on Note that assigned_to is optional - unassigned tickets are displayed on
the dashboard to prompt users to take ownership of them. the dashboard to prompt users to take ownership of them.
""" """
@ -84,7 +207,7 @@ class Ticket(models.Model):
REOPENED_STATUS = 2 REOPENED_STATUS = 2
RESOLVED_STATUS = 3 RESOLVED_STATUS = 3
CLOSED_STATUS = 4 CLOSED_STATUS = 4
STATUS_CHOICES = ( STATUS_CHOICES = (
(OPEN_STATUS, _('Open')), (OPEN_STATUS, _('Open')),
(REOPENED_STATUS, _('Reopened')), (REOPENED_STATUS, _('Reopened')),
@ -100,26 +223,87 @@ class Ticket(models.Model):
(5, _('5. Very Low')), (5, _('5. Very Low')),
) )
title = models.CharField(_('Title'), max_length=200) title = models.CharField(
_('Title'),
max_length=200,
)
queue = models.ForeignKey(Queue) queue = models.ForeignKey(Queue)
created = models.DateTimeField(_('Created'), blank=True)
modified = models.DateTimeField(_('Modified'), blank=True)
submitter_email = models.EmailField(_('Submitter E-Mail'), blank=True, null=True, help_text=_('The submitter will receive an email for all public follow-ups left for this task.'))
assigned_to = models.ForeignKey(User, related_name='assigned_to', blank=True, null=True)
status = models.IntegerField(_('Status'), choices=STATUS_CHOICES, default=OPEN_STATUS)
on_hold = models.BooleanField(_('On Hold'), blank=True, null=True) created = models.DateTimeField(
_('Created'),
blank=True,
help_text=_('Date this ticket was first created'),
)
description = models.TextField(_('Description'), blank=True, null=True) modified = models.DateTimeField(
resolution = models.TextField(_('Resolution'), blank=True, null=True) _('Modified'),
blank=True,
help_text=_('Date this ticket was most recently changed.'),
)
priority = models.IntegerField(_('Priority'), choices=PRIORITY_CHOICES, default=3, blank=3) submitter_email = models.EmailField(
_('Submitter E-Mail'),
last_escalation = models.DateTimeField(blank=True, null=True, editable=False) blank=True,
null=True,
help_text=_('The submitter will receive an email for all public '
'follow-ups left for this task.'),
)
assigned_to = models.ForeignKey(
User,
related_name='assigned_to',
blank=True,
null=True,
)
status = models.IntegerField(
_('Status'),
choices=STATUS_CHOICES,
default=OPEN_STATUS,
)
on_hold = models.BooleanField(
_('On Hold'),
blank=True,
null=True,
help_text=_('If a ticket is on hold, it will not automatically be '
'escalated.'),
)
description = models.TextField(
_('Description'),
blank=True,
null=True,
help_text=_('The content of the customers query.'),
)
resolution = models.TextField(
_('Resolution'),
blank=True,
null=True,
help_text=_('The resolution provided to the customer by our staff.'),
)
priority = models.IntegerField(
_('Priority'),
choices=PRIORITY_CHOICES,
default=3,
blank=3,
help_text=_('1 = Highest Priority, 5 = Low Priority'),
)
last_escalation = models.DateTimeField(
blank=True,
null=True,
editable=False,
help_text=_('The date this ticket was last escalated - updated '
'automatically by management/commands/escalate_tickets.py.'),
)
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
has a full name configured, we use that, otherwise their username. """ has a full name configured, we use that, otherwise their username. """
if not self.assigned_to: if not self.assigned_to:
return _('Unassigned') return _('Unassigned')
@ -131,44 +315,69 @@ class Ticket(models.Model):
get_assigned_to = property(_get_assigned_to) get_assigned_to = property(_get_assigned_to)
def _get_ticket(self): def _get_ticket(self):
""" A user-friendly ticket ID, which is a combination of ticket ID """ A user-friendly ticket ID, which is a combination of ticket ID
and queue slug. This is generally used in e-mails. """ and queue slug. This is generally used in e-mail subjects. """
return u"[%s]" % (self.ticket_for_url) return u"[%s]" % (self.ticket_for_url)
ticket = property(_get_ticket) ticket = property(_get_ticket)
def _get_ticket_for_url(self): def _get_ticket_for_url(self):
""" A URL-friendly ticket ID, used in links. """
return u"%s-%s" % (self.queue.slug, self.id) return u"%s-%s" % (self.queue.slug, self.id)
ticket_for_url = property(_get_ticket_for_url) ticket_for_url = property(_get_ticket_for_url)
def _get_priority_img(self): def _get_priority_img(self):
""" Image-based representation of the priority """
from django.conf import settings from django.conf import settings
return u"%s/helpdesk/priorities/priority%s.png" % (settings.MEDIA_URL, self.priority) return u"%s/helpdesk/priorities/priority%s.png" % (settings.MEDIA_URL, self.priority)
get_priority_img = property(_get_priority_img) get_priority_img = property(_get_priority_img)
def _get_priority_span(self): def _get_priority_span(self):
"""
A HTML <span> providing a CSS_styled representation of the priority.
"""
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
return mark_safe(u"<span class='priority%s'>%s</span>" % (self.priority, self.priority)) return mark_safe(u"<span class='priority%s'>%s</span>" % (self.priority, self.priority))
get_priority_span = property(_get_priority_span) get_priority_span = property(_get_priority_span)
def _get_status(self): def _get_status(self):
"""
Displays the ticket status, with an "On Hold" message if needed.
"""
held_msg = '' held_msg = ''
if self.on_hold: held_msg = _(' - On Hold') if self.on_hold: held_msg = _(' - On Hold')
return u'%s%s' % (self.get_status_display(), held_msg) return u'%s%s' % (self.get_status_display(), held_msg)
get_status = property(_get_status) get_status = property(_get_status)
def _get_ticket_url(self): def _get_ticket_url(self):
"""
Returns a publicly-viewable URL for this ticket, used when giving
a URL to the submitter of a ticket.
"""
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
site = Site.objects.get_current() site = Site.objects.get_current()
return u"http://%s%s?ticket=%s&email=%s" % (site.domain, reverse('helpdesk_public_view'), self.ticket_for_url, self.submitter_email) return u"http://%s%s?ticket=%s&email=%s" % (
site.domain,
reverse('helpdesk_public_view'),
self.ticket_for_url,
self.submitter_email
)
ticket_url = property(_get_ticket_url) ticket_url = property(_get_ticket_url)
def _get_staff_url(self): def _get_staff_url(self):
"""
Returns a staff-only URL for this ticket, used when giving a URL to
a staff member (in emails etc)
"""
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
site = Site.objects.get_current() site = Site.objects.get_current()
return u"http://%s%s" % (site.domain, reverse('helpdesk_view', args=[self.id])) return u"http://%s%s" % (
site.domain,
reverse('helpdesk_view',
args=[self.id])
)
staff_url = property(_get_staff_url) staff_url = property(_get_staff_url)
class Meta: class Meta:
@ -185,67 +394,120 @@ class Ticket(models.Model):
if not self.id: if not self.id:
# This is a new ticket as no ID yet exists. # This is a new ticket as no ID yet exists.
self.created = datetime.now() self.created = datetime.now()
if not self.priority: if not self.priority:
self.priority = 3 self.priority = 3
self.modified = datetime.now() self.modified = datetime.now()
super(Ticket, self).save() super(Ticket, self).save()
class FollowUpManager(models.Manager): class FollowUpManager(models.Manager):
def private_followups(self): def private_followups(self):
return self.filter(public=False) return self.filter(public=False)
def public_followups(self): def public_followups(self):
return self.filter(public=True) return self.filter(public=True)
class FollowUp(models.Model): class FollowUp(models.Model):
""" A FollowUp is a comment and/or change to a ticket. We keep a simple """
title, the comment entered by the user, and the new status of a ticket A FollowUp is a comment and/or change to a ticket. We keep a simple
title, the comment entered by the user, and the new status of a ticket
to enable easy flagging of details on the view-ticket page. to enable easy flagging of details on the view-ticket page.
The title is automatically generated at save-time, based on what action The title is automatically generated at save-time, based on what action
the user took. the user took.
Tickets that aren't public are never shown to or e-mailed to the submitter, Tickets that aren't public are never shown to or e-mailed to the submitter,
although all staff can see them. although all staff can see them.
""" """
ticket = models.ForeignKey(Ticket) ticket = models.ForeignKey(Ticket)
date = models.DateTimeField(_('Date'), auto_now_add=True)
title = models.CharField(_('Title'), max_length=200, blank=True, null=True) date = models.DateTimeField(
comment = models.TextField(_('Comment'), blank=True, null=True) _('Date'),
public = models.BooleanField(_('Public'), blank=True, null=True) )
user = models.ForeignKey(User, blank=True, null=True)
title = models.CharField(
new_status = models.IntegerField(_('New Status'), choices=Ticket.STATUS_CHOICES, blank=True, null=True) _('Title'),
max_length=200,
blank=True,
null=True,
)
comment = models.TextField(
_('Comment'),
blank=True,
null=True,
)
public = models.BooleanField(
_('Public'),
blank=True,
null=True,
help_text=_('Public tickets are viewable by the submitter and all '
'staff, but non-public tickets can only be seen by staff.'),
)
user = models.ForeignKey(
User,
blank=True,
null=True,
)
new_status = models.IntegerField(
_('New Status'),
choices=Ticket.STATUS_CHOICES,
blank=True,
null=True,
help_text=_('If the status was changed, what was it changed to?'),
)
objects = FollowUpManager() objects = FollowUpManager()
class Meta: class Meta:
ordering = ['date'] ordering = ['date']
def __unicode__(self): def __unicode__(self):
return u'%s' % self.title return u'%s' % self.title
def get_absolute_url(self): def get_absolute_url(self):
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id) return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
def save(self): def save(self):
t = self.ticket t = self.ticket
t.modified = datetime.now() t.modified = datetime.now()
self.date = datetime.now()
t.save() t.save()
super(FollowUp, self).save() super(FollowUp, self).save()
class TicketChange(models.Model): class TicketChange(models.Model):
""" For each FollowUp, any changes to the parent ticket (eg Title, Priority, """
For each FollowUp, any changes to the parent ticket (eg Title, Priority,
etc) are tracked here for display purposes. etc) are tracked here for display purposes.
""" """
followup = models.ForeignKey(FollowUp) followup = models.ForeignKey(FollowUp)
field = models.CharField(_('Field'), max_length=100)
old_value = models.TextField(_('Old Value'), blank=True, null=True) field = models.CharField(
new_value = models.TextField(_('New Value'), blank=True, null=True) _('Field'),
max_length=100,
)
old_value = models.TextField(
_('Old Value'),
blank=True,
null=True,
)
new_value = models.TextField(
_('New Value'),
blank=True,
null=True,
)
def __unicode__(self): def __unicode__(self):
str = u'%s ' % field str = u'%s ' % field
@ -254,7 +516,10 @@ class TicketChange(models.Model):
elif not old_value: elif not old_value:
str += _('set to %s' % new_value) str += _('set to %s' % new_value)
else: else:
str += _('changed from "%(old_value)s" to "%(new_value)s"' % {'old_value': old_value, 'new_value': new_value}) str += _('changed from "%(old_value)s" to "%(new_value)s"' % {
'old_value': old_value,
'new_value': new_value
})
return str return str
@ -263,7 +528,7 @@ class DynamicFileField(models.FileField):
Allows model instance to specify upload_to dynamically. Allows model instance to specify upload_to dynamically.
Model class should have a method like: Model class should have a method like:
def get_upload_to(self, attname): def get_upload_to(self, attname):
return 'path/to/%d' % self.parent.id return 'path/to/%d' % self.parent.id
@ -282,18 +547,43 @@ class DynamicFileField(models.FileField):
def db_type(self): def db_type(self):
"""Required by Django for ORM.""" """Required by Django for ORM."""
return 'varchar(100)' return 'varchar(100)'
class Attachment(models.Model): class Attachment(models.Model):
"""
Represents a file attached to a follow-up. This could come from an e-mail
attachment, or it could be uploaded via the web interface.
"""
followup = models.ForeignKey(FollowUp) followup = models.ForeignKey(FollowUp)
file = DynamicFileField(_('File'), upload_to='helpdesk/attachments')
filename = models.CharField(_('Filename'), max_length=100) file = DynamicFileField(
mime_type = models.CharField(_('MIME Type'), max_length=30) _('File'),
size = models.IntegerField(_('Size'), help_text=_('Size of this file in bytes')) upload_to='helpdesk/attachments',
)
filename = models.CharField(
_('Filename'),
max_length=100,
)
mime_type = models.CharField(
_('MIME Type'),
max_length=30,
)
size = models.IntegerField(
_('Size'),
help_text=_('Size of this file in bytes'),
)
def get_upload_to(self, field_attname): def get_upload_to(self, field_attname):
""" Get upload_to path specific to this item """ """ Get upload_to path specific to this item """
return u'helpdesk/attachments/%s/%s' % (self.followup.ticket.ticket_for_url, self.followup.id) return u'helpdesk/attachments/%s/%s' % (
self.followup.ticket.ticket_for_url,
self.followup.id
)
def __unicode__(self): def __unicode__(self):
return u'%s' % self.filename return u'%s' % self.filename
@ -303,18 +593,38 @@ class Attachment(models.Model):
class PreSetReply(models.Model): class PreSetReply(models.Model):
""" We can allow the admin to define a number of pre-set replies, used to """
We can allow the admin to define a number of pre-set replies, used to
simplify the sending of updates and resolutions. These are basically Django simplify the sending of updates and resolutions. These are basically Django
templates with a limited context - however if yo uwanted to get crafy it would templates with a limited context - however if you wanted to get crafy it would
be easy to write a reply that displays ALL updates in hierarchical order etc be easy to write a reply that displays ALL updates in hierarchical order etc
with use of for loops over {{ ticket.followup_set.all }} and friends. with use of for loops over {{ ticket.followup_set.all }} and friends.
When replying to a ticket, the user can select any reply set for the current When replying to a ticket, the user can select any reply set for the current
queue, and the body text is fetched via AJAX.""" queue, and the body text is fetched via AJAX.
"""
queues = models.ManyToManyField(Queue, blank=True, null=True, help_text=_('Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.'))
name = models.CharField(_('Name'), max_length=100, help_text=_('Only used to assist users with selecting a reply - not shown to the user.')) queues = models.ManyToManyField(
body = models.TextField(_('Body'), help_text=_('Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); {{ queue }} - The queue; and {{ user }} - the current user.')) Queue,
blank=True,
null=True,
help_text=_('Leave blank to allow this reply to be used for all '
'queues, or select those queues you wish to limit this reply to.'),
)
name = models.CharField(
_('Name'),
max_length=100,
help_text=_('Only used to assist users with selecting a reply - not '
'shown to the user.'),
)
body = models.TextField(
_('Body'),
help_text=_('Context available: {{ ticket }} - ticket object (eg '
'{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} '
'- the current user.'),
)
class Meta: class Meta:
ordering = ['name',] ordering = ['name',]
@ -322,28 +632,84 @@ 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 be applied to all queues, or select those queues you wish to exclude with this entry.'))
name = models.CharField(_('Name'), max_length=100) class EscalationExclusion(models.Model):
"""
date = models.DateField(_('Date'), help_text=_('Date on which escalation should not happen')) An 'EscalationExclusion' lets us define a date on which escalation should
not happen, for example a weekend or public holiday.
You may also have a queue that is only used on one day per week.
To create these on a regular basis, check out the README file for an
example cronjob that runs 'create_escalation_exclusions.py'.
"""
queues = models.ManyToManyField(
Queue,
blank=True,
null=True,
help_text=_('Leave blank for this exclusion to be applied to all '
'queues, or select those queues you wish to exclude with this '
'entry.'),
)
name = models.CharField(
_('Name'),
max_length=100,
)
date = models.DateField(
_('Date'),
help_text=_('Date on which escalation should not happen'),
)
def __unicode__(self): def __unicode__(self):
return u'%s' % self.name return u'%s' % self.name
class EmailTemplate(models.Model): class EmailTemplate(models.Model):
""" """
Since these are more likely to be changed than other templates, we store Since these are more likely to be changed than other templates, we store
them in the database. them in the database.
This means that an admin can change email templates without having to have
access to the filesystem.
""" """
template_name = models.CharField(_('Template Name'), max_length=100, unique=True) template_name = models.CharField(
_('Template Name'),
subject = models.CharField(_('Subject'), max_length=100, help_text=_('This will be prefixed with "[ticket.ticket] ticket.title". We recommend something simple such as "(Updated") or "(Closed)" - the same context is available as in plain_text, below.')) max_length=100,
heading = models.CharField(_('Heading'), max_length=100, help_text=_('In HTML e-mails, this will be the heading at the top of the email - the same context is available as in plain_text, below.')) unique=True,
plain_text = models.TextField(_('Plain Text'), help_text=_('The context available to you includes {{ ticket }}, {{ queue }}, and depending on the time of the call: {{ resolution }} or {{ comment }}.')) )
html = models.TextField(_('HTML'), help_text=_('The same context is available here as in plain_text, above.'))
subject = models.CharField(
_('Subject'),
max_length=100,
help_text=_('This will be prefixed with "[ticket.ticket] ticket.title"'
'. We recommend something simple such as "(Updated") or "(Closed)"'
' - the same context is available as in plain_text, below.'),
)
heading = models.CharField(
_('Heading'),
max_length=100,
help_text=_('In HTML e-mails, this will be the heading at the top of '
'the email - the same context is available as in plain_text, '
'below.'),
)
plain_text = models.TextField(
_('Plain Text'),
help_text=_('The context available to you includes {{ ticket }}, '
'{{ queue }}, and depending on the time of the call: '
'{{ resolution }} or {{ comment }}.'),
)
html = models.TextField(
_('HTML'),
help_text=_('The same context is available here as in plain_text, '
'above.'),
)
def __unicode__(self): def __unicode__(self):
return u'%s' % self.template_name return u'%s' % self.template_name
@ -351,40 +717,72 @@ class EmailTemplate(models.Model):
class Meta: class Meta:
ordering = ['template_name',] ordering = ['template_name',]
class KBCategory(models.Model): class KBCategory(models.Model):
""" """
Lets help users help themselves: the Knowledge Base is a categorised Lets help users help themselves: the Knowledge Base is a categorised
listing of questions & answers. listing of questions & answers.
""" """
title = models.CharField(_('Title'), max_length=100) title = models.CharField(
slug = models.SlugField(_('Slug')) _('Title'),
description = models.TextField(_('Description')) max_length=100,
)
slug = models.SlugField(
_('Slug'),
)
description = models.TextField(
_('Description'),
)
def __unicode__(self): def __unicode__(self):
return u'%s' % self.title return u'%s' % self.title
class Meta: class Meta:
ordering = ['title',] ordering = ['title',]
def get_absolute_url(self): def get_absolute_url(self):
return ('helpdesk_kb_category', [str(self.slug)]) return ('helpdesk_kb_category', [str(self.slug)])
get_absolute_url = models.permalink(get_absolute_url) get_absolute_url = models.permalink(get_absolute_url)
class KBItem(models.Model): class KBItem(models.Model):
""" """
An item within the knowledgebase. Very straightforward question/answer An item within the knowledgebase. Very straightforward question/answer
style system. style system.
""" """
category = models.ForeignKey(KBCategory) category = models.ForeignKey(KBCategory)
title = models.CharField(_('Title'), max_length=100)
question = models.TextField(_('Question'))
answer = models.TextField(_('Answer'))
votes = models.IntegerField(_('Votes'), help_text=_('Total number of votes cast for this item'))
recommendations = models.IntegerField(_('Positive Votes'), help_text=_('Number of votes for this item which were POSITIVE.'))
last_updated = models.DateTimeField(_('Last Updated')) title = models.CharField(
_('Title'),
max_length=100,
)
question = models.TextField(
_('Question'),
)
answer = models.TextField(
_('Answer'),
)
votes = models.IntegerField(
_('Votes'),
help_text=_('Total number of votes cast for this item'),
)
recommendations = models.IntegerField(
_('Positive Votes'),
help_text=_('Number of votes for this item which were POSITIVE.'),
)
last_updated = models.DateTimeField(
_('Last Updated'),
help_text=_('The date on which this question was most recently '
'changed.'),
)
def save(self): def save(self):
self.last_updated = datetime.now() self.last_updated = datetime.now()
@ -396,7 +794,7 @@ class KBItem(models.Model):
else: else:
return _('Unrated') return _('Unrated')
score = property(_score) score = property(_score)
def __unicode__(self): def __unicode__(self):
return u'%s' % self.title return u'%s' % self.title
@ -406,3 +804,4 @@ class KBItem(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return ('helpdesk_kb_item', [str(self.id)]) return ('helpdesk_kb_item', [str(self.id)])
get_absolute_url = models.permalink(get_absolute_url) get_absolute_url = models.permalink(get_absolute_url)

View File

@ -3,7 +3,7 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
templatetags/in_list.py - Very simple template tag to allow us to use the templatetags/in_list.py - Very simple template tag to allow us to use the
equivilent of 'if x in y' in templates. eg: equivilent of 'if x in y' in templates. eg:
Assuming 'food' = 'pizza' and 'best_foods' = ['pizza', 'pie', 'cake]: Assuming 'food' = 'pizza' and 'best_foods' = ['pizza', 'pie', 'cake]:
@ -14,8 +14,11 @@ Assuming 'food' = 'pizza' and 'best_foods' = ['pizza', 'pie', 'cake]:
Your food isn't one of our favourites. Your food isn't one of our favourites.
{% endif %} {% endif %}
""" """
from django import template from django import template
def in_list(value, arg): def in_list(value, arg):
return value in arg return value in arg
register = template.Library() register = template.Library()
register.filter(in_list) register.filter(in_list)

View File

@ -5,18 +5,24 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
templatetags/ticket_to_link.py - Used in ticket comments to allow wiki-style templatetags/ticket_to_link.py - Used in ticket comments to allow wiki-style
linking to other tickets. Including text such linking to other tickets. Including text such
as '#3180' in a comment automatically links as '#3180' in a comment automatically links
that text to ticket number 3180, with styling that text to ticket number 3180, with styling
to show the status of that ticket (eg a closed to show the status of that ticket (eg a closed
ticket would have a strikethrough). ticket would have a strikethrough).
""" """
import re
from django import template from django import template
from django.core.urlresolvers import reverse
from helpdesk.models import Ticket from helpdesk.models import Ticket
class ReverseProxy: class ReverseProxy:
def __init__(self, sequence): def __init__(self, sequence):
self.sequence = sequence self.sequence = sequence
def __iter__(self): def __iter__(self):
length = len(self.sequence) length = len(self.sequence)
i = length i = length
@ -24,16 +30,15 @@ class ReverseProxy:
i = i - 1 i = i - 1
yield self.sequence[i] yield self.sequence[i]
def num_to_link(text): def num_to_link(text):
if text == '': if text == '':
return text return text
import re
from django.core.urlresolvers import reverse
matches = [] matches = []
for match in re.finditer("#(\d+)", text): for match in re.finditer("#(\d+)", text):
matches.append(match) matches.append(match)
for match in ReverseProxy(matches): for match in ReverseProxy(matches):
start = match.start() start = match.start()
end = match.end() end = match.end()

38
urls.py
View File

@ -3,43 +3,41 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
urls.py - Mapping of URL's to our various views. Note we always used NAMED urls.py - Mapping of URL's to our various views. Note we always used NAMED
views for simplicity in linking later on. views for simplicity in linking later on.
""" """
from django.conf.urls.defaults import * from django.conf.urls.defaults import *
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.syndication.views import feed as django_feed
from helpdesk.views.feeds import feed_setup from helpdesk.views.feeds import feed_setup
from django.contrib.syndication.views import feed as django_feed
urlpatterns = patterns('helpdesk.views.staff', urlpatterns = patterns('helpdesk.views.staff',
url(r'^dashboard/$', url(r'^dashboard/$',
'dashboard', 'dashboard',
name='helpdesk_dashboard'), name='helpdesk_dashboard'),
url(r'^tickets/$', url(r'^tickets/$',
'ticket_list', 'ticket_list',
name='helpdesk_list'), name='helpdesk_list'),
url(r'^tickets/submit/$', url(r'^tickets/submit/$',
'create_ticket', 'create_ticket',
name='helpdesk_submit'), name='helpdesk_submit'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/$', url(r'^tickets/(?P<ticket_id>[0-9]+)/$',
'view_ticket', 'view_ticket',
name='helpdesk_view'), name='helpdesk_view'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$', url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$',
'update_ticket', 'update_ticket',
name='helpdesk_update'), name='helpdesk_update'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/delete/$', url(r'^tickets/(?P<ticket_id>[0-9]+)/delete/$',
'delete_ticket', 'delete_ticket',
name='helpdesk_delete'), name='helpdesk_delete'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/hold/$', url(r'^tickets/(?P<ticket_id>[0-9]+)/hold/$',
'hold_ticket', 'hold_ticket',
name='helpdesk_hold'), name='helpdesk_hold'),
@ -51,7 +49,7 @@ urlpatterns = patterns('helpdesk.views.staff',
url(r'^raw/(?P<type>\w+)/$', url(r'^raw/(?P<type>\w+)/$',
'raw_details', 'raw_details',
name='helpdesk_raw'), name='helpdesk_raw'),
url(r'^rss/$', url(r'^rss/$',
'rss_list', 'rss_list',
name='helpdesk_rss_index'), name='helpdesk_rss_index'),
@ -59,14 +57,14 @@ urlpatterns = patterns('helpdesk.views.staff',
url(r'^reports/$', url(r'^reports/$',
'report_index', 'report_index',
name='helpdesk_report_index'), name='helpdesk_report_index'),
url(r'^reports/(?P<report>\w+)/$', url(r'^reports/(?P<report>\w+)/$',
'run_report', 'run_report',
name='helpdesk_run_report'), name='helpdesk_run_report'),
) )
urlpatterns += patterns('helpdesk.views.public', urlpatterns += patterns('helpdesk.views.public',
url(r'^$', url(r'^$',
'homepage', 'homepage',
name='helpdesk_home'), name='helpdesk_home'),
@ -80,7 +78,7 @@ urlpatterns += patterns('',
login_required(django_feed), login_required(django_feed),
{'feed_dict': feed_setup}, {'feed_dict': feed_setup},
name='helpdesk_rss'), name='helpdesk_rss'),
url(r'^api/(?P<method>[a-z_-]+)/$', url(r'^api/(?P<method>[a-z_-]+)/$',
'helpdesk.views.api.api', 'helpdesk.views.api.api',
name='helpdesk_api'), name='helpdesk_api'),
@ -89,7 +87,7 @@ urlpatterns += patterns('',
'django.views.generic.simple.direct_to_template', 'django.views.generic.simple.direct_to_template',
{'template': 'helpdesk/api_help.html',}, {'template': 'helpdesk/api_help.html',},
name='helpdesk_api_help'), name='helpdesk_api_help'),
url(r'^login/$', url(r'^login/$',
'django.contrib.auth.views.login', 'django.contrib.auth.views.login',
name='login'), name='login'),
@ -102,13 +100,13 @@ urlpatterns += patterns('',
urlpatterns += patterns('helpdesk.views.kb', urlpatterns += patterns('helpdesk.views.kb',
url(r'^kb/$', url(r'^kb/$',
'index', name='helpdesk_kb_index'), 'index', name='helpdesk_kb_index'),
url(r'^kb/(?P<slug>[A-Za-z_-]+)/$', url(r'^kb/(?P<slug>[A-Za-z_-]+)/$',
'category', name='helpdesk_kb_category'), 'category', name='helpdesk_kb_category'),
url(r'^kb/(?P<item>[0-9]+)/$', url(r'^kb/(?P<item>[0-9]+)/$',
'item', name='helpdesk_kb_item'), 'item', name='helpdesk_kb_item'),
url(r'^kb/(?P<item>[0-9]+)/vote/$', url(r'^kb/(?P<item>[0-9]+)/vote/$',
'vote', name='helpdesk_kb_vote'), 'vote', name='helpdesk_kb_vote'),
) )

View File

@ -1,4 +1,4 @@
""" .. """ ..
Jutda Helpdesk - A Django powered ticket tracker for small enterprise. Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
@ -6,23 +6,24 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
api.py - Wrapper around API calls, and core functions to provide complete api.py - Wrapper around API calls, and core functions to provide complete
API to third party applications. API to third party applications.
The API documentation can be accessed by visiting http://helpdesk/api/help/ The API documentation can be accessed by visiting http://helpdesk/api/help/
(obviously, substitute helpdesk for your Jutda Helpdesk URI), or by reading (obviously, substitute helpdesk for your Jutda Helpdesk URI), or by reading
through templates/helpdesk/api_help.html. through templates/helpdesk/api_help.html.
""" """
from datetime import datetime
from django.utils import simplejson
from django.contrib.auth.models import User from datetime import datetime
from django import forms
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.template import loader, Context from django.template import loader, Context
from django import forms from django.utils import simplejson
from helpdesk.forms import TicketForm
from helpdesk.lib import send_templated_mail from helpdesk.lib import send_templated_mail
from helpdesk.models import Ticket, Queue, FollowUp from helpdesk.models import Ticket, Queue, FollowUp
from helpdesk.forms import TicketForm
STATUS_OK = 200 STATUS_OK = 200
@ -31,27 +32,34 @@ STATUS_ERROR_NOT_FOUND = 404
STATUS_ERROR_PERMISSIONS = 403 STATUS_ERROR_PERMISSIONS = 403
STATUS_ERROR_BADMETHOD = 405 STATUS_ERROR_BADMETHOD = 405
def api(request, method):
if method == 'help':
""" Regardless of any other paramaters, we provide a help screen
to the user if they requested one. """
return render_to_response('helpdesk/api_help.html')
def api(request, method):
""" """
Regardless of any other paramaters, we provide a help screen
to the user if they requested one.
If the user isn't looking for help, then we enforce a few conditions: If the user isn't looking for help, then we enforce a few conditions:
* The request must be sent via HTTP POST * The request must be sent via HTTP POST
* The request must contain a 'user' and 'password' which * The request must contain a 'user' and 'password' which
must be valid users must be valid users
* The method must match one of the public methods of the API class. * The method must match one of the public methods of the API class.
""" """
if method == 'help':
return render_to_response('helpdesk/api_help.html')
if request.method != 'POST': if request.method != 'POST':
return api_return(STATUS_ERROR_BADMETHOD) return api_return(STATUS_ERROR_BADMETHOD)
request.user = authenticate(username=request.POST.get('user', False), password=request.POST.get('password')) # TODO: Move away from having the username & password in every request.
request.user = authenticate(
username=request.POST.get('user', False),
password=request.POST.get('password'))
if request.user is None: if request.user is None:
return api_return(STATUS_ERROR_PERMISSIONS) return api_return(STATUS_ERROR_PERMISSIONS)
api = API(request) api = API(request)
if hasattr(api, 'api_public_%s' % method): if hasattr(api, 'api_public_%s' % method):
return getattr(api, 'api_public_%s' % method)() return getattr(api, 'api_public_%s' % method)()
@ -63,7 +71,7 @@ def api_return(status, text='', json=False):
content_type = 'text/plain' content_type = 'text/plain'
if status == STATUS_OK and json: if status == STATUS_OK and json:
content_type = 'text/json' content_type = 'text/json'
if text is None: if text is None:
if status == STATUS_ERROR: if status == STATUS_ERROR:
text = 'Error' text = 'Error'
@ -75,10 +83,12 @@ def api_return(status, text='', json=False):
text = 'Invalid request method' text = 'Invalid request method'
elif status == STATUS_OK: elif status == STATUS_OK:
text = 'OK' text = 'OK'
r = HttpResponse(status=status, content=text, content_type=content_type) r = HttpResponse(status=status, content=text, content_type=content_type)
if status == STATUS_ERROR_BADMETHOD: if status == STATUS_ERROR_BADMETHOD:
r.Allow = 'POST' r.Allow = 'POST'
return r return r
@ -86,27 +96,30 @@ class API:
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request
def api_public_create_ticket(self):
def api_public_create_ticket(self):
form = TicketForm(self.request.POST) form = TicketForm(self.request.POST)
form.fields['queue'].choices = [[q.id, q.title] for q in Queue.objects.all()] form.fields['queue'].choices = [[q.id, q.title] for q in Queue.objects.all()]
form.fields['assigned_to'].choices = [[u.id, u.username] for u in User.objects.filter(is_active=True)] form.fields['assigned_to'].choices = [[u.id, u.username] for u in User.objects.filter(is_active=True)]
if form.is_valid(): if form.is_valid():
ticket = form.save(user=self.request.user) ticket = form.save(user=self.request.user)
return api_return(STATUS_OK, "%s" % ticket.id) return api_return(STATUS_OK, "%s" % ticket.id)
else: else:
return api_return(STATUS_ERROR, text=form.errors.as_text()) return api_return(STATUS_ERROR, text=form.errors.as_text())
def api_public_list_queues(self): def api_public_list_queues(self):
return api_return(STATUS_OK, simplejson.dumps([{"id": "%s" % q.id, "title": "%s" % q.title} for q in Queue.objects.all()]), json=True) return api_return(STATUS_OK, simplejson.dumps([{"id": "%s" % q.id, "title": "%s" % q.title} for q in Queue.objects.all()]), json=True)
def api_public_find_user(self): def api_public_find_user(self):
username = self.request.POST.get('username', False) username = self.request.POST.get('username', False)
try: try:
u = User.objects.get(username=username) u = User.objects.get(username=username)
return api_return(STATUS_OK, "%s" % u.id) return api_return(STATUS_OK, "%s" % u.id)
except User.DoesNotExist: except User.DoesNotExist:
return api_return(STATUS_ERROR, "Invalid username provided") return api_return(STATUS_ERROR, "Invalid username provided")
@ -121,9 +134,10 @@ class API:
return api_return(STATUS_ERROR, "Invalid ticket ID") return api_return(STATUS_ERROR, "Invalid ticket ID")
ticket.delete() ticket.delete()
return api_return(STATUS_OK) return api_return(STATUS_OK)
def api_public_hold_ticket(self): def api_public_hold_ticket(self):
try: try:
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False)) ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
@ -135,7 +149,7 @@ class API:
return api_return(STATUS_OK) return api_return(STATUS_OK)
def api_public_unhold_ticket(self): def api_public_unhold_ticket(self):
try: try:
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False)) ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
@ -162,12 +176,20 @@ class API:
if not message: if not message:
return api_return(STATUS_ERROR, "Blank message") return api_return(STATUS_ERROR, "Blank message")
f = FollowUp(ticket=ticket, date=datetime.now(), comment=message, user=self.request.user, title='Comment Added') f = FollowUp(
ticket=ticket,
date=datetime.now(),
comment=message,
user=self.request.user,
title='Comment Added',
)
if public: if public:
f.public = True f.public = True
f.save() f.save()
context = { context = {
'ticket': ticket, 'ticket': ticket,
'queue': ticket.queue, 'queue': ticket.queue,
@ -175,13 +197,31 @@ class API:
} }
if public and ticket.submitter_email: if public and ticket.submitter_email:
send_templated_mail('updated_submitter', context, recipients=ticket.submitter_email, sender=ticket.queue.from_address, fail_silently=True) send_templated_mail(
'updated_submitter',
context,
recipients=ticket.submitter_email,
sender=ticket.queue.from_address,
fail_silently=True,
)
if ticket.queue.updated_ticket_cc: if ticket.queue.updated_ticket_cc:
send_templated_mail('updated_cc', context, recipients=ticket.queue.updated_ticket_cc, sender=ticket.queue.from_address, fail_silently=True) send_templated_mail(
'updated_cc',
context,
recipients=ticket.queue.updated_ticket_cc,
sender=ticket.queue.from_address,
fail_silently=True,
)
if ticket.assigned_to and self.request.user != ticket.assigned_to: if ticket.assigned_to and self.request.user != ticket.assigned_to:
send_templated_mail('updated_owner', context, recipients=ticket.assigned_to.email, sender=ticket.queue.from_address, fail_silently=True) send_templated_mail(
'updated_owner',
context,
recipients=ticket.assigned_to.email,
sender=ticket.queue.from_address,
fail_silently=True,
)
ticket.save() ticket.save()
@ -198,26 +238,51 @@ class API:
if not resolution: if not resolution:
return api_return(STATUS_ERROR, "Blank resolution") return api_return(STATUS_ERROR, "Blank resolution")
f = FollowUp(ticket=ticket, date=datetime.now(), comment=resolution, user=self.request.user, title='Resolved', public=True) f = FollowUp(
ticket=ticket,
date=datetime.now(),
comment=resolution,
user=self.request.user,
title='Resolved',
public=True,
)
f.save() f.save()
context = { context = {
'ticket': ticket, 'ticket': ticket,
'queue': ticket.queue, 'queue': ticket.queue,
'resolution': f.comment, 'resolution': f.comment,
} }
subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title) subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title)
if ticket.submitter_email: if ticket.submitter_email:
send_templated_mail('resolved_submitter', context, recipients=ticket.submitter_email, sender=ticket.queue.from_address, fail_silently=True) send_templated_mail(
'resolved_submitter',
context,
recipients=ticket.submitter_email,
sender=ticket.queue.from_address,
fail_silently=True,
)
if ticket.queue.updated_ticket_cc: if ticket.queue.updated_ticket_cc:
send_templated_mail('resolved_cc', context, recipients=ticket.queue.updated_ticket_cc, sender=ticket.queue.from_address, fail_silently=True) send_templated_mail(
'resolved_cc',
context,
recipients=ticket.queue.updated_ticket_cc,
sender=ticket.queue.from_address,
fail_silently=True,
)
if ticket.assigned_to and self.request.user != ticket.assigned_to: if ticket.assigned_to and self.request.user != ticket.assigned_to:
send_templated_mail('resolved_resolved', context, recipients=ticket.assigned_to.email, sender=ticket.queue.from_address, fail_silently=True) send_templated_mail(
'resolved_resolved',
context,
recipients=ticket.assigned_to.email,
sender=ticket.queue.from_address,
fail_silently=True,
)
ticket.resoltuion = f.comment ticket.resoltuion = f.comment
ticket.status = Ticket.RESOLVED_STATUS ticket.status = Ticket.RESOLVED_STATUS
@ -225,3 +290,4 @@ class API:
ticket.save() ticket.save()
return api_return(STATUS_OK) return api_return(STATUS_OK)

View File

@ -1,3 +1,12 @@
"""
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
views/feeds.py - A handful of staff-only RSS feeds to provide ticket details
to feed readers or similar software.
"""
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.syndication.feeds import Feed from django.contrib.syndication.feeds import Feed
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -23,28 +32,55 @@ class OpenTicketsByUser(Feed):
def title(self, obj): def title(self, obj):
if obj['queue']: if obj['queue']:
return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {'queue': obj['queue'].title, 'username': obj['user'].username} return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {
'queue': obj['queue'].title,
'username': obj['user'].username,
}
else: else:
return _("Helpdesk: Open Tickets for %(username)s") % {'username': obj['user'].username} return _("Helpdesk: Open Tickets for %(username)s") % {
'username': obj['user'].username,
}
def description(self, obj): def description(self, obj):
if obj['queue']: if obj['queue']:
return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % {'queue': obj['queue'].title, 'username': obj['user'].username} return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % {
'queue': obj['queue'].title,
'username': obj['user'].username,
}
else: else:
return _("Open and Reopened Tickets for %(username)s") % {'username': obj['user'].username} return _("Open and Reopened Tickets for %(username)s") % {
'username': obj['user'].username,
}
def link(self, obj): def link(self, obj):
if obj['queue']: if obj['queue']:
return u'%s?assigned_to=%s&queue=%s' % (reverse('helpdesk_list'), obj['user'].id, obj['queue'].id) return u'%s?assigned_to=%s&queue=%s' % (
reverse('helpdesk_list'),
obj['user'].id,
obj['queue'].id,
)
else: else:
return u'%s?assigned_to=%s' % (reverse('helpdesk_list'), obj['user'].id) return u'%s?assigned_to=%s' % (
reverse('helpdesk_list'),
obj['user'].id,
)
def items(self, obj): def items(self, obj):
if obj['queue']: if obj['queue']:
return Ticket.objects.filter(assigned_to=obj['user']).filter(queue=obj['queue']).filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)) return Ticket.objects.filter(
assigned_to=obj['user']
).filter(
queue=obj['queue']
).filter(
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
)
else: else:
return Ticket.objects.filter(assigned_to=obj['user']).filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)) return Ticket.objects.filter(
assigned_to=obj['user']
).filter(
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
)
def item_pubdate(self, item): def item_pubdate(self, item):
return item.created return item.created
@ -58,18 +94,22 @@ class OpenTicketsByUser(Feed):
class UnassignedTickets(Feed): class UnassignedTickets(Feed):
title_template = 'helpdesk/rss/ticket_title.html' title_template = 'helpdesk/rss/ticket_title.html'
description_template = 'helpdesk/rss/ticket_description.html' description_template = 'helpdesk/rss/ticket_description.html'
title = _('Helpdesk: Unassigned Tickets') title = _('Helpdesk: Unassigned Tickets')
description = _('Unassigned Open and Reopened tickets') description = _('Unassigned Open and Reopened tickets')
link = ''#%s?assigned_to=' % reverse('helpdesk_list') link = ''#%s?assigned_to=' % reverse('helpdesk_list')
def items(self, obj): def items(self, obj):
return Ticket.objects.filter(assigned_to__isnull=True).filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)) return Ticket.objects.filter(
assigned_to__isnull=True
).filter(
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
)
def item_pubdate(self, item): def item_pubdate(self, item):
return item.created return item.created
def item_author_name(self, item): def item_author_name(self, item):
if item.assigned_to: if item.assigned_to:
return item.assigned_to.username return item.assigned_to.username
@ -99,20 +139,31 @@ class OpenTicketsByQueue(Feed):
return Queue.objects.get(slug__exact=bits[0]) return Queue.objects.get(slug__exact=bits[0])
def title(self, obj): def title(self, obj):
return _('Helpdesk: Open Tickets in queue %(queue)s') % {'queue': obj.title} return _('Helpdesk: Open Tickets in queue %(queue)s') % {
'queue': obj.title,
}
def description(self, obj): def description(self, obj):
return _('Open and Reopened Tickets in queue %(queue)s') % {'queue': obj.title} return _('Open and Reopened Tickets in queue %(queue)s') % {
'queue': obj.title,
}
def link(self, obj): def link(self, obj):
return '%s?queue=%s' % (reverse('helpdesk_list'), obj.id) return '%s?queue=%s' % (
reverse('helpdesk_list'),
obj.id,
)
def items(self, obj): def items(self, obj):
return Ticket.objects.filter(queue=obj).filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)) return Ticket.objects.filter(
queue=obj
).filter(
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
)
def item_pubdate(self, item): def item_pubdate(self, item):
return item.created return item.created
def item_author_name(self, item): def item_author_name(self, item):
if item.assigned_to: if item.assigned_to:
return item.assigned_to.username return item.assigned_to.username

View File

@ -3,10 +3,11 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
views/kb.py - Public-facing knowledgebase views. The knowledgebase is a views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
simple categorised question/answer system to show common simple categorised question/answer system to show common
resolutions to common problems. resolutions to common problems.
""" """
from datetime import datetime from datetime import datetime
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
@ -16,23 +17,26 @@ from django.utils.translation import ugettext as _
from helpdesk.models import KBCategory, KBItem from helpdesk.models import KBCategory, KBItem
def index(request): def index(request):
category_list = KBCategory.objects.all() category_list = KBCategory.objects.all()
# Add most popular items here. # TODO: It'd be great to have a list of most popular items here.
return render_to_response('helpdesk/kb_index.html', return render_to_response('helpdesk/kb_index.html',
RequestContext(request, { RequestContext(request, {
'categories': category_list, 'categories': category_list,
})) }))
def category(request, slug): def category(request, slug):
category = get_object_or_404(KBCategory, slug__iexact=slug) category = get_object_or_404(KBCategory, slug__iexact=slug)
items = category.kbitem_set.all() items = category.kbitem_set.all()
return render_to_response('helpdesk/kb_category.html', return render_to_response('helpdesk/kb_category.html',
RequestContext(request, { RequestContext(request, {
'category': category, 'category': category,
'items': items, 'items': items,
})) }))
def item(request, item): def item(request, item):
item = get_object_or_404(KBItem, pk=item) item = get_object_or_404(KBItem, pk=item)
return render_to_response('helpdesk/kb_item.html', return render_to_response('helpdesk/kb_item.html',
@ -40,6 +44,7 @@ def item(request, item):
'item': item, 'item': item,
})) }))
def vote(request, item): def vote(request, item):
item = get_object_or_404(KBItem, pk=item) item = get_object_or_404(KBItem, pk=item)
vote = request.GET.get('vote', None) vote = request.GET.get('vote', None)
@ -50,3 +55,4 @@ def vote(request, item):
item.save() item.save()
return HttpResponseRedirect(item.get_absolute_url()) return HttpResponseRedirect(item.get_absolute_url())

View File

@ -6,6 +6,7 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
views/public.py - All public facing views, eg non-staff (no authentication views/public.py - All public facing views, eg non-staff (no authentication
required) views. required) views.
""" """
from datetime import datetime from datetime import datetime
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -18,16 +19,21 @@ from helpdesk.forms import PublicTicketForm
from helpdesk.lib import send_templated_mail from helpdesk.lib import send_templated_mail
from helpdesk.models import Ticket, Queue from helpdesk.models import Ticket, Queue
def homepage(request): def homepage(request):
if request.user.is_authenticated(): if request.user.is_authenticated():
return HttpResponseRedirect(reverse('helpdesk_dashboard')) return HttpResponseRedirect(reverse('helpdesk_dashboard'))
if request.method == 'POST': if request.method == 'POST':
form = PublicTicketForm(request.POST) form = PublicTicketForm(request.POST)
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)] form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
if form.is_valid(): if form.is_valid():
ticket = form.save() ticket = form.save()
return HttpResponseRedirect('%s?ticket=%s&email=%s'% (reverse('helpdesk_public_view'), ticket.ticket_for_url, ticket.submitter_email)) return HttpResponseRedirect('%s?ticket=%s&email=%s'% (
reverse('helpdesk_public_view'),
ticket.ticket_for_url,
ticket.submitter_email)
)
else: else:
form = PublicTicketForm() form = PublicTicketForm()
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)] form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
@ -37,24 +43,30 @@ def homepage(request):
'form': form, 'form': form,
})) }))
def view_ticket(request): def view_ticket(request):
ticket = request.GET.get('ticket', '') ticket = request.GET.get('ticket', '')
email = request.GET.get('email', '') email = request.GET.get('email', '')
error_message = '' error_message = ''
if ticket and email: if ticket and email:
queue, ticket_id = ticket.split('-')
try: try:
queue, ticket_id = ticket.split('-')
t = Ticket.objects.get(id=ticket_id, queue__slug__iexact=queue, submitter_email__iexact=email) t = Ticket.objects.get(id=ticket_id, queue__slug__iexact=queue, submitter_email__iexact=email)
return render_to_response('helpdesk/public_view_ticket.html',
RequestContext(request, {'ticket': t,}))
except Ticket.DoesNotExist: except Ticket.DoesNotExist:
t = False; t = False;
error_message = _('Invalid ticket ID or e-mail address. Please try again.') error_message = _('Invalid ticket ID or e-mail address. Please try again.')
return render_to_response('helpdesk/public_view_form.html', if t:
return render_to_response('helpdesk/public_view_ticket.html',
RequestContext(request, {
'ticket': t,
}))
return render_to_response('helpdesk/public_view_form.html',
RequestContext(request, { RequestContext(request, {
'ticket': ticket, 'ticket': ticket,
'email': email, 'email': email,
'error_message': error_message, 'error_message': error_message,
})) }))

View File

@ -3,15 +3,17 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. (c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
views/staff.py - The bulk of the application - provides most business logic and views/staff.py - The bulk of the application - provides most business logic and
renders all staff-facing views. renders all staff-facing views.
""" """
from datetime import datetime from datetime import datetime
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import connection
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect, Http404, HttpResponse from django.http import HttpResponseRedirect, Http404, HttpResponse
from django.shortcuts import render_to_response, get_object_or_404 from django.shortcuts import render_to_response, get_object_or_404
@ -22,17 +24,28 @@ from helpdesk.forms import TicketForm
from helpdesk.lib import send_templated_mail, line_chart, bar_chart, query_to_dict from helpdesk.lib import send_templated_mail, line_chart, bar_chart, query_to_dict
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment
def dashboard(request): def dashboard(request):
""" """
A quick summary overview for users: A list of their own tickets, a table A quick summary overview for users: A list of their own tickets, a table
showing ticket counts by queue/status, and a list of unassigned tickets showing ticket counts by queue/status, and a list of unassigned tickets
with options for them to 'Take' ownership of said tickets. with options for them to 'Take' ownership of said tickets.
""" """
tickets = Ticket.objects.filter(assigned_to=request.user).exclude(status=Ticket.CLOSED_STATUS) tickets = Ticket.objects
unassigned_tickets = Ticket.objects.filter(assigned_to__isnull=True).exclude(status=Ticket.CLOSED_STATUS) .filter(assigned_to=request.user)
.exclude(status=Ticket.CLOSED_STATUS)
unassigned_tickets = Ticket.objects
.filter(assigned_to__isnull=True)
.exclude(status=Ticket.CLOSED_STATUS)
# The following query builds a grid of queues & ticket statuses,
# to be displayed to the user. EG:
# Open Resolved
# Queue 1 10 4
# Queue 2 4 12
from django.db import connection
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute(""" cursor.execute("""
SELECT q.id as queue, SELECT q.id as queue,
@ -55,9 +68,10 @@ def dashboard(request):
})) }))
dashboard = login_required(dashboard) dashboard = login_required(dashboard)
def delete_ticket(request, ticket_id): def delete_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if request.method == 'GET': if request.method == 'GET':
return render_to_response('helpdesk/delete_ticket.html', return render_to_response('helpdesk/delete_ticket.html',
RequestContext(request, { RequestContext(request, {
@ -68,18 +82,31 @@ def delete_ticket(request, ticket_id):
return HttpResponseRedirect(reverse('helpdesk_home')) return HttpResponseRedirect(reverse('helpdesk_home'))
delete_ticket = login_required(delete_ticket) delete_ticket = login_required(delete_ticket)
def view_ticket(request, ticket_id): def view_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if request.GET.has_key('take'): if request.GET.has_key('take'):
# Allow the user to assign the ticket to themselves whilst viewing it.
ticket.assigned_to = request.user ticket.assigned_to = request.user
ticket.save() ticket.save()
if request.GET.has_key('close') and ticket.status == Ticket.RESOLVED_STATUS: if request.GET.has_key('close') and ticket.status == Ticket.RESOLVED_STATUS:
if not ticket.assigned_to: if not ticket.assigned_to:
owner = 0 owner = 0
else: else:
owner = ticket.assigned_to.id owner = ticket.assigned_to.id
request.POST = {'new_status': Ticket.CLOSED_STATUS, 'public': 1, 'owner': owner, 'title': ticket.title, 'comment': _('Accepted resolution and closed ticket')}
# Trick the update_ticket() view into thinking it's being called with
# a valid POST.
request.POST = {
'new_status': Ticket.CLOSED_STATUS,
'public': 1,
'owner': owner,
'title': ticket.title,
'comment': _('Accepted resolution and closed ticket'),
}
return update_ticket(request, ticket_id) return update_ticket(request, ticket_id)
return render_to_response('helpdesk/ticket.html', return render_to_response('helpdesk/ticket.html',
@ -91,6 +118,7 @@ def view_ticket(request, ticket_id):
})) }))
view_ticket = login_required(view_ticket) view_ticket = login_required(view_ticket)
def update_ticket(request, ticket_id): def update_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
@ -100,26 +128,27 @@ def update_ticket(request, ticket_id):
public = request.POST.get('public', False) public = request.POST.get('public', False)
owner = int(request.POST.get('owner', None)) owner = int(request.POST.get('owner', None))
priority = int(request.POST.get('priority', ticket.priority)) priority = int(request.POST.get('priority', ticket.priority))
if not owner and ticket.assigned_to: if not owner and ticket.assigned_to:
owner = ticket.assigned_to.id owner = ticket.assigned_to.id
f = FollowUp(ticket=ticket, date=datetime.now(), comment=comment, user=request.user) f = FollowUp(ticket=ticket, date=datetime.now(), comment=comment, user=request.user)
if public: f.public = public
f.public = True
reassigned = False reassigned = False
if owner: if owner:
if owner != 0 and (ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to: if owner != 0 and (ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to:
new_user = User.objects.get(id=owner) new_user = User.objects.get(id=owner)
f.title = _('Assigned to %(username)s') % {'username': new_user.username} f.title = _('Assigned to %(username)s') % {
'username': new_user.username,
}
ticket.assigned_to = new_user ticket.assigned_to = new_user
reassigned = True reassigned = True
else: else:
f.title = _('Unassigned') f.title = _('Unassigned')
ticket.assigned_to = None ticket.assigned_to = None
if new_status != ticket.status: if new_status != ticket.status:
ticket.status = new_status ticket.status = new_status
ticket.save() ticket.save()
@ -136,20 +165,30 @@ def update_ticket(request, ticket_id):
f.title = _('Updated') f.title = _('Updated')
f.save() f.save()
if title != ticket.title: if title != ticket.title:
c = TicketChange(followup=f, field=_('Title'), old_value=ticket.title, new_value=title) c = TicketChange(
followup=f,
field=_('Title'),
old_value=ticket.title,
new_value=title,
)
c.save() c.save()
ticket.title = title ticket.title = title
if priority != ticket.priority: if priority != ticket.priority:
c = TicketChange(followup=f, field=_('Priority'), old_value=ticket.priority, new_value=priority) c = TicketChange(
followup=f,
field=_('Priority'),
old_value=ticket.priority,
new_value=priority,
)
c.save() c.save()
ticket.priority = priority ticket.priority = priority
if f.new_status == Ticket.RESOLVED_STATUS: if f.new_status == Ticket.RESOLVED_STATUS:
ticket.resolution = comment ticket.resolution = comment
if ticket.submitter_email and ((f.comment != '' and public) or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))): if ticket.submitter_email and ((f.comment != '' and public) or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))):
context = { context = {
'ticket': ticket, 'ticket': ticket,
@ -157,16 +196,24 @@ def update_ticket(request, ticket_id):
'resolution': ticket.resolution, 'resolution': ticket.resolution,
'comment': f.comment, 'comment': f.comment,
} }
if f.new_status == Ticket.RESOLVED_STATUS: if f.new_status == Ticket.RESOLVED_STATUS:
template = 'resolved_submitter' template = 'resolved_submitter'
elif f.new_status == Ticket.CLOSED_STATUS: elif f.new_status == Ticket.CLOSED_STATUS:
template = 'closed_submitter' template = 'closed_submitter'
else: else:
template = 'updated_submitter' template = 'updated_submitter'
send_templated_mail(template, context, recipients=ticket.submitter_email, sender=ticket.queue.from_address, fail_silently=True)
send_templated_mail(
template,
context,
recipients=ticket.submitter_email,
sender=ticket.queue.from_address,
fail_silently=True,
)
if ticket.assigned_to and request.user != ticket.assigned_to and ticket.assigned_to.email: if ticket.assigned_to and request.user != ticket.assigned_to and ticket.assigned_to.email:
# We only send e-mails to staff members if the ticket is updated by # We only send e-mails to staff members if the ticket is updated by
# another user. # another user.
if reassigned: if reassigned:
template_staff = 'assigned_owner' template_staff = 'assigned_owner'
@ -176,9 +223,15 @@ def update_ticket(request, ticket_id):
template_staff = 'closed_owner' template_staff = 'closed_owner'
else: else:
template_staff = 'updated_owner' template_staff = 'updated_owner'
send_templated_mail(template_staff, context, recipients=ticket.assigned_to.email, sender=ticket.queue.from_address, fail_silently=True) send_templated_mail(
template_staff,
context,
recipients=ticket.assigned_to.email,
sender=ticket.queue.from_address,
fail_silently=True,
)
if ticket.queue.updated_ticket_cc: if ticket.queue.updated_ticket_cc:
if reassigned: if reassigned:
template_cc = 'assigned_cc' template_cc = 'assigned_cc'
@ -188,22 +241,33 @@ def update_ticket(request, ticket_id):
template_cc = 'closed_cc' template_cc = 'closed_cc'
else: else:
template_cc = 'updated_cc' template_cc = 'updated_cc'
send_templated_mail(template_cc, context, recipients=ticket.queue.updated_ticket_cc, sender=ticket.queue.from_address, fail_silently=True) send_templated_mail(
template_cc,
context,
recipients=ticket.queue.updated_ticket_cc,
sender=ticket.queue.from_address,
fail_silently=True,
)
if request.FILES: if request.FILES:
for file in request.FILES.getlist('attachment'): for file in request.FILES.getlist('attachment'):
filename = file['filename'].replace(' ', '_') filename = file['filename'].replace(' ', '_')
a = Attachment(followup=f, filename=filename, mime_type=file['content-type'], size=len(file['content'])) a = Attachment(
#a.save_file_file(file['filename'], file['content']) followup=f,
filename=filename,
mime_type=file['content-type'],
size=len(file['content']),
)
a.file.save(file['filename'], ContentFile(file['content'])) a.file.save(file['filename'], ContentFile(file['content']))
a.save() a.save()
ticket.save() ticket.save()
return HttpResponseRedirect(ticket.get_absolute_url()) return HttpResponseRedirect(ticket.get_absolute_url())
update_ticket = login_required(update_ticket) update_ticket = login_required(update_ticket)
def ticket_list(request): def ticket_list(request):
tickets = Ticket.objects.select_related() tickets = Ticket.objects.select_related()
context = {} context = {}
@ -229,7 +293,7 @@ def ticket_list(request):
### KEYWORD SEARCHING ### KEYWORD SEARCHING
q = request.GET.get('q', None) q = request.GET.get('q', None)
if q: if q:
qset = ( qset = (
Q(title__icontains=q) | Q(title__icontains=q) |
@ -239,7 +303,7 @@ def ticket_list(request):
) )
tickets = tickets.filter(qset) tickets = tickets.filter(qset)
context = dict(context, query=q) context = dict(context, query=q)
### SORTING ### SORTING
sort = request.GET.get('sort', None) sort = request.GET.get('sort', None)
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority'): if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority'):
@ -257,6 +321,7 @@ def ticket_list(request):
))) )))
ticket_list = login_required(ticket_list) ticket_list = login_required(ticket_list)
def create_ticket(request): def create_ticket(request):
if request.method == 'POST': if request.method == 'POST':
form = TicketForm(request.POST) form = TicketForm(request.POST)
@ -270,26 +335,32 @@ def create_ticket(request):
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()] form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()]
form.fields['assigned_to'].choices = [('', '--------')] + [[u.id, u.username] for u in User.objects.filter(is_active=True)] form.fields['assigned_to'].choices = [('', '--------')] + [[u.id, u.username] for u in User.objects.filter(is_active=True)]
return render_to_response('helpdesk/create_ticket.html', return render_to_response('helpdesk/create_ticket.html',
RequestContext(request, { RequestContext(request, {
'form': form, 'form': form,
})) }))
create_ticket = login_required(create_ticket) create_ticket = login_required(create_ticket)
def raw_details(request, type): def raw_details(request, type):
# TODO: This currently only supports spewing out 'PreSetReply' objects,
# in the future it needs to be expanded to include other items. All it
# does is return a plain-text representation of an object.
if not type in ('preset',): if not type in ('preset',):
raise Http404 raise Http404
if type == 'preset' and request.GET.get('id', False): if type == 'preset' and request.GET.get('id', False):
try: try:
preset = PreSetReply.objects.get(id=request.GET.get('id')) preset = PreSetReply.objects.get(id=request.GET.get('id'))
return HttpResponse(preset.body) return HttpResponse(preset.body)
except PreSetReply.DoesNotExist: except PreSetReply.DoesNotExist:
raise Http404 raise Http404
raise Http404 raise Http404
raw_details = login_required(raw_details) raw_details = login_required(raw_details)
def hold_ticket(request, ticket_id, unhold=False): def hold_ticket(request, ticket_id, unhold=False):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
@ -299,7 +370,7 @@ def hold_ticket(request, ticket_id, unhold=False):
else: else:
ticket.on_hold = True ticket.on_hold = True
title = _('Ticket placed on hold') title = _('Ticket placed on hold')
f = FollowUp( f = FollowUp(
ticket = ticket, ticket = ticket,
user = request.user, user = request.user,
@ -308,28 +379,32 @@ def hold_ticket(request, ticket_id, unhold=False):
public = True, public = True,
) )
f.save() f.save()
ticket.save() ticket.save()
return HttpResponseRedirect(ticket.get_absolute_url()) return HttpResponseRedirect(ticket.get_absolute_url())
hold_ticket = login_required(hold_ticket) hold_ticket = login_required(hold_ticket)
def unhold_ticket(request, ticket_id): def unhold_ticket(request, ticket_id):
return hold_ticket(request, ticket_id, unhold=True) return hold_ticket(request, ticket_id, unhold=True)
unhold_ticket = login_required(unhold_ticket) unhold_ticket = login_required(unhold_ticket)
def rss_list(request): def rss_list(request):
return render_to_response('helpdesk/rss_list.html', return render_to_response('helpdesk/rss_list.html',
RequestContext(request, { RequestContext(request, {
'queues': Queue.objects.all(), 'queues': Queue.objects.all(),
})) }))
rss_list = login_required(rss_list) rss_list = login_required(rss_list)
def report_index(request): def report_index(request):
return render_to_response('helpdesk/report_index.html', return render_to_response('helpdesk/report_index.html',
RequestContext(request, {})) RequestContext(request, {}))
report_index = login_required(report_index) report_index = login_required(report_index)
def run_report(request, report): def run_report(request, report):
priority_sql = [] priority_sql = []
priority_columns = [] priority_columns = []
@ -337,14 +412,14 @@ def run_report(request, report):
priority_sql.append("COUNT(CASE t.priority WHEN '%s' THEN t.id END) AS \"%s\"" % (p[0], p[1]._proxy____unicode_cast())) priority_sql.append("COUNT(CASE t.priority WHEN '%s' THEN t.id END) AS \"%s\"" % (p[0], p[1]._proxy____unicode_cast()))
priority_columns.append("%s" % p[1]._proxy____unicode_cast()) priority_columns.append("%s" % p[1]._proxy____unicode_cast())
priority_sql = ", ".join(priority_sql) priority_sql = ", ".join(priority_sql)
status_sql = [] status_sql = []
status_columns = [] status_columns = []
for s in Ticket.STATUS_CHOICES: for s in Ticket.STATUS_CHOICES:
status_sql.append("COUNT(CASE t.status WHEN '%s' THEN t.id END) AS \"%s\"" % (s[0], s[1]._proxy____unicode_cast())) status_sql.append("COUNT(CASE t.status WHEN '%s' THEN t.id END) AS \"%s\"" % (s[0], s[1]._proxy____unicode_cast()))
status_columns.append("%s" % s[1]._proxy____unicode_cast()) status_columns.append("%s" % s[1]._proxy____unicode_cast())
status_sql = ", ".join(status_sql) status_sql = ", ".join(status_sql)
queue_sql = [] queue_sql = []
queue_columns = [] queue_columns = []
for q in Queue.objects.all(): for q in Queue.objects.all():
@ -368,11 +443,11 @@ def run_report(request, report):
'Dec', 'Dec',
) )
month_columns = [] month_columns = []
first_ticket = Ticket.objects.all().order_by('created')[0] first_ticket = Ticket.objects.all().order_by('created')[0]
first_month = first_ticket.created.month first_month = first_ticket.created.month
first_year = first_ticket.created.year first_year = first_ticket.created.year
last_ticket = Ticket.objects.all().order_by('-created')[0] last_ticket = Ticket.objects.all().order_by('-created')[0]
last_month = last_ticket.created.month last_month = last_ticket.created.month
last_year = last_ticket.created.year last_year = last_ticket.created.year
@ -420,33 +495,32 @@ def run_report(request, report):
if report == 'userpriority': if report == 'userpriority':
sql = user_base_sql % priority_sql sql = user_base_sql % priority_sql
columns = ['username'] + priority_columns columns = ['username'] + priority_columns
elif report == 'userqueue': elif report == 'userqueue':
sql = user_base_sql % queue_sql sql = user_base_sql % queue_sql
columns = ['username'] + queue_columns columns = ['username'] + queue_columns
elif report == 'userstatus': elif report == 'userstatus':
sql = user_base_sql % status_sql sql = user_base_sql % status_sql
columns = ['username'] + status_columns columns = ['username'] + status_columns
elif report == 'usermonth': elif report == 'usermonth':
sql = user_base_sql % month_sql sql = user_base_sql % month_sql
columns = ['username'] + month_columns columns = ['username'] + month_columns
elif report == 'queuepriority': elif report == 'queuepriority':
sql = queue_base_sql % priority_sql sql = queue_base_sql % priority_sql
columns = ['queue'] + priority_columns columns = ['queue'] + priority_columns
elif report == 'queuestatus': elif report == 'queuestatus':
sql = queue_base_sql % status_sql sql = queue_base_sql % status_sql
columns = ['queue'] + status_columns columns = ['queue'] + status_columns
elif report == 'queuemonth': elif report == 'queuemonth':
sql = queue_base_sql % month_sql sql = queue_base_sql % month_sql
columns = ['queue'] + month_columns columns = ['queue'] + month_columns
from django.db import connection
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute(sql) cursor.execute(sql)
report_output = query_to_dict(cursor.fetchall(), cursor.description) report_output = query_to_dict(cursor.fetchall(), cursor.description)
@ -465,7 +539,7 @@ def run_report(request, report):
chart_url = bar_chart([columns] + data) chart_url = bar_chart([columns] + data)
else: else:
chart_url = '' chart_url = ''
return render_to_response('helpdesk/report_output.html', return render_to_response('helpdesk/report_output.html',
RequestContext(request, { RequestContext(request, {
'headings': columns, 'headings': columns,
@ -474,3 +548,4 @@ def run_report(request, report):
'chart': chart_url, 'chart': chart_url,
})) }))
run_report = login_required(run_report) run_report = login_required(run_report)