* Enlarged Chart sizes to allow more data to be displayed

* Added superuser 'System settings' page with links to admin
* Added ability to ignore e-mail addresses (using wildcards) from the e-mail parser
* Added link to ignore email address from ticket details page (for superusers only)
* Cleaned up report output by styling text & labels in the same way as tables in other views
* Cleaned up dashboard lists to show text in place of tickets if no tickets are found
* Added ability to sort in reverse order

NOTE: REQUIRES A 'syncdb' TO CREATE THE EMAIL-IGNORE TABLES. No other DB changes were made.
This commit is contained in:
Ross Poulton 2008-10-24 22:52:34 +00:00
parent 5914e98d43
commit c97a255155
15 changed files with 273 additions and 24 deletions

View File

@ -14,7 +14,7 @@ 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
from helpdesk.models import Ticket, Queue, FollowUp, IgnoreEmail
class TicketForm(forms.Form):
queue = forms.ChoiceField(
@ -265,3 +265,7 @@ class UserSettingsForm(forms.Form):
help_text=_('If a ticket is altered by the API, do you want to receive an e-mail?'),
required=False,
)
class EmailIgnoreForm(forms.ModelForm):
class Meta:
model = IgnoreEmail

10
lib.py
View File

@ -165,8 +165,8 @@ def line_chart(data):
max = field
# Set width to '65px * number of months'.
chart_url = 'http://chart.apis.google.com/chart?cht=lc&chs=%sx90&chd=t:' % (len(column_headings)*65)
# Set width to '65px * number of months + 100 for headings.'.
chart_url = 'http://chart.apis.google.com/chart?cht=lc&chs=%sx150&chd=t:' % (len(column_headings)*65+100)
first_row = True
row_headings = []
for row in data[1:]:
@ -210,8 +210,8 @@ def bar_chart(data):
max = field
# Set width to '150px * number of months'.
chart_url = 'http://chart.apis.google.com/chart?cht=bvg&chs=%sx90&chd=t:' % (len(column_headings) * 150)
# Set width to '220px * number of months'.
chart_url = 'http://chart.apis.google.com/chart?cht=bvg&chs=%sx150&chd=t:' % (len(column_headings) * 220)
first_row = True
row_headings = []
for row in data[1:]:
@ -276,6 +276,8 @@ def apply_query(queryset, params):
queryset = queryset.filter(params['other_filter'])
if params.get('sorting', None):
if params.get('sortreverse', None):
params['sorting'] = "-%s" % params['sorting']
queryset = queryset.order_by(params['sorting'])
return queryset

View File

@ -20,10 +20,11 @@ from email.Utils import parseaddr
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.utils.translation import ugettext as _
from helpdesk.lib import send_templated_mail
from helpdesk.models import Queue, Ticket, FollowUp, Attachment
from helpdesk.models import Queue, Ticket, FollowUp, Attachment, IgnoreEmail
class Command(BaseCommand):
@ -76,9 +77,11 @@ def process_queue(q):
msgSize = msg.split(" ")[1]
full_message = "\n".join(server.retr(msgNum)[1])
ticket_from_message(message=full_message, queue=q)
ticket = ticket_from_message(message=full_message, queue=q)
if ticket:
server.dele(msgNum)
server.quit()
elif q.email_box_type == 'imap':
@ -94,8 +97,10 @@ def process_queue(q):
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)
ticket = ticket_from_message(message=data[0][1], queue=q)
if ticket:
server.store(num, '+FLAGS', '\\Deleted')
server.expunge()
server.close()
server.logout()
@ -111,8 +116,10 @@ def ticket_from_message(message, queue):
sender = message.get('from', _('Unknown Sender'))
sender_email = parseaddr(sender)[1]
if sender_email.startswith('postmaster'):
sender_email = ''
for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)):
if ignore.test(sender_email):
return False
regex = re.compile("^\[[A-Za-z0-9]+-\d+\]")
if regex.match(subject):
@ -256,6 +263,8 @@ def ticket_from_message(message, queue):
a.save()
print " - %s" % file['filename']
return ticket
if __name__ == '__main__':
process_email()

View File

