forked from extern/django-helpdesk
* 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.
|
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||||
|
|
||||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
(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.
|
tickets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from helpdesk.models import Ticket, Queue, FollowUp
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.utils.translation import ugettext as _
|
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):
|
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,
|
title = forms.CharField(
|
||||||
widget=forms.TextInput(),
|
max_length=100,
|
||||||
label=_('Summary of the problem'))
|
required=True,
|
||||||
|
widget=forms.TextInput(),
|
||||||
submitter_email = forms.EmailField(required=False,
|
label=_('Summary of the problem'),
|
||||||
label=_('Submitter E-Mail Address'),
|
)
|
||||||
help_text=_('This e-mail address will receive copies of all public updates to this ticket.'))
|
|
||||||
|
submitter_email = forms.EmailField(
|
||||||
body = forms.CharField(widget=forms.Textarea(),
|
required=False,
|
||||||
label=_('Description of Issue'), required=True)
|
label=_('Submitter E-Mail Address'),
|
||||||
|
help_text=_('This e-mail address will receive copies of all public '
|
||||||
assigned_to = forms.ChoiceField(choices=(), required=False,
|
'updates to this ticket.'),
|
||||||
label=_('Case owner'),
|
)
|
||||||
help_text=_('If you select an owner other than yourself, they\'ll be e-mailed details of this ticket immediately.'))
|
|
||||||
|
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):
|
def save(self, user):
|
||||||
"""
|
"""
|
||||||
Writes and returns a Ticket() object
|
Writes and returns a Ticket() object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
q = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
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'],
|
submitter_email = self.cleaned_data['submitter_email'],
|
||||||
created = datetime.now(),
|
created = datetime.now(),
|
||||||
status = Ticket.OPEN_STATUS,
|
status = Ticket.OPEN_STATUS,
|
||||||
@ -52,7 +75,7 @@ class TicketForm(forms.Form):
|
|||||||
description = self.cleaned_data['body'],
|
description = self.cleaned_data['body'],
|
||||||
priority = self.cleaned_data['priority'],
|
priority = self.cleaned_data['priority'],
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.cleaned_data['assigned_to']:
|
if self.cleaned_data['assigned_to']:
|
||||||
try:
|
try:
|
||||||
u = User.objects.get(id=self.cleaned_data['assigned_to'])
|
u = User.objects.get(id=self.cleaned_data['assigned_to'])
|
||||||
@ -69,91 +92,151 @@ class TicketForm(forms.Form):
|
|||||||
user = user,
|
user = user,
|
||||||
)
|
)
|
||||||
if self.cleaned_data['assigned_to']:
|
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()
|
f.save()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'ticket': t,
|
'ticket': t,
|
||||||
'queue': q,
|
'queue': q,
|
||||||
}
|
}
|
||||||
|
|
||||||
from helpdesk.lib import send_templated_mail
|
|
||||||
|
|
||||||
if t.submitter_email:
|
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:
|
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:
|
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:
|
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
|
return t
|
||||||
|
|
||||||
class PublicTicketForm(forms.Form):
|
|
||||||
queue = forms.ChoiceField(label=_('Queue'), required=True, choices=())
|
|
||||||
|
|
||||||
title = forms.CharField(max_length=100, required=True,
|
class PublicTicketForm(forms.Form):
|
||||||
widget=forms.TextInput(),
|
queue = forms.ChoiceField(
|
||||||
label=_('Summary of your query'))
|
label=_('Queue'),
|
||||||
|
required=True,
|
||||||
submitter_email = forms.EmailField(required=True,
|
choices=()
|
||||||
label=_('Your E-Mail Address'),
|
)
|
||||||
help_text=_('We will e-mail you when your ticket is updated.'))
|
|
||||||
|
title = forms.CharField(
|
||||||
body = forms.CharField(widget=forms.Textarea(),
|
max_length=100,
|
||||||
label=_('Description of your issue'), required=True,
|
required=True,
|
||||||
help_text=_('Please be as descriptive as possible, including any details we may need to address your query.'))
|
widget=forms.TextInput(),
|
||||||
|
label=_('Summary of your query'),
|
||||||
priority = forms.ChoiceField(choices=Ticket.PRIORITY_CHOICES,
|
)
|
||||||
required=True,
|
|
||||||
initial='3',
|
submitter_email = forms.EmailField(
|
||||||
label=_('Urgency'),
|
required=True,
|
||||||
help_text=_('Please select a priority carefully.'))
|
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):
|
def save(self):
|
||||||
"""
|
"""
|
||||||
Writes and returns a Ticket() object
|
Writes and returns a Ticket() object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
q = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
q = Queue.objects.get(id=int(self.cleaned_data['queue']))
|
||||||
|
|
||||||
t = Ticket( title = self.cleaned_data['title'],
|
t = Ticket(
|
||||||
submitter_email = self.cleaned_data['submitter_email'],
|
title = self.cleaned_data['title'],
|
||||||
created = datetime.now(),
|
submitter_email = self.cleaned_data['submitter_email'],
|
||||||
status = Ticket.OPEN_STATUS,
|
created = datetime.now(),
|
||||||
queue = q,
|
status = Ticket.OPEN_STATUS,
|
||||||
description = self.cleaned_data['body'],
|
queue = q,
|
||||||
priority = self.cleaned_data['priority'],
|
description = self.cleaned_data['body'],
|
||||||
)
|
priority = self.cleaned_data['priority'],
|
||||||
|
)
|
||||||
|
|
||||||
t.save()
|
t.save()
|
||||||
|
|
||||||
f = FollowUp( ticket = t,
|
f = FollowUp(
|
||||||
title = _('Ticket Opened Via Web'),
|
ticket = t,
|
||||||
date = datetime.now(),
|
title = _('Ticket Opened Via Web'),
|
||||||
public = True,
|
date = datetime.now(),
|
||||||
comment = self.cleaned_data['body'],
|
public = True,
|
||||||
)
|
comment = self.cleaned_data['body'],
|
||||||
|
)
|
||||||
|
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'ticket': t,
|
'ticket': t,
|
||||||
'queue': q,
|
'queue': q,
|
||||||
}
|
}
|
||||||
|
|
||||||
from helpdesk.lib import send_templated_mail
|
send_templated_mail(
|
||||||
send_templated_mail('newticket_submitter', context, recipients=t.submitter_email, sender=q.from_address, fail_silently=True)
|
'newticket_submitter',
|
||||||
|
context,
|
||||||
|
recipients=t.submitter_email,
|
||||||
|
sender=q.from_address,
|
||||||
|
fail_silently=True,
|
||||||
|
)
|
||||||
|
|
||||||
if q.new_ticket_cc:
|
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:
|
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
|
return t
|
||||||
|
|
||||||
|
98
lib.py
98
lib.py
@ -1,15 +1,46 @@
|
|||||||
""" ..
|
"""
|
||||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||||
|
|
||||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
||||||
|
|
||||||
lib.py - Common functions (eg multipart e-mail)
|
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):
|
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.core.mail import EmailMultiAlternatives
|
||||||
from django.template import loader, Context
|
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)
|
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
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
context = Context(email_context)
|
context = Context(email_context)
|
||||||
|
|
||||||
text_part = loader.get_template_from_string("%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text).render(context)
|
text_part = loader.get_template_from_string(
|
||||||
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)
|
"%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text
|
||||||
subject_part = loader.get_template_from_string("{{ ticket.ticket }} {{ ticket.title }} %s" % t.subject).render(context)
|
).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:
|
if type(recipients) != list:
|
||||||
recipients = [recipients,]
|
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")
|
msg.attach_alternative(html_part, "text/html")
|
||||||
|
|
||||||
if files:
|
if files:
|
||||||
@ -34,18 +77,18 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
|
|||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
msg.attach_file(file)
|
msg.attach_file(file)
|
||||||
|
|
||||||
return msg.send(fail_silently)
|
return msg.send(fail_silently)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def send_multipart_mail(template_name, email_context, subject, recipients, sender=None, bcc=None, fail_silently=False, files=None):
|
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
|
This function will send a multi-part e-mail with both HTML and
|
||||||
Text parts.
|
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
|
template_name must NOT contain an extension. Both HTML (.html) and TEXT
|
||||||
(.txt) versions must exist, eg 'emails/public_submit' will use both
|
(.txt) versions must exist, eg 'emails/public_submit' will use both
|
||||||
public_submit.html and public_submit.txt.
|
public_submit.html and public_submit.txt.
|
||||||
|
|
||||||
email_context should be a plain python dictionary. It is applied against
|
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:
|
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.
|
['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.
|
DEFAULT_FROM_EMAIL will be used.
|
||||||
|
|
||||||
Originally posted on my blog at http://www.rossp.org/
|
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
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
context = Context(email_context)
|
context = Context(email_context)
|
||||||
|
|
||||||
text_part = loader.get_template('%s.txt' % template_name).render(context)
|
text_part = loader.get_template('%s.txt' % template_name).render(context)
|
||||||
html_part = loader.get_template('%s.html' % 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)
|
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:
|
for file in files:
|
||||||
msg.attach_file(file)
|
msg.attach_file(file)
|
||||||
|
|
||||||
return msg.send(fail_silently)
|
return msg.send(fail_silently)
|
||||||
|
|
||||||
|
|
||||||
def normalise_data(data, to=100):
|
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)
|
max_value = max(data)
|
||||||
if max_value > to:
|
if max_value > to:
|
||||||
@ -103,7 +149,6 @@ def normalise_data(data, to=100):
|
|||||||
data = new_data
|
data = new_data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
chart_colours = ('80C65A', '990066', 'FF9900', '3399CC', 'BBCCED', '3399CC', 'FFCC33')
|
|
||||||
|
|
||||||
def line_chart(data):
|
def line_chart(data):
|
||||||
"""
|
"""
|
||||||
@ -138,7 +183,7 @@ def line_chart(data):
|
|||||||
first = True
|
first = True
|
||||||
for row in range(rows):
|
for row in range(rows):
|
||||||
# Set maximum data ranges to '0:x' where 'x' is the maximum number in use.
|
# 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 += ','
|
||||||
chart_url += '0,%s' % max
|
chart_url += '0,%s' % max
|
||||||
first = False
|
first = False
|
||||||
@ -149,6 +194,7 @@ def line_chart(data):
|
|||||||
|
|
||||||
return chart_url
|
return chart_url
|
||||||
|
|
||||||
|
|
||||||
def bar_chart(data):
|
def bar_chart(data):
|
||||||
"""
|
"""
|
||||||
'data' is a list of lists making a table.
|
'data' is a list of lists making a table.
|
||||||
@ -185,12 +231,16 @@ def bar_chart(data):
|
|||||||
|
|
||||||
return chart_url
|
return chart_url
|
||||||
|
|
||||||
|
|
||||||
def query_to_dict(results, descriptions):
|
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.
|
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
|
Converts the results of a raw SQL query into a list of dictionaries, suitable
|
||||||
for use in templates etc. """
|
for use in templates etc.
|
||||||
|
"""
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
for data in results:
|
for data in results:
|
||||||
row = {}
|
row = {}
|
||||||
|
Binary file not shown.
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -16,7 +16,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\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/dashboard.html:26 templates/helpdesk/dashboard.html:41
|
||||||
#: templates/helpdesk/rss_list.html:23 templates/helpdesk/ticket_list.html:14
|
#: templates/helpdesk/rss_list.html:23 templates/helpdesk/ticket_list.html:14
|
||||||
#: templates/helpdesk/ticket_list.html:25
|
#: templates/helpdesk/ticket_list.html:25
|
||||||
@ -24,520 +24,564 @@ msgstr ""
|
|||||||
msgid "Queue"
|
msgid "Queue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:21
|
#: forms.py:30
|
||||||
msgid "Summary of the problem"
|
msgid "Summary of the problem"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:24
|
#: forms.py:35
|
||||||
msgid "Submitter E-Mail Address"
|
msgid "Submitter E-Mail Address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:25
|
#: forms.py:36
|
||||||
msgid ""
|
msgid ""
|
||||||
"This e-mail address will receive copies of all public updates to this ticket."
|
"This e-mail address will receive copies of all public updates to this ticket."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:28
|
#: forms.py:42
|
||||||
msgid "Description of Issue"
|
msgid "Description of Issue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:31
|
#: forms.py:49
|
||||||
msgid "Case owner"
|
msgid "Case owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:32
|
#: forms.py:50
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you select an owner other than yourself, they'll be e-mailed details of "
|
"If you select an owner other than yourself, they'll be e-mailed details of "
|
||||||
"this ticket immediately."
|
"this ticket immediately."
|
||||||
msgstr ""
|
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/public_view_ticket.html:29
|
||||||
#: templates/helpdesk/ticket.html:67 templates/helpdesk/ticket.html.py:159
|
#: 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"
|
msgid "Priority"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:38
|
#: forms.py:59
|
||||||
msgid "Please select a priority carefully. If unsure, leave it as '3'."
|
msgid "Please select a priority carefully. If unsure, leave it as '3'."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:65
|
#: forms.py:88
|
||||||
msgid "Ticket Opened"
|
msgid "Ticket Opened"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:72
|
#: forms.py:95
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ticket Opened & Assigned to %(name)s"
|
msgid "Ticket Opened & Assigned to %(name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:102
|
#: forms.py:156
|
||||||
msgid "Summary of your query"
|
msgid "Summary of your query"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:105
|
#: forms.py:161
|
||||||
msgid "Your E-Mail Address"
|
msgid "Your E-Mail Address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:106
|
#: forms.py:162
|
||||||
msgid "We will e-mail you when your ticket is updated."
|
msgid "We will e-mail you when your ticket is updated."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:109
|
#: forms.py:167
|
||||||
msgid "Description of your issue"
|
msgid "Description of your issue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:110
|
#: forms.py:169
|
||||||
msgid ""
|
msgid ""
|
||||||
"Please be as descriptive as possible, including any details we may need to "
|
"Please be as descriptive as possible, including any details we may need to "
|
||||||
"address your query."
|
"address your query."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:115
|
#: forms.py:177
|
||||||
msgid "Urgency"
|
msgid "Urgency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:116
|
#: forms.py:178
|
||||||
msgid "Please select a priority carefully."
|
msgid "Please select a priority carefully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms.py:137
|
#: forms.py:202
|
||||||
msgid "Ticket Opened Via Web"
|
msgid "Ticket Opened Via Web"
|
||||||
msgstr ""
|
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/dashboard.html:26 templates/helpdesk/dashboard.html:41
|
||||||
#: templates/helpdesk/ticket.html:153 templates/helpdesk/ticket_list.html:24
|
#: 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"
|
msgid "Title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:29 models.py:361
|
#: models.py:34 models.py:733
|
||||||
msgid "Slug"
|
msgid "Slug"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:29
|
#: models.py:35
|
||||||
msgid ""
|
msgid ""
|
||||||
"This slug is used when building ticket ID's. Once set, try not to change it "
|
"This slug is used when building ticket ID's. Once set, try not to change it "
|
||||||
"or e-mailing may get messy."
|
"or e-mailing may get messy."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:30
|
#: models.py:40
|
||||||
msgid "E-Mail Address"
|
msgid "E-Mail Address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:30
|
#: models.py:43
|
||||||
msgid ""
|
msgid ""
|
||||||
"All outgoing e-mails for this queue will use this e-mail address. If you use "
|
"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."
|
"IMAP or POP3, this should be the e-mail address for that mailbox."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:31
|
#: models.py:49
|
||||||
msgid "Allow Public Submission?"
|
msgid "Allow Public Submission?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:31
|
#: models.py:52
|
||||||
msgid "Should this queue be listed on the public submission form?"
|
msgid "Should this queue be listed on the public submission form?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:32
|
#: models.py:57
|
||||||
msgid "Allow E-Mail Submission?"
|
msgid "Allow E-Mail Submission?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:32
|
#: models.py:60
|
||||||
msgid "Do you want to poll the e-mail box below for new tickets?"
|
msgid "Do you want to poll the e-mail box below for new tickets?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:33
|
#: models.py:65
|
||||||
msgid "Escalation Days"
|
msgid "Escalation Days"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:33
|
#: models.py:68
|
||||||
msgid ""
|
msgid ""
|
||||||
"For tickets which are not held, how often do you wish to increase their "
|
"For tickets which are not held, how often do you wish to increase their "
|
||||||
"priority? Set to 0 for no escalation."
|
"priority? Set to 0 for no escalation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:42
|
#: models.py:73
|
||||||
msgid "New Ticket CC Address"
|
msgid "New Ticket CC Address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:42
|
#: models.py:76
|
||||||
msgid ""
|
msgid ""
|
||||||
"If an e-mail address is entered here, then it will receive notification of "
|
"If an e-mail address is entered here, then it will receive notification of "
|
||||||
"all new tickets created for this queue"
|
"all new tickets created for this queue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:43
|
#: models.py:81
|
||||||
msgid "Updated Ticket CC Address"
|
msgid "Updated Ticket CC Address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:43
|
#: models.py:84
|
||||||
msgid ""
|
msgid ""
|
||||||
"If an e-mail address is entered here, then it will receive notification of "
|
"If an e-mail address is entered here, then it will receive notification of "
|
||||||
"all activity (new tickets, closed tickets, updates, reassignments, etc) for "
|
"all activity (new tickets, closed tickets, updates, reassignments, etc) for "
|
||||||
"this queue"
|
"this queue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:45
|
#: models.py:90
|
||||||
msgid "E-Mail Box Type"
|
msgid "E-Mail Box Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:45
|
#: models.py:92
|
||||||
msgid "POP 3"
|
msgid "POP 3"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:45
|
#: models.py:92
|
||||||
msgid "IMAP"
|
msgid "IMAP"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:45
|
#: models.py:95
|
||||||
msgid ""
|
msgid ""
|
||||||
"E-Mail server type for creating tickets automatically from a mailbox - both "
|
"E-Mail server type for creating tickets automatically from a mailbox - both "
|
||||||
"POP3 and IMAP are supported."
|
"POP3 and IMAP are supported."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:46
|
#: models.py:100
|
||||||
msgid "E-Mail Hostname"
|
msgid "E-Mail Hostname"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:46
|
#: models.py:104
|
||||||
msgid ""
|
msgid ""
|
||||||
"Your e-mail server address - either the domain name or IP address. May be "
|
"Your e-mail server address - either the domain name or IP address. May be "
|
||||||
"\"localhost\"."
|
"\"localhost\"."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:47
|
#: models.py:109
|
||||||
msgid "E-Mail Port"
|
msgid "E-Mail Port"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:47
|
#: models.py:112
|
||||||
msgid ""
|
msgid ""
|
||||||
"Port number to use for accessing e-mail. Default for POP3 is \"110\", and "
|
"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 "
|
"for IMAP is \"143\". This may differ on some servers. Leave it blank to use "
|
||||||
"the defaults."
|
"the defaults."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:48
|
#: models.py:118
|
||||||
msgid "E-Mail Username"
|
msgid "E-Mail Username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:48
|
#: models.py:122
|
||||||
msgid "Username for accessing this mailbox."
|
msgid "Username for accessing this mailbox."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:49
|
#: models.py:126
|
||||||
msgid "E-Mail Password"
|
msgid "E-Mail Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:49
|
#: models.py:130
|
||||||
msgid "Password for the above username"
|
msgid "Password for the above username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:50
|
#: models.py:134
|
||||||
msgid "IMAP Folder"
|
msgid "IMAP Folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:50
|
#: models.py:138
|
||||||
msgid ""
|
msgid ""
|
||||||
"If using IMAP, what folder do you wish to fetch messages from? This allows "
|
"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 "
|
"you to use one IMAP account for multiple queues, by filtering messages on "
|
||||||
"your IMAP server into separate folders. Default: INBOX."
|
"your IMAP server into separate folders. Default: INBOX."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:51
|
#: models.py:145
|
||||||
msgid "E-Mail Check Interval"
|
msgid "E-Mail Check Interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:51
|
#: models.py:146
|
||||||
msgid "How often do you wish to check this mailbox? (in Minutes)"
|
msgid "How often do you wish to check this mailbox? (in Minutes)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:89 templates/helpdesk/dashboard.html:10
|
#: models.py:212 templates/helpdesk/dashboard.html:10
|
||||||
#: templates/helpdesk/ticket.html:123
|
#: templates/helpdesk/ticket.html:123
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr ""
|
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
|
#: templates/helpdesk/ticket.html.py:133 templates/helpdesk/ticket.html:138
|
||||||
msgid "Reopened"
|
msgid "Reopened"
|
||||||
msgstr ""
|
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:124 templates/helpdesk/ticket.html.py:129
|
||||||
#: templates/helpdesk/ticket.html:134
|
#: templates/helpdesk/ticket.html:134
|
||||||
msgid "Resolved"
|
msgid "Resolved"
|
||||||
msgstr ""
|
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:130 templates/helpdesk/ticket.html:135
|
||||||
#: templates/helpdesk/ticket.html.py:139
|
#: templates/helpdesk/ticket.html.py:139
|
||||||
msgid "Closed"
|
msgid "Closed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:96
|
#: models.py:219
|
||||||
msgid "1. Critical"
|
msgid "1. Critical"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:97
|
#: models.py:220
|
||||||
msgid "2. High"
|
msgid "2. High"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:98
|
#: models.py:221
|
||||||
msgid "3. Normal"
|
msgid "3. Normal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:99
|
#: models.py:222
|
||||||
msgid "4. Low"
|
msgid "4. Low"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:100
|
#: models.py:223
|
||||||
msgid "5. Very Low"
|
msgid "5. Very Low"
|
||||||
msgstr ""
|
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:23
|
||||||
#: templates/helpdesk/ticket_list.html:59
|
#: templates/helpdesk/ticket_list.html:59
|
||||||
msgid "Created"
|
msgid "Created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:106
|
#: models.py:236
|
||||||
|
msgid "Date this ticket was first created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:240
|
||||||
msgid "Modified"
|
msgid "Modified"
|
||||||
msgstr ""
|
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
|
#: templates/helpdesk/ticket.html:62
|
||||||
msgid "Submitter E-Mail"
|
msgid "Submitter E-Mail"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:107
|
#: models.py:249
|
||||||
msgid ""
|
msgid ""
|
||||||
"The submitter will receive an email for all public follow-ups left for this "
|
"The submitter will receive an email for all public follow-ups left for this "
|
||||||
"task."
|
"task."
|
||||||
msgstr ""
|
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:15
|
||||||
#: templates/helpdesk/ticket_list.html:26
|
#: templates/helpdesk/ticket_list.html:26
|
||||||
#: templates/helpdesk/ticket_list.html:59
|
#: templates/helpdesk/ticket_list.html:59
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:111
|
#: models.py:267
|
||||||
msgid "On Hold"
|
msgid "On Hold"
|
||||||
msgstr ""
|
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
|
#: templates/helpdesk/ticket.html:72
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
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
|
#: templates/helpdesk/ticket.html:79
|
||||||
msgid "Resolution"
|
msgid "Resolution"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:125 templates/helpdesk/ticket.html:58 views/feeds.py:55
|
#: models.py:285
|
||||||
#: views/feeds.py:77 views/feeds.py:120 views/staff.py:120
|
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"
|
msgid "Unassigned"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:156
|
#: models.py:348
|
||||||
msgid " - On Hold"
|
msgid " - On Hold"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:215 models.py:330
|
#: models.py:430 models.py:662
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:217 views/staff.py:134
|
#: models.py:441 views/staff.py:163
|
||||||
msgid "Comment"
|
msgid "Comment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:218
|
#: models.py:447
|
||||||
msgid "Public"
|
msgid "Public"
|
||||||
msgstr ""
|
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"
|
msgid "New Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:246
|
#: models.py:465
|
||||||
|
msgid "If the status was changed, what was it changed to?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:496
|
||||||
msgid "Field"
|
msgid "Field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:247
|
#: models.py:501
|
||||||
msgid "Old Value"
|
msgid "Old Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:248
|
#: models.py:507
|
||||||
msgid "New Value"
|
msgid "New Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:253
|
#: models.py:515
|
||||||
msgid "removed"
|
msgid "removed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:255
|
#: models.py:517
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "set to %s"
|
msgid "set to %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:257
|
#: models.py:519
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "changed from \"%(old_value)s\" to \"%(new_value)s\""
|
msgid "changed from \"%(old_value)s\" to \"%(new_value)s\""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:289
|
#: models.py:562
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:290
|
#: models.py:567
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:291
|
#: models.py:572
|
||||||
msgid "MIME Type"
|
msgid "MIME Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:292
|
#: models.py:577
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:292
|
#: models.py:578
|
||||||
msgid "Size of this file in bytes"
|
msgid "Size of this file in bytes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:315
|
#: models.py:611
|
||||||
msgid ""
|
msgid ""
|
||||||
"Leave blank to allow this reply to be used for all queues, or select those "
|
"Leave blank to allow this reply to be used for all queues, or select those "
|
||||||
"queues you wish to limit this reply to."
|
"queues you wish to limit this reply to."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:316 models.py:328
|
#: models.py:616 models.py:657
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:316
|
#: models.py:618
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only used to assist users with selecting a reply - not shown to the user."
|
"Only used to assist users with selecting a reply - not shown to the user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:317
|
#: models.py:623
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:317
|
#: models.py:624
|
||||||
msgid ""
|
msgid ""
|
||||||
"Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); "
|
"Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); "
|
||||||
"{{ queue }} - The queue; and {{ user }} - the current user."
|
"{{ queue }} - The queue; and {{ user }} - the current user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:326
|
#: models.py:651
|
||||||
msgid ""
|
msgid ""
|
||||||
"Leave blank for this exclusion to be applied to all queues, or select those "
|
"Leave blank for this exclusion to be applied to all queues, or select those "
|
||||||
"queues you wish to exclude with this entry."
|
"queues you wish to exclude with this entry."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:330
|
#: models.py:663
|
||||||
msgid "Date on which escalation should not happen"
|
msgid "Date on which escalation should not happen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:341
|
#: models.py:680
|
||||||
msgid "Template Name"
|
msgid "Template Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:343
|
#: models.py:686
|
||||||
msgid "Subject"
|
msgid "Subject"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:343
|
#: models.py:688
|
||||||
msgid ""
|
msgid ""
|
||||||
"This will be prefixed with \"[ticket.ticket] ticket.title\". We recommend "
|
"This will be prefixed with \"[ticket.ticket] ticket.title\". We recommend "
|
||||||
"something simple such as \"(Updated\") or \"(Closed)\" - the same context is "
|
"something simple such as \"(Updated\") or \"(Closed)\" - the same context is "
|
||||||
"available as in plain_text, below."
|
"available as in plain_text, below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:344
|
#: models.py:694
|
||||||
msgid "Heading"
|
msgid "Heading"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:344
|
#: models.py:696
|
||||||
msgid ""
|
msgid ""
|
||||||
"In HTML e-mails, this will be the heading at the top of the email - the same "
|
"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."
|
"context is available as in plain_text, below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:345
|
#: models.py:702
|
||||||
msgid "Plain Text"
|
msgid "Plain Text"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:345
|
#: models.py:703
|
||||||
msgid ""
|
msgid ""
|
||||||
"The context available to you includes {{ ticket }}, {{ queue }}, and "
|
"The context available to you includes {{ ticket }}, {{ queue }}, and "
|
||||||
"depending on the time of the call: {{ resolution }} or {{ comment }}."
|
"depending on the time of the call: {{ resolution }} or {{ comment }}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:346
|
#: models.py:709
|
||||||
msgid "HTML"
|
msgid "HTML"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:346
|
#: models.py:710
|
||||||
msgid "The same context is available here as in plain_text, above."
|
msgid "The same context is available here as in plain_text, above."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:381
|
#: models.py:764
|
||||||
msgid "Question"
|
msgid "Question"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:382
|
#: models.py:768
|
||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:384
|
#: models.py:772
|
||||||
msgid "Votes"
|
msgid "Votes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:384
|
#: models.py:773
|
||||||
msgid "Total number of votes cast for this item"
|
msgid "Total number of votes cast for this item"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:385
|
#: models.py:777
|
||||||
msgid "Positive Votes"
|
msgid "Positive Votes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:385
|
#: models.py:778
|
||||||
msgid "Number of votes for this item which were POSITIVE."
|
msgid "Number of votes for this item which were POSITIVE."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:387
|
#: models.py:782
|
||||||
msgid "Last Updated"
|
msgid "Last Updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:397
|
#: models.py:783
|
||||||
|
msgid "The date on which this question was most recently changed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:795
|
||||||
msgid "Unrated"
|
msgid "Unrated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: scripts/escalate_tickets.py:70
|
#: management/commands/escalate_tickets.py:146
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ticket escalated after %s days"
|
msgid "Ticket escalated after %s days"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: scripts/get_email.py:74
|
#: management/commands/get_email.py:97
|
||||||
msgid "Created from e-mail"
|
msgid "Created from e-mail"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: scripts/get_email.py:77
|
#: management/commands/get_email.py:100
|
||||||
msgid "Unknown Sender"
|
msgid "Unknown Sender"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: scripts/get_email.py:156
|
#: management/commands/get_email.py:206
|
||||||
msgid " (Updated)"
|
msgid " (Updated)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: scripts/get_email.py:166
|
#: management/commands/get_email.py:228
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "E-Mail Received from %s"
|
msgid "E-Mail Received from %(sender_email)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/helpdesk/base.html:4
|
#: templates/helpdesk/base.html:4
|
||||||
@ -806,7 +850,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: templates/helpdesk/public_view_ticket.html:16
|
#: templates/helpdesk/public_view_ticket.html:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Queue: %(ticket.queue)s"
|
msgid "Queue: %(queue_name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/helpdesk/public_view_ticket.html:19
|
#: templates/helpdesk/public_view_ticket.html:19
|
||||||
@ -1087,75 +1131,75 @@ msgstr ""
|
|||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:26
|
#: views/feeds.py:35
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Helpdesk: Open Tickets in queue %(queue)s for %(username)s"
|
msgid "Helpdesk: Open Tickets in queue %(queue)s for %(username)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:28
|
#: views/feeds.py:40
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Helpdesk: Open Tickets for %(username)s"
|
msgid "Helpdesk: Open Tickets for %(username)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:32
|
#: views/feeds.py:46
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Open and Reopened Tickets in queue %(queue)s for %(username)s"
|
msgid "Open and Reopened Tickets in queue %(queue)s for %(username)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:34
|
#: views/feeds.py:51
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Open and Reopened Tickets for %(username)s"
|
msgid "Open and Reopened Tickets for %(username)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:62
|
#: views/feeds.py:98
|
||||||
msgid "Helpdesk: Unassigned Tickets"
|
msgid "Helpdesk: Unassigned Tickets"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:63
|
#: views/feeds.py:99
|
||||||
msgid "Unassigned Open and Reopened tickets"
|
msgid "Unassigned Open and Reopened tickets"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:84
|
#: views/feeds.py:124
|
||||||
msgid "Helpdesk: Recent Followups"
|
msgid "Helpdesk: Recent Followups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:85
|
#: views/feeds.py:125
|
||||||
msgid ""
|
msgid ""
|
||||||
"Recent FollowUps, such as e-mail replies, comments, attachments and "
|
"Recent FollowUps, such as e-mail replies, comments, attachments and "
|
||||||
"resolutions"
|
"resolutions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:102
|
#: views/feeds.py:142
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Helpdesk: Open Tickets in queue %(queue)s"
|
msgid "Helpdesk: Open Tickets in queue %(queue)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/feeds.py:105
|
#: views/feeds.py:147
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Open and Reopened Tickets in queue %(queue)s"
|
msgid "Open and Reopened Tickets in queue %(queue)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/public.py:53
|
#: views/public.py:58
|
||||||
msgid "Invalid ticket ID or e-mail address. Please try again."
|
msgid "Invalid ticket ID or e-mail address. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/staff.py:82
|
#: views/staff.py:107
|
||||||
msgid "Accepted resolution and closed ticket"
|
msgid "Accepted resolution and closed ticket"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/staff.py:116
|
#: views/staff.py:143
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Assigned to %(username)s"
|
msgid "Assigned to %(username)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/staff.py:136
|
#: views/staff.py:165
|
||||||
msgid "Updated"
|
msgid "Updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/staff.py:298
|
#: views/staff.py:369
|
||||||
msgid "Ticket taken off hold"
|
msgid "Ticket taken off hold"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/staff.py:301
|
#: views/staff.py:372
|
||||||
msgid "Ticket placed on hold"
|
msgid "Ticket placed on hold"
|
||||||
msgstr ""
|
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
|
days to the list of days on which no
|
||||||
escalation should take place.
|
escalation should take place.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import datetime, timedelta, date
|
||||||
from django.db.models import Q
|
import getopt
|
||||||
from helpdesk.models import EscalationExclusion, Queue
|
from optparse import make_option
|
||||||
import sys, getopt
|
import sys
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
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):
|
class Command(BaseCommand):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -22,15 +26,15 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
self.option_list += (
|
self.option_list += (
|
||||||
make_option(
|
make_option(
|
||||||
'--days', '-d',
|
'--days', '-d',
|
||||||
help='Days of week (monday, tuesday, etc)'),
|
help='Days of week (monday, tuesday, etc)'),
|
||||||
make_option(
|
make_option(
|
||||||
'--occurrences', '-o',
|
'--occurrences', '-o',
|
||||||
type='int',
|
type='int',
|
||||||
default=1,
|
default=1,
|
||||||
help='Occurrences: How many weeks ahead to exclude this day'),
|
help='Occurrences: How many weeks ahead to exclude this day'),
|
||||||
make_option(
|
make_option(
|
||||||
'--queues', '-q',
|
'--queues', '-q',
|
||||||
help='Queues to include (default: all). Use queue slugs'),
|
help='Queues to include (default: all). Use queue slugs'),
|
||||||
make_option(
|
make_option(
|
||||||
'--verbose', '-v',
|
'--verbose', '-v',
|
||||||
@ -53,7 +57,7 @@ class Command(BaseCommand):
|
|||||||
if not occurrences: occurrences = 1
|
if not occurrences: occurrences = 1
|
||||||
if not (days and occurrences):
|
if not (days and occurrences):
|
||||||
raise CommandError('One or more occurrences must be specified.')
|
raise CommandError('One or more occurrences must be specified.')
|
||||||
|
|
||||||
if queue_slugs is not None:
|
if queue_slugs is not None:
|
||||||
queue_set = queue_slugs.split(',')
|
queue_set = queue_slugs.split(',')
|
||||||
for queue in queue_set:
|
for queue in queue_set:
|
||||||
@ -65,6 +69,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
|
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
|
||||||
|
|
||||||
|
|
||||||
day_names = {
|
day_names = {
|
||||||
'monday': 0,
|
'monday': 0,
|
||||||
'tuesday': 1,
|
'tuesday': 1,
|
||||||
@ -75,6 +80,7 @@ day_names = {
|
|||||||
'sunday': 6,
|
'sunday': 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def create_exclusions(days, occurrences, verbose, queues):
|
def create_exclusions(days, occurrences, verbose, queues):
|
||||||
days = days.split(',')
|
days = days.split(',')
|
||||||
for day in days:
|
for day in days:
|
||||||
@ -87,10 +93,10 @@ def create_exclusions(days, occurrences, verbose, queues):
|
|||||||
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
|
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
|
||||||
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate)
|
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate)
|
||||||
esc.save()
|
esc.save()
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print "Created exclusion for %s %s" % (day_name, workdate)
|
print "Created exclusion for %s %s" % (day_name, workdate)
|
||||||
|
|
||||||
for q in queues:
|
for q in queues:
|
||||||
esc.queues.add(q)
|
esc.queues.add(q)
|
||||||
if verbose:
|
if verbose:
|
||||||
@ -107,13 +113,15 @@ def usage():
|
|||||||
print " --queues, -q: Queues to include (default: all). Use queue slugs"
|
print " --queues, -q: Queues to include (default: all). Use queue slugs"
|
||||||
print " --verbose, -v: Display a list of dates excluded"
|
print " --verbose, -v: Display a list of dates excluded"
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# This script can be run from the command-line or via Django's manage.py.
|
||||||
try:
|
try:
|
||||||
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
|
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
|
||||||
except getopt.GetoptError:
|
except getopt.GetoptError:
|
||||||
usage()
|
usage()
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
days = None
|
days = None
|
||||||
occurrences = None
|
occurrences = None
|
||||||
verbose = False
|
verbose = False
|
||||||
@ -134,7 +142,7 @@ if __name__ == '__main__':
|
|||||||
if not (days and occurrences):
|
if not (days and occurrences):
|
||||||
usage()
|
usage()
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
if queue_slugs is not None:
|
if queue_slugs is not None:
|
||||||
queue_set = queue_slugs.split(',')
|
queue_set = queue_slugs.split(',')
|
||||||
for queue in queue_set:
|
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.
|
(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.
|
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.db.models import Q
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
|
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
|
||||||
from helpdesk.lib import send_templated_mail
|
from helpdesk.lib import send_templated_mail
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from optparse import make_option
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -25,7 +27,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
self.option_list += (
|
self.option_list += (
|
||||||
make_option(
|
make_option(
|
||||||
'--queues', '-q',
|
'--queues', '-q',
|
||||||
help='Queues to include (default: all). Use queue slugs'),
|
help='Queues to include (default: all). Use queue slugs'),
|
||||||
make_option(
|
make_option(
|
||||||
'--verbose', '-v',
|
'--verbose', '-v',
|
||||||
@ -33,17 +35,17 @@ class Command(BaseCommand):
|
|||||||
default=False,
|
default=False,
|
||||||
help='Display a list of dates excluded'),
|
help='Display a list of dates excluded'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
verbose = False
|
verbose = False
|
||||||
queue_slugs = None
|
queue_slugs = None
|
||||||
queues = []
|
queues = []
|
||||||
|
|
||||||
if options['verbose']:
|
if options['verbose']:
|
||||||
verbose = True
|
verbose = True
|
||||||
if options['queues']:
|
if options['queues']:
|
||||||
queue_slugs = options['queues']
|
queue_slugs = options['queues']
|
||||||
|
|
||||||
if queue_slugs is not None:
|
if queue_slugs is not None:
|
||||||
queue_set = queue_slugs.split(',')
|
queue_set = queue_slugs.split(',')
|
||||||
for queue in queue_set:
|
for queue in queue_set:
|
||||||
@ -55,12 +57,13 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
escalate_tickets(queues=queues, verbose=verbose)
|
escalate_tickets(queues=queues, verbose=verbose)
|
||||||
|
|
||||||
|
|
||||||
def escalate_tickets(queues, verbose):
|
def escalate_tickets(queues, verbose):
|
||||||
""" Only include queues with escalation configured """
|
""" Only include queues with escalation configured """
|
||||||
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
|
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
|
||||||
if queues:
|
if queues:
|
||||||
queryset = queryset.filter(slug__in=queues)
|
queryset = queryset.filter(slug__in=queues)
|
||||||
|
|
||||||
for q in queryset:
|
for q in queryset:
|
||||||
last = date.today() - timedelta(days=q.escalate_days)
|
last = date.today() - timedelta(days=q.escalate_days)
|
||||||
today = date.today()
|
today = date.today()
|
||||||
@ -78,28 +81,62 @@ def escalate_tickets(queues, verbose):
|
|||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print "Processing: %s" % q
|
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.last_escalation = datetime.now()
|
||||||
t.priority -= 1
|
t.priority -= 1
|
||||||
t.save()
|
t.save()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'ticket': t,
|
'ticket': t,
|
||||||
'queue': q,
|
'queue': q,
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.submitter_email:
|
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:
|
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:
|
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:
|
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(
|
f = FollowUp(
|
||||||
ticket = t,
|
ticket = t,
|
||||||
@ -118,11 +155,13 @@ def escalate_tickets(queues, verbose):
|
|||||||
)
|
)
|
||||||
tc.save()
|
tc.save()
|
||||||
|
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
print "Options:"
|
print "Options:"
|
||||||
print " --queues, -q: Queues to include (default: all). Use queue slugs"
|
print " --queues, -q: Queues to include (default: all). Use queue slugs"
|
||||||
print " --verbose, -v: Display a list of dates excluded"
|
print " --verbose, -v: Display a list of dates excluded"
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
try:
|
try:
|
||||||
opts, args = getopt.getopt(sys.argv[1:], 'q:v', ['queues=', 'verbose'])
|
opts, args = getopt.getopt(sys.argv[1:], 'q:v', ['queues=', 'verbose'])
|
||||||
@ -133,13 +172,13 @@ if __name__ == '__main__':
|
|||||||
verbose = False
|
verbose = False
|
||||||
queue_slugs = None
|
queue_slugs = None
|
||||||
queues = []
|
queues = []
|
||||||
|
|
||||||
for o, a in opts:
|
for o, a in opts:
|
||||||
if o in ('-v', '--verbose'):
|
if o in ('-v', '--verbose'):
|
||||||
verbose = True
|
verbose = True
|
||||||
if o in ('-q', '--queues'):
|
if o in ('-q', '--queues'):
|
||||||
queue_slugs = a
|
queue_slugs = a
|
||||||
|
|
||||||
if queue_slugs is not None:
|
if queue_slugs is not None:
|
||||||
queue_set = queue_slugs.split(',')
|
queue_set = queue_slugs.split(',')
|
||||||
for queue in queue_set:
|
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.
|
(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
|
scripts/get_email.py - Designed to be run from cron, this script checks the
|
||||||
POP and IMAP boxes defined for the queues within a
|
POP and IMAP boxes defined for the queues within a
|
||||||
helpdesk, creating tickets from the new messages (or
|
helpdesk, creating tickets from the new messages (or
|
||||||
adding to existing tickets if needed)
|
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.lib import send_templated_mail
|
||||||
from helpdesk.models import Queue, Ticket, FollowUp, Attachment
|
from helpdesk.models import Queue, Ticket, FollowUp, Attachment
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
process_email()
|
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
|
continue
|
||||||
|
|
||||||
process_queue(q)
|
process_queue(q)
|
||||||
@ -42,6 +53,7 @@ def process_email():
|
|||||||
q.email_box_last_check = datetime.now()
|
q.email_box_last_check = datetime.now()
|
||||||
q.save()
|
q.save()
|
||||||
|
|
||||||
|
|
||||||
def process_queue(q):
|
def process_queue(q):
|
||||||
print "Processing: %s" % q
|
print "Processing: %s" % q
|
||||||
if q.email_box_type == 'pop3':
|
if q.email_box_type == 'pop3':
|
||||||
@ -55,16 +67,16 @@ def process_queue(q):
|
|||||||
for msg in messagesInfo:
|
for msg in messagesInfo:
|
||||||
msgNum = msg.split(" ")[0]
|
msgNum = msg.split(" ")[0]
|
||||||
msgSize = msg.split(" ")[1]
|
msgSize = msg.split(" ")[1]
|
||||||
|
|
||||||
full_message = "\n".join(server.retr(msgNum)[1])
|
full_message = "\n".join(server.retr(msgNum)[1])
|
||||||
ticket_from_message(message=full_message, queue=q)
|
ticket_from_message(message=full_message, queue=q)
|
||||||
|
|
||||||
server.dele(msgNum)
|
server.dele(msgNum)
|
||||||
server.quit()
|
server.quit()
|
||||||
|
|
||||||
elif q.email_box_type == 'imap':
|
elif q.email_box_type == 'imap':
|
||||||
if not q.email_box_port: q.email_box_port = 143
|
if not q.email_box_port: q.email_box_port = 143
|
||||||
|
|
||||||
server = imaplib.IMAP4(q.email_box_host, q.email_box_port)
|
server = imaplib.IMAP4(q.email_box_host, q.email_box_port)
|
||||||
server.login(q.email_box_user, q.email_box_pass)
|
server.login(q.email_box_user, q.email_box_pass)
|
||||||
server.select(q.email_box_imap_folder)
|
server.select(q.email_box_imap_folder)
|
||||||
@ -77,15 +89,16 @@ def process_queue(q):
|
|||||||
server.close()
|
server.close()
|
||||||
server.logout()
|
server.logout()
|
||||||
|
|
||||||
|
|
||||||
def ticket_from_message(message, queue):
|
def ticket_from_message(message, queue):
|
||||||
# 'message' must be an RFC822 formatted message.
|
# 'message' must be an RFC822 formatted message.
|
||||||
msg = message
|
msg = message
|
||||||
message = email.message_from_string(msg)
|
message = email.message_from_string(msg)
|
||||||
subject = message.get('subject', _('Created from e-mail'))
|
subject = message.get('subject', _('Created from e-mail'))
|
||||||
subject = subject.replace("Re: ", "").replace("Fw: ", "").strip()
|
subject = subject.replace("Re: ", "").replace("Fw: ", "").strip()
|
||||||
|
|
||||||
sender = message.get('from', _('Unknown Sender'))
|
sender = message.get('from', _('Unknown Sender'))
|
||||||
|
|
||||||
sender_email = parseaddr(sender)[1]
|
sender_email = parseaddr(sender)[1]
|
||||||
if sender_email.startswith('postmaster'):
|
if sender_email.startswith('postmaster'):
|
||||||
sender_email = ''
|
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')
|
ticket = re.match(r"^\[(?P<queue>[A-Za-z0-9]+)-(?P<id>\d+)\]", subject).group('id')
|
||||||
else:
|
else:
|
||||||
ticket = None
|
ticket = None
|
||||||
|
|
||||||
counter = 0
|
counter = 0
|
||||||
files = []
|
files = []
|
||||||
|
|
||||||
for part in message.walk():
|
for part in message.walk():
|
||||||
if part.get_content_maintype() == 'multipart':
|
if part.get_content_maintype() == 'multipart':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = part.get_param("name")
|
name = part.get_param("name")
|
||||||
|
|
||||||
if part.get_content_maintype() == 'text' and name == None:
|
if part.get_content_maintype() == 'text' and name == None:
|
||||||
body = part.get_payload()
|
body = part.get_payload()
|
||||||
else:
|
else:
|
||||||
if not name:
|
if not name:
|
||||||
ext = mimetypes.guess_extension(part.get_content_type())
|
ext = mimetypes.guess_extension(part.get_content_type())
|
||||||
name = "part-%i%s" % (counter, ext)
|
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
|
counter += 1
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
if ticket:
|
if ticket:
|
||||||
@ -130,7 +150,8 @@ def ticket_from_message(message, queue):
|
|||||||
|
|
||||||
high_priority_types = ('high', 'important', '1', 'urgent')
|
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
|
priority = 2
|
||||||
|
|
||||||
if ticket == None:
|
if ticket == None:
|
||||||
@ -145,7 +166,7 @@ def ticket_from_message(message, queue):
|
|||||||
t.save()
|
t.save()
|
||||||
new = True
|
new = True
|
||||||
update = ''
|
update = ''
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'ticket': t,
|
'ticket': t,
|
||||||
'queue': queue,
|
'queue': queue,
|
||||||
@ -154,26 +175,57 @@ def ticket_from_message(message, queue):
|
|||||||
if new:
|
if new:
|
||||||
|
|
||||||
if sender_email:
|
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:
|
if queue.new_ticket_cc:
|
||||||
send_templated_mail('newticket_cc', context, recipients=queue.new_ticket_cc, sender=queue.from_address, fail_silently=True)
|
send_templated_mail(
|
||||||
|
'newticket_cc',
|
||||||
if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc:
|
context,
|
||||||
send_templated_mail('newticket_cc', context, recipients=queue.updated_ticket_cc, sender=queue.from_address, fail_silently=True)
|
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:
|
else:
|
||||||
update = _(' (Updated)')
|
update = _(' (Updated)')
|
||||||
|
|
||||||
if t.assigned_to:
|
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:
|
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(
|
f = FollowUp(
|
||||||
ticket = t,
|
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(),
|
date = datetime.now(),
|
||||||
public = True,
|
public = True,
|
||||||
comment = body,
|
comment = body,
|
||||||
@ -184,11 +236,17 @@ def ticket_from_message(message, queue):
|
|||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
filename = file['filename'].replace(' ', '_')
|
filename = file['filename'].replace(' ', '_')
|
||||||
a = Attachment(followup=f, filename=filename, mime_type=file['type'], size=len(file['content']))
|
a = Attachment(
|
||||||
#a.save_file_file(file['filename'], file['content'])
|
followup=f,
|
||||||
|
filename=filename,
|
||||||
|
mime_type=file['type'],
|
||||||
|
size=len(file['content']),
|
||||||
|
)
|
||||||
a.file.save(file['filename'], ContentFile(file['content']))
|
a.file.save(file['filename'], ContentFile(file['content']))
|
||||||
a.save()
|
a.save()
|
||||||
print " - %s" % file['filename']
|
print " - %s" % file['filename']
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
process_email()
|
process_email()
|
||||||
|
|
||||||
|
629
models.py
629
models.py
@ -14,69 +14,192 @@ from django.db import models
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class Queue(models.Model):
|
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.
|
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.
|
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):
|
title = models.CharField(
|
||||||
if not self.email_address:
|
_('Title'),
|
||||||
return u'NO QUEUE EMAIL ADDRESS DEFINED <%s>' % settings.DEFAULT_FROM_EMAIL
|
max_length=100,
|
||||||
else:
|
)
|
||||||
return u'%s <%s>' % (self.title, self.email_address)
|
|
||||||
from_address = property(_from_address)
|
|
||||||
|
|
||||||
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'))
|
slug = models.SlugField(
|
||||||
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'),
|
||||||
|
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_address = models.EmailField(
|
||||||
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".'))
|
_('E-Mail Address'),
|
||||||
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.'))
|
blank=True,
|
||||||
email_box_user = models.CharField(_('E-Mail Username'), max_length=200, blank=True, null=True, help_text=_('Username for accessing this mailbox.'))
|
null=True,
|
||||||
email_box_pass = models.CharField(_('E-Mail Password'), max_length=200, blank=True, null=True, help_text=_('Password for the above username'))
|
help_text=_('All outgoing e-mails for this queue will use this e-mail '
|
||||||
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.'))
|
'address. If you use IMAP or POP3, this should be the e-mail '
|
||||||
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')
|
'address for that mailbox.'),
|
||||||
email_box_last_check = models.DateTimeField(blank=True, null=True, editable=False) # Updated by the auto-pop3-and-imap-checker
|
)
|
||||||
|
|
||||||
|
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):
|
def __unicode__(self):
|
||||||
return u"%s" % self.title
|
return u"%s" % self.title
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('title',)
|
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):
|
def save(self):
|
||||||
if self.email_box_type == 'imap' and not self.email_box_imap_folder:
|
if self.email_box_type == 'imap' and not self.email_box_imap_folder:
|
||||||
self.email_box_imap_folder = 'INBOX'
|
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()
|
super(Queue, self).save()
|
||||||
|
|
||||||
|
|
||||||
class Ticket(models.Model):
|
class Ticket(models.Model):
|
||||||
"""
|
"""
|
||||||
To allow a ticket to be entered as quickly as possible, only the
|
To allow a ticket to be entered as quickly as possible, only the
|
||||||
bare minimum fields are required. These basically allow us to
|
bare minimum fields are required. These basically allow us to
|
||||||
sort and manage the ticket. The user can always go back and
|
sort and manage the ticket. The user can always go back and
|
||||||
enter more information later.
|
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
|
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
|
and get off the phone, then add in further detail at a later time
|
||||||
(once the customer is not on the line).
|
(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.
|
the dashboard to prompt users to take ownership of them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -84,7 +207,7 @@ class Ticket(models.Model):
|
|||||||
REOPENED_STATUS = 2
|
REOPENED_STATUS = 2
|
||||||
RESOLVED_STATUS = 3
|
RESOLVED_STATUS = 3
|
||||||
CLOSED_STATUS = 4
|
CLOSED_STATUS = 4
|
||||||
|
|
||||||
STATUS_CHOICES = (
|
STATUS_CHOICES = (
|
||||||
(OPEN_STATUS, _('Open')),
|
(OPEN_STATUS, _('Open')),
|
||||||
(REOPENED_STATUS, _('Reopened')),
|
(REOPENED_STATUS, _('Reopened')),
|
||||||
@ -100,26 +223,87 @@ class Ticket(models.Model):
|
|||||||
(5, _('5. Very Low')),
|
(5, _('5. Very Low')),
|
||||||
)
|
)
|
||||||
|
|
||||||
title = models.CharField(_('Title'), max_length=200)
|
title = models.CharField(
|
||||||
|
_('Title'),
|
||||||
|
max_length=200,
|
||||||
|
)
|
||||||
|
|
||||||
queue = models.ForeignKey(Queue)
|
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)
|
modified = models.DateTimeField(
|
||||||
resolution = models.TextField(_('Resolution'), blank=True, null=True)
|
_('Modified'),
|
||||||
|
blank=True,
|
||||||
|
help_text=_('Date this ticket was most recently changed.'),
|
||||||
|
)
|
||||||
|
|
||||||
priority = models.IntegerField(_('Priority'), choices=PRIORITY_CHOICES, default=3, blank=3)
|
submitter_email = models.EmailField(
|
||||||
|
_('Submitter E-Mail'),
|
||||||
last_escalation = models.DateTimeField(blank=True, null=True, editable=False)
|
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):
|
def _get_assigned_to(self):
|
||||||
""" Custom property to allow us to easily print 'Unassigned' if a
|
""" 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
|
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. """
|
has a full name configured, we use that, otherwise their username. """
|
||||||
if not self.assigned_to:
|
if not self.assigned_to:
|
||||||
return _('Unassigned')
|
return _('Unassigned')
|
||||||
@ -131,44 +315,69 @@ class Ticket(models.Model):
|
|||||||
get_assigned_to = property(_get_assigned_to)
|
get_assigned_to = property(_get_assigned_to)
|
||||||
|
|
||||||
def _get_ticket(self):
|
def _get_ticket(self):
|
||||||
""" A user-friendly ticket ID, which is a combination of ticket ID
|
""" A user-friendly ticket ID, which is a combination of ticket ID
|
||||||
and queue slug. This is generally used in e-mails. """
|
and queue slug. This is generally used in e-mail subjects. """
|
||||||
|
|
||||||
return u"[%s]" % (self.ticket_for_url)
|
return u"[%s]" % (self.ticket_for_url)
|
||||||
ticket = property(_get_ticket)
|
ticket = property(_get_ticket)
|
||||||
|
|
||||||
def _get_ticket_for_url(self):
|
def _get_ticket_for_url(self):
|
||||||
|
""" A URL-friendly ticket ID, used in links. """
|
||||||
return u"%s-%s" % (self.queue.slug, self.id)
|
return u"%s-%s" % (self.queue.slug, self.id)
|
||||||
ticket_for_url = property(_get_ticket_for_url)
|
ticket_for_url = property(_get_ticket_for_url)
|
||||||
|
|
||||||
def _get_priority_img(self):
|
def _get_priority_img(self):
|
||||||
|
""" Image-based representation of the priority """
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
return u"%s/helpdesk/priorities/priority%s.png" % (settings.MEDIA_URL, self.priority)
|
return u"%s/helpdesk/priorities/priority%s.png" % (settings.MEDIA_URL, self.priority)
|
||||||
get_priority_img = property(_get_priority_img)
|
get_priority_img = property(_get_priority_img)
|
||||||
|
|
||||||
def _get_priority_span(self):
|
def _get_priority_span(self):
|
||||||
|
"""
|
||||||
|
A HTML <span> providing a CSS_styled representation of the priority.
|
||||||
|
"""
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
return mark_safe(u"<span class='priority%s'>%s</span>" % (self.priority, self.priority))
|
return mark_safe(u"<span class='priority%s'>%s</span>" % (self.priority, self.priority))
|
||||||
get_priority_span = property(_get_priority_span)
|
get_priority_span = property(_get_priority_span)
|
||||||
|
|
||||||
def _get_status(self):
|
def _get_status(self):
|
||||||
|
"""
|
||||||
|
Displays the ticket status, with an "On Hold" message if needed.
|
||||||
|
"""
|
||||||
held_msg = ''
|
held_msg = ''
|
||||||
if self.on_hold: held_msg = _(' - On Hold')
|
if self.on_hold: held_msg = _(' - On Hold')
|
||||||
return u'%s%s' % (self.get_status_display(), held_msg)
|
return u'%s%s' % (self.get_status_display(), held_msg)
|
||||||
get_status = property(_get_status)
|
get_status = property(_get_status)
|
||||||
|
|
||||||
def _get_ticket_url(self):
|
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.contrib.sites.models import Site
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
site = Site.objects.get_current()
|
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)
|
ticket_url = property(_get_ticket_url)
|
||||||
|
|
||||||
def _get_staff_url(self):
|
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.contrib.sites.models import Site
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
site = Site.objects.get_current()
|
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)
|
staff_url = property(_get_staff_url)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -185,67 +394,120 @@ class Ticket(models.Model):
|
|||||||
if not self.id:
|
if not self.id:
|
||||||
# This is a new ticket as no ID yet exists.
|
# This is a new ticket as no ID yet exists.
|
||||||
self.created = datetime.now()
|
self.created = datetime.now()
|
||||||
|
|
||||||
if not self.priority:
|
if not self.priority:
|
||||||
self.priority = 3
|
self.priority = 3
|
||||||
|
|
||||||
self.modified = datetime.now()
|
self.modified = datetime.now()
|
||||||
|
|
||||||
super(Ticket, self).save()
|
super(Ticket, self).save()
|
||||||
|
|
||||||
|
|
||||||
class FollowUpManager(models.Manager):
|
class FollowUpManager(models.Manager):
|
||||||
def private_followups(self):
|
def private_followups(self):
|
||||||
return self.filter(public=False)
|
return self.filter(public=False)
|
||||||
|
|
||||||
def public_followups(self):
|
def public_followups(self):
|
||||||
return self.filter(public=True)
|
return self.filter(public=True)
|
||||||
|
|
||||||
|
|
||||||
class FollowUp(models.Model):
|
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.
|
to enable easy flagging of details on the view-ticket page.
|
||||||
|
|
||||||
The title is automatically generated at save-time, based on what action
|
The title is automatically generated at save-time, based on what action
|
||||||
the user took.
|
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.
|
although all staff can see them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ticket = models.ForeignKey(Ticket)
|
ticket = models.ForeignKey(Ticket)
|
||||||
date = models.DateTimeField(_('Date'), auto_now_add=True)
|
|
||||||
title = models.CharField(_('Title'), max_length=200, blank=True, null=True)
|
date = models.DateTimeField(
|
||||||
comment = models.TextField(_('Comment'), blank=True, null=True)
|
_('Date'),
|
||||||
public = models.BooleanField(_('Public'), blank=True, null=True)
|
)
|
||||||
user = models.ForeignKey(User, blank=True, null=True)
|
|
||||||
|
title = models.CharField(
|
||||||
new_status = models.IntegerField(_('New Status'), choices=Ticket.STATUS_CHOICES, blank=True, null=True)
|
_('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()
|
objects = FollowUpManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['date']
|
ordering = ['date']
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'%s' % self.title
|
return u'%s' % self.title
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
||||||
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
t = self.ticket
|
t = self.ticket
|
||||||
t.modified = datetime.now()
|
t.modified = datetime.now()
|
||||||
|
self.date = datetime.now()
|
||||||
t.save()
|
t.save()
|
||||||
super(FollowUp, self).save()
|
super(FollowUp, self).save()
|
||||||
|
|
||||||
|
|
||||||
class TicketChange(models.Model):
|
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.
|
etc) are tracked here for display purposes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
followup = models.ForeignKey(FollowUp)
|
followup = models.ForeignKey(FollowUp)
|
||||||
field = models.CharField(_('Field'), max_length=100)
|
|
||||||
old_value = models.TextField(_('Old Value'), blank=True, null=True)
|
field = models.CharField(
|
||||||
new_value = models.TextField(_('New Value'), blank=True, null=True)
|
_('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):
|
def __unicode__(self):
|
||||||
str = u'%s ' % field
|
str = u'%s ' % field
|
||||||
@ -254,7 +516,10 @@ class TicketChange(models.Model):
|
|||||||
elif not old_value:
|
elif not old_value:
|
||||||
str += _('set to %s' % new_value)
|
str += _('set to %s' % new_value)
|
||||||
else:
|
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
|
return str
|
||||||
|
|
||||||
|
|
||||||
@ -263,7 +528,7 @@ class DynamicFileField(models.FileField):
|
|||||||
Allows model instance to specify upload_to dynamically.
|
Allows model instance to specify upload_to dynamically.
|
||||||
|
|
||||||
Model class should have a method like:
|
Model class should have a method like:
|
||||||
|
|
||||||
def get_upload_to(self, attname):
|
def get_upload_to(self, attname):
|
||||||
return 'path/to/%d' % self.parent.id
|
return 'path/to/%d' % self.parent.id
|
||||||
|
|
||||||
@ -282,18 +547,43 @@ class DynamicFileField(models.FileField):
|
|||||||
|
|
||||||
def db_type(self):
|
def db_type(self):
|
||||||
"""Required by Django for ORM."""
|
"""Required by Django for ORM."""
|
||||||
return 'varchar(100)'
|
return 'varchar(100)'
|
||||||
|
|
||||||
|
|
||||||
class Attachment(models.Model):
|
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)
|
followup = models.ForeignKey(FollowUp)
|
||||||
file = DynamicFileField(_('File'), upload_to='helpdesk/attachments')
|
|
||||||
filename = models.CharField(_('Filename'), max_length=100)
|
file = DynamicFileField(
|
||||||
mime_type = models.CharField(_('MIME Type'), max_length=30)
|
_('File'),
|
||||||
size = models.IntegerField(_('Size'), help_text=_('Size of this file in bytes'))
|
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):
|
def get_upload_to(self, field_attname):
|
||||||
""" Get upload_to path specific to this item """
|
""" 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):
|
def __unicode__(self):
|
||||||
return u'%s' % self.filename
|
return u'%s' % self.filename
|
||||||
@ -303,18 +593,38 @@ class Attachment(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class PreSetReply(models.Model):
|
class PreSetReply(models.Model):
|
||||||
""" We can allow the admin to define a number of pre-set replies, used to
|
"""
|
||||||
|
We can allow the admin to define a number of pre-set replies, used to
|
||||||
simplify the sending of updates and resolutions. These are basically Django
|
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
|
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
|
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.
|
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
|
When replying to a ticket, the user can select any reply set for the current
|
||||||
queue, and the body text is fetched via AJAX."""
|
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.'))
|
queues = models.ManyToManyField(
|
||||||
body = models.TextField(_('Body'), help_text=_('Context available: {{ ticket }} - ticket object (eg {{ ticket.title }}); {{ queue }} - The queue; and {{ user }} - the current user.'))
|
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:
|
class Meta:
|
||||||
ordering = ['name',]
|
ordering = ['name',]
|
||||||
@ -322,28 +632,84 @@ class PreSetReply(models.Model):
|
|||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'%s' % self.name
|
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)
|
class EscalationExclusion(models.Model):
|
||||||
|
"""
|
||||||
date = models.DateField(_('Date'), help_text=_('Date on which escalation should not happen'))
|
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):
|
def __unicode__(self):
|
||||||
return u'%s' % self.name
|
return u'%s' % self.name
|
||||||
|
|
||||||
|
|
||||||
class EmailTemplate(models.Model):
|
class EmailTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
Since these are more likely to be changed than other templates, we store
|
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)
|
template_name = models.CharField(
|
||||||
|
_('Template Name'),
|
||||||
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.'))
|
max_length=100,
|
||||||
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.'))
|
unique=True,
|
||||||
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.'))
|
|
||||||
|
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):
|
def __unicode__(self):
|
||||||
return u'%s' % self.template_name
|
return u'%s' % self.template_name
|
||||||
@ -351,40 +717,72 @@ class EmailTemplate(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['template_name',]
|
ordering = ['template_name',]
|
||||||
|
|
||||||
|
|
||||||
class KBCategory(models.Model):
|
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.
|
listing of questions & answers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title = models.CharField(_('Title'), max_length=100)
|
title = models.CharField(
|
||||||
slug = models.SlugField(_('Slug'))
|
_('Title'),
|
||||||
description = models.TextField(_('Description'))
|
max_length=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
slug = models.SlugField(
|
||||||
|
_('Slug'),
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.TextField(
|
||||||
|
_('Description'),
|
||||||
|
)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'%s' % self.title
|
return u'%s' % self.title
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['title',]
|
ordering = ['title',]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return ('helpdesk_kb_category', [str(self.slug)])
|
return ('helpdesk_kb_category', [str(self.slug)])
|
||||||
get_absolute_url = models.permalink(get_absolute_url)
|
get_absolute_url = models.permalink(get_absolute_url)
|
||||||
|
|
||||||
|
|
||||||
class KBItem(models.Model):
|
class KBItem(models.Model):
|
||||||
"""
|
"""
|
||||||
An item within the knowledgebase. Very straightforward question/answer
|
An item within the knowledgebase. Very straightforward question/answer
|
||||||
style system.
|
style system.
|
||||||
"""
|
"""
|
||||||
category = models.ForeignKey(KBCategory)
|
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):
|
def save(self):
|
||||||
self.last_updated = datetime.now()
|
self.last_updated = datetime.now()
|
||||||
@ -396,7 +794,7 @@ class KBItem(models.Model):
|
|||||||
else:
|
else:
|
||||||
return _('Unrated')
|
return _('Unrated')
|
||||||
score = property(_score)
|
score = property(_score)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'%s' % self.title
|
return u'%s' % self.title
|
||||||
|
|
||||||
@ -406,3 +804,4 @@ class KBItem(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return ('helpdesk_kb_item', [str(self.id)])
|
return ('helpdesk_kb_item', [str(self.id)])
|
||||||
get_absolute_url = models.permalink(get_absolute_url)
|
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.
|
(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:
|
equivilent of 'if x in y' in templates. eg:
|
||||||
|
|
||||||
Assuming 'food' = 'pizza' and 'best_foods' = ['pizza', 'pie', 'cake]:
|
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.
|
Your food isn't one of our favourites.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
def in_list(value, arg):
|
def in_list(value, arg):
|
||||||
return value in arg
|
return value in arg
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
register.filter(in_list)
|
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
|
templatetags/ticket_to_link.py - Used in ticket comments to allow wiki-style
|
||||||
linking to other tickets. Including text such
|
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
|
that text to ticket number 3180, with styling
|
||||||
to show the status of that ticket (eg a closed
|
to show the status of that ticket (eg a closed
|
||||||
ticket would have a strikethrough).
|
ticket would have a strikethrough).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from helpdesk.models import Ticket
|
from helpdesk.models import Ticket
|
||||||
|
|
||||||
|
|
||||||
class ReverseProxy:
|
class ReverseProxy:
|
||||||
def __init__(self, sequence):
|
def __init__(self, sequence):
|
||||||
self.sequence = sequence
|
self.sequence = sequence
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
length = len(self.sequence)
|
length = len(self.sequence)
|
||||||
i = length
|
i = length
|
||||||
@ -24,16 +30,15 @@ class ReverseProxy:
|
|||||||
i = i - 1
|
i = i - 1
|
||||||
yield self.sequence[i]
|
yield self.sequence[i]
|
||||||
|
|
||||||
|
|
||||||
def num_to_link(text):
|
def num_to_link(text):
|
||||||
if text == '':
|
if text == '':
|
||||||
return text
|
return text
|
||||||
import re
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
|
|
||||||
matches = []
|
matches = []
|
||||||
for match in re.finditer("#(\d+)", text):
|
for match in re.finditer("#(\d+)", text):
|
||||||
matches.append(match)
|
matches.append(match)
|
||||||
|
|
||||||
for match in ReverseProxy(matches):
|
for match in ReverseProxy(matches):
|
||||||
start = match.start()
|
start = match.start()
|
||||||
end = match.end()
|
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.
|
(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.
|
views for simplicity in linking later on.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
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 helpdesk.views.feeds import feed_setup
|
||||||
|
|
||||||
from django.contrib.syndication.views import feed as django_feed
|
|
||||||
|
|
||||||
urlpatterns = patterns('helpdesk.views.staff',
|
urlpatterns = patterns('helpdesk.views.staff',
|
||||||
url(r'^dashboard/$',
|
url(r'^dashboard/$',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
name='helpdesk_dashboard'),
|
name='helpdesk_dashboard'),
|
||||||
|
|
||||||
url(r'^tickets/$',
|
url(r'^tickets/$',
|
||||||
'ticket_list',
|
'ticket_list',
|
||||||
name='helpdesk_list'),
|
name='helpdesk_list'),
|
||||||
|
|
||||||
url(r'^tickets/submit/$',
|
url(r'^tickets/submit/$',
|
||||||
'create_ticket',
|
'create_ticket',
|
||||||
name='helpdesk_submit'),
|
name='helpdesk_submit'),
|
||||||
|
|
||||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/$',
|
url(r'^tickets/(?P<ticket_id>[0-9]+)/$',
|
||||||
'view_ticket',
|
'view_ticket',
|
||||||
name='helpdesk_view'),
|
name='helpdesk_view'),
|
||||||
|
|
||||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$',
|
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$',
|
||||||
'update_ticket',
|
'update_ticket',
|
||||||
name='helpdesk_update'),
|
name='helpdesk_update'),
|
||||||
|
|
||||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/delete/$',
|
url(r'^tickets/(?P<ticket_id>[0-9]+)/delete/$',
|
||||||
'delete_ticket',
|
'delete_ticket',
|
||||||
name='helpdesk_delete'),
|
name='helpdesk_delete'),
|
||||||
|
|
||||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/hold/$',
|
url(r'^tickets/(?P<ticket_id>[0-9]+)/hold/$',
|
||||||
'hold_ticket',
|
'hold_ticket',
|
||||||
name='helpdesk_hold'),
|
name='helpdesk_hold'),
|
||||||
@ -51,7 +49,7 @@ urlpatterns = patterns('helpdesk.views.staff',
|
|||||||
url(r'^raw/(?P<type>\w+)/$',
|
url(r'^raw/(?P<type>\w+)/$',
|
||||||
'raw_details',
|
'raw_details',
|
||||||
name='helpdesk_raw'),
|
name='helpdesk_raw'),
|
||||||
|
|
||||||
url(r'^rss/$',
|
url(r'^rss/$',
|
||||||
'rss_list',
|
'rss_list',
|
||||||
name='helpdesk_rss_index'),
|
name='helpdesk_rss_index'),
|
||||||
@ -59,14 +57,14 @@ urlpatterns = patterns('helpdesk.views.staff',
|
|||||||
url(r'^reports/$',
|
url(r'^reports/$',
|
||||||
'report_index',
|
'report_index',
|
||||||
name='helpdesk_report_index'),
|
name='helpdesk_report_index'),
|
||||||
|
|
||||||
url(r'^reports/(?P<report>\w+)/$',
|
url(r'^reports/(?P<report>\w+)/$',
|
||||||
'run_report',
|
'run_report',
|
||||||
name='helpdesk_run_report'),
|
name='helpdesk_run_report'),
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns += patterns('helpdesk.views.public',
|
urlpatterns += patterns('helpdesk.views.public',
|
||||||
url(r'^$',
|
url(r'^$',
|
||||||
'homepage',
|
'homepage',
|
||||||
name='helpdesk_home'),
|
name='helpdesk_home'),
|
||||||
|
|
||||||
@ -80,7 +78,7 @@ urlpatterns += patterns('',
|
|||||||
login_required(django_feed),
|
login_required(django_feed),
|
||||||
{'feed_dict': feed_setup},
|
{'feed_dict': feed_setup},
|
||||||
name='helpdesk_rss'),
|
name='helpdesk_rss'),
|
||||||
|
|
||||||
url(r'^api/(?P<method>[a-z_-]+)/$',
|
url(r'^api/(?P<method>[a-z_-]+)/$',
|
||||||
'helpdesk.views.api.api',
|
'helpdesk.views.api.api',
|
||||||
name='helpdesk_api'),
|
name='helpdesk_api'),
|
||||||
@ -89,7 +87,7 @@ urlpatterns += patterns('',
|
|||||||
'django.views.generic.simple.direct_to_template',
|
'django.views.generic.simple.direct_to_template',
|
||||||
{'template': 'helpdesk/api_help.html',},
|
{'template': 'helpdesk/api_help.html',},
|
||||||
name='helpdesk_api_help'),
|
name='helpdesk_api_help'),
|
||||||
|
|
||||||
url(r'^login/$',
|
url(r'^login/$',
|
||||||
'django.contrib.auth.views.login',
|
'django.contrib.auth.views.login',
|
||||||
name='login'),
|
name='login'),
|
||||||
@ -102,13 +100,13 @@ urlpatterns += patterns('',
|
|||||||
urlpatterns += patterns('helpdesk.views.kb',
|
urlpatterns += patterns('helpdesk.views.kb',
|
||||||
url(r'^kb/$',
|
url(r'^kb/$',
|
||||||
'index', name='helpdesk_kb_index'),
|
'index', name='helpdesk_kb_index'),
|
||||||
|
|
||||||
url(r'^kb/(?P<slug>[A-Za-z_-]+)/$',
|
url(r'^kb/(?P<slug>[A-Za-z_-]+)/$',
|
||||||
'category', name='helpdesk_kb_category'),
|
'category', name='helpdesk_kb_category'),
|
||||||
|
|
||||||
url(r'^kb/(?P<item>[0-9]+)/$',
|
url(r'^kb/(?P<item>[0-9]+)/$',
|
||||||
'item', name='helpdesk_kb_item'),
|
'item', name='helpdesk_kb_item'),
|
||||||
|
|
||||||
url(r'^kb/(?P<item>[0-9]+)/vote/$',
|
url(r'^kb/(?P<item>[0-9]+)/vote/$',
|
||||||
'vote', name='helpdesk_kb_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.
|
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||||
|
|
||||||
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
(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.py - Wrapper around API calls, and core functions to provide complete
|
||||||
API to third party applications.
|
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
|
(obviously, substitute helpdesk for your Jutda Helpdesk URI), or by reading
|
||||||
through templates/helpdesk/api_help.html.
|
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 import authenticate
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render_to_response
|
from django.shortcuts import render_to_response
|
||||||
from django.template import loader, Context
|
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.lib import send_templated_mail
|
||||||
from helpdesk.models import Ticket, Queue, FollowUp
|
from helpdesk.models import Ticket, Queue, FollowUp
|
||||||
from helpdesk.forms import TicketForm
|
|
||||||
|
|
||||||
STATUS_OK = 200
|
STATUS_OK = 200
|
||||||
|
|
||||||
@ -31,27 +32,34 @@ STATUS_ERROR_NOT_FOUND = 404
|
|||||||
STATUS_ERROR_PERMISSIONS = 403
|
STATUS_ERROR_PERMISSIONS = 403
|
||||||
STATUS_ERROR_BADMETHOD = 405
|
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:
|
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 be sent via HTTP POST
|
||||||
* The request must contain a 'user' and 'password' which
|
* The request must contain a 'user' and 'password' which
|
||||||
must be valid users
|
must be valid users
|
||||||
* The method must match one of the public methods of the API class.
|
* 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':
|
if request.method != 'POST':
|
||||||
return api_return(STATUS_ERROR_BADMETHOD)
|
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:
|
if request.user is None:
|
||||||
return api_return(STATUS_ERROR_PERMISSIONS)
|
return api_return(STATUS_ERROR_PERMISSIONS)
|
||||||
|
|
||||||
api = API(request)
|
api = API(request)
|
||||||
if hasattr(api, 'api_public_%s' % method):
|
if hasattr(api, 'api_public_%s' % method):
|
||||||
return getattr(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'
|
content_type = 'text/plain'
|
||||||
if status == STATUS_OK and json:
|
if status == STATUS_OK and json:
|
||||||
content_type = 'text/json'
|
content_type = 'text/json'
|
||||||
|
|
||||||
if text is None:
|
if text is None:
|
||||||
if status == STATUS_ERROR:
|
if status == STATUS_ERROR:
|
||||||
text = 'Error'
|
text = 'Error'
|
||||||
@ -75,10 +83,12 @@ def api_return(status, text='', json=False):
|
|||||||
text = 'Invalid request method'
|
text = 'Invalid request method'
|
||||||
elif status == STATUS_OK:
|
elif status == STATUS_OK:
|
||||||
text = 'OK'
|
text = 'OK'
|
||||||
|
|
||||||
r = HttpResponse(status=status, content=text, content_type=content_type)
|
r = HttpResponse(status=status, content=text, content_type=content_type)
|
||||||
|
|
||||||
if status == STATUS_ERROR_BADMETHOD:
|
if status == STATUS_ERROR_BADMETHOD:
|
||||||
r.Allow = 'POST'
|
r.Allow = 'POST'
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@ -86,27 +96,30 @@ class API:
|
|||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
def api_public_create_ticket(self):
|
|
||||||
|
|
||||||
|
def api_public_create_ticket(self):
|
||||||
form = TicketForm(self.request.POST)
|
form = TicketForm(self.request.POST)
|
||||||
form.fields['queue'].choices = [[q.id, q.title] for q in Queue.objects.all()]
|
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)]
|
form.fields['assigned_to'].choices = [[u.id, u.username] for u in User.objects.filter(is_active=True)]
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
ticket = form.save(user=self.request.user)
|
ticket = form.save(user=self.request.user)
|
||||||
return api_return(STATUS_OK, "%s" % ticket.id)
|
return api_return(STATUS_OK, "%s" % ticket.id)
|
||||||
else:
|
else:
|
||||||
return api_return(STATUS_ERROR, text=form.errors.as_text())
|
return api_return(STATUS_ERROR, text=form.errors.as_text())
|
||||||
|
|
||||||
|
|
||||||
def api_public_list_queues(self):
|
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)
|
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):
|
def api_public_find_user(self):
|
||||||
username = self.request.POST.get('username', False)
|
username = self.request.POST.get('username', False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
u = User.objects.get(username=username)
|
u = User.objects.get(username=username)
|
||||||
return api_return(STATUS_OK, "%s" % u.id)
|
return api_return(STATUS_OK, "%s" % u.id)
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return api_return(STATUS_ERROR, "Invalid username provided")
|
return api_return(STATUS_ERROR, "Invalid username provided")
|
||||||
|
|
||||||
@ -121,9 +134,10 @@ class API:
|
|||||||
return api_return(STATUS_ERROR, "Invalid ticket ID")
|
return api_return(STATUS_ERROR, "Invalid ticket ID")
|
||||||
|
|
||||||
ticket.delete()
|
ticket.delete()
|
||||||
|
|
||||||
return api_return(STATUS_OK)
|
return api_return(STATUS_OK)
|
||||||
|
|
||||||
|
|
||||||
def api_public_hold_ticket(self):
|
def api_public_hold_ticket(self):
|
||||||
try:
|
try:
|
||||||
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
||||||
@ -135,7 +149,7 @@ class API:
|
|||||||
|
|
||||||
return api_return(STATUS_OK)
|
return api_return(STATUS_OK)
|
||||||
|
|
||||||
|
|
||||||
def api_public_unhold_ticket(self):
|
def api_public_unhold_ticket(self):
|
||||||
try:
|
try:
|
||||||
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
ticket = Ticket.objects.get(id=self.request.POST.get('ticket', False))
|
||||||
@ -162,12 +176,20 @@ class API:
|
|||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
return api_return(STATUS_ERROR, "Blank 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:
|
if public:
|
||||||
f.public = True
|
f.public = True
|
||||||
|
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'ticket': ticket,
|
'ticket': ticket,
|
||||||
'queue': ticket.queue,
|
'queue': ticket.queue,
|
||||||
@ -175,13 +197,31 @@ class API:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if public and ticket.submitter_email:
|
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:
|
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:
|
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()
|
ticket.save()
|
||||||
|
|
||||||
@ -198,26 +238,51 @@ class API:
|
|||||||
|
|
||||||
if not resolution:
|
if not resolution:
|
||||||
return api_return(STATUS_ERROR, "Blank 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()
|
f.save()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'ticket': ticket,
|
'ticket': ticket,
|
||||||
'queue': ticket.queue,
|
'queue': ticket.queue,
|
||||||
'resolution': f.comment,
|
'resolution': f.comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title)
|
subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title)
|
||||||
|
|
||||||
if ticket.submitter_email:
|
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:
|
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:
|
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.resoltuion = f.comment
|
||||||
ticket.status = Ticket.RESOLVED_STATUS
|
ticket.status = Ticket.RESOLVED_STATUS
|
||||||
@ -225,3 +290,4 @@ class API:
|
|||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
return api_return(STATUS_OK)
|
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.auth.models import User
|
||||||
from django.contrib.syndication.feeds import Feed
|
from django.contrib.syndication.feeds import Feed
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
@ -23,28 +32,55 @@ class OpenTicketsByUser(Feed):
|
|||||||
|
|
||||||
def title(self, obj):
|
def title(self, obj):
|
||||||
if obj['queue']:
|
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:
|
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):
|
def description(self, obj):
|
||||||
if obj['queue']:
|
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:
|
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):
|
def link(self, obj):
|
||||||
if obj['queue']:
|
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:
|
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):
|
def items(self, obj):
|
||||||
if obj['queue']:
|
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:
|
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):
|
def item_pubdate(self, item):
|
||||||
return item.created
|
return item.created
|
||||||
|
|
||||||
@ -58,18 +94,22 @@ class OpenTicketsByUser(Feed):
|
|||||||
class UnassignedTickets(Feed):
|
class UnassignedTickets(Feed):
|
||||||
title_template = 'helpdesk/rss/ticket_title.html'
|
title_template = 'helpdesk/rss/ticket_title.html'
|
||||||
description_template = 'helpdesk/rss/ticket_description.html'
|
description_template = 'helpdesk/rss/ticket_description.html'
|
||||||
|
|
||||||
title = _('Helpdesk: Unassigned Tickets')
|
title = _('Helpdesk: Unassigned Tickets')
|
||||||
description = _('Unassigned Open and Reopened tickets')
|
description = _('Unassigned Open and Reopened tickets')
|
||||||
link = ''#%s?assigned_to=' % reverse('helpdesk_list')
|
link = ''#%s?assigned_to=' % reverse('helpdesk_list')
|
||||||
|
|
||||||
def items(self, obj):
|
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):
|
def item_pubdate(self, item):
|
||||||
return item.created
|
return item.created
|
||||||
|
|
||||||
|
|
||||||
def item_author_name(self, item):
|
def item_author_name(self, item):
|
||||||
if item.assigned_to:
|
if item.assigned_to:
|
||||||
return item.assigned_to.username
|
return item.assigned_to.username
|
||||||
@ -99,20 +139,31 @@ class OpenTicketsByQueue(Feed):
|
|||||||
return Queue.objects.get(slug__exact=bits[0])
|
return Queue.objects.get(slug__exact=bits[0])
|
||||||
|
|
||||||
def title(self, obj):
|
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):
|
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):
|
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):
|
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):
|
def item_pubdate(self, item):
|
||||||
return item.created
|
return item.created
|
||||||
|
|
||||||
def item_author_name(self, item):
|
def item_author_name(self, item):
|
||||||
if item.assigned_to:
|
if item.assigned_to:
|
||||||
return item.assigned_to.username
|
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.
|
(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
|
simple categorised question/answer system to show common
|
||||||
resolutions to common problems.
|
resolutions to common problems.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
@ -16,23 +17,26 @@ from django.utils.translation import ugettext as _
|
|||||||
|
|
||||||
from helpdesk.models import KBCategory, KBItem
|
from helpdesk.models import KBCategory, KBItem
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
category_list = KBCategory.objects.all()
|
category_list = KBCategory.objects.all()
|
||||||
# Add most popular items here.
|
# TODO: It'd be great to have a list of most popular items here.
|
||||||
return render_to_response('helpdesk/kb_index.html',
|
return render_to_response('helpdesk/kb_index.html',
|
||||||
RequestContext(request, {
|
RequestContext(request, {
|
||||||
'categories': category_list,
|
'categories': category_list,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
def category(request, slug):
|
def category(request, slug):
|
||||||
category = get_object_or_404(KBCategory, slug__iexact=slug)
|
category = get_object_or_404(KBCategory, slug__iexact=slug)
|
||||||
items = category.kbitem_set.all()
|
items = category.kbitem_set.all()
|
||||||
return render_to_response('helpdesk/kb_category.html',
|
return render_to_response('helpdesk/kb_category.html',
|
||||||
RequestContext(request, {
|
RequestContext(request, {
|
||||||
'category': category,
|
'category': category,
|
||||||
'items': items,
|
'items': items,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
def item(request, item):
|
def item(request, item):
|
||||||
item = get_object_or_404(KBItem, pk=item)
|
item = get_object_or_404(KBItem, pk=item)
|
||||||
return render_to_response('helpdesk/kb_item.html',
|
return render_to_response('helpdesk/kb_item.html',
|
||||||
@ -40,6 +44,7 @@ def item(request, item):
|
|||||||
'item': item,
|
'item': item,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
def vote(request, item):
|
def vote(request, item):
|
||||||
item = get_object_or_404(KBItem, pk=item)
|
item = get_object_or_404(KBItem, pk=item)
|
||||||
vote = request.GET.get('vote', None)
|
vote = request.GET.get('vote', None)
|
||||||
@ -50,3 +55,4 @@ def vote(request, item):
|
|||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
return HttpResponseRedirect(item.get_absolute_url())
|
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
|
views/public.py - All public facing views, eg non-staff (no authentication
|
||||||
required) views.
|
required) views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
@ -18,16 +19,21 @@ from helpdesk.forms import PublicTicketForm
|
|||||||
from helpdesk.lib import send_templated_mail
|
from helpdesk.lib import send_templated_mail
|
||||||
from helpdesk.models import Ticket, Queue
|
from helpdesk.models import Ticket, Queue
|
||||||
|
|
||||||
|
|
||||||
def homepage(request):
|
def homepage(request):
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated():
|
||||||
return HttpResponseRedirect(reverse('helpdesk_dashboard'))
|
return HttpResponseRedirect(reverse('helpdesk_dashboard'))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = PublicTicketForm(request.POST)
|
form = PublicTicketForm(request.POST)
|
||||||
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
|
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
ticket = form.save()
|
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:
|
else:
|
||||||
form = PublicTicketForm()
|
form = PublicTicketForm()
|
||||||
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.filter(allow_public_submission=True)]
|
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,
|
'form': form,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
def view_ticket(request):
|
def view_ticket(request):
|
||||||
ticket = request.GET.get('ticket', '')
|
ticket = request.GET.get('ticket', '')
|
||||||
email = request.GET.get('email', '')
|
email = request.GET.get('email', '')
|
||||||
error_message = ''
|
error_message = ''
|
||||||
|
|
||||||
if ticket and email:
|
if ticket and email:
|
||||||
|
queue, ticket_id = ticket.split('-')
|
||||||
try:
|
try:
|
||||||
queue, ticket_id = ticket.split('-')
|
|
||||||
t = Ticket.objects.get(id=ticket_id, queue__slug__iexact=queue, submitter_email__iexact=email)
|
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:
|
except Ticket.DoesNotExist:
|
||||||
t = False;
|
t = False;
|
||||||
error_message = _('Invalid ticket ID or e-mail address. Please try again.')
|
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, {
|
RequestContext(request, {
|
||||||
'ticket': ticket,
|
'ticket': ticket,
|
||||||
'email': email,
|
'email': email,
|
||||||
'error_message': error_message,
|
'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.
|
(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.
|
renders all staff-facing views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db import connection
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponseRedirect, Http404, HttpResponse
|
from django.http import HttpResponseRedirect, Http404, HttpResponse
|
||||||
from django.shortcuts import render_to_response, get_object_or_404
|
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.lib import send_templated_mail, line_chart, bar_chart, query_to_dict
|
||||||
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment
|
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment
|
||||||
|
|
||||||
|
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
"""
|
"""
|
||||||
A quick summary overview for users: A list of their own tickets, a table
|
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.
|
with options for them to 'Take' ownership of said tickets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tickets = Ticket.objects.filter(assigned_to=request.user).exclude(status=Ticket.CLOSED_STATUS)
|
tickets = Ticket.objects
|
||||||
unassigned_tickets = Ticket.objects.filter(assigned_to__isnull=True).exclude(status=Ticket.CLOSED_STATUS)
|
.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 = connection.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT q.id as queue,
|
SELECT q.id as queue,
|
||||||
@ -55,9 +68,10 @@ def dashboard(request):
|
|||||||
}))
|
}))
|
||||||
dashboard = login_required(dashboard)
|
dashboard = login_required(dashboard)
|
||||||
|
|
||||||
|
|
||||||
def delete_ticket(request, ticket_id):
|
def delete_ticket(request, ticket_id):
|
||||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return render_to_response('helpdesk/delete_ticket.html',
|
return render_to_response('helpdesk/delete_ticket.html',
|
||||||
RequestContext(request, {
|
RequestContext(request, {
|
||||||
@ -68,18 +82,31 @@ def delete_ticket(request, ticket_id):
|
|||||||
return HttpResponseRedirect(reverse('helpdesk_home'))
|
return HttpResponseRedirect(reverse('helpdesk_home'))
|
||||||
delete_ticket = login_required(delete_ticket)
|
delete_ticket = login_required(delete_ticket)
|
||||||
|
|
||||||
|
|
||||||
def view_ticket(request, ticket_id):
|
def view_ticket(request, ticket_id):
|
||||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||||
|
|
||||||
if request.GET.has_key('take'):
|
if request.GET.has_key('take'):
|
||||||
|
# Allow the user to assign the ticket to themselves whilst viewing it.
|
||||||
ticket.assigned_to = request.user
|
ticket.assigned_to = request.user
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
if request.GET.has_key('close') and ticket.status == Ticket.RESOLVED_STATUS:
|
if request.GET.has_key('close') and ticket.status == Ticket.RESOLVED_STATUS:
|
||||||
if not ticket.assigned_to:
|
if not ticket.assigned_to:
|
||||||
owner = 0
|
owner = 0
|
||||||
else:
|
else:
|
||||||
owner = ticket.assigned_to.id
|
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 update_ticket(request, ticket_id)
|
||||||
|
|
||||||
return render_to_response('helpdesk/ticket.html',
|
return render_to_response('helpdesk/ticket.html',
|
||||||
@ -91,6 +118,7 @@ def view_ticket(request, ticket_id):
|
|||||||
}))
|
}))
|
||||||
view_ticket = login_required(view_ticket)
|
view_ticket = login_required(view_ticket)
|
||||||
|
|
||||||
|
|
||||||
def update_ticket(request, ticket_id):
|
def update_ticket(request, ticket_id):
|
||||||
ticket = get_object_or_404(Ticket, id=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)
|
public = request.POST.get('public', False)
|
||||||
owner = int(request.POST.get('owner', None))
|
owner = int(request.POST.get('owner', None))
|
||||||
priority = int(request.POST.get('priority', ticket.priority))
|
priority = int(request.POST.get('priority', ticket.priority))
|
||||||
|
|
||||||
if not owner and ticket.assigned_to:
|
if not owner and ticket.assigned_to:
|
||||||
owner = ticket.assigned_to.id
|
owner = ticket.assigned_to.id
|
||||||
|
|
||||||
f = FollowUp(ticket=ticket, date=datetime.now(), comment=comment, user=request.user)
|
f = FollowUp(ticket=ticket, date=datetime.now(), comment=comment, user=request.user)
|
||||||
if public:
|
f.public = public
|
||||||
f.public = True
|
|
||||||
|
|
||||||
reassigned = False
|
reassigned = False
|
||||||
|
|
||||||
if owner:
|
if owner:
|
||||||
if owner != 0 and (ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to:
|
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)
|
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
|
ticket.assigned_to = new_user
|
||||||
reassigned = True
|
reassigned = True
|
||||||
else:
|
else:
|
||||||
f.title = _('Unassigned')
|
f.title = _('Unassigned')
|
||||||
ticket.assigned_to = None
|
ticket.assigned_to = None
|
||||||
|
|
||||||
if new_status != ticket.status:
|
if new_status != ticket.status:
|
||||||
ticket.status = new_status
|
ticket.status = new_status
|
||||||
ticket.save()
|
ticket.save()
|
||||||
@ -136,20 +165,30 @@ def update_ticket(request, ticket_id):
|
|||||||
f.title = _('Updated')
|
f.title = _('Updated')
|
||||||
|
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
if title != ticket.title:
|
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()
|
c.save()
|
||||||
ticket.title = title
|
ticket.title = title
|
||||||
|
|
||||||
if priority != ticket.priority:
|
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()
|
c.save()
|
||||||
ticket.priority = priority
|
ticket.priority = priority
|
||||||
|
|
||||||
if f.new_status == Ticket.RESOLVED_STATUS:
|
if f.new_status == Ticket.RESOLVED_STATUS:
|
||||||
ticket.resolution = comment
|
ticket.resolution = comment
|
||||||
|
|
||||||
if ticket.submitter_email and ((f.comment != '' and public) or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))):
|
if ticket.submitter_email and ((f.comment != '' and public) or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))):
|
||||||
context = {
|
context = {
|
||||||
'ticket': ticket,
|
'ticket': ticket,
|
||||||
@ -157,16 +196,24 @@ def update_ticket(request, ticket_id):
|
|||||||
'resolution': ticket.resolution,
|
'resolution': ticket.resolution,
|
||||||
'comment': f.comment,
|
'comment': f.comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.new_status == Ticket.RESOLVED_STATUS:
|
if f.new_status == Ticket.RESOLVED_STATUS:
|
||||||
template = 'resolved_submitter'
|
template = 'resolved_submitter'
|
||||||
elif f.new_status == Ticket.CLOSED_STATUS:
|
elif f.new_status == Ticket.CLOSED_STATUS:
|
||||||
template = 'closed_submitter'
|
template = 'closed_submitter'
|
||||||
else:
|
else:
|
||||||
template = 'updated_submitter'
|
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:
|
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.
|
# another user.
|
||||||
if reassigned:
|
if reassigned:
|
||||||
template_staff = 'assigned_owner'
|
template_staff = 'assigned_owner'
|
||||||
@ -176,9 +223,15 @@ def update_ticket(request, ticket_id):
|
|||||||
template_staff = 'closed_owner'
|
template_staff = 'closed_owner'
|
||||||
else:
|
else:
|
||||||
template_staff = 'updated_owner'
|
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 ticket.queue.updated_ticket_cc:
|
||||||
if reassigned:
|
if reassigned:
|
||||||
template_cc = 'assigned_cc'
|
template_cc = 'assigned_cc'
|
||||||
@ -188,22 +241,33 @@ def update_ticket(request, ticket_id):
|
|||||||
template_cc = 'closed_cc'
|
template_cc = 'closed_cc'
|
||||||
else:
|
else:
|
||||||
template_cc = 'updated_cc'
|
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:
|
if request.FILES:
|
||||||
for file in request.FILES.getlist('attachment'):
|
for file in request.FILES.getlist('attachment'):
|
||||||
filename = file['filename'].replace(' ', '_')
|
filename = file['filename'].replace(' ', '_')
|
||||||
a = Attachment(followup=f, filename=filename, mime_type=file['content-type'], size=len(file['content']))
|
a = Attachment(
|
||||||
#a.save_file_file(file['filename'], file['content'])
|
followup=f,
|
||||||
|
filename=filename,
|
||||||
|
mime_type=file['content-type'],
|
||||||
|
size=len(file['content']),
|
||||||
|
)
|
||||||
a.file.save(file['filename'], ContentFile(file['content']))
|
a.file.save(file['filename'], ContentFile(file['content']))
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
return HttpResponseRedirect(ticket.get_absolute_url())
|
return HttpResponseRedirect(ticket.get_absolute_url())
|
||||||
update_ticket = login_required(update_ticket)
|
update_ticket = login_required(update_ticket)
|
||||||
|
|
||||||
|
|
||||||
def ticket_list(request):
|
def ticket_list(request):
|
||||||
tickets = Ticket.objects.select_related()
|
tickets = Ticket.objects.select_related()
|
||||||
context = {}
|
context = {}
|
||||||
@ -229,7 +293,7 @@ def ticket_list(request):
|
|||||||
|
|
||||||
### KEYWORD SEARCHING
|
### KEYWORD SEARCHING
|
||||||
q = request.GET.get('q', None)
|
q = request.GET.get('q', None)
|
||||||
|
|
||||||
if q:
|
if q:
|
||||||
qset = (
|
qset = (
|
||||||
Q(title__icontains=q) |
|
Q(title__icontains=q) |
|
||||||
@ -239,7 +303,7 @@ def ticket_list(request):
|
|||||||
)
|
)
|
||||||
tickets = tickets.filter(qset)
|
tickets = tickets.filter(qset)
|
||||||
context = dict(context, query=q)
|
context = dict(context, query=q)
|
||||||
|
|
||||||
### SORTING
|
### SORTING
|
||||||
sort = request.GET.get('sort', None)
|
sort = request.GET.get('sort', None)
|
||||||
if sort not in ('status', 'assigned_to', 'created', 'title', 'queue', 'priority'):
|
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)
|
ticket_list = login_required(ticket_list)
|
||||||
|
|
||||||
|
|
||||||
def create_ticket(request):
|
def create_ticket(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = TicketForm(request.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['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)]
|
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, {
|
RequestContext(request, {
|
||||||
'form': form,
|
'form': form,
|
||||||
}))
|
}))
|
||||||
create_ticket = login_required(create_ticket)
|
create_ticket = login_required(create_ticket)
|
||||||
|
|
||||||
|
|
||||||
def raw_details(request, type):
|
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',):
|
if not type in ('preset',):
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
if type == 'preset' and request.GET.get('id', False):
|
if type == 'preset' and request.GET.get('id', False):
|
||||||
try:
|
try:
|
||||||
preset = PreSetReply.objects.get(id=request.GET.get('id'))
|
preset = PreSetReply.objects.get(id=request.GET.get('id'))
|
||||||
return HttpResponse(preset.body)
|
return HttpResponse(preset.body)
|
||||||
except PreSetReply.DoesNotExist:
|
except PreSetReply.DoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
raise Http404
|
raise Http404
|
||||||
raw_details = login_required(raw_details)
|
raw_details = login_required(raw_details)
|
||||||
|
|
||||||
|
|
||||||
def hold_ticket(request, ticket_id, unhold=False):
|
def hold_ticket(request, ticket_id, unhold=False):
|
||||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||||
|
|
||||||
@ -299,7 +370,7 @@ def hold_ticket(request, ticket_id, unhold=False):
|
|||||||
else:
|
else:
|
||||||
ticket.on_hold = True
|
ticket.on_hold = True
|
||||||
title = _('Ticket placed on hold')
|
title = _('Ticket placed on hold')
|
||||||
|
|
||||||
f = FollowUp(
|
f = FollowUp(
|
||||||
ticket = ticket,
|
ticket = ticket,
|
||||||
user = request.user,
|
user = request.user,
|
||||||
@ -308,28 +379,32 @@ def hold_ticket(request, ticket_id, unhold=False):
|
|||||||
public = True,
|
public = True,
|
||||||
)
|
)
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
ticket.save()
|
ticket.save()
|
||||||
|
|
||||||
return HttpResponseRedirect(ticket.get_absolute_url())
|
return HttpResponseRedirect(ticket.get_absolute_url())
|
||||||
hold_ticket = login_required(hold_ticket)
|
hold_ticket = login_required(hold_ticket)
|
||||||
|
|
||||||
|
|
||||||
def unhold_ticket(request, ticket_id):
|
def unhold_ticket(request, ticket_id):
|
||||||
return hold_ticket(request, ticket_id, unhold=True)
|
return hold_ticket(request, ticket_id, unhold=True)
|
||||||
unhold_ticket = login_required(unhold_ticket)
|
unhold_ticket = login_required(unhold_ticket)
|
||||||
|
|
||||||
|
|
||||||
def rss_list(request):
|
def rss_list(request):
|
||||||
return render_to_response('helpdesk/rss_list.html',
|
return render_to_response('helpdesk/rss_list.html',
|
||||||
RequestContext(request, {
|
RequestContext(request, {
|
||||||
'queues': Queue.objects.all(),
|
'queues': Queue.objects.all(),
|
||||||
}))
|
}))
|
||||||
rss_list = login_required(rss_list)
|
rss_list = login_required(rss_list)
|
||||||
|
|
||||||
|
|
||||||
def report_index(request):
|
def report_index(request):
|
||||||
return render_to_response('helpdesk/report_index.html',
|
return render_to_response('helpdesk/report_index.html',
|
||||||
RequestContext(request, {}))
|
RequestContext(request, {}))
|
||||||
report_index = login_required(report_index)
|
report_index = login_required(report_index)
|
||||||
|
|
||||||
|
|
||||||
def run_report(request, report):
|
def run_report(request, report):
|
||||||
priority_sql = []
|
priority_sql = []
|
||||||
priority_columns = []
|
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_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_columns.append("%s" % p[1]._proxy____unicode_cast())
|
||||||
priority_sql = ", ".join(priority_sql)
|
priority_sql = ", ".join(priority_sql)
|
||||||
|
|
||||||
status_sql = []
|
status_sql = []
|
||||||
status_columns = []
|
status_columns = []
|
||||||
for s in Ticket.STATUS_CHOICES:
|
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_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_columns.append("%s" % s[1]._proxy____unicode_cast())
|
||||||
status_sql = ", ".join(status_sql)
|
status_sql = ", ".join(status_sql)
|
||||||
|
|
||||||
queue_sql = []
|
queue_sql = []
|
||||||
queue_columns = []
|
queue_columns = []
|
||||||
for q in Queue.objects.all():
|
for q in Queue.objects.all():
|
||||||
@ -368,11 +443,11 @@ def run_report(request, report):
|
|||||||
'Dec',
|
'Dec',
|
||||||
)
|
)
|
||||||
month_columns = []
|
month_columns = []
|
||||||
|
|
||||||
first_ticket = Ticket.objects.all().order_by('created')[0]
|
first_ticket = Ticket.objects.all().order_by('created')[0]
|
||||||
first_month = first_ticket.created.month
|
first_month = first_ticket.created.month
|
||||||
first_year = first_ticket.created.year
|
first_year = first_ticket.created.year
|
||||||
|
|
||||||
last_ticket = Ticket.objects.all().order_by('-created')[0]
|
last_ticket = Ticket.objects.all().order_by('-created')[0]
|
||||||
last_month = last_ticket.created.month
|
last_month = last_ticket.created.month
|
||||||
last_year = last_ticket.created.year
|
last_year = last_ticket.created.year
|
||||||
@ -420,33 +495,32 @@ def run_report(request, report):
|
|||||||
if report == 'userpriority':
|
if report == 'userpriority':
|
||||||
sql = user_base_sql % priority_sql
|
sql = user_base_sql % priority_sql
|
||||||
columns = ['username'] + priority_columns
|
columns = ['username'] + priority_columns
|
||||||
|
|
||||||
elif report == 'userqueue':
|
elif report == 'userqueue':
|
||||||
sql = user_base_sql % queue_sql
|
sql = user_base_sql % queue_sql
|
||||||
columns = ['username'] + queue_columns
|
columns = ['username'] + queue_columns
|
||||||
|
|
||||||
elif report == 'userstatus':
|
elif report == 'userstatus':
|
||||||
sql = user_base_sql % status_sql
|
sql = user_base_sql % status_sql
|
||||||
columns = ['username'] + status_columns
|
columns = ['username'] + status_columns
|
||||||
|
|
||||||
elif report == 'usermonth':
|
elif report == 'usermonth':
|
||||||
sql = user_base_sql % month_sql
|
sql = user_base_sql % month_sql
|
||||||
columns = ['username'] + month_columns
|
columns = ['username'] + month_columns
|
||||||
|
|
||||||
elif report == 'queuepriority':
|
elif report == 'queuepriority':
|
||||||
sql = queue_base_sql % priority_sql
|
sql = queue_base_sql % priority_sql
|
||||||
columns = ['queue'] + priority_columns
|
columns = ['queue'] + priority_columns
|
||||||
|
|
||||||
elif report == 'queuestatus':
|
elif report == 'queuestatus':
|
||||||
sql = queue_base_sql % status_sql
|
sql = queue_base_sql % status_sql
|
||||||
columns = ['queue'] + status_columns
|
columns = ['queue'] + status_columns
|
||||||
|
|
||||||
elif report == 'queuemonth':
|
elif report == 'queuemonth':
|
||||||
sql = queue_base_sql % month_sql
|
sql = queue_base_sql % month_sql
|
||||||
columns = ['queue'] + month_columns
|
columns = ['queue'] + month_columns
|
||||||
|
|
||||||
|
|
||||||
from django.db import connection
|
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
report_output = query_to_dict(cursor.fetchall(), cursor.description)
|
report_output = query_to_dict(cursor.fetchall(), cursor.description)
|
||||||
@ -465,7 +539,7 @@ def run_report(request, report):
|
|||||||
chart_url = bar_chart([columns] + data)
|
chart_url = bar_chart([columns] + data)
|
||||||
else:
|
else:
|
||||||
chart_url = ''
|
chart_url = ''
|
||||||
|
|
||||||
return render_to_response('helpdesk/report_output.html',
|
return render_to_response('helpdesk/report_output.html',
|
||||||
RequestContext(request, {
|
RequestContext(request, {
|
||||||
'headings': columns,
|
'headings': columns,
|
||||||
@ -474,3 +548,4 @@ def run_report(request, report):
|
|||||||
'chart': chart_url,
|
'chart': chart_url,
|
||||||
}))
|
}))
|
||||||
run_report = login_required(run_report)
|
run_report = login_required(run_report)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user