* 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.
(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.
"""
from django import forms
from helpdesk.models import Ticket, Queue, FollowUp
from django.contrib.auth.models import User
from datetime import datetime
from django import forms
from django.contrib.auth.models import User
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):
queue = forms.ChoiceField(label=_('Queue'), required=True, choices=())
queue = forms.ChoiceField(
label=_('Queue'),
required=True,
choices=()
)
title = forms.CharField(max_length=100, required=True,
widget=forms.TextInput(),
label=_('Summary of the problem'))
submitter_email = forms.EmailField(required=False,
label=_('Submitter E-Mail Address'),
help_text=_('This e-mail address will receive copies of all public updates to this ticket.'))
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.'))
title = forms.CharField(
max_length=100,
required=True,
widget=forms.TextInput(),
label=_('Summary of the problem'),
)
submitter_email = forms.EmailField(
required=False,
label=_('Submitter E-Mail Address'),
help_text=_('This e-mail address will receive copies of all public '
'updates to this ticket.'),
)
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):
"""
Writes and returns a Ticket() object
"""
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'],
created = datetime.now(),
status = Ticket.OPEN_STATUS,
@ -52,7 +75,7 @@ class TicketForm(forms.Form):
description = self.cleaned_data['body'],
priority = self.cleaned_data['priority'],
)
if self.cleaned_data['assigned_to']:
try:
u = User.objects.get(id=self.cleaned_data['assigned_to'])
@ -69,91 +92,151 @@ class TicketForm(forms.Form):
user = user,
)
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()
context = {
'ticket': t,
'queue': q,
}
from helpdesk.lib import send_templated_mail
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:
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:
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:
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
class PublicTicketForm(forms.Form):
queue = forms.ChoiceField(label=_('Queue'), required=True, choices=())
title = forms.CharField(max_length=100, required=True,
widget=forms.TextInput(),
label=_('Summary of your query'))
submitter_email = forms.EmailField(required=True,
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.'))
class PublicTicketForm(forms.Form):
queue = forms.ChoiceField(
label=_('Queue'),
required=True,
choices=()
)
title = forms.CharField(
max_length=100,
required=True,
widget=forms.TextInput(),
label=_('Summary of your query'),
)
submitter_email = forms.EmailField(
required=True,
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):
"""
Writes and returns a Ticket() object
"""
q = Queue.objects.get(id=int(self.cleaned_data['queue']))
t = Ticket( title = self.cleaned_data['title'],
submitter_email = self.cleaned_data['submitter_email'],
created = datetime.now(),
status = Ticket.OPEN_STATUS,
queue = q,
description = self.cleaned_data['body'],
priority = self.cleaned_data['priority'],
)
t = Ticket(
title = self.cleaned_data['title'],
submitter_email = self.cleaned_data['submitter_email'],
created = datetime.now(),
status = Ticket.OPEN_STATUS,
queue = q,
description = self.cleaned_data['body'],
priority = self.cleaned_data['priority'],
)
t.save()
f = FollowUp( ticket = t,
title = _('Ticket Opened Via Web'),
date = datetime.now(),
public = True,
comment = self.cleaned_data['body'],
)
f = FollowUp(
ticket = t,
title = _('Ticket Opened Via Web'),
date = datetime.now(),
public = True,
comment = self.cleaned_data['body'],
)
f.save()
context = {
'ticket': t,
'queue': q,
}
from helpdesk.lib import send_templated_mail
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 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:
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

98
lib.py
View File

@ -1,15 +1,46 @@
""" ..
"""
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
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):
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.template import loader, Context
from django.conf import settings
from helpdesk.models import EmailTemplate
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
context = Context(email_context)
text_part = loader.get_template_from_string("%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text).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)
text_part = loader.get_template_from_string(
"%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text
).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:
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")
if files:
@ -34,18 +77,18 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
for file in files:
msg.attach_file(file)
return msg.send(fail_silently)
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
Text parts.
This function will send a multi-part e-mail with both HTML and
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
(.txt) versions must exist, eg 'emails/public_submit' will use both
template_name must NOT contain an extension. Both HTML (.html) and TEXT
(.txt) versions must exist, eg 'emails/public_submit' will use both
public_submit.html and public_submit.txt.
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:
['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.
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
context = Context(email_context)
text_part = loader.get_template('%s.txt' % 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)
@ -87,13 +130,16 @@ def send_multipart_mail(template_name, email_context, subject, recipients, sende
for file in files:
msg.attach_file(file)
return msg.send(fail_silently)
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)
if max_value > to:
@ -103,7 +149,6 @@ def normalise_data(data, to=100):
data = new_data
return data
chart_colours = ('80C65A', '990066', 'FF9900', '3399CC', 'BBCCED', '3399CC', 'FFCC33')
def line_chart(data):
"""
@ -138,7 +183,7 @@ def line_chart(data):
first = True
for row in range(rows):
# 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 += '0,%s' % max
first = False
@ -149,6 +194,7 @@ def line_chart(data):
return chart_url
def bar_chart(data):
"""
'data' is a list of lists making a table.
@ -185,12 +231,16 @@ def bar_chart(data):
return chart_url
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.
Converts the results of a raw SQL query into a list of dictionaries, suitable
for use in templates etc. """
Converts the results of a raw SQL query into a list of dictionaries, suitable
for use in templates etc.
"""
output = []
for data in results:
row = {}

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\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/rss_list.html:23 templates/helpdesk/ticket_list.html:14
#: templates/helpdesk/ticket_list.html:25
@ -24,520 +24,564 @@ msgstr ""
msgid "Queue"
msgstr ""
#: forms.py:21
#: forms.py:30
msgid "Summary of the problem"
msgstr ""
#: forms.py:24
#: forms.py:35
msgid "Submitter E-Mail Address"
msgstr ""
#: forms.py:25
#: forms.py:36
msgid ""
"This e-mail address will receive copies of all public updates to this ticket."
msgstr ""
#: forms.py:28
#: forms.py:42
msgid "Description of Issue"
msgstr ""
#: forms.py:31
#: forms.py:49
msgid "Case owner"
msgstr ""
#: forms.py:32
#: forms.py:50
msgid ""
"If you select an owner other than yourself, they'll be e-mailed details of "
"this ticket immediately."
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/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"
msgstr ""
#: forms.py:38
#: forms.py:59
msgid "Please select a priority carefully. If unsure, leave it as '3'."
msgstr ""
#: forms.py:65
#: forms.py:88
msgid "Ticket Opened"
msgstr ""
#: forms.py:72
#: forms.py:95
#, python-format
msgid "Ticket Opened & Assigned to %(name)s"
msgstr ""
#: forms.py:102
#: forms.py:156
msgid "Summary of your query"
msgstr ""
#: forms.py:105
#: forms.py:161
msgid "Your E-Mail Address"
msgstr ""
#: forms.py:106
#: forms.py:162
msgid "We will e-mail you when your ticket is updated."
msgstr ""
#: forms.py:109
#: forms.py:167
msgid "Description of your issue"
msgstr ""
#: forms.py:110
#: forms.py:169
msgid ""
"Please be as descriptive as possible, including any details we may need to "
"address your query."
msgstr ""
#: forms.py:115
#: forms.py:177
msgid "Urgency"
msgstr ""
#: forms.py:116
#: forms.py:178
msgid "Please select a priority carefully."
msgstr ""
#: forms.py:137
#: forms.py:202
msgid "Ticket Opened Via Web"
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/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"
msgstr ""
#: models.py:29 models.py:361
#: models.py:34 models.py:733
msgid "Slug"
msgstr ""
#: models.py:29
#: models.py:35
msgid ""
"This slug is used when building ticket ID's. Once set, try not to change it "
"or e-mailing may get messy."
msgstr ""
#: models.py:30
#: models.py:40
msgid "E-Mail Address"
msgstr ""
#: models.py:30
#: models.py:43
msgid ""
"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."
msgstr ""
#: models.py:31
#: models.py:49
msgid "Allow Public Submission?"
msgstr ""
#: models.py:31
#: models.py:52
msgid "Should this queue be listed on the public submission form?"
msgstr ""
#: models.py:32
#: models.py:57
msgid "Allow E-Mail Submission?"
msgstr ""
#: models.py:32
#: models.py:60
msgid "Do you want to poll the e-mail box below for new tickets?"
msgstr ""
#: models.py:33
#: models.py:65
msgid "Escalation Days"
msgstr ""
#: models.py:33
#: models.py:68
msgid ""
"For tickets which are not held, how often do you wish to increase their "
"priority? Set to 0 for no escalation."
msgstr ""
#: models.py:42
#: models.py:73
msgid "New Ticket CC Address"
msgstr ""
#: models.py:42
#: models.py:76
msgid ""
"If an e-mail address is entered here, then it will receive notification of "
"all new tickets created for this queue"
msgstr ""
#: models.py:43
#: models.py:81
msgid "Updated Ticket CC Address"
msgstr ""
#: models.py:43
#: models.py:84
msgid ""
"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"
msgstr ""
#: models.py:45
#: models.py:90
msgid "E-Mail Box Type"
msgstr ""
#: models.py:45
#: models.py:92
msgid "POP 3"
msgstr ""
#: models.py:45
#: models.py:92
msgid "IMAP"
msgstr ""
#: models.py:45
#: models.py:95
msgid ""
"E-Mail server type for creating tickets automatically from a mailbox - both "
"POP3 and IMAP are supported."
msgstr ""
#: models.py:46
#: models.py:100
msgid "E-Mail Hostname"
msgstr ""
#: models.py:46
#: models.py:104
msgid ""
"Your e-mail server address - either the domain name or IP address. May be "
"\"localhost\"."
msgstr ""
#: models.py:47
#: models.py:109
msgid "E-Mail Port"
msgstr ""
#: models.py:47
#: models.py:112
msgid ""
"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."
msgstr ""
#: models.py:48
#: models.py:118
msgid "E-Mail Username"
msgstr ""
#: models.py:48
#: models.py:122
msgid "Username for accessing this mailbox."
msgstr ""
#: models.py:49
#: models.py:126
msgid "E-Mail Password"
msgstr ""
#: models.py:49
#: models.py:130
msgid "Password for the above username"
msgstr ""
#: models.py:50
#: models.py:134
msgid "IMAP Folder"
msgstr ""
#: models.py:50
#: models.py:138
msgid ""
"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."
msgstr ""
#: models.py:51
#: models.py:145
msgid "E-Mail Check Interval"
msgstr ""
#: models.py:51
#: models.py:146
msgid "How often do you wish to check this mailbox? (in Minutes)"
msgstr ""
#: models.py:89 templates/helpdesk/dashboard.html:10
#: models.py:212 templates/helpdesk/dashboard.html:10
#: templates/helpdesk/ticket.html:123
msgid "Open"
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
msgid "Reopened"
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:134
msgid "Resolved"
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:139
msgid "Closed"
msgstr ""
#: models.py:96
#: models.py:219
msgid "1. Critical"
msgstr ""
#: models.py:97
#: models.py:220
msgid "2. High"
msgstr ""
#: models.py:98
#: models.py:221
msgid "3. Normal"
msgstr ""
#: models.py:99
#: models.py:222
msgid "4. Low"
msgstr ""
#: models.py:100
#: models.py:223
msgid "5. Very Low"
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:59
msgid "Created"
msgstr ""
#: models.py:106
#: models.py:236
msgid "Date this ticket was first created"
msgstr ""
#: models.py:240
msgid "Modified"
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
msgid "Submitter E-Mail"
msgstr ""
#: models.py:107
#: models.py:249
msgid ""
"The submitter will receive an email for all public follow-ups left for this "
"task."
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:26
#: templates/helpdesk/ticket_list.html:59
msgid "Status"
msgstr ""
#: models.py:111
#: models.py:267
msgid "On Hold"
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
msgid "Description"
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
msgid "Resolution"
msgstr ""
#: models.py:125 templates/helpdesk/ticket.html:58 views/feeds.py:55
#: views/feeds.py:77 views/feeds.py:120 views/staff.py:120
#: models.py:285
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"
msgstr ""
#: models.py:156
#: models.py:348
msgid " - On Hold"
msgstr ""
#: models.py:215 models.py:330
#: models.py:430 models.py:662
msgid "Date"
msgstr ""
#: models.py:217 views/staff.py:134
#: models.py:441 views/staff.py:163
msgid "Comment"
msgstr ""
#: models.py:218
#: models.py:447
msgid "Public"
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"
msgstr ""
#: models.py:246
#: models.py:465
msgid "If the status was changed, what was it changed to?"
msgstr ""
#: models.py:496
msgid "Field"
msgstr ""
#: models.py:247
#: models.py:501
msgid "Old Value"
msgstr ""
#: models.py:248
#: models.py:507
msgid "New Value"
msgstr ""
#: models.py:253
#: models.py:515
msgid "removed"
msgstr ""
#: models.py:255
#: models.py:517
#, python-format
msgid "set to %s"
msgstr ""
#: models.py:257
#: models.py:519
#, python-format
msgid "changed from \"%(old_value)s\" to \"%(new_value)s\""
msgstr ""
#: models.py:289
#: models.py:562
msgid "File"
msgstr ""
#: models.py:290
#: models.py:567
msgid "Filename"
msgstr ""
#: models.py:291
#: models.py:572
msgid "MIME Type"
msgstr ""
#: models.py:292
#: models.py:577
msgid "Size"
msgstr ""
#: models.py:292
#: models.py:578
msgid "Size of this file in bytes"
msgstr ""
#: models.py:315
#: models.py:611
msgid ""
"Leave blank to allow this reply to be used for all queues, or select those "
"queues you wish to limit this reply to."
msgstr ""
#: models.py:316 models.py:328
#: models.py:616 models.py:657
msgid "Name"
msgstr ""
#: models.py:316
#: models.py:618
msgid ""
"Only used to assist users with selecting a reply - not shown to the user."
msgstr ""
#: models.py:317
#: models.py:623
msgid "Body"
msgstr ""
#: models.py:317
#: models.py:624
msgid ""
"Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); "
"{{ queue }} - The queue; and {{ user }} - the current user."
msgstr ""
#: models.py:326
#: models.py:651
msgid ""
"Leave blank for this exclusion to be applied to all queues, or select those "
"queues you wish to exclude with this entry."
msgstr ""
#: models.py:330
#: models.py:663
msgid "Date on which escalation should not happen"
msgstr ""
#: models.py:341
#: models.py:680
msgid "Template Name"
msgstr ""
#: models.py:343
#: models.py:686
msgid "Subject"
msgstr ""
#: models.py:343
#: models.py:688
msgid ""
"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."
msgstr ""
#: models.py:344
#: models.py:694
msgid "Heading"
msgstr ""
#: models.py:344
#: models.py:696
msgid ""
"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."
msgstr ""
#: models.py:345
#: models.py:702
msgid "Plain Text"
msgstr ""
#: models.py:345
#: models.py:703
msgid ""
"The context available to you includes {{ ticket }}, {{ queue }}, and "
"depending on the time of the call: {{ resolution }} or {{ comment }}."
msgstr ""
#: models.py:346
#: models.py:709
msgid "HTML"
msgstr ""
#: models.py:346
#: models.py:710
msgid "The same context is available here as in plain_text, above."
msgstr ""
#: models.py:381
#: models.py:764
msgid "Question"
msgstr ""
#: models.py:382
#: models.py:768
msgid "Answer"
msgstr ""
#: models.py:384
#: models.py:772
msgid "Votes"
msgstr ""
#: models.py:384
#: models.py:773
msgid "Total number of votes cast for this item"
msgstr ""
#: models.py:385
#: models.py:777
msgid "Positive Votes"
msgstr ""
#: models.py:385
#: models.py:778
msgid "Number of votes for this item which were POSITIVE."
msgstr ""
#: models.py:387
#: models.py:782
msgid "Last Updated"
msgstr ""
#: models.py:397
#: models.py:783
msgid "The date on which this question was most recently changed."
msgstr ""
#: models.py:795
msgid "Unrated"
msgstr ""
#: scripts/escalate_tickets.py:70
#: management/commands/escalate_tickets.py:146
#, python-format
msgid "Ticket escalated after %s days"
msgstr ""
#: scripts/get_email.py:74
#: management/commands/get_email.py:97
msgid "Created from e-mail"
msgstr ""
#: scripts/get_email.py:77
#: management/commands/get_email.py:100
msgid "Unknown Sender"
msgstr ""
#: scripts/get_email.py:156
#: management/commands/get_email.py:206
msgid " (Updated)"
msgstr ""
#: scripts/get_email.py:166
#: management/commands/get_email.py:228
#, python-format
msgid "E-Mail Received from %s"
msgid "E-Mail Received from %(sender_email)s"
msgstr ""
#: templates/helpdesk/base.html:4
@ -806,7 +850,7 @@ msgstr ""
#: templates/helpdesk/public_view_ticket.html:16
#, python-format
msgid "Queue: %(ticket.queue)s"
msgid "Queue: %(queue_name)s"
msgstr ""
#: templates/helpdesk/public_view_ticket.html:19
@ -1087,75 +1131,75 @@ msgstr ""
msgid "Password"
msgstr ""
#: views/feeds.py:26
#: views/feeds.py:35
#, python-format
msgid "Helpdesk: Open Tickets in queue %(queue)s for %(username)s"
msgstr ""
#: views/feeds.py:28
#: views/feeds.py:40
#, python-format
msgid "Helpdesk: Open Tickets for %(username)s"
msgstr ""
#: views/feeds.py:32
#: views/feeds.py:46
#, python-format
msgid "Open and Reopened Tickets in queue %(queue)s for %(username)s"
msgstr ""
#: views/feeds.py:34
#: views/feeds.py:51
#, python-format
msgid "Open and Reopened Tickets for %(username)s"
msgstr ""
#: views/feeds.py:62
#: views/feeds.py:98
msgid "Helpdesk: Unassigned Tickets"
msgstr ""
#: views/feeds.py:63
#: views/feeds.py:99
msgid "Unassigned Open and Reopened tickets"
msgstr ""
#: views/feeds.py:84
#: views/feeds.py:124
msgid "Helpdesk: Recent Followups"
msgstr ""
#: views/feeds.py:85
#: views/feeds.py:125
msgid ""
"Recent FollowUps, such as e-mail replies, comments, attachments and "
"resolutions"
msgstr ""
#: views/feeds.py:102
#: views/feeds.py:142
#, python-format
msgid "Helpdesk: Open Tickets in queue %(queue)s"
msgstr ""
#: views/feeds.py:105
#: views/feeds.py:147
#, python-format
msgid "Open and Reopened Tickets in queue %(queue)s"
msgstr ""
#: views/public.py:53
#: views/public.py:58
msgid "Invalid ticket ID or e-mail address. Please try again."
msgstr ""
#: views/staff.py:82
#: views/staff.py:107
msgid "Accepted resolution and closed ticket"
msgstr ""
#: views/staff.py:116
#: views/staff.py:143
#, python-format
msgid "Assigned to %(username)s"
msgstr ""
#: views/staff.py:136
#: views/staff.py:165
msgid "Updated"
msgstr ""
#: views/staff.py:298
#: views/staff.py:369
msgid "Ticket taken off hold"
msgstr ""
#: views/staff.py:301
#: views/staff.py:372
msgid "Ticket placed on hold"
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
escalation should take place.
"""
from datetime import datetime, timedelta, date
from django.db.models import Q
from helpdesk.models import EscalationExclusion, Queue
import sys, getopt
import getopt
from optparse import make_option
import sys
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):
def __init__(self):
@ -22,15 +26,15 @@ class Command(BaseCommand):
self.option_list += (
make_option(
'--days', '-d',
'--days', '-d',
help='Days of week (monday, tuesday, etc)'),
make_option(
'--occurrences', '-o',
'--occurrences', '-o',
type='int',
default=1,
help='Occurrences: How many weeks ahead to exclude this day'),
make_option(
'--queues', '-q',
'--queues', '-q',
help='Queues to include (default: all). Use queue slugs'),
make_option(
'--verbose', '-v',
@ -53,7 +57,7 @@ class Command(BaseCommand):
if not occurrences: occurrences = 1
if not (days and occurrences):
raise CommandError('One or more occurrences must be specified.')
if queue_slugs is not None:
queue_set = queue_slugs.split(',')
for queue in queue_set:
@ -65,6 +69,7 @@ class Command(BaseCommand):
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
day_names = {
'monday': 0,
'tuesday': 1,
@ -75,6 +80,7 @@ day_names = {
'sunday': 6,
}
def create_exclusions(days, occurrences, verbose, queues):
days = days.split(',')
for day in days:
@ -87,10 +93,10 @@ def create_exclusions(days, occurrences, verbose, queues):
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate)
esc.save()
if verbose:
print "Created exclusion for %s %s" % (day_name, workdate)
for q in queues:
esc.queues.add(q)
if verbose:
@ -107,13 +113,15 @@ def usage():
print " --queues, -q: Queues to include (default: all). Use queue slugs"
print " --verbose, -v: Display a list of dates excluded"
if __name__ == '__main__':
# This script can be run from the command-line or via Django's manage.py.
try:
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
except getopt.GetoptError:
usage()
sys.exit(2)
days = None
occurrences = None
verbose = False
@ -134,7 +142,7 @@ if __name__ == '__main__':
if not (days and occurrences):
usage()
sys.exit(2)
if queue_slugs is not None:
queue_set = queue_slugs.split(',')
for queue in queue_set:

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.
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.
"""
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.utils.translation import ugettext as _
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
from helpdesk.lib import send_templated_mail
from django.core.management.base import BaseCommand
from optparse import make_option
class Command(BaseCommand):
def __init__(self):
@ -25,7 +27,7 @@ class Command(BaseCommand):
self.option_list += (
make_option(
'--queues', '-q',
'--queues', '-q',
help='Queues to include (default: all). Use queue slugs'),
make_option(
'--verbose', '-v',
@ -33,17 +35,17 @@ class Command(BaseCommand):
default=False,
help='Display a list of dates excluded'),
)
def handle(self, *args, **options):
verbose = False
queue_slugs = None
queues = []
if options['verbose']:
verbose = True
if options['queues']:
queue_slugs = options['queues']
if queue_slugs is not None:
queue_set = queue_slugs.split(',')
for queue in queue_set:
@ -55,12 +57,13 @@ class Command(BaseCommand):
escalate_tickets(queues=queues, verbose=verbose)
def escalate_tickets(queues, verbose):
""" Only include queues with escalation configured """
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
if queues:
queryset = queryset.filter(slug__in=queues)
for q in queryset:
last = date.today() - timedelta(days=q.escalate_days)
today = date.today()
@ -78,28 +81,62 @@ def escalate_tickets(queues, verbose):
if verbose:
print "Processing: %s" % q
for t in q.ticket_set.filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)).exclude(priority=1).filter(Q(on_hold__isnull=True) | Q(on_hold=False)).filter(Q(last_escalation__lte=req_last_escl_date) | Q(last_escalation__isnull=True)):
for t in q.ticket_set.filter(
Q(status=Ticket.OPEN_STATUS)
| Q(status=Ticket.REOPENED_STATUS)
).exclude(
priority=1
).filter(
Q(on_hold__isnull=True)
| Q(on_hold=False)
).filter(
Q(last_escalation__lte=req_last_escl_date)
| Q(last_escalation__isnull=True)
):
t.last_escalation = datetime.now()
t.priority -= 1
t.save()
context = {
'ticket': t,
'queue': q,
}
if t.submitter_email:
send_templated_mail('escalated_submitter', context, recipients=t.submitter_email, sender=t.queue.from_address, fail_silently=True)
send_templated_mail(
'escalated_submitter',
context,
recipients=t.submitter_email,
sender=t.queue.from_address,
fail_silently=True,
)
if t.queue.updated_ticket_cc:
send_templated_mail('escalated_cc', context, recipients=t.queue.updated_ticket_cc, sender=t.queue.from_address, fail_silently=True)
send_templated_mail(
'escalated_cc',
context,
recipients=t.queue.updated_ticket_cc,
sender=t.queue.from_address,
fail_silently=True,
)
if t.assigned_to:
send_templated_mail('escalated_owner', context, recipients=t.assigned_to.email, sender=t.queue.from_address, fail_silently=True)
send_templated_mail(
'escalated_owner',
context,
recipients=t.assigned_to.email,
sender=t.queue.from_address,
fail_silently=True,
)
if verbose:
print " - Esclating %s from %s>%s" % (t.ticket, t.priority+1, t.priority)
print " - Esclating %s from %s>%s" % (
t.ticket,
t.priority+1,
t.priority
)
f = FollowUp(
ticket = t,
@ -118,11 +155,13 @@ def escalate_tickets(queues, verbose):
)
tc.save()
def usage():
print "Options:"
print " --queues, -q: Queues to include (default: all). Use queue slugs"
print " --verbose, -v: Display a list of dates excluded"
if __name__ == '__main__':
try:
opts, args = getopt.getopt(sys.argv[1:], 'q:v', ['queues=', 'verbose'])
@ -133,13 +172,13 @@ if __name__ == '__main__':
verbose = False
queue_slugs = None
queues = []
for o, a in opts:
if o in ('-v', '--verbose'):
verbose = True
if o in ('-q', '--queues'):
queue_slugs = a
if queue_slugs is not None:
queue_set = queue_slugs.split(',')
for queue in queue_set:

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.
scripts/get_email.py - Designed to be run from cron, this script checks the
POP and IMAP boxes defined for the queues within a
helpdesk, creating tickets from the new messages (or
POP and IMAP boxes defined for the queues within a
helpdesk, creating tickets from the new messages (or
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.models import Queue, Ticket, FollowUp, Attachment
class Command(BaseCommand):
def handle(self, *args, **options):
process_email()
def process_email():
for q in Queue.objects.filter(email_box_type__isnull=False, allow_email_submission=True):
if not q.email_box_last_check: q.email_box_last_check = datetime.now()-timedelta(minutes=30)
if not q.email_box_interval: q.email_box_interval = 0
if (q.email_box_last_check + timedelta(minutes=q.email_box_interval)) > datetime.now():
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
process_queue(q)
@ -42,6 +53,7 @@ def process_email():
q.email_box_last_check = datetime.now()
q.save()
def process_queue(q):
print "Processing: %s" % q
if q.email_box_type == 'pop3':
@ -55,16 +67,16 @@ def process_queue(q):
for msg in messagesInfo:
msgNum = msg.split(" ")[0]
msgSize = msg.split(" ")[1]
full_message = "\n".join(server.retr(msgNum)[1])
ticket_from_message(message=full_message, queue=q)
server.dele(msgNum)
server.quit()
elif q.email_box_type == 'imap':
if not q.email_box_port: q.email_box_port = 143
server = imaplib.IMAP4(q.email_box_host, q.email_box_port)
server.login(q.email_box_user, q.email_box_pass)
server.select(q.email_box_imap_folder)
@ -77,15 +89,16 @@ def process_queue(q):
server.close()
server.logout()
def ticket_from_message(message, queue):
# 'message' must be an RFC822 formatted message.
msg = message
message = email.message_from_string(msg)
subject = message.get('subject', _('Created from e-mail'))
subject = subject.replace("Re: ", "").replace("Fw: ", "").strip()
sender = message.get('from', _('Unknown Sender'))
sender_email = parseaddr(sender)[1]
if sender_email.startswith('postmaster'):
sender_email = ''
@ -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')
else:
ticket = None
counter = 0
files = []
for part in message.walk():
if part.get_content_maintype() == 'multipart':
continue
name = part.get_param("name")
if part.get_content_maintype() == 'text' and name == None:
body = part.get_payload()
else:
if not name:
ext = mimetypes.guess_extension(part.get_content_type())
name = "part-%i%s" % (counter, ext)
files.append({'filename': name, 'content': part.get_payload(decode=True), 'type': part.get_content_type()})
files.append({
'filename': name,
'content': part.get_payload(decode=True),
'type': part.get_content_type()},
)
counter += 1
now = datetime.now()
if ticket:
@ -130,7 +150,8 @@ def ticket_from_message(message, queue):
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
if ticket == None:
@ -145,7 +166,7 @@ def ticket_from_message(message, queue):
t.save()
new = True
update = ''
context = {
'ticket': t,
'queue': queue,
@ -154,26 +175,57 @@ def ticket_from_message(message, queue):
if new:
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:
send_templated_mail('newticket_cc', context, recipients=queue.new_ticket_cc, sender=queue.from_address, fail_silently=True)
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
send_templated_mail('newticket_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True)
send_templated_mail(
'newticket_cc',
context,
recipients=queue.new_ticket_cc,
sender=queue.from_address,
fail_silently=True,
)
if queue.updated_ticket_cc
and queue.updated_ticket_cc != queue.new_ticket_cc:
send_templated_mail(
'newticket_cc',
context,
recipients=queue.updated_ticket_cc,
sender=queue.from_address,
fail_silently=True,
)
else:
update = _(' (Updated)')
if t.assigned_to:
send_templated_mail('updated_owner', context, recipients=t.assigned_to.email, sender=queue.from_address, fail_silently=True)
send_templated_mail(
'updated_owner',
context,
recipients=t.assigned_to.email,
sender=queue.from_address,
fail_silently=True,
)
if queue.updated_ticket_cc:
send_templated_mail('updated_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True)
send_templated_mail(
'updated_cc',
context,
recipients=queue.updated_ticket_cc,
sender=queue.from_address,
fail_silently=True,
)
f = FollowUp(
ticket = t,
title = _('E-Mail Received from %s' % sender_email),
title = _('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
date = datetime.now(),
public = True,
comment = body,
@ -184,11 +236,17 @@ def ticket_from_message(message, queue):
for file in files:
filename = file['filename'].replace(' ', '_')
a = Attachment(followup=f, filename=filename, mime_type=file['type'], size=len(file['content']))
#a.save_file_file(file['filename'], file['content'])
a = Attachment(
followup=f,
filename=filename,
mime_type=file['type'],
size=len(file['content']),
)
a.file.save(file['filename'], ContentFile(file['content']))
a.save()
print " - %s" % file['filename']
if __name__ == '__main__':
process_email()

629
models.py
View File

@ -14,69 +14,192 @@ from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
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.
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.
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):
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)
title = models.CharField(
_('Title'),
max_length=100,
)
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'))
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_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) # Updated by the auto-pop3-and-imap-checker
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.'),
)
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):
return u"%s" % self.title
class Meta:
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):
if self.email_box_type == 'imap' and not self.email_box_imap_folder:
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()
class Ticket(models.Model):
"""
To allow a ticket to be entered as quickly as possible, only the
bare minimum fields are required. These basically allow us to
sort and manage the ticket. The user can always go back and
To allow a ticket to be entered as quickly as possible, only the
bare minimum fields are required. These basically allow us to
sort and manage the ticket. The user can always go back and
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
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
(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.
"""
@ -84,7 +207,7 @@ class Ticket(models.Model):
REOPENED_STATUS = 2
RESOLVED_STATUS = 3
CLOSED_STATUS = 4
STATUS_CHOICES = (
(OPEN_STATUS, _('Open')),
(REOPENED_STATUS, _('Reopened')),
@ -100,26 +223,87 @@ class Ticket(models.Model):
(5, _('5. Very Low')),
)
title = models.CharField(_('Title'), max_length=200)
title = models.CharField(
_('Title'),
max_length=200,
)
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)
resolution = models.TextField(_('Resolution'), blank=True, null=True)
modified = models.DateTimeField(
_('Modified'),
blank=True,
help_text=_('Date this ticket was most recently changed.'),
)
priority = models.IntegerField(_('Priority'), choices=PRIORITY_CHOICES, default=3, blank=3)
last_escalation = models.DateTimeField(blank=True, null=True, editable=False)
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,
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):
""" 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
""" 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
has a full name configured, we use that, otherwise their username. """
if not self.assigned_to:
return _('Unassigned')
@ -131,44 +315,69 @@ class Ticket(models.Model):
get_assigned_to = property(_get_assigned_to)
def _get_ticket(self):
""" A user-friendly ticket ID, which is a combination of ticket ID
and queue slug. This is generally used in e-mails. """
""" A user-friendly ticket ID, which is a combination of ticket ID
and queue slug. This is generally used in e-mail subjects. """
return u"[%s]" % (self.ticket_for_url)
ticket = property(_get_ticket)
def _get_ticket_for_url(self):
""" A URL-friendly ticket ID, used in links. """
return u"%s-%s" % (self.queue.slug, self.id)
ticket_for_url = property(_get_ticket_for_url)
def _get_priority_img(self):
""" Image-based representation of the priority """
from django.conf import settings
return u"%s/helpdesk/priorities/priority%s.png" % (settings.MEDIA_URL, self.priority)
get_priority_img = property(_get_priority_img)
def _get_priority_span(self):
"""
A HTML <span> providing a CSS_styled representation of the priority.
"""
from django.utils.safestring import mark_safe
return mark_safe(u"<span class='priority%s'>%s</span>" % (self.priority, self.priority))
get_priority_span = property(_get_priority_span)
def _get_status(self):
"""
Displays the ticket status, with an "On Hold" message if needed.
"""
held_msg = ''
if self.on_hold: held_msg = _(' - On Hold')
return u'%s%s' % (self.get_status_display(), held_msg)
get_status = property(_get_status)
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.core.urlresolvers import reverse
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)
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.core.urlresolvers import reverse
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)
class Meta:
@ -185,67 +394,120 @@ class Ticket(models.Model):
if not self.id:
# This is a new ticket as no ID yet exists.
self.created = datetime.now()
if not self.priority:
self.priority = 3
self.modified = datetime.now()
super(Ticket, self).save()
class FollowUpManager(models.Manager):
def private_followups(self):
return self.filter(public=False)
def public_followups(self):
return self.filter(public=True)
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.
The title is automatically generated at save-time, based on what action
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.
"""
ticket = models.ForeignKey(Ticket)
date = models.DateTimeField(_('Date'), auto_now_add=True)
title = models.CharField(_('Title'), max_length=200, blank=True, null=True)
comment = models.TextField(_('Comment'), blank=True, null=True)
public = models.BooleanField(_('Public'), blank=True, null=True)
user = models.ForeignKey(User, blank=True, null=True)
new_status = models.IntegerField(_('New Status'), choices=Ticket.STATUS_CHOICES, blank=True, null=True)
date = models.DateTimeField(
_('Date'),
)
title = models.CharField(
_('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()
class Meta:
ordering = ['date']
def __unicode__(self):
return u'%s' % self.title
def get_absolute_url(self):
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
def save(self):
t = self.ticket
t.modified = datetime.now()
self.date = datetime.now()
t.save()
super(FollowUp, self).save()
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.
"""
followup = models.ForeignKey(FollowUp)
field = models.CharField(_('Field'), max_length=100)
old_value = models.TextField(_('Old Value'), blank=True, null=True)
new_value = models.TextField(_('New Value'), blank=True, null=True)
field = models.CharField(
_('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):
str = u'%s ' % field
@ -254,7 +516,10 @@ class TicketChange(models.Model):
elif not old_value:
str += _('set to %s' % new_value)
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
@ -263,7 +528,7 @@ class DynamicFileField(models.FileField):
Allows model instance to specify upload_to dynamically.
Model class should have a method like:
def get_upload_to(self, attname):
return 'path/to/%d' % self.parent.id
@ -282,18 +547,43 @@ class DynamicFileField(models.FileField):
def db_type(self):
"""Required by Django for ORM."""
return 'varchar(100)'
return 'varchar(100)'
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)
file = DynamicFileField(_('File'), 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'))
file = DynamicFileField(
_('File'),
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):
""" 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):
return u'%s' % self.filename
@ -303,18 +593,38 @@ class Attachment(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
templates with a limited context - however if yo uwanted to get crafy it would
be easy to write a reply that displays ALL updates in hierarchical order etc
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
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
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.'))
body = models.TextField(_('Body'), help_text=_('Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); {{ queue }} - The queue; and {{ user }} - the current user.'))
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.'),
)
body = models.TextField(
_('Body'),
help_text=_('Context available: {{ ticket }} - ticket object (eg '
'{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} '
'- the current user.'),
)
class Meta:
ordering = ['name',]
@ -322,28 +632,84 @@ class PreSetReply(models.Model):
def __unicode__(self):
return u'%s' % self.name
class EscalationExclusion(models.Model):
queues = models.ManyToManyField(Queue, blank=True, null=True, help_text=_('Leave blank for this exclusion to 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'))
class EscalationExclusion(models.Model):
"""
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):
return u'%s' % self.name
class EmailTemplate(models.Model):
"""
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)
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.'))
template_name = models.CharField(
_('Template Name'),
max_length=100,
unique=True,
)
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):
return u'%s' % self.template_name
@ -351,40 +717,72 @@ class EmailTemplate(models.Model):
class Meta:
ordering = ['template_name',]
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.
"""
title = models.CharField(_('Title'), max_length=100)
slug = models.SlugField(_('Slug'))
description = models.TextField(_('Description'))
title = models.CharField(
_('Title'),
max_length=100,
)
slug = models.SlugField(
_('Slug'),
)
description = models.TextField(
_('Description'),
)
def __unicode__(self):
return u'%s' % self.title
class Meta:
ordering = ['title',]
def get_absolute_url(self):
return ('helpdesk_kb_category', [str(self.slug)])
get_absolute_url = models.permalink(get_absolute_url)
class KBItem(models.Model):
"""
An item within the knowledgebase. Very straightforward question/answer
An item within the knowledgebase. Very straightforward question/answer
style system.
"""
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):
self.last_updated = datetime.now()
@ -396,7 +794,7 @@ class KBItem(models.Model):
else:
return _('Unrated')
score = property(_score)
def __unicode__(self):
return u'%s' % self.title
@ -406,3 +804,4 @@ class KBItem(models.Model):
def get_absolute_url(self):
return ('helpdesk_kb_item', [str(self.id)])
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.
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:
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.
{% endif %}
"""
from django import template
def in_list(value, arg):
return value in arg
register = template.Library()
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
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
to show the status of that ticket (eg a closed
ticket would have a strikethrough).
"""
import re
from django import template
from django.core.urlresolvers import reverse
from helpdesk.models import Ticket
class ReverseProxy:
def __init__(self, sequence):
self.sequence = sequence
def __iter__(self):
length = len(self.sequence)
i = length
@ -24,16 +30,15 @@ class ReverseProxy:
i = i - 1
yield self.sequence[i]
def num_to_link(text):
if text == '':
return text
import re
from django.core.urlresolvers import reverse
matches = []
for match in re.finditer("#(\d+)", text):
matches.append(match)
for match in ReverseProxy(matches):
start = match.start()
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.
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.
"""
from django.conf.urls.defaults import *
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 django.contrib.syndication.views import feed as django_feed
urlpatterns = patterns('helpdesk.views.staff',
url(r'^dashboard/$',
url(r'^dashboard/$',
'dashboard',
name='helpdesk_dashboard'),
url(r'^tickets/$',
url(r'^tickets/$',
'ticket_list',
name='helpdesk_list'),
url(r'^tickets/submit/$',
url(r'^tickets/submit/$',
'create_ticket',
name='helpdesk_submit'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/$',
'view_ticket',
name='helpdesk_view'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$',
'update_ticket',
name='helpdesk_update'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/delete/$',
'delete_ticket',
name='helpdesk_delete'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/hold/$',
'hold_ticket',
name='helpdesk_hold'),
@ -51,7 +49,7 @@ urlpatterns = patterns('helpdesk.views.staff',
url(r'^raw/(?P<type>\w+)/$',
'raw_details',
name='helpdesk_raw'),
url(r'^rss/$',
'rss_list',
name='helpdesk_rss_index'),
@ -59,14 +57,14 @@ urlpatterns = patterns('helpdesk.views.staff',
url(r'^reports/$',
'report_index',
name='helpdesk_report_index'),
url(r'^reports/(?P<report>\w+)/$',
'run_report',
name='helpdesk_run_report'),
)
urlpatterns += patterns('helpdesk.views.public',
url(r'^$',
url(r'^$',
'homepage',
name='helpdesk_home'),
@ -80,7 +78,7 @@ urlpatterns += patterns('',
login_required(django_feed),
{'feed_dict': feed_setup},
name='helpdesk_rss'),
url(r'^api/(?P<method>[a-z_-]+)/$',
'helpdesk.views.api.api',
name='helpdesk_api'),
@ -89,7 +87,7 @@ urlpatterns += patterns('',
'django.views.generic.simple.direct_to_template',
{'template': 'helpdesk/api_help.html',},
name='helpdesk_api_help'),
url(r'^login/$',
'django.contrib.auth.views.login',
name='login'),
@ -102,13 +100,13 @@ urlpatterns += patterns('',
urlpatterns += patterns('helpdesk.views.kb',
url(r'^kb/$',
'index', name='helpdesk_kb_index'),
url(r'^kb/(?P<slug>[A-Za-z_-]+)/$',
'category', name='helpdesk_kb_category'),
url(r'^kb/(?P<item>[0-9]+)/$',
'item', name='helpdesk_kb_item'),
url(r'^kb/(?P<item>[0-9]+)/vote/$',
'vote', name='helpdesk_kb_vote'),
)

View File

@ -1,4 +1,4 @@
""" ..
""" ..
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(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 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
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.models import User
from django.http import HttpResponse
from django.shortcuts import render_to_response
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.models import Ticket, Queue, FollowUp
from helpdesk.forms import TicketForm
STATUS_OK = 200
@ -31,27 +32,34 @@ STATUS_ERROR_NOT_FOUND = 404
STATUS_ERROR_PERMISSIONS = 403
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:
* The request must be sent via HTTP POST
* The request must contain a 'user' and 'password' which
must be valid users
* 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':
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:
return api_return(STATUS_ERROR_PERMISSIONS)
api = API(request)
if hasattr(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'
if status == STATUS_OK and json:
content_type = 'text/json'
if text is None:
if status == STATUS_ERROR:
text = 'Error'
@ -75,10 +83,12 @@ def api_return(status, text='', json=False):
text = 'Invalid request method'
elif status == STATUS_OK:
text = 'OK'
r = HttpResponse(status=status, content=text, content_type=content_type)
if status == STATUS_ERROR_BADMETHOD:
r.Allow = 'POST'
return r
@ -86,27 +96,30 @@ class API:
def __init__(self, request):
self.request = request
def api_public_create_ticket(self):
def api_public_create_ticket(self):
form = TicketForm(self.request.POST)
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)]
if form.is_valid():
ticket = form.save(user=self.request.user)
return api_return(STATUS_OK, "%s" % ticket.id)
else:
return api_return(STATUS_ERROR, text=form.errors.as_text())
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)
def api_public_find_user(self):
username = self.request.POST.get('username', False)
try:
u = User.objects.get(username=username)
return api_return(STATUS_OK, "%s" % u.id)
except User.DoesNotExist:
return api_return(STATUS_ERROR, "Invalid username provided")
@ -121,9 +134,10 @@ class API:
return api_return(STATUS_ERROR, "Invalid ticket ID")
ticket.delete()
return api_return(STATUS_OK)
def api_public_hold_ticket(self):
try:
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
@ -135,7 +149,7 @@ class API:
return api_return(STATUS_OK)
def api_public_unhold_ticket(self):
try:
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
@ -162,12 +176,20 @@ class API:
if not 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:
f.public = True
f.save()
context = {
'ticket': ticket,
'queue': ticket.queue,
@ -175,13 +197,31 @@ class API:
}
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:
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:
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()
@ -198,26 +238,51 @@ class API:
if not 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()
context = {
'ticket': ticket,
'queue': ticket.queue,
'resolution': f.comment,
}
subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title)
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:
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:
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.status = Ticket.RESOLVED_STATUS
@ -225,3 +290,4 @@ class API:
ticket.save()
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.syndication.feeds import Feed
from django.core.urlresolvers import reverse
@ -23,28 +32,55 @@ class OpenTicketsByUser(Feed):
def title(self, obj):
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:
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):
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:
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):
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:
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):
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:
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):
return item.created
@ -58,18 +94,22 @@ class OpenTicketsByUser(Feed):
class UnassignedTickets(Feed):
title_template = 'helpdesk/rss/ticket_title.html'
description_template = 'helpdesk/rss/ticket_description.html'
title = _('Helpdesk: Unassigned Tickets')
description = _('Unassigned Open and Reopened tickets')
link = ''#%s?assigned_to=' % reverse('helpdesk_list')
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):
return item.created
def item_author_name(self, item):
if item.assigned_to:
return item.assigned_to.username
@ -99,20 +139,31 @@ class OpenTicketsByQueue(Feed):
return Queue.objects.get(slug__exact=bits[0])
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):
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):
return '%s?queue=%s' % (reverse('helpdesk_list'), obj.id)
return '%s?queue=%s' % (
reverse('helpdesk_list'),
obj.id,
)
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):
return item.created
def item_author_name(self, item):
if item.assigned_to:
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.
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
resolutions to common problems.
"""
from datetime import datetime
from django.http import HttpResponseRedirect
@ -16,23 +17,26 @@ from django.utils.translation import ugettext as _
from helpdesk.models import KBCategory, KBItem
def index(request):
category_list = KBCategory.objects.all()
# Add most popular items here.
return render_to_response('helpdesk/kb_index.html',
# TODO: It'd be great to have a list of most popular items here.
return render_to_response('helpdesk/kb_index.html',
RequestContext(request, {
'categories': category_list,
}))
def category(request, slug):
category = get_object_or_404(KBCategory, slug__iexact=slug)
items = category.kbitem_set.all()
return render_to_response('helpdesk/kb_category.html',
return render_to_response('helpdesk/kb_category.html',
RequestContext(request, {
'category': category,
'items': items,
}))
def item(request, item):
item = get_object_or_404(KBItem, pk=item)
return render_to_response('helpdesk/kb_item.html',
@ -40,6 +44,7 @@ def item(request, item):
'item': item,
}))
def vote(request, item):
item = get_object_or_404(KBItem, pk=item)
vote = request.GET.get('vote', None)
@ -50,3 +55,4 @@ def vote(request, item):
item.save()
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
required) views.
"""
from datetime import datetime
from django.core.urlresolvers import reverse
@ -18,16 +19,21 @@ from helpdesk.forms import PublicTicketForm
from helpdesk.lib import send_templated_mail
from helpdesk.models import Ticket, Queue
def homepage(request):
if request.user.is_authenticated():
return HttpResponseRedirect(reverse('helpdesk_dashboard'))
if request.method == 'POST':
form = PublicTicketForm(request.POST)
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
if form.is_valid():
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:
form = PublicTicketForm()
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,
}))
def view_ticket(request):
ticket = request.GET.get('ticket', '')
email = request.GET.get('email', '')
error_message = ''
if ticket and email:
queue, ticket_id = ticket.split('-')
try:
queue, ticket_id = ticket.split('-')
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:
t = False;
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, {
'ticket': ticket,
'email': email,
'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.
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.
"""
from datetime import datetime
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.core.files.base import ContentFile
from django.core.urlresolvers import reverse
from django.db import connection
from django.db.models import Q
from django.http import HttpResponseRedirect, Http404, HttpResponse
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.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment
def dashboard(request):
"""
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.
"""
tickets = Ticket.objects.filter(assigned_to=request.user).exclude(status=Ticket.CLOSED_STATUS)
unassigned_tickets = Ticket.objects.filter(assigned_to__isnull=True).exclude(status=Ticket.CLOSED_STATUS)
tickets = Ticket.objects
.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.execute("""
SELECT q.id as queue,
@ -55,9 +68,10 @@ def dashboard(request):
}))
dashboard = login_required(dashboard)
def delete_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id)
if request.method == 'GET':
return render_to_response('helpdesk/delete_ticket.html',
RequestContext(request, {
@ -68,18 +82,31 @@ def delete_ticket(request, ticket_id):
return HttpResponseRedirect(reverse('helpdesk_home'))
delete_ticket = login_required(delete_ticket)
def view_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id)
if request.GET.has_key('take'):
# Allow the user to assign the ticket to themselves whilst viewing it.
ticket.assigned_to = request.user
ticket.save()
if request.GET.has_key('close') and ticket.status == Ticket.RESOLVED_STATUS:
if not ticket.assigned_to:
if not ticket.assigned_to:
owner = 0
else:
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 render_to_response('helpdesk/ticket.html',
@ -91,6 +118,7 @@ def view_ticket(request, ticket_id):
}))
view_ticket = login_required(view_ticket)
def update_ticket(request, 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)
owner = int(request.POST.get('owner', None))
priority = int(request.POST.get('priority', ticket.priority))
if not owner and ticket.assigned_to:
owner = ticket.assigned_to.id
f = FollowUp(ticket=ticket, date=datetime.now(), comment=comment, user=request.user)
if public:
f.public = True
f.public = public
reassigned = False
if owner:
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)
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
reassigned = True
else:
f.title = _('Unassigned')
ticket.assigned_to = None
if new_status != ticket.status:
ticket.status = new_status
ticket.save()
@ -136,20 +165,30 @@ def update_ticket(request, ticket_id):
f.title = _('Updated')
f.save()
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()
ticket.title = title
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()
ticket.priority = priority
if f.new_status == Ticket.RESOLVED_STATUS:
ticket.resolution = comment
if ticket.submitter_email and ((f.comment != '' and public) or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))):
context = {
'ticket': ticket,
@ -157,16 +196,24 @@ def update_ticket(request, ticket_id):
'resolution': ticket.resolution,
'comment': f.comment,
}
if f.new_status == Ticket.RESOLVED_STATUS:
template = 'resolved_submitter'
elif f.new_status == Ticket.CLOSED_STATUS:
template = 'closed_submitter'
else:
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:
# 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.
if reassigned:
template_staff = 'assigned_owner'
@ -176,9 +223,15 @@ def update_ticket(request, ticket_id):
template_staff = 'closed_owner'
else:
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 reassigned:
template_cc = 'assigned_cc'
@ -188,22 +241,33 @@ def update_ticket(request, ticket_id):
template_cc = 'closed_cc'
else:
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:
for file in request.FILES.getlist('attachment'):
filename = file['filename'].replace(' ', '_')
a = Attachment(followup=f, filename=filename, mime_type=file['content-type'], size=len(file['content']))
#a.save_file_file(file['filename'], file['content'])
a = Attachment(
followup=f,
filename=filename,
mime_type=file['content-type'],
size=len(file['content']),
)
a.file.save(file['filename'], ContentFile(file['content']))
a.save()
ticket.save()
return HttpResponseRedirect(ticket.get_absolute_url())
update_ticket = login_required(update_ticket)
def ticket_list(request):
tickets = Ticket.objects.select_related()
context = {}
@ -229,7 +293,7 @@ def ticket_list(request):
### KEYWORD SEARCHING
q = request.GET.get('q', None)
if q:
qset = (
Q(title__icontains=q) |
@ -239,7 +303,7 @@ def ticket_list(request):
)
tickets = tickets.filter(qset)
context = dict(context, query=q)
### SORTING
sort = request.GET.get('sort', None)
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)
def create_ticket(request):
if request.method == '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['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, {
'form': form,
}))
create_ticket = login_required(create_ticket)
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',):
raise Http404
if type == 'preset' and request.GET.get('id', False):
try:
preset = PreSetReply.objects.get(id=request.GET.get('id'))
return HttpResponse(preset.body)
except PreSetReply.DoesNotExist:
raise Http404
raise Http404
raw_details = login_required(raw_details)
def hold_ticket(request, ticket_id, unhold=False):
ticket = get_object_or_404(Ticket, id=ticket_id)
@ -299,7 +370,7 @@ def hold_ticket(request, ticket_id, unhold=False):
else:
ticket.on_hold = True
title = _('Ticket placed on hold')
f = FollowUp(
ticket = ticket,
user = request.user,
@ -308,28 +379,32 @@ def hold_ticket(request, ticket_id, unhold=False):
public = True,
)
f.save()
ticket.save()
return HttpResponseRedirect(ticket.get_absolute_url())
hold_ticket = login_required(hold_ticket)
def unhold_ticket(request, ticket_id):
return hold_ticket(request, ticket_id, unhold=True)
unhold_ticket = login_required(unhold_ticket)
def rss_list(request):
return render_to_response('helpdesk/rss_list.html',
return render_to_response('helpdesk/rss_list.html',
RequestContext(request, {
'queues': Queue.objects.all(),
}))
rss_list = login_required(rss_list)
def report_index(request):
return render_to_response('helpdesk/report_index.html',
RequestContext(request, {}))
report_index = login_required(report_index)
def run_report(request, report):
priority_sql = []
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_columns.append("%s" % p[1]._proxy____unicode_cast())
priority_sql = ", ".join(priority_sql)
status_sql = []
status_columns = []
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_columns.append("%s" % s[1]._proxy____unicode_cast())
status_sql = ", ".join(status_sql)
queue_sql = []
queue_columns = []
for q in Queue.objects.all():
@ -368,11 +443,11 @@ def run_report(request, report):
'Dec',
)
month_columns = []
first_ticket = Ticket.objects.all().order_by('created')[0]
first_month = first_ticket.created.month
first_year = first_ticket.created.year
last_ticket = Ticket.objects.all().order_by('-created')[0]
last_month = last_ticket.created.month
last_year = last_ticket.created.year
@ -420,33 +495,32 @@ def run_report(request, report):
if report == 'userpriority':
sql = user_base_sql % priority_sql
columns = ['username'] + priority_columns
elif report == 'userqueue':
sql = user_base_sql % queue_sql
columns = ['username'] + queue_columns
elif report == 'userstatus':
sql = user_base_sql % status_sql
columns = ['username'] + status_columns
elif report == 'usermonth':
sql = user_base_sql % month_sql
columns = ['username'] + month_columns
elif report == 'queuepriority':
sql = queue_base_sql % priority_sql
columns = ['queue'] + priority_columns
elif report == 'queuestatus':
sql = queue_base_sql % status_sql
columns = ['queue'] + status_columns
elif report == 'queuemonth':
sql = queue_base_sql % month_sql
columns = ['queue'] + month_columns
from django.db import connection
cursor = connection.cursor()
cursor.execute(sql)
report_output = query_to_dict(cursor.fetchall(), cursor.description)
@ -465,7 +539,7 @@ def run_report(request, report):
chart_url = bar_chart([columns] + data)
else:
chart_url = ''
return render_to_response('helpdesk/report_output.html',
RequestContext(request, {
'headings': columns,
@ -474,3 +548,4 @@ def run_report(request, report):
'chart': chart_url,
}))
run_report = login_required(run_report)