@ -926,3 +926,68 @@ def create_usersettings(sender, created_models=[], instance=None, created=False,
models.signals.post_syncdb.connect(create_usersettings)
models.signals.post_save.connect(create_usersettings, sender=User)
class IgnoreEmail(models.Model):
"""
This model lets us easily ignore e-mails from certain senders when
processing IMAP and POP3 mailboxes, eg mails from postmaster or from
known trouble-makers.
"""
queues = models.ManyToManyField(
Queue,
blank=True,
null=True,
help_text=_('Leave blank for this e-mail to be ignored on all '
'queues, or select those queues you wish to ignore this e-mail '
'for.'),
)
name = models.CharField(
_('Name'),
max_length=100,
)
date = models.DateField(
_('Date'),
help_text=_('Date on which this e-mail address was added'),
blank=True,
editable=False
)
email_address = models.CharField(
_('E-Mail Address'),
max_length=150,
help_text=_('Enter a full e-mail address, or portions with '
'wildcards, eg *@domain.com or postmaster@*.'),
)
def __unicode__(self):
return u'%s' % self.name
def save(self):
if not self.date:
self.date = datetime.now()
return super(IgnoreEmail, self).save()
def test(self, email):
"""
Possible situations:
1. Username & Domain both match
2. Username is wildcard, domain matches
3. Username matches, domain is wildcard
4. username & domain are both wildcards
5. Other (no match)
1-4 return True, 5 returns False.
"""
own_parts = self.email_address.split("@")
email_parts = email.split("@")
if self.email_address == email \
or own_parts[0] == "*" and own_parts[1] == email_parts[1] \
or own_parts[1] == "*" and own_parts[0] == email_parts[0] \
or own_parts[0] == "*" and own_parts[1] == "*":
return True
else:
return False

View File

@ -28,7 +28,7 @@
{% block helpdesk_body %}{% endblock %}
</div>
<div id='footer'>
<p>{% trans "Powered by <a href='http://www.jutda.com.au/'>Jutda Helpdesk</a>." %} <a href='{% url helpdesk_rss_index %}'><img src='{{ MEDIA_URL }}/helpdesk/rss_icon.png' width='14' height='14' alt='{% trans "RSS Icon" %}' title='{% trans "RSS Feeds" %}' border='0' />{% trans "RSS Feeds" %}</a> <a href='{% url helpdesk_api_help %}'>{% trans "API" %}</a> <a href='{% url helpdesk_user_settings %}'>{% trans "User Settings" %}</a></p>
<p>{% trans "Powered by <a href='http://www.jutda.com.au/'>Jutda Helpdesk</a>." %} <a href='{% url helpdesk_rss_index %}'><img src='{{ MEDIA_URL }}/helpdesk/rss_icon.png' width='14' height='14' alt='{% trans "RSS Icon" %}' title='{% trans "RSS Feeds" %}' border='0' />{% trans "RSS Feeds" %}</a> <a href='{% url helpdesk_api_help %}'>{% trans "API" %}</a> <a href='{% url helpdesk_user_settings %}'>{% trans "User Settings" %}</a> {% if user.is_superuser %}<a href='{% url helpdesk_system_settings %}'>{% trans "System Settings" %}</a>{% endif %}</p>
</div>
</div>
{% include "helpdesk/debug.html" %}

View File

@ -5,7 +5,7 @@
{% endblock %}
{% block helpdesk_body %}
<table width='30%' align='left'>
<table width='40%' align='left'>
<tr class='row_tablehead'><td colspan='4'>{% trans "Helpdesk Summary" %}</td></tr>
<tr class='row_columnheads'><th>{% trans "Queue" %}</th><th>{% trans "Open" %}</th><th>{% trans "Resolved" %}</th></tr>
{% for queue in dash_tickets %}
@ -17,7 +17,7 @@
{% endfor %}
</table>
<p style='padding-left: 5px;'>Welcome to your Helpdesk Dashboard! From here you can quickly see your own tickets, and those tickets that have no owner. Why not pick up an orphan ticket and sort it out for a customer?</p>
<p style='margin-left: 42%;'>Welcome to your Helpdesk Dashboard! From here you can quickly see your own tickets, and those tickets that have no owner. Why not pick up an orphan ticket and sort it out for a customer?</p>
<br style='clear: both;' />
@ -34,6 +34,9 @@
<td><span title='{{ ticket.modified|date:"r" }}'>{{ ticket.modified|timesince }}</span></td>
</tr>
{% endfor %}
{% if not unassigned_tickets %}
<tr class='row_odd'><td colspan='5'>{% trans "You have no tickets assigned to you." %}</td></tr>
{% endif %}
</table>
<table width='100%'>
@ -49,6 +52,9 @@
<th><a href='{{ ticket.get_absolute_url }}?take'><span class='button button_take'>{% trans "Take" %}</span></a></th>
</tr>
{% endfor %}
{% if not unassigned_tickets %}
<tr class='row_odd'><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr>
{% endif %}
</table>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "helpdesk/base.html" %}{% load i18n %}
{% block helpdesk_title %}{% trans "Ignore E-Mail Address" %}{% endblock %}
{% block helpdesk_body %}{% blocktrans %}
<h2>Ignore E-Mail Address</h2>
<p>To ignore an e-mail address and prevent any emails from that address creating tickets automatically, enter the e-mail address below.</p>
<p>You can either enter a whole e-mail address such as <em>email@domain.com</em> or a portion of an e-mail address with a wildcard, such as <em>*@domain.com</em> or <em>user@*</em>.</p>{% endblocktrans %}
<form method='post' action='./'>
<fieldset>
<dl>{% for field in form %}
<dt><label for='id_{{ field.name }}'>{{ field.label }}</label></dt>
<dd>{{ field }}</dd>
{% if field.errors %}<dd class='error'>{{ field.errors }}</dd>{% endif %}
{% if field.help_text %}<dd class='form_help_text'>{{ field.help_text }}</dd>{% endif %}
{% endfor %}</dl>
</fieldset>
<input type='submit' value='{% trans "Ignore E-Mail Address" %}' />
</form>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "helpdesk/base.html" %}{% load i18n %}
{% block helpdesk_title %}{% trans "Delete Ignored E-Mail Address" %}{% endblock %}
{% block helpdesk_body %}{% blocktrans with ignore.email_address as email_address %}
<h2>Un-Ignore E-Mail Address</h2>
<p>Are you sure you wish to stop removing this email address (<em>{{ email_address }}</em>) and allow their e-mails to automatically create tickets in your system? You can re-add this e-mail address at any time.<?p>
{% endblocktrans %}
{% blocktrans %}<p><a href='../../'>Keep Ignoring It</a></p>
<form method='post' action='./'><input type='submit' value='Stop Ignoring It' /></form>
{% endblocktrans %}{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "helpdesk/base.html" %}{% load i18n %}
{% block helpdesk_title %}{% trans "Ignored E-Mail Addresses" %}{% endblock %}
{% block helpdesk_body %}{% blocktrans %}
<h2>Ignored E-Mail Addresses</h2>
<p>The following e-mail addresses are currently being ignored by the incoming e-mail processor. You can <a href='add/'>add a new e-mail address to the list</a> or delete any of the items below as required.</p>{% endblocktrans %}
<table width='100%'>
<thead>
<tr class='row_tablehead'><td colspan='5'>{% trans "Ignored E-Mail Addresses" %}</td></tr>
<tr class='row_columnheads'><th>{% trans "Name" %}</th><th>{% trans "E-Mail Address" %}</th><th>{% trans "Date Added" %}</th><th>{% trans "Queues" %}</th><th>{% trans "Delete" %}</th></tr>
</thead>
<tbody>
{% for ignore in ignore_list %}
<tr class='row_{% cycle odd,even %}'>
<td>{{ ignore.name }}</td>
<td>{{ ignore.email_address }}</td>
<td>{{ ignore.date }}</td>
<td>{% for queue in ignore.queues.all %}{{ queue.slug }}{% if not forloop.last %}, {% endif %}{% endfor %}{% if not ignore.queues.all %}All{% endif %}</td>
<td><a href='{% url helpdesk_email_ignore_del ignore.id %}'>Delete</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -6,8 +6,10 @@
<h2>{% trans "Reports &amp; Statistics" %}</h2>
<table>
<tr>{% for h in headings %}<th>{{ h }}</th>{% endfor %}</tr>
{% for d in data %}<tr>{% for f in d %}<td>{{ f }}</td>{% endfor %}</tr>{% endfor %}
<tr class='row_tablehead'><td colspan='{{ headings|length }}'>{{ title }}</td></tr>
<tr class='row_columnheads'>{% for h in headings %}<th>{% if forloop.first %}{{ h|title }}{% else %}{{ h }}{% endif %}</th>{% endfor %}</tr>
{% for d in data %}
<tr class='row_{% cycle odd,even %}'>{% for f in d %}<td>{{ f }}</td>{% endfor %}</tr>{% endfor %}
</table>
{% if chart %}<img src='{{ chart }}' />{% endif %}

