* Add README and LICENSE files

* Add e-mail templates for submitter-based emails
* Add send_multipart_email() to lib.py - sends HTML/plain emails
* Add 'Take' link to unassigned tickets on ticket detail view
* Add Description to ticket detail view
* When resolving ticket, copy comment into ticket resolution
* Display resolution and 'Accept & Close' link on ticket detail view
* Create scripts/ folder
* Added POP/IMAP details to Queue model
* Added get_email.py; polls POP/IMAP boxes & creates ticket
* Added keyword search functionality
This commit is contained in:
Ross Poulton 2008-01-07 20:22:13 +00:00
parent 3fccc19af8
commit 10158056b6
20 changed files with 545 additions and 14 deletions

34
LICENSE Normal file
View File

@ -0,0 +1,34 @@
..
.,::;::::::
..,::::::::,,,,::: Jutda Helpdesk - A Django
.,,::::::,,,,,,,,,,,,,:: powered ticket tracker for
.,::::::,,,,,,,,,,,,,,,,,,:;r. small enterprise
.::::,,,,,,,,,,,,,,,,,,,,,,:;;rr.
.:::,,,,,,,,,,,,,,,,,,,,,,,:;;;;;rr (c) Copyright 2008
.:::,,,,,,,,,,,,,,,,,,,,,,,:;;;:::;;rr
.:::,,,,,,,,,,,,,,,,,,,,. ,;;;::::::;;rr Jutda
.:::,,,,,,,,,,,,,,,,,,. .:;;:::::::::;;rr
.:::,,,,,,,,,,,,,,,. .;r;::::::::::::;r; All Rights Reserved
.:::,,,,,,,,,,,,,,, .;r;;:::::::::::;;:.
.:::,,,,,,,,,,,,,,,. .;r;;::::::::::::;:.
.;:,,,,,,,,,,,,,,, .,;rr;::::::::::::;:. This software is released
.,:,,,,,,,,,,,,,. .,:;rrr;;::::::::::::;;. under a limited-use license that
:,,,,,,,,,,,,,..:;rrrrr;;;::::::::::::;;. allows you to freely download this
:,,,,,,,:::;;;rr;;;;;;:::::::::::::;;, software from it's manufacturer and
::::;;;;;;;;;;;:::::::::::::::::;;, use it yourself, however you may not
.r;;;;:::::::::::::::::::::::;;;, distribute it. For further details, see
.r;::::::::::::::::::::;;;;;:, the enclosed LICENSE file.
.;;::::::::::::::;;;;;:,.
.;;:::::::;;;;;;:,. Please direct people who wish to download this
.r;;;;;;;;:,. software themselves to www.jutda.com.au.
,,,..
$Id$
LICENSING INFORMATION
This software is not Free Software. You may freely download this software from it's manufacturer (Jutda, www.jutda.com.au) and use it for your own purposes. You may not distribute this software or give it to others.
You may download and install this software for other users, for example for a client or customer of yours or your companies.

105
README Normal file
View File

