mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-11-21 23:43:11 +01:00
* Large change to clean up the codebase: Decrease excess whitespace at ends
of lines; Increase line-wrapping of commands to limit code to 80 columns wherever possible * Re-built 'en' locale to match some new strings * Clean up import statements somewhat
This commit is contained in:
parent
ef25b571db
commit
5040d3d243
247
forms.py
247
forms.py
@ -1,50 +1,73 @@
|
||||
""" ..
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
forms.py - Definitions of newforms-based forms for creating and maintaining
|
||||
forms.py - Definitions of newforms-based forms for creating and maintaining
|
||||
tickets.
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from helpdesk.models import Ticket, Queue, FollowUp
|
||||
from django.contrib.auth.models import User
|
||||
from datetime import datetime
|
||||
|
||||
from django import forms
|
||||
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
|
||||
|
||||
class TicketForm(forms.Form):
|
||||
queue = forms.ChoiceField(label=_('Queue'), required=True, choices=())
|
||||
queue = forms.ChoiceField(
|
||||
label=_('Queue'),
|
||||
required=True,
|
||||
choices=()
|
||||
)
|
||||
|
||||
title = forms.CharField(max_length=100, required=True,
|
||||
widget=forms.TextInput(),
|
||||
label=_('Summary of the problem'))
|
||||
|
||||
submitter_email = forms.EmailField(required=False,
|
||||
label=_('Submitter E-Mail Address'),
|
||||
help_text=_('This e-mail address will receive copies of all public updates to this ticket.'))
|
||||
|
||||
body = forms.CharField(widget=forms.Textarea(),
|
||||
label=_('Description of Issue'), required=True)
|
||||
|
||||
assigned_to = forms.ChoiceField(choices=(), required=False,
|
||||
label=_('Case owner'),
|
||||
help_text=_('If you select an owner other than yourself, they\'ll be e-mailed details of this ticket immediately.'))
|
||||
title = forms.CharField(
|
||||
max_length=100,
|
||||
required=True,
|
||||
widget=forms.TextInput(),
|
||||
label=_('Summary of the problem'),
|
||||
)
|
||||
|
||||
submitter_email = forms.EmailField(
|
||||
required=False,
|
||||
label=_('Submitter E-Mail Address'),
|
||||
help_text=_('This e-mail address will receive copies of all public '
|
||||
'updates to this ticket.'),
|
||||
)
|
||||
|
||||
body = forms.CharField(
|
||||
widget=forms.Textarea(),
|
||||
label=_('Description of Issue'),
|
||||
required=True,
|
||||
)
|
||||
|
||||
assigned_to = forms.ChoiceField(
|
||||
choices=(),
|
||||
required=False,
|
||||
label=_('Case owner'),
|
||||
help_text=_('If you select an owner other than yourself, they\'ll be '
|
||||
'e-mailed details of this ticket immediately.'),
|
||||
)
|
||||
|
||||
priority = forms.ChoiceField(
|
||||
choices=Ticket.PRIORITY_CHOICES,
|
||||
required=False,
|
||||
initial='3',
|
||||
label=_('Priority'),
|
||||
help_text=_('Please select a priority carefully. If unsure, leave it '
|
||||
'as \'3\'.'),
|
||||
)
|
||||
|
||||
priority = forms.ChoiceField(choices=Ticket.PRIORITY_CHOICES,
|
||||
required=False,
|
||||
initial='3',
|
||||
label=_('Priority'),
|
||||
help_text=_('Please select a priority carefully. If unsure, leave it as \'3\'.'))
|
||||
|
||||
def save(self, user):
|
||||
"""
|
||||
Writes and returns a Ticket() object
|
||||
|
||||
"""
|
||||
|
||||
q = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
|
||||
t = Ticket( title = self.cleaned_data['title'],
|
||||
|
||||
t = Ticket( title = self.cleaned_data['title'],
|
||||
submitter_email = self.cleaned_data['submitter_email'],
|
||||
created = datetime.now(),
|
||||
status = Ticket.OPEN_STATUS,
|
||||
@ -52,7 +75,7 @@ class TicketForm(forms.Form):
|
||||
description = self.cleaned_data['body'],
|
||||
priority = self.cleaned_data['priority'],
|
||||
)
|
||||
|
||||
|
||||
if self.cleaned_data['assigned_to']:
|
||||
try:
|
||||
u = User.objects.get(id=self.cleaned_data['assigned_to'])
|
||||
@ -69,91 +92,151 @@ class TicketForm(forms.Form):
|
||||
user = user,
|
||||
)
|
||||
if self.cleaned_data['assigned_to']:
|
||||
f.title = _('Ticket Opened & Assigned to %(name)s') % {'name': t.get_assigned_to}
|
||||
f.title = _('Ticket Opened & Assigned to %(name)s') % {
|
||||
'name': t.get_assigned_to
|
||||
}
|
||||
|
||||
f.save()
|
||||
|
||||
|
||||
context = {
|
||||
'ticket': t,
|
||||
'queue': q,
|
||||
}
|
||||
|
||||
from helpdesk.lib import send_templated_mail
|
||||
|
||||
if t.submitter_email:
|
||||
send_templated_mail('newticket_submitter', context, recipients=t.submitter_email, sender=q.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'newticket_submitter',
|
||||
context,
|
||||
recipients=t.submitter_email,
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if t.assigned_to and t.assigned_to != user:
|
||||
send_templated_mail('assigned_owner', context, recipients=t.assigned_to.email, sender=q.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'assigned_owner',
|
||||
context,
|
||||
recipients=t.assigned_to.email,
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if q.new_ticket_cc:
|
||||
send_templated_mail('newticket_cc', context, recipients=q.new_ticket_cc, sender=q.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=q.new_ticket_cc,
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc:
|
||||
send_templated_mail('newticket_cc', context, recipients=q.updated_ticket_cc, sender=q.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=q.updated_ticket_cc,
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
return t
|
||||
|
||||
class PublicTicketForm(forms.Form):
|
||||
queue = forms.ChoiceField(label=_('Queue'), required=True, choices=())
|
||||
|
||||
title = forms.CharField(max_length=100, required=True,
|
||||
widget=forms.TextInput(),
|
||||
label=_('Summary of your query'))
|
||||
|
||||
submitter_email = forms.EmailField(required=True,
|
||||
label=_('Your E-Mail Address'),
|
||||
help_text=_('We will e-mail you when your ticket is updated.'))
|
||||
|
||||
body = forms.CharField(widget=forms.Textarea(),
|
||||
label=_('Description of your issue'), required=True,
|
||||
help_text=_('Please be as descriptive as possible, including any details we may need to address your query.'))
|
||||
|
||||
priority = forms.ChoiceField(choices=Ticket.PRIORITY_CHOICES,
|
||||
required=True,
|
||||
initial='3',
|
||||
label=_('Urgency'),
|
||||
help_text=_('Please select a priority carefully.'))
|
||||
|
||||
class PublicTicketForm(forms.Form):
|
||||
queue = forms.ChoiceField(
|
||||
label=_('Queue'),
|
||||
required=True,
|
||||
choices=()
|
||||
)
|
||||
|
||||
title = forms.CharField(
|
||||
max_length=100,
|
||||
required=True,
|
||||
widget=forms.TextInput(),
|
||||
label=_('Summary of your query'),
|
||||
)
|
||||
|
||||
submitter_email = forms.EmailField(
|
||||
required=True,
|
||||
label=_('Your E-Mail Address'),
|
||||
help_text=_('We will e-mail you when your ticket is updated.'),
|
||||
)
|
||||
|
||||
body = forms.CharField(
|
||||
widget=forms.Textarea(),
|
||||
label=_('Description of your issue'),
|
||||
required=True,
|
||||
help_text=_('Please be as descriptive as possible, including any '
|
||||
'details we may need to address your query.'),
|
||||
)
|
||||
|
||||
priority = forms.ChoiceField(
|
||||
choices=Ticket.PRIORITY_CHOICES,
|
||||
required=True,
|
||||
initial='3',
|
||||
label=_('Urgency'),
|
||||
help_text=_('Please select a priority carefully.'),
|
||||
)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Writes and returns a Ticket() object
|
||||
|
||||
"""
|
||||
|
||||
q = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||
|
||||
t = Ticket( title = self.cleaned_data['title'],
|
||||
submitter_email = self.cleaned_data['submitter_email'],
|
||||
created = datetime.now(),
|
||||
status = Ticket.OPEN_STATUS,
|
||||
queue = q,
|
||||
description = self.cleaned_data['body'],
|
||||
priority = self.cleaned_data['priority'],
|
||||
)
|
||||
|
||||
|
||||
t = Ticket(
|
||||
title = self.cleaned_data['title'],
|
||||
submitter_email = self.cleaned_data['submitter_email'],
|
||||
created = datetime.now(),
|
||||
status = Ticket.OPEN_STATUS,
|
||||
queue = q,
|
||||
description = self.cleaned_data['body'],
|
||||
priority = self.cleaned_data['priority'],
|
||||
)
|
||||
|
||||
t.save()
|
||||
|
||||
f = FollowUp( ticket = t,
|
||||
title = _('Ticket Opened Via Web'),
|
||||
date = datetime.now(),
|
||||
public = True,
|
||||
comment = self.cleaned_data['body'],
|
||||
)
|
||||
f = FollowUp(
|
||||
ticket = t,
|
||||
title = _('Ticket Opened Via Web'),
|
||||
date = datetime.now(),
|
||||
public = True,
|
||||
comment = self.cleaned_data['body'],
|
||||
)
|
||||
|
||||
f.save()
|
||||
|
||||
|
||||
context = {
|
||||
'ticket': t,
|
||||
'queue': q,
|
||||
}
|
||||
|
||||
from helpdesk.lib import send_templated_mail
|
||||
send_templated_mail('newticket_submitter', context, recipients=t.submitter_email, sender=q.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'newticket_submitter',
|
||||
context,
|
||||
recipients=t.submitter_email,
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if q.new_ticket_cc:
|
||||
send_templated_mail('newticket_cc', context, recipients=q.new_ticket_cc, sender=q.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=q.new_ticket_cc,
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc:
|
||||
send_templated_mail('newticket_cc', context, recipients=q.updated_ticket_cc, sender=q.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=q.updated_ticket_cc,
|
||||
sender=q.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
return t
|
||||
|
||||
|
98
lib.py
98
lib.py
@ -1,15 +1,46 @@
|
||||
""" ..
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
lib.py - Common functions (eg multipart e-mail)
|
||||
"""
|
||||
|
||||
chart_colours = ('80C65A', '990066', 'FF9900', '3399CC', 'BBCCED', '3399CC', 'FFCC33')
|
||||
|
||||
|
||||
def send_templated_mail(template_name, email_context, recipients, sender=None, bcc=None, fail_silently=False, files=None):
|
||||
from helpdesk.models import EmailTemplate
|
||||
"""
|
||||
send_templated_mail() is a warpper around Django's e-mail routines that
|
||||
allows us to easily send multipart (text/plain & text/html) e-mails using
|
||||
templates that are stored in the database. This lets the admin provide
|
||||
both a text and a HTML template for each message.
|
||||
|
||||
template_name is the slug of the template to use for this message (see
|
||||
models.EmailTemplate)
|
||||
|
||||
email_context is a dictionary to be used when rendering the template
|
||||
|
||||
recipients can be either a string, eg 'a@b.com', or a list of strings.
|
||||
|
||||
sender should contain a string, eg 'My Site <me@z.com>'. If you leave it
|
||||
blank, it'll use settings.DEFAULT_FROM_EMAIL as a fallback.
|
||||
|
||||
bcc is an optional list of addresses that will receive this message as a
|
||||
blind carbon copy.
|
||||
|
||||
fail_silently is passed to Django's mail routine. Set to 'True' to ignore
|
||||
any errors at send time.
|
||||
|
||||
files can be a list of file paths to be attached, or it can be left blank.
|
||||
eg ('/tmp/file1.txt', '/tmp/image.png')
|
||||
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template import loader, Context
|
||||
from django.conf import settings
|
||||
|
||||
from helpdesk.models import EmailTemplate
|
||||
|
||||
t = EmailTemplate.objects.get(template_name__iexact=template_name)
|
||||
|
||||
@ -17,15 +48,27 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
|
||||
sender = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
context = Context(email_context)
|
||||
|
||||
text_part = loader.get_template_from_string("%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text).render(context)
|
||||
html_part = loader.get_template_from_string("{%% extends 'helpdesk/email_html_base.html' %%}{%% block title %%}%s{%% endblock %%}{%% block content %%}%s{%% endblock %%}" % (t.heading, t.html)).render(context)
|
||||
subject_part = loader.get_template_from_string("{{ ticket.ticket }} {{ ticket.title }} %s" % t.subject).render(context)
|
||||
|
||||
text_part = loader.get_template_from_string(
|
||||
"%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text
|
||||
).render(context)
|
||||
|
||||
html_part = loader.get_template_from_string(
|
||||
"{%% extends 'helpdesk/email_html_base.html' %%}{%% block title %%}%s{%% endblock %%}{%% block content %%}%s{%% endblock %%}" % (t.heading, t.html)
|
||||
).render(context)
|
||||
|
||||
subject_part = loader.get_template_from_string(
|
||||
"{{ ticket.ticket }} {{ ticket.title }} %s" % t.subject
|
||||
).render(context)
|
||||
|
||||
if type(recipients) != list:
|
||||
recipients = [recipients,]
|
||||
|
||||
msg = EmailMultiAlternatives(subject_part, text_part, sender, recipients, bcc=bcc)
|
||||
msg = EmailMultiAlternatives( subject_part,
|
||||
text_part,
|
||||
sender,
|
||||
recipients,
|
||||
bcc=bcc)
|
||||
msg.attach_alternative(html_part, "text/html")
|
||||
|
||||
if files:
|
||||
@ -34,18 +77,18 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
|
||||
|
||||
for file in files:
|
||||
msg.attach_file(file)
|
||||
|
||||
|
||||
return msg.send(fail_silently)
|
||||
|
||||
|
||||
|
||||
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.
|
||||
This function will send a multi-part e-mail with both HTML and
|
||||
Text parts. Note we don't use this any more; wsee send_templated_mail
|
||||
instead.
|
||||
|
||||
template_name must NOT contain an extension. Both HTML (.html) and TEXT
|
||||
(.txt) versions must exist, eg 'emails/public_submit' will use both
|
||||
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
|
||||
@ -57,7 +100,7 @@ def send_multipart_mail(template_name, email_context, subject, recipients, sende
|
||||
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
|
||||
sender can be an e-mail, 'Name <email>' or None. If unspecified, the
|
||||
DEFAULT_FROM_EMAIL will be used.
|
||||
|
||||
Originally posted on my blog at http://www.rossp.org/
|
||||
@ -70,7 +113,7 @@ def send_multipart_mail(template_name, email_context, subject, recipients, sende
|
||||
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)
|
||||
@ -87,13 +130,16 @@ def send_multipart_mail(template_name, email_context, subject, recipients, sende
|
||||
|
||||
for file in files:
|
||||
msg.attach_file(file)
|
||||
|
||||
|
||||
return msg.send(fail_silently)
|
||||
|
||||
|
||||
def normalise_data(data, to=100):
|
||||
"""
|
||||
Used for normalising data prior to graphing with Google charting API
|
||||
Used for normalising data prior to graphing with Google charting API. EG:
|
||||
|
||||
[1, 4, 10] becomes [10, 40, 100]
|
||||
[36, 54, 240] becomes [15, 23, 100]
|
||||
"""
|
||||
max_value = max(data)
|
||||
if max_value > to:
|
||||
@ -103,7 +149,6 @@ def normalise_data(data, to=100):
|
||||
data = new_data
|
||||
return data
|
||||
|
||||
chart_colours = ('80C65A', '990066', 'FF9900', '3399CC', 'BBCCED', '3399CC', 'FFCC33')
|
||||
|
||||
def line_chart(data):
|
||||
"""
|
||||
@ -138,7 +183,7 @@ def line_chart(data):
|
||||
first = True
|
||||
for row in range(rows):
|
||||
# Set maximum data ranges to '0:x' where 'x' is the maximum number in use.
|
||||
if not first:
|
||||
if not first:
|
||||
chart_url += ','
|
||||
chart_url += '0,%s' % max
|
||||
first = False
|
||||
@ -149,6 +194,7 @@ def line_chart(data):
|
||||
|
||||
return chart_url
|
||||
|
||||
|
||||
def bar_chart(data):
|
||||
"""
|
||||
'data' is a list of lists making a table.
|
||||
@ -185,12 +231,16 @@ def bar_chart(data):
|
||||
|
||||
return chart_url
|
||||
|
||||
|
||||
def query_to_dict(results, descriptions):
|
||||
""" Replacement method for cursor.dictfetchall() as that method no longer
|
||||
"""
|
||||
Replacement method for cursor.dictfetchall() as that method no longer
|
||||
exists in psycopg2, and I'm guessing in other backends too.
|
||||
|
||||
Converts the results of a raw SQL query into a list of dictionaries, suitable
|
||||
for use in templates etc. """
|
||||
|
||||
Converts the results of a raw SQL query into a list of dictionaries, suitable
|
||||
for use in templates etc.
|
||||
"""
|
||||
|
||||
output = []
|
||||
for data in results:
|
||||
row = {}
|
||||
|
Binary file not shown.
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2008-08-13 23:32+0000\n"
|
||||
"POT-Creation-Date: 2008-08-19 07:01+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: forms.py:17 forms.py:98 templates/helpdesk/dashboard.html:10
|
||||
#: forms.py:21 forms.py:147 templates/helpdesk/dashboard.html:10
|
||||
#: templates/helpdesk/dashboard.html:26 templates/helpdesk/dashboard.html:41
|
||||
#: templates/helpdesk/rss_list.html:23 templates/helpdesk/ticket_list.html:14
|
||||
#: templates/helpdesk/ticket_list.html:25
|
||||
@ -24,520 +24,564 @@ msgstr ""
|
||||
msgid "Queue"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:21
|
||||
#: forms.py:30
|
||||
msgid "Summary of the problem"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:24
|
||||
#: forms.py:35
|
||||
msgid "Submitter E-Mail Address"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:25
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"This e-mail address will receive copies of all public updates to this ticket."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
#: forms.py:42
|
||||
msgid "Description of Issue"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:31
|
||||
#: forms.py:49
|
||||
msgid "Case owner"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:32
|
||||
#: forms.py:50
|
||||
msgid ""
|
||||
"If you select an owner other than yourself, they'll be e-mailed details of "
|
||||
"this ticket immediately."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:37 models.py:116 scripts/escalate_tickets.py:76
|
||||
#: forms.py:58 models.py:289 management/commands/escalate_tickets.py:152
|
||||
#: templates/helpdesk/public_view_ticket.html:29
|
||||
#: templates/helpdesk/ticket.html:67 templates/helpdesk/ticket.html.py:159
|
||||
#: templates/helpdesk/ticket_list.html:27 views/staff.py:146
|
||||
#: templates/helpdesk/ticket_list.html:27 views/staff.py:182
|
||||
msgid "Priority"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:38
|
||||
#: forms.py:59
|
||||
msgid "Please select a priority carefully. If unsure, leave it as '3'."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:65
|
||||
#: forms.py:88
|
||||
msgid "Ticket Opened"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:72
|
||||
#: forms.py:95
|
||||
#, python-format
|
||||
msgid "Ticket Opened & Assigned to %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:102
|
||||
#: forms.py:156
|
||||
msgid "Summary of your query"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:105
|
||||
#: forms.py:161
|
||||
msgid "Your E-Mail Address"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:106
|
||||
#: forms.py:162
|
||||
msgid "We will e-mail you when your ticket is updated."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:109
|
||||
#: forms.py:167
|
||||
msgid "Description of your issue"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:110
|
||||
#: forms.py:169
|
||||
msgid ""
|
||||
"Please be as descriptive as possible, including any details we may need to "
|
||||
"address your query."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:115
|
||||
#: forms.py:177
|
||||
msgid "Urgency"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:116
|
||||
#: forms.py:178
|
||||
msgid "Please select a priority carefully."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:137
|
||||
#: forms.py:202
|
||||
msgid "Ticket Opened Via Web"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:28 models.py:103 models.py:216 models.py:360 models.py:380
|
||||
#: models.py:29 models.py:227 models.py:434 models.py:728 models.py:759
|
||||
#: templates/helpdesk/dashboard.html:26 templates/helpdesk/dashboard.html:41
|
||||
#: templates/helpdesk/ticket.html:153 templates/helpdesk/ticket_list.html:24
|
||||
#: templates/helpdesk/ticket_list.html:59 views/staff.py:141
|
||||
#: templates/helpdesk/ticket_list.html:59 views/staff.py:172
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:29 models.py:361
|
||||
#: models.py:34 models.py:733
|
||||
msgid "Slug"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:29
|
||||
#: models.py:35
|
||||
msgid ""
|
||||
"This slug is used when building ticket ID's. Once set, try not to change it "
|
||||
"or e-mailing may get messy."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:30
|
||||
#: models.py:40
|
||||
msgid "E-Mail Address"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:30
|
||||
#: models.py:43
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:31
|
||||
#: models.py:49
|
||||
msgid "Allow Public Submission?"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:31
|
||||
#: models.py:52
|
||||
msgid "Should this queue be listed on the public submission form?"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:32
|
||||
#: models.py:57
|
||||
msgid "Allow E-Mail Submission?"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:32
|
||||
#: models.py:60
|
||||
msgid "Do you want to poll the e-mail box below for new tickets?"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:33
|
||||
#: models.py:65
|
||||
msgid "Escalation Days"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:33
|
||||
#: models.py:68
|
||||
msgid ""
|
||||
"For tickets which are not held, how often do you wish to increase their "
|
||||
"priority? Set to 0 for no escalation."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:42
|
||||
#: models.py:73
|
||||
msgid "New Ticket CC Address"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:42
|
||||
#: models.py:76
|
||||
msgid ""
|
||||
"If an e-mail address is entered here, then it will receive notification of "
|
||||
"all new tickets created for this queue"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:43
|
||||
#: models.py:81
|
||||
msgid "Updated Ticket CC Address"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:43
|
||||
#: models.py:84
|
||||
msgid ""
|
||||
"If an e-mail address is entered here, then it will receive notification of "
|
||||
"all activity (new tickets, closed tickets, updates, reassignments, etc) for "
|
||||
"this queue"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:45
|
||||
#: models.py:90
|
||||
msgid "E-Mail Box Type"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:45
|
||||
#: models.py:92
|
||||
msgid "POP 3"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:45
|
||||
#: models.py:92
|
||||
msgid "IMAP"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:45
|
||||
#: models.py:95
|
||||
msgid ""
|
||||
"E-Mail server type for creating tickets automatically from a mailbox - both "
|
||||
"POP3 and IMAP are supported."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:46
|
||||
#: models.py:100
|
||||
msgid "E-Mail Hostname"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:46
|
||||
#: models.py:104
|
||||
msgid ""
|
||||
"Your e-mail server address - either the domain name or IP address. May be "
|
||||
"\"localhost\"."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:47
|
||||
#: models.py:109
|
||||
msgid "E-Mail Port"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:47
|
||||
#: models.py:112
|
||||
msgid ""
|
||||
"Port number to use for accessing e-mail. Default for POP3 is \"110\", and "
|
||||
"for IMAP is \"143\". This may differ on some servers. Leave it blank to use "
|
||||
"the defaults."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:48
|
||||
#: models.py:118
|
||||
msgid "E-Mail Username"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:48
|
||||
#: models.py:122
|
||||
msgid "Username for accessing this mailbox."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:49
|
||||
#: models.py:126
|
||||
msgid "E-Mail Password"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:49
|
||||
#: models.py:130
|
||||
msgid "Password for the above username"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:50
|
||||
#: models.py:134
|
||||
msgid "IMAP Folder"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:50
|
||||
#: models.py:138
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:51
|
||||
#: models.py:145
|
||||
msgid "E-Mail Check Interval"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:51
|
||||
#: models.py:146
|
||||
msgid "How often do you wish to check this mailbox? (in Minutes)"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:89 templates/helpdesk/dashboard.html:10
|
||||
#: models.py:212 templates/helpdesk/dashboard.html:10
|
||||
#: templates/helpdesk/ticket.html:123
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:90 templates/helpdesk/ticket.html:128
|
||||
#: models.py:213 templates/helpdesk/ticket.html:128
|
||||
#: templates/helpdesk/ticket.html.py:133 templates/helpdesk/ticket.html:138
|
||||
msgid "Reopened"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:91 templates/helpdesk/dashboard.html:10
|
||||
#: models.py:214 templates/helpdesk/dashboard.html:10
|
||||
#: templates/helpdesk/ticket.html:124 templates/helpdesk/ticket.html.py:129
|
||||
#: templates/helpdesk/ticket.html:134
|
||||
msgid "Resolved"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:92 templates/helpdesk/ticket.html:125
|
||||
#: models.py:215 templates/helpdesk/ticket.html:125
|
||||
#: templates/helpdesk/ticket.html.py:130 templates/helpdesk/ticket.html:135
|
||||
#: templates/helpdesk/ticket.html.py:139
|
||||
msgid "Closed"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:96
|
||||
#: models.py:219
|
||||
msgid "1. Critical"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:97
|
||||
#: models.py:220
|
||||
msgid "2. High"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:98
|
||||
#: models.py:221
|
||||
msgid "3. Normal"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:99
|
||||
#: models.py:222
|
||||
msgid "4. Low"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:100
|
||||
#: models.py:223
|
||||
msgid "5. Very Low"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:105 templates/helpdesk/dashboard.html:41
|
||||
#: models.py:234 templates/helpdesk/dashboard.html:41
|
||||
#: templates/helpdesk/ticket_list.html:23
|
||||
#: templates/helpdesk/ticket_list.html:59
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:106
|
||||
#: models.py:236
|
||||
msgid "Date this ticket was first created"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:240
|
||||
msgid "Modified"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:107 templates/helpdesk/public_view_ticket.html:24
|
||||
#: models.py:242
|
||||
msgid "Date this ticket was most recently changed."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:246 templates/helpdesk/public_view_ticket.html:24
|
||||
#: templates/helpdesk/ticket.html:62
|
||||
msgid "Submitter E-Mail"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:107
|
||||
#: models.py:249
|
||||
msgid ""
|
||||
"The submitter will receive an email for all public follow-ups left for this "
|
||||
"task."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:109 templates/helpdesk/dashboard.html:26
|
||||
#: models.py:261 templates/helpdesk/dashboard.html:26
|
||||
#: templates/helpdesk/ticket_list.html:15
|
||||
#: templates/helpdesk/ticket_list.html:26
|
||||
#: templates/helpdesk/ticket_list.html:59
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:111
|
||||
#: models.py:267
|
||||
msgid "On Hold"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:113 models.py:362 templates/helpdesk/public_view_ticket.html:34
|
||||
#: models.py:270
|
||||
msgid "If a ticket is on hold, it will not automatically be escalated."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:275 models.py:737 templates/helpdesk/public_view_ticket.html:34
|
||||
#: templates/helpdesk/ticket.html:72
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:114 templates/helpdesk/public_view_ticket.html:41
|
||||
#: models.py:278
|
||||
msgid "The content of the customers query."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:282 templates/helpdesk/public_view_ticket.html:41
|
||||
#: templates/helpdesk/ticket.html:79
|
||||
msgid "Resolution"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:125 templates/helpdesk/ticket.html:58 views/feeds.py:55
|
||||
#: views/feeds.py:77 views/feeds.py:120 views/staff.py:120
|
||||
#: models.py:285
|
||||
msgid "The resolution provided to the customer by our staff."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:293
|
||||
msgid "1 = Highest Priority, 5 = Low Priority"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:300
|
||||
msgid ""
|
||||
"The date this ticket was last escalated - updated automatically by "
|
||||
"management/commands/escalate_tickets.py."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:309 templates/helpdesk/ticket.html:58 views/feeds.py:91
|
||||
#: views/feeds.py:117 views/feeds.py:171 views/staff.py:149
|
||||
msgid "Unassigned"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:156
|
||||
#: models.py:348
|
||||
msgid " - On Hold"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:215 models.py:330
|
||||
#: models.py:430 models.py:662
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:217 views/staff.py:134
|
||||
#: models.py:441 views/staff.py:163
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:218
|
||||
#: models.py:447
|
||||
msgid "Public"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:221 templates/helpdesk/ticket.html:121
|
||||
#: models.py:450
|
||||
msgid ""
|
||||
"Public tickets are viewable by the submitter and all staff, but non-public "
|
||||
"tickets can only be seen by staff."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:461 templates/helpdesk/ticket.html:121
|
||||
msgid "New Status"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:246
|
||||
#: models.py:465
|
||||
msgid "If the status was changed, what was it changed to?"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:496
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:247
|
||||
#: models.py:501
|
||||
msgid "Old Value"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:248
|
||||
#: models.py:507
|
||||
msgid "New Value"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:253
|
||||
#: models.py:515
|
||||
msgid "removed"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:255
|
||||
#: models.py:517
|
||||
#, python-format
|
||||
msgid "set to %s"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:257
|
||||
#: models.py:519
|
||||
#, python-format
|
||||
msgid "changed from \"%(old_value)s\" to \"%(new_value)s\""
|
||||
msgstr ""
|
||||
|
||||
#: models.py:289
|
||||
#: models.py:562
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:290
|
||||
#: models.py:567
|
||||
msgid "Filename"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:291
|
||||
#: models.py:572
|
||||
msgid "MIME Type"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:292
|
||||
#: models.py:577
|
||||
msgid "Size"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:292
|
||||
#: models.py:578
|
||||
msgid "Size of this file in bytes"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:315
|
||||
#: models.py:611
|
||||
msgid ""
|
||||
"Leave blank to allow this reply to be used for all queues, or select those "
|
||||
"queues you wish to limit this reply to."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:316 models.py:328
|
||||
#: models.py:616 models.py:657
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:316
|
||||
#: models.py:618
|
||||
msgid ""
|
||||
"Only used to assist users with selecting a reply - not shown to the user."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:317
|
||||
#: models.py:623
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:317
|
||||
#: models.py:624
|
||||
msgid ""
|
||||
"Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); "
|
||||
"{{ queue }} - The queue; and {{ user }} - the current user."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:326
|
||||
#: models.py:651
|
||||
msgid ""
|
||||
"Leave blank for this exclusion to be applied to all queues, or select those "
|
||||
"queues you wish to exclude with this entry."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:330
|
||||
#: models.py:663
|
||||
msgid "Date on which escalation should not happen"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:341
|
||||
#: models.py:680
|
||||
msgid "Template Name"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:343
|
||||
#: models.py:686
|
||||
msgid "Subject"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:343
|
||||
#: models.py:688
|
||||
msgid ""
|
||||
"This will be prefixed with \"[ticket.ticket] ticket.title\". We recommend "
|
||||
"something simple such as \"(Updated\") or \"(Closed)\" - the same context is "
|
||||
"available as in plain_text, below."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:344
|
||||
#: models.py:694
|
||||
msgid "Heading"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:344
|
||||
#: models.py:696
|
||||
msgid ""
|
||||
"In HTML e-mails, this will be the heading at the top of the email - the same "
|
||||
"context is available as in plain_text, below."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:345
|
||||
#: models.py:702
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:345
|
||||
#: models.py:703
|
||||
msgid ""
|
||||
"The context available to you includes {{ ticket }}, {{ queue }}, and "
|
||||
"depending on the time of the call: {{ resolution }} or {{ comment }}."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:346
|
||||
#: models.py:709
|
||||
msgid "HTML"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:346
|
||||
#: models.py:710
|
||||
msgid "The same context is available here as in plain_text, above."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:381
|
||||
#: models.py:764
|
||||
msgid "Question"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:382
|
||||
#: models.py:768
|
||||
msgid "Answer"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:384
|
||||
#: models.py:772
|
||||
msgid "Votes"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:384
|
||||
#: models.py:773
|
||||
msgid "Total number of votes cast for this item"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:385
|
||||
#: models.py:777
|
||||
msgid "Positive Votes"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:385
|
||||
#: models.py:778
|
||||
msgid "Number of votes for this item which were POSITIVE."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:387
|
||||
#: models.py:782
|
||||
msgid "Last Updated"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:397
|
||||
#: models.py:783
|
||||
msgid "The date on which this question was most recently changed."
|
||||
msgstr ""
|
||||
|
||||
#: models.py:795
|
||||
msgid "Unrated"
|
||||
msgstr ""
|
||||
|
||||
#: scripts/escalate_tickets.py:70
|
||||
#: management/commands/escalate_tickets.py:146
|
||||
#, python-format
|
||||
msgid "Ticket escalated after %s days"
|
||||
msgstr ""
|
||||
|
||||
#: scripts/get_email.py:74
|
||||
#: management/commands/get_email.py:97
|
||||
msgid "Created from e-mail"
|
||||
msgstr ""
|
||||
|
||||
#: scripts/get_email.py:77
|
||||
#: management/commands/get_email.py:100
|
||||
msgid "Unknown Sender"
|
||||
msgstr ""
|
||||
|
||||
#: scripts/get_email.py:156
|
||||
#: management/commands/get_email.py:206
|
||||
msgid " (Updated)"
|
||||
msgstr ""
|
||||
|
||||
#: scripts/get_email.py:166
|
||||
#: management/commands/get_email.py:228
|
||||
#, python-format
|
||||
msgid "E-Mail Received from %s"
|
||||
msgid "E-Mail Received from %(sender_email)s"
|
||||
msgstr ""
|
||||
|
||||
#: templates/helpdesk/base.html:4
|
||||
@ -806,7 +850,7 @@ msgstr ""
|
||||
|
||||
#: templates/helpdesk/public_view_ticket.html:16
|
||||
#, python-format
|
||||
msgid "Queue: %(ticket.queue)s"
|
||||
msgid "Queue: %(queue_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: templates/helpdesk/public_view_ticket.html:19
|
||||
@ -1087,75 +1131,75 @@ msgstr ""
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:26
|
||||
#: views/feeds.py:35
|
||||
#, python-format
|
||||
msgid "Helpdesk: Open Tickets in queue %(queue)s for %(username)s"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:28
|
||||
#: views/feeds.py:40
|
||||
#, python-format
|
||||
msgid "Helpdesk: Open Tickets for %(username)s"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:32
|
||||
#: views/feeds.py:46
|
||||
#, python-format
|
||||
msgid "Open and Reopened Tickets in queue %(queue)s for %(username)s"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:34
|
||||
#: views/feeds.py:51
|
||||
#, python-format
|
||||
msgid "Open and Reopened Tickets for %(username)s"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:62
|
||||
#: views/feeds.py:98
|
||||
msgid "Helpdesk: Unassigned Tickets"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:63
|
||||
#: views/feeds.py:99
|
||||
msgid "Unassigned Open and Reopened tickets"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:84
|
||||
#: views/feeds.py:124
|
||||
msgid "Helpdesk: Recent Followups"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:85
|
||||
#: views/feeds.py:125
|
||||
msgid ""
|
||||
"Recent FollowUps, such as e-mail replies, comments, attachments and "
|
||||
"resolutions"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:102
|
||||
#: views/feeds.py:142
|
||||
#, python-format
|
||||
msgid "Helpdesk: Open Tickets in queue %(queue)s"
|
||||
msgstr ""
|
||||
|
||||
#: views/feeds.py:105
|
||||
#: views/feeds.py:147
|
||||
#, python-format
|
||||
msgid "Open and Reopened Tickets in queue %(queue)s"
|
||||
msgstr ""
|
||||
|
||||
#: views/public.py:53
|
||||
#: views/public.py:58
|
||||
msgid "Invalid ticket ID or e-mail address. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: views/staff.py:82
|
||||
#: views/staff.py:107
|
||||
msgid "Accepted resolution and closed ticket"
|
||||
msgstr ""
|
||||
|
||||
#: views/staff.py:116
|
||||
#: views/staff.py:143
|
||||
#, python-format
|
||||
msgid "Assigned to %(username)s"
|
||||
msgstr ""
|
||||
|
||||
#: views/staff.py:136
|
||||
#: views/staff.py:165
|
||||
msgid "Updated"
|
||||
msgstr ""
|
||||
|
||||
#: views/staff.py:298
|
||||
#: views/staff.py:369
|
||||
msgid "Ticket taken off hold"
|
||||
msgstr ""
|
||||
|
||||
#: views/staff.py:301
|
||||
#: views/staff.py:372
|
||||
msgid "Ticket placed on hold"
|
||||
msgstr ""
|
||||
|
@ -8,13 +8,17 @@ scripts/create_escalation_exclusion.py - Easy way to routinely add particular
|
||||
days to the list of days on which no
|
||||
escalation should take place.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, date
|
||||
from django.db.models import Q
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
import sys, getopt
|
||||
import getopt
|
||||
from optparse import make_option
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from optparse import make_option
|
||||
from django.db.models import Q
|
||||
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def __init__(self):
|
||||
@ -22,15 +26,15 @@ class Command(BaseCommand):
|
||||
|
||||
self.option_list += (
|
||||
make_option(
|
||||
'--days', '-d',
|
||||
'--days', '-d',
|
||||
help='Days of week (monday, tuesday, etc)'),
|
||||
make_option(
|
||||
'--occurrences', '-o',
|
||||
'--occurrences', '-o',
|
||||
type='int',
|
||||
default=1,
|
||||
help='Occurrences: How many weeks ahead to exclude this day'),
|
||||
make_option(
|
||||
'--queues', '-q',
|
||||
'--queues', '-q',
|
||||
help='Queues to include (default: all). Use queue slugs'),
|
||||
make_option(
|
||||
'--verbose', '-v',
|
||||
@ -53,7 +57,7 @@ class Command(BaseCommand):
|
||||
if not occurrences: occurrences = 1
|
||||
if not (days and occurrences):
|
||||
raise CommandError('One or more occurrences must be specified.')
|
||||
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
@ -65,6 +69,7 @@ class Command(BaseCommand):
|
||||
|
||||
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
|
||||
|
||||
|
||||
day_names = {
|
||||
'monday': 0,
|
||||
'tuesday': 1,
|
||||
@ -75,6 +80,7 @@ day_names = {
|
||||
'sunday': 6,
|
||||
}
|
||||
|
||||
|
||||
def create_exclusions(days, occurrences, verbose, queues):
|
||||
days = days.split(',')
|
||||
for day in days:
|
||||
@ -87,10 +93,10 @@ def create_exclusions(days, occurrences, verbose, queues):
|
||||
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
|
||||
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate)
|
||||
esc.save()
|
||||
|
||||
|
||||
if verbose:
|
||||
print "Created exclusion for %s %s" % (day_name, workdate)
|
||||
|
||||
|
||||
for q in queues:
|
||||
esc.queues.add(q)
|
||||
if verbose:
|
||||
@ -107,13 +113,15 @@ def usage():
|
||||
print " --queues, -q: Queues to include (default: all). Use queue slugs"
|
||||
print " --verbose, -v: Display a list of dates excluded"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# This script can be run from the command-line or via Django's manage.py.
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
|
||||
except getopt.GetoptError:
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
days = None
|
||||
occurrences = None
|
||||
verbose = False
|
||||
@ -134,7 +142,7 @@ if __name__ == '__main__':
|
||||
if not (days and occurrences):
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
|
@ -4,20 +4,22 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
|
||||
scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
|
||||
designed to be run from Cron or similar.
|
||||
"""
|
||||
from datetime import datetime, timedelta, date
|
||||
import sys, getopt
|
||||
|
||||
from datetime import datetime, timedelta, date
|
||||
import getopt
|
||||
from optparse import make_option
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
|
||||
from helpdesk.lib import send_templated_mail
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from optparse import make_option
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def __init__(self):
|
||||
@ -25,7 +27,7 @@ class Command(BaseCommand):
|
||||
|
||||
self.option_list += (
|
||||
make_option(
|
||||
'--queues', '-q',
|
||||
'--queues', '-q',
|
||||
help='Queues to include (default: all). Use queue slugs'),
|
||||
make_option(
|
||||
'--verbose', '-v',
|
||||
@ -33,17 +35,17 @@ class Command(BaseCommand):
|
||||
default=False,
|
||||
help='Display a list of dates excluded'),
|
||||
)
|
||||
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbose = False
|
||||
queue_slugs = None
|
||||
queues = []
|
||||
|
||||
|
||||
if options['verbose']:
|
||||
verbose = True
|
||||
if options['queues']:
|
||||
queue_slugs = options['queues']
|
||||
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
@ -55,12 +57,13 @@ class Command(BaseCommand):
|
||||
|
||||
escalate_tickets(queues=queues, verbose=verbose)
|
||||
|
||||
|
||||
def escalate_tickets(queues, verbose):
|
||||
""" Only include queues with escalation configured """
|
||||
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
|
||||
if queues:
|
||||
queryset = queryset.filter(slug__in=queues)
|
||||
|
||||
|
||||
for q in queryset:
|
||||
last = date.today() - timedelta(days=q.escalate_days)
|
||||
today = date.today()
|
||||
@ -78,28 +81,62 @@ def escalate_tickets(queues, verbose):
|
||||
|
||||
if verbose:
|
||||
print "Processing: %s" % q
|
||||
|
||||
for t in q.ticket_set.filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)).exclude(priority=1).filter(Q(on_hold__isnull=True) | Q(on_hold=False)).filter(Q(last_escalation__lte=req_last_escl_date) | Q(last_escalation__isnull=True)):
|
||||
|
||||
for t in q.ticket_set.filter(
|
||||
Q(status=Ticket.OPEN_STATUS)
|
||||
| Q(status=Ticket.REOPENED_STATUS)
|
||||
).exclude(
|
||||
priority=1
|
||||
).filter(
|
||||
Q(on_hold__isnull=True)
|
||||
| Q(on_hold=False)
|
||||
).filter(
|
||||
Q(last_escalation__lte=req_last_escl_date)
|
||||
| Q(last_escalation__isnull=True)
|
||||
):
|
||||
|
||||
t.last_escalation = datetime.now()
|
||||
t.priority -= 1
|
||||
t.save()
|
||||
|
||||
|
||||
context = {
|
||||
'ticket': t,
|
||||
'queue': q,
|
||||
}
|
||||
|
||||
if t.submitter_email:
|
||||
send_templated_mail('escalated_submitter', context, recipients=t.submitter_email, sender=t.queue.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
'escalated_submitter',
|
||||
context,
|
||||
recipients=t.submitter_email,
|
||||
sender=t.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if t.queue.updated_ticket_cc:
|
||||
send_templated_mail('escalated_cc', context, recipients=t.queue.updated_ticket_cc, sender=t.queue.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
'escalated_cc',
|
||||
context,
|
||||
recipients=t.queue.updated_ticket_cc,
|
||||
sender=t.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if t.assigned_to:
|
||||
send_templated_mail('escalated_owner', context, recipients=t.assigned_to.email, sender=t.queue.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'escalated_owner',
|
||||
context,
|
||||
recipients=t.assigned_to.email,
|
||||
sender=t.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if verbose:
|
||||
print " - Esclating %s from %s>%s" % (t.ticket, t.priority+1, t.priority)
|
||||
print " - Esclating %s from %s>%s" % (
|
||||
t.ticket,
|
||||
t.priority+1,
|
||||
t.priority
|
||||
)
|
||||
|
||||
f = FollowUp(
|
||||
ticket = t,
|
||||
@ -118,11 +155,13 @@ def escalate_tickets(queues, verbose):
|
||||
)
|
||||
tc.save()
|
||||
|
||||
|
||||
def usage():
|
||||
print "Options:"
|
||||
print " --queues, -q: Queues to include (default: all). Use queue slugs"
|
||||
print " --verbose, -v: Display a list of dates excluded"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], 'q:v', ['queues=', 'verbose'])
|
||||
@ -133,13 +172,13 @@ if __name__ == '__main__':
|
||||
verbose = False
|
||||
queue_slugs = None
|
||||
queues = []
|
||||
|
||||
|
||||
for o, a in opts:
|
||||
if o in ('-v', '--verbose'):
|
||||
verbose = True
|
||||
if o in ('-q', '--queues'):
|
||||
queue_slugs = a
|
||||
|
||||
|
||||
if queue_slugs is not None:
|
||||
queue_set = queue_slugs.split(',')
|
||||
for queue in queue_set:
|
||||
|
@ -5,8 +5,8 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
scripts/get_email.py - Designed to be run from cron, this script checks the
|
||||
POP and IMAP boxes defined for the queues within a
|
||||
helpdesk, creating tickets from the new messages (or
|
||||
POP and IMAP boxes defined for the queues within a
|
||||
helpdesk, creating tickets from the new messages (or
|
||||
adding to existing tickets if needed)
|
||||
"""
|
||||
|
||||
@ -25,16 +25,27 @@ from django.utils.translation import ugettext as _
|
||||
from helpdesk.lib import send_templated_mail
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, Attachment
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
process_email()
|
||||
|
||||
def process_email():
|
||||
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
|
||||
|
||||
if (q.email_box_last_check + timedelta(minutes=q.email_box_interval)) > datetime.now():
|
||||
def process_email():
|
||||
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
|
||||
|
||||
|
||||
queue_time_delta = timedelta(minutes=q.email_box_interval)
|
||||
|
||||
if (q.email_box_last_check + queue_time_delta) > datetime.now():
|
||||
continue
|
||||
|
||||
process_queue(q)
|
||||
@ -42,6 +53,7 @@ def process_email():
|
||||
q.email_box_last_check = datetime.now()
|
||||
q.save()
|
||||
|
||||
|
||||
def process_queue(q):
|
||||
print "Processing: %s" % q
|
||||
if q.email_box_type == 'pop3':
|
||||
@ -55,16 +67,16 @@ def process_queue(q):
|
||||
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)
|
||||
@ -77,15 +89,16 @@ def process_queue(q):
|
||||
server.close()
|
||||
server.logout()
|
||||
|
||||
|
||||
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(sender)[1]
|
||||
if sender_email.startswith('postmaster'):
|
||||
sender_email = ''
|
||||
@ -96,24 +109,31 @@ def ticket_from_message(message, queue):
|
||||
ticket = re.match(r"^\[(?P<queue>[A-Za-z0-9]+)-(?P<id>\d+)\]", subject).group('id')
|
||||
else:
|
||||
ticket = None
|
||||
|
||||
counter = 0
|
||||
files = []
|
||||
|
||||
for part in message.walk():
|
||||
if part.get_content_maintype() == 'multipart':
|
||||
continue
|
||||
|
||||
|
||||
name = part.get_param("name")
|
||||
|
||||
|
||||
if part.get_content_maintype() == 'text' and name == None:
|
||||
body = part.get_payload()
|
||||
else:
|
||||
if not name:
|
||||
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()})
|
||||
|
||||
files.append({
|
||||
'filename': name,
|
||||
'content': part.get_payload(decode=True),
|
||||
'type': part.get_content_type()},
|
||||
)
|
||||
|
||||
counter += 1
|
||||
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
if ticket:
|
||||
@ -130,7 +150,8 @@ def ticket_from_message(message, queue):
|
||||
|
||||
high_priority_types = ('high', 'important', '1', 'urgent')
|
||||
|
||||
if smtp_priority in high_priority_types or smtp_importance in high_priority_types:
|
||||
if smtp_priority in high_priority_types
|
||||
or smtp_importance in high_priority_types:
|
||||
priority = 2
|
||||
|
||||
if ticket == None:
|
||||
@ -145,7 +166,7 @@ def ticket_from_message(message, queue):
|
||||
t.save()
|
||||
new = True
|
||||
update = ''
|
||||
|
||||
|
||||
context = {
|
||||
'ticket': t,
|
||||
'queue': queue,
|
||||
@ -154,26 +175,57 @@ def ticket_from_message(message, queue):
|
||||
if new:
|
||||
|
||||
if sender_email:
|
||||
send_templated_mail('newticket_submitter', context, recipients=sender_email, sender=queue.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'newticket_submitter',
|
||||
context,
|
||||
recipients=sender_email,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if queue.new_ticket_cc:
|
||||
send_templated_mail('newticket_cc', context, recipients=queue.new_ticket_cc, sender=queue.from_address, fail_silently=True)
|
||||
|
||||
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
|
||||
send_templated_mail('newticket_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=queue.new_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if queue.updated_ticket_cc
|
||||
and queue.updated_ticket_cc != queue.new_ticket_cc:
|
||||
send_templated_mail(
|
||||
'newticket_cc',
|
||||
context,
|
||||
recipients=queue.updated_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
else:
|
||||
update = _(' (Updated)')
|
||||
|
||||
if t.assigned_to:
|
||||
send_templated_mail('updated_owner', context, recipients=t.assigned_to.email, sender=queue.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'updated_owner',
|
||||
context,
|
||||
recipients=t.assigned_to.email,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if queue.updated_ticket_cc:
|
||||
send_templated_mail('updated_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'updated_cc',
|
||||
context,
|
||||
recipients=queue.updated_ticket_cc,
|
||||
sender=queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
f = FollowUp(
|
||||
ticket = t,
|
||||
title = _('E-Mail Received from %s' % sender_email),
|
||||
title = _('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
|
||||
date = datetime.now(),
|
||||
public = True,
|
||||
comment = body,
|
||||
@ -184,11 +236,17 @@ def ticket_from_message(message, queue):
|
||||
|
||||
for file in files:
|
||||
filename = file['filename'].replace(' ', '_')
|
||||
a = Attachment(followup=f, filename=filename, mime_type=file['type'], size=len(file['content']))
|
||||
#a.save_file_file(file['filename'], file['content'])
|
||||
a = Attachment(
|
||||
followup=f,
|
||||
filename=filename,
|
||||
mime_type=file['type'],
|
||||
size=len(file['content']),
|
||||
)
|
||||
a.file.save(file['filename'], ContentFile(file['content']))
|
||||
a.save()
|
||||
print " - %s" % file['filename']
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
process_email()
|
||||
|
||||
|
629
models.py
629
models.py
@ -14,69 +14,192 @@ from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class Queue(models.Model):
|
||||
"""
|
||||
A queue is a collection of tickets into what would generally be business
|
||||
A queue is a collection of tickets into what would generally be business
|
||||
areas or departments.
|
||||
|
||||
For example, a company may have a queue for each Product they provide, or
|
||||
For example, a company may have a queue for each Product they provide, or
|
||||
a queue for each of Accounts, Pre-Sales, and Support.
|
||||
|
||||
TODO: Add e-mail inboxes (either using piped e-mail or IMAP/POP3) so we
|
||||
can automatically get tickets via e-mail.
|
||||
"""
|
||||
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):
|
||||
if not self.email_address:
|
||||
return u'NO QUEUE EMAIL ADDRESS DEFINED <%s>' % settings.DEFAULT_FROM_EMAIL
|
||||
else:
|
||||
return u'%s <%s>' % (self.title, self.email_address)
|
||||
from_address = property(_from_address)
|
||||
title = models.CharField(
|
||||
_('Title'),
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
new_ticket_cc = models.EmailField(_('New Ticket CC Address'), blank=True, null=True, help_text=_('If an e-mail address is entered here, then it will receive notification of all new tickets created for this queue'))
|
||||
updated_ticket_cc = models.EmailField(_('Updated Ticket CC Address'), blank=True, null=True, help_text=_('If an e-mail address is entered here, then it will receive notification of all activity (new tickets, closed tickets, updates, reassignments, etc) for this queue'))
|
||||
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_box_type = models.CharField(_('E-Mail Box Type'), max_length=5, choices=(('pop3', _('POP 3')),('imap', _('IMAP'))), blank=True, null=True, help_text=_('E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported.'))
|
||||
email_box_host = models.CharField(_('E-Mail Hostname'), max_length=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(_('E-Mail Port'), 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. Leave it blank to use the defaults.'))
|
||||
email_box_user = models.CharField(_('E-Mail Username'), max_length=200, blank=True, null=True, help_text=_('Username for accessing this mailbox.'))
|
||||
email_box_pass = models.CharField(_('E-Mail Password'), max_length=200, blank=True, null=True, help_text=_('Password for the above username'))
|
||||
email_box_imap_folder = models.CharField(_('IMAP Folder'), max_length=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(_('E-Mail Check Interval'), 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
|
||||
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.'),
|
||||
)
|
||||
|
||||
new_ticket_cc = models.EmailField(
|
||||
_('New Ticket CC Address'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('If an e-mail address is entered here, then it will '
|
||||
'receive notification of all new tickets created for this queue'),
|
||||
)
|
||||
|
||||
updated_ticket_cc = models.EmailField(
|
||||
_('Updated Ticket CC Address'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('If an e-mail address is entered here, then it will '
|
||||
'receive notification of all activity (new tickets, closed '
|
||||
'tickets, updates, reassignments, etc) for this queue'),
|
||||
)
|
||||
|
||||
email_box_type = models.CharField(
|
||||
_('E-Mail Box Type'),
|
||||
max_length=5,
|
||||
choices=(('pop3', _('POP 3')), ('imap', _('IMAP'))),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('E-Mail server type for creating tickets automatically '
|
||||
'from a mailbox - both POP3 and IMAP are supported.'),
|
||||
)
|
||||
|
||||
email_box_host = models.CharField(
|
||||
_('E-Mail Hostname'),
|
||||
max_length=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(
|
||||
_('E-Mail Port'),
|
||||
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. Leave it blank to use the defaults.'),
|
||||
)
|
||||
|
||||
email_box_user = models.CharField(
|
||||
_('E-Mail Username'),
|
||||
max_length=200,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Username for accessing this mailbox.'),
|
||||
)
|
||||
|
||||
email_box_pass = models.CharField(
|
||||
_('E-Mail Password'),
|
||||
max_length=200,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Password for the above username'),
|
||||
)
|
||||
|
||||
email_box_imap_folder = models.CharField(
|
||||
_('IMAP Folder'),
|
||||
max_length=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(
|
||||
_('E-Mail Check Interval'),
|
||||
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,
|
||||
# This is updated by management/commands/get_mail.py.
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s" % self.title
|
||||
|
||||
class Meta:
|
||||
ordering = ('title',)
|
||||
|
||||
|
||||
def _from_address(self):
|
||||
"""
|
||||
Short property to provide a sender address in SMTP format,
|
||||
eg 'Name <email>'. We do this so we can put a simple error message
|
||||
in the sender name field, so hopefully the admin can see and fix it.
|
||||
"""
|
||||
if not self.email_address:
|
||||
return u'NO QUEUE EMAIL ADDRESS DEFINED <%s>' % settings.DEFAULT_FROM_EMAIL
|
||||
else:
|
||||
return u'%s <%s>' % (self.title, self.email_address)
|
||||
from_address = property(_from_address)
|
||||
|
||||
def save(self):
|
||||
if self.email_box_type == 'imap' and not self.email_box_imap_folder:
|
||||
self.email_box_imap_folder = 'INBOX'
|
||||
|
||||
if not self.email_box_port:
|
||||
if self.email_box_type == 'imap':
|
||||
self.email_box_port = 143
|
||||
else:
|
||||
self.email_box_port = 110
|
||||
super(Queue, self).save()
|
||||
|
||||
|
||||
class Ticket(models.Model):
|
||||
"""
|
||||
To allow a ticket to be entered as quickly as possible, only the
|
||||
bare minimum fields are required. These basically allow us to
|
||||
sort and manage the ticket. The user can always go back and
|
||||
To allow a ticket to be entered as quickly as possible, only the
|
||||
bare minimum fields are required. These basically allow us to
|
||||
sort and manage the ticket. The user can always go back and
|
||||
enter more information later.
|
||||
|
||||
A good example of this is when a customer is on the phone, and
|
||||
A good example of this is when a customer is on the phone, and
|
||||
you want to give them a ticket ID as quickly as possible. You can
|
||||
enter some basic info, save the ticket, give the customer the ID
|
||||
enter some basic info, save the ticket, give the customer the ID
|
||||
and get off the phone, then add in further detail at a later time
|
||||
(once the customer is not on the line).
|
||||
|
||||
Note that assigned_to is optional - unassigned tickets are displayed on
|
||||
Note that assigned_to is optional - unassigned tickets are displayed on
|
||||
the dashboard to prompt users to take ownership of them.
|
||||
"""
|
||||
|
||||
@ -84,7 +207,7 @@ class Ticket(models.Model):
|
||||
REOPENED_STATUS = 2
|
||||
RESOLVED_STATUS = 3
|
||||
CLOSED_STATUS = 4
|
||||
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(OPEN_STATUS, _('Open')),
|
||||
(REOPENED_STATUS, _('Reopened')),
|
||||
@ -100,26 +223,87 @@ class Ticket(models.Model):
|
||||
(5, _('5. Very Low')),
|
||||
)
|
||||
|
||||
title = models.CharField(_('Title'), max_length=200)
|
||||
title = models.CharField(
|
||||
_('Title'),
|
||||
max_length=200,
|
||||
)
|
||||
|
||||
queue = models.ForeignKey(Queue)
|
||||
created = models.DateTimeField(_('Created'), blank=True)
|
||||
modified = models.DateTimeField(_('Modified'), blank=True)
|
||||
submitter_email = models.EmailField(_('Submitter E-Mail'), blank=True, null=True, help_text=_('The submitter will receive an email for all public follow-ups left for this task.'))
|
||||
assigned_to = models.ForeignKey(User, related_name='assigned_to', blank=True, null=True)
|
||||
status = models.IntegerField(_('Status'), choices=STATUS_CHOICES, default=OPEN_STATUS)
|
||||
|
||||
on_hold = models.BooleanField(_('On Hold'), blank=True, null=True)
|
||||
created = models.DateTimeField(
|
||||
_('Created'),
|
||||
blank=True,
|
||||
help_text=_('Date this ticket was first created'),
|
||||
)
|
||||
|
||||
description = models.TextField(_('Description'), blank=True, null=True)
|
||||
resolution = models.TextField(_('Resolution'), blank=True, null=True)
|
||||
modified = models.DateTimeField(
|
||||
_('Modified'),
|
||||
blank=True,
|
||||
help_text=_('Date this ticket was most recently changed.'),
|
||||
)
|
||||
|
||||
priority = models.IntegerField(_('Priority'), choices=PRIORITY_CHOICES, default=3, blank=3)
|
||||
|
||||
last_escalation = models.DateTimeField(blank=True, null=True, editable=False)
|
||||
submitter_email = models.EmailField(
|
||||
_('Submitter E-Mail'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('The submitter will receive an email for all public '
|
||||
'follow-ups left for this task.'),
|
||||
)
|
||||
|
||||
assigned_to = models.ForeignKey(
|
||||
User,
|
||||
related_name='assigned_to',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
status = models.IntegerField(
|
||||
_('Status'),
|
||||
choices=STATUS_CHOICES,
|
||||
default=OPEN_STATUS,
|
||||
)
|
||||
|
||||
on_hold = models.BooleanField(
|
||||
_('On Hold'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('If a ticket is on hold, it will not automatically be '
|
||||
'escalated.'),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
_('Description'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('The content of the customers query.'),
|
||||
)
|
||||
|
||||
resolution = models.TextField(
|
||||
_('Resolution'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('The resolution provided to the customer by our staff.'),
|
||||
)
|
||||
|
||||
priority = models.IntegerField(
|
||||
_('Priority'),
|
||||
choices=PRIORITY_CHOICES,
|
||||
default=3,
|
||||
blank=3,
|
||||
help_text=_('1 = Highest Priority, 5 = Low Priority'),
|
||||
)
|
||||
|
||||
last_escalation = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text=_('The date this ticket was last escalated - updated '
|
||||
'automatically by management/commands/escalate_tickets.py.'),
|
||||
)
|
||||
|
||||
def _get_assigned_to(self):
|
||||
""" Custom property to allow us to easily print 'Unassigned' if a
|
||||
ticket has no owner, or the users name if it's assigned. If the user
|
||||
""" Custom property to allow us to easily print 'Unassigned' if a
|
||||
ticket has no owner, or the users name if it's assigned. If the user
|
||||
has a full name configured, we use that, otherwise their username. """
|
||||
if not self.assigned_to:
|
||||
return _('Unassigned')
|
||||
@ -131,44 +315,69 @@ class Ticket(models.Model):
|
||||
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. """
|
||||
""" A user-friendly ticket ID, which is a combination of ticket ID
|
||||
and queue slug. This is generally used in e-mail subjects. """
|
||||
|
||||
return u"[%s]" % (self.ticket_for_url)
|
||||
ticket = property(_get_ticket)
|
||||
|
||||
def _get_ticket_for_url(self):
|
||||
""" A URL-friendly ticket ID, used in links. """
|
||||
return u"%s-%s" % (self.queue.slug, self.id)
|
||||
ticket_for_url = property(_get_ticket_for_url)
|
||||
|
||||
def _get_priority_img(self):
|
||||
""" Image-based representation of the priority """
|
||||
from django.conf import settings
|
||||
return u"%s/helpdesk/priorities/priority%s.png" % (settings.MEDIA_URL, self.priority)
|
||||
get_priority_img = property(_get_priority_img)
|
||||
|
||||
def _get_priority_span(self):
|
||||
"""
|
||||
A HTML <span> providing a CSS_styled representation of the priority.
|
||||
"""
|
||||
from django.utils.safestring import mark_safe
|
||||
return mark_safe(u"<span class='priority%s'>%s</span>" % (self.priority, self.priority))
|
||||
get_priority_span = property(_get_priority_span)
|
||||
|
||||
def _get_status(self):
|
||||
"""
|
||||
Displays the ticket status, with an "On Hold" message if needed.
|
||||
"""
|
||||
held_msg = ''
|
||||
if self.on_hold: held_msg = _(' - On Hold')
|
||||
return u'%s%s' % (self.get_status_display(), held_msg)
|
||||
get_status = property(_get_status)
|
||||
|
||||
def _get_ticket_url(self):
|
||||
"""
|
||||
Returns a publicly-viewable URL for this ticket, used when giving
|
||||
a URL to the submitter of a ticket.
|
||||
"""
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.urlresolvers import reverse
|
||||
site = Site.objects.get_current()
|
||||
return u"http://%s%s?ticket=%s&email=%s" % (site.domain, reverse('helpdesk_public_view'), self.ticket_for_url, self.submitter_email)
|
||||
return u"http://%s%s?ticket=%s&email=%s" % (
|
||||
site.domain,
|
||||
reverse('helpdesk_public_view'),
|
||||
self.ticket_for_url,
|
||||
self.submitter_email
|
||||
)
|
||||
ticket_url = property(_get_ticket_url)
|
||||
|
||||
def _get_staff_url(self):
|
||||
"""
|
||||
Returns a staff-only URL for this ticket, used when giving a URL to
|
||||
a staff member (in emails etc)
|
||||
"""
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.urlresolvers import reverse
|
||||
site = Site.objects.get_current()
|
||||
return u"http://%s%s" % (site.domain, reverse('helpdesk_view', args=[self.id]))
|
||||
return u"http://%s%s" % (
|
||||
site.domain,
|
||||
reverse('helpdesk_view',
|
||||
args=[self.id])
|
||||
)
|
||||
staff_url = property(_get_staff_url)
|
||||
|
||||
class Meta:
|
||||
@ -185,67 +394,120 @@ class Ticket(models.Model):
|
||||
if not self.id:
|
||||
# This is a new ticket as no ID yet exists.
|
||||
self.created = datetime.now()
|
||||
|
||||
|
||||
if not self.priority:
|
||||
self.priority = 3
|
||||
|
||||
|
||||
self.modified = datetime.now()
|
||||
|
||||
super(Ticket, self).save()
|
||||
|
||||
|
||||
class FollowUpManager(models.Manager):
|
||||
def private_followups(self):
|
||||
return self.filter(public=False)
|
||||
|
||||
|
||||
def public_followups(self):
|
||||
return self.filter(public=True)
|
||||
|
||||
|
||||
class FollowUp(models.Model):
|
||||
""" A FollowUp is a comment and/or change to a ticket. We keep a simple
|
||||
title, the comment entered by the user, and the new status of a ticket
|
||||
"""
|
||||
A FollowUp is a comment and/or change to a ticket. We keep a simple
|
||||
title, the comment entered by the user, and the new status of a ticket
|
||||
to enable easy flagging of details on the view-ticket page.
|
||||
|
||||
The title is automatically generated at save-time, based on what action
|
||||
the user took.
|
||||
|
||||
Tickets that aren't public are never shown to or e-mailed to the submitter,
|
||||
Tickets that aren't public are never shown to or e-mailed to the submitter,
|
||||
although all staff can see them.
|
||||
"""
|
||||
|
||||
ticket = models.ForeignKey(Ticket)
|
||||
date = models.DateTimeField(_('Date'), auto_now_add=True)
|
||||
title = models.CharField(_('Title'), max_length=200, blank=True, null=True)
|
||||
comment = models.TextField(_('Comment'), blank=True, null=True)
|
||||
public = models.BooleanField(_('Public'), blank=True, null=True)
|
||||
user = models.ForeignKey(User, blank=True, null=True)
|
||||
|
||||
new_status = models.IntegerField(_('New Status'), choices=Ticket.STATUS_CHOICES, blank=True, null=True)
|
||||
|
||||
date = models.DateTimeField(
|
||||
_('Date'),
|
||||
)
|
||||
|
||||
title = models.CharField(
|
||||
_('Title'),
|
||||
max_length=200,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
comment = models.TextField(
|
||||
_('Comment'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
public = models.BooleanField(
|
||||
_('Public'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Public tickets are viewable by the submitter and all '
|
||||
'staff, but non-public tickets can only be seen by staff.'),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
new_status = models.IntegerField(
|
||||
_('New Status'),
|
||||
choices=Ticket.STATUS_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('If the status was changed, what was it changed to?'),
|
||||
)
|
||||
|
||||
objects = FollowUpManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['date']
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
||||
|
||||
|
||||
def save(self):
|
||||
t = self.ticket
|
||||
t.modified = datetime.now()
|
||||
self.date = datetime.now()
|
||||
t.save()
|
||||
super(FollowUp, self).save()
|
||||
|
||||
|
||||
class TicketChange(models.Model):
|
||||
""" For each FollowUp, any changes to the parent ticket (eg Title, Priority,
|
||||
"""
|
||||
For each FollowUp, any changes to the parent ticket (eg Title, Priority,
|
||||
etc) are tracked here for display purposes.
|
||||
"""
|
||||
|
||||
followup = models.ForeignKey(FollowUp)
|
||||
field = models.CharField(_('Field'), max_length=100)
|
||||
old_value = models.TextField(_('Old Value'), blank=True, null=True)
|
||||
new_value = models.TextField(_('New Value'), blank=True, null=True)
|
||||
|
||||
field = models.CharField(
|
||||
_('Field'),
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
old_value = models.TextField(
|
||||
_('Old Value'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
new_value = models.TextField(
|
||||
_('New Value'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
str = u'%s ' % field
|
||||
@ -254,7 +516,10 @@ class TicketChange(models.Model):
|
||||
elif not old_value:
|
||||
str += _('set to %s' % new_value)
|
||||
else:
|
||||
str += _('changed from "%(old_value)s" to "%(new_value)s"' % {'old_value': old_value, 'new_value': new_value})
|
||||
str += _('changed from "%(old_value)s" to "%(new_value)s"' % {
|
||||
'old_value': old_value,
|
||||
'new_value': new_value
|
||||
})
|
||||
return str
|
||||
|
||||
|
||||
@ -263,7 +528,7 @@ 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
|
||||
|
||||
@ -282,18 +547,43 @@ class DynamicFileField(models.FileField):
|
||||
|
||||
def db_type(self):
|
||||
"""Required by Django for ORM."""
|
||||
return 'varchar(100)'
|
||||
return 'varchar(100)'
|
||||
|
||||
|
||||
class Attachment(models.Model):
|
||||
"""
|
||||
Represents a file attached to a follow-up. This could come from an e-mail
|
||||
attachment, or it could be uploaded via the web interface.
|
||||
"""
|
||||
|
||||
followup = models.ForeignKey(FollowUp)
|
||||
file = DynamicFileField(_('File'), upload_to='helpdesk/attachments')
|
||||
filename = models.CharField(_('Filename'), max_length=100)
|
||||
mime_type = models.CharField(_('MIME Type'), max_length=30)
|
||||
size = models.IntegerField(_('Size'), help_text=_('Size of this file in bytes'))
|
||||
|
||||
file = DynamicFileField(
|
||||
_('File'),
|
||||
upload_to='helpdesk/attachments',
|
||||
)
|
||||
|
||||
filename = models.CharField(
|
||||
_('Filename'),
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
mime_type = models.CharField(
|
||||
_('MIME Type'),
|
||||
max_length=30,
|
||||
)
|
||||
|
||||
size = models.IntegerField(
|
||||
_('Size'),
|
||||
help_text=_('Size of this file in bytes'),
|
||||
)
|
||||
|
||||
def get_upload_to(self, field_attname):
|
||||
""" Get upload_to path specific to this item """
|
||||
return u'helpdesk/attachments/%s/%s' % (self.followup.ticket.ticket_for_url, self.followup.id)
|
||||
return u'helpdesk/attachments/%s/%s' % (
|
||||
self.followup.ticket.ticket_for_url,
|
||||
self.followup.id
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.filename
|
||||
@ -303,18 +593,38 @@ class Attachment(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
|
||||
simplify the sending of updates and resolutions. These are basically Django
|
||||
templates with a limited context - however if yo uwanted to get crafy it would
|
||||
be easy to write a reply that displays ALL updates in hierarchical order etc
|
||||
templates with a limited context - however if you wanted to get crafy it would
|
||||
be easy to write a reply that displays ALL updates in hierarchical order etc
|
||||
with use of for loops over {{ ticket.followup_set.all }} and friends.
|
||||
|
||||
|
||||
When replying to a ticket, the user can select any reply set for the current
|
||||
queue, and the body text is fetched via AJAX."""
|
||||
|
||||
queues = models.ManyToManyField(Queue, blank=True, null=True, help_text=_('Leave blank to allow this reply to be used for all queues, or select those queues you wish to limit this reply to.'))
|
||||
name = models.CharField(_('Name'), max_length=100, help_text=_('Only used to assist users with selecting a reply - not shown to the user.'))
|
||||
body = models.TextField(_('Body'), help_text=_('Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); {{ queue }} - The queue; and {{ user }} - the current user.'))
|
||||
queue, and the body text is fetched via AJAX.
|
||||
"""
|
||||
|
||||
queues = models.ManyToManyField(
|
||||
Queue,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Leave blank to allow this reply to be used for all '
|
||||
'queues, or select those queues you wish to limit this reply to.'),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
_('Name'),
|
||||
max_length=100,
|
||||
help_text=_('Only used to assist users with selecting a reply - not '
|
||||
'shown to the user.'),
|
||||
)
|
||||
|
||||
body = models.TextField(
|
||||
_('Body'),
|
||||
help_text=_('Context available: {{ ticket }} - ticket object (eg '
|
||||
'{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} '
|
||||
'- the current user.'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name',]
|
||||
@ -322,28 +632,84 @@ class PreSetReply(models.Model):
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.name
|
||||
|
||||
class EscalationExclusion(models.Model):
|
||||
queues = models.ManyToManyField(Queue, blank=True, null=True, help_text=_('Leave blank for this exclusion to be applied to all queues, or select those queues you wish to exclude with this entry.'))
|
||||
|
||||
name = models.CharField(_('Name'), max_length=100)
|
||||
|
||||
date = models.DateField(_('Date'), help_text=_('Date on which escalation should not happen'))
|
||||
class EscalationExclusion(models.Model):
|
||||
"""
|
||||
An 'EscalationExclusion' lets us define a date on which escalation should
|
||||
not happen, for example a weekend or public holiday.
|
||||
|
||||
You may also have a queue that is only used on one day per week.
|
||||
|
||||
To create these on a regular basis, check out the README file for an
|
||||
example cronjob that runs 'create_escalation_exclusions.py'.
|
||||
"""
|
||||
|
||||
queues = models.ManyToManyField(
|
||||
Queue,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Leave blank for this exclusion to be applied to all '
|
||||
'queues, or select those queues you wish to exclude with this '
|
||||
'entry.'),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
_('Name'),
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
date = models.DateField(
|
||||
_('Date'),
|
||||
help_text=_('Date on which escalation should not happen'),
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.name
|
||||
|
||||
|
||||
class EmailTemplate(models.Model):
|
||||
"""
|
||||
Since these are more likely to be changed than other templates, we store
|
||||
them in the database.
|
||||
them in the database.
|
||||
|
||||
This means that an admin can change email templates without having to have
|
||||
access to the filesystem.
|
||||
"""
|
||||
|
||||
template_name = models.CharField(_('Template Name'), max_length=100, unique=True)
|
||||
|
||||
subject = models.CharField(_('Subject'), max_length=100, help_text=_('This will be prefixed with "[ticket.ticket] ticket.title". We recommend something simple such as "(Updated") or "(Closed)" - the same context is available as in plain_text, below.'))
|
||||
heading = models.CharField(_('Heading'), max_length=100, help_text=_('In HTML e-mails, this will be the heading at the top of the email - the same context is available as in plain_text, below.'))
|
||||
plain_text = models.TextField(_('Plain Text'), help_text=_('The context available to you includes {{ ticket }}, {{ queue }}, and depending on the time of the call: {{ resolution }} or {{ comment }}.'))
|
||||
html = models.TextField(_('HTML'), help_text=_('The same context is available here as in plain_text, above.'))
|
||||
template_name = models.CharField(
|
||||
_('Template Name'),
|
||||
max_length=100,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
subject = models.CharField(
|
||||
_('Subject'),
|
||||
max_length=100,
|
||||
help_text=_('This will be prefixed with "[ticket.ticket] ticket.title"'
|
||||
'. We recommend something simple such as "(Updated") or "(Closed)"'
|
||||
' - the same context is available as in plain_text, below.'),
|
||||
)
|
||||
|
||||
heading = models.CharField(
|
||||
_('Heading'),
|
||||
max_length=100,
|
||||
help_text=_('In HTML e-mails, this will be the heading at the top of '
|
||||
'the email - the same context is available as in plain_text, '
|
||||
'below.'),
|
||||
)
|
||||
|
||||
plain_text = models.TextField(
|
||||
_('Plain Text'),
|
||||
help_text=_('The context available to you includes {{ ticket }}, '
|
||||
'{{ queue }}, and depending on the time of the call: '
|
||||
'{{ resolution }} or {{ comment }}.'),
|
||||
)
|
||||
|
||||
html = models.TextField(
|
||||
_('HTML'),
|
||||
help_text=_('The same context is available here as in plain_text, '
|
||||
'above.'),
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.template_name
|
||||
@ -351,40 +717,72 @@ class EmailTemplate(models.Model):
|
||||
class Meta:
|
||||
ordering = ['template_name',]
|
||||
|
||||
|
||||
class KBCategory(models.Model):
|
||||
"""
|
||||
Lets help users help themselves: the Knowledge Base is a categorised
|
||||
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'))
|
||||
|
||||
title = models.CharField(
|
||||
_('Title'),
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
slug = models.SlugField(
|
||||
_('Slug'),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
_('Description'),
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.title
|
||||
|
||||
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
|
||||
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'))
|
||||
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'),
|
||||
help_text=_('The date on which this question was most recently '
|
||||
'changed.'),
|
||||
)
|
||||
|
||||
def save(self):
|
||||
self.last_updated = datetime.now()
|
||||
@ -396,7 +794,7 @@ class KBItem(models.Model):
|
||||
else:
|
||||
return _('Unrated')
|
||||
score = property(_score)
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.title
|
||||
|
||||
@ -406,3 +804,4 @@ class KBItem(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return ('helpdesk_kb_item', [str(self.id)])
|
||||
get_absolute_url = models.permalink(get_absolute_url)
|
||||
|
||||
|
@ -3,7 +3,7 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
templatetags/in_list.py - Very simple template tag to allow us to use the
|
||||
templatetags/in_list.py - Very simple template tag to allow us to use the
|
||||
equivilent of 'if x in y' in templates. eg:
|
||||
|
||||
Assuming 'food' = 'pizza' and 'best_foods' = ['pizza', 'pie', 'cake]:
|
||||
@ -14,8 +14,11 @@ Assuming 'food' = 'pizza' and 'best_foods' = ['pizza', 'pie', 'cake]:
|
||||
Your food isn't one of our favourites.
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
from django import template
|
||||
|
||||
def in_list(value, arg):
|
||||
return value in arg
|
||||
|
||||
register = template.Library()
|
||||
register.filter(in_list)
|
||||
|
@ -5,18 +5,24 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
templatetags/ticket_to_link.py - Used in ticket comments to allow wiki-style
|
||||
linking to other tickets. Including text such
|
||||
as '#3180' in a comment automatically links
|
||||
as '#3180' in a comment automatically links
|
||||
that text to ticket number 3180, with styling
|
||||
to show the status of that ticket (eg a closed
|
||||
ticket would have a strikethrough).
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django import template
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from helpdesk.models import Ticket
|
||||
|
||||
|
||||
class ReverseProxy:
|
||||
def __init__(self, sequence):
|
||||
self.sequence = sequence
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
length = len(self.sequence)
|
||||
i = length
|
||||
@ -24,16 +30,15 @@ class ReverseProxy:
|
||||
i = i - 1
|
||||
yield self.sequence[i]
|
||||
|
||||
|
||||
def num_to_link(text):
|
||||
if text == '':
|
||||
return text
|
||||
import re
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
matches = []
|
||||
for match in re.finditer("#(\d+)", text):
|
||||
matches.append(match)
|
||||
|
||||
|
||||
for match in ReverseProxy(matches):
|
||||
start = match.start()
|
||||
end = match.end()
|
||||
|
38
urls.py
38
urls.py
@ -3,43 +3,41 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
urls.py - Mapping of URL's to our various views. Note we always used NAMED
|
||||
urls.py - Mapping of URL's to our various views. Note we always used NAMED
|
||||
views for simplicity in linking later on.
|
||||
"""
|
||||
|
||||
from django.conf.urls.defaults import *
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.syndication.views import feed as django_feed
|
||||
|
||||
from helpdesk.views.feeds import feed_setup
|
||||
|
||||
from django.contrib.syndication.views import feed as django_feed
|
||||
|
||||
urlpatterns = patterns('helpdesk.views.staff',
|
||||
url(r'^dashboard/$',
|
||||
url(r'^dashboard/$',
|
||||
'dashboard',
|
||||
name='helpdesk_dashboard'),
|
||||
|
||||
url(r'^tickets/$',
|
||||
|
||||
url(r'^tickets/$',
|
||||
'ticket_list',
|
||||
name='helpdesk_list'),
|
||||
|
||||
url(r'^tickets/submit/$',
|
||||
|
||||
url(r'^tickets/submit/$',
|
||||
'create_ticket',
|
||||
name='helpdesk_submit'),
|
||||
|
||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/$',
|
||||
'view_ticket',
|
||||
name='helpdesk_view'),
|
||||
|
||||
|
||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$',
|
||||
'update_ticket',
|
||||
name='helpdesk_update'),
|
||||
|
||||
|
||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/delete/$',
|
||||
'delete_ticket',
|
||||
name='helpdesk_delete'),
|
||||
|
||||
|
||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/hold/$',
|
||||
'hold_ticket',
|
||||
name='helpdesk_hold'),
|
||||
@ -51,7 +49,7 @@ urlpatterns = patterns('helpdesk.views.staff',
|
||||
url(r'^raw/(?P<type>\w+)/$',
|
||||
'raw_details',
|
||||
name='helpdesk_raw'),
|
||||
|
||||
|
||||
url(r'^rss/$',
|
||||
'rss_list',
|
||||
name='helpdesk_rss_index'),
|
||||
@ -59,14 +57,14 @@ urlpatterns = patterns('helpdesk.views.staff',
|
||||
url(r'^reports/$',
|
||||
'report_index',
|
||||
name='helpdesk_report_index'),
|
||||
|
||||
|
||||
url(r'^reports/(?P<report>\w+)/$',
|
||||
'run_report',
|
||||
name='helpdesk_run_report'),
|
||||
)
|
||||
|
||||
urlpatterns += patterns('helpdesk.views.public',
|
||||
url(r'^$',
|
||||
url(r'^$',
|
||||
'homepage',
|
||||
name='helpdesk_home'),
|
||||
|
||||
@ -80,7 +78,7 @@ urlpatterns += patterns('',
|
||||
login_required(django_feed),
|
||||
{'feed_dict': feed_setup},
|
||||
name='helpdesk_rss'),
|
||||
|
||||
|
||||
url(r'^api/(?P<method>[a-z_-]+)/$',
|
||||
'helpdesk.views.api.api',
|
||||
name='helpdesk_api'),
|
||||
@ -89,7 +87,7 @@ urlpatterns += patterns('',
|
||||
'django.views.generic.simple.direct_to_template',
|
||||
{'template': 'helpdesk/api_help.html',},
|
||||
name='helpdesk_api_help'),
|
||||
|
||||
|
||||
url(r'^login/$',
|
||||
'django.contrib.auth.views.login',
|
||||
name='login'),
|
||||
@ -102,13 +100,13 @@ urlpatterns += patterns('',
|
||||
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'),
|
||||
)
|
||||
|
150
views/api.py
150
views/api.py
@ -1,4 +1,4 @@
|
||||
""" ..
|
||||
""" ..
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
@ -6,23 +6,24 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
api.py - Wrapper around API calls, and core functions to provide complete
|
||||
API to third party applications.
|
||||
|
||||
The API documentation can be accessed by visiting http://helpdesk/api/help/
|
||||
The API documentation can be accessed by visiting http://helpdesk/api/help/
|
||||
(obviously, substitute helpdesk for your Jutda Helpdesk URI), or by reading
|
||||
through templates/helpdesk/api_help.html.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from django.utils import simplejson
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from datetime import datetime
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import loader, Context
|
||||
from django import forms
|
||||
from django.utils import simplejson
|
||||
|
||||
from helpdesk.forms import TicketForm
|
||||
from helpdesk.lib import send_templated_mail
|
||||
from helpdesk.models import Ticket, Queue, FollowUp
|
||||
from helpdesk.forms import TicketForm
|
||||
|
||||
STATUS_OK = 200
|
||||
|
||||
@ -31,27 +32,34 @@ STATUS_ERROR_NOT_FOUND = 404
|
||||
STATUS_ERROR_PERMISSIONS = 403
|
||||
STATUS_ERROR_BADMETHOD = 405
|
||||
|
||||
def api(request, method):
|
||||
if method == 'help':
|
||||
""" Regardless of any other paramaters, we provide a help screen
|
||||
to the user if they requested one. """
|
||||
return render_to_response('helpdesk/api_help.html')
|
||||
|
||||
def api(request, method):
|
||||
"""
|
||||
Regardless of any other paramaters, we provide a help screen
|
||||
to the user if they requested one.
|
||||
|
||||
If the user isn't looking for help, then we enforce a few conditions:
|
||||
* The request must be sent via HTTP POST
|
||||
* The request must contain a 'user' and 'password' which
|
||||
must be valid users
|
||||
* The method must match one of the public methods of the API class.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
if method == 'help':
|
||||
return render_to_response('helpdesk/api_help.html')
|
||||
|
||||
if request.method != 'POST':
|
||||
return api_return(STATUS_ERROR_BADMETHOD)
|
||||
|
||||
request.user = authenticate(username=request.POST.get('user', False), password=request.POST.get('password'))
|
||||
|
||||
# TODO: Move away from having the username & password in every request.
|
||||
request.user = authenticate(
|
||||
username=request.POST.get('user', False),
|
||||
password=request.POST.get('password'))
|
||||
|
||||
if request.user is None:
|
||||
return api_return(STATUS_ERROR_PERMISSIONS)
|
||||
|
||||
|
||||
api = API(request)
|
||||
if hasattr(api, 'api_public_%s' % method):
|
||||
return getattr(api, 'api_public_%s' % method)()
|
||||
@ -63,7 +71,7 @@ def api_return(status, text='', json=False):
|
||||
content_type = 'text/plain'
|
||||
if status == STATUS_OK and json:
|
||||
content_type = 'text/json'
|
||||
|
||||
|
||||
if text is None:
|
||||
if status == STATUS_ERROR:
|
||||
text = 'Error'
|
||||
@ -75,10 +83,12 @@ def api_return(status, text='', json=False):
|
||||
text = 'Invalid request method'
|
||||
elif status == STATUS_OK:
|
||||
text = 'OK'
|
||||
|
||||
r = HttpResponse(status=status, content=text, content_type=content_type)
|
||||
|
||||
|
||||
if status == STATUS_ERROR_BADMETHOD:
|
||||
r.Allow = 'POST'
|
||||
|
||||
return r
|
||||
|
||||
|
||||
@ -86,27 +96,30 @@ class API:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def api_public_create_ticket(self):
|
||||
|
||||
def api_public_create_ticket(self):
|
||||
form = TicketForm(self.request.POST)
|
||||
form.fields['queue'].choices = [[q.id, q.title] for q in Queue.objects.all()]
|
||||
form.fields['assigned_to'].choices = [[u.id, u.username] for u in User.objects.filter(is_active=True)]
|
||||
|
||||
if form.is_valid():
|
||||
ticket = form.save(user=self.request.user)
|
||||
return api_return(STATUS_OK, "%s" % ticket.id)
|
||||
else:
|
||||
return api_return(STATUS_ERROR, text=form.errors.as_text())
|
||||
|
||||
|
||||
|
||||
def api_public_list_queues(self):
|
||||
return api_return(STATUS_OK, simplejson.dumps([{"id": "%s" % q.id, "title": "%s" % q.title} for q in Queue.objects.all()]), json=True)
|
||||
|
||||
|
||||
|
||||
|
||||
def api_public_find_user(self):
|
||||
username = self.request.POST.get('username', False)
|
||||
|
||||
try:
|
||||
u = User.objects.get(username=username)
|
||||
return api_return(STATUS_OK, "%s" % u.id)
|
||||
|
||||
except User.DoesNotExist:
|
||||
return api_return(STATUS_ERROR, "Invalid username provided")
|
||||
|
||||
@ -121,9 +134,10 @@ class API:
|
||||
return api_return(STATUS_ERROR, "Invalid ticket ID")
|
||||
|
||||
ticket.delete()
|
||||
|
||||
return api_return(STATUS_OK)
|
||||
|
||||
|
||||
|
||||
|
||||
def api_public_hold_ticket(self):
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
||||
@ -135,7 +149,7 @@ class API:
|
||||
|
||||
return api_return(STATUS_OK)
|
||||
|
||||
|
||||
|
||||
def api_public_unhold_ticket(self):
|
||||
try:
|
||||
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
||||
@ -162,12 +176,20 @@ class API:
|
||||
|
||||
if not message:
|
||||
return api_return(STATUS_ERROR, "Blank message")
|
||||
|
||||
f = FollowUp(ticket=ticket, date=datetime.now(), comment=message, user=self.request.user, title='Comment Added')
|
||||
|
||||
f = FollowUp(
|
||||
ticket=ticket,
|
||||
date=datetime.now(),
|
||||
comment=message,
|
||||
user=self.request.user,
|
||||
title='Comment Added',
|
||||
)
|
||||
|
||||
if public:
|
||||
f.public = True
|
||||
|
||||
f.save()
|
||||
|
||||
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
'queue': ticket.queue,
|
||||
@ -175,13 +197,31 @@ class API:
|
||||
}
|
||||
|
||||
if public and ticket.submitter_email:
|
||||
send_templated_mail('updated_submitter', context, recipients=ticket.submitter_email, sender=ticket.queue.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
'updated_submitter',
|
||||
context,
|
||||
recipients=ticket.submitter_email,
|
||||
sender=ticket.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if ticket.queue.updated_ticket_cc:
|
||||
send_templated_mail('updated_cc', context, recipients=ticket.queue.updated_ticket_cc, sender=ticket.queue.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
'updated_cc',
|
||||
context,
|
||||
recipients=ticket.queue.updated_ticket_cc,
|
||||
sender=ticket.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if ticket.assigned_to and self.request.user != ticket.assigned_to:
|
||||
send_templated_mail('updated_owner', context, recipients=ticket.assigned_to.email, sender=ticket.queue.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'updated_owner',
|
||||
context,
|
||||
recipients=ticket.assigned_to.email,
|
||||
sender=ticket.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
ticket.save()
|
||||
|
||||
@ -198,26 +238,51 @@ class API:
|
||||
|
||||
if not resolution:
|
||||
return api_return(STATUS_ERROR, "Blank resolution")
|
||||
|
||||
f = FollowUp(ticket=ticket, date=datetime.now(), comment=resolution, user=self.request.user, title='Resolved', public=True)
|
||||
|
||||
f = FollowUp(
|
||||
ticket=ticket,
|
||||
date=datetime.now(),
|
||||
comment=resolution,
|
||||
user=self.request.user,
|
||||
title='Resolved',
|
||||
public=True,
|
||||
)
|
||||
f.save()
|
||||
|
||||
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
'queue': ticket.queue,
|
||||
'resolution': f.comment,
|
||||
}
|
||||
|
||||
|
||||
subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title)
|
||||
|
||||
if ticket.submitter_email:
|
||||
send_templated_mail('resolved_submitter', context, recipients=ticket.submitter_email, sender=ticket.queue.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
'resolved_submitter',
|
||||
context,
|
||||
recipients=ticket.submitter_email,
|
||||
sender=ticket.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if ticket.queue.updated_ticket_cc:
|
||||
send_templated_mail('resolved_cc', context, recipients=ticket.queue.updated_ticket_cc, sender=ticket.queue.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
'resolved_cc',
|
||||
context,
|
||||
recipients=ticket.queue.updated_ticket_cc,
|
||||
sender=ticket.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if ticket.assigned_to and self.request.user != ticket.assigned_to:
|
||||
send_templated_mail('resolved_resolved', context, recipients=ticket.assigned_to.email, sender=ticket.queue.from_address, fail_silently=True)
|
||||
send_templated_mail(
|
||||
'resolved_resolved',
|
||||
context,
|
||||
recipients=ticket.assigned_to.email,
|
||||
sender=ticket.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
ticket.resoltuion = f.comment
|
||||
ticket.status = Ticket.RESOLVED_STATUS
|
||||
@ -225,3 +290,4 @@ class API:
|
||||
ticket.save()
|
||||
|
||||
return api_return(STATUS_OK)
|
||||
|
||||
|
@ -1,3 +1,12 @@
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
views/feeds.py - A handful of staff-only RSS feeds to provide ticket details
|
||||
to feed readers or similar software.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.syndication.feeds import Feed
|
||||
from django.core.urlresolvers import reverse
|
||||
@ -23,28 +32,55 @@ class OpenTicketsByUser(Feed):
|
||||
|
||||
def title(self, obj):
|
||||
if obj['queue']:
|
||||
return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {'queue': obj['queue'].title, 'username': obj['user'].username}
|
||||
return _("Helpdesk: Open Tickets in queue %(queue)s for %(username)s") % {
|
||||
'queue': obj['queue'].title,
|
||||
'username': obj['user'].username,
|
||||
}
|
||||
else:
|
||||
return _("Helpdesk: Open Tickets for %(username)s") % {'username': obj['user'].username}
|
||||
return _("Helpdesk: Open Tickets for %(username)s") % {
|
||||
'username': obj['user'].username,
|
||||
}
|
||||
|
||||
def description(self, obj):
|
||||
if obj['queue']:
|
||||
return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % {'queue': obj['queue'].title, 'username': obj['user'].username}
|
||||
return _("Open and Reopened Tickets in queue %(queue)s for %(username)s") % {
|
||||
'queue': obj['queue'].title,
|
||||
'username': obj['user'].username,
|
||||
}
|
||||
else:
|
||||
return _("Open and Reopened Tickets for %(username)s") % {'username': obj['user'].username}
|
||||
return _("Open and Reopened Tickets for %(username)s") % {
|
||||
'username': obj['user'].username,
|
||||
}
|
||||
|
||||
def link(self, obj):
|
||||
if obj['queue']:
|
||||
return u'%s?assigned_to=%s&queue=%s' % (reverse('helpdesk_list'), obj['user'].id, obj['queue'].id)
|
||||
return u'%s?assigned_to=%s&queue=%s' % (
|
||||
reverse('helpdesk_list'),
|
||||
obj['user'].id,
|
||||
obj['queue'].id,
|
||||
)
|
||||
else:
|
||||
return u'%s?assigned_to=%s' % (reverse('helpdesk_list'), obj['user'].id)
|
||||
return u'%s?assigned_to=%s' % (
|
||||
reverse('helpdesk_list'),
|
||||
obj['user'].id,
|
||||
)
|
||||
|
||||
def items(self, obj):
|
||||
if obj['queue']:
|
||||
return Ticket.objects.filter(assigned_to=obj['user']).filter(queue=obj['queue']).filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS))
|
||||
return Ticket.objects.filter(
|
||||
assigned_to=obj['user']
|
||||
).filter(
|
||||
queue=obj['queue']
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
else:
|
||||
return Ticket.objects.filter(assigned_to=obj['user']).filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS))
|
||||
|
||||
return Ticket.objects.filter(
|
||||
assigned_to=obj['user']
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
|
||||
@ -58,18 +94,22 @@ class OpenTicketsByUser(Feed):
|
||||
class UnassignedTickets(Feed):
|
||||
title_template = 'helpdesk/rss/ticket_title.html'
|
||||
description_template = 'helpdesk/rss/ticket_description.html'
|
||||
|
||||
|
||||
title = _('Helpdesk: Unassigned Tickets')
|
||||
description = _('Unassigned Open and Reopened tickets')
|
||||
link = ''#%s?assigned_to=' % reverse('helpdesk_list')
|
||||
|
||||
def items(self, obj):
|
||||
return Ticket.objects.filter(assigned_to__isnull=True).filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS))
|
||||
|
||||
return Ticket.objects.filter(
|
||||
assigned_to__isnull=True
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
|
||||
|
||||
|
||||
|
||||
def item_author_name(self, item):
|
||||
if item.assigned_to:
|
||||
return item.assigned_to.username
|
||||
@ -99,20 +139,31 @@ class OpenTicketsByQueue(Feed):
|
||||
return Queue.objects.get(slug__exact=bits[0])
|
||||
|
||||
def title(self, obj):
|
||||
return _('Helpdesk: Open Tickets in queue %(queue)s') % {'queue': obj.title}
|
||||
return _('Helpdesk: Open Tickets in queue %(queue)s') % {
|
||||
'queue': obj.title,
|
||||
}
|
||||
|
||||
def description(self, obj):
|
||||
return _('Open and Reopened Tickets in queue %(queue)s') % {'queue': obj.title}
|
||||
return _('Open and Reopened Tickets in queue %(queue)s') % {
|
||||
'queue': obj.title,
|
||||
}
|
||||
|
||||
def link(self, obj):
|
||||
return '%s?queue=%s' % (reverse('helpdesk_list'), obj.id)
|
||||
return '%s?queue=%s' % (
|
||||
reverse('helpdesk_list'),
|
||||
obj.id,
|
||||
)
|
||||
|
||||
def items(self, obj):
|
||||
return Ticket.objects.filter(queue=obj).filter(Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS))
|
||||
return Ticket.objects.filter(
|
||||
queue=obj
|
||||
).filter(
|
||||
Q(status=Ticket.OPEN_STATUS) | Q(status=Ticket.REOPENED_STATUS)
|
||||
)
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.created
|
||||
|
||||
|
||||
def item_author_name(self, item):
|
||||
if item.assigned_to:
|
||||
return item.assigned_to.username
|
||||
|
14
views/kb.py
14
views/kb.py
@ -3,10 +3,11 @@ 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
|
||||
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
|
||||
@ -16,23 +17,26 @@ 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',
|
||||
# TODO: It'd be great to have a list of 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',
|
||||
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',
|
||||
@ -40,6 +44,7 @@ def item(request, item):
|
||||
'item': item,
|
||||
}))
|
||||
|
||||
|
||||
def vote(request, item):
|
||||
item = get_object_or_404(KBItem, pk=item)
|
||||
vote = request.GET.get('vote', None)
|
||||
@ -50,3 +55,4 @@ def vote(request, item):
|
||||
item.save()
|
||||
|
||||
return HttpResponseRedirect(item.get_absolute_url())
|
||||
|
||||
|
@ -6,6 +6,7 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
views/public.py - All public facing views, eg non-staff (no authentication
|
||||
required) views.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
@ -18,16 +19,21 @@ 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))
|
||||
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)]
|
||||
@ -37,24 +43,30 @@ def homepage(request):
|
||||
'form': form,
|
||||
}))
|
||||
|
||||
|
||||
def view_ticket(request):
|
||||
ticket = request.GET.get('ticket', '')
|
||||
email = request.GET.get('email', '')
|
||||
error_message = ''
|
||||
|
||||
if ticket and email:
|
||||
queue, ticket_id = ticket.split('-')
|
||||
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 Ticket.DoesNotExist:
|
||||
t = False;
|
||||
error_message = _('Invalid ticket ID or e-mail address. Please try again.')
|
||||
|
||||
return render_to_response('helpdesk/public_view_form.html',
|
||||
if t:
|
||||
return render_to_response('helpdesk/public_view_ticket.html',
|
||||
RequestContext(request, {
|
||||
'ticket': t,
|
||||
}))
|
||||
|
||||
return render_to_response('helpdesk/public_view_form.html',
|
||||
RequestContext(request, {
|
||||
'ticket': ticket,
|
||||
'email': email,
|
||||
'error_message': error_message,
|
||||
}))
|
||||
|
||||
|
171
views/staff.py
171
views/staff.py
@ -3,15 +3,17 @@ Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||
|
||||
views/staff.py - The bulk of the application - provides most business logic and
|
||||
views/staff.py - The bulk of the application - provides most business logic and
|
||||
renders all staff-facing views.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import connection
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect, Http404, HttpResponse
|
||||
from django.shortcuts import render_to_response, get_object_or_404
|
||||
@ -22,17 +24,28 @@ 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):
|
||||
"""
|
||||
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
|
||||
showing ticket counts by queue/status, and a list of unassigned tickets
|
||||
with options for them to 'Take' ownership of said tickets.
|
||||
"""
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
# The following query builds a grid of queues & ticket statuses,
|
||||
# to be displayed to the user. EG:
|
||||
# Open Resolved
|
||||
# Queue 1 10 4
|
||||
# Queue 2 4 12
|
||||
|
||||
from django.db import connection
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("""
|
||||
SELECT q.id as queue,
|
||||
@ -55,9 +68,10 @@ def dashboard(request):
|
||||
}))
|
||||
dashboard = login_required(dashboard)
|
||||
|
||||
|
||||
def delete_ticket(request, ticket_id):
|
||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_to_response('helpdesk/delete_ticket.html',
|
||||
RequestContext(request, {
|
||||
@ -68,18 +82,31 @@ def delete_ticket(request, ticket_id):
|
||||
return HttpResponseRedirect(reverse('helpdesk_home'))
|
||||
delete_ticket = login_required(delete_ticket)
|
||||
|
||||
|
||||
def view_ticket(request, ticket_id):
|
||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||
|
||||
if request.GET.has_key('take'):
|
||||
# Allow the user to assign the ticket to themselves whilst viewing it.
|
||||
ticket.assigned_to = request.user
|
||||
ticket.save()
|
||||
|
||||
if request.GET.has_key('close') and ticket.status == Ticket.RESOLVED_STATUS:
|
||||
if not ticket.assigned_to:
|
||||
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')}
|
||||
|
||||
# Trick the update_ticket() view into thinking it's being called with
|
||||
# a valid POST.
|
||||
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',
|
||||
@ -91,6 +118,7 @@ def view_ticket(request, ticket_id):
|
||||
}))
|
||||
view_ticket = login_required(view_ticket)
|
||||
|
||||
|
||||
def update_ticket(request, ticket_id):
|
||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||
|
||||
@ -100,26 +128,27 @@ def update_ticket(request, ticket_id):
|
||||
public = request.POST.get('public', False)
|
||||
owner = int(request.POST.get('owner', None))
|
||||
priority = int(request.POST.get('priority', ticket.priority))
|
||||
|
||||
|
||||
if not owner and ticket.assigned_to:
|
||||
owner = ticket.assigned_to.id
|
||||
|
||||
f = FollowUp(ticket=ticket, date=datetime.now(), comment=comment, user=request.user)
|
||||
if public:
|
||||
f.public = True
|
||||
f.public = public
|
||||
|
||||
reassigned = False
|
||||
|
||||
if owner:
|
||||
if owner != 0 and (ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to:
|
||||
new_user = User.objects.get(id=owner)
|
||||
f.title = _('Assigned to %(username)s') % {'username': new_user.username}
|
||||
f.title = _('Assigned to %(username)s') % {
|
||||
'username': new_user.username,
|
||||
}
|
||||
ticket.assigned_to = new_user
|
||||
reassigned = True
|
||||
else:
|
||||
f.title = _('Unassigned')
|
||||
ticket.assigned_to = None
|
||||
|
||||
|
||||
if new_status != ticket.status:
|
||||
ticket.status = new_status
|
||||
ticket.save()
|
||||
@ -136,20 +165,30 @@ def update_ticket(request, ticket_id):
|
||||
f.title = _('Updated')
|
||||
|
||||
f.save()
|
||||
|
||||
|
||||
if title != ticket.title:
|
||||
c = TicketChange(followup=f, field=_('Title'), old_value=ticket.title, new_value=title)
|
||||
c = TicketChange(
|
||||
followup=f,
|
||||
field=_('Title'),
|
||||
old_value=ticket.title,
|
||||
new_value=title,
|
||||
)
|
||||
c.save()
|
||||
ticket.title = title
|
||||
|
||||
if priority != ticket.priority:
|
||||
c = TicketChange(followup=f, field=_('Priority'), old_value=ticket.priority, new_value=priority)
|
||||
c = TicketChange(
|
||||
followup=f,
|
||||
field=_('Priority'),
|
||||
old_value=ticket.priority,
|
||||
new_value=priority,
|
||||
)
|
||||
c.save()
|
||||
ticket.priority = priority
|
||||
|
||||
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))):
|
||||
context = {
|
||||
'ticket': ticket,
|
||||
@ -157,16 +196,24 @@ def update_ticket(request, ticket_id):
|
||||
'resolution': ticket.resolution,
|
||||
'comment': f.comment,
|
||||
}
|
||||
|
||||
if f.new_status == Ticket.RESOLVED_STATUS:
|
||||
template = 'resolved_submitter'
|
||||
elif f.new_status == Ticket.CLOSED_STATUS:
|
||||
template = 'closed_submitter'
|
||||
else:
|
||||
template = 'updated_submitter'
|
||||
send_templated_mail(template, context, recipients=ticket.submitter_email, sender=ticket.queue.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
template,
|
||||
context,
|
||||
recipients=ticket.submitter_email,
|
||||
sender=ticket.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
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
|
||||
# another user.
|
||||
if reassigned:
|
||||
template_staff = 'assigned_owner'
|
||||
@ -176,9 +223,15 @@ def update_ticket(request, ticket_id):
|
||||
template_staff = 'closed_owner'
|
||||
else:
|
||||
template_staff = 'updated_owner'
|
||||
|
||||
send_templated_mail(template_staff, context, recipients=ticket.assigned_to.email, sender=ticket.queue.from_address, fail_silently=True)
|
||||
|
||||
|
||||
send_templated_mail(
|
||||
template_staff,
|
||||
context,
|
||||
recipients=ticket.assigned_to.email,
|
||||
sender=ticket.queue.from_address,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
if ticket.queue.updated_ticket_cc:
|
||||
if reassigned:
|
||||
template_cc = 'assigned_cc'
|
||||
@ -188,22 +241,33 @@ def update_ticket(request, ticket_id):
|
||||
template_cc = 'closed_cc'
|
||||
else:
|
||||
template_cc = 'updated_cc'
|
||||
|
||||
send_templated_mail(template_cc, context, recipients=ticket.queue.updated_ticket_cc, sender=ticket.queue.from_address, fail_silently=True)
|
||||
|
||||
send_templated_mail(
|
||||
template_cc,
|
||||
context,
|
||||
recipients=ticket.queue.updated_ticket_cc,
|
||||
sender=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 = Attachment(
|
||||
followup=f,
|
||||
filename=filename,
|
||||
mime_type=file['content-type'],
|
||||
size=len(file['content']),
|
||||
)
|
||||
a.file.save(file['filename'], ContentFile(file['content']))
|
||||
a.save()
|
||||
|
||||
ticket.save()
|
||||
|
||||
|
||||
return HttpResponseRedirect(ticket.get_absolute_url())
|
||||
update_ticket = login_required(update_ticket)
|
||||
|
||||
|
||||
def ticket_list(request):
|
||||
tickets = Ticket.objects.select_related()
|
||||
context = {}
|
||||
@ -229,7 +293,7 @@ def ticket_list(request):
|
||||
|
||||
### KEYWORD SEARCHING
|
||||
q = request.GET.get('q', None)
|
||||
|
||||
|
||||
if q:
|
||||
qset = (
|
||||
Q(title__icontains=q) |
|
||||
@ -239,7 +303,7 @@ def ticket_list(request):
|
||||
)
|
||||
tickets = tickets.filter(qset)
|
||||
context = dict(context, query=q)
|
||||
|
||||
|
||||
### SORTING
|
||||
sort = request.GET.get('sort', None)
|
||||
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority'):
|
||||
@ -257,6 +321,7 @@ def ticket_list(request):
|
||||
)))
|
||||
ticket_list = login_required(ticket_list)
|
||||
|
||||
|
||||
def create_ticket(request):
|
||||
if request.method == 'POST':
|
||||
form = TicketForm(request.POST)
|
||||
@ -270,26 +335,32 @@ def create_ticket(request):
|
||||
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()]
|
||||
form.fields['assigned_to'].choices = [('', '--------')] + [[u.id, u.username] for u in User.objects.filter(is_active=True)]
|
||||
|
||||
return render_to_response('helpdesk/create_ticket.html',
|
||||
return render_to_response('helpdesk/create_ticket.html',
|
||||
RequestContext(request, {
|
||||
'form': form,
|
||||
}))
|
||||
create_ticket = login_required(create_ticket)
|
||||
|
||||
|
||||
def raw_details(request, type):
|
||||
# TODO: This currently only supports spewing out 'PreSetReply' objects,
|
||||
# in the future it needs to be expanded to include other items. All it
|
||||
# does is return a plain-text representation of an object.
|
||||
|
||||
if not type in ('preset',):
|
||||
raise Http404
|
||||
|
||||
|
||||
if type == 'preset' and request.GET.get('id', False):
|
||||
try:
|
||||
preset = PreSetReply.objects.get(id=request.GET.get('id'))
|
||||
return HttpResponse(preset.body)
|
||||
except PreSetReply.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
|
||||
raise Http404
|
||||
raw_details = login_required(raw_details)
|
||||
|
||||
|
||||
def hold_ticket(request, ticket_id, unhold=False):
|
||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||
|
||||
@ -299,7 +370,7 @@ def hold_ticket(request, ticket_id, unhold=False):
|
||||
else:
|
||||
ticket.on_hold = True
|
||||
title = _('Ticket placed on hold')
|
||||
|
||||
|
||||
f = FollowUp(
|
||||
ticket = ticket,
|
||||
user = request.user,
|
||||
@ -308,28 +379,32 @@ def hold_ticket(request, ticket_id, unhold=False):
|
||||
public = True,
|
||||
)
|
||||
f.save()
|
||||
|
||||
|
||||
ticket.save()
|
||||
|
||||
|
||||
return HttpResponseRedirect(ticket.get_absolute_url())
|
||||
hold_ticket = login_required(hold_ticket)
|
||||
|
||||
|
||||
def unhold_ticket(request, ticket_id):
|
||||
return hold_ticket(request, ticket_id, unhold=True)
|
||||
unhold_ticket = login_required(unhold_ticket)
|
||||
|
||||
|
||||
def rss_list(request):
|
||||
return render_to_response('helpdesk/rss_list.html',
|
||||
return render_to_response('helpdesk/rss_list.html',
|
||||
RequestContext(request, {
|
||||
'queues': Queue.objects.all(),
|
||||
}))
|
||||
rss_list = login_required(rss_list)
|
||||
|
||||
|
||||
def report_index(request):
|
||||
return render_to_response('helpdesk/report_index.html',
|
||||
RequestContext(request, {}))
|
||||
report_index = login_required(report_index)
|
||||
|
||||
|
||||
def run_report(request, report):
|
||||
priority_sql = []
|
||||
priority_columns = []
|
||||
@ -337,14 +412,14 @@ def run_report(request, report):
|
||||
priority_sql.append("COUNT(CASE t.priority WHEN '%s' THEN t.id END) AS \"%s\"" % (p[0], p[1]._proxy____unicode_cast()))
|
||||
priority_columns.append("%s" % p[1]._proxy____unicode_cast())
|
||||
priority_sql = ", ".join(priority_sql)
|
||||
|
||||
|
||||
status_sql = []
|
||||
status_columns = []
|
||||
for s in Ticket.STATUS_CHOICES:
|
||||
status_sql.append("COUNT(CASE t.status WHEN '%s' THEN t.id END) AS \"%s\"" % (s[0], s[1]._proxy____unicode_cast()))
|
||||
status_columns.append("%s" % s[1]._proxy____unicode_cast())
|
||||
status_sql = ", ".join(status_sql)
|
||||
|
||||
|
||||
queue_sql = []
|
||||
queue_columns = []
|
||||
for q in Queue.objects.all():
|
||||
@ -368,11 +443,11 @@ def run_report(request, report):
|
||||
'Dec',
|
||||
)
|
||||
month_columns = []
|
||||
|
||||
|
||||
first_ticket = Ticket.objects.all().order_by('created')[0]
|
||||
first_month = first_ticket.created.month
|
||||
first_year = first_ticket.created.year
|
||||
|
||||
|
||||
last_ticket = Ticket.objects.all().order_by('-created')[0]
|
||||
last_month = last_ticket.created.month
|
||||
last_year = last_ticket.created.year
|
||||
@ -420,33 +495,32 @@ def run_report(request, report):
|
||||
if report == 'userpriority':
|
||||
sql = user_base_sql % priority_sql
|
||||
columns = ['username'] + priority_columns
|
||||
|
||||
|
||||
elif report == 'userqueue':
|
||||
sql = user_base_sql % queue_sql
|
||||
columns = ['username'] + queue_columns
|
||||
|
||||
|
||||
elif report == 'userstatus':
|
||||
sql = user_base_sql % status_sql
|
||||
columns = ['username'] + status_columns
|
||||
|
||||
|
||||
elif report == 'usermonth':
|
||||
sql = user_base_sql % month_sql
|
||||
columns = ['username'] + month_columns
|
||||
|
||||
|
||||
elif report == 'queuepriority':
|
||||
sql = queue_base_sql % priority_sql
|
||||
columns = ['queue'] + priority_columns
|
||||
|
||||
|
||||
elif report == 'queuestatus':
|
||||
sql = queue_base_sql % status_sql
|
||||
columns = ['queue'] + status_columns
|
||||
|
||||
|
||||
elif report == 'queuemonth':
|
||||
sql = queue_base_sql % month_sql
|
||||
columns = ['queue'] + month_columns
|
||||
|
||||
|
||||
from django.db import connection
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(sql)
|
||||
report_output = query_to_dict(cursor.fetchall(), cursor.description)
|
||||
@ -465,7 +539,7 @@ def run_report(request, report):
|
||||
chart_url = bar_chart([columns] + data)
|
||||
else:
|
||||
chart_url = ''
|
||||
|
||||
|
||||
return render_to_response('helpdesk/report_output.html',
|
||||
RequestContext(request, {
|
||||
'headings': columns,
|
||||
@ -474,3 +548,4 @@ def run_report(request, report):
|
||||
'chart': chart_url,
|
||||
}))
|
||||
run_report = login_required(run_report)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user