View File

@ -0,0 +1,19 @@
{% extends "helpdesk/base.html" %}{% load i18n %}
{% block helpdesk_title %}{% trans "Change System Settings" %}{% endblock %}
{% block helpdesk_body %}{% blocktrans %}
<h2>System Settings</h2>
<p>The following items can be maintained by you or other superusers:</p>{% endblocktrans %}
<ul>
<li><a href='../ignore/'>{% trans "E-Mail Ignore list" %}</a></li>
<li><a href='/admin/helpdesk/queue/'>{% trans "Maintain Queues" %}</a></li>
<li><a href='/admin/helpdesk/presetreply/'>{% trans "Maintain Pre-Set Replies" %}</a></li>
<li><a href='/admin/helpdesk/kbcategory/'>{% trans "Maintain Knowledgebase Categories" %}</a></li>
<li><a href='/admin/helpdesk/kbitem/'>{% trans "Maintain Knowledgebase Items" %}</a></li>
<li><a href='/admin/helpdesk/emailtemplate/'>{% trans "Maintain E-Mail Templates" %}</a></li>
<li><a href='/admin/auth/user/'>{% trans "Maintain Users" %}</a></li>
</ul>
{% endblock %}

View File

@ -60,7 +60,7 @@
<tr class='row_odd'>
<th>{% trans "Submitter E-Mail" %}</th>
<td>{{ ticket.submitter_email }}</td>
<td>{{ ticket.submitter_email }}{% if user.is_superuser %} <strong><a href='{% url helpdesk_email_ignore_add %}?email={{ ticket.submitter_email }}'>{% trans "Ignore" %}</a></strong>{% endif %}</td>
</tr>
<tr class='row_even'>