@ -0,0 +1,105 @@
..
.,::;::::::
..,::::::::,,,,::: Jutda Helpdesk - A Django
.,,::::::,,,,,,,,,,,,,:: powered ticket tracker for
.,::::::,,,,,,,,,,,,,,,,,,:;r. small enterprise
.::::,,,,,,,,,,,,,,,,,,,,,,:;;rr.
.:::,,,,,,,,,,,,,,,,,,,,,,,:;;;;;rr (c) Copyright 2008
.:::,,,,,,,,,,,,,,,,,,,,,,,:;;;:::;;rr
.:::,,,,,,,,,,,,,,,,,,,,. ,;;;::::::;;rr Jutda
.:::,,,,,,,,,,,,,,,,,,. .:;;:::::::::;;rr
.:::,,,,,,,,,,,,,,,. .;r;::::::::::::;r; All Rights Reserved
.:::,,,,,,,,,,,,,,, .;r;;:::::::::::;;:.
.:::,,,,,,,,,,,,,,,. .;r;;::::::::::::;:.
.;:,,,,,,,,,,,,,,, .,;rr;::::::::::::;:. This software is released
.,:,,,,,,,,,,,,,. .,:;rrr;;::::::::::::;;. under a limited-use license that
:,,,,,,,,,,,,,..:;rrrrr;;;::::::::::::;;. allows you to freely download this
:,,,,,,,:::;;;rr;;;;;;:::::::::::::;;, software from it's manufacturer and
::::;;;;;;;;;;;:::::::::::::::::;;, use it yourself, however you may not
.r;;;;:::::::::::::::::::::::;;;, distribute it. For further details, see
.r;::::::::::::::::::::;;;;;:, the enclosed LICENSE file.
.;;::::::::::::::;;;;;:,.
.;;:::::::;;;;;;:,. Please direct people who wish to download this
.r;;;;;;;;:,. software themselves to www.jutda.com.au.
,,,..
$Id$
#########################
0. Table of Contents
#########################
1. Licensing
2. Dependencies (pre-flight checklist)
3. Installation
4. Initial Configuration
#########################
1. Licensing
#########################
See the file 'LICENSE' for licensing terms and conditions.
#########################
2. Dependencies (pre-flight checklist)
#########################
1. Python 2.3+
2. Django (post-0.96, eg SVN checkout)
3. Markdown
4. An existing WORKING Django project with database etc
#########################
3. Installation
#########################
1. Place 'helpdesk' in your Python path
2. In your projects' settings.py file, add these lines to the INSTALLED_APPS setting:
'helpdesk',
'django.contrib.admin',
'django.contrib.markup'
3. In your projects' urls.py file, add this line:
(r'helpdesk/', include('helpdesk.urls')),
You can substitute 'helpdesk/' for something else, eg 'support/' or even ''.
4. Ensure the admin line is un-hashed in urls.py:
# Uncomment this for admin:
(r'^admin/', include('django.contrib.admin.urls')),
If you use helpdesk at the top of your domain (at /), ensure the admin
line comes BEFORE the helpdesk line.
5. In your project directory (NOT the helpdesk directory) run
./manage.py syncdb
to create database tables
#########################
4. Initial Configuration
#########################
1. Visit http://yoursite/admin/ and add a Helpdesk Queue. If you wish,
enter your POP3 or IMAP server details.
IMPORTANT NOTE: Any tickets created via POP3 or IMAP mailboxes will DELETE
the original e-mail from the mail server.
2. Visit http://yoursite/helpdesk/ (or other path as defined in your urls.py)
3. If you wish to automatically create tickets from the contents of an e-mail
inbox, set up a cronjob to run scripts/get_email.py on a regular basis.
Don't forget to set the relevant Django environment variables in your
crnotab:
5 * * * * DJANGO_SETTINGS_MODULE='myproject.settings' python /path/to/helpdesk/scripts/get_email.py
IMPORTANT NOTE: Any tickets created via POP3 or IMAP mailboxes will DELETE
the original e-mail from the mail server.
You're now up and running!

78
lib.py Normal file
View File

