* Add localisation hooks

* Add VERY basic knowledgebase functions
* Restructure views.py into views/*.py
This commit is contained in:
Ross Poulton 2008-05-21 21:16:44 +00:00
parent dfb821336e
commit 23084499c1
17 changed files with 315 additions and 86 deletions

View File

@ -33,6 +33,7 @@ table {
margin: 2px;
border: solid #444 1px;
color: #000;
background-color: #eee;
text-decoration: none;
font-size: 10pt;
line-height: 12pt;

View File

@ -29,6 +29,8 @@ class Queue(models.Model):
title = models.CharField(_('Title'), max_length=100)
slug = models.SlugField(_('Slug'), help_text=_('This slug is used when building ticket ID\'s. Once set, try not to change it or e-mailing may get messy.'))
email_address = models.EmailField(_('E-Mail Address'), blank=True, null=True, help_text=_('All outgoing e-mails for this queue will use this e-mail address. If you use IMAP or POP3, this should be the e-mail address for that mailbox.'))
allow_public_submission = models.BooleanField(_('Allow Public Submission?'), blank=True, null=True, help_text=_('Should this queue be listed on the public submission form?'))
allow_email_submission = models.BooleanField(_('Allow E-Mail Submission?'), blank=True, null=True, help_text=_('Do you want to poll the e-mail box below for new tickets?'))
escalate_days = models.IntegerField(_('Escalation Days'), blank=True, null=True, help_text=_('For tickets which are not held, how often do you wish to increase their priority? Set to 0 for no escalation.'))
def _from_address(self):
@ -180,7 +182,7 @@ class Ticket(models.Model):
return u'%s' % self.title
def get_absolute_url(self):
return ('helpdesk.views.view_ticket', [str(self.id)])
return ('helpdesk_view', [str(self.id)])
get_absolute_url = models.permalink(get_absolute_url)
def save(self):
@ -364,3 +366,65 @@ class EmailTemplate(models.Model):
class Meta:
ordering = ['template_name',]
class KBCategory(models.Model):
"""
Lets help users help themselves: the Knowledge Base is a categorised
listing of questions & answers.
"""
title = models.CharField(_('Title'), max_length=100)
slug = models.SlugField(_('Slug'))
description = models.TextField(_('Description'))
def __unicode__(self):
return u'%s' % self.title
class Admin:
pass
class Meta:
ordering = ['title',]
def get_absolute_url(self):
return ('helpdesk_kb_category', [str(self.slug)])
get_absolute_url = models.permalink(get_absolute_url)
class KBItem(models.Model):
"""
An item within the knowledgebase. Very straightforward question/answer
style system.
"""
category = models.ForeignKey(KBCategory)
title = models.CharField(_('Title'), max_length=100)
question = models.TextField(_('Question'))
answer = models.TextField(_('Answer'))
votes = models.IntegerField(_('Votes'), help_text=_('Total number of votes cast for this item'))
recommendations = models.IntegerField(_('Positive Votes'), help_text=_('Number of votes for this item which were POSITIVE.'))
last_updated = models.DateTimeField(_('Last Updated'))
def save(self):
self.last_updated = datetime.now()
return super(KBItem, self).save()
def _score(self):
if self.votes > 0:
return int(self.recommendations / self.votes)
else:
return _('Unrated')
score = property(_score)
def __unicode__(self):
return u'%s' % self.title
class Admin:
pass
class Meta:
ordering = ['title',]
def get_absolute_url(self):
return ('helpdesk_kb_item', [str(self.id)])
get_absolute_url = models.permalink(get_absolute_url)

View File

@ -18,7 +18,7 @@ from helpdesk.models import Queue, Ticket, FollowUp, Attachment
from helpdesk.lib import send_templated_mail
def process_email():
for q in Queue.objects.filter(email_box_type__isnull=False):
for q in Queue.objects.filter(email_box_type__isnull=False, allow_email_submission=True):
if not q.email_box_last_check: q.email_box_last_check = datetime.now()-timedelta(minutes=30)
if not q.email_box_interval: q.email_box_interval = 0

View File

@ -14,7 +14,7 @@
<div id='header'>
<h1>{% trans "Helpdesk" %}</h1>
<ul>
<li><a href='{% url helpdesk_home %}'>{% trans "Dashboard" %}</a></li>
<li><a href='{% url helpdesk_dashboard %}'>{% trans "Dashboard" %}</a></li>
<li><a href='{% url helpdesk_list %}'>{% trans "Tickets" %}</a></li>
<li><a href='{% url helpdesk_submit %}'>{% trans "New Ticket" %}</a></li>
<li><a href='{% url helpdesk_report_index %}'>{% trans "Stats" %}</a></li>

View File

@ -0,0 +1,20 @@
{% extends "helpdesk/public_base.html" %}{% load i18n %}
{% block helpdesk_body %}
<h2>{% blocktrans with category.title as kbcat %}Knowledgebase Category: {{ kbcat }}{% endblocktrans %}</h2>
<p>{% blocktrans with category.title as kbcat %}You are viewing all items in the {{ kbcat }} category.{% endblocktrans %}</p>
<p>{{ category.description }}</p>
<table width='100%'>
<tr class='row_tablehead'><td colspan='3'>{% blocktrans with category.title as kbcat %}Knowledgebase Category: {{ kbcat }}{% endblocktrans %}</td></tr>
<tr class='row_columnheads'><th colspan='3'>{% trans "Article" %}</th></tr>
{% for item in items %}
<tr class='row_even row_hover'><th><a href='{{ item.get_absolute_url }}'>{{ item.title }}</a></th><td>{{ item.score }}</td><td>{{ item.last_updated|timesince }}</td></tr>
<tr class='row_odd'><td colspan='3'>{{ item.question }}</td></tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "helpdesk/public_base.html" %}{% load i18n %}
{% block helpdesk_body %}
<h2>{% trans "Knowledgebase" %}</h2>
<p>{% trans "We have listed a number of knowledgebase articles for your perusal in the following categories. Please check to see if any of these articles address your problem prior to opening a support ticket." %}</p>
<table width='100%'>
<tr class='row_tablehead'><td>{% trans "Knowledgebase Categories" %}</td></tr>
<tr class='row_columnheads'><th>{% trans "Category" %}</th></tr>
{% for category in categories %}
<tr class='row_even row_hover'><th><a href='{{ category.get_absolute_url }}'>{{ category.title }}</a></th></tr>
<tr class='row_odd'><td>{{ category.description }}</td></tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends "helpdesk/public_base.html" %}{% load i18n %}
{% block helpdesk_body %}
<h2>{% blocktrans with item.title as item %}Knowledgebase: {{ item }}{% endblocktrans %}</h2>
<table width='100%'>
<tr class='row_tablehead'><td>{{ item.title }}</td></tr>
<tr class='row_even row_hover'><th>{{ item.question }}</th></tr>
<tr class='row_odd'><td>{{ item.answer }}</td></tr>
</table>
<p>{% blocktrans with item.category.title as category_title and item.category.get_absolute_url as category_url %}View <a href='{{ category_url }}'>other <em>{{ category_title }}</em> articles</a>, or continue <a href='../'>viewing other knowledgebase articles</a>.{% endblocktrans %}</p>
<h2>{% trans "Feedback" %}</h2>
<p>{% trans "We give our users an opportunity to vote for items that they believe have helped them out, in order for us to better serve future customers. We would appreciate your feedback on this article. Did you find it useful?" %}</p>
<ul>
<li><a href='vote/?vote=up'>{% trans "This article was useful to me" %}</a></li>
<li><a href='vote/?vote=down'>{% trans "This article was <strong>not</strong> useful to me" %}</a></li>
</ul>
<p>{% trans "The results of voting by other readers of this article are below:" %}</p>
<ul>
<li>{% blocktrans with item.recommendations as recommendations %}Recommendations: {{ recommendations }}{% endblocktrans %}</li>
<li>{% blocktrans with item.votes as votes %}Votes: {{ votes }}{% endblocktrans %}</li>
<li>{% blocktrans with item.score as score %}Overall Rating: {{ score }}{% endblocktrans %}</li>
</ul>
{% endblock %}

View File

@ -11,6 +11,7 @@
<h1>{% trans "Helpdesk" %}</h1>
<ul>
<li><a href='{% url helpdesk_home %}'>{% trans "Submit A Ticket" %}</a></li>
<li><a href='{% url helpdesk_kb_index %}'>{% trans "Knowledgebase" %}</a></li>
<li><a href='{% url login %}'>{% trans "Log In" %}</a></li>
</ul>
</div>

View File

@ -16,7 +16,7 @@
<dd><input type='text' name='email' /></dd>
</dl>
<input type='submit' value='{% "View Ticket" %}' />
<input type='submit' value='{% trans "View Ticket" %}' />
</fieldset>
</form>

View File

@ -93,7 +93,7 @@
{% if followup.comment %}{{ followup.comment|num_to_link|safe }}{% endif %}
{% for change in followup.ticketchange_set.all %}
{% if forloop.first %}<div class='changes'><ul>{% endif %}
<li>{% blocktrans %}Changed {{ change.field }} from {{ change.old_value }} to {{ change.new_value }}.{% endblocktrans %}</li>
<li>{% blocktrans with change.field as field and change.old_value as old_value and change.new_value as new_value %}Changed {{ field }} from {{ old_value }} to {{ new_value }}.{% endblocktrans %}</li>
{% if forloop.last %}</div></ul>{% endif %}
{% endfor %}
{% for attachment in followup.attachment_set.all %}{% if forloop.first %}<div class='attachments'><ul>{% endif %}
@ -116,7 +116,7 @@
<dt><label for='commentBox'>{% trans "Comment / Resolution" %}</label></dt>
<dd><textarea rows='8' cols='70' name='comment' id='commentBox'></textarea></dd>
<dd class='form_help_text'>{% trans "You can use the ticket and queue template variables in your message." %}</dd>
<dd class='form_help_text'>{% trans "You can use the {{ ticket }} and {{ queue }} template variables in your message." %}</dd>
<dt><label>{% trans "New Status" %}</label></dt>
{% ifequal ticket.status 1 %}

42
urls.py
View File

@ -11,14 +11,14 @@ from django.conf.urls.defaults import *
from django.contrib.auth.decorators import login_required
from feeds import feed_setup
from helpdesk.views.feeds import feed_setup
from django.contrib.syndication.views import feed as django_feed
urlpatterns = patterns('helpdesk.views',
url(r'^$',
urlpatterns = patterns('helpdesk.views.staff',
url(r'^dashboard/$',
'dashboard',
name='helpdesk_home'),
name='helpdesk_dashboard'),
url(r'^tickets/$',
'ticket_list',
@ -51,10 +51,6 @@ urlpatterns = patterns('helpdesk.views',
url(r'^raw/(?P<type>\w+)/$',
'raw_details',
name='helpdesk_raw'),
url(r'^view/$',
'public_view',
name='helpdesk_public_view'),
url(r'^rss/$',
'rss_list',
@ -69,16 +65,24 @@ urlpatterns = patterns('helpdesk.views',
name='helpdesk_run_report'),
)
urlpatterns += patterns('helpdesk.views.public',
url(r'^$',
'homepage',
name='helpdesk_home'),
url(r'^view/$',
'view_ticket',
name='helpdesk_public_view'),
)
urlpatterns += patterns('',
url(r'^rss/(?P<url>.*)/$',
login_required(django_feed),
{'feed_dict': feed_setup},
name='helpdesk_rss'),
)
urlpatterns += patterns('',
url(r'^api/(?P<method>[a-z_-]+)/$',
'helpdesk.api.api',
'helpdesk.views.api.api',
name='helpdesk_api'),
url(r'^api/$',
@ -94,3 +98,17 @@ urlpatterns += patterns('',
'django.contrib.auth.views.logout',
name='logout'),
)
urlpatterns += patterns('helpdesk.views.kb',
url(r'^kb/$',
'index', name='helpdesk_kb_index'),
url(r'^kb/(?P<slug>[A-Za-z_-]+)/$',
'category', name='helpdesk_kb_category'),
url(r'^kb/(?P<item>[0-9]+)/$',
'item', name='helpdesk_kb_item'),
url(r'^kb/(?P<item>[0-9]+)/vote/$',
'vote', name='helpdesk_kb_vote'),
)

0
views/__init__.py Normal file
View File

View File

@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse
from django.db.models import Q
from django.utils.translation import ugettext as _
from models import Ticket, FollowUp, Queue
from helpdesk.models import Ticket, FollowUp, Queue
class OpenTicketsByUser(Feed):

52
views/kb.py Normal file
View File

@ -0,0 +1,52 @@
"""
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
simple categorised question/answer system to show common
resolutions to common problems.
"""
from datetime import datetime
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django.utils.translation import ugettext as _
from helpdesk.models import KBCategory, KBItem
def index(request):
category_list = KBCategory.objects.all()
# Add most popular items here.
return render_to_response('helpdesk/kb_index.html',
RequestContext(request, {
'categories': category_list,
}))
def category(request, slug):
category = get_object_or_404(KBCategory, slug__iexact=slug)
items = category.kbitem_set.all()
return render_to_response('helpdesk/kb_category.html',
RequestContext(request, {
'category': category,
'items': items,
}))
def item(request, item):
item = get_object_or_404(KBItem, pk=item)
return render_to_response('helpdesk/kb_item.html',
RequestContext(request, {
'item': item,
}))
def vote(request, item):
item = get_object_or_404(KBItem, pk=item)
vote = request.GET.get('vote', None)
if vote in ('up', 'down'):
item.votes += 1
if vote == 'up':
item.recommendations += 1
item.save()
return HttpResponseRedirect(item.get_absolute_url())

60
views/public.py Normal file
View File

@ -0,0 +1,60 @@
"""
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
views/public.py - All public facing views, eg non-staff (no authentication
required) views.
"""
from datetime import datetime
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, Http404, HttpResponse
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 PublicTicketForm
from helpdesk.lib import send_templated_mail
from helpdesk.models import Ticket, Queue
def homepage(request):
if request.user.is_authenticated():
return HttpResponseRedirect(reverse('helpdesk_dashboard'))
if request.method == 'POST':
form = PublicTicketForm(request.POST)
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
if form.is_valid():
ticket = form.save()
return HttpResponseRedirect('%s?ticket=%s&email=%s'% (reverse('helpdesk_public_view'), ticket.ticket_for_url, ticket.submitter_email))
else:
form = PublicTicketForm()
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
return render_to_response('helpdesk/public_homepage.html',
RequestContext(request, {
'form': form,
}))
def view_ticket(request):
ticket = request.GET.get('ticket', '')
email = request.GET.get('email', '')
error_message = ''
if ticket and email:
try:
queue, ticket_id = ticket.split('-')
t = Ticket.objects.get(id=ticket_id, queue__slug__iexact=queue, submitter_email__iexact=email)
return render_to_response('helpdesk/public_view_ticket.html',
RequestContext(request, {'ticket': t,}))
except:
t = False;
error_message = _('Invalid ticket ID or e-mail address. Please try again.')
return render_to_response('helpdesk/public_view_form.html',
RequestContext(request, {
'ticket': ticket,
'email': email,
'error_message': error_message,
}))

View File

@ -3,8 +3,8 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
views.py - The bulk of the application - provides most business logic and
renders all user-facing views.
views/staff.py - The bulk of the application - provides most business logic and
renders all staff-facing views.
"""
from datetime import datetime
@ -17,57 +17,42 @@ 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, PublicTicketForm
from helpdesk.forms import TicketForm
from helpdesk.lib import send_templated_mail, line_chart, bar_chart, query_to_dict
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment
def dashboard(request):
"""
This isn't always truly a "dashboard" view. If the user is not logged in,
we instead show the user a "public submission" form and a way to view
existing tickets.
A quick summary overview for users: A list of their own tickets, a table
showing ticket counts by queue/status, and a list of unassigned tickets
with options for them to 'Take' ownership of said tickets.
"""
if request.user.is_authenticated():
tickets = Ticket.objects.filter(assigned_to=request.user).exclude(status=Ticket.CLOSED_STATUS)
unassigned_tickets = Ticket.objects.filter(assigned_to__isnull=True).exclude(status=Ticket.CLOSED_STATUS)
from django.db import connection
cursor = connection.cursor()
cursor.execute("""
SELECT q.id as queue,
q.title AS name,
COUNT(CASE t.status WHEN '1' THEN t.id WHEN '2' THEN t.id END) AS open,
COUNT(CASE t.status WHEN '3' THEN t.id END) AS resolved
FROM helpdesk_ticket t,
helpdesk_queue q
WHERE q.id = t.queue_id
GROUP BY queue, name
ORDER BY q.id;
""")
dash_tickets = query_to_dict(cursor.fetchall(), cursor.description)
return render_to_response('helpdesk/dashboard.html',
RequestContext(request, {
'user_tickets': tickets,
'unassigned_tickets': unassigned_tickets,
'dash_tickets': dash_tickets,
}))
else:
# Not a logged in user
if request.method == 'POST':
form = PublicTicketForm(request.POST)
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()]
if form.is_valid():
ticket = form.save()
return HttpResponseRedirect('%s?ticket=%s&email=%s'% (reverse('helpdesk_public_view'), ticket.ticket_for_url, ticket.submitter_email))
else:
form = PublicTicketForm()
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()]
tickets = Ticket.objects.filter(assigned_to=request.user).exclude(status=Ticket.CLOSED_STATUS)
unassigned_tickets = Ticket.objects.filter(assigned_to__isnull=True).exclude(status=Ticket.CLOSED_STATUS)
return render_to_response('helpdesk/public_homepage.html',
RequestContext(request, {
'form': form,
}))
from django.db import connection
cursor = connection.cursor()
cursor.execute("""
SELECT q.id as queue,
q.title AS name,
COUNT(CASE t.status WHEN '1' THEN t.id WHEN '2' THEN t.id END) AS open,
COUNT(CASE t.status WHEN '3' THEN t.id END) AS resolved
FROM helpdesk_ticket t,
helpdesk_queue q
WHERE q.id = t.queue_id
GROUP BY queue, name
ORDER BY q.id;
""")
dash_tickets = query_to_dict(cursor.fetchall(), cursor.description)
return render_to_response('helpdesk/dashboard.html',
RequestContext(request, {
'user_tickets': tickets,
'unassigned_tickets': unassigned_tickets,
'dash_tickets': dash_tickets,
}))
dashboard = login_required(dashboard)
def delete_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id)
@ -303,28 +288,6 @@ def raw_details(request, type):
raise Http404
raw_details = login_required(raw_details)
def public_view(request):
ticket = request.GET.get('ticket', '')
email = request.GET.get('email', '')
error_message = ''
if ticket and email:
queue, ticket_id = ticket.split('-')
try:
t = Ticket.objects.get(id=ticket_id, queue__slug__iexact=queue, submitter_email__iexact=email)
return render_to_response('helpdesk/public_view_ticket.html',
RequestContext(request, {'ticket': t,}))
except:
t = False;
error_message = _('Invalid ticket ID or e-mail address. Please try again.')
return render_to_response('helpdesk/public_view_form.html',
RequestContext(request, {
'ticket': ticket,
'email': email,
'error_message': error_message,
}))
def hold_ticket(request, ticket_id, unhold=False):
ticket = get_object_or_404(Ticket, id=ticket_id)