View File

@ -43,6 +43,7 @@ $(document).ready(function() {
<option value='priority'{% ifequal query_params.sorting "priority"%} selected='selected'{% endifequal %}>{% trans "Priority" %}</option>
<option value='assigned_to'{% ifequal query_params.sorting "assigned_to"%} selected='selected'{% endifequal %}>{% trans "Owner" %}</option>
</select>
<label for='id_sortreverse'>Reverse</label><input type='checkbox' name='sortreverse' id='id_sortreverse'{% if query_params.sortreverse %} checked='checked'{% endif %} />
<p class='filterHelp'>Ordering applied to tickets</p>
<input type='button' class='filterBuilderRemove' value='-' />
</div>

17
urls.py
View File

@ -73,6 +73,18 @@ urlpatterns = patterns('helpdesk.views.staff',
url(r'^settings/$',
'user_settings',
name='helpdesk_user_settings'),
url(r'^ignore/$',
'email_ignore',
name='helpdesk_email_ignore'),
url(r'^ignore/add/$',
'email_ignore_add',
name='helpdesk_email_ignore_add'),
url(r'^ignore/delete/(?P<id>[0-9]+)/$',
'email_ignore_del',
name='helpdesk_email_ignore_del'),
)
urlpatterns += patterns('helpdesk.views.public',
@ -129,4 +141,9 @@ urlpatterns += patterns('',
'django.views.generic.simple.direct_to_template',
{'template': 'helpdesk/help_context.html',},
name='helpdesk_help_context'),
url(r'^system_settings/$',
'django.views.generic.simple.direct_to_template',
{'template': 'helpdesk/system_settings.html',},
name='helpdesk_system_settings'),
)

View File

@ -10,7 +10,7 @@ views/staff.py - The bulk of the application - provides most business logic and
from datetime import datetime
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.files.base import ContentFile
from django.core.urlresolvers import reverse
from django.db import connection
@ -20,9 +20,14 @@ from django.shortcuts import render_to_response, get_object_or_404
from django.template import loader, Context, RequestContext
from django.utils.translation import ugettext as _
from helpdesk.forms import TicketForm, UserSettingsForm
from helpdesk.forms import TicketForm, UserSettingsForm, EmailIgnoreForm
from helpdesk.lib import send_templated_mail, line_chart, bar_chart, query_to_dict, apply_query, safe_template_context
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch, IgnoreEmail
staff_member_required = user_passes_test(lambda u: u.is_authenticated() and u.is_active and u.is_staff)
superuser_required = user_passes_test(lambda u: u.is_authenticated() and u.is_active and u.is_superuser)
def dashboard(request):
@ -199,7 +204,7 @@ def update_ticket(request, ticket_id):
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))):
if ticket.submitter_email and public and (f.comment or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))):
context = {
'ticket': ticket,
'queue': ticket.queue,
@ -287,6 +292,7 @@ def ticket_list(request):
query_params = {
'filtering': {},
'sorting': None,
'sortreverse': False,
'keyword': None,
'other_filter': None,
}
@ -308,7 +314,8 @@ def ticket_list(request):
or request.GET.has_key('assigned_to')
or request.GET.has_key('status')
or request.GET.has_key('q')
or request.GET.has_key('sort') ):
or request.GET.has_key('sort')
or request.GET.has_key('sortreverse') ):
# Fall-back if no querying is being done, force the list to only
# show open/reopened/resolved (not closed) cases sorted by creation
@ -354,6 +361,9 @@ def ticket_list(request):
sort = 'created'
query_params['sorting'] = sort
sortreverse = request.GET.get('sortreverse', None)
query_params['sortreverse'] = sortreverse
tickets = apply_query(Ticket.objects.select_related(), query_params)
import cPickle, base64
@ -555,30 +565,37 @@ def run_report(request, report):
if report == 'userpriority':
sql = user_base_sql % priority_sql
columns = ['username'] + priority_columns
title = 'User by Priority'
elif report == 'userqueue':
sql = user_base_sql % queue_sql
columns = ['username'] + queue_columns
title = 'User by Queue'
elif report == 'userstatus':
sql = user_base_sql % status_sql
columns = ['username'] + status_columns
title = 'User by Status'
elif report == 'usermonth':
sql = user_base_sql % month_sql
columns = ['username'] + month_columns
title = 'User by Month'
elif report == 'queuepriority':
sql = queue_base_sql % priority_sql
columns = ['queue'] + priority_columns
title = 'Queue by Priority'
elif report == 'queuestatus':
sql = queue_base_sql % status_sql
columns = ['queue'] + status_columns
title = 'Queue by Status'
elif report == 'queuemonth':
sql = queue_base_sql % month_sql
columns = ['queue'] + month_columns
title = 'Queue by Month'
cursor = connection.cursor()
@ -604,8 +621,8 @@ def run_report(request, report):
RequestContext(request, {
'headings': columns,
'data': data,
'sql': sql,
'chart': chart_url,
'title': title,
}))
run_report = login_required(run_report)
@ -638,6 +655,7 @@ def delete_saved_query(request, id):
}))
delete_saved_query = login_required(delete_saved_query)
def user_settings(request):
s = request.user.usersettings
if request.POST:
@ -653,3 +671,40 @@ def user_settings(request):
'form': form,
}))
user_settings = login_required(user_settings)
def email_ignore(request):
return render_to_response('helpdesk/email_ignore_list.html',
RequestContext(request, {
'ignore_list': IgnoreEmail.objects.all(),
}))
email_ignore = superuser_required(email_ignore)
def email_ignore_add(request):
if request.method == 'POST':
form = EmailIgnoreForm(request.POST)
if form.is_valid():
ignore = form.save()
return HttpResponseRedirect(reverse('helpdesk_email_ignore'))
else:
form = EmailIgnoreForm(request.GET)
return render_to_response('helpdesk/email_ignore_add.html',
RequestContext(request, {
'form': form,
}))
email_ignore_add = superuser_required(email_ignore_add)
def email_ignore_del(request, id):
ignore = get_object_or_404(IgnoreEmail, id=id)
if request.method == 'POST':
ignore.delete()
return HttpResponseRedirect(reverse('helpdesk_email_ignore'))
else:
return render_to_response('helpdesk/email_ignore_del.html',
RequestContext(request, {
'ignore': ignore,
}))
email_ignore_del = superuser_required(email_ignore_del)