forked from extern/django-helpdesk
* Added ability to attach files
* Via e-mail (and replies) * Via web interface * Updated README with attachment detail * Other various bugfixes as they crop up
This commit is contained in:
parent
420e9073cf
commit
41a3199513
25
README
25
README
@ -10,6 +10,7 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
|||||||
2. Dependencies (pre-flight checklist)
|
2. Dependencies (pre-flight checklist)
|
||||||
3. Installation
|
3. Installation
|
||||||
4. Initial Configuration
|
4. Initial Configuration
|
||||||
|
5. API Usage
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
1. Licensing
|
1. Licensing
|
||||||
@ -69,6 +70,20 @@ LICENSE.JQUERY and LICENSE.NICEDIT for their respective license terms.
|
|||||||
This application assumes all helpdesk media will be accessible at
|
This application assumes all helpdesk media will be accessible at
|
||||||
http://MEDIA_PATH/helpdesk/
|
http://MEDIA_PATH/helpdesk/
|
||||||
|
|
||||||
|
7. Inside your MEDIA_ROOT folder, inside the 'helpdesk' folder, is a folder
|
||||||
|
called 'attachments'. Ensure your web server software can write to this
|
||||||
|
folder - something like this should do the trick:
|
||||||
|
|
||||||
|
chown www-data:www-data attachments/; chmod 700 attachments
|
||||||
|
(substitute www-data for the user / group that your web server runs
|
||||||
|
as, eg 'apache' or 'httpd')
|
||||||
|
|
||||||
|
If all else fails ensure all users can write to it:
|
||||||
|
|
||||||
|
chmod 777 attachments/
|
||||||
|
|
||||||
|
This is NOT recommended, especially if you're on a shared server.
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
4. Initial Configuration
|
4. Initial Configuration
|
||||||
#########################
|
#########################
|
||||||
@ -111,3 +126,13 @@ LICENSE.JQUERY and LICENSE.NICEDIT for their respective license terms.
|
|||||||
This will, on a weekly basis, create exclusions for the coming weekend.
|
This will, on a weekly basis, create exclusions for the coming weekend.
|
||||||
|
|
||||||
You're now up and running!
|
You're now up and running!
|
||||||
|
|
||||||
|
#########################
|
||||||
|
5. API Usage
|
||||||
|
#########################
|
||||||
|
|
||||||
|
Jutda Helpdesk includes an API accessible via HTTP POST requests, allowing
|
||||||
|
you to create and alter tickets from 3rd party software and systems.
|
||||||
|
|
||||||
|
For usage instructions and command syntax, see the file
|
||||||
|
templates/helpdesk/api_help.html, or visit http://helpdesk/api/help/.
|
||||||
|
40
models.py
40
models.py
@ -12,6 +12,7 @@ from datetime import datetime
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.dispatch import dispatcher
|
||||||
|
|
||||||
class Queue(models.Model):
|
class Queue(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -257,18 +258,50 @@ class TicketChange(models.Model):
|
|||||||
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):
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Based on: http://scottbarnham.com/blog/2007/07/31/uploading-images-to-a-dynamic-path-with-django/
|
||||||
|
"""
|
||||||
|
|
||||||
|
def contribute_to_class(self, cls, name):
|
||||||
|
"""Hook up events so we can access the instance."""
|
||||||
|
super(DynamicFileField, self).contribute_to_class(cls, name)
|
||||||
|
dispatcher.connect(self._post_init, models.signals.post_init, sender=cls)
|
||||||
|
|
||||||
|
def _post_init(self, instance=None):
|
||||||
|
"""Get dynamic upload_to value from the model instance."""
|
||||||
|
if hasattr(instance, 'get_upload_to'):
|
||||||
|
self.upload_to = instance.get_upload_to(self.attname)
|
||||||
|
|
||||||
|
def db_type(self):
|
||||||
|
"""Required by Django for ORM."""
|
||||||
|
return 'varchar(100)'
|
||||||
|
|
||||||
|
class Attachment(models.Model):
|
||||||
followup = models.ForeignKey(FollowUp, edit_inline=models.TABULAR)
|
followup = models.ForeignKey(FollowUp, edit_inline=models.TABULAR)
|
||||||
file = models.FileField()
|
file = DynamicFileField(upload_to='helpdesk/attachments', core=True)
|
||||||
filename = models.CharField(maxlength=100)
|
filename = models.CharField(maxlength=100)
|
||||||
mime_type = models.CharField(maxlength=30)
|
mime_type = models.CharField(maxlength=30)
|
||||||
size = models.IntegerField(help_text='Size of this file in bytes')
|
size = models.IntegerField(help_text='Size of this file in bytes')
|
||||||
|
|
||||||
|
def get_upload_to(self, field_attname):
|
||||||
|
""" Get upload_to path specific to this item """
|
||||||
|
return 'helpdesk/attachments/%s/%s' % (self.followup.ticket.ticket_for_url, self.followup.id)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'%s' % self.filename
|
return u'%s' % self.filename
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['filename',] """
|
ordering = ['filename',]
|
||||||
|
|
||||||
|
|
||||||
class PreSetReply(models.Model):
|
class PreSetReply(models.Model):
|
||||||
""" We can allow the admin to define a number of pre-set replies, used to
|
""" We can allow the admin to define a number of pre-set replies, used to
|
||||||
@ -305,3 +338,4 @@ class EscalationExclusion(models.Model):
|
|||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'%s' % self.name
|
return u'%s' % self.name
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import imaplib
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import email, mimetypes, re
|
import email, mimetypes, re
|
||||||
from email.Utils import parseaddr
|
from email.Utils import parseaddr
|
||||||
from helpdesk.models import Queue, Ticket, FollowUp
|
from helpdesk.models import Queue, Ticket, FollowUp, Attachment
|
||||||
from helpdesk.lib import send_multipart_mail
|
from helpdesk.lib import send_multipart_mail
|
||||||
|
|
||||||
def process_email():
|
def process_email():
|
||||||
@ -71,6 +71,8 @@ def ticket_from_message(message, queue):
|
|||||||
sender = message.get('from', 'Unknown Sender')
|
sender = message.get('from', 'Unknown Sender')
|
||||||
|
|
||||||
sender_email = parseaddr(message.get('from', 'Unknown Sender'))[1]
|
sender_email = parseaddr(message.get('from', 'Unknown Sender'))[1]
|
||||||
|
if sender_email.startswith('postmaster'):
|
||||||
|
sender_email = ''
|
||||||
|
|
||||||
regex = re.compile("^\[[A-Za-z0-9]+-\d+\]")
|
regex = re.compile("^\[[A-Za-z0-9]+-\d+\]")
|
||||||
if regex.match(subject):
|
if regex.match(subject):
|
||||||
@ -101,6 +103,7 @@ def ticket_from_message(message, queue):
|
|||||||
if ticket:
|
if ticket:
|
||||||
try:
|
try:
|
||||||
t = Ticket.objects.get(id=ticket)
|
t = Ticket.objects.get(id=ticket)
|
||||||
|
new = False
|
||||||
except:
|
except:
|
||||||
ticket = None
|
ticket = None
|
||||||
|
|
||||||
@ -124,50 +127,51 @@ def ticket_from_message(message, queue):
|
|||||||
priority=priority,
|
priority=priority,
|
||||||
)
|
)
|
||||||
t.save()
|
t.save()
|
||||||
|
new = True
|
||||||
|
update = ''
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'ticket': t,
|
'ticket': t,
|
||||||
'queue': queue,
|
'queue': queue,
|
||||||
}
|
}
|
||||||
|
|
||||||
update = ""
|
if new:
|
||||||
|
|
||||||
if sender_email:
|
if sender_email:
|
||||||
send_multipart_mail('helpdesk/emails/submitter_newticket', context, '%s %s' % (t.ticket, t.title), sender_email, queue.from_address)
|
send_multipart_mail('helpdesk/emails/submitter_newticket', context, '%s %s' % (t.ticket, t.title), sender_email, queue.from_address, fail_silently=True)
|
||||||
|
|
||||||
if queue.new_ticket_cc:
|
if queue.new_ticket_cc:
|
||||||
send_multipart_mail('helpdesk/emails/cc_newticket', context, '%s %s (Opened)' % (t.ticket, t.title), queue.updated_ticket_cc, queue.from_address)
|
send_multipart_mail('helpdesk/emails/cc_newticket', context, '%s %s (Opened)' % (t.ticket, t.title), queue.updated_ticket_cc, queue.from_address, fail_silently=True)
|
||||||
|
|
||||||
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
|
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
|
||||||
send_multipart_mail('helpdesk/emails/cc_newticket', context, '%s %s (Opened)' % (t.ticket, t.title), queue.updated_ticket_cc, queue.from_address)
|
send_multipart_mail('helpdesk/emails/cc_newticket', context, '%s %s (Opened)' % (t.ticket, t.title), queue.updated_ticket_cc, queue.from_address, fail_silently=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
f = FollowUp(
|
|
||||||
ticket = t,
|
|
||||||
title = 'E-Mail Received from %s' % sender_email,
|
|
||||||
date = datetime.now(),
|
|
||||||
public = True,
|
|
||||||
comment = body,
|
|
||||||
)
|
|
||||||
f.save()
|
|
||||||
update = " (Updated)"
|
update = " (Updated)"
|
||||||
|
|
||||||
if t.assigned_to:
|
if t.assigned_to:
|
||||||
send_multipart_mail('helpdesk/emails/owner_updated', context, '%s %s (Updated)' % (t.ticket, t.title), t.assigned_to.email, queue.from_address)
|
send_multipart_mail('helpdesk/emails/owner_updated', context, '%s %s (Updated)' % (t.ticket, t.title), t.assigned_to.email, queue.from_address, file_silently=True)
|
||||||
|
|
||||||
if queue.updated_ticket_cc:
|
if queue.updated_ticket_cc:
|
||||||
send_multipart_mail('helpdesk/emails/cc_updatedt', context, '%s %s (Updated)' % (t.ticket, t.title), queue.updated_ticket_cc, queue.from_address)
|
send_multipart_mail('helpdesk/emails/cc_updated', context, '%s %s (Updated)' % (t.ticket, t.title), queue.updated_ticket_cc, queue.from_address, fail_silently=True)
|
||||||
|
|
||||||
|
f = FollowUp(
|
||||||
|
ticket = t,
|
||||||
|
title = 'E-Mail Received from %s' % sender_email,
|
||||||
|
date = datetime.now(),
|
||||||
|
public = True,
|
||||||
|
comment = body,
|
||||||
|
)
|
||||||
|
f.save()
|
||||||
|
|
||||||
print " [%s-%s] %s%s" % (t.queue.slug, t.id, t.title, update)
|
print " [%s-%s] %s%s" % (t.queue.slug, t.id, t.title, update)
|
||||||
|
|
||||||
#for file in files:
|
for file in files:
|
||||||
#data = file['content']
|
filename = file['filename'].replace(' ', '_')
|
||||||
#filename = file['filename'].replace(' ', '_')
|
a = Attachment(followup=f, filename=filename, mime_type=file['type'], size=len(file['content']))
|
||||||
#type = file['type']
|
a.save_file_file(file['filename'], file['content'])
|
||||||
#a = Attachment(followup=f, filename=filename, mimetype=type, size=len(data))
|
a.save()
|
||||||
#a.save()
|
print " - %s" % file['filename']
|
||||||
#print " - %s" % file['filename']
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
process_email()
|
process_email()
|
||||||
|
@ -10,6 +10,19 @@
|
|||||||
$("#ShowFurtherOptPara").hide();
|
$("#ShowFurtherOptPara").hide();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#ShowFileUpload").click(function() {
|
||||||
|
$("#FileUpload").fadeIn();
|
||||||
|
$("#ShowFileUploadPara").hide();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".AddAnotherFile").click(function() {
|
||||||
|
$(this).hide();
|
||||||
|
$("#FileUpload>dl").append("<dt><label>Attach another File</label></dt><dd><input type='file' name='attachment' id='file' /> (<a href='#' class='AddAnotherFile'>Add Another File</a>)</dd>");
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
$('#id_preset').change(function() {
|
$('#id_preset').change(function() {
|
||||||
preset = $('#id_preset').val();
|
preset = $('#id_preset').val();
|
||||||
if (preset != '') {
|
if (preset != '') {
|
||||||
@ -71,18 +84,22 @@
|
|||||||
<div class='followup'>
|
<div class='followup'>
|
||||||
<div class='title'>{{ followup.title }} <span class='byline'>{% if followup.user %}by {{ followup.user }}{% endif %} <span title='{{ 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'>{% if followup.user %}by {{ followup.user }}{% endif %} <span title='{{ followup.date|date:"r" }}'>{{ followup.date|timesince }} ago</span>{% if not followup.public %} <span class='private'>(Private)</span>{% endif %}</span></div>
|
||||||
{% if followup.comment %}{{ followup.comment|num_to_link }}{% endif %}
|
{% if followup.comment %}{{ followup.comment|num_to_link }}{% endif %}
|
||||||
{% if followup.ticketchange_set.all %}<div class='changes'><ul>
|
|
||||||
{% for change in followup.ticketchange_set.all %}
|
{% for change in followup.ticketchange_set.all %}
|
||||||
|
{% if forloop.first %}<div class='changes'><ul>{% endif %}
|
||||||
<li>Changed {{ change.field }} from {{ change.old_value }} to {{ change.new_value }}.</li>
|
<li>Changed {{ change.field }} from {{ change.old_value }} to {{ change.new_value }}.</li>
|
||||||
|
{% if forloop.last %}</div></ul>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for attachment in followup.attachment_set.all %}{% if forloop.first %}<div class='attachments'><ul>{% endif %}
|
||||||
|
<li><a href='{{ attachment.get_file_url }}'>{{ attachment.filename }}</a> ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})</li>
|
||||||
|
{% if forloop.last %}</ul></div>{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div></ul>{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h3>Respond to this ticket</h3>
|
<h3>Respond to this ticket</h3>
|
||||||
|
|
||||||
<form method='post' action='update/'>
|
<form method='post' action='update/' enctype='multipart/form-data'>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<dl>
|
<dl>
|
||||||
@ -139,8 +156,20 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
<p id='ShowFileUploadPara'><a href='#' id='ShowFileUpload'>Attach File(s) »</a></p>
|
||||||
|
|
||||||
|
<div id='FileUpload' style='display: none;'>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
|
||||||
|
<dt><label for='id_file'>Attach a File</label></dt>
|
||||||
|
<dd><input type='file' name='attachment' id='file' /> (<a href='#' class='AddAnotherFile'>Add Another File</a>)</dd>
|
||||||
|
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<input type='submit' value='Update This Ticket' />
|
<input type='submit' value='Update This Ticket' />
|
||||||
|
|
||||||
|
15
views.py
15
views.py
@ -18,7 +18,7 @@ from django.template import loader, Context, RequestContext
|
|||||||
|
|
||||||
from helpdesk.forms import TicketForm, PublicTicketForm
|
from helpdesk.forms import TicketForm, PublicTicketForm
|
||||||
from helpdesk.lib import send_multipart_mail
|
from helpdesk.lib import send_multipart_mail
|
||||||
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply
|
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment
|
||||||
|
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
"""
|
"""
|
||||||
@ -172,7 +172,7 @@ def update_ticket(request, ticket_id):
|
|||||||
else:
|
else:
|
||||||
template = 'helpdesk/emails/submitter_updated'
|
template = 'helpdesk/emails/submitter_updated'
|
||||||
subject = '%s %s (Updated)' % (ticket.ticket, ticket.title)
|
subject = '%s %s (Updated)' % (ticket.ticket, ticket.title)
|
||||||
send_multipart_mail(template, context, subject, ticket.submitter_email, ticket.queue.from_address)
|
send_multipart_mail(template, context, subject, ticket.submitter_email, ticket.queue.from_address, fail_silently=True)
|
||||||
|
|
||||||
if ticket.assigned_to and request.user != ticket.assigned_to and ticket.assigned_to.email:
|
if ticket.assigned_to and request.user != ticket.assigned_to and ticket.assigned_to.email:
|
||||||
# We only send e-mails to staff members if the ticket is updated by
|
# We only send e-mails to staff members if the ticket is updated by
|
||||||
@ -190,7 +190,7 @@ def update_ticket(request, ticket_id):
|
|||||||
template_staff = 'helpdesk/emails/owner_updated'
|
template_staff = 'helpdesk/emails/owner_updated'
|
||||||
subject = '%s %s (Updated)' % (ticket.ticket, ticket.title)
|
subject = '%s %s (Updated)' % (ticket.ticket, ticket.title)
|
||||||
|
|
||||||
send_multipart_mail(template_staff, context, subject, ticket.assigned_to.email, ticket.queue.from_address)
|
send_multipart_mail(template_staff, context, subject, ticket.assigned_to.email, ticket.queue.from_address, fail_silently=True)
|
||||||
|
|
||||||
|
|
||||||
if ticket.queue.updated_ticket_cc:
|
if ticket.queue.updated_ticket_cc:
|
||||||
@ -207,7 +207,14 @@ def update_ticket(request, ticket_id):
|
|||||||
template_cc = 'helpdesk/emails/cc_updated'
|
template_cc = 'helpdesk/emails/cc_updated'
|
||||||
subject = '%s %s (Updated)' % (ticket.ticket, ticket.title)
|
subject = '%s %s (Updated)' % (ticket.ticket, ticket.title)
|
||||||
|
|
||||||
send_multipart_mail(template_cc, context, subject, ticket.queue.updated_ticket_cc, ticket.queue.from_address)
|
send_multipart_mail(template_cc, context, subject, ticket.queue.updated_ticket_cc, 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.save()
|
||||||
|
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user