Initial general clean-up of stuff

This commit is contained in:
Alex Barcelo 2016-10-21 17:14:12 +02:00
parent 5e340338b4
commit 24d88be8d9
27 changed files with 827 additions and 710 deletions

View File

@ -5,10 +5,14 @@ from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem
from helpdesk.models import TicketChange, Attachment, IgnoreEmail from helpdesk.models import TicketChange, Attachment, IgnoreEmail
from helpdesk.models import CustomField from helpdesk.models import CustomField
@admin.register(Queue)
class QueueAdmin(admin.ModelAdmin): class QueueAdmin(admin.ModelAdmin):
list_display = ('title', 'slug', 'email_address', 'locale') list_display = ('title', 'slug', 'email_address', 'locale')
prepopulated_fields = {"slug": ("title",)} prepopulated_fields = {"slug": ("title",)}
@admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin): class TicketAdmin(admin.ModelAdmin):
list_display = ('title', 'status', 'assigned_to', 'queue', 'hidden_submitter_email',) list_display = ('title', 'status', 'assigned_to', 'queue', 'hidden_submitter_email',)
date_hierarchy = 'created' date_hierarchy = 'created'
@ -24,34 +28,38 @@ class TicketAdmin(admin.ModelAdmin):
return ticket.submitter_email return ticket.submitter_email
hidden_submitter_email.short_description = _('Submitter E-Mail') hidden_submitter_email.short_description = _('Submitter E-Mail')
class TicketChangeInline(admin.StackedInline): class TicketChangeInline(admin.StackedInline):
model = TicketChange model = TicketChange
class AttachmentInline(admin.StackedInline): class AttachmentInline(admin.StackedInline):
model = Attachment model = Attachment
@admin.register(FollowUp)
class FollowUpAdmin(admin.ModelAdmin): class FollowUpAdmin(admin.ModelAdmin):
inlines = [TicketChangeInline, AttachmentInline] inlines = [TicketChangeInline, AttachmentInline]
@admin.register(KBItem)
class KBItemAdmin(admin.ModelAdmin): class KBItemAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'last_updated',) list_display = ('category', 'title', 'last_updated',)
list_display_links = ('title',) list_display_links = ('title',)
@admin.register(CustomField)
class CustomFieldAdmin(admin.ModelAdmin): class CustomFieldAdmin(admin.ModelAdmin):
list_display = ('name', 'label', 'data_type') list_display = ('name', 'label', 'data_type')
@admin.register(EmailTemplate)
class EmailTemplateAdmin(admin.ModelAdmin): class EmailTemplateAdmin(admin.ModelAdmin):
list_display = ('template_name', 'heading', 'locale') list_display = ('template_name', 'heading', 'locale')
list_filter = ('locale', ) list_filter = ('locale', )
admin.site.register(Ticket, TicketAdmin)
admin.site.register(Queue, QueueAdmin)
admin.site.register(FollowUp, FollowUpAdmin)
admin.site.register(PreSetReply) admin.site.register(PreSetReply)
admin.site.register(EscalationExclusion) admin.site.register(EscalationExclusion)
admin.site.register(EmailTemplate, EmailTemplateAdmin)
admin.site.register(KBCategory) admin.site.register(KBCategory)
admin.site.register(KBItem, KBItemAdmin)
admin.site.register(IgnoreEmail) admin.site.register(IgnoreEmail)
admin.site.register(CustomField, CustomFieldAdmin)

View File

@ -55,7 +55,7 @@ Usage example::
""" """
import os, sys import os
from urllib import urlencode from urllib import urlencode
import socket import socket
@ -104,9 +104,13 @@ else:
class AkismetError(Exception): class AkismetError(Exception):
"""Base class for all akismet exceptions.""" """Base class for all akismet exceptions."""
pass
class APIKeyError(AkismetError): class APIKeyError(AkismetError):
"""Invalid API key.""" """Invalid API key."""
pass
class Akismet(object): class Akismet(object):
"""A class for working with the akismet API""" """A class for working with the akismet API"""
@ -120,7 +124,6 @@ class Akismet(object):
self.user_agent = user_agent % (agent, __version__) self.user_agent = user_agent % (agent, __version__)
self.setAPIKey(key, blog_url) self.setAPIKey(key, blog_url)
def _getURL(self): def _getURL(self):
""" """
Fetch the url to make requests to. Fetch the url to make requests to.
@ -128,8 +131,7 @@ class Akismet(object):
This comprises of api key plus the baseurl. This comprises of api key plus the baseurl.
""" """
return 'http://%s.%s' % (self.key, self.baseurl) return 'http://%s.%s' % (self.key, self.baseurl)
def _safeRequest(self, url, data, headers): def _safeRequest(self, url, data, headers):
try: try:
resp = _fetch_url(url, data, headers) resp = _fetch_url(url, data, headers)
@ -137,7 +139,6 @@ class Akismet(object):
raise AkismetError(str(e)) raise AkismetError(str(e))
return resp return resp
def setAPIKey(self, key=None, blog_url=None): def setAPIKey(self, key=None, blog_url=None):
""" """
Set the wordpress API key for all transactions. Set the wordpress API key for all transactions.
@ -151,7 +152,7 @@ class Akismet(object):
""" """
if key is None and isfile('apikey.txt'): if key is None and isfile('apikey.txt'):
the_file = [l.strip() for l in open('apikey.txt').readlines() the_file = [l.strip() for l in open('apikey.txt').readlines()
if l.strip() and not l.strip().startswith('#')] if l.strip() and not l.strip().startswith('#')]
try: try:
self.key = the_file[0] self.key = the_file[0]
self.blog_url = the_file[1] self.blog_url = the_file[1]
@ -161,7 +162,6 @@ class Akismet(object):
self.key = key self.key = key
self.blog_url = blog_url self.blog_url = blog_url
def verify_key(self): def verify_key(self):
""" """
This equates to the ``verify-key`` call against the akismet API. This equates to the ``verify-key`` call against the akismet API.
@ -179,12 +179,12 @@ class Akismet(object):
""" """
if self.key is None: if self.key is None:
raise APIKeyError("Your have not set an API key.") raise APIKeyError("Your have not set an API key.")
data = { 'key': self.key, 'blog': self.blog_url } data = {'key': self.key, 'blog': self.blog_url}
# this function *doesn't* use the key as part of the URL # this function *doesn't* use the key as part of the URL
url = 'http://%sverify-key' % self.baseurl url = 'http://%sverify-key' % self.baseurl
# we *don't* trap the error here # we *don't* trap the error here
# so if akismet is down it will raise an HTTPError or URLError # so if akismet is down it will raise an HTTPError or URLError
headers = {'User-Agent' : self.user_agent} headers = {'User-Agent': self.user_agent}
resp = self._safeRequest(url, urlencode(data), headers) resp = self._safeRequest(url, urlencode(data), headers)
if resp.lower() == 'valid': if resp.lower() == 'valid':
return True return True
@ -226,14 +226,11 @@ class Akismet(object):
data.setdefault('SERVER_ADMIN', os.environ.get('SERVER_ADMIN', '')) data.setdefault('SERVER_ADMIN', os.environ.get('SERVER_ADMIN', ''))
data.setdefault('SERVER_NAME', os.environ.get('SERVER_NAME', '')) data.setdefault('SERVER_NAME', os.environ.get('SERVER_NAME', ''))
data.setdefault('SERVER_PORT', os.environ.get('SERVER_PORT', '')) data.setdefault('SERVER_PORT', os.environ.get('SERVER_PORT', ''))
data.setdefault('SERVER_SIGNATURE', os.environ.get('SERVER_SIGNATURE', data.setdefault('SERVER_SIGNATURE', os.environ.get('SERVER_SIGNATURE', ''))
'')) data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE', ''))
data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE',
''))
data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', '')) data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
data.setdefault('blog', self.blog_url) data.setdefault('blog', self.blog_url)
def comment_check(self, comment, data=None, build_data=True, DEBUG=False): def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
""" """
This is the function that checks comments. This is the function that checks comments.
@ -316,7 +313,7 @@ class Akismet(object):
url = '%scomment-check' % self._getURL() url = '%scomment-check' % self._getURL()
# we *don't* trap the error here # we *don't* trap the error here
# so if akismet is down it will raise an HTTPError or URLError # so if akismet is down it will raise an HTTPError or URLError
headers = {'User-Agent' : self.user_agent} headers = {'User-Agent': self.user_agent}
resp = self._safeRequest(url, urlencode(data), headers) resp = self._safeRequest(url, urlencode(data), headers)
if DEBUG: if DEBUG:
return resp return resp
@ -329,7 +326,6 @@ class Akismet(object):
# NOTE: Happens when you get a 'howdy wilbur' response ! # NOTE: Happens when you get a 'howdy wilbur' response !
raise AkismetError('missing required argument.') raise AkismetError('missing required argument.')
def submit_spam(self, comment, data=None, build_data=True): def submit_spam(self, comment, data=None, build_data=True):
""" """
This function is used to tell akismet that a comment it marked as ham, This function is used to tell akismet that a comment it marked as ham,
@ -347,10 +343,9 @@ class Akismet(object):
url = '%ssubmit-spam' % self._getURL() url = '%ssubmit-spam' % self._getURL()
# we *don't* trap the error here # we *don't* trap the error here
# so if akismet is down it will raise an HTTPError or URLError # so if akismet is down it will raise an HTTPError or URLError
headers = {'User-Agent' : self.user_agent} headers = {'User-Agent': self.user_agent}
self._safeRequest(url, urlencode(data), headers) self._safeRequest(url, urlencode(data), headers)
def submit_ham(self, comment, data=None, build_data=True): def submit_ham(self, comment, data=None, build_data=True):
""" """
This function is used to tell akismet that a comment it marked as spam, This function is used to tell akismet that a comment it marked as spam,
@ -368,5 +363,5 @@ class Akismet(object):
url = '%ssubmit-ham' % self._getURL() url = '%ssubmit-ham' % self._getURL()
# we *don't* trap the error here # we *don't* trap the error here
# so if akismet is down it will raise an HTTPError or URLError # so if akismet is down it will raise an HTTPError or URLError
headers = {'User-Agent' : self.user_agent} headers = {'User-Agent': self.user_agent}
self._safeRequest(url, urlencode(data), headers) self._safeRequest(url, urlencode(data), headers)

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class HelpdeskConfig(AppConfig): class HelpdeskConfig(AppConfig):
name = 'helpdesk' name = 'helpdesk'
verbose_name = "Helpdesk" verbose_name = "Helpdesk"

View File

