forked from extern/django-helpdesk
* 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:
parent
3fccc19af8
commit
10158056b6
34
LICENSE
Normal file
34
LICENSE
Normal 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
105
README
Normal 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
78
lib.py
Normal 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)
|
||||||
|
|
28
models.py
28
models.py
@ -46,12 +46,29 @@ class Queue(models.Model):
|
|||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
email_address = models.EmailField(blank=True, null=True)
|
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):
|
def __unicode__(self):
|
||||||
return u"%s" % self.title
|
return u"%s" % self.title
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
pass
|
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):
|
class Ticket(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -105,6 +122,13 @@ class Ticket(models.Model):
|
|||||||
return self.assigned_to
|
return self.assigned_to
|
||||||
get_assigned_to = property(_get_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:
|
class Admin:
|
||||||
list_display = ('title', 'status', 'assigned_to',)
|
list_display = ('title', 'status', 'assigned_to',)
|
||||||
date_hierarchy = 'created'
|
date_hierarchy = 'created'
|
||||||
@ -175,3 +199,7 @@ class TicketChange(models.Model):
|
|||||||
else:
|
else:
|
||||||
str += 'changed from "%s" to "%s"' % (old_value, new_value)
|
str += 'changed from "%s" to "%s"' % (old_value, new_value)
|
||||||
return str
|
return str
|
||||||
|
|
||||||
|
#class Attachment(models.Model):
|
||||||
|
#followup = models.ForeignKey(FollowUp, edit_inline=models.TABULAR)
|
||||||
|
#file = models.FileField()
|
||||||
|
125
scripts/get_email.py
Normal file
125
scripts/get_email.py
Normal 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()
|
@ -13,7 +13,7 @@
|
|||||||
<li><a href='{% url helpdesk_home %}'>Dashboard</a></li>
|
<li><a href='{% url helpdesk_home %}'>Dashboard</a></li>
|
||||||
<li><a href='{% url helpdesk_list %}'>Tickets</a></li>
|
<li><a href='{% url helpdesk_list %}'>Tickets</a></li>
|
||||||
<li><a href='{% url helpdesk_submit %}'>Submit Ticket</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div id='body'>
|
<div id='body'>
|
||||||
|
@ -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.id }}</a></th>
|
||||||
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></th>
|
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></th>
|
||||||
<td>{{ ticket.queue }}</td>
|
<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>
|
<th><a href='{{ ticket.get_absolute_url }}?take'>Take</a></th>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
9
templates/helpdesk/emails/base.html
Normal file
9
templates/helpdesk/emails/base.html
Normal 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>
|
9
templates/helpdesk/emails/sample.html
Normal file
9
templates/helpdesk/emails/sample.html
Normal 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 %}
|
17
templates/helpdesk/emails/submitter_newticket.html
Normal file
17
templates/helpdesk/emails/submitter_newticket.html
Normal 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 %}
|
11
templates/helpdesk/emails/submitter_newticket.txt
Normal file
11
templates/helpdesk/emails/submitter_newticket.txt
Normal 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" %}
|
16
templates/helpdesk/emails/submitter_resolved.html
Normal file
16
templates/helpdesk/emails/submitter_resolved.html
Normal 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 %}
|
11
templates/helpdesk/emails/submitter_resolved.txt
Normal file
11
templates/helpdesk/emails/submitter_resolved.txt
Normal 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" %}
|
16
templates/helpdesk/emails/submitter_updated.html
Normal file
16
templates/helpdesk/emails/submitter_updated.html
Normal 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 %}
|
11
templates/helpdesk/emails/submitter_updated.txt
Normal file
11
templates/helpdesk/emails/submitter_updated.txt
Normal 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" %}
|
6
templates/helpdesk/emails/text_footer.txt
Normal file
6
templates/helpdesk/emails/text_footer.txt
Normal 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.
|
@ -22,17 +22,31 @@
|
|||||||
|
|
||||||
<tr class='row_odd'>
|
<tr class='row_odd'>
|
||||||
<th>Submitted On</th>
|
<th>Submitted On</th>
|
||||||
<td>{{ ticket.created }} ({{ ticket.created|timesince }} ago)</td>
|
<td>{{ ticket.created|date:"r" }} ({{ ticket.created|timesince }} ago)</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr class='row_even'>
|
<tr class='row_even'>
|
||||||
<th>Assigned To</th>
|
<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>
|
</tr>
|
||||||
|
|
||||||
{% if ticket.submitter_email %}<tr class='row_odd'>
|
<tr class='row_odd'>
|
||||||
<th>Submitter E-Mail</th>
|
<th>Submitter E-Mail</th>
|
||||||
<td>{{ ticket.submitter_email }}</td>
|
<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 & Close</a>]{% endifequal %}</th>
|
||||||
|
</tr>
|
||||||
|
<tr class='row_odd'>
|
||||||
|
<td colspan='2'>{{ ticket.resolution }}</td>
|
||||||
</tr>{% endif %}
|
</tr>{% endif %}
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
@ -42,7 +56,7 @@
|
|||||||
{% load ticket_to_link %}
|
{% load ticket_to_link %}
|
||||||
{% for followup in ticket.followup_set.all %}
|
{% for followup in ticket.followup_set.all %}
|
||||||
<div class='followup'>
|
<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 }}
|
{{ followup.comment|markdown|num_to_link }}
|
||||||
{% if followup.ticketchange_set.all %}<div class='changes'>
|
{% if followup.ticketchange_set.all %}<div class='changes'>
|
||||||
{% for change in followup.ticketchange_set.all %}
|
{% 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>
|
<input type='radio' name='new_status' value='4' id='st_closed'><label for='st_closed'>Closed</label>
|
||||||
{% endifequal %}
|
{% endifequal %}
|
||||||
{% ifequal ticket.status 2 %}
|
{% 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='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='3' id='st_resolved'><label for='st_resolved'>Resolved</label> »
|
||||||
<input type='radio' name='new_status' value='4' id='st_closed'><label for='st_closed'>Closed</label>
|
<input type='radio' name='new_status' value='4' id='st_closed'><label for='st_closed'>Closed</label>
|
||||||
{% endifequal %}
|
{% endifequal %}
|
||||||
{% ifequal ticket.status 3 %}
|
{% 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='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='3' id='st_resolved' checked='checked'><label for='st_resolved' class='active'>Resolved</label> »
|
||||||
<input type='radio' name='new_status' value='4' id='st_closed'><label for='st_closed'>Closed</label>
|
<input type='radio' name='new_status' value='4' id='st_closed'><label for='st_closed'>Closed</label>
|
||||||
{% endifequal %}
|
{% endifequal %}
|
||||||
|
|
||||||
{% ifequal ticket.status 4 %}
|
{% 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> «
|
||||||
<input type='radio' name='new_status' value='4' id='st_closed' checked='checked'><label for='st_closed'>Closed</label>
|
<input type='radio' name='new_status' value='4' id='st_closed' checked='checked'><label for='st_closed'>Closed</label>
|
||||||
{% endifequal %}
|
{% endifequal %}
|
||||||
|
|
||||||
|
@ -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_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!' />
|
<input type='submit' value='Go!' />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ $(document).ready(function() {
|
|||||||
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></th>
|
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></th>
|
||||||
<td>{{ ticket.queue }}</td>
|
<td>{{ ticket.queue }}</td>
|
||||||
<td>{{ ticket.get_status_display }}</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>
|
<td>{{ ticket.get_assigned_to }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}{% else %}
|
{% endfor %}{% else %}
|
||||||
|
2
urls.py
2
urls.py
@ -48,7 +48,7 @@ urlpatterns = patterns('helpdesk.views',
|
|||||||
|
|
||||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$',
|
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$',
|
||||||
'update_ticket',
|
'update_ticket',
|
||||||
name='helpdesk_view'),
|
name='helpdesk_update'),
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns += patterns('',
|
urlpatterns += patterns('',
|
||||||
|
39
views.py
39
views.py
@ -67,6 +67,14 @@ def view_ticket(request, ticket_id):
|
|||||||
ticket.assigned_to = request.user
|
ticket.assigned_to = request.user
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
|
if request.GET.has_key('close') and ticket.status == Ticket.RESOLVED_STATUS:
|
||||||
|
if 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',
|
return render_to_response('helpdesk/ticket.html',
|
||||||
RequestContext(request, {
|
RequestContext(request, {
|
||||||
'ticket': ticket,
|
'ticket': ticket,
|
||||||
@ -120,6 +128,24 @@ def update_ticket(request, ticket_id):
|
|||||||
c.save()
|
c.save()
|
||||||
ticket.title = title
|
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()
|
ticket.save()
|
||||||
|
|
||||||
return HttpResponseRedirect(ticket.get_absolute_url())
|
return HttpResponseRedirect(ticket.get_absolute_url())
|
||||||
@ -148,6 +174,19 @@ def ticket_list(request):
|
|||||||
tickets = tickets.filter(status__in=statuses)
|
tickets = tickets.filter(status__in=statuses)
|
||||||
context = dict(context, statuses=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
|
### SORTING
|
||||||
sort = request.GET.get('sort', None)
|
sort = request.GET.get('sort', None)
|
||||||
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue'):
|
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue'):
|
||||||
|
Loading…
Reference in New Issue
Block a user