@ -0,0 +1,78 @@
""" ..
.,::;::::::
..,::::::::,,,,::: Jutda Helpdesk - A Django
.,,::::::,,,,,,,,,,,,,:: powered ticket tracker for
.,::::::,,,,,,,,,,,,,,,,,,:;r. small enterprise
.::::,,,,,,,,,,,,,,,,,,,,,,:;;rr.
.:::,,,,,,,,,,,,,,,,,,,,,,,:;;;;;rr (c) Copyright 2008
.:::,,,,,,,,,,,,,,,,,,,,,,,:;;;:::;;rr
.:::,,,,,,,,,,,,,,,,,,,,. ,;;;::::::;;rr Jutda
.:::,,,,,,,,,,,,,,,,,,. .:;;:::::::::;;rr
.:::,,,,,,,,,,,,,,,. .;r;::::::::::::;r; All Rights Reserved
.:::,,,,,,,,,,,,,,, .;r;;:::::::::::;;:.
.:::,,,,,,,,,,,,,,,. .;r;;::::::::::::;:.
.;:,,,,,,,,,,,,,,, .,;rr;::::::::::::;:. This software is released
.,:,,,,,,,,,,,,,. .,:;rrr;;::::::::::::;;. under a limited-use license that
:,,,,,,,,,,,,,..:;rrrrr;;;::::::::::::;;. allows you to freely download this
:,,,,,,,:::;;;rr;;;;;;:::::::::::::;;, software from it's manufacturer and
::::;;;;;;;;;;;:::::::::::::::::;;, use it yourself, however you may not
.r;;;;:::::::::::::::::::::::;;;, distribute it. For further details, see
.r;::::::::::::::::::::;;;;;:, the enclosed LICENSE file.
.;;::::::::::::::;;;;;:,.
.;;:::::::;;;;;;:,. Please direct people who wish to download this
.r;;;;;;;;:,. software themselves to www.jutda.com.au.
,,,..
$Id$
"""
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.
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
both the email messages (templates) & the subject.
subject can be plain text or a Django template string, eg:
New Job: {{ job.id }} {{ job.title }}
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
DEFAULT_FROM_EMAIL will be used.
"""
from django.core.mail import EmailMultiAlternatives
from django.template import loader, Context
from django.conf import settings
if not sender:
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)
if type(recipients) != list:
recipients = [recipients,]
msg = EmailMultiAlternatives(subject_part, text_part, sender, recipients, bcc=bcc)
msg.attach_alternative(html_part, "text/html")
if files:
if type(files) != list:
files = [files,]
for file in files:
msg.attach_file(file)
return msg.send(fail_silently)

View File

@ -46,12 +46,29 @@ class Queue(models.Model):
slug = models.SlugField()
email_address = models.EmailField(blank=True, null=True)
def _from_address(self):
return '%s <%s>' % (self.title, self.email_address)
from_address = property(_from_address)
email_box_type = models.CharField(maxlength=5, choices=(('pop3', 'POP 3'),('imap', 'IMAP')), blank=True, null=True, help_text='E-Mail Server Type - Both POP3 and IMAP are supported. Select your email server type here.')
email_box_host = models.CharField(maxlength=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(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.')
email_box_user = models.CharField(maxlength=200, blank=True, null=True, help_text='Username for accessing this mailbox.')
email_box_pass = models.CharField(maxlength=200, blank=True, null=True, help_text='Password for the above username')
email_box_imap_folder = models.CharField(maxlength=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(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
def __unicode__(self):
return u"%s" % self.title
class Admin:
pass
def save(self):
if self.email_box_type == 'imap' and not self.email_box_imap_folder:
self.email_box_imap_folder = 'INBOX'
super(Queue, self).save()
class Ticket(models.Model):
"""
@ -105,6 +122,13 @@ class Ticket(models.Model):
return self.assigned_to
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. """
return "[%s-%s]" % (self.queue.slug, self.id)
ticket = property(_get_ticket)
class Admin:
list_display = ('title', 'status', 'assigned_to',)
date_hierarchy = 'created'
@ -175,3 +199,7 @@ class TicketChange(models.Model):
else:
str += 'changed from "%s" to "%s"' % (old_value, new_value)
return str
#class Attachment(models.Model):
#followup = models.ForeignKey(FollowUp, edit_inline=models.TABULAR)
#file = models.FileField()

125
scripts/get_email.py Normal file
View File

@ -0,0 +1,125 @@
import poplib
import imaplib
from datetime import datetime, timedelta
import email, mimetypes, re
from email.Utils import parseaddr
from helpdesk.models import Queue,Ticket
from helpdesk.lib import send_multipart_mail
def process_email():
for q in Queue.objects.filter(email_box_type__isnull=False):
if not q.email_box_last_check: q.email_box_last_check = datetime.now()-timedelta(minutes=30)
if not q.email_box_interval: q.email_box_interval = 0
if (q.email_box_last_check + timedelta(minutes=q.email_box_interval)) > datetime.now():
continue
print "Processing: %s" % q
if q.email_box_type == 'pop3':
server = poplib.POP3(q.email_box_host)
server.getwelcome()
server.user(q.email_box_user)
server.pass_(q.email_box_pass)
messagesInfo = server.list()[1]
for msg in messagesInfo:
msgNum = msg.split(" ")[0]
msgSize = msg.split(" ")[1]
full_message = "\n".join(server.retr(msgNum)[1])
ticket_from_message(message=full_message, queue=q)
server.dele(msgNum)
server.quit()
elif q.email_box_type == 'imap':
if not q.email_box_port: q.email_box_port = 143
server = imaplib.IMAP4(q.email_box_host, q.email_box_port)
server.login(q.email_box_user, q.email_box_pass)
server.select(q.email_box_imap_folder)
status, data = server.search(None, 'ALL')
for num in data[0].split():
status, data = server.fetch(num, '(RFC822)')
ticket_from_message(message=data[0][1], queue=q)
server.store(num, '+FLAGS', '\\Deleted')
server.expunge()
server.close()
server.logout()
q.email_box_last_check = datetime.now()
q.save()
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(message.get('from', 'Unknown Sender'))[1]
regex = re.compile("^\[\d+\]")
if regex.match(subject):
# This is a reply or forward.
ticket = re.match(r"^\[(?P<id>\d+)\]", subject).group('id')
else:
ticket = None
counter = 0
files = []
for part in message.walk():
if part.get_main_type() == 'multipart':
continue
name = part.get_param("name")
if part.get_content_maintype() == 'text' and name == None:
body = part.get_payload()
else:
if name == None:
ext = mimetypes.guess_extension(part.get_content_type())
name = "part-%i%s" % (counter, ext)
files.append({'filename': name, 'content': part.get_payload(decode=True), 'type': part.get_content_type()})
counter += 1
now = datetime.now()
if ticket:
try:
t = Ticket.objects.get(id=ticket)
except:
ticket = None
if ticket == None:
t = Ticket(
title=subject,
queue=queue,
submitter_email=sender_email,
created=now,
description=body,
)
t.save()
context = {
'ticket': t,
'queue': queue,
}
if sender_email:
send_multipart_mail('helpdesk/emails/submitter_newticket', context, '%s %s' % (t.ticket, t.title), sender_email, queue.from_address)
print " [%s-%s] %s" % (t.queue.slug, t.id, t.title)
#for file in files:
#data = file['content']
#filename = file['filename'].replace(' ', '_')
#type = file['type']
#a = Attachment(followup=f, filename=filename, mimetype=type, size=len(data))
#a.save()
#print " - %s" % file['filename']
if __name__ == '__main__':
process_email()

View File

@ -13,7 +13,7 @@
<li><a href='{% url helpdesk_home %}'>Dashboard</a></li>
<li><a href='{% url helpdesk_list %}'>Tickets</a></li>
<li><a href='{% url helpdesk_submit %}'>Submit Ticket</a></li>
<li>Search</li>
{% if not query %}<li><form method='get' action='{% url helpdesk_list %}'><input type='text' name='q' size='10' /><input type='hidden' name='status' value='1' /><input type='hidden' name='status' value='2' /><input type='hidden' name='status' value='3' /><input type='submit' value='Search' /></form></li>{% endif %}
</ul>
</div>
<div id='body'>

View File

@ -40,7 +40,7 @@ $(document).ready(function() {
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}</a></th>
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></th>
<td>{{ ticket.queue }}</td>
<td>{{ ticket.created|timesince }} ago</td>
<td><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|timesince }} ago</span></td>
<th><a href='{{ ticket.get_absolute_url }}?take'>Take</a></th>
</tr>
{% endfor %}

View File

@ -0,0 +1,9 @@
<h1 style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 14pt; color: #6593C0'>{% block header %}Helpdesk{% endblock %}</h1>
{% block content %}{% endblock %}
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>Regards,</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'><b>{{ queue.title }}</b>{% if queue.email_address %}<br><a href='mailto:{{ queue.email_address }}'>{{ queue.email_address }}</a>{% endif %}</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 9pt; color: #808080;' color='#808080'>This e-mail was sent to you as a user of our support service, in accordance with our privacy policy. Please advise us if you believe you have received this e-mail in error.</p>

View File

@ -0,0 +1,9 @@
{% extends "helpdesk/emails/base.html" %}
{% block header %}E-Mail Heading Here{% endblock %}
{% block content %}
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>Dear User,</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>This is some content.</p>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "helpdesk/emails/base.html" %}
{% block header %}Thank You For Your Submission{% endblock %}
{% block content %}
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>Hello,</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>This is a courtesy e-mail to let you know that we have received your helpdesk query with a subject of <i>{{ ticket.title }}</i>. </p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>You do not have to do anything further at this stage. Your ticket has been assigned a number of <b>{{ ticket.ticket }}</b>.</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>If you wish to send us further details, or if you have any queries about this ticket, please include the ticket id of <b>{{ ticket.ticket }}</b> in the subject. The easiest way to do this is just press 'reply' to this message.</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>We will investigate your query and attempt to resolve it as soon as possible. You will receive further updates and a resolution via this e-mail address.</p>
{% endblock %}

View File

@ -0,0 +1,11 @@
Hello,
This is a courtesy e-mail to let you know that we have received your helpdesk query with a subject of {{ ticket.title }}.
You do not have to do anything further at this stage. Your ticket has been assigned a number of {{ ticket.ticket }}.
If you wish to send us further details, or if you have any queries about this ticket, please include the ticket id of {{ ticket.ticket }} in the subject. The easiest way to do this is just press 'reply' to this message.
We will investigate your query and attempt to resolve it as soon as possible. You will receive further updates and a resolution via this e-mail address.
{% include "helpdesk/emails/text_footer.txt" %}

View File

@ -0,0 +1,16 @@
{% extends "helpdesk/emails/base.html" %}
{% block header %}Your Ticket Has Been Resolved{% endblock %}
{% block content %}
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>Hello,</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>You recently logged a ticket with a subject of <i>{{ ticket.title }}</i> with us. This e-mail is to advise you of a resolution to that ticket.</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>The following resolution was added to ticket <b>{{ ticket.ticket }}</b>:</p>
<blockquote style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt; padding-left: 20px; border-left: solid #ccc 2px;'>{{ resolution }}</blockquote>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>Can you please confirm that this resolution addresses your needs so we may close this ticket? If you have any further queries, or if you do not believe this resolution is adequate, please reply to this e-mail and keep the subject intact.</p>
{% endblock %}

View File

@ -0,0 +1,11 @@
Hello,
You recently logged a ticket with a subject of '{{ ticket.title }}' with us. This e-mail is to advise you of a resolution to that ticket.
The following resolution was added to ticket {{ ticket.ticket }}:
{{ resolution }}
Can you please confirm that this resolution addresses your needs so we may close this ticket? If you have any further queries, or if you do not believe this resolution is adequate, please reply to this e-mail and keep the subject intact.
{% include "helpdesk/emails/text_footer.txt" %}

View File

@ -0,0 +1,16 @@
{% extends "helpdesk/emails/base.html" %}
{% block header %}Your Ticket Has Been Updated{% endblock %}
{% block content %}
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>Hello,</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>You recently logged a ticket with a subject of <i>{{ ticket.title }}</i> with us. This e-mail is to advise you of an update to that ticket.</p>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>The following comment was added to ticket <b>{{ ticket.ticket }}</b>:</p>
<blockquote style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt; padding-left: 20px; border-left: solid #ccc 2px;'>{{ comment }}</blockquote>
<p style='font-family: "Trebuchet MS", Arial, sans-serif; font-size: 11pt;'>To provide us with further information, please reply to this e-mail.</p>
{% endblock %}

View File

@ -0,0 +1,11 @@
Hello,
You recently logged a ticket with a subject of '{{ ticket.title }}' with us. This e-mail is to advise you of an update to that ticket.
The following comment was added to ticket {{ ticket.ticket }}:
{{ comment }}
To provide us with further information, please reply to this e-mail.
{% include "helpdesk/emails/text_footer.txt" %}

View File

@ -0,0 +1,6 @@
Regards,
{{ queue.title }}{% if queue.email_address %}
{{ queue.email_address }}{% endif %}
This e-mail was sent to you as a user of our support service, in accordance with our privacy policy. Please advise us if you believe you have received this e-mail in error.

View File

@ -22,17 +22,31 @@
<tr class='row_odd'>
<th>Submitted On</th>
<td>{{ ticket.created }} ({{ ticket.created|timesince }} ago)</td>
<td>{{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago)</td>
</tr>
<tr class='row_even'>
<th>Assigned To</th>
<td>{{ ticket.get_assigned_to }}</td>
<td>{{ ticket.get_assigned_to }}{% ifequal ticket.get_assigned_to 'Unassigned' %} <strong><a href='?take'>Take</a></strong>{% endifequal %}</td>
</tr>
{% if ticket.submitter_email %}<tr class='row_odd'>
<tr class='row_odd'>
<th>Submitter E-Mail</th>
<td>{{ ticket.submitter_email }}</td>
</tr>
<tr class='row_even'>
<th colspan='2'>Description</th>
</tr>
<tr class='row_odd'>
<td colspan='2'>{{ ticket.description }}</td>
</tr>
{% if ticket.resolution %}<tr class='row_even'>
<th colspan='2'>Resolution{% ifequal ticket.get_status_display "Resolved" %} [<a href='?close'>Accept &amp; Close</a>]{% endifequal %}</th>
</tr>
<tr class='row_odd'>
<td colspan='2'>{{ ticket.resolution }}</td>
</tr>{% endif %}
</table>
@ -42,7 +56,7 @@
{% load ticket_to_link %}
{% for followup in ticket.followup_set.all %}
<div class='followup'>
<div class='title'>{{ followup.title }} <span class='byline'>by {{ followup.user }} <span title='at {{ followup.date|date:"r" }}'>{{ followup.date|timesince }} ago</span>{% if not followup.public %} <span class='private'>(Private)</span>{% endif %}</span></div>
<div class='title'>{{ followup.title }} <span class='byline'>by {{ followup.user }} <span title='{{ followup.date|date:"r" }}'>{{ followup.date|timesince }} ago</span>{% if not followup.public %} <span class='private'>(Private)</span>{% endif %}</span></div>
{{ followup.comment|markdown|num_to_link }}
{% if followup.ticketchange_set.all %}<div class='changes'>
{% for change in followup.ticketchange_set.all %}
@ -63,18 +77,18 @@ Changed {{ change.field }} from {{ change.old_value }} to {{ change.new_value }}
<input type='radio' name='new_status' value='4' id='st_closed'><label for='st_closed'>Closed</label>
{% endifequal %}
{% ifequal ticket.status 2 %}
<input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'><label for='st_reopened' class='active'>Reopened</label>
<input type='radio' name='new_status' value='3' id='st_resolved'><label for='st_resolved'>Resolved</label>
<input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'><label for='st_reopened' class='active'>Reopened</label> &raquo;
<input type='radio' name='new_status' value='3' id='st_resolved'><label for='st_resolved'>Resolved</label> &raquo;
<input type='radio' name='new_status' value='4' id='st_closed'><label for='st_closed'>Closed</label>
{% endifequal %}
{% ifequal ticket.status 3 %}
<input type='radio' name='new_status' value='2' id='st_reopened'><label for='st_reopened'>Reopened</label>
<input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'><label for='st_resolved' class='active'>Resolved</label>
<input type='radio' name='new_status' value='2' id='st_reopened'><label for='st_reopened'>Reopened</label> &laquo;
<input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'><label for='st_resolved' class='active'>Resolved</label> &raquo;
<input type='radio' name='new_status' value='4' id='st_closed'><label for='st_closed'>Closed</label>
{% endifequal %}
{% ifequal ticket.status 4 %}
<input type='radio' name='new_status' value='2' id='st_reopened'><label for='st_reopened'>Reopened</label>
<input type='radio' name='new_status' value='2' id='st_reopened'><label for='st_reopened'>Reopened</label> &laquo;
<input type='radio' name='new_status' value='4' id='st_closed' checked='checked'><label for='st_closed'>Closed</label>
{% endifequal %}

View File

@ -24,6 +24,8 @@ $(document).ready(function() {
<label for='id_statuses'>Status(es)</label> {% for s in status_choices %}<input type='checkbox' name='status' value='{{ s.0 }}'{% if s.0|in_list:statuses %} checked='checked'{% endif %}> {{ s.1 }}{% endfor %}
<label for='id_query'>Keywords</label> <input type='text' name='q' value='{{ query }}' id='id_query' />
<input type='submit' value='Go!' />
</form>
@ -36,7 +38,7 @@ $(document).ready(function() {
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></th>
<td>{{ ticket.queue }}</td>
<td>{{ ticket.get_status_display }}</td>
<td>{{ ticket.created }}</td>
<td><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|timesince }} ago</span></td>
<td>{{ ticket.get_assigned_to }}</td>
</tr>
{% endfor %}{% else %}

View File

@ -48,7 +48,7 @@ urlpatterns = patterns('helpdesk.views',
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$',
'update_ticket',
name='helpdesk_view'),
name='helpdesk_update'),
)
urlpatterns += patterns('',

View File

@ -67,6 +67,14 @@ def view_ticket(request, ticket_id):
ticket.assigned_to = request.user
ticket.save()
if request.GET.has_key('close') and ticket.status == Ticket.RESOLVED_STATUS:
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"}
return update_ticket(request, ticket_id)
return render_to_response('helpdesk/ticket.html',
RequestContext(request, {
'ticket': ticket,
@ -120,6 +128,24 @@ def update_ticket(request, ticket_id):
c.save()
ticket.title = title
if f.new_status == Ticket.RESOLVED_STATUS:
ticket.resolution = comment
if public and ticket.submitter_email:
context = {
'ticket': ticket,
'queue': ticket.queue,
'resolution': ticket.resolution,
'comment': f.comment,
}
if f.new_status == Ticket.RESOLVED_STATUS:
template = 'helpdesk/emails/submitter_resolved'
subject = '%s Ticket Resolved'
else:
template = 'helpdesk/emails/submitter_updated'
subject = '%s Ticket Updated'
send_multipart_mail(template, context, subject, ticket.submitter_email, ticket.queue.from_address)
ticket.save()
return HttpResponseRedirect(ticket.get_absolute_url())
@ -148,6 +174,19 @@ def ticket_list(request):
tickets = tickets.filter(status__in=statuses)
context = dict(context, statuses=statuses)
### KEYWORD SEARCHING
q = request.GET.get('q', None)
if q:
qset = (
Q(title__icontains=q) |
Q(description__icontains=q) |
Q(resolution__icontains=q) |
Q(submitter_email__icontains=q)
)
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'):