@ -6,6 +6,8 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
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.core.exceptions import ObjectDoesNotExist
try: try:
from StringIO import StringIO from StringIO import StringIO
except ImportError: except ImportError:
@ -13,23 +15,22 @@ except ImportError:
from django import forms from django import forms
from django.forms import extras from django.forms import extras
from django.core.files.storage import default_storage
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 _
try: from django.contrib.auth import get_user_model
from django.contrib.auth import get_user_model
User = get_user_model()
except ImportError:
from django.contrib.auth.models import User
try: try:
from django.utils import timezone from django.utils import timezone
except ImportError: except ImportError:
from datetime import datetime as timezone from datetime import datetime as timezone
from helpdesk.lib import send_templated_mail, safe_template_context from helpdesk.lib import send_templated_mail, safe_template_context
from helpdesk.models import Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC, CustomField, TicketCustomFieldValue, TicketDependency from helpdesk.models import (Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC,
CustomField, TicketCustomFieldValue, TicketDependency)
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
User = get_user_model()
class CustomFieldMixin(object): class CustomFieldMixin(object):
""" """
Mixin that provides a method to turn CustomFields into an actual field Mixin that provides a method to turn CustomFields into an actual field
@ -52,7 +53,7 @@ class CustomFieldMixin(object):
fieldclass = forms.ChoiceField fieldclass = forms.ChoiceField
choices = field.choices_as_array choices = field.choices_as_array
if field.empty_selection_list: if field.empty_selection_list:
choices.insert(0, ('','---------' ) ) choices.insert(0, ('', '---------'))
instanceargs['choices'] = choices instanceargs['choices'] = choices
elif field.data_type == 'boolean': elif field.data_type == 'boolean':
fieldclass = forms.BooleanField fieldclass = forms.BooleanField
@ -73,6 +74,7 @@ class CustomFieldMixin(object):
self.fields['custom_%s' % field.name] = fieldclass(**instanceargs) self.fields['custom_%s' % field.name] = fieldclass(**instanceargs)
class EditTicketForm(CustomFieldMixin, forms.ModelForm): class EditTicketForm(CustomFieldMixin, forms.ModelForm):
class Meta: class Meta:
model = Ticket model = Ticket
@ -99,7 +101,6 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
self.customfield_to_field(field, instanceargs) self.customfield_to_field(field, instanceargs)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
for field, value in self.cleaned_data.items(): for field, value in self.cleaned_data.items():
@ -108,7 +109,7 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
customfield = CustomField.objects.get(name=field_name) customfield = CustomField.objects.get(name=field_name)
try: try:
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
except: except ObjectDoesNotExist:
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
cfv.value = value cfv.value = value
cfv.save() cfv.save()
@ -117,14 +118,16 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
class EditFollowUpForm(forms.ModelForm): class EditFollowUpForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
"Filter not openned tickets here."
super(EditFollowUpForm, self).__init__(*args, **kwargs)
self.fields["ticket"].queryset = Ticket.objects.filter(status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
class Meta: class Meta:
model = FollowUp model = FollowUp
exclude = ('date', 'user',) exclude = ('date', 'user',)
def __init__(self, *args, **kwargs):
"""Filter not openned tickets here."""
super(EditFollowUpForm, self).__init__(*args, **kwargs)
self.fields["ticket"].queryset = Ticket.objects.filter(status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
class TicketForm(CustomFieldMixin, forms.Form): class TicketForm(CustomFieldMixin, forms.Form):
queue = forms.ChoiceField( queue = forms.ChoiceField(
label=_('Queue'), label=_('Queue'),
@ -158,7 +161,7 @@ class TicketForm(CustomFieldMixin, forms.Form):
required=False, required=False,
label=_('Case owner'), label=_('Case owner'),
help_text=_('If you select an owner other than yourself, they\'ll be ' help_text=_('If you select an owner other than yourself, they\'ll be '
'e-mailed details of this ticket immediately.'), 'e-mailed details of this ticket immediately.'),
) )
priority = forms.ChoiceField( priority = forms.ChoiceField(
@ -166,8 +169,7 @@ class TicketForm(CustomFieldMixin, forms.Form):
required=False, required=False,
initial='3', initial='3',
label=_('Priority'), label=_('Priority'),
help_text=_('Please select a priority carefully. If unsure, leave it ' help_text=_('Please select a priority carefully. If unsure, leave it as \'3\'.'),
'as \'3\'.'),
) )
due_date = forms.DateTimeField( due_date = forms.DateTimeField(
@ -178,9 +180,9 @@ class TicketForm(CustomFieldMixin, forms.Form):
def clean_due_date(self): def clean_due_date(self):
data = self.cleaned_data['due_date'] data = self.cleaned_data['due_date']
#TODO: add Google calendar update hook # TODO: add Google calendar update hook
#if not hasattr(self, 'instance') or self.instance.due_date != new_data: # if not hasattr(self, 'instance') or self.instance.due_date != new_data:
# print "you changed!" # print "you changed!"
return data return data
attachment = forms.FileField( attachment = forms.FileField(
@ -203,7 +205,6 @@ class TicketForm(CustomFieldMixin, forms.Form):
self.customfield_to_field(field, instanceargs) self.customfield_to_field(field, instanceargs)
def save(self, user): def save(self, user):
""" """
Writes and returns a Ticket() object Writes and returns a Ticket() object
@ -211,15 +212,15 @@ class TicketForm(CustomFieldMixin, forms.Form):
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 = timezone.now(), created=timezone.now(),
status = Ticket.OPEN_STATUS, status=Ticket.OPEN_STATUS,
queue = q, queue=q,
description = self.cleaned_data['body'], description=self.cleaned_data['body'],
priority = self.cleaned_data['priority'], priority=self.cleaned_data['priority'],
due_date = self.cleaned_data['due_date'], due_date=self.cleaned_data['due_date'],
) )
if self.cleaned_data['assigned_to']: if self.cleaned_data['assigned_to']:
try: try:
@ -234,16 +235,16 @@ class TicketForm(CustomFieldMixin, forms.Form):
field_name = field.replace('custom_', '', 1) field_name = field.replace('custom_', '', 1)
customfield = CustomField.objects.get(name=field_name) customfield = CustomField.objects.get(name=field_name)
cfv = TicketCustomFieldValue(ticket=t, cfv = TicketCustomFieldValue(ticket=t,
field=customfield, field=customfield,
value=value) value=value)
cfv.save() cfv.save()
f = FollowUp( ticket = t, f = FollowUp(ticket=t,
title = _('Ticket Opened'), title=_('Ticket Opened'),
date = timezone.now(), date=timezone.now(),
public = True, public=True,
comment = self.cleaned_data['body'], comment=self.cleaned_data['body'],
user = user, user=user,
) )
if self.cleaned_data['assigned_to']: if self.cleaned_data['assigned_to']:
f.title = _('Ticket Opened & Assigned to %(name)s') % { f.title = _('Ticket Opened & Assigned to %(name)s') % {
@ -290,7 +291,11 @@ class TicketForm(CustomFieldMixin, forms.Form):
) )
messages_sent_to.append(t.submitter_email) messages_sent_to.append(t.submitter_email)
if t.assigned_to and t.assigned_to != user and t.assigned_to.usersettings.settings.get('email_on_ticket_assign', False) and t.assigned_to.email and t.assigned_to.email not in messages_sent_to: if t.assigned_to and \
t.assigned_to != user and \
t.assigned_to.usersettings.settings.get('email_on_ticket_assign', False) and \
t.assigned_to.email and \
t.assigned_to.email not in messages_sent_to:
send_templated_mail( send_templated_mail(
'assigned_owner', 'assigned_owner',
context, context,
@ -312,7 +317,9 @@ class TicketForm(CustomFieldMixin, forms.Form):
) )
messages_sent_to.append(q.new_ticket_cc) messages_sent_to.append(q.new_ticket_cc)
if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc and q.updated_ticket_cc not in messages_sent_to: if q.updated_ticket_cc and \
q.updated_ticket_cc != q.new_ticket_cc and \
q.updated_ticket_cc not in messages_sent_to:
send_templated_mail( send_templated_mail(
'newticket_cc', 'newticket_cc',
context, context,
@ -350,7 +357,7 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
label=_('Description of your issue'), label=_('Description of your issue'),
required=True, required=True,
help_text=_('Please be as descriptive as possible, including any ' help_text=_('Please be as descriptive as possible, including any '
'details we may need to address your query.'), 'details we may need to address your query.'),
) )
priority = forms.ChoiceField( priority = forms.ChoiceField(
@ -396,14 +403,14 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
q = Queue.objects.get(id=int(self.cleaned_data['queue'])) q = Queue.objects.get(id=int(self.cleaned_data['queue']))
t = Ticket( t = Ticket(
title = self.cleaned_data['title'], title=self.cleaned_data['title'],
submitter_email = self.cleaned_data['submitter_email'], submitter_email=self.cleaned_data['submitter_email'],
created = timezone.now(), created=timezone.now(),
status = Ticket.OPEN_STATUS, status=Ticket.OPEN_STATUS,
queue = q, queue=q,
description = self.cleaned_data['body'], description=self.cleaned_data['body'],
priority = self.cleaned_data['priority'], priority=self.cleaned_data['priority'],
due_date = self.cleaned_data['due_date'], due_date=self.cleaned_data['due_date'],
) )
if q.default_owner and not t.assigned_to: if q.default_owner and not t.assigned_to:
@ -416,16 +423,16 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
field_name = field.replace('custom_', '', 1) field_name = field.replace('custom_', '', 1)
customfield = CustomField.objects.get(name=field_name) customfield = CustomField.objects.get(name=field_name)
cfv = TicketCustomFieldValue(ticket=t, cfv = TicketCustomFieldValue(ticket=t,
field=customfield, field=customfield,
value=value) value=value)
cfv.save() cfv.save()
f = FollowUp( f = FollowUp(
ticket = t, ticket=t,
title = _('Ticket Opened Via Web'), title=_('Ticket Opened Via Web'),
date = timezone.now(), date=timezone.now(),
public = True, public=True,
comment = self.cleaned_data['body'], comment=self.cleaned_data['body'],
) )
f.save() f.save()
@ -463,7 +470,10 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
) )
messages_sent_to.append(t.submitter_email) messages_sent_to.append(t.submitter_email)
if t.assigned_to and t.assigned_to.usersettings.settings.get('email_on_ticket_assign', False) and t.assigned_to.email and t.assigned_to.email not in messages_sent_to: if t.assigned_to and \
t.assigned_to.usersettings.settings.get('email_on_ticket_assign', False) and \
t.assigned_to.email and \
t.assigned_to.email not in messages_sent_to:
send_templated_mail( send_templated_mail(
'assigned_owner', 'assigned_owner',
context, context,
@ -485,7 +495,9 @@ class PublicTicketForm(CustomFieldMixin, forms.Form):
) )
messages_sent_to.append(q.new_ticket_cc) messages_sent_to.append(q.new_ticket_cc)
if q.updated_ticket_cc and q.updated_ticket_cc != q.new_ticket_cc and q.updated_ticket_cc not in messages_sent_to: if q.updated_ticket_cc and \
q.updated_ticket_cc != q.new_ticket_cc and \
q.updated_ticket_cc not in messages_sent_to:
send_templated_mail( send_templated_mail(
'newticket_cc', 'newticket_cc',
context, context,
@ -537,12 +549,18 @@ class UserSettingsForm(forms.Form):
required=False, required=False,
) )
class EmailIgnoreForm(forms.ModelForm): class EmailIgnoreForm(forms.ModelForm):
class Meta: class Meta:
model = IgnoreEmail model = IgnoreEmail
exclude = [] exclude = []
class TicketCCForm(forms.ModelForm): class TicketCCForm(forms.ModelForm):
class Meta:
model = TicketCC
exclude = ('ticket',)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TicketCCForm, self).__init__(*args, **kwargs) super(TicketCCForm, self).__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
@ -550,9 +568,7 @@ class TicketCCForm(forms.ModelForm):
else: else:
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
self.fields['user'].queryset = users self.fields['user'].queryset = users
class Meta:
model = TicketCC
exclude = ('ticket',)
class TicketDependencyForm(forms.ModelForm): class TicketDependencyForm(forms.ModelForm):
class Meta: class Meta:

View File

@ -6,7 +6,7 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
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') import logging
try: try:
from base64 import urlsafe_b64encode as b64encode from base64 import urlsafe_b64encode as b64encode
@ -17,13 +17,20 @@ try:
except ImportError: except ImportError:
from base64 import decodestring as b64decode from base64 import decodestring as b64decode
import logging
logger = logging.getLogger('helpdesk')
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.db.models import Q from django.db.models import Q
from django.utils.safestring import mark_safe
def send_templated_mail(template_name, email_context, recipients, sender=None, bcc=None, fail_silently=False, files=None): logger = logging.getLogger('helpdesk')
def send_templated_mail(template_name,
email_context,
recipients,
sender=None,
bcc=None,
fail_silently=False,
files=None):
""" """
send_templated_mail() is a warpper around Django's e-mail routines that 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 allows us to easily send multipart (text/plain & text/html) e-mails using
@ -83,16 +90,17 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
try: try:
t = EmailTemplate.objects.get(template_name__iexact=template_name, locale__isnull=True) t = EmailTemplate.objects.get(template_name__iexact=template_name, locale__isnull=True)
except EmailTemplate.DoesNotExist: except EmailTemplate.DoesNotExist:
logger.warning('template "%s" does not exist, no mail sent' % logger.warning('template "%s" does not exist, no mail sent',
template_name) template_name)
return # just ignore if template doesn't exist return # just ignore if template doesn't exist
if not sender: if not sender:
sender = settings.DEFAULT_FROM_EMAIL sender = settings.DEFAULT_FROM_EMAIL
footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt') footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt')
# get_template_from_string was removed in Django 1.8 http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html # get_template_from_string was removed in Django 1.8
# http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
try: try:
from django.template import engines from django.template import engines
template_func = engines['django'].from_string template_func = engines['django'].from_string
@ -105,21 +113,22 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html') email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html')
# keep new lines in html emails
''' keep new lines in html emails '''
from django.utils.safestring import mark_safe
if 'comment' in context: if 'comment' in context:
html_txt = context['comment'] html_txt = context['comment']
html_txt = html_txt.replace('\r\n', '<br>') html_txt = html_txt.replace('\r\n', '<br>')
context['comment'] = mark_safe(html_txt) context['comment'] = mark_safe(html_txt)
# get_template_from_string was removed in Django 1.8 http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html # get_template_from_string was removed in Django 1.8
# http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
html_part = template_func( html_part = template_func(
"{%% extends '%s' %%}{%% block title %%}%s{%% endblock %%}{%% block content %%}%s{%% endblock %%}" % (email_html_base_file, t.heading, t.html) "{%% extends '%s' %%}{%% block title %%}"
).render(context) "%s"
"{%% endblock %%}{%% block content %%}%s{%% endblock %%}" %
(email_html_base_file, t.heading, t.html)).render(context)
# get_template_from_string was removed in Django 1.8 http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html # get_template_from_string was removed in Django 1.8
# http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
subject_part = template_func( subject_part = template_func(
HELPDESK_EMAIL_SUBJECT_TEMPLATE % { HELPDESK_EMAIL_SUBJECT_TEMPLATE % {
"subject": t.subject, "subject": t.subject,
@ -129,13 +138,11 @@ def send_templated_mail(template_name, email_context, recipients, sender=None, b
if recipients.find(','): if recipients.find(','):
recipients = recipients.split(',') recipients = recipients.split(',')
elif type(recipients) != list: elif type(recipients) != list:
recipients = [recipients,] recipients = [recipients, ]
msg = EmailMultiAlternatives( subject_part.replace('\n', '').replace('\r', ''), msg = EmailMultiAlternatives(
text_part, subject_part.replace('\n', '').replace('\r', ''),
sender, text_part, sender, recipients, bcc=bcc)
recipients,
bcc=bcc)
msg.attach_alternative(html_part, "text/html") msg.attach_alternative(html_part, "text/html")
if files: if files:
@ -230,19 +237,19 @@ def safe_template_context(ticket):
} }
queue = ticket.queue queue = ticket.queue
for field in ( 'title', 'slug', 'email_address', 'from_address', 'locale'): for field in ('title', 'slug', 'email_address', 'from_address', 'locale'):
attr = getattr(queue, field, None) attr = getattr(queue, field, None)
if callable(attr): if callable(attr):
context['queue'][field] = attr() context['queue'][field] = attr()
else: else:
context['queue'][field] = attr context['queue'][field] = attr
for field in ( 'title', 'created', 'modified', 'submitter_email', for field in ('title', 'created', 'modified', 'submitter_email',
'status', 'get_status_display', 'on_hold', 'description', 'status', 'get_status_display', 'on_hold', 'description',
'resolution', 'priority', 'get_priority_display', 'resolution', 'priority', 'get_priority_display',
'last_escalation', 'ticket', 'ticket_for_url', 'last_escalation', 'ticket', 'ticket_for_url',
'get_status', 'ticket_url', 'staff_url', '_get_assigned_to' 'get_status', 'ticket_url', 'staff_url', '_get_assigned_to'
): ):
attr = getattr(ticket, field, None) attr = getattr(ticket, field, None)
if callable(attr): if callable(attr):
context['ticket'][field] = '%s' % attr() context['ticket'][field] = '%s' % attr()
@ -278,10 +285,10 @@ def text_is_spam(text, request):
) )
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'): if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
ak.setAPIKey(key = settings.TYPEPAD_ANTISPAM_API_KEY) ak.setAPIKey(key=settings.TYPEPAD_ANTISPAM_API_KEY)
ak.baseurl = 'api.antispam.typepad.com/1.1/' ak.baseurl = 'api.antispam.typepad.com/1.1/'
elif hasattr(settings, 'AKISMET_API_KEY'): elif hasattr(settings, 'AKISMET_API_KEY'):
ak.setAPIKey(key = settings.AKISMET_API_KEY) ak.setAPIKey(key=settings.AKISMET_API_KEY)
else: else:
return False return False

View File

@ -16,7 +16,6 @@ from optparse import make_option
import sys import sys
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q
from helpdesk.models import EscalationExclusion, Queue from helpdesk.models import EscalationExclusion, Queue
@ -47,7 +46,8 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
days = options['days'] days = options['days']
occurrences = options['occurrences'] # optparse should already handle the `or 1`
occurrences = options['occurrences'] or 1
verbose = False verbose = False
queue_slugs = options['queues'] queue_slugs = options['queues']
queues = [] queues = []
@ -55,8 +55,6 @@ class Command(BaseCommand):
if options['escalate-verbosely']: if options['escalate-verbosely']:
verbose = True verbose = True
# this should already be handled by optparse
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.')
@ -116,7 +114,6 @@ def usage():
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. # This script can be run from the command-line or via Django's manage.py.
try: try:
@ -126,7 +123,7 @@ if __name__ == '__main__':
sys.exit(2) sys.exit(2)
days = None days = None
occurrences = None occurrences = 1
verbose = False verbose = False
queue_slugs = None queue_slugs = None
queues = [] queues = []
@ -139,9 +136,8 @@ if __name__ == '__main__':
if o in ('-q', '--queues'): if o in ('-q', '--queues'):
queue_slugs = a queue_slugs = a
if o in ('-o', '--occurrences'): if o in ('-o', '--occurrences'):
occurrences = int(a) occurrences = int(a) or 1
if not occurrences: occurrences = 1
if not (days and occurrences): if not (days and occurrences):
usage() usage()
sys.exit(2) sys.exit(2)

View File

@ -10,17 +10,16 @@ users who don't yet have them.
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
try: from django.contrib.auth import get_user_model
from django.contrib.auth import get_user_model
User = get_user_model()
except ImportError:
from django.contrib.auth.models import User
from helpdesk.models import UserSettings from helpdesk.models import UserSettings
from helpdesk.settings import DEFAULT_USER_SETTINGS from helpdesk.settings import DEFAULT_USER_SETTINGS
User = get_user_model()
class Command(BaseCommand): class Command(BaseCommand):
"create_usersettings command" """create_usersettings command"""
help = _('Check for user without django-helpdesk UserSettings ' help = _('Check for user without django-helpdesk UserSettings '
'and create settings if required. Uses ' 'and create settings if required. Uses '
@ -28,10 +27,7 @@ class Command(BaseCommand):
'suit your situation.') 'suit your situation.')
def handle(self, *args, **options): def handle(self, *args, **options):
"handle command line" """handle command line"""
for u in User.objects.all(): for u in User.objects.all():
try: UserSettings.objects.get_or_create(user=u,
s = UserSettings.objects.get(user=u) defaults={'settings': DEFAULT_USER_SETTINGS})
except UserSettings.DoesNotExist:
s = UserSettings(user=u, settings=DEFAULT_USER_SETTINGS)
s.save()

View File

@ -56,7 +56,7 @@ class Command(BaseCommand):
queue_set = queue_slugs.split(',') queue_set = queue_slugs.split(',')
for queue in queue_set: for queue in queue_set:
try: try:
q = Queue.objects.get(slug__exact=queue) Queue.objects.get(slug__exact=queue)
except Queue.DoesNotExist: except Queue.DoesNotExist:
raise CommandError("Queue %s does not exist." % queue) raise CommandError("Queue %s does not exist." % queue)
queues.append(queue) queues.append(queue)
@ -82,24 +82,23 @@ def escalate_tickets(queues, verbose):
days += 1 days += 1
workdate = workdate + timedelta(days=1) workdate = workdate + timedelta(days=1)
req_last_escl_date = date.today() - timedelta(days=days) req_last_escl_date = date.today() - timedelta(days=days)
if verbose: if verbose:
print("Processing: %s" % q) print("Processing: %s" % q)
for t in q.ticket_set.filter( for t in q.ticket_set.filter(
Q(status=Ticket.OPEN_STATUS) Q(status=Ticket.OPEN_STATUS)
| Q(status=Ticket.REOPENED_STATUS) | Q(status=Ticket.REOPENED_STATUS)
).exclude( ).exclude(
priority=1 priority=1
).filter( ).filter(
Q(on_hold__isnull=True) Q(on_hold__isnull=True)
| Q(on_hold=False) | Q(on_hold=False)
).filter( ).filter(
Q(last_escalation__lte=req_last_escl_date) Q(last_escalation__lte=req_last_escl_date)
| Q(last_escalation__isnull=True, created__lte=req_last_escl_date) | Q(last_escalation__isnull=True, created__lte=req_last_escl_date)
): ):
t.last_escalation = timezone.now() t.last_escalation = timezone.now()
t.priority -= 1 t.priority -= 1
@ -143,8 +142,8 @@ def escalate_tickets(queues, verbose):
) )
f = FollowUp( f = FollowUp(
ticket = t, ticket=t,
title = 'Ticket Escalated', title='Ticket Escalated',
date=timezone.now(), date=timezone.now(),
public=True, public=True,
comment=_('Ticket escalated after %s days' % q.escalate_days), comment=_('Ticket escalated after %s days' % q.escalate_days),
@ -152,10 +151,10 @@ def escalate_tickets(queues, verbose):
f.save() f.save()
tc = TicketChange( tc = TicketChange(
followup = f, followup=f,
field = _('Priority'), field=_('Priority'),
old_value = t.priority + 1, old_value=t.priority + 1,
new_value = t.priority, new_value=t.priority,
) )
tc.save() tc.save()

View File

@ -40,6 +40,14 @@ from helpdesk.lib import send_templated_mail, safe_template_context
from helpdesk.models import Queue, Ticket, FollowUp, Attachment, IgnoreEmail from helpdesk.models import Queue, Ticket, FollowUp, Attachment, IgnoreEmail
STRIPPED_SUBJECT_STRINGS = [
"Re: ",
"Fw: ",
"RE: ",
"FW: ",
"Automatic reply: ",
]
class Command(BaseCommand): class Command(BaseCommand):
def __init__(self): def __init__(self):
BaseCommand.__init__(self) BaseCommand.__init__(self)
@ -52,7 +60,8 @@ class Command(BaseCommand):
help='Hide details about each queue/message as they are processed'), help='Hide details about each queue/message as they are processed'),
) )
help = 'Process Jutda Helpdesk queues and process e-mails via POP3/IMAP as required, feeding them into the helpdesk.' help = 'Process Jutda Helpdesk queues and process e-mails via ' \
'POP3/IMAP as required, feeding them into the helpdesk.'
def handle(self, *args, **options): def handle(self, *args, **options):
quiet = options.get('quiet', False) quiet = options.get('quiet', False)
@ -70,7 +79,6 @@ def process_email(quiet=False):
if not q.email_box_interval: if not q.email_box_interval:
q.email_box_interval = 0 q.email_box_interval = 0
queue_time_delta = timedelta(minutes=q.email_box_interval) queue_time_delta = timedelta(minutes=q.email_box_interval)
if (q.email_box_last_check + queue_time_delta) > timezone.now(): if (q.email_box_last_check + queue_time_delta) > timezone.now():
@ -90,39 +98,47 @@ def process_queue(q, quiet=False):
try: try:
import socks import socks
except ImportError: except ImportError:
raise ImportError("Queue has been configured with proxy settings, but no socks library was installed. Try to install PySocks via pypi.") raise ImportError("Queue has been configured with proxy settings, "
"but no socks library was installed. "
"Try to install PySocks via pypi.")
proxy_type = { proxy_type = {
'socks4': socks.SOCKS4, 'socks4': socks.SOCKS4,
'socks5': socks.SOCKS5, 'socks5': socks.SOCKS5,
}.get(q.socks_proxy_type) }.get(q.socks_proxy_type)
socks.set_default_proxy(proxy_type=proxy_type, addr=q.socks_proxy_host, port=q.socks_proxy_port) socks.set_default_proxy(proxy_type=proxy_type,
addr=q.socks_proxy_host,
port=q.socks_proxy_port)
socket.socket = socks.socksocket socket.socket = socks.socksocket
else: else:
socket.socket = socket._socketobject socket.socket = socket._socketobject
email_box_type = settings.QUEUE_EMAIL_BOX_TYPE if settings.QUEUE_EMAIL_BOX_TYPE else q.email_box_type email_box_type = settings.QUEUE_EMAIL_BOX_TYPE or q.email_box_type
if email_box_type == 'pop3': if email_box_type == 'pop3':
if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL:
if not q.email_box_port: q.email_box_port = 995 if not q.email_box_port:
server = poplib.POP3_SSL(q.email_box_host or settings.QUEUE_EMAIL_BOX_HOST, int(q.email_box_port)) q.email_box_port = 995
server = poplib.POP3_SSL(q.email_box_host or
settings.QUEUE_EMAIL_BOX_HOST,
int(q.email_box_port))
else: else:
if not q.email_box_port: q.email_box_port = 110 if not q.email_box_port:
server = poplib.POP3(q.email_box_host or settings.QUEUE_EMAIL_BOX_HOST, int(q.email_box_port)) q.email_box_port = 110
server = poplib.POP3(q.email_box_host or
settings.QUEUE_EMAIL_BOX_HOST,
int(q.email_box_port))
server.getwelcome() server.getwelcome()
server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER) server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
messagesInfo = server.list()[1] messagesInfo = server.list()[1]
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 = ticket_from_message(message=full_message, queue=q, quiet=quiet) ticket = ticket_from_message(message=full_message, queue=q, quiet=quiet)
@ -134,13 +150,22 @@ def process_queue(q, quiet=False):
elif email_box_type == 'imap': elif email_box_type == 'imap':
if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL:
if not q.email_box_port: q.email_box_port = 993 if not q.email_box_port:
server = imaplib.IMAP4_SSL(q.email_box_host or settings.QUEUE_EMAIL_BOX_HOST, int(q.email_box_port)) q.email_box_port = 993
server = imaplib.IMAP4_SSL(q.email_box_host or
settings.QUEUE_EMAIL_BOX_HOST,
int(q.email_box_port))
else: else:
if not q.email_box_port: q.email_box_port = 143 if not q.email_box_port:
server = imaplib.IMAP4(q.email_box_host or settings.QUEUE_EMAIL_BOX_HOST, int(q.email_box_port)) q.email_box_port = 143
server = imaplib.IMAP4(q.email_box_host or
settings.QUEUE_EMAIL_BOX_HOST,
int(q.email_box_port))
server.login(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER, q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) server.login(q.email_box_user or
settings.QUEUE_EMAIL_BOX_USER,
q.email_box_pass or
settings.QUEUE_EMAIL_BOX_PASSWORD)
server.select(q.email_box_imap_folder) server.select(q.email_box_imap_folder)
status, data = server.search(None, 'NOT', 'DELETED') status, data = server.search(None, 'NOT', 'DELETED')
@ -160,22 +185,26 @@ def process_queue(q, quiet=False):
def decodeUnknown(charset, string): def decodeUnknown(charset, string):
if not charset: if not charset:
try: try:
return string.decode('utf-8','ignore') return string.decode('utf-8', 'ignore')
except: except:
return string.decode('iso8859-1','ignore') return string.decode('iso8859-1', 'ignore')
return unicode(string, charset) return unicode(string, charset)
def decode_mail_headers(string): def decode_mail_headers(string):
decoded = decode_header(string) decoded = decode_header(string)
return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded]) return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded])
def ticket_from_message(message, queue, quiet): def ticket_from_message(message, queue, quiet):
# '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 = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject))
subject = subject.replace("Re: ", "").replace("Fw: ", "").replace("RE: ", "").replace("FW: ", "").replace("Automatic reply: ", "").strip() for affix in STRIPPED_SUBJECT_STRINGS:
subject = subject.replace(affix, "")
subject = subject.strip()
sender = message.get('from', _('Unknown Sender')) sender = message.get('from', _('Unknown Sender'))
sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender)) sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender))
@ -210,9 +239,10 @@ def ticket_from_message(message, queue, quiet):
if name: if name:
name = collapse_rfc2231_value(name) name = collapse_rfc2231_value(name)
if part.get_content_maintype() == 'text' and name == None: if part.get_content_maintype() == 'text' and name is None:
if part.get_content_subtype() == 'plain': if part.get_content_subtype() == 'plain':
body_plain = EmailReplyParser.parse_reply(decodeUnknown(part.get_content_charset(), part.get_payload(decode=True))) body_plain = EmailReplyParser.parse_reply(
decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)))
else: else:
body_html = part.get_payload(decode=True) body_html = part.get_payload(decode=True)
else: else:
@ -259,7 +289,7 @@ def ticket_from_message(message, queue, quiet):
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 is None:
t = Ticket( t = Ticket(
title=subject, title=subject,
queue=queue, queue=queue,
@ -270,18 +300,18 @@ def ticket_from_message(message, queue, quiet):
) )
t.save() t.save()
new = True new = True
update = '' # update = ''
elif t.status == Ticket.CLOSED_STATUS: elif t.status == Ticket.CLOSED_STATUS:
t.status = Ticket.REOPENED_STATUS t.status = Ticket.REOPENED_STATUS
t.save() t.save()
f = FollowUp( f = FollowUp(
ticket = t, ticket=t,
title = _('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}), title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
date = timezone.now(), date=timezone.now(),
public = True, public=True,
comment = body, comment=body,
) )
if t.status == Ticket.REOPENED_STATUS: if t.status == Ticket.REOPENED_STATUS:
@ -308,7 +338,6 @@ def ticket_from_message(message, queue, quiet):
if not quiet: if not quiet:
print(" - %s" % filename) print(" - %s" % filename)
context = safe_template_context(t) context = safe_template_context(t)
if new: if new:
@ -343,10 +372,10 @@ def ticket_from_message(message, queue, quiet):
else: else:
context.update(comment=f.comment) context.update(comment=f.comment)
if t.status == Ticket.REOPENED_STATUS: # if t.status == Ticket.REOPENED_STATUS:
update = _(' (Reopened)') # update = _(' (Reopened)')
else: # else:
update = _(' (Updated)') # update = _(' (Updated)')
if t.assigned_to: if t.assigned_to:
send_templated_mail( send_templated_mail(

View File

@ -25,7 +25,7 @@ def load_fixture(apps, schema_editor):
def unload_fixture(apps, schema_editor): def unload_fixture(apps, schema_editor):
"Delete all EmailTemplate objects" """Delete all EmailTemplate objects"""
objects = deserialize_fixture() objects = deserialize_fixture()

View File

@ -12,14 +12,10 @@ from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.contrib.auth import get_user_model
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _, ugettext from django.utils.translation import ugettext_lazy as _, ugettext
from django import VERSION
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from helpdesk import settings as helpdesk_settings
try: try:
from django.utils import timezone from django.utils import timezone
except ImportError: except ImportError:
@ -47,7 +43,7 @@ class Queue(models.Model):
max_length=50, max_length=50,
unique=True, unique=True,
help_text=_('This slug is used when building ticket ID\'s. Once set, ' help_text=_('This slug is used when building ticket ID\'s. Once set, '
'try not to change it or e-mailing may get messy.'), 'try not to change it or e-mailing may get messy.'),
) )
email_address = models.EmailField( email_address = models.EmailField(
@ -55,8 +51,8 @@ class Queue(models.Model):
blank=True, blank=True,
null=True, null=True,
help_text=_('All outgoing e-mails for this queue will use this e-mail ' 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. If you use IMAP or POP3, this should be the e-mail '
'address for that mailbox.'), 'address for that mailbox.'),
) )
locale = models.CharField( locale = models.CharField(
@ -64,15 +60,15 @@ class Queue(models.Model):
max_length=10, max_length=10,
blank=True, blank=True,
null=True, null=True,
help_text=_('Locale of this queue. All correspondence in this queue will be in this language.'), help_text=_('Locale of this queue. All correspondence in this '
'queue will be in this language.'),
) )
allow_public_submission = models.BooleanField( allow_public_submission = models.BooleanField(
_('Allow Public Submission?'), _('Allow Public Submission?'),
blank=True, blank=True,
default=False, default=False,
help_text=_('Should this queue be listed on the public submission ' help_text=_('Should this queue be listed on the public submission form?'),
'form?'),
) )
allow_email_submission = models.BooleanField( allow_email_submission = models.BooleanField(
@ -80,7 +76,7 @@ class Queue(models.Model):
blank=True, blank=True,
default=False, default=False,
help_text=_('Do you want to poll the e-mail box below for new ' help_text=_('Do you want to poll the e-mail box below for new '
'tickets?'), 'tickets?'),
) )
escalate_days = models.IntegerField( escalate_days = models.IntegerField(
@ -88,7 +84,7 @@ class Queue(models.Model):
blank=True, blank=True,
null=True, null=True,
help_text=_('For tickets which are not held, how often do you wish to ' help_text=_('For tickets which are not held, how often do you wish to '
'increase their priority? Set to 0 for no escalation.'), 'increase their priority? Set to 0 for no escalation.'),
) )
new_ticket_cc = models.CharField( new_ticket_cc = models.CharField(
@ -97,8 +93,8 @@ class Queue(models.Model):
null=True, null=True,
max_length=200, max_length=200,
help_text=_('If an e-mail address is entered here, then it will ' help_text=_('If an e-mail address is entered here, then it will '
'receive notification of all new tickets created for this queue. ' 'receive notification of all new tickets created for this queue. '
'Enter a comma between multiple e-mail addresses.'), 'Enter a comma between multiple e-mail addresses.'),
) )
updated_ticket_cc = models.CharField( updated_ticket_cc = models.CharField(
@ -107,9 +103,9 @@ class Queue(models.Model):
null=True, null=True,
max_length=200, max_length=200,
help_text=_('If an e-mail address is entered here, then it will ' help_text=_('If an e-mail address is entered here, then it will '
'receive notification of all activity (new tickets, closed ' 'receive notification of all activity (new tickets, closed '
'tickets, updates, reassignments, etc) for this queue. Separate ' 'tickets, updates, reassignments, etc) for this queue. Separate '
'multiple addresses with a comma.'), 'multiple addresses with a comma.'),
) )
email_box_type = models.CharField( email_box_type = models.CharField(
@ -119,7 +115,7 @@ class Queue(models.Model):
blank=True, blank=True,
null=True, null=True,
help_text=_('E-Mail server type for creating tickets automatically ' help_text=_('E-Mail server type for creating tickets automatically '
'from a mailbox - both POP3 and IMAP are supported.'), 'from a mailbox - both POP3 and IMAP are supported.'),
) )
email_box_host = models.CharField( email_box_host = models.CharField(
@ -128,7 +124,7 @@ class Queue(models.Model):
blank=True, blank=True,
null=True, null=True,
help_text=_('Your e-mail server address - either the domain name or ' help_text=_('Your e-mail server address - either the domain name or '
'IP address. May be "localhost".'), 'IP address. May be "localhost".'),
) )
email_box_port = models.IntegerField( email_box_port = models.IntegerField(
@ -136,8 +132,8 @@ class Queue(models.Model):
blank=True, blank=True,
null=True, null=True,
help_text=_('Port number to use for accessing e-mail. Default for ' 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 ' 'POP3 is "110", and for IMAP is "143". This may differ on some '
'servers. Leave it blank to use the defaults.'), 'servers. Leave it blank to use the defaults.'),
) )
email_box_ssl = models.BooleanField( email_box_ssl = models.BooleanField(
@ -145,7 +141,7 @@ class Queue(models.Model):
blank=True, blank=True,
default=False, default=False,
help_text=_('Whether to use SSL for IMAP or POP3 - the default ports ' help_text=_('Whether to use SSL for IMAP or POP3 - the default ports '
'when using SSL are 993 for IMAP and 995 for POP3.'), 'when using SSL are 993 for IMAP and 995 for POP3.'),
) )
email_box_user = models.CharField( email_box_user = models.CharField(
@ -170,9 +166,9 @@ class Queue(models.Model):
blank=True, blank=True,
null=True, null=True,
help_text=_('If using IMAP, what folder do you wish to fetch messages ' help_text=_('If using IMAP, what folder do you wish to fetch messages '
'from? This allows you to use one IMAP account for multiple ' 'from? This allows you to use one IMAP account for multiple '
'queues, by filtering messages on your IMAP server into separate ' 'queues, by filtering messages on your IMAP server into separate '
'folders. Default: INBOX.'), 'folders. Default: INBOX.'),
) )
permission_name = models.CharField( permission_name = models.CharField(
@ -375,7 +371,7 @@ class Ticket(models.Model):
blank=True, blank=True,
null=True, null=True,
help_text=_('The submitter will receive an email for all public ' help_text=_('The submitter will receive an email for all public '
'follow-ups left for this task.'), 'follow-ups left for this task.'),
) )
assigned_to = models.ForeignKey( assigned_to = models.ForeignKey(
@ -396,8 +392,7 @@ class Ticket(models.Model):
_('On Hold'), _('On Hold'),
blank=True, blank=True,
default=False, default=False,
help_text=_('If a ticket is on hold, it will not automatically be ' help_text=_('If a ticket is on hold, it will not automatically be escalated.'),
'escalated.'),
) )
description = models.TextField( description = models.TextField(
@ -433,7 +428,7 @@ class Ticket(models.Model):
null=True, null=True,
editable=False, editable=False,
help_text=_('The date this ticket was last escalated - updated ' help_text=_('The date this ticket was last escalated - updated '
'automatically by management/commands/escalate_tickets.py.'), 'automatically by management/commands/escalate_tickets.py.'),
) )
def _get_assigned_to(self): def _get_assigned_to(self):
@ -453,7 +448,7 @@ class Ticket(models.Model):
""" 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-mail subjects. """ 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):
@ -486,7 +481,8 @@ class Ticket(models.Model):
held_msg = '' held_msg = ''
if self.on_hold: held_msg = _(' - On Hold') if self.on_hold: held_msg = _(' - On Hold')
dep_msg = '' dep_msg = ''
if self.can_be_resolved == False: dep_msg = _(' - Open dependencies') if not self.can_be_resolved:
dep_msg = _(' - Open dependencies')
return u'%s%s%s' % (self.get_status_display(), held_msg, dep_msg) return u'%s%s%s' % (self.get_status_display(), held_msg, dep_msg)
get_status = property(_get_status) get_status = property(_get_status)
@ -534,7 +530,8 @@ class Ticket(models.Model):
False = There are non-resolved dependencies False = There are non-resolved dependencies
""" """
OPEN_STATUSES = (Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS) OPEN_STATUSES = (Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS)
return TicketDependency.objects.filter(ticket=self).filter(depends_on__status__in=OPEN_STATUSES).count() == 0 return TicketDependency.objects.filter(ticket=self).filter(
depends_on__status__in=OPEN_STATUSES).count() == 0
can_be_resolved = property(_can_be_resolved) can_be_resolved = property(_can_be_resolved)
class Meta: class Meta:
@ -547,7 +544,7 @@ class Ticket(models.Model):
return '%s %s' % (self.id, self.title) return '%s %s' % (self.id, self.title)
def get_absolute_url(self): def get_absolute_url(self):
return ('helpdesk_view', (self.id,)) return 'helpdesk_view', (self.id,)
get_absolute_url = models.permalink(get_absolute_url) get_absolute_url = models.permalink(get_absolute_url)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -562,8 +559,8 @@ class Ticket(models.Model):
super(Ticket, self).save(*args, **kwargs) super(Ticket, self).save(*args, **kwargs)
@classmethod @staticmethod
def queue_and_id_from_query(klass, query): def queue_and_id_from_query(query):
# Apply the opposite logic here compared to self._get_ticket_for_url # Apply the opposite logic here compared to self._get_ticket_for_url
# Ensure that queues with '-' in them will work # Ensure that queues with '-' in them will work
parts = query.split('-') parts = query.split('-')
@ -621,7 +618,7 @@ class FollowUp(models.Model):
blank=True, blank=True,
default=False, default=False,
help_text=_('Public tickets are viewable by the submitter and all ' help_text=_('Public tickets are viewable by the submitter and all '
'staff, but non-public tickets can only be seen by staff.'), 'staff, but non-public tickets can only be seen by staff.'),
) )
user = models.ForeignKey( user = models.ForeignKey(
@ -785,33 +782,32 @@ class PreSetReply(models.Model):
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.
""" """
class Meta:
ordering = ['name', ]
verbose_name = _('Pre-set reply')
verbose_name_plural = _('Pre-set replies')
queues = models.ManyToManyField( queues = models.ManyToManyField(
Queue, Queue,
blank=True, blank=True,
help_text=_('Leave blank to allow this reply to be used for all ' 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.'), 'queues, or select those queues you wish to limit this reply to.'),
) )
name = models.CharField( name = models.CharField(
_('Name'), _('Name'),
max_length=100, max_length=100,
help_text=_('Only used to assist users with selecting a reply - not ' help_text=_('Only used to assist users with selecting a reply - not '
'shown to the user.'), 'shown to the user.'),
) )
body = models.TextField( body = models.TextField(
_('Body'), _('Body'),
help_text=_('Context available: {{ ticket }} - ticket object (eg ' help_text=_('Context available: {{ ticket }} - ticket object (eg '
'{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} ' '{{ ticket.title }}); {{ queue }} - The queue; and {{ user }} '
'- the current user.'), '- the current user.'),
) )
class Meta:
ordering = ['name',]
verbose_name = _('Pre-set reply')
verbose_name_plural = _('Pre-set replies')
def __str__(self): def __str__(self):
return '%s' % self.name return '%s' % self.name
@ -831,9 +827,8 @@ class EscalationExclusion(models.Model):
queues = models.ManyToManyField( queues = models.ManyToManyField(
Queue, Queue,
blank=True, blank=True,
help_text=_('Leave blank for this exclusion to be applied to all ' help_text=_('Leave blank for this exclusion to be applied to all queues, '
'queues, or select those queues you wish to exclude with this ' 'or select those queues you wish to exclude with this entry.'),
'entry.'),
) )
name = models.CharField( name = models.CharField(
@ -873,29 +868,28 @@ class EmailTemplate(models.Model):
_('Subject'), _('Subject'),
max_length=100, max_length=100,
help_text=_('This will be prefixed with "[ticket.ticket] ticket.title"' help_text=_('This will be prefixed with "[ticket.ticket] ticket.title"'
'. We recommend something simple such as "(Updated") or "(Closed)"' '. We recommend something simple such as "(Updated") or "(Closed)"'
' - the same context is available as in plain_text, below.'), ' - the same context is available as in plain_text, below.'),
) )
heading = models.CharField( heading = models.CharField(
_('Heading'), _('Heading'),
max_length=100, max_length=100,
help_text=_('In HTML e-mails, this will be the heading at the top of ' 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, ' 'the email - the same context is available as in plain_text, '
'below.'), 'below.'),
) )
plain_text = models.TextField( plain_text = models.TextField(
_('Plain Text'), _('Plain Text'),
help_text=_('The context available to you includes {{ ticket }}, ' help_text=_('The context available to you includes {{ ticket }}, '
'{{ queue }}, and depending on the time of the call: ' '{{ queue }}, and depending on the time of the call: '
'{{ resolution }} or {{ comment }}.'), '{{ resolution }} or {{ comment }}.'),
) )
html = models.TextField( html = models.TextField(
_('HTML'), _('HTML'),
help_text=_('The same context is available here as in plain_text, ' help_text=_('The same context is available here as in plain_text, above.'),
'above.'),
) )
locale = models.CharField( locale = models.CharField(
@ -944,7 +938,7 @@ class KBCategory(models.Model):
verbose_name_plural = _('Knowledge base categories') verbose_name_plural = _('Knowledge base categories')
def get_absolute_url(self): def get_absolute_url(self):
return ('helpdesk_kb_category', (), {'slug': self.slug}) return 'helpdesk_kb_category', (), {'slug': self.slug}
get_absolute_url = models.permalink(get_absolute_url) get_absolute_url = models.permalink(get_absolute_url)
@ -986,8 +980,7 @@ class KBItem(models.Model):
last_updated = models.DateTimeField( last_updated = models.DateTimeField(
_('Last Updated'), _('Last Updated'),
help_text=_('The date on which this question was most recently ' help_text=_('The date on which this question was most recently changed.'),
'changed.'),
blank=True, blank=True,
) )
@ -1012,7 +1005,7 @@ class KBItem(models.Model):
verbose_name_plural = _('Knowledge base items') verbose_name_plural = _('Knowledge base items')
def get_absolute_url(self): def get_absolute_url(self):
return ('helpdesk_kb_item', (self.id,)) return 'helpdesk_kb_item', (self.id,)
get_absolute_url = models.permalink(get_absolute_url) get_absolute_url = models.permalink(get_absolute_url)
@ -1076,7 +1069,8 @@ class UserSettings(models.Model):
settings_pickled = models.TextField( settings_pickled = models.TextField(
_('Settings Dictionary'), _('Settings Dictionary'),
help_text=_('This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.'), help_text=_('This is a base64-encoded representation of a pickled Python dictionary. '
'Do not change this field via the admin.'),
blank=True, blank=True,
null=True, null=True,
) )
@ -1125,15 +1119,7 @@ def create_usersettings(sender, instance, created, **kwargs):
if created: if created:
UserSettings.objects.create(user=instance, settings=DEFAULT_USER_SETTINGS) UserSettings.objects.create(user=instance, settings=DEFAULT_USER_SETTINGS)
try: models.signals.post_save.connect(create_usersettings, sender=settings.AUTH_USER_MODEL)
# Connecting via settings.AUTH_USER_MODEL (string) fails in Django < 1.7. We need the actual model there.
# https://docs.djangoproject.com/en/1.7/topics/auth/customizing/#referencing-the-user-model
if VERSION < (1, 7):
raise ValueError
models.signals.post_save.connect(create_usersettings, sender=settings.AUTH_USER_MODEL)
except:
signal_user = get_user_model()
models.signals.post_save.connect(create_usersettings, sender=signal_user)
@python_2_unicode_compatible @python_2_unicode_compatible
@ -1143,12 +1129,15 @@ class IgnoreEmail(models.Model):
processing IMAP and POP3 mailboxes, eg mails from postmaster or from processing IMAP and POP3 mailboxes, eg mails from postmaster or from
known trouble-makers. known trouble-makers.
""" """
class Meta:
verbose_name = _('Ignored e-mail address')
verbose_name_plural = _('Ignored e-mail addresses')
queues = models.ManyToManyField( queues = models.ManyToManyField(
Queue, Queue,
blank=True, blank=True,
help_text=_('Leave blank for this e-mail to be ignored on all ' help_text=_('Leave blank for this e-mail to be ignored on all queues, '
'queues, or select those queues you wish to ignore this e-mail ' 'or select those queues you wish to ignore this e-mail for.'),
'for.'),
) )
name = models.CharField( name = models.CharField(
@ -1167,16 +1156,15 @@ class IgnoreEmail(models.Model):
_('E-Mail Address'), _('E-Mail Address'),
max_length=150, max_length=150,
help_text=_('Enter a full e-mail address, or portions with ' help_text=_('Enter a full e-mail address, or portions with '
'wildcards, eg *@domain.com or postmaster@*.'), 'wildcards, eg *@domain.com or postmaster@*.'),
) )
keep_in_mailbox = models.BooleanField( keep_in_mailbox = models.BooleanField(
_('Save Emails in Mailbox?'), _('Save Emails in Mailbox?'),
blank=True, blank=True,
default=False, default=False,
help_text=_('Do you want to save emails from this address in the ' help_text=_('Do you want to save emails from this address in the mailbox? '
'mailbox? If this is unticked, emails from this address will ' 'If this is unticked, emails from this address will be deleted.'),
'be deleted.'),
) )
def __str__(self): def __str__(self):
@ -1202,18 +1190,14 @@ class IgnoreEmail(models.Model):
own_parts = self.email_address.split("@") own_parts = self.email_address.split("@")
email_parts = email.split("@") email_parts = email.split("@")
if self.email_address == email \ if self.email_address == email or \
or own_parts[0] == "*" and own_parts[1] == email_parts[1] \ own_parts[0] == "*" and own_parts[1] == email_parts[1] or \
or own_parts[1] == "*" and own_parts[0] == email_parts[0] \ own_parts[1] == "*" and own_parts[0] == email_parts[0] or \
or own_parts[0] == "*" and own_parts[1] == "*": own_parts[0] == "*" and own_parts[1] == "*":
return True return True
else: else:
return False return False
class Meta:
verbose_name = _('Ignored e-mail address')
verbose_name_plural = _('Ignored e-mail addresses')
@python_2_unicode_compatible @python_2_unicode_compatible
class TicketCC(models.Model): class TicketCC(models.Model):
@ -1277,6 +1261,7 @@ class TicketCC(models.Model):
def __str__(self): def __str__(self):
return '%s for %s' % (self.display, self.ticket.title) return '%s for %s' % (self.display, self.ticket.title)
class CustomFieldManager(models.Manager): class CustomFieldManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super(CustomFieldManager, self).get_queryset().order_by('ordering') return super(CustomFieldManager, self).get_queryset().order_by('ordering')
@ -1290,7 +1275,8 @@ class CustomField(models.Model):
name = models.SlugField( name = models.SlugField(
_('Field Name'), _('Field Name'),
help_text=_('As used in the database and behind the scenes. Must be unique and consist of only lowercase letters with no punctuation.'), help_text=_('As used in the database and behind the scenes. '
'Must be unique and consist of only lowercase letters with no punctuation.'),
unique=True, unique=True,
) )
@ -1346,7 +1332,8 @@ class CustomField(models.Model):
empty_selection_list = models.BooleanField( empty_selection_list = models.BooleanField(
_('Add empty first choice to List?'), _('Add empty first choice to List?'),
default=False, default=False,
help_text=_('Only for List: adds an empty first entry to the choices list, which enforces that the user makes an active choice.'), help_text=_('Only for List: adds an empty first entry to the choices list, '
'which enforces that the user makes an active choice.'),
) )
list_values = models.TextField( list_values = models.TextField(
@ -1379,14 +1366,15 @@ class CustomField(models.Model):
staff_only = models.BooleanField( staff_only = models.BooleanField(
_('Staff Only?'), _('Staff Only?'),
help_text=_('If this is ticked, then the public submission form will NOT show this field'), help_text=_('If this is ticked, then the public submission form '
'will NOT show this field'),
default=False, default=False,
) )
objects = CustomFieldManager() objects = CustomFieldManager()
def __str__(self): def __str__(self):
return '%s' % (self.name) return '%s' % self.name
class Meta: class Meta:
verbose_name = _('Custom field') verbose_name = _('Custom field')
@ -1423,6 +1411,11 @@ class TicketDependency(models.Model):
To help enforce this, a helper function `can_be_resolved` on each Ticket instance checks that To help enforce this, a helper function `can_be_resolved` on each Ticket instance checks that
these have all been resolved. these have all been resolved.
""" """
class Meta:
unique_together = (('ticket', 'depends_on'),)
verbose_name = _('Ticket dependency')
verbose_name_plural = _('Ticket dependencies')
ticket = models.ForeignKey( ticket = models.ForeignKey(
Ticket, Ticket,
verbose_name=_('Ticket'), verbose_name=_('Ticket'),
@ -1437,8 +1430,3 @@ class TicketDependency(models.Model):
def __str__(self): def __str__(self):
return '%s / %s' % (self.ticket, self.depends_on) return '%s / %s' % (self.ticket, self.depends_on)
class Meta:
unique_together = (('ticket', 'depends_on'),)
verbose_name = _('Ticket dependency')
verbose_name_plural = _('Ticket dependencies')

View File

@ -11,22 +11,27 @@ try:
except: except:
DEFAULT_USER_SETTINGS = None DEFAULT_USER_SETTINGS = None
if type(DEFAULT_USER_SETTINGS) != type(dict()): if not isinstance(DEFAULT_USER_SETTINGS, dict):
DEFAULT_USER_SETTINGS = { DEFAULT_USER_SETTINGS = {
'use_email_as_submitter': True, 'use_email_as_submitter': True,
'email_on_ticket_assign': True, 'email_on_ticket_assign': True,
'email_on_ticket_change': True, 'email_on_ticket_change': True,
'login_view_ticketlist': True, 'login_view_ticketlist': True,
'email_on_ticket_apichange': True, 'email_on_ticket_apichange': True,
'tickets_per_page': 25 'tickets_per_page': 25
} }
HAS_TAG_SUPPORT = False HAS_TAG_SUPPORT = False
''' generic options - visible on all pages ''' ##########################################
# generic options - visible on all pages #
##########################################
# redirect to login page instead of the default homepage when users visits "/"? # redirect to login page instead of the default homepage when users visits "/"?
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings, 'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT', False) HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
False)
# show knowledgebase links? # show knowledgebase links?
HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True) HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
@ -34,14 +39,20 @@ HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
# show extended navigation by default, to all users, irrespective of staff status? # show extended navigation by default, to all users, irrespective of staff status?
HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False) HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False)
# use public CDNs to serve jquery and other javascript by default? otherwise, use built-in static copy # use public CDNs to serve jquery and other javascript by default?
# otherwise, use built-in static copy
HELPDESK_USE_CDN = getattr(settings, 'HELPDESK_USE_CDN', False) HELPDESK_USE_CDN = getattr(settings, 'HELPDESK_USE_CDN', False)
# show dropdown list of languages that ticket comments can be translated into? # show dropdown list of languages that ticket comments can be translated into?
HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(settings, 'HELPDESK_TRANSLATE_TICKET_COMMENTS', False) HELPDESK_TRANSLATE_TICKET_COMMENTS = getattr(settings,
'HELPDESK_TRANSLATE_TICKET_COMMENTS',
False)
# list of languages to offer. if set to false, all default google translate languages will be shown. # list of languages to offer. if set to false,
HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings, 'HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG', ["en", "de", "fr", "it", "ru"]) # all default google translate languages will be shown.
HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings,
'HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG',
["en", "de", "fr", "it", "ru"])
# show link to 'change password' on 'User Settings' page? # show link to 'change password' on 'User Settings' page?
HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False) HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False)
@ -50,10 +61,15 @@ HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False) HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
# auto-subscribe user to ticket if (s)he responds to a ticket? # auto-subscribe user to ticket if (s)he responds to a ticket?
HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings, 'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE', False) HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
'HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE',
False)
''' options for public pages ''' ############################
# options for public pages #
############################
# show 'view a ticket' section on public page? # show 'view a ticket' section on public page?
HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True) HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True)
@ -61,17 +77,25 @@ HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True) HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
###################################
# options for update_ticket views #
###################################
''' options for update_ticket views ''' # allow non-staff users to interact with tickets?
# allow non-staff users to interact with tickets? this will also change how 'staff_member_required' # this will also change how 'staff_member_required'
# in staff.py will be defined. # in staff.py will be defined.
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False) HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings,
'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE',
False)
# show edit buttons in ticket follow ups. # show edit buttons in ticket follow ups.
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings, 'HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP', True) HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
'HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP',
True)
# show delete buttons in ticket follow ups if user is 'superuser' # show delete buttons in ticket follow ups if user is 'superuser'
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False) HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False)
# make all updates public by default? this will hide the 'is this update public' checkbox # make all updates public by default? this will hide the 'is this update public' checkbox
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False) HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
@ -82,21 +106,28 @@ HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKE
# only show staff users in ticket cc drop-down # only show staff users in ticket cc drop-down
HELPDESK_STAFF_ONLY_TICKET_CC = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False) HELPDESK_STAFF_ONLY_TICKET_CC = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False)
# allow the subject to have a configurable template. # allow the subject to have a configurable template.
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(settings, 'HELPDESK_EMAIL_SUBJECT_TEMPLATE', "{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s") HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
settings, 'HELPDESK_EMAIL_SUBJECT_TEMPLATE',
"{{ ticket.ticket }} {{ ticket.title|safe }} %(subject)s")
# default fallback locale when queue locale not found # default fallback locale when queue locale not found
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en') HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en')
''' options for staff.create_ticket view ''' ########################################
# options for staff.create_ticket view #
########################################
# hide the 'assigned to' / 'Case owner' field from the 'create_ticket' view? # hide the 'assigned to' / 'Case owner' field from the 'create_ticket' view?
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False) HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
#################
# email options #
#################
''' email options '''
# default Queue email submission settings # default Queue email submission settings
QUEUE_EMAIL_BOX_TYPE = getattr(settings, 'QUEUE_EMAIL_BOX_TYPE', None) QUEUE_EMAIL_BOX_TYPE = getattr(settings, 'QUEUE_EMAIL_BOX_TYPE', None)
QUEUE_EMAIL_BOX_SSL = getattr(settings, 'QUEUE_EMAIL_BOX_SSL', None) QUEUE_EMAIL_BOX_SSL = getattr(settings, 'QUEUE_EMAIL_BOX_SSL', None)
@ -104,6 +135,6 @@ QUEUE_EMAIL_BOX_HOST = getattr(settings, 'QUEUE_EMAIL_BOX_HOST', None)
QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None) QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None)
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None) QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None)
# only allow users to access queues that they are members of? # only allow users to access queues that they are members of?
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False) HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)

View File

@ -17,8 +17,9 @@ Assuming 'food' = 'pizza' and 'best_foods' = ['pizza', 'pie', 'cake]:
from django import template from django import template
def in_list(value, arg): def in_list(value, arg):
return value in ( arg or [] ) return value in (arg or [])
register = template.Library() register = template.Library()
register.filter(in_list) register.filter(in_list)

View File

@ -4,17 +4,19 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
templatetags/load_helpdesk_settings.py - returns the settings as defined in templatetags/load_helpdesk_settings.py - returns the settings as defined in
django-helpdesk/helpdesk/settings.py django-helpdesk/helpdesk/settings.py
""" """
from __future__ import print_function
from django.template import Library from django.template import Library
from helpdesk import settings as helpdesk_settings_config from helpdesk import settings as helpdesk_settings_config
def load_helpdesk_settings(request): def load_helpdesk_settings(request):
try: try:
return helpdesk_settings_config return helpdesk_settings_config
except Exception as e: except Exception as e:
import sys import sys
print >> sys.stderr, "'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:" print("'load_helpdesk_settings' template tag (django-helpdesk) crashed with following error:",
print >> sys.stderr, e file=sys.stderr)
print(e, file=sys.stderr)
return '' return ''
register = Library() register = Library()

View File

@ -20,18 +20,6 @@ from django.utils.safestring import mark_safe
from helpdesk.models import Ticket from helpdesk.models import Ticket
class ReverseProxy:
def __init__(self, sequence):
self.sequence = sequence
def __iter__(self):
length = len(self.sequence)
i = length
while i > 0:
i = i - 1
yield self.sequence[i]
def num_to_link(text): def num_to_link(text):
if text == '': if text == '':
return text return text
@ -40,9 +28,7 @@ def num_to_link(text):
for match in re.finditer(r"(?:[^&]|\b|^)#(\d+)\b", text): for match in re.finditer(r"(?:[^&]|\b|^)#(\d+)\b", text):
matches.append(match) matches.append(match)
for match in ReverseProxy(matches): for match in reversed(matches):
start = match.start()
end = match.end()
number = match.groups()[0] number = match.groups()[0]
url = reverse('helpdesk_view', args=[number]) url = reverse('helpdesk_view', args=[number])
try: try:
@ -52,7 +38,8 @@ def num_to_link(text):
if ticket: if ticket:
style = ticket.get_status_display() style = ticket.get_status_display()
text = "%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s" % (text[:match.start()], url, style, match.groups()[0], text[match.end():]) text = "%s <a href='%s' class='ticket_link_status ticket_link_status_%s'>#%s</a>%s" % (
text[:match.start()], url, style, match.groups()[0], text[match.end():])
return mark_safe(text) return mark_safe(text)
register = template.Library() register = template.Library()

View File

@ -12,6 +12,7 @@ templatetags/admin_url.py - Very simple template tag allow linking to the
from django import template from django import template
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
def user_admin_url(action): def user_admin_url(action):
user = get_user_model() user = get_user_model()
try: try:

View File

@ -1,11 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys import sys
try: from django.contrib.auth import get_user_model
from django.contrib.auth import get_user_model
except ImportError: User = get_user_model()
from django.contrib.auth.models import User
else:
User = get_user_model()
def get_staff_user(username='helpdesk.staff', password='password'): def get_staff_user(username='helpdesk.staff', password='password'):

View File

@ -1,9 +1,9 @@
from helpdesk.models import Queue, CustomField, Ticket from helpdesk.models import Queue, Ticket
from django.test import TestCase from django.test import TestCase
from django.core import mail
from django.test.client import Client from django.test.client import Client
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
class PublicActionsTestCase(TestCase): class PublicActionsTestCase(TestCase):
""" """
Tests for public actions: Tests for public actions:
@ -15,13 +15,23 @@ class PublicActionsTestCase(TestCase):
""" """
Create a queue & ticket we can use for later tests. Create a queue & ticket we can use for later tests.
""" """
self.queue = Queue.objects.create(title='Queue 1', slug='q', allow_public_submission=True, new_ticket_cc='new.public@example.com', updated_ticket_cc='update.public@example.com') self.queue = Queue.objects.create(title='Queue 1',
self.ticket = Ticket.objects.create(title='Test Ticket', queue=self.queue, submitter_email='test.submitter@example.com', description='This is a test ticket.') slug='q',
allow_public_submission=True,
new_ticket_cc='new.public@example.com',
updated_ticket_cc='update.public@example.com')
self.ticket = Ticket.objects.create(title='Test Ticket',
queue=self.queue,
submitter_email='test.submitter@example.com',
description='This is a test ticket.')
self.client = Client() self.client = Client()
def test_public_view_ticket(self): def test_public_view_ticket(self):
response = self.client.get('%s?ticket=%s&email=%s' % (reverse('helpdesk_public_view'), self.ticket.ticket_for_url, 'test.submitter@example.com')) response = self.client.get('%s?ticket=%s&email=%s' % (
reverse('helpdesk_public_view'),
self.ticket.ticket_for_url,
'test.submitter@example.com'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html') self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
@ -38,7 +48,10 @@ class PublicActionsTestCase(TestCase):
current_followups = ticket.followup_set.all().count() current_followups = ticket.followup_set.all().count()
response = self.client.get('%s?ticket=%s&email=%s&close' % (reverse('helpdesk_public_view'), ticket.ticket_for_url, 'test.submitter@example.com')) response = self.client.get('%s?ticket=%s&email=%s&close' % (
reverse('helpdesk_public_view'),
ticket.ticket_for_url,
'test.submitter@example.com'))
ticket = Ticket.objects.get(id=self.ticket.id) ticket = Ticket.objects.get(id=self.ticket.id)

View File

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from helpdesk.models import Ticket, Queue from helpdesk.models import Queue
from helpdesk.tests.helpers import get_staff_user, reload_urlconf from helpdesk.tests.helpers import get_staff_user
class TestSavingSharedQuery(TestCase): class TestSavingSharedQuery(TestCase):
def setUp(self): def setUp(self):
@ -15,12 +16,15 @@ class TestSavingSharedQuery(TestCase):
url = reverse('helpdesk_savequery') url = reverse('helpdesk_savequery')
self.client.login(username=get_staff_user().get_username(), self.client.login(username=get_staff_user().get_username(),
password='password') password='password')
response = self.client.post(url, response = self.client.post(
data={'title': 'ticket on my queue', url,
'queue':self.q, data={
'shared':'on', 'title': 'ticket on my queue',
'query_encoded':'KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKGxwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu'}) 'queue': self.q,
'shared': 'on',
'query_encoded':
'KGRwMApWZmlsdGVyaW5nCnAxCihkcDIKVnN0YXR1c19faW4KcDMKKG'
'xwNApJMQphSTIKYUkzCmFzc1Zzb3J0aW5nCnA1ClZjcmVhdGVkCnA2CnMu'
})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue('tickets/?saved_query=1' in response.url) self.assertTrue('tickets/?saved_query=1' in response.url)

View File

@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from helpdesk.models import Ticket, Queue from helpdesk.models import Ticket, Queue
class TestKBDisabled(TestCase): class TestKBDisabled(TestCase):
def setUp(self): def setUp(self):
q = Queue(title='Q1', slug='q1') q = Queue(title='Q1', slug='q1')
@ -14,22 +15,19 @@ class TestKBDisabled(TestCase):
def test_ticket_by_id(self): def test_ticket_by_id(self):
"""Can a ticket be looked up by its ID""" """Can a ticket be looked up by its ID"""
from django.core.urlresolvers import NoReverseMatch
# get the ticket from models # get the ticket from models
t = Ticket.objects.get(id=self.ticket.id) t = Ticket.objects.get(id=self.ticket.id)
self.assertEqual(t.title, self.ticket.title) self.assertEqual(t.title, self.ticket.title)
def test_ticket_by_link(self): def test_ticket_by_link(self):
"""Can a ticket be looked up by its link from (eg) an email""" """Can a ticket be looked up by its link from (eg) an email"""
# Work out the link which would have been inserted into the email # Instead of using the ticket_for_url link,
link = self.ticket.ticket_url # we will exercise 'reverse' to lookup/build the URL
# however instead of using that link, we will exercise 'reverse' # from the ticket info we have
# to lookup/build the URL from the ticket info we have # http://example.com/helpdesk/view/?ticket=q1-1&email=None
# http://example.com/helpdesk/view/?ticket=q1-1&email=None
response = self.client.get(reverse('helpdesk_public_view'), response = self.client.get(reverse('helpdesk_public_view'),
{'ticket': self.ticket.ticket_for_url, {'ticket': self.ticket.ticket_for_url,
'email':self.ticket.submitter_email}) 'email': self.ticket.submitter_email})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_ticket_with_changed_queue(self): def test_ticket_with_changed_queue(self):
@ -40,7 +38,7 @@ class TestKBDisabled(TestCase):
# grab the URL / params which would have been emailed out to submitter. # grab the URL / params which would have been emailed out to submitter.
url = reverse('helpdesk_public_view') url = reverse('helpdesk_public_view')
params = {'ticket': self.ticket.ticket_for_url, params = {'ticket': self.ticket.ticket_for_url,
'email':self.ticket.submitter_email} 'email': self.ticket.submitter_email}
# Pickup the ticket created in setup() and change its queue # Pickup the ticket created in setup() and change its queue
self.ticket.queue = q2 self.ticket.queue = q2
self.ticket.save() self.ticket.save()

View File

@ -14,13 +14,23 @@ class TicketBasicsTestCase(TestCase):
fixtures = ['emailtemplate.json'] fixtures = ['emailtemplate.json']
def setUp(self): def setUp(self):
self.queue_public = Queue.objects.create(title='Queue 1', slug='q1', allow_public_submission=True, new_ticket_cc='new.public@example.com', updated_ticket_cc='update.public@example.com') self.queue_public = Queue.objects.create(
self.queue_private = Queue.objects.create(title='Queue 2', slug='q2', allow_public_submission=False, new_ticket_cc='new.private@example.com', updated_ticket_cc='update.private@example.com') title='Queue 1',
slug='q1',
allow_public_submission=True,
new_ticket_cc='new.public@example.com',
updated_ticket_cc='update.public@example.com')
self.queue_private = Queue.objects.create(
title='Queue 2',
slug='q2',
allow_public_submission=False,
new_ticket_cc='new.private@example.com',
updated_ticket_cc='update.private@example.com')
self.ticket_data = { self.ticket_data = {
'title': 'Test Ticket', 'title': 'Test Ticket',
'description': 'Some Test Ticket', 'description': 'Some Test Ticket',
} }
self.client = Client() self.client = Client()
@ -30,7 +40,6 @@ class TicketBasicsTestCase(TestCase):
ticket = Ticket.objects.create(**ticket_data) ticket = Ticket.objects.create(**ticket_data)
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id) self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
self.assertEqual(email_count, len(mail.outbox)) self.assertEqual(email_count, len(mail.outbox))
def test_create_ticket_public(self): def test_create_ticket_public(self):
email_count = len(mail.outbox) email_count = len(mail.outbox)
@ -49,7 +58,7 @@ class TicketBasicsTestCase(TestCase):
response = self.client.post(reverse('helpdesk_home'), post_data, follow=True) response = self.client.post(reverse('helpdesk_home'), post_data, follow=True)
last_redirect = response.redirect_chain[-1] last_redirect = response.redirect_chain[-1]
last_redirect_url = last_redirect[0] last_redirect_url = last_redirect[0]
last_redirect_status = last_redirect[1] # last_redirect_status = last_redirect[1]
# Ensure we landed on the "View" page. # Ensure we landed on the "View" page.
# Django 1.9 compatible way of testing this # Django 1.9 compatible way of testing this
@ -63,12 +72,12 @@ class TicketBasicsTestCase(TestCase):
def test_create_ticket_private(self): def test_create_ticket_private(self):
email_count = len(mail.outbox) email_count = len(mail.outbox)
post_data = { post_data = {
'title': 'Private ticket test', 'title': 'Private ticket test',
'queue': self.queue_private.id, 'queue': self.queue_private.id,
'submitter_email': 'ticket2.submitter@example.com', 'submitter_email': 'ticket2.submitter@example.com',
'body': 'Test ticket body', 'body': 'Test ticket body',
'priority': 3, 'priority': 3,
} }
response = self.client.post(reverse('helpdesk_home'), post_data) response = self.client.post(reverse('helpdesk_home'), post_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -77,23 +86,34 @@ class TicketBasicsTestCase(TestCase):
def test_create_ticket_customfields(self): def test_create_ticket_customfields(self):
email_count = len(mail.outbox) email_count = len(mail.outbox)
queue_custom = Queue.objects.create(title='Queue 3', slug='q3', allow_public_submission=True, updated_ticket_cc='update.custom@example.com') queue_custom = Queue.objects.create(
custom_field_1 = CustomField.objects.create(name='textfield', label='Text Field', data_type='varchar', max_length=100, ordering=10, required=False, staff_only=False) title='Queue 3',
slug='q3',
allow_public_submission=True,
updated_ticket_cc='update.custom@example.com')
custom_field_1 = CustomField.objects.create(
name='textfield',
label='Text Field',
data_type='varchar',
max_length=100,
ordering=10,
required=False,
staff_only=False)
post_data = { post_data = {
'queue': queue_custom.id, 'queue': queue_custom.id,
'title': 'Ticket with custom text field', 'title': 'Ticket with custom text field',
'submitter_email': 'ticket3.submitter@example.com', 'submitter_email': 'ticket3.submitter@example.com',
'body': 'Test ticket body', 'body': 'Test ticket body',
'priority': 3, 'priority': 3,
'custom_textfield': 'This is my custom text.', 'custom_textfield': 'This is my custom text.',
} }
response = self.client.post(reverse('helpdesk_home'), post_data, follow=True) response = self.client.post(reverse('helpdesk_home'), post_data, follow=True)
custom_field_1.delete() custom_field_1.delete()
last_redirect = response.redirect_chain[-1] last_redirect = response.redirect_chain[-1]
last_redirect_url = last_redirect[0] last_redirect_url = last_redirect[0]
last_redirect_status = last_redirect[1] # last_redirect_status = last_redirect[1]
# Ensure we landed on the "View" page. # Ensure we landed on the "View" page.
# Django 1.9 compatible way of testing this # Django 1.9 compatible way of testing this

View File

@ -7,21 +7,18 @@ 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 import settings from django.conf.urls import url
import django
if django.get_version().startswith("1.3"):
from django.conf.urls.defaults import *
else:
from django.conf.urls import *
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth import views as auth_views
from django.views.generic import TemplateView
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.views import feeds, staff, public, api, kb from helpdesk.views import feeds, staff, public, api, kb
from django.contrib.auth import views as auth_views
from django.views.generic import TemplateView
class DirectTemplateView(TemplateView): class DirectTemplateView(TemplateView):
extra_context = None extra_context = None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(self.__class__, self).get_context_data(**kwargs) context = super(self.__class__, self).get_context_data(**kwargs)
if self.extra_context is not None: if self.extra_context is not None:

View File

@ -11,16 +11,10 @@ The API documentation can be accessed by visiting http://helpdesk/api/help/
through templates/helpdesk/help_api.html. through templates/helpdesk/help_api.html.
""" """
from django import forms
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
try: from django.contrib.auth import get_user_model
from django.contrib.auth import get_user_model
User = get_user_model()
except ImportError:
from django.contrib.auth.models import User
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render from django.shortcuts import render
from django.template import loader, Context
import simplejson import simplejson
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -35,6 +29,8 @@ from helpdesk.models import Ticket, Queue, FollowUp
import warnings import warnings
User = get_user_model()
STATUS_OK = 200 STATUS_OK = 200
STATUS_ERROR = 400 STATUS_ERROR = 400
@ -61,7 +57,9 @@ def api(request, method):
""" """
warnings.warn("django-helpdesk API will be removed in January 2016. See https://github.com/django-helpdesk/django-helpdesk/issues/198 for details.", category=DeprecationWarning) warnings.warn("django-helpdesk API will be removed in January 2016. "
"See https://github.com/django-helpdesk/django-helpdesk/issues/198 for details.",
category=DeprecationWarning)
if method == 'help': if method == 'help':
return render(request, template_name='helpdesk/help_api.html') return render(request, template_name='helpdesk/help_api.html')
@ -114,7 +112,6 @@ 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()]
@ -126,10 +123,11 @@ class API:
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)
@ -141,7 +139,6 @@ class API:
except User.DoesNotExist: except User.DoesNotExist:
return api_return(STATUS_ERROR, "Invalid username provided") return api_return(STATUS_ERROR, "Invalid username provided")
def api_public_delete_ticket(self): def api_public_delete_ticket(self):
if not self.request.POST.get('confirm', False): if not self.request.POST.get('confirm', False):
return api_return(STATUS_ERROR, "No confirmation provided") return api_return(STATUS_ERROR, "No confirmation provided")
@ -155,7 +152,6 @@ class API:
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))
@ -167,7 +163,6 @@ 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))
@ -179,7 +174,6 @@ class API:
return api_return(STATUS_OK) return api_return(STATUS_OK)
def api_public_add_followup(self): def api_public_add_followup(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))
@ -264,7 +258,6 @@ class API:
return api_return(STATUS_OK) return api_return(STATUS_OK)
def api_public_resolve(self): def api_public_resolve(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))
@ -289,7 +282,7 @@ class API:
context = safe_template_context(ticket) context = safe_template_context(ticket)
context['resolution'] = f.comment context['resolution'] = f.comment
subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title) # subject = '%s %s (Resolved)' % (ticket.ticket, ticket.title)
messages_sent_to = [] messages_sent_to = []
@ -324,7 +317,12 @@ class API:
) )
messages_sent_to.append(ticket.queue.updated_ticket_cc) messages_sent_to.append(ticket.queue.updated_ticket_cc)
if ticket.assigned_to and self.request.user != ticket.assigned_to and getattr(ticket.assigned_to.usersettings.settings, 'email_on_ticket_apichange', False) and ticket.assigned_to.email and ticket.assigned_to.email not in messages_sent_to: if ticket.assigned_to and \
self.request.user != ticket.assigned_to and \
getattr(ticket.assigned_to.usersettings.settings,
'email_on_ticket_apichange', False) and \
ticket.assigned_to.email and \
ticket.assigned_to.email not in messages_sent_to:
send_templated_mail( send_templated_mail(
'resolved_resolved', 'resolved_resolved',
context, context,

View File

@ -7,11 +7,7 @@ views/feeds.py - A handful of staff-only RSS feeds to provide ticket details
to feed readers or similar software. to feed readers or similar software.
""" """
try: from django.contrib.auth import get_user_model
from django.contrib.auth import get_user_model
User = get_user_model()
except ImportError:
from django.contrib.auth.models import User
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Q from django.db.models import Q
@ -20,6 +16,8 @@ from django.shortcuts import get_object_or_404
from helpdesk.models import Ticket, FollowUp, Queue from helpdesk.models import Ticket, FollowUp, Queue
User = get_user_model()
class OpenTicketsByUser(Feed): class OpenTicketsByUser(Feed):
title_template = 'helpdesk/rss/ticket_title.html' title_template = 'helpdesk/rss/ticket_title.html'
@ -101,7 +99,7 @@ class UnassignedTickets(Feed):
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( return Ticket.objects.filter(
@ -113,7 +111,6 @@ class UnassignedTickets(Feed):
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.get_username() return item.assigned_to.get_username()
@ -127,7 +124,7 @@ class RecentFollowUps(Feed):
title = _('Helpdesk: Recent Followups') title = _('Helpdesk: Recent Followups')
description = _('Recent FollowUps, such as e-mail replies, comments, attachments and resolutions') description = _('Recent FollowUps, such as e-mail replies, comments, attachments and resolutions')
link = '/tickets/' # reverse('helpdesk_list') link = '/tickets/' # reverse('helpdesk_list')
def items(self): def items(self):
return FollowUp.objects.order_by('-date')[:20] return FollowUp.objects.order_by('-date')[:20]

View File

@ -8,12 +8,8 @@ views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
resolutions to common problems. resolutions to common problems.
""" """
from datetime import datetime
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.template import RequestContext
from django.utils.translation import ugettext as _
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import KBCategory, KBItem from helpdesk.models import KBCategory, KBItem
@ -22,31 +18,28 @@ from helpdesk.models import KBCategory, KBItem
def index(request): def index(request):
category_list = KBCategory.objects.all() category_list = KBCategory.objects.all()
# TODO: It'd be great to have a list of most popular items here. # TODO: It'd be great to have a list of most popular items here.
return render(request, template_name='helpdesk/kb_index.html', return render(request, 'helpdesk/kb_index.html', {
context = { 'kb_categories': category_list,
'kb_categories': category_list, 'helpdesk_settings': helpdesk_settings,
'helpdesk_settings': helpdesk_settings, })
})
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(request, template_name='helpdesk/kb_category.html', return render(request, 'helpdesk/kb_category.html', {
context = { 'category': category,
'category': category, 'items': items,
'items': items, 'helpdesk_settings': helpdesk_settings,
'helpdesk_settings': helpdesk_settings, })
})
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(request, template_name='helpdesk/kb_item.html', return render(request, 'helpdesk/kb_item.html', {
context = { 'item': item,
'item': item, 'helpdesk_settings': helpdesk_settings,
'helpdesk_settings': helpdesk_settings, })
})
def vote(request, item): def vote(request, item):
@ -59,4 +52,3 @@ def vote(request, item):
item.save() item.save()
return HttpResponseRedirect(item.get_absolute_url()) return HttpResponseRedirect(item.get_absolute_url())

View File

@ -6,16 +6,15 @@ django-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 django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, Http404, HttpResponse from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render
from django.template import loader, Context, RequestContext
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.forms import PublicTicketForm from helpdesk.forms import PublicTicketForm
from helpdesk.lib import send_templated_mail, text_is_spam from helpdesk.lib import text_is_spam
from helpdesk.models import Ticket, Queue, UserSettings, KBCategory from helpdesk.models import Ticket, Queue, UserSettings, KBCategory
@ -23,7 +22,9 @@ def homepage(request):
if not request.user.is_authenticated() and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT: if not request.user.is_authenticated() and helpdesk_settings.HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT:
return HttpResponseRedirect(reverse('login')) return HttpResponseRedirect(reverse('login'))
if (request.user.is_staff or (request.user.is_authenticated() and helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)): if request.user.is_staff or \
(request.user.is_authenticated() and
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE):
try: try:
if request.user.usersettings.settings.get('login_view_ticketlist', False): if request.user.usersettings.settings.get('login_view_ticketlist', False):
return HttpResponseRedirect(reverse('helpdesk_list')) return HttpResponseRedirect(reverse('helpdesk_list'))
@ -34,14 +35,15 @@ def homepage(request):
if request.method == 'POST': if request.method == 'POST':
form = PublicTicketForm(request.POST, request.FILES) form = PublicTicketForm(request.POST, request.FILES)
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():
if text_is_spam(form.cleaned_data['body'], request): if text_is_spam(form.cleaned_data['body'], request):
# This submission is spam. Let's not save it. # This submission is spam. Let's not save it.
return render(request, template_name='helpdesk/public_spam.html') return render(request, template_name='helpdesk/public_spam.html')
else: else:
ticket = form.save() ticket = form.save()
return HttpResponseRedirect('%s?ticket=%s&email=%s'% ( return HttpResponseRedirect('%s?ticket=%s&email=%s' % (
reverse('helpdesk_public_view'), reverse('helpdesk_public_view'),
ticket.ticket_for_url, ticket.ticket_for_url,
ticket.submitter_email) ticket.submitter_email)
@ -59,34 +61,36 @@ def homepage(request):
initial_data['submitter_email'] = request.user.email initial_data['submitter_email'] = request.user.email
form = PublicTicketForm(initial=initial_data) form = PublicTicketForm(initial=initial_data)
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)]
knowledgebase_categories = KBCategory.objects.all() knowledgebase_categories = KBCategory.objects.all()
return render(request, 'helpdesk/public_homepage.html', return render(request, 'helpdesk/public_homepage.html', {
{
'form': form, 'form': form,
'helpdesk_settings': helpdesk_settings, 'helpdesk_settings': helpdesk_settings,
'kb_categories': knowledgebase_categories 'kb_categories': knowledgebase_categories
}) })
def view_ticket(request): def view_ticket(request):
ticket_req = request.GET.get('ticket', '') ticket_req = request.GET.get('ticket', '')
ticket = False
email = request.GET.get('email', '') email = request.GET.get('email', '')
error_message = ''
if ticket_req and email: if ticket_req and email:
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req) queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
try: try:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email) ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
except: except ObjectDoesNotExist:
ticket = 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.')
if ticket: return render(request, 'helpdesk/public_view_form.html', {
'ticket': False,
'email': email,
'error_message': error_message,
'helpdesk_settings': helpdesk_settings,
})
else:
if request.user.is_staff: if request.user.is_staff:
redirect_url = reverse('helpdesk_view', args=[ticket_id]) redirect_url = reverse('helpdesk_view', args=[ticket_id])
if 'close' in request.GET: if 'close' in request.GET:
@ -114,25 +118,16 @@ def view_ticket(request):
if helpdesk_settings.HELPDESK_NAVIGATION_ENABLED: if helpdesk_settings.HELPDESK_NAVIGATION_ENABLED:
redirect_url = reverse('helpdesk_view', args=[ticket_id]) redirect_url = reverse('helpdesk_view', args=[ticket_id])
return render(request, 'helpdesk/public_view_ticket.html', return render(request, 'helpdesk/public_view_ticket.html', {
{ 'ticket': ticket,
'ticket': ticket, 'helpdesk_settings': helpdesk_settings,
'helpdesk_settings': helpdesk_settings, 'next': redirect_url,
'next': redirect_url, })
})
return render(request, template_name='helpdesk/public_view_form.html',
context = {
'ticket': ticket,
'email': email,
'error_message': error_message,
'helpdesk_settings': helpdesk_settings,
})
def change_language(request): def change_language(request):
return_to = '' return_to = ''
if 'return_to' in request.GET: if 'return_to' in request.GET:
return_to = request.GET['return_to'] return_to = request.GET['return_to']
return render(request, template_name='helpdesk/public_change_language.html', return render(request, 'helpdesk/public_change_language.html', {'next': return_to})
context = {'next': return_to})

View File

@ -7,19 +7,12 @@ views/staff.py - The bulk of the application - provides most business logic and
renders all staff-facing views. renders all staff-facing views.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.encoding import python_2_unicode_compatible
from datetime import datetime, timedelta from datetime import datetime, timedelta
import sys
from django import VERSION from django import VERSION
from django.conf import settings from django.conf import settings
try: from django.contrib.auth import get_user_model
from django.contrib.auth import get_user_model from django.contrib.auth.decorators import user_passes_test
User = get_user_model()
except ImportError:
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.files.base import ContentFile
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError, PermissionDenied from django.core.exceptions import ValidationError, PermissionDenied
from django.core import paginator from django.core import paginator
@ -27,7 +20,6 @@ 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, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.template import loader, Context, RequestContext
from django.utils.dates import MONTHS_3 from django.utils.dates import MONTHS_3
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.html import escape from django.utils.html import escape
@ -38,19 +30,33 @@ try:
except ImportError: except ImportError:
from datetime import datetime as timezone from datetime import datetime as timezone
from helpdesk.forms import TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm, EditFollowUpForm, TicketDependencyForm from helpdesk.forms import (
from helpdesk.lib import send_templated_mail, query_to_dict, apply_query, safe_template_context TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm,
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch, IgnoreEmail, TicketCC, TicketDependency EditFollowUpForm, TicketDependencyForm
)
from helpdesk.lib import (
send_templated_mail, query_to_dict, apply_query, safe_template_context,
)
from helpdesk.models import (
Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch,
IgnoreEmail, TicketCC, TicketDependency,
)
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
User = get_user_model()
if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE: if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE:
# treat 'normal' users like 'staff' # treat 'normal' users like 'staff'
staff_member_required = user_passes_test(lambda u: u.is_authenticated() and u.is_active) staff_member_required = user_passes_test(
lambda u: u.is_authenticated() and u.is_active)
else: else:
staff_member_required = user_passes_test(lambda u: u.is_authenticated() and u.is_active and u.is_staff) staff_member_required = user_passes_test(
lambda u: u.is_authenticated() and u.is_active and u.is_staff)
superuser_required = user_passes_test(lambda u: u.is_authenticated() and u.is_active and u.is_superuser) superuser_required = user_passes_test(
lambda u: u.is_authenticated() and u.is_active and u.is_superuser)
def _get_user_queues(user): def _get_user_queues(user):
@ -60,7 +66,9 @@ def _get_user_queues(user):
:return: A Python list of Queues :return: A Python list of Queues
""" """
all_queues = Queue.objects.all() all_queues = Queue.objects.all()
limit_queues_by_user = helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION and not user.is_superuser limit_queues_by_user = \
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
and not user.is_superuser
if limit_queues_by_user: if limit_queues_by_user:
id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)] id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)]
return all_queues.filter(pk__in=id_list) return all_queues.filter(pk__in=id_list)
@ -92,13 +100,13 @@ def dashboard(request):
tickets = Ticket.objects.select_related('queue').filter( tickets = Ticket.objects.select_related('queue').filter(
assigned_to=request.user, assigned_to=request.user,
).exclude( ).exclude(
status__in = [Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS], status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS],
) )
# closed & resolved tickets, assigned to current user # closed & resolved tickets, assigned to current user
tickets_closed_resolved = Ticket.objects.select_related('queue').filter( tickets_closed_resolved = Ticket.objects.select_related('queue').filter(
assigned_to=request.user, assigned_to=request.user,
status__in = [Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS]) status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS])
user_queues = _get_user_queues(request.user) user_queues = _get_user_queues(request.user)
@ -117,10 +125,10 @@ def dashboard(request):
submitter_email=email_current_user, submitter_email=email_current_user,
).order_by('status') ).order_by('status')
Tickets = Ticket.objects.filter( tickets_in_queues = Ticket.objects.filter(
queue__in=user_queues, queue__in=user_queues,
) )
basic_ticket_stats = calc_basic_ticket_stats(Tickets) basic_ticket_stats = calc_basic_ticket_stats(tickets_in_queues)
# The following query builds a grid of queues & ticket statuses, # The following query builds a grid of queues & ticket statuses,
# to be displayed to the user. EG: # to be displayed to the user. EG:
@ -153,15 +161,14 @@ def dashboard(request):
dash_tickets = query_to_dict(cursor.fetchall(), cursor.description) dash_tickets = query_to_dict(cursor.fetchall(), cursor.description)
return render(request, 'helpdesk/dashboard.html', return render(request, 'helpdesk/dashboard.html', {
{ 'user_tickets': tickets,
'user_tickets': tickets, 'user_tickets_closed_resolved': tickets_closed_resolved,
'user_tickets_closed_resolved': tickets_closed_resolved, 'unassigned_tickets': unassigned_tickets,
'unassigned_tickets': unassigned_tickets, 'all_tickets_reported_by_current_user': all_tickets_reported_by_current_user,
'all_tickets_reported_by_current_user': all_tickets_reported_by_current_user, 'dash_tickets': dash_tickets,
'dash_tickets': dash_tickets, 'basic_ticket_stats': basic_ticket_stats,
'basic_ticket_stats': basic_ticket_stats, })
})
dashboard = staff_member_required(dashboard) dashboard = staff_member_required(dashboard)
@ -171,38 +178,38 @@ def delete_ticket(request, ticket_id):
raise PermissionDenied() raise PermissionDenied()
if request.method == 'GET': if request.method == 'GET':
return render(request, template_name='helpdesk/delete_ticket.html', return render(request, 'helpdesk/delete_ticket.html', {
context = { 'ticket': ticket,
'ticket': ticket, })
})
else: else:
ticket.delete() ticket.delete()
return HttpResponseRedirect(reverse('helpdesk_home')) return HttpResponseRedirect(reverse('helpdesk_home'))
delete_ticket = staff_member_required(delete_ticket) delete_ticket = staff_member_required(delete_ticket)
def followup_edit(request, ticket_id, followup_id): def followup_edit(request, ticket_id, followup_id):
"Edit followup options with an ability to change the ticket." """Edit followup options with an ability to change the ticket."""
followup = get_object_or_404(FollowUp, id=followup_id) followup = get_object_or_404(FollowUp, id=followup_id)
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): if not _has_access_to_queue(request.user, ticket.queue):
raise PermissionDenied() raise PermissionDenied()
if request.method == 'GET': if request.method == 'GET':
form = EditFollowUpForm(initial= form = EditFollowUpForm(initial={
{'title': escape(followup.title), 'title': escape(followup.title),
'ticket': followup.ticket, 'ticket': followup.ticket,
'comment': escape(followup.comment), 'comment': escape(followup.comment),
'public': followup.public, 'public': followup.public,
'new_status': followup.new_status, 'new_status': followup.new_status,
}) })
ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(request.user, ticket) ticketcc_string, show_subscribe = \
return_ticketccstring_and_show_subscribe(request.user, ticket)
return render(request, template_name='helpdesk/followup_edit.html', return render(request, 'helpdesk/followup_edit.html', {
context = { 'followup': followup,
'followup': followup, 'ticket': ticket,
'ticket': ticket, 'form': form,
'form': form, 'ticketcc_string': ticketcc_string,
'ticketcc_string': ticketcc_string,
}) })
elif request.method == 'POST': elif request.method == 'POST':
form = EditFollowUpForm(request.POST) form = EditFollowUpForm(request.POST)
@ -212,7 +219,7 @@ def followup_edit(request, ticket_id, followup_id):
comment = form.cleaned_data['comment'] comment = form.cleaned_data['comment']
public = form.cleaned_data['public'] public = form.cleaned_data['public']
new_status = form.cleaned_data['new_status'] new_status = form.cleaned_data['new_status']
#will save previous date # will save previous date
old_date = followup.date old_date = followup.date
new_followup = FollowUp(title=title, date=old_date, ticket=_ticket, comment=comment, public=public, new_status=new_status, ) new_followup = FollowUp(title=title, date=old_date, ticket=_ticket, comment=comment, public=public, new_status=new_status, )
# keep old user if one did exist before. # keep old user if one did exist before.
@ -229,8 +236,9 @@ def followup_edit(request, ticket_id, followup_id):
return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket.id])) return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket.id]))
followup_edit = staff_member_required(followup_edit) followup_edit = staff_member_required(followup_edit)
def followup_delete(request, ticket_id, followup_id): def followup_delete(request, ticket_id, followup_id):
''' followup delete for superuser''' """followup delete for superuser"""
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not request.user.is_superuser: if not request.user.is_superuser:
@ -262,8 +270,9 @@ def view_ticket(request, ticket_id):
if 'subscribe' in request.GET: if 'subscribe' in request.GET:
# Allow the user to subscribe him/herself to the ticket whilst viewing it. # Allow the user to subscribe him/herself to the ticket whilst viewing it.
ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(request.user, ticket) ticket_cc, show_subscribe = \
if SHOW_SUBSCRIBE: return_ticketccstring_and_show_subscribe(request.user, ticket)
if show_subscribe:
subscribe_staff_member_to_ticket(ticket, request.user) subscribe_staff_member_to_ticket(ticket, request.user)
return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket.id])) return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket.id]))
@ -292,24 +301,26 @@ def view_ticket(request, ticket_id):
# TODO: shouldn't this template get a form to begin with? # TODO: shouldn't this template get a form to begin with?
form = TicketForm(initial={'due_date':ticket.due_date}) form = TicketForm(initial={'due_date': ticket.due_date})
ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(request.user, ticket) ticketcc_string, show_subscribe = \
return_ticketccstring_and_show_subscribe(request.user, ticket)
return render(request, template_name='helpdesk/ticket.html', return render(request, 'helpdesk/ticket.html', {
context = { 'ticket': ticket,
'ticket': ticket, 'form': form,
'form': form, 'active_users': users,
'active_users': users, 'priorities': Ticket.PRIORITY_CHOICES,
'priorities': Ticket.PRIORITY_CHOICES, 'preset_replies': PreSetReply.objects.filter(
'preset_replies': PreSetReply.objects.filter(Q(queues=ticket.queue) | Q(queues__isnull=True)), Q(queues=ticket.queue) | Q(queues__isnull=True)),
'ticketcc_string': ticketcc_string, 'ticketcc_string': ticketcc_string,
'SHOW_SUBSCRIBE': SHOW_SUBSCRIBE, 'SHOW_SUBSCRIBE': show_subscribe,
}) })
view_ticket = staff_member_required(view_ticket) view_ticket = staff_member_required(view_ticket)
def return_ticketccstring_and_show_subscribe(user, ticket): def return_ticketccstring_and_show_subscribe(user, ticket):
''' used in view_ticket() and followup_edit()''' """used in view_ticket() and followup_edit()"""
# create the ticketcc_string and check whether current user is already # create the ticketcc_string and check whether current user is already
# subscribed # subscribed
username = user.get_username().upper() username = user.get_username().upper()
@ -321,14 +332,14 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
ticketcc_string = '' ticketcc_string = ''
all_ticketcc = ticket.ticketcc_set.all() all_ticketcc = ticket.ticketcc_set.all()
counter_all_ticketcc = len(all_ticketcc) - 1 counter_all_ticketcc = len(all_ticketcc) - 1
SHOW_SUBSCRIBE = True show_subscribe = True
for i, ticketcc in enumerate(all_ticketcc): for i, ticketcc in enumerate(all_ticketcc):
ticketcc_this_entry = str(ticketcc.display) ticketcc_this_entry = str(ticketcc.display)
ticketcc_string = ticketcc_string + ticketcc_this_entry ticketcc_string += ticketcc_this_entry
if i < counter_all_ticketcc: if i < counter_all_ticketcc:
ticketcc_string = ticketcc_string + ', ' ticketcc_string += ', '
if strings_to_check.__contains__(ticketcc_this_entry.upper()): if strings_to_check.__contains__(ticketcc_this_entry.upper()):
SHOW_SUBSCRIBE = False show_subscribe = False
# check whether current user is a submitter or assigned to ticket # check whether current user is a submitter or assigned to ticket
assignedto_username = str(ticket.assigned_to).upper() assignedto_username = str(ticket.assigned_to).upper()
@ -337,24 +348,30 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
strings_to_check.append(assignedto_username) strings_to_check.append(assignedto_username)
strings_to_check.append(submitter_email) strings_to_check.append(submitter_email)
if strings_to_check.__contains__(username) or strings_to_check.__contains__(useremail): if strings_to_check.__contains__(username) or strings_to_check.__contains__(useremail):
SHOW_SUBSCRIBE = False show_subscribe = False
return ticketcc_string, SHOW_SUBSCRIBE return ticketcc_string, show_subscribe
def subscribe_staff_member_to_ticket(ticket, user): def subscribe_staff_member_to_ticket(ticket, user):
''' used in view_ticket() and update_ticket() ''' """used in view_ticket() and update_ticket()"""
ticketcc = TicketCC() ticketcc = TicketCC(
ticketcc.ticket = ticket ticket=ticket,
ticketcc.user = user user=user,
ticketcc.can_view = True can_view=True,
ticketcc.can_update = True can_update=True,
)
ticketcc.save() ticketcc.save()
def update_ticket(request, ticket_id, public=False): def update_ticket(request, ticket_id, public=False):
if not (public or (request.user.is_authenticated() and request.user.is_active and (request.user.is_staff or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE))): if not (public or (
return HttpResponseRedirect('%s?next=%s' % (reverse('login'), request.path)) request.user.is_authenticated() and
request.user.is_active and (
request.user.is_staff or
helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE))):
return HttpResponseRedirect('%s?next=%s' %
(reverse('login'), request.path))
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
@ -384,7 +401,8 @@ def update_ticket(request, ticket_id, public=False):
title == ticket.title, title == ticket.title,
priority == int(ticket.priority), priority == int(ticket.priority),
due_date == ticket.due_date, due_date == ticket.due_date,
(owner == -1) or (not owner and not ticket.assigned_to) or (owner and User.objects.get(id=owner) == ticket.assigned_to), (owner == -1) or (not owner and not ticket.assigned_to) or
(owner and User.objects.get(id=owner) == ticket.assigned_to),
]) ])
if no_changes: if no_changes:
return return_to_ticket(request.user, helpdesk_settings, ticket) return return_to_ticket(request.user, helpdesk_settings, ticket)
@ -398,7 +416,8 @@ def update_ticket(request, ticket_id, public=False):
# then the following line will give us a crash, since django expects {% if %} # then the following line will give us a crash, since django expects {% if %}
# to be closed with an {% endif %} tag. # to be closed with an {% endif %} tag.
# get_template_from_string was removed in Django 1.8 http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html # get_template_from_string was removed in Django 1.8
# http://django.readthedocs.org/en/1.8.x/ref/templates/upgrading.html
try: try:
from django.template import engines from django.template import engines
template_func = engines['django'].from_string template_func = engines['django'].from_string
@ -455,7 +474,7 @@ def update_ticket(request, ticket_id, public=False):
files = [] files = []
if request.FILES: if request.FILES:
import mimetypes, os import mimetypes
for file in request.FILES.getlist('attachment'): for file in request.FILES.getlist('attachment'):
filename = file.name.encode('ascii', 'ignore') filename = file.name.encode('ascii', 'ignore')
a = Attachment( a = Attachment(
@ -472,7 +491,6 @@ def update_ticket(request, ticket_id, public=False):
# settings.MAX_EMAIL_ATTACHMENT_SIZE) are sent via email. # settings.MAX_EMAIL_ATTACHMENT_SIZE) are sent via email.
files.append([a.filename, a.file]) files.append([a.filename, a.file])
if title != ticket.title: if title != ticket.title:
c = TicketChange( c = TicketChange(
followup=f, followup=f,
@ -517,9 +535,9 @@ def update_ticket(request, ticket_id, public=False):
comment=f.comment, comment=f.comment,
) )
if public and (f.comment or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))): if public and (f.comment or (
f.new_status in (Ticket.RESOLVED_STATUS,
Ticket.CLOSED_STATUS))):
if f.new_status == Ticket.RESOLVED_STATUS: if f.new_status == Ticket.RESOLVED_STATUS:
template = 'resolved_' template = 'resolved_'
elif f.new_status == Ticket.CLOSED_STATUS: elif f.new_status == Ticket.CLOSED_STATUS:
@ -554,7 +572,10 @@ def update_ticket(request, ticket_id, public=False):
) )
messages_sent_to.append(cc.email_address) messages_sent_to.append(cc.email_address)
if ticket.assigned_to and request.user != ticket.assigned_to and ticket.assigned_to.email and ticket.assigned_to.email not in messages_sent_to: if ticket.assigned_to and \
request.user != ticket.assigned_to and \
ticket.assigned_to.email and \
ticket.assigned_to.email not in messages_sent_to:
# 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. The actual template varies, depending on what has been # another user. The actual template varies, depending on what has been
# changed. # changed.
@ -567,7 +588,13 @@ def update_ticket(request, ticket_id, public=False):
else: else:
template_staff = 'updated_owner' template_staff = 'updated_owner'
if (not reassigned or ( reassigned and ticket.assigned_to.usersettings.settings.get('email_on_ticket_assign', False))) or (not reassigned and ticket.assigned_to.usersettings.settings.get('email_on_ticket_change', False)): if (not reassigned or
(reassigned and
ticket.assigned_to.usersettings.settings.get(
'email_on_ticket_assign', False))) or \
(not reassigned and
ticket.assigned_to.usersettings.settings.get(
'email_on_ticket_change', False)):
send_templated_mail( send_templated_mail(
template_staff, template_staff,
context, context,
@ -609,7 +636,7 @@ def update_ticket(request, ticket_id, public=False):
def return_to_ticket(user, helpdesk_settings, ticket): def return_to_ticket(user, helpdesk_settings, ticket):
''' Helpder function for update_ticket ''' """Helper function for update_ticket"""
if user.is_staff or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE: if user.is_staff or helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE:
return HttpResponseRedirect(ticket.get_absolute_url()) return HttpResponseRedirect(ticket.get_absolute_url())
@ -638,29 +665,47 @@ def mass_update(request):
if action == 'assign' and t.assigned_to != user: if action == 'assign' and t.assigned_to != user:
t.assigned_to = user t.assigned_to = user
t.save() t.save()
f = FollowUp(ticket=t, date=timezone.now(), title=_('Assigned to %(username)s in bulk update' % {'username': user.get_username()}), public=True, user=request.user) f = FollowUp(ticket=t,
date=timezone.now(),
title=_('Assigned to %(username)s in bulk update' % {
'username': user.get_username()
}),
public=True,
user=request.user)
f.save() f.save()
elif action == 'unassign' and t.assigned_to is not None: elif action == 'unassign' and t.assigned_to is not None:
t.assigned_to = None t.assigned_to = None
t.save() t.save()
f = FollowUp(ticket=t, date=timezone.now(), title=_('Unassigned in bulk update'), public=True, user=request.user) f = FollowUp(ticket=t,
date=timezone.now(),
title=_('Unassigned in bulk update'),
public=True,
user=request.user)
f.save() f.save()
elif action == 'close' and t.status != Ticket.CLOSED_STATUS: elif action == 'close' and t.status != Ticket.CLOSED_STATUS:
t.status = Ticket.CLOSED_STATUS t.status = Ticket.CLOSED_STATUS
t.save() t.save()
f = FollowUp(ticket=t, date=timezone.now(), title=_('Closed in bulk update'), public=False, user=request.user, new_status=Ticket.CLOSED_STATUS) f = FollowUp(ticket=t,
date=timezone.now(),
title=_('Closed in bulk update'),
public=False,
user=request.user,
new_status=Ticket.CLOSED_STATUS)
f.save() f.save()
elif action == 'close_public' and t.status != Ticket.CLOSED_STATUS: elif action == 'close_public' and t.status != Ticket.CLOSED_STATUS:
t.status = Ticket.CLOSED_STATUS t.status = Ticket.CLOSED_STATUS
t.save() t.save()
f = FollowUp(ticket=t, date=timezone.now(), title=_('Closed in bulk update'), public=True, user=request.user, new_status=Ticket.CLOSED_STATUS) f = FollowUp(ticket=t,
date=timezone.now(),
title=_('Closed in bulk update'),
public=True,
user=request.user,
new_status=Ticket.CLOSED_STATUS)
f.save() f.save()
# Send email to Submitter, Owner, Queue CC # Send email to Submitter, Owner, Queue CC
context = safe_template_context(t) context = safe_template_context(t)
context.update( context.update(resolution=t.resolution,
resolution = t.resolution, queue=t.queue)
queue = t.queue,
)
messages_sent_to = [] messages_sent_to = []
@ -685,7 +730,10 @@ def mass_update(request):
) )
messages_sent_to.append(cc.email_address) messages_sent_to.append(cc.email_address)
if t.assigned_to and request.user != t.assigned_to and t.assigned_to.email and t.assigned_to.email not in messages_sent_to: if t.assigned_to and \
request.user != t.assigned_to and \
t.assigned_to.email and \
t.assigned_to.email not in messages_sent_to:
send_templated_mail( send_templated_mail(
'closed_owner', 'closed_owner',
context, context,
@ -695,7 +743,8 @@ def mass_update(request):
) )
messages_sent_to.append(t.assigned_to.email) messages_sent_to.append(t.assigned_to.email)
if t.queue.updated_ticket_cc and t.queue.updated_ticket_cc not in messages_sent_to: if t.queue.updated_ticket_cc and \
t.queue.updated_ticket_cc not in messages_sent_to:
send_templated_mail( send_templated_mail(
'closed_cc', 'closed_cc',
context, context,
@ -710,6 +759,7 @@ def mass_update(request):
return HttpResponseRedirect(reverse('helpdesk_list')) return HttpResponseRedirect(reverse('helpdesk_list'))
mass_update = staff_member_required(mass_update) mass_update = staff_member_required(mass_update)
def ticket_list(request): def ticket_list(request):
context = {} context = {}
@ -745,7 +795,7 @@ def ticket_list(request):
id = None id = None
if id: if id:
filter = {'queue__slug': queue, 'id': id } filter = {'queue__slug': queue, 'id': id}
else: else:
try: try:
query = int(query) query = int(query)
@ -753,7 +803,7 @@ def ticket_list(request):
query = None query = None
if query: if query:
filter = {'id': int(query) } filter = {'id': int(query)}
if filter: if filter:
try: try:
@ -781,13 +831,13 @@ def ticket_list(request):
# Query deserialization failed. (E.g. was a pickled query) # Query deserialization failed. (E.g. was a pickled query)
return HttpResponseRedirect(reverse('helpdesk_list')) return HttpResponseRedirect(reverse('helpdesk_list'))
elif not ( 'queue' in request.GET elif not ('queue' in request.GET
or 'assigned_to' in request.GET or 'assigned_to' in request.GET
or 'status' in request.GET or 'status' in request.GET
or 'q' in request.GET or 'q' in request.GET
or 'sort' in request.GET or 'sort' in request.GET
or 'sortreverse' in request.GET or 'sortreverse' in request.GET
): ):
# Fall-back if no querying is being done, force the list to only # Fall-back if no querying is being done, force the list to only
# show open/reopened/resolved (not closed) cases sorted by creation # show open/reopened/resolved (not closed) cases sorted by creation
@ -830,14 +880,14 @@ def ticket_list(request):
if date_to: if date_to:
query_params['filtering']['created__lte'] = date_to query_params['filtering']['created__lte'] = date_to
### KEYWORD SEARCHING # KEYWORD SEARCHING
q = request.GET.get('q', None) q = request.GET.get('q', None)
if q: if q:
context = dict(context, query=q) context = dict(context, query=q)
query_params['search_string'] = q query_params['search_string'] = 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'):
sort = 'created' sort = 'created'
@ -858,7 +908,9 @@ def ticket_list(request):
} }
ticket_qs = apply_query(tickets, query_params) ticket_qs = apply_query(tickets, query_params)
ticket_paginator = paginator.Paginator(ticket_qs, request.user.usersettings.settings.get('tickets_per_page') or 20) ticket_paginator = paginator.Paginator(
ticket_qs,
request.user.usersettings.settings.get('tickets_per_page') or 20)
try: try:
page = int(request.GET.get('page', '1')) page = int(request.GET.get('page', '1'))
except ValueError: except ValueError:
@ -871,8 +923,13 @@ def ticket_list(request):
search_message = '' search_message = ''
if 'query' in context and settings.DATABASES['default']['ENGINE'].endswith('sqlite'): if 'query' in context and settings.DATABASES['default']['ENGINE'].endswith('sqlite'):
search_message = _('<p><strong>Note:</strong> Your keyword search is case sensitive because of your database. This means the search will <strong>not</strong> be accurate. By switching to a different database system you will gain better searching! For more information, read the <a href="http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching">Django Documentation on string matching in SQLite</a>.') search_message = _(
'<p><strong>Note:</strong> Your keyword search is case sensitive '
'because of your database. This means the search will <strong>not</strong> '
'be accurate. By switching to a different database system you will gain '
'better searching! For more information, read the '
'<a href="http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching">'
'Django Documentation on string matching in SQLite</a>.')
import json import json
from helpdesk.lib import b64encode from helpdesk.lib import b64encode
@ -883,22 +940,20 @@ def ticket_list(request):
querydict = request.GET.copy() querydict = request.GET.copy()
querydict.pop('page', 1) querydict.pop('page', 1)
return render(request, 'helpdesk/ticket_list.html', dict(
return render(request, 'helpdesk/ticket_list.html', context,
dict( query_string=querydict.urlencode(),
context, tickets=tickets,
query_string=querydict.urlencode(), user_choices=User.objects.filter(is_active=True, is_staff=True),
tickets=tickets, queue_choices=user_queues,
user_choices=User.objects.filter(is_active=True,is_staff=True), status_choices=Ticket.STATUS_CHOICES,
queue_choices=user_queues, urlsafe_query=urlsafe_query,
status_choices=Ticket.STATUS_CHOICES, user_saved_queries=user_saved_queries,
urlsafe_query=urlsafe_query, query_params=query_params,
user_saved_queries=user_saved_queries, from_saved_query=from_saved_query,
query_params=query_params, saved_query=saved_query,
from_saved_query=from_saved_query, search_message=search_message,
saved_query=saved_query, ))
search_message=search_message,
))
ticket_list = staff_member_required(ticket_list) ticket_list = staff_member_required(ticket_list)
@ -915,12 +970,10 @@ def edit_ticket(request, ticket_id):
else: else:
form = EditTicketForm(instance=ticket) form = EditTicketForm(instance=ticket)
return render(request, template_name='helpdesk/edit_ticket.html', return render(request, 'helpdesk/edit_ticket.html', {'form': form})
context = {
'form': form,
})
edit_ticket = staff_member_required(edit_ticket) edit_ticket = staff_member_required(edit_ticket)
def create_ticket(request): def create_ticket(request):
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
assignable_users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) assignable_users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
@ -929,8 +982,10 @@ def create_ticket(request):
if request.method == 'POST': if request.method == 'POST':
form = TicketForm(request.POST, request.FILES) form = TicketForm(request.POST, request.FILES)
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()] form.fields['queue'].choices = [('', '--------')] + [
form.fields['assigned_to'].choices = [('', '--------')] + [[u.id, u.get_username()] for u in assignable_users] (q.id, q.title) for q in Queue.objects.all()]
form.fields['assigned_to'].choices = [('', '--------')] + [
(u.id, u.get_username()) for u in assignable_users]
if form.is_valid(): if form.is_valid():
ticket = form.save(user=request.user) ticket = form.save(user=request.user)
if _has_access_to_queue(request.user, ticket.queue): if _has_access_to_queue(request.user, ticket.queue):
@ -945,13 +1000,14 @@ def create_ticket(request):
initial_data['queue'] = request.GET['queue'] initial_data['queue'] = request.GET['queue']
form = TicketForm(initial=initial_data) form = TicketForm(initial=initial_data)
form.fields['queue'].choices = [('', '--------')] + [[q.id, q.title] for q in Queue.objects.all()] form.fields['queue'].choices = [('', '--------')] + [
form.fields['assigned_to'].choices = [('', '--------')] + [[u.id, u.get_username()] for u in assignable_users] (q.id, q.title) for q in Queue.objects.all()]
form.fields['assigned_to'].choices = [('', '--------')] + [
(u.id, u.get_username()) for u in assignable_users]
if helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO: if helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO:
form.fields['assigned_to'].widget = forms.HiddenInput() form.fields['assigned_to'].widget = forms.HiddenInput()
return render(request, template_name='helpdesk/create_ticket.html', return render(request, 'helpdesk/create_ticket.html', {'form': form})
context = {'form': form})
create_ticket = staff_member_required(create_ticket) create_ticket = staff_member_required(create_ticket)
@ -960,7 +1016,7 @@ def raw_details(request, type):
# in the future it needs to be expanded to include other items. All it # in the future it needs to be expanded to include other items. All it
# does is return a plain-text representation of an object. # does is return a plain-text representation of an object.
if not type in ('preset',): if type not in ('preset',):
raise Http404 raise Http404
if type == 'preset' and request.GET.get('id', False): if type == 'preset' and request.GET.get('id', False):
@ -987,11 +1043,11 @@ def hold_ticket(request, ticket_id, unhold=False):
title = _('Ticket placed on hold') title = _('Ticket placed on hold')
f = FollowUp( f = FollowUp(
ticket = ticket, ticket=ticket,
user = request.user, user=request.user,
title = title, title=title,
date = timezone.now(), date=timezone.now(),
public = True, public=True,
) )
f.save() f.save()
@ -1007,26 +1063,24 @@ unhold_ticket = staff_member_required(unhold_ticket)
def rss_list(request): def rss_list(request):
return render(request, template_name='helpdesk/rss_list.html', return render(request, 'helpdesk/rss_list.html', {'queues': Queue.objects.all()})
context = {
'queues': Queue.objects.all(),
})
rss_list = staff_member_required(rss_list) rss_list = staff_member_required(rss_list)
def report_index(request): def report_index(request):
number_tickets = Ticket.objects.all().count() number_tickets = Ticket.objects.all().count()
saved_query = request.GET.get('saved_query', None) saved_query = request.GET.get('saved_query', None)
return render(request, template_name='helpdesk/report_index.html', return render(request, 'helpdesk/report_index.html', {
context = { 'number_tickets': number_tickets,
'number_tickets': number_tickets, 'saved_query': saved_query,
'saved_query': saved_query, })
})
report_index = staff_member_required(report_index) report_index = staff_member_required(report_index)
def run_report(request, report): def run_report(request, report):
if Ticket.objects.all().count() == 0 or report not in ('queuemonth', 'usermonth', 'queuestatus', 'queuepriority', 'userstatus', 'userpriority', 'userqueue', 'daysuntilticketclosedbymonth'): if Ticket.objects.all().count() == 0 or report not in (
'queuemonth', 'usermonth', 'queuestatus', 'queuepriority', 'userstatus',
'userpriority', 'userqueue', 'daysuntilticketclosedbymonth'):
return HttpResponseRedirect(reverse("helpdesk_report_index")) return HttpResponseRedirect(reverse("helpdesk_report_index"))
report_queryset = Ticket.objects.all().select_related().filter( report_queryset = Ticket.objects.all().select_related().filter(
@ -1191,15 +1245,14 @@ def run_report(request, report):
data.append(summarytable[item, hdr]) data.append(summarytable[item, hdr])
table.append([item] + data) table.append([item] + data)
return render(request, 'helpdesk/report_output.html', return render(request, 'helpdesk/report_output.html', {
{ 'title': title,
'title': title, 'charttype': charttype,
'charttype': charttype, 'data': table,
'data': table, 'headings': column_headings,
'headings': column_headings, 'from_saved_query': from_saved_query,
'from_saved_query': from_saved_query, 'saved_query': saved_query,
'saved_query': saved_query, })
})
run_report = staff_member_required(run_report) run_report = staff_member_required(run_report)
@ -1227,10 +1280,7 @@ def delete_saved_query(request, id):
query.delete() query.delete()
return HttpResponseRedirect(reverse('helpdesk_list')) return HttpResponseRedirect(reverse('helpdesk_list'))
else: else:
return render(request, template_name='helpdesk/confirm_delete_saved_query.html', return render(request, 'helpdesk/confirm_delete_saved_query.html', {'query': query})
context = {
'query': query,
})
delete_saved_query = staff_member_required(delete_saved_query) delete_saved_query = staff_member_required(delete_saved_query)
@ -1244,18 +1294,14 @@ def user_settings(request):
else: else:
form = UserSettingsForm(s.settings) form = UserSettingsForm(s.settings)
return render(request, template_name='helpdesk/user_settings.html', return render(request, 'helpdesk/user_settings.html', {'form': form})
context = {
'form': form,
})
user_settings = staff_member_required(user_settings) user_settings = staff_member_required(user_settings)
def email_ignore(request): def email_ignore(request):
return render(request, template_name='helpdesk/email_ignore_list.html', return render(request, 'helpdesk/email_ignore_list.html', {
context = { 'ignore_list': IgnoreEmail.objects.all(),
'ignore_list': IgnoreEmail.objects.all(), })
})
email_ignore = superuser_required(email_ignore) email_ignore = superuser_required(email_ignore)
@ -1263,15 +1309,12 @@ def email_ignore_add(request):
if request.method == 'POST': if request.method == 'POST':
form = EmailIgnoreForm(request.POST) form = EmailIgnoreForm(request.POST)
if form.is_valid(): if form.is_valid():
ignore = form.save() form.save()
return HttpResponseRedirect(reverse('helpdesk_email_ignore')) return HttpResponseRedirect(reverse('helpdesk_email_ignore'))
else: else:
form = EmailIgnoreForm(request.GET) form = EmailIgnoreForm(request.GET)
return render(request, template_name='helpdesk/email_ignore_add.html', return render(request, 'helpdesk/email_ignore_add.html', {'form': form})
context = {
'form': form,
})
email_ignore_add = superuser_required(email_ignore_add) email_ignore_add = superuser_required(email_ignore_add)
@ -1281,25 +1324,23 @@ def email_ignore_del(request, id):
ignore.delete() ignore.delete()
return HttpResponseRedirect(reverse('helpdesk_email_ignore')) return HttpResponseRedirect(reverse('helpdesk_email_ignore'))
else: else:
return render(request, template_name='helpdesk/email_ignore_del.html', return render(request, 'helpdesk/email_ignore_del.html', {'ignore': ignore})
context = {
'ignore': ignore,
})
email_ignore_del = superuser_required(email_ignore_del) email_ignore_del = superuser_required(email_ignore_del)
def ticket_cc(request, ticket_id): def ticket_cc(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): if not _has_access_to_queue(request.user, ticket.queue):
raise PermissionDenied() raise PermissionDenied()
copies_to = ticket.ticketcc_set.all() copies_to = ticket.ticketcc_set.all()
return render(request, template_name='helpdesk/ticket_cc_list.html', return render(request, 'helpdesk/ticket_cc_list.html', {
context = { 'copies_to': copies_to,
'copies_to': copies_to, 'ticket': ticket,
'ticket': ticket, })
})
ticket_cc = staff_member_required(ticket_cc) ticket_cc = staff_member_required(ticket_cc)
def ticket_cc_add(request, ticket_id): def ticket_cc_add(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): if not _has_access_to_queue(request.user, ticket.queue):
@ -1311,28 +1352,28 @@ def ticket_cc_add(request, ticket_id):
ticketcc = form.save(commit=False) ticketcc = form.save(commit=False)
ticketcc.ticket = ticket ticketcc.ticket = ticket
ticketcc.save() ticketcc.save()
return HttpResponseRedirect(reverse('helpdesk_ticket_cc', kwargs={'ticket_id': ticket.id})) return HttpResponseRedirect(reverse('helpdesk_ticket_cc',
kwargs={'ticket_id': ticket.id}))
else: else:
form = TicketCCForm() form = TicketCCForm()
return render(request, template_name='helpdesk/ticket_cc_add.html', return render(request, 'helpdesk/ticket_cc_add.html', {
context = { 'ticket': ticket,
'ticket': ticket, 'form': form,
'form': form, })
})
ticket_cc_add = staff_member_required(ticket_cc_add) ticket_cc_add = staff_member_required(ticket_cc_add)
def ticket_cc_del(request, ticket_id, cc_id): def ticket_cc_del(request, ticket_id, cc_id):
cc = get_object_or_404(TicketCC, ticket__id=ticket_id, id=cc_id) cc = get_object_or_404(TicketCC, ticket__id=ticket_id, id=cc_id)
if request.method == 'POST': if request.method == 'POST':
cc.delete() cc.delete()
return HttpResponseRedirect(reverse('helpdesk_ticket_cc', kwargs={'ticket_id': cc.ticket.id})) return HttpResponseRedirect(reverse('helpdesk_ticket_cc',
return render(request, template_name='helpdesk/ticket_cc_del.html', kwargs={'ticket_id': cc.ticket.id}))
context = { return render(request, 'helpdesk/ticket_cc_del.html', {'cc': cc})
'cc': cc,
})
ticket_cc_del = staff_member_required(ticket_cc_del) ticket_cc_del = staff_member_required(ticket_cc_del)
def ticket_dependency_add(request, ticket_id): def ticket_dependency_add(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): if not _has_access_to_queue(request.user, ticket.queue):
@ -1347,24 +1388,22 @@ def ticket_dependency_add(request, ticket_id):
return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket.id])) return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket.id]))
else: else:
form = TicketDependencyForm() form = TicketDependencyForm()
return render(request, template_name='helpdesk/ticket_dependency_add.html', return render(request, 'helpdesk/ticket_dependency_add.html', {
context = { 'ticket': ticket,
'ticket': ticket, 'form': form,
'form': form, })
})
ticket_dependency_add = staff_member_required(ticket_dependency_add) ticket_dependency_add = staff_member_required(ticket_dependency_add)
def ticket_dependency_del(request, ticket_id, dependency_id): def ticket_dependency_del(request, ticket_id, dependency_id):
dependency = get_object_or_404(TicketDependency, ticket__id=ticket_id, id=dependency_id) dependency = get_object_or_404(TicketDependency, ticket__id=ticket_id, id=dependency_id)
if request.method == 'POST': if request.method == 'POST':
dependency.delete() dependency.delete()
return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket_id])) return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket_id]))
return render(request, template_name='helpdesk/ticket_dependency_del.html', return render(request, 'helpdesk/ticket_dependency_del.html', {'dependency': dependency})
context = {
'dependency': dependency,
})
ticket_dependency_del = staff_member_required(ticket_dependency_del) ticket_dependency_del = staff_member_required(ticket_dependency_del)
def attachment_del(request, ticket_id, attachment_id): def attachment_del(request, ticket_id, attachment_id):
ticket = get_object_or_404(Ticket, id=ticket_id) ticket = get_object_or_404(Ticket, id=ticket_id)
if not _has_access_to_queue(request.user, ticket.queue): if not _has_access_to_queue(request.user, ticket.queue):
@ -1374,6 +1413,7 @@ def attachment_del(request, ticket_id, attachment_id):
return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket_id])) return HttpResponseRedirect(reverse('helpdesk_view', args=[ticket_id]))
attachment_del = staff_member_required(attachment_del) attachment_del = staff_member_required(attachment_del)
def calc_average_nbr_days_until_ticket_resolved(Tickets): def calc_average_nbr_days_until_ticket_resolved(Tickets):
nbr_closed_tickets = len(Tickets) nbr_closed_tickets = len(Tickets)
days_per_ticket = 0 days_per_ticket = 0
@ -1392,9 +1432,10 @@ def calc_average_nbr_days_until_ticket_resolved(Tickets):
return mean_per_ticket return mean_per_ticket
def calc_basic_ticket_stats(Tickets): def calc_basic_ticket_stats(Tickets):
# all not closed tickets (open, reopened, resolved,) - independent of user # all not closed tickets (open, reopened, resolved,) - independent of user
all_open_tickets = Tickets.exclude(status = Ticket.CLOSED_STATUS) all_open_tickets = Tickets.exclude(status=Ticket.CLOSED_STATUS)
today = datetime.today() today = datetime.today()
date_30 = date_rel_to_today(today, 30) date_30 = date_rel_to_today(today, 30)
@ -1403,57 +1444,66 @@ def calc_basic_ticket_stats(Tickets):
date_60_str = date_60.strftime('%Y-%m-%d') date_60_str = date_60.strftime('%Y-%m-%d')
# > 0 & <= 30 # > 0 & <= 30
ota_le_30 = all_open_tickets.filter(created__gte = date_30_str) ota_le_30 = all_open_tickets.filter(created__gte=date_30_str)
N_ota_le_30 = len(ota_le_30) N_ota_le_30 = len(ota_le_30)
# >= 30 & <= 60 # >= 30 & <= 60
ota_le_60_ge_30 = all_open_tickets.filter(created__gte = date_60_str, created__lte = date_30_str) ota_le_60_ge_30 = all_open_tickets.filter(created__gte=date_60_str, created__lte=date_30_str)
N_ota_le_60_ge_30 = len(ota_le_60_ge_30) N_ota_le_60_ge_30 = len(ota_le_60_ge_30)
# >= 60 # >= 60
ota_ge_60 = all_open_tickets.filter(created__lte = date_60_str) ota_ge_60 = all_open_tickets.filter(created__lte=date_60_str)
N_ota_ge_60 = len(ota_ge_60) N_ota_ge_60 = len(ota_ge_60)
# (O)pen (T)icket (S)tats # (O)pen (T)icket (S)tats
ots = list() ots = list()
# label, number entries, color, sort_string # label, number entries, color, sort_string
ots.append(['< 30 days', N_ota_le_30, get_color_for_nbr_days(N_ota_le_30), sort_string(date_30_str, ''), ]) ots.append(['< 30 days', N_ota_le_30, get_color_for_nbr_days(N_ota_le_30),
ots.append(['30 - 60 days', N_ota_le_60_ge_30, get_color_for_nbr_days(N_ota_le_60_ge_30), sort_string(date_60_str, date_30_str), ]) sort_string(date_30_str, ''), ])
ots.append(['> 60 days', N_ota_ge_60, get_color_for_nbr_days(N_ota_ge_60), sort_string('', date_60_str), ]) ots.append(['30 - 60 days', N_ota_le_60_ge_30, get_color_for_nbr_days(N_ota_le_60_ge_30),
sort_string(date_60_str, date_30_str), ])
ots.append(['> 60 days', N_ota_ge_60, get_color_for_nbr_days(N_ota_ge_60),
sort_string('', date_60_str), ])
# all closed tickets - independent of user. # all closed tickets - independent of user.
all_closed_tickets = Tickets.filter(status = Ticket.CLOSED_STATUS) all_closed_tickets = Tickets.filter(status=Ticket.CLOSED_STATUS)
average_nbr_days_until_ticket_closed = calc_average_nbr_days_until_ticket_resolved(all_closed_tickets) average_nbr_days_until_ticket_closed = \
calc_average_nbr_days_until_ticket_resolved(all_closed_tickets)
# all closed tickets that were opened in the last 60 days. # all closed tickets that were opened in the last 60 days.
all_closed_last_60_days = all_closed_tickets.filter(created__gte = date_60_str) all_closed_last_60_days = all_closed_tickets.filter(created__gte=date_60_str)
average_nbr_days_until_ticket_closed_last_60_days = calc_average_nbr_days_until_ticket_resolved(all_closed_last_60_days) average_nbr_days_until_ticket_closed_last_60_days = \
calc_average_nbr_days_until_ticket_resolved(all_closed_last_60_days)
# put together basic stats # put together basic stats
basic_ticket_stats = { 'average_nbr_days_until_ticket_closed': average_nbr_days_until_ticket_closed, basic_ticket_stats = {
'average_nbr_days_until_ticket_closed_last_60_days': average_nbr_days_until_ticket_closed_last_60_days, 'average_nbr_days_until_ticket_closed': average_nbr_days_until_ticket_closed,
'open_ticket_stats': ots, } 'average_nbr_days_until_ticket_closed_last_60_days':
average_nbr_days_until_ticket_closed_last_60_days,
'open_ticket_stats': ots,
}
return basic_ticket_stats return basic_ticket_stats
def get_color_for_nbr_days(nbr_days): def get_color_for_nbr_days(nbr_days):
''' '''
if nbr_days < 5: if nbr_days < 5:
color_string = 'green' color_string = 'green'
elif nbr_days >= 5 and nbr_days < 10: elif nbr_days < 10:
color_string = 'orange' color_string = 'orange'
else: # more than 10 days else: # more than 10 days
color_string = 'red' color_string = 'red'
return color_string return color_string
def days_since_created(today, ticket): def days_since_created(today, ticket):
return (today - ticket.created).days return (today - ticket.created).days
def date_rel_to_today(today, offset): def date_rel_to_today(today, offset):
return today - timedelta(days = offset) return today - timedelta(days = offset)
def sort_string(begin, end): def sort_string(begin, end):
return 'sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s' %(begin, end, Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS, Ticket.RESOLVED_STATUS) return 'sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s' % (
begin, end, Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS, Ticket.RESOLVED_STATUS)