pycodestyle formatting

This commit is contained in:
Martin Whitehouse 2022-07-12 12:34:19 +02:00
parent d36cc7e381
commit aa876f8016
No known key found for this signature in database
GPG Key ID: 8492A7A45769CD35
39 changed files with 892 additions and 464 deletions

View File

@ -97,13 +97,13 @@ WSGI_APPLICATION = 'demo.demodesk.config.wsgi.application'
# Some common settings are below. # Some common settings are below.
HELPDESK_DEFAULT_SETTINGS = { HELPDESK_DEFAULT_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,
'preset_replies': True, 'preset_replies': True,
'tickets_per_page': 25 'tickets_per_page': 25
} }
# Should the public web portal be enabled? # Should the public web portal be enabled?
@ -153,7 +153,7 @@ SITE_ID = 1
# Sessions # Sessions
# https://docs.djangoproject.com/en/1.11/topics/http/sessions # https://docs.djangoproject.com/en/1.11/topics/http/sessions
SESSION_COOKIE_AGE = 86400 # = 1 day SESSION_COOKIE_AGE = 86400 # = 1 day
# For better default security, set these cookie flags, but # For better default security, set these cookie flags, but
# these are likely to cause problems when testing locally # these are likely to cause problems when testing locally

View File

@ -2,6 +2,7 @@
import os import os
import sys import sys
def main(): def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
try: try:
@ -21,5 +22,6 @@ def main():
raise raise
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -2,6 +2,7 @@
import os import os
import sys import sys
def main(): def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
try: try:
@ -21,5 +22,6 @@ def main():
raise raise
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -28,7 +28,7 @@ KEYWORDS = []
PACKAGES = ['demodesk'] PACKAGES = ['demodesk']
REQUIREMENTS = [ REQUIREMENTS = [
'django-helpdesk' 'django-helpdesk'
] ]
ENTRY_POINTS = { ENTRY_POINTS = {
'console_scripts': ['demodesk = demodesk.manage:main'] 'console_scripts': ['demodesk = demodesk.manage:main']
} }

View File

@ -11,14 +11,15 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys, os import sys
import os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.')) #sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0' #needs_sphinx = '1.0'
@ -87,7 +88,7 @@ pygments_style = 'sphinx'
#modindex_common_prefix = [] #modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
@ -167,7 +168,7 @@ html_static_path = ['_static']
htmlhelp_basename = 'django-helpdeskdoc' htmlhelp_basename = 'django-helpdeskdoc'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------
# The paper size ('letter' or 'a4'). # The paper size ('letter' or 'a4').
#latex_paper_size = 'letter' #latex_paper_size = 'letter'
@ -178,8 +179,8 @@ htmlhelp_basename = 'django-helpdeskdoc'
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'django-helpdesk.tex', u'django-helpdesk Documentation', ('index', 'django-helpdesk.tex', u'django-helpdesk Documentation',
u'Ross Poulton + django-helpdesk Contributors', 'manual'), u'Ross Poulton + django-helpdesk Contributors', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -206,7 +207,7 @@ latex_documents = [
#latex_domain_indices = True #latex_domain_indices = True
# -- Options for manual page output -------------------------------------------- # -- Options for manual page output --------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).

View File

@ -9,6 +9,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import KBCategory from helpdesk.models import KBCategory
from helpdesk.models import KBItem from helpdesk.models import KBItem
@admin.register(Queue) @admin.register(Queue)
class QueueAdmin(admin.ModelAdmin): class QueueAdmin(admin.ModelAdmin):
list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent') list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent')
@ -74,7 +75,8 @@ class FollowUpAdmin(admin.ModelAdmin):
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
@admin.register(KBItem) @admin.register(KBItem)
class KBItemAdmin(admin.ModelAdmin): class KBItemAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'last_updated', 'team', 'order', 'enabled') list_display = ('category', 'title', 'last_updated',
'team', 'order', 'enabled')
inlines = [KBIAttachmentInline] inlines = [KBIAttachmentInline]
readonly_fields = ('voted_by', 'downvoted_by') readonly_fields = ('voted_by', 'downvoted_by')

View File

@ -5,5 +5,6 @@ class HelpdeskConfig(AppConfig):
name = 'helpdesk' name = 'helpdesk'
verbose_name = "Helpdesk" verbose_name = "Helpdesk"
# for Django 3.2 support: # for Django 3.2 support:
# see: https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field # see:
# https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
default_auto_field = 'django.db.models.AutoField' default_auto_field = 'django.db.models.AutoField'

View File

@ -72,7 +72,8 @@ def process_email(quiet=False):
# Log messages to specific file only if the queue has it configured # Log messages to specific file only if the queue has it configured
if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set
log_file_handler = logging.FileHandler(join(q.logging_dir, q.slug + '_get_email.log')) log_file_handler = logging.FileHandler(
join(q.logging_dir, q.slug + '_get_email.log'))
logger.addHandler(log_file_handler) logger.addHandler(log_file_handler)
else: else:
log_file_handler = None log_file_handler = None
@ -105,7 +106,8 @@ def pop3_sync(q, logger, server):
try: try:
server.stls() server.stls()
except Exception: except Exception:
logger.warning("POP3 StartTLS failed or unsupported. Connection will be unencrypted.") logger.warning(
"POP3 StartTLS failed or unsupported. Connection will be unencrypted.")
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)
@ -127,16 +129,21 @@ def pop3_sync(q, logger, server):
raw_content = server.retr(msgNum)[1] raw_content = server.retr(msgNum)[1]
if type(raw_content[0]) is bytes: if type(raw_content[0]) is bytes:
full_message = "\n".join([elm.decode('utf-8') for elm in raw_content]) full_message = "\n".join([elm.decode('utf-8')
for elm in raw_content])
else: else:
full_message = encoding.force_str("\n".join(raw_content), errors='replace') full_message = encoding.force_str(
ticket = object_from_message(message=full_message, queue=q, logger=logger) "\n".join(raw_content), errors='replace')
ticket = object_from_message(
message=full_message, queue=q, logger=logger)
if ticket: if ticket:
server.dele(msgNum) server.dele(msgNum)
logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum) logger.info(
"Successfully processed message %s, deleted from POP3 server" % msgNum)
else: else:
logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum) logger.warn(
"Message %s was not successfully processed, and will be left on POP3 server" % msgNum)
server.quit() server.quit()
@ -146,7 +153,8 @@ def imap_sync(q, logger, server):
try: try:
server.starttls() server.starttls()
except Exception: except Exception:
logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.") logger.warning(
"IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
server.login(q.email_box_user or server.login(q.email_box_user or
settings.QUEUE_EMAIL_BOX_USER, settings.QUEUE_EMAIL_BOX_USER,
q.email_box_pass or q.email_box_pass or
@ -177,14 +185,17 @@ def imap_sync(q, logger, server):
status, data = server.fetch(num, '(RFC822)') status, data = server.fetch(num, '(RFC822)')
full_message = encoding.force_str(data[0][1], errors='replace') full_message = encoding.force_str(data[0][1], errors='replace')
try: try:
ticket = object_from_message(message=full_message, queue=q, logger=logger) ticket = object_from_message(
message=full_message, queue=q, logger=logger)
except TypeError: except TypeError:
ticket = None # hotfix. Need to work out WHY. ticket = None # hotfix. Need to work out WHY.
if ticket: if ticket:
server.store(num, '+FLAGS', '\\Deleted') server.store(num, '+FLAGS', '\\Deleted')
logger.info("Successfully processed message %s, deleted from IMAP server" % num) logger.info(
"Successfully processed message %s, deleted from IMAP server" % num)
else: else:
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num) logger.warn(
"Message %s was not successfully processed, and will be left on IMAP server" % num)
except imaplib.IMAP4.error: except imaplib.IMAP4.error:
logger.error( logger.error(
"IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?", "IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?",
@ -261,7 +272,8 @@ def process_queue(q, logger):
elif email_box_type == 'local': elif email_box_type == 'local':
mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/' mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/'
mail = [join(mail_dir, f) for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))] mail = [join(mail_dir, f)
for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))]
logger.info("Found %d messages in local mailbox directory" % len(mail)) logger.info("Found %d messages in local mailbox directory" % len(mail))
logger.info("Found %d messages in local mailbox directory" % len(mail)) logger.info("Found %d messages in local mailbox directory" % len(mail))
@ -269,17 +281,22 @@ def process_queue(q, logger):
logger.info("Processing message %d" % i) logger.info("Processing message %d" % i)
with open(m, 'r') as f: with open(m, 'r') as f:
full_message = encoding.force_str(f.read(), errors='replace') full_message = encoding.force_str(f.read(), errors='replace')
ticket = object_from_message(message=full_message, queue=q, logger=logger) ticket = object_from_message(
message=full_message, queue=q, logger=logger)
if ticket: if ticket:
logger.info("Successfully processed message %d, ticket/comment created.", i) logger.info(
"Successfully processed message %d, ticket/comment created.", i)
try: try:
os.unlink(m) # delete message file if ticket was successful # delete message file if ticket was successful
os.unlink(m)
except OSError as e: except OSError as e:
logger.error("Unable to delete message %d (%s).", i, str(e)) logger.error(
"Unable to delete message %d (%s).", i, str(e))
else: else:
logger.info("Successfully deleted message %d.", i) logger.info("Successfully deleted message %d.", i)
else: else:
logger.warn("Message %d was not successfully processed, and will be left in local directory", i) logger.warn(
"Message %d was not successfully processed, and will be left in local directory", i)
def decodeUnknown(charset, string): def decodeUnknown(charset, string):
@ -309,8 +326,10 @@ def is_autoreply(message):
So we don't start mail loops So we don't start mail loops
""" """
any_if_this = [ any_if_this = [
False if not message.get("Auto-Submitted") else message.get("Auto-Submitted").lower() != "no", False if not message.get(
True if message.get("X-Auto-Response-Suppress") in ("DR", "AutoReply", "All") else False, "Auto-Submitted") else message.get("Auto-Submitted").lower() != "no",
True if message.get("X-Auto-Response-Suppress") in ("DR",
"AutoReply", "All") else False,
message.get("List-Id"), message.get("List-Id"),
message.get("List-Unsubscribe"), message.get("List-Unsubscribe"),
] ]
@ -340,7 +359,8 @@ def create_ticket_cc(ticket, cc_list):
pass pass
try: try:
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email) ticket_cc = subscribe_to_ticket_updates(
ticket=ticket, user=user, email=cced_email)
new_ticket_ccs.append(ticket_cc) new_ticket_ccs.append(ticket_cc)
except ValidationError: except ValidationError:
pass pass
@ -370,7 +390,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
if in_reply_to is not None: if in_reply_to is not None:
try: try:
queryset = FollowUp.objects.filter(message_id=in_reply_to).order_by('-date') queryset = FollowUp.objects.filter(
message_id=in_reply_to).order_by('-date')
if queryset.count() > 0: if queryset.count() > 0:
previous_followup = queryset.first() previous_followup = queryset.first()
ticket = previous_followup.ticket ticket = previous_followup.ticket
@ -386,7 +407,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
new = False new = False
# Check if the ticket has been merged to another ticket # Check if the ticket has been merged to another ticket
if ticket.merged_to: if ticket.merged_to:
logger.info("Ticket has been merged to %s" % ticket.merged_to.ticket) logger.info("Ticket has been merged to %s" %
ticket.merged_to.ticket)
# Use the ticket in which it was merged to for next operations # Use the ticket in which it was merged to for next operations
ticket = ticket.merged_to ticket = ticket.merged_to
@ -402,7 +424,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
priority=payload['priority'], priority=payload['priority'],
) )
ticket.save() ticket.save()
logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id)) logger.debug("Created new ticket %s-%s" %
(ticket.queue.slug, ticket.id))
new = True new = True
@ -413,7 +436,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
f = FollowUp( f = FollowUp(
ticket=ticket, ticket=ticket,
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=now, date=now,
public=True, public=True,
comment=payload.get('full_body', payload['body']) or "", comment=payload.get('full_body', payload['body']) or "",
@ -422,7 +446,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
if ticket.status == Ticket.REOPENED_STATUS: if ticket.status == Ticket.REOPENED_STATUS:
f.new_status = Ticket.REOPENED_STATUS f.new_status = Ticket.REOPENED_STATUS
f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}) f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' %
{'sender_email': sender_email})
f.save() f.save()
logger.debug("Created new FollowUp for Ticket") logger.debug("Created new FollowUp for Ticket")
@ -445,14 +470,16 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
if queue.enable_notifications_on_email_events and len(notifications_to_be_sent): if queue.enable_notifications_on_email_events and len(notifications_to_be_sent):
ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True) ticket_cc_list = TicketCC.objects.filter(
ticket=ticket).all().values_list('email', flat=True)
for email_address in ticket_cc_list: for email_address in ticket_cc_list:
notifications_to_be_sent.append(email_address) notifications_to_be_sent.append(email_address)
autoreply = is_autoreply(message) autoreply = is_autoreply(message)
if autoreply: if autoreply:
logger.info("Message seems to be auto-reply, not sending any emails back to the sender") logger.info(
"Message seems to be auto-reply, not sending any emails back to the sender")
else: else:
# send mail to appropriate people now depending on what objects # send mail to appropriate people now depending on what objects
# were created and who was CC'd # were created and who was CC'd
@ -494,7 +521,8 @@ def object_from_message(message, queue, logger):
message = email.message_from_string(message) message = email.message_from_string(message)
subject = message.get('subject', _('Comment from e-mail')) subject = message.get('subject', _('Comment from e-mail'))
subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) subject = decode_mail_headers(
decodeUnknown(message.get_charset(), subject))
for affix in STRIPPED_SUBJECT_STRINGS: for affix in STRIPPED_SUBJECT_STRINGS:
subject = subject.replace(affix, "") subject = subject.replace(affix, "")
subject = subject.strip() subject = subject.strip()
@ -508,13 +536,16 @@ def object_from_message(message, queue, logger):
# Note that the replace won't work on just an email with no real name, # Note that the replace won't work on just an email with no real name,
# but the getaddresses() function seems to be able to handle just unclosed quotes # but the getaddresses() function seems to be able to handle just unclosed quotes
# correctly. Not ideal, but this seems to work for now. # correctly. Not ideal, but this seems to work for now.
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1] sender_email = email.utils.getaddresses(
['\"' + sender.replace('<', '\" <')])[0][1]
cc = message.get_all('cc', None) cc = message.get_all('cc', None)
if cc: if cc:
# first, fixup the encoding if necessary # first, fixup the encoding if necessary
cc = [decode_mail_headers(decodeUnknown(message.get_charset(), x)) for x in cc] cc = [decode_mail_headers(decodeUnknown(
# get_all checks if multiple CC headers, but individual emails may be comma separated too message.get_charset(), x)) for x in cc]
# get_all checks if multiple CC headers, but individual emails may be
# comma separated too
tempcc = [] tempcc = []
for hdr in cc: for hdr in cc:
tempcc.extend(hdr.split(',')) tempcc.extend(hdr.split(','))
@ -561,14 +592,16 @@ def object_from_message(message, queue, logger):
# have to use django_settings here so overwritting it works in tests # have to use django_settings here so overwritting it works in tests
# the default value is False anyway # the default value is False anyway
if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False): if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False):
# first message in thread, we save full body to avoid losing forwards and things like that # first message in thread, we save full body to avoid
# losing forwards and things like that
body_parts = [] body_parts = []
for f in EmailReplyParser.read(body).fragments: for f in EmailReplyParser.read(body).fragments:
body_parts.append(f.content) body_parts.append(f.content)
full_body = '\n\n'.join(body_parts) full_body = '\n\n'.join(body_parts)
body = EmailReplyParser.parse_reply(body) body = EmailReplyParser.parse_reply(body)
else: else:
# second and other reply, save only first part of the message # second and other reply, save only first part of the
# message
body = EmailReplyParser.parse_reply(body) body = EmailReplyParser.parse_reply(body)
full_body = body full_body = body
# workaround to get unicode text out rather than escaped text # workaround to get unicode text out rather than escaped text
@ -579,13 +612,17 @@ def object_from_message(message, queue, logger):
logger.debug("Discovered plain text MIME part") logger.debug("Discovered plain text MIME part")
else: else:
try: try:
email_body = encoding.smart_str(part.get_payload(decode=True)) email_body = encoding.smart_str(
part.get_payload(decode=True))
except UnicodeDecodeError: except UnicodeDecodeError:
email_body = encoding.smart_str(part.get_payload(decode=False)) email_body = encoding.smart_str(
part.get_payload(decode=False))
if not body and not full_body: if not body and not full_body:
# no text has been parsed so far - try such deep parsing for some messages # no text has been parsed so far - try such deep parsing
altered_body = email_body.replace("</p>", "</p>\n").replace("<br", "\n<br") # for some messages
altered_body = email_body.replace(
"</p>", "</p>\n").replace("<br", "\n<br")
mail = BeautifulSoup(str(altered_body), "html.parser") mail = BeautifulSoup(str(altered_body), "html.parser")
full_body = mail.get_text() full_body = mail.get_text()
@ -601,7 +638,8 @@ def object_from_message(message, queue, logger):
'</html>' '</html>'
) % email_body ) % email_body
files.append( files.append(
SimpleUploadedFile(_("email_html_body.html"), payload.encode("utf-8"), 'text/html') SimpleUploadedFile(
_("email_html_body.html"), payload.encode("utf-8"), 'text/html')
) )
logger.debug("Discovered HTML MIME part") logger.debug("Discovered HTML MIME part")
else: else:
@ -627,7 +665,8 @@ def object_from_message(message, queue, logger):
# except non_b64_err: # except non_b64_err:
# logger.debug("Payload was not base64 encoded, using raw bytes") # logger.debug("Payload was not base64 encoded, using raw bytes")
# # payloadToWrite = payload # # payloadToWrite = payload
files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) files.append(SimpleUploadedFile(name, part.get_payload(
decode=True), mimetypes.guess_type(name)[0]))
logger.debug("Found MIME attachment %s" % name) logger.debug("Found MIME attachment %s" % name)
counter += 1 counter += 1
@ -645,7 +684,8 @@ def object_from_message(message, queue, logger):
body = "" body = ""
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False): if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False):
# save message as attachment in case of some complex markup renders wrong # save message as attachment in case of some complex markup renders
# wrong
files.append( files.append(
SimpleUploadedFile( SimpleUploadedFile(
_("original_message.eml").replace( _("original_message.eml").replace(
@ -660,7 +700,8 @@ def object_from_message(message, queue, logger):
smtp_priority = message.get('priority', '') smtp_priority = message.get('priority', '')
smtp_importance = message.get('importance', '') smtp_importance = message.get('importance', '')
high_priority_types = {'high', 'important', '1', 'urgent'} high_priority_types = {'high', 'important', '1', 'urgent'}
priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3 priority = 2 if high_priority_types & {
smtp_priority, smtp_importance} else 3
payload = { payload = {
'body': body, 'body': body,

View File

@ -37,40 +37,49 @@ class CustomFieldMixin(object):
def customfield_to_field(self, field, instanceargs): def customfield_to_field(self, field, instanceargs):
# Use TextInput widget by default # Use TextInput widget by default
instanceargs['widget'] = forms.TextInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.TextInput(
attrs={'class': 'form-control'})
# if-elif branches start with special cases # if-elif branches start with special cases
if field.data_type == 'varchar': if field.data_type == 'varchar':
fieldclass = forms.CharField fieldclass = forms.CharField
instanceargs['max_length'] = field.max_length instanceargs['max_length'] = field.max_length
elif field.data_type == 'text': elif field.data_type == 'text':
fieldclass = forms.CharField fieldclass = forms.CharField
instanceargs['widget'] = forms.Textarea(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.Textarea(
attrs={'class': 'form-control'})
instanceargs['max_length'] = field.max_length instanceargs['max_length'] = field.max_length
elif field.data_type == 'integer': elif field.data_type == 'integer':
fieldclass = forms.IntegerField fieldclass = forms.IntegerField
instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.NumberInput(
attrs={'class': 'form-control'})
elif field.data_type == 'decimal': elif field.data_type == 'decimal':
fieldclass = forms.DecimalField fieldclass = forms.DecimalField
instanceargs['decimal_places'] = field.decimal_places instanceargs['decimal_places'] = field.decimal_places
instanceargs['max_digits'] = field.max_length instanceargs['max_digits'] = field.max_length
instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.NumberInput(
attrs={'class': 'form-control'})
elif field.data_type == 'list': elif field.data_type == 'list':
fieldclass = forms.ChoiceField fieldclass = forms.ChoiceField
instanceargs['choices'] = field.get_choices() instanceargs['choices'] = field.get_choices()
instanceargs['widget'] = forms.Select(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.Select(
attrs={'class': 'form-control'})
else: else:
# Try to use the immediate equivalences dictionary # Try to use the immediate equivalences dictionary
try: try:
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type] fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
# Change widgets for the following classes # Change widgets for the following classes
if fieldclass == forms.DateField: if fieldclass == forms.DateField:
instanceargs['widget'] = forms.DateInput(attrs={'class': 'form-control date-field'}) instanceargs['widget'] = forms.DateInput(
attrs={'class': 'form-control date-field'})
elif fieldclass == forms.DateTimeField: elif fieldclass == forms.DateTimeField:
instanceargs['widget'] = forms.DateTimeInput(attrs={'class': 'form-control datetime-field'}) instanceargs['widget'] = forms.DateTimeInput(
attrs={'class': 'form-control datetime-field'})
elif fieldclass == forms.TimeField: elif fieldclass == forms.TimeField:
instanceargs['widget'] = forms.TimeInput(attrs={'class': 'form-control time-field'}) instanceargs['widget'] = forms.TimeInput(
attrs={'class': 'form-control time-field'})
elif fieldclass == forms.BooleanField: elif fieldclass == forms.BooleanField:
instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.CheckboxInput(
attrs={'class': 'form-control'})
except KeyError: except KeyError:
# The data_type was not found anywhere # The data_type was not found anywhere
@ -83,10 +92,12 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
class Meta: class Meta:
model = Ticket model = Ticket
exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to') exclude = ('created', 'modified', 'status', 'on_hold',
'resolution', 'last_escalation', 'assigned_to')
class Media: class Media:
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js') js = ('helpdesk/js/init_due_date.js',
'helpdesk/js/init_datetime_classes.js')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
@ -96,21 +107,28 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
# Disable and add help_text to the merged_to field on this form # Disable and add help_text to the merged_to field on this form
self.fields['merged_to'].disabled = True self.fields['merged_to'].disabled = True
self.fields['merged_to'].help_text = _('This ticket is merged into the selected ticket.') self.fields['merged_to'].help_text = _(
'This ticket is merged into the selected ticket.')
for field in CustomField.objects.all(): for field in CustomField.objects.all():
initial_value = None initial_value = None
try: try:
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field) current_value = TicketCustomFieldValue.objects.get(
ticket=self.instance, field=field)
initial_value = current_value.value initial_value = current_value.value
# Attempt to convert from fixed format string to date/time data type # Attempt to convert from fixed format string to date/time data
# type
if 'datetime' == current_value.field.data_type: if 'datetime' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATETIME_FORMAT) initial_value = datetime.strptime(
initial_value, CUSTOMFIELD_DATETIME_FORMAT)
elif 'date' == current_value.field.data_type: elif 'date' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATE_FORMAT) initial_value = datetime.strptime(
initial_value, CUSTOMFIELD_DATE_FORMAT)
elif 'time' == current_value.field.data_type: elif 'time' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_TIME_FORMAT) initial_value = datetime.strptime(
# If it is boolean field, transform the value to a real boolean instead of a string initial_value, CUSTOMFIELD_TIME_FORMAT)
# If it is boolean field, transform the value to a real boolean
# instead of a string
elif 'boolean' == current_value.field.data_type: elif 'boolean' == current_value.field.data_type:
initial_value = 'True' == initial_value initial_value = 'True' == initial_value
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError): except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
@ -133,9 +151,11 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
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)
try: try:
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue.objects.get(
ticket=self.instance, field=customfield)
except ObjectDoesNotExist: except ObjectDoesNotExist:
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue(
ticket=self.instance, field=customfield)
cfv.value = convert_value(value) cfv.value = convert_value(value)
cfv.save() cfv.save()
@ -152,7 +172,8 @@ class EditFollowUpForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Filter not openned tickets here.""" """Filter not openned tickets here."""
super(EditFollowUpForm, self).__init__(*args, **kwargs) super(EditFollowUpForm, self).__init__(*args, **kwargs)
self.fields["ticket"].queryset = Ticket.objects.filter(status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS)) self.fields["ticket"].queryset = Ticket.objects.filter(
status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
class AbstractTicketForm(CustomFieldMixin, forms.Form): class AbstractTicketForm(CustomFieldMixin, forms.Form):
@ -178,7 +199,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.Textarea(attrs={'class': 'form-control'}), widget=forms.Textarea(attrs={'class': 'form-control'}),
label=_('Description of your issue'), label=_('Description of your issue'),
required=True, required=True,
help_text=_('Please be as descriptive as possible and include all details'), help_text=_(
'Please be as descriptive as possible and include all details'),
) )
priority = forms.ChoiceField( priority = forms.ChoiceField(
@ -187,13 +209,16 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
required=True, required=True,
initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'), initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'),
label=_('Priority'), label=_('Priority'),
help_text=_("Please select a priority carefully. If unsure, leave it as '3'."), help_text=_(
"Please select a priority carefully. If unsure, leave it as '3'."),
) )
due_date = forms.DateTimeField( due_date = forms.DateTimeField(
widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}), widget=forms.TextInput(
attrs={'class': 'form-control', 'autocomplete': 'off'}),
required=False, required=False,
input_formats=[CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"], input_formats=[CUSTOMFIELD_DATE_FORMAT,
CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
label=_('Due on'), label=_('Due on'),
) )
@ -205,7 +230,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
) )
class Media: class Media:
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js') js = ('helpdesk/js/init_due_date.js',
'helpdesk/js/init_datetime_classes.js')
def __init__(self, kbcategory=None, *args, **kwargs): def __init__(self, kbcategory=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -215,7 +241,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.Select(attrs={'class': 'form-control'}), widget=forms.Select(attrs={'class': 'form-control'}),
required=False, required=False,
label=_('Knowledge Base Item'), label=_('Knowledge Base Item'),
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(category=kbcategory.pk, enabled=True)], choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(
category=kbcategory.pk, enabled=True)],
) )
def _add_form_custom_fields(self, staff_only_filter=None): def _add_form_custom_fields(self, staff_only_filter=None):
@ -307,7 +334,8 @@ class TicketForm(AbstractTicketForm):
submitter_email = forms.EmailField( submitter_email = forms.EmailField(
required=False, required=False,
label=_('Submitter E-Mail Address'), label=_('Submitter E-Mail Address'),
widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}), widget=forms.TextInput(
attrs={'class': 'form-control', 'type': 'email'}),
help_text=_('This e-mail address will receive copies of all public ' help_text=_('This e-mail address will receive copies of all public '
'updates to this ticket.'), 'updates to this ticket.'),
) )
@ -335,10 +363,13 @@ class TicketForm(AbstractTicketForm):
self.fields['queue'].choices = queue_choices self.fields['queue'].choices = queue_choices
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)
else: else:
assignable_users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) assignable_users = User.objects.filter(
self.fields['assigned_to'].choices = [('', '--------')] + [(u.id, u.get_username()) for u in assignable_users] is_active=True).order_by(User.USERNAME_FIELD)
self.fields['assigned_to'].choices = [
('', '--------')] + [(u.id, u.get_username()) for u in assignable_users]
self._add_form_custom_fields() self._add_form_custom_fields()
def save(self, user): def save(self, user):
@ -380,7 +411,8 @@ class PublicTicketForm(AbstractTicketForm):
Ticket Form creation for all users (public-facing). Ticket Form creation for all users (public-facing).
""" """
submitter_email = forms.EmailField( submitter_email = forms.EmailField(
widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}), widget=forms.TextInput(
attrs={'class': 'form-control', 'type': 'email'}),
required=True, required=True,
label=_('Your E-Mail Address'), label=_('Your E-Mail Address'),
help_text=_('We will e-mail you when your ticket is updated.'), help_text=_('We will e-mail you when your ticket is updated.'),
@ -406,7 +438,8 @@ class PublicTicketForm(AbstractTicketForm):
} }
for field_name, field_setting_key in field_deletion_table.items(): for field_name, field_setting_key in field_deletion_table.items():
has_settings_default_value = getattr(settings, field_setting_key, None) has_settings_default_value = getattr(
settings, field_setting_key, None)
if has_settings_default_value is not None: if has_settings_default_value is not None:
del self.fields[field_name] del self.fields[field_name]
@ -485,9 +518,11 @@ class TicketCCForm(forms.ModelForm):
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:
users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
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
@ -497,9 +532,11 @@ class TicketCCUserForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TicketCCUserForm, self).__init__(*args, **kwargs) super(TicketCCUserForm, self).__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
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: class Meta:
@ -538,8 +575,11 @@ class MultipleTicketSelectForm(forms.Form):
if len(tickets) < 2: if len(tickets) < 2:
raise ValidationError(_('Please choose at least 2 tickets.')) raise ValidationError(_('Please choose at least 2 tickets.'))
if len(tickets) > 4: if len(tickets) > 4:
raise ValidationError(_('Impossible to merge more than 4 tickets...')) raise ValidationError(
queues = tickets.order_by('queue').distinct().values_list('queue', flat=True) _('Impossible to merge more than 4 tickets...'))
queues = tickets.order_by('queue').distinct(
).values_list('queue', flat=True)
if len(queues) != 1: if len(queues) != 1:
raise ValidationError(_('All selected tickets must share the same queue in order to be merged.')) raise ValidationError(
_('All selected tickets must share the same queue in order to be merged.'))
return tickets return tickets

View File

@ -129,7 +129,8 @@ def text_is_spam(text, request):
def process_attachments(followup, attached_files): def process_attachments(followup, attached_files):
max_email_attachment_size = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) max_email_attachment_size = getattr(
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
attachments = [] attachments = []
for attached in attached_files: for attached in attached_files:
@ -152,7 +153,8 @@ def process_attachments(followup, attached_files):
if attached.size < max_email_attachment_size: if attached.size < max_email_attachment_size:
# Only files smaller than 512kb (or as defined in # Only files smaller than 512kb (or as defined in
# settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via email. # settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via
# email.
attachments.append([filename, att.file]) attachments.append([filename, att.file])
return attachments return attachments

View File

@ -66,7 +66,8 @@ class Command(BaseCommand):
raise CommandError("Queue %s does not exist." % queue) raise CommandError("Queue %s does not exist." % queue)
queues.append(q) queues.append(q)
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues) create_exclusions(days=days, occurrences=occurrences,
verbose=verbose, queues=queues)
day_names = { day_names = {
@ -90,11 +91,13 @@ def create_exclusions(days, occurrences, verbose, queues):
while i < occurrences: while i < occurrences:
if day == workdate.weekday(): if day == workdate.weekday():
if EscalationExclusion.objects.filter(date=workdate).count() == 0: if EscalationExclusion.objects.filter(date=workdate).count() == 0:
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate) esc = EscalationExclusion(
name='Auto Exclusion for %s' % day_name, date=workdate)
esc.save() esc.save()
if verbose: if verbose:
print("Created exclusion for %s %s" % (day_name, workdate)) print("Created exclusion for %s %s" %
(day_name, workdate))
for q in queues: for q in queues:
esc.queues.add(q) esc.queues.add(q)
@ -116,7 +119,8 @@ def usage():
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:
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues=']) opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', [
'days=', 'occurrences=', 'verbose', 'queues='])
except getopt.GetoptError: except getopt.GetoptError:
usage() usage()
sys.exit(2) sys.exit(2)
@ -151,4 +155,5 @@ if __name__ == '__main__':
sys.exit(2) sys.exit(2)
queues.append(q) queues.append(q)
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues) create_exclusions(days=days, occurrences=occurrences,
verbose=verbose, queues=queues)

View File

@ -55,14 +55,17 @@ class Command(BaseCommand):
self.stdout.write("Preparing Queue %s [%s]" % (q.title, q.slug)) self.stdout.write("Preparing Queue %s [%s]" % (q.title, q.slug))
if q.permission_name: if q.permission_name:
self.stdout.write(" .. already has `permission_name=%s`" % q.permission_name) self.stdout.write(
" .. already has `permission_name=%s`" % q.permission_name)
basename = q.permission_name[9:] basename = q.permission_name[9:]
else: else:
basename = q.generate_permission_name() basename = q.generate_permission_name()
self.stdout.write(" .. generated `permission_name=%s`" % q.permission_name) self.stdout.write(
" .. generated `permission_name=%s`" % q.permission_name)
q.save() q.save()
self.stdout.write(" .. checking permission codename `%s`" % basename) self.stdout.write(
" .. checking permission codename `%s`" % basename)
try: try:
Permission.objects.create( Permission.objects.create(

View File

@ -62,7 +62,8 @@ class Command(BaseCommand):
def escalate_tickets(queues, verbose): def escalate_tickets(queues, verbose):
""" Only include queues with escalation configured """ """ Only include queues with escalation configured """
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0) queryset = Queue.objects.filter(
escalate_days__isnull=False).exclude(escalate_days=0)
if queues: if queues:
queryset = queryset.filter(slug__in=queues) queryset = queryset.filter(slug__in=queues)
@ -143,7 +144,8 @@ def usage():
if __name__ == '__main__': if __name__ == '__main__':
try: try:
opts, args = getopt.getopt(sys.argv[1:], ['queues=', 'verboseescalation']) opts, args = getopt.getopt(
sys.argv[1:], ['queues=', 'verboseescalation'])
except getopt.GetoptError: except getopt.GetoptError:
usage() usage()
sys.exit(2) sys.exit(2)

View File

@ -128,7 +128,8 @@ class Queue(models.Model):
_('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 form?'), help_text=_(
'Should this queue be listed on the public submission form?'),
) )
allow_email_submission = models.BooleanField( allow_email_submission = models.BooleanField(
@ -180,7 +181,8 @@ class Queue(models.Model):
email_box_type = models.CharField( email_box_type = models.CharField(
_('E-Mail Box Type'), _('E-Mail Box Type'),
max_length=5, max_length=5,
choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), ('local', _('Local Directory'))), choices=(('pop3', _('POP 3')), ('imap', _('IMAP')),
('local', _('Local Directory'))),
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 '
@ -262,7 +264,8 @@ class Queue(models.Model):
email_box_interval = models.IntegerField( email_box_interval = models.IntegerField(
_('E-Mail Check Interval'), _('E-Mail Check Interval'),
help_text=_('How often do you wish to check this mailbox? (in Minutes)'), help_text=_(
'How often do you wish to check this mailbox? (in Minutes)'),
blank=True, blank=True,
null=True, null=True,
default='5', default='5',
@ -281,7 +284,8 @@ class Queue(models.Model):
choices=(('socks4', _('SOCKS4')), ('socks5', _('SOCKS5'))), choices=(('socks4', _('SOCKS4')), ('socks5', _('SOCKS5'))),
blank=True, blank=True,
null=True, null=True,
help_text=_('SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'), help_text=_(
'SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'),
) )
socks_proxy_host = models.GenericIPAddressField( socks_proxy_host = models.GenericIPAddressField(
@ -295,7 +299,8 @@ class Queue(models.Model):
_('Socks Proxy Port'), _('Socks Proxy Port'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Socks proxy port number. Default: 9150 (default TOR port)'), help_text=_(
'Socks proxy port number. Default: 9150 (default TOR port)'),
) )
logging_type = models.CharField( logging_type = models.CharField(
@ -356,7 +361,8 @@ class Queue(models.Model):
""" """
if not self.email_address: if not self.email_address:
# must check if given in format "Foo <foo@example.com>" # must check if given in format "Foo <foo@example.com>"
default_email = re.match(".*<(?P<email>.*@*.)>", settings.DEFAULT_FROM_EMAIL) default_email = re.match(
".*<(?P<email>.*@*.)>", settings.DEFAULT_FROM_EMAIL)
if default_email is not None: if default_email is not None:
# already in the right format, so just include it here # already in the right format, so just include it here
return u'NO QUEUE EMAIL ADDRESS DEFINED %s' % settings.DEFAULT_FROM_EMAIL return u'NO QUEUE EMAIL ADDRESS DEFINED %s' % settings.DEFAULT_FROM_EMAIL
@ -532,7 +538,8 @@ 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 escalated.'), help_text=_(
'If a ticket is on hold, it will not automatically be escalated.'),
) )
description = models.TextField( description = models.TextField(
@ -582,7 +589,8 @@ class Ticket(models.Model):
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('Knowledge base item the user was viewing when they created this ticket.'), verbose_name=_(
'Knowledge base item the user was viewing when they created this ticket.'),
) )
merged_to = models.ForeignKey( merged_to = models.ForeignKey(
@ -648,7 +656,8 @@ class Ticket(models.Model):
def send(role, recipient): def send(role, recipient):
if recipient and recipient not in recipients and role in roles: if recipient and recipient not in recipients and role in roles:
template, context = roles[role] template, context = roles[role]
send_templated_mail(template, context, recipient, sender=self.queue.from_address, **kwargs) send_templated_mail(
template, context, recipient, sender=self.queue.from_address, **kwargs)
recipients.add(recipient) recipients.add(recipient)
send('submitter', self.submitter_email) send('submitter', self.submitter_email)
@ -844,7 +853,8 @@ class Ticket(models.Model):
# Ignore if user has no email address # Ignore if user has no email address
return return
elif not email: elif not email:
raise ValueError('You must provide at least one parameter to get the email from') raise ValueError(
'You must provide at least one parameter to get the email from')
# Prepare all emails already into the ticket # Prepare all emails already into the ticket
ticket_emails = [x.display for x in self.ticketcc_set.all()] ticket_emails = [x.display for x in self.ticketcc_set.all()]
@ -1280,7 +1290,8 @@ class EmailTemplate(models.Model):
html = models.TextField( html = models.TextField(
_('HTML'), _('HTML'),
help_text=_('The same context is available here as in plain_text, above.'), help_text=_(
'The same context is available here as in plain_text, above.'),
) )
locale = models.CharField( locale = models.CharField(
@ -1329,7 +1340,8 @@ class KBCategory(models.Model):
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('Default queue when creating a ticket after viewing this category.'), verbose_name=_(
'Default queue when creating a ticket after viewing this category.'),
) )
public = models.BooleanField( public = models.BooleanField(
@ -1396,7 +1408,8 @@ 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 changed.'), help_text=_(
'The date on which this question was most recently changed.'),
blank=True, blank=True,
) )
@ -1555,7 +1568,8 @@ class UserSettings(models.Model):
login_view_ticketlist = models.BooleanField( login_view_ticketlist = models.BooleanField(
verbose_name=_('Show Ticket List on Login?'), verbose_name=_('Show Ticket List on Login?'),
help_text=_('Display the ticket list upon login? Otherwise, the dashboard is shown.'), help_text=_(
'Display the ticket list upon login? Otherwise, the dashboard is shown.'),
default=login_view_ticketlist_default, default=login_view_ticketlist_default,
) )
@ -1570,13 +1584,15 @@ class UserSettings(models.Model):
email_on_ticket_assign = models.BooleanField( email_on_ticket_assign = models.BooleanField(
verbose_name=_('E-mail me when assigned a ticket?'), verbose_name=_('E-mail me when assigned a ticket?'),
help_text=_('If you are assigned a ticket via the web, do you want to receive an e-mail?'), help_text=_(
'If you are assigned a ticket via the web, do you want to receive an e-mail?'),
default=email_on_ticket_assign_default, default=email_on_ticket_assign_default,
) )
tickets_per_page = models.IntegerField( tickets_per_page = models.IntegerField(
verbose_name=_('Number of tickets to show per page'), verbose_name=_('Number of tickets to show per page'),
help_text=_('How many tickets do you want to see on the Ticket List page?'), help_text=_(
'How many tickets do you want to see on the Ticket List page?'),
default=tickets_per_page_default, default=tickets_per_page_default,
choices=PAGE_SIZES, choices=PAGE_SIZES,
) )
@ -1611,7 +1627,8 @@ def create_usersettings(sender, instance, created, **kwargs):
UserSettings.objects.create(user=instance) UserSettings.objects.create(user=instance)
models.signals.post_save.connect(create_usersettings, sender=settings.AUTH_USER_MODEL) models.signals.post_save.connect(
create_usersettings, sender=settings.AUTH_USER_MODEL)
class IgnoreEmail(models.Model): class IgnoreEmail(models.Model):
@ -1851,14 +1868,16 @@ class CustomField(models.Model):
ordering = models.IntegerField( ordering = models.IntegerField(
_('Ordering'), _('Ordering'),
help_text=_('Lower numbers are displayed first; higher numbers are listed later'), help_text=_(
'Lower numbers are displayed first; higher numbers are listed later'),
blank=True, blank=True,
null=True, null=True,
) )
def _choices_as_array(self): def _choices_as_array(self):
valuebuffer = StringIO(self.list_values) valuebuffer = StringIO(self.list_values)
choices = [[item.strip(), item.strip()] for item in valuebuffer.readlines()] choices = [[item.strip(), item.strip()]
for item in valuebuffer.readlines()]
valuebuffer.close() valuebuffer.close()
return choices return choices
choices_as_array = property(_choices_as_array) choices_as_array = property(_choices_as_array)
@ -1912,10 +1931,10 @@ class CustomField(models.Model):
# Prepare attributes for each types # Prepare attributes for each types
attributes = { attributes = {
'label': self.label, 'label': self.label,
'help_text': self.help_text, 'help_text': self.help_text,
'required': self.required, 'required': self.required,
} }
if self.data_type in ('varchar', 'text'): if self.data_type in ('varchar', 'text'):
attributes['max_length'] = self.max_length attributes['max_length'] = self.max_length
if self.data_type == 'text': if self.data_type == 'text':

View File

@ -103,8 +103,10 @@ def get_query_class():
class __Query__: class __Query__:
def __init__(self, huser, base64query=None, query_params=None): def __init__(self, huser, base64query=None, query_params=None):
self.huser = huser self.huser = huser
self.params = query_params if query_params else query_from_base64(base64query) self.params = query_params if query_params else query_from_base64(
self.base64 = base64query if base64query else query_to_base64(query_params) base64query)
self.base64 = base64query if base64query else query_to_base64(
query_params)
self.result = None self.result = None
def get_search_filter_args(self): def get_search_filter_args(self):
@ -128,7 +130,8 @@ class __Query__:
""" """
filter = self.params.get('filtering', {}) filter = self.params.get('filtering', {})
filter_or = self.params.get('filtering_or', {}) filter_or = self.params.get('filtering_or', {})
queryset = queryset.filter((Q(**filter) | Q(**filter_or)) & self.get_search_filter_args()) queryset = queryset.filter(
(Q(**filter) | Q(**filter_or)) & self.get_search_filter_args())
sorting = self.params.get('sorting', None) sorting = self.params.get('sorting', None)
if sorting: if sorting:
sortreverse = self.params.get('sortreverse', None) sortreverse = self.params.get('sortreverse', None)
@ -191,11 +194,13 @@ class __Query__:
'text': { 'text': {
'headline': ticket.title + ' - ' + followup.title, 'headline': ticket.title + ' - ' + followup.title,
'text': ( 'text': (
(escape(followup.comment) if followup.comment else _('No text')) (escape(followup.comment)
if followup.comment else _('No text'))
+ +
'<br/> <a href="%s" class="btn" role="button">%s</a>' '<br/> <a href="%s" class="btn" role="button">%s</a>'
% %
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")) (reverse('helpdesk:view', kwargs={
'ticket_id': ticket.pk}), _("View ticket"))
), ),
}, },
'group': _('Messages'), 'group': _('Messages'),

View File

@ -46,13 +46,13 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
# Enable the Dependencies field on ticket view # Enable the Dependencies field on ticket view
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings, HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings,
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET', 'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
True) True)
# Enable the Time spent on field on ticket view # Enable the Time spent on field on ticket view
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings, HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings,
'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET', 'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET',
True) True)
# raises a 404 to anon users. It's like it was invisible # raises a 404 to anon users. It's like it was invisible
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings, HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
@ -63,10 +63,13 @@ HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True) HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
# Disable Timeline on ticket list # Disable Timeline on ticket list
HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True) HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(
settings, 'HELPDESK_TICKETS_TIMELINE_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
HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False) # status?
HELPDESK_NAVIGATION_ENABLED = getattr(
settings, 'HELPDESK_NAVIGATION_ENABLED', False)
# use public CDNs to serve jquery and other javascript by default? # use public CDNs to serve jquery and other javascript by default?
# otherwise, use built-in static copy # otherwise, use built-in static copy
@ -84,7 +87,8 @@ HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings,
["en", "de", "es", "fr", "it", "ru"]) ["en", "de", "es", "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)
# allow user to override default layout for 'followups' - work in progress. # allow user to override default layout for 'followups' - work in progress.
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False) HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
@ -96,17 +100,19 @@ HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
# URL schemes that are allowed within links # URL schemes that are allowed within links
ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', ( ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', (
'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
)) ))
############################ ############################
# 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)
# show 'submit a ticket' section on public page? # show 'submit a ticket' section on public page?
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)
# change that to custom class to have extra fields or validation (like captcha) # change that to custom class to have extra fields or validation (like captcha)
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr( HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
@ -137,8 +143,10 @@ CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M"
''' 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?
# can be True/False or a callable accepting the active user and returning True if they must be considered helpdesk staff # can be True/False or a callable accepting the active user and returning
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False) # True if they must be considered helpdesk staff
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(
settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)): if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)):
warnings.warn( warnings.warn(
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.", "HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
@ -154,14 +162,18 @@ HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr( HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False) 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
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False) # public' checkbox
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(
settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
# only show staff users in ticket owner drop-downs # only show staff users in ticket owner drop-downs
HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False) HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(
settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False)
# 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( HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
@ -173,11 +185,13 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
raise ImproperlyConfigured raise ImproperlyConfigured
# 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')
# default maximum email attachment size, in bytes # default maximum email attachment size, in bytes
# only attachments smaller than this size will be sent via email # only attachments smaller than this size will be sent via email
HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
######################################## ########################################
@ -189,7 +203,8 @@ HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False) settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
# Activate the API endpoint to manage tickets thanks to Django REST Framework # Activate the API endpoint to manage tickets thanks to Django REST Framework
HELPDESK_ACTIVATE_API_ENDPOINT = getattr(settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False) HELPDESK_ACTIVATE_API_ENDPOINT = getattr(
settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False)
################# #################
@ -204,25 +219,32 @@ 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 process emails with a valid tracking ID? (throws away all other mail) # only process emails with a valid tracking ID? (throws away all other mail)
QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False) QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(
settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False)
# 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( HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False) settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)
# use https in the email links # use https in the email links
HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False) HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(
settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False)
HELPDESK_TEAMS_MODEL = getattr(settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team') HELPDESK_TEAMS_MODEL = getattr(
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [('pinax_teams', '0004_auto_20170511_0856')]) settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team')
HELPDESK_KBITEM_TEAM_GETTER = getattr(settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team) HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [
('pinax_teams', '0004_auto_20170511_0856')])
HELPDESK_KBITEM_TEAM_GETTER = getattr(
settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team)
# Include all signatures and forwards in the first ticket message if set # Include all signatures and forwards in the first ticket message if set
# Useful if you get forwards dropped from them while they are useful part of request # Useful if you get forwards dropped from them while they are useful part
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False) # of request
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(
settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
# If set then we always save incoming emails as .eml attachments # If set then we always save incoming emails as .eml attachments
# which is quite noisy but very helpful for complicated markup, forwards and so on # which is quite noisy but very helpful for complicated markup, forwards and so on
# (which gets stripped/corrupted otherwise) # (which gets stripped/corrupted otherwise)
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False) HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(
settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False)

View File

@ -58,12 +58,15 @@ def send_templated_mail(template_name,
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
try: try:
t = EmailTemplate.objects.get(template_name__iexact=template_name, locale=locale) t = EmailTemplate.objects.get(
template_name__iexact=template_name, locale=locale)
except EmailTemplate.DoesNotExist: except EmailTemplate.DoesNotExist:
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', template_name) logger.warning(
'template "%s" does not exist, no mail sent', template_name)
return # just ignore if template doesn't exist return # just ignore if template doesn't exist
subject_part = from_string( subject_part = from_string(
@ -77,10 +80,12 @@ def send_templated_mail(template_name,
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file) "%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
).render(context) ).render(context)
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
if 'comment' in context: if 'comment' in context:
context['comment'] = mark_safe(context['comment'].replace('\r\n', '<br>')) context['comment'] = mark_safe(
context['comment'].replace('\r\n', '<br>'))
html_part = from_string( html_part = from_string(
"{%% extends '%s' %%}" "{%% extends '%s' %%}"
@ -112,7 +117,8 @@ def send_templated_mail(template_name,
try: try:
return msg.send() return msg.send()
except SMTPException as e: except SMTPException as e:
logger.exception('SMTPException raised while sending email to {}'.format(recipients)) logger.exception(
'SMTPException raised while sending email to {}'.format(recipients))
if not fail_silently: if not fail_silently:
raise e raise e
return 0 return 0

View File

@ -19,4 +19,5 @@ def helpdesk_staff(user):
try: try:
return is_helpdesk_staff(user) return is_helpdesk_staff(user)
except Exception: except Exception:
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed") logger.exception(
"'helpdesk_staff' template tag (django-helpdesk) crashed")

View File

@ -22,13 +22,16 @@ def datetime_string_format(value):
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats :return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
""" """
try: try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT) new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
except (TypeError, ValueError): except (TypeError, ValueError):
try: try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT) new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
except (TypeError, ValueError): except (TypeError, ValueError):
try: try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT) new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
except (TypeError, ValueError): except (TypeError, ValueError):
# If NoneType return empty string, else return original value # If NoneType return empty string, else return original value
new_value = "" if value is None else value new_value = "" if value is None else value

View File

@ -69,23 +69,28 @@ class TicketTest(APITestCase):
self.assertEqual(response.status_code, HTTP_201_CREATED) self.assertEqual(response.status_code, HTTP_201_CREATED)
created_ticket = Ticket.objects.get() created_ticket = Ticket.objects.get()
self.assertEqual(created_ticket.title, 'Test title') self.assertEqual(created_ticket.title, 'Test title')
self.assertEqual(created_ticket.description, 'Test description\nMulti lines') self.assertEqual(created_ticket.description,
'Test description\nMulti lines')
self.assertEqual(created_ticket.submitter_email, 'test@mail.com') self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 4) self.assertEqual(created_ticket.priority, 4)
def test_create_api_ticket_with_basic_auth(self): def test_create_api_ticket_with_basic_auth(self):
username = 'admin' username = 'admin'
password = 'admin' password = 'admin'
User.objects.create_user(username=username, password=password, is_staff=True) User.objects.create_user(
username=username, password=password, is_staff=True)
test_user = User.objects.create_user(username='test') test_user = User.objects.create_user(username='test')
merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket') merge_ticket = Ticket.objects.create(
queue=self.queue, title='merge ticket')
# Generate base64 credentials string # Generate base64 credentials string
credentials = f"{username}:{password}" credentials = f"{username}:{password}"
base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) base64_credentials = base64.b64encode(credentials.encode(
HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}") self.client.credentials(
HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
response = self.client.post( response = self.client.post(
'/api/tickets/', '/api/tickets/',
{ {
@ -107,21 +112,27 @@ class TicketTest(APITestCase):
created_ticket = Ticket.objects.last() created_ticket = Ticket.objects.last()
self.assertEqual(created_ticket.title, 'Title') self.assertEqual(created_ticket.title, 'Title')
self.assertEqual(created_ticket.description, 'Description') self.assertEqual(created_ticket.description, 'Description')
self.assertIsNone(created_ticket.resolution) # resolution can not be set on creation # resolution can not be set on creation
self.assertIsNone(created_ticket.resolution)
self.assertEqual(created_ticket.assigned_to, test_user) self.assertEqual(created_ticket.assigned_to, test_user)
self.assertEqual(created_ticket.submitter_email, 'test@mail.com') self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 1) self.assertEqual(created_ticket.priority, 1)
self.assertFalse(created_ticket.on_hold) # on_hold is False on creation # on_hold is False on creation
self.assertEqual(created_ticket.status, Ticket.OPEN_STATUS) # status is always open on creation self.assertFalse(created_ticket.on_hold)
# status is always open on creation
self.assertEqual(created_ticket.status, Ticket.OPEN_STATUS)
self.assertEqual(created_ticket.due_date, self.due_date) self.assertEqual(created_ticket.due_date, self.due_date)
self.assertIsNone(created_ticket.merged_to) # merged_to can not be set on creation # merged_to can not be set on creation
self.assertIsNone(created_ticket.merged_to)
def test_edit_api_ticket(self): def test_edit_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') test_ticket = Ticket.objects.create(
queue=self.queue, title='Test ticket')
test_user = User.objects.create_user(username='test') test_user = User.objects.create_user(username='test')
merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket') merge_ticket = Ticket.objects.create(
queue=self.queue, title='merge ticket')
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
response = self.client.put( response = self.client.put(
@ -156,7 +167,8 @@ class TicketTest(APITestCase):
def test_partial_edit_api_ticket(self): def test_partial_edit_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') test_ticket = Ticket.objects.create(
queue=self.queue, title='Test ticket')
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
response = self.client.patch( response = self.client.patch(
@ -172,7 +184,8 @@ class TicketTest(APITestCase):
def test_delete_api_ticket(self): def test_delete_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') test_ticket = Ticket.objects.create(
queue=self.queue, title='Test ticket')
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
response = self.client.delete('/api/tickets/%d/' % test_ticket.id) response = self.client.delete('/api/tickets/%d/' % test_ticket.id)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
@ -195,7 +208,8 @@ class TicketTest(APITestCase):
Blue Blue
Red Red
Yellow''' Yellow'''
CustomField.objects.create(name=field_type, label=field_display, data_type=field_type, **extra_data) CustomField.objects.create(
name=field_type, label=field_display, data_type=field_type, **extra_data)
staff_user = User.objects.create_user(username='test', is_staff=True) staff_user = User.objects.create_user(username='test', is_staff=True)
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
@ -209,7 +223,8 @@ class TicketTest(APITestCase):
'priority': 4 'priority': 4
}) })
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'custom_integer': [ErrorDetail(string='This field is required.', code='required')]}) self.assertEqual(response.data, {'custom_integer': [ErrorDetail(
string='This field is required.', code='required')]})
# Test creation with custom field values # Test creation with custom field values
response = self.client.post('/api/tickets/', { response = self.client.post('/api/tickets/', {
@ -261,4 +276,3 @@ class TicketTest(APITestCase):
'custom_ipaddress': '127.0.0.1', 'custom_ipaddress': '127.0.0.1',
'custom_slug': 'test-slug' 'custom_slug': 'test-slug'
}) })

View File

@ -47,7 +47,8 @@ class AttachmentIntegrationTests(TestCase):
} }
def test_create_pub_ticket_with_attachment(self): def test_create_pub_ticket_with_attachment(self):
test_file = SimpleUploadedFile('test_att.txt', b'attached file content', 'text/plain') test_file = SimpleUploadedFile(
'test_att.txt', b'attached file content', 'text/plain')
post_data = self.ticket_data.copy() post_data = self.ticket_data.copy()
post_data.update({ post_data.update({
'queue': self.queue_public.id, 'queue': self.queue_public.id,
@ -55,17 +56,20 @@ class AttachmentIntegrationTests(TestCase):
}) })
# Ensure ticket form submits with attachment successfully # Ensure ticket form submits with attachment successfully
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) response = self.client.post(
reverse('helpdesk:home'), post_data, follow=True)
self.assertContains(response, test_file.name) self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content # Ensure attachment is available with correct content
att = models.FollowUpAttachment.objects.get(followup__ticket=response.context['ticket']) att = models.FollowUpAttachment.objects.get(
followup__ticket=response.context['ticket'])
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk: with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = file_on_disk.read() disk_content = file_on_disk.read()
self.assertEqual(disk_content, 'attached file content') self.assertEqual(disk_content, 'attached file content')
def test_create_pub_ticket_with_attachment_utf8(self): def test_create_pub_ticket_with_attachment_utf8(self):
test_file = SimpleUploadedFile('ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8') test_file = SimpleUploadedFile(
'ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8')
post_data = self.ticket_data.copy() post_data = self.ticket_data.copy()
post_data.update({ post_data.update({
'queue': self.queue_public.id, 'queue': self.queue_public.id,
@ -73,11 +77,13 @@ class AttachmentIntegrationTests(TestCase):
}) })
# Ensure ticket form submits with attachment successfully # Ensure ticket form submits with attachment successfully
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) response = self.client.post(
reverse('helpdesk:home'), post_data, follow=True)
self.assertContains(response, test_file.name) self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content # Ensure attachment is available with correct content
att = models.FollowUpAttachment.objects.get(followup__ticket=response.context['ticket']) att = models.FollowUpAttachment.objects.get(
followup__ticket=response.context['ticket'])
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk: with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = smart_str(file_on_disk.read(), 'utf-8') disk_content = smart_str(file_on_disk.read(), 'utf-8')
self.assertEqual(disk_content, 'โจ') self.assertEqual(disk_content, 'โจ')
@ -105,7 +111,8 @@ class AttachmentUnitTests(TestCase):
@skip("Rework with model relocation") @skip("Rework with model relocation")
def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" check utf-8 data is parsed correctly """ """ check utf-8 data is parsed correctly """
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0] filename, fileobj = lib.process_attachments(
self.follow_up, [self.test_file])[0]
mock_att_save.assert_called_with( mock_att_save.assert_called_with(
file=self.test_file, file=self.test_file,
filename=self.file_attrs['filename'], filename=self.file_attrs['filename'],
@ -154,8 +161,10 @@ class AttachmentUnitTests(TestCase):
@override_settings(MEDIA_ROOT=MEDIA_DIR) @override_settings(MEDIA_ROOT=MEDIA_DIR)
def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" don't mock saving to filesystem to test file renames caused by storage layer """ """ don't mock saving to filesystem to test file renames caused by storage layer """
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0] filename, fileobj = lib.process_attachments(
# Attachment object was zeroth positional arg (i.e. self) of att.save call self.follow_up, [self.test_file])[0]
# Attachment object was zeroth positional arg (i.e. self) of att.save
# call
attachment_obj = mock_att_save.return_value attachment_obj = mock_att_save.return_value
mock_att_save.assert_called_once_with(attachment_obj) mock_att_save.assert_called_once_with(attachment_obj)

View File

@ -37,7 +37,8 @@ class GetEmailCommonTests(TestCase):
# tests correct syntax for command line option # tests correct syntax for command line option
def test_get_email_quiet_option(self): def test_get_email_quiet_option(self):
"""Test quiet option is properly propagated""" """Test quiet option is properly propagated"""
# Test get_email with quiet set to True and also False, and verify handle receives quiet option set properly # Test get_email with quiet set to True and also False, and verify
# handle receives quiet option set properly
for quiet_test_value in [True, False]: for quiet_test_value in [True, False]:
with mock.patch.object(Command, 'handle', return_value=None) as mocked_handle: with mock.patch.object(Command, 'handle', return_value=None) as mocked_handle:
call_command('get_email', quiet=quiet_test_value) call_command('get_email', quiet=quiet_test_value)
@ -52,7 +53,8 @@ class GetEmailCommonTests(TestCase):
""" """
with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml")) as fd: with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml")) as fd:
test_email = fd.read() test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) ticket = helpdesk.email.object_from_message(
test_email, self.queue_public, self.logger)
# title got truncated because of max_lengh of the model.title field # title got truncated because of max_lengh of the model.title field
assert ticket.title == ( assert ticket.title == (
@ -68,16 +70,19 @@ class GetEmailCommonTests(TestCase):
""" """
with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml")) as fd: with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml")) as fd:
test_email = fd.read() test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) ticket = helpdesk.email.object_from_message(
test_email, self.queue_public, self.logger)
self.assertEqual(ticket.title, "Český test") self.assertEqual(ticket.title, "Český test")
self.assertEqual(ticket.description, "Tohle je test českých písmen odeslaných z gmailu.") self.assertEqual(ticket.description,
"Tohle je test českých písmen odeslaných z gmailu.")
followups = FollowUp.objects.filter(ticket=ticket) followups = FollowUp.objects.filter(ticket=ticket)
self.assertEqual(len(followups), 1) self.assertEqual(len(followups), 1)
followup = followups[0] followup = followups[0]
attachments = FollowUpAttachment.objects.filter(followup=followup) attachments = FollowUpAttachment.objects.filter(followup=followup)
self.assertEqual(len(attachments), 1) self.assertEqual(len(attachments), 1)
attachment = attachments[0] attachment = attachments[0]
self.assertIn('<div dir="ltr">Tohle je test českých písmen odeslaných z gmailu.</div>\n', attachment.file.read().decode("utf-8")) self.assertIn('<div dir="ltr">Tohle je test českých písmen odeslaných z gmailu.</div>\n',
attachment.file.read().decode("utf-8"))
def test_email_with_8bit_encoding_and_utf_8(self): def test_email_with_8bit_encoding_and_utf_8(self):
""" """
@ -86,7 +91,8 @@ class GetEmailCommonTests(TestCase):
""" """
with open(os.path.join(THIS_DIR, "test_files/all-special-chars.eml")) as fd: with open(os.path.join(THIS_DIR, "test_files/all-special-chars.eml")) as fd:
test_email = fd.read() test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) ticket = helpdesk.email.object_from_message(
test_email, self.queue_public, self.logger)
self.assertEqual(ticket.title, "Testovácí email") self.assertEqual(ticket.title, "Testovácí email")
self.assertEqual(ticket.description, "íářčšáíéřášč") self.assertEqual(ticket.description, "íářčšáíéřášč")
@ -98,14 +104,17 @@ class GetEmailCommonTests(TestCase):
""" """
with open(os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml")) as fd: with open(os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml")) as fd:
test_email = fd.read() test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) ticket = helpdesk.email.object_from_message(
self.assertEqual(ticket.title, "Fwd: Cyklozaměstnavatel - změna vyhodnocení") test_email, self.queue_public, self.logger)
self.assertEqual(
ticket.title, "Fwd: Cyklozaměstnavatel - změna vyhodnocení")
self.assertIn("prosazuje lepší", ticket.description) self.assertIn("prosazuje lepší", ticket.description)
followups = FollowUp.objects.filter(ticket=ticket) followups = FollowUp.objects.filter(ticket=ticket)
followup = followups[0] followup = followups[0]
attachments = FollowUpAttachment.objects.filter(followup=followup) attachments = FollowUpAttachment.objects.filter(followup=followup)
attachment = attachments[0] attachment = attachments[0]
self.assertIn('prosazuje lepší', attachment.file.read().decode("utf-8")) self.assertIn('prosazuje lepší',
attachment.file.read().decode("utf-8"))
@override_settings(HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL=True) @override_settings(HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL=True)
def test_email_with_forwarded_message(self): def test_email_with_forwarded_message(self):
@ -114,12 +123,15 @@ class GetEmailCommonTests(TestCase):
""" """
with open(os.path.join(THIS_DIR, "test_files/forwarded-message.eml")) as fd: with open(os.path.join(THIS_DIR, "test_files/forwarded-message.eml")) as fd:
test_email = fd.read() test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) ticket = helpdesk.email.object_from_message(
self.assertEqual(ticket.title, "Test with original message from GitHub") test_email, self.queue_public, self.logger)
self.assertEqual(
ticket.title, "Test with original message from GitHub")
self.assertIn("This is email body", ticket.description) self.assertIn("This is email body", ticket.description)
assert "Hello there!" not in ticket.description, ticket.description assert "Hello there!" not in ticket.description, ticket.description
assert FollowUp.objects.filter(ticket=ticket).count() == 1 assert FollowUp.objects.filter(ticket=ticket).count() == 1
assert "Hello there!" in FollowUp.objects.filter(ticket=ticket).first().comment assert "Hello there!" in FollowUp.objects.filter(
ticket=ticket).first().comment
class GetEmailParametricTemplate(object): class GetEmailParametricTemplate(object):
@ -160,11 +172,13 @@ class GetEmailParametricTemplate(object):
For each email source supported, we mock the backend to provide For each email source supported, we mock the backend to provide
authentically formatted responses containing our test data.""" authentically formatted responses containing our test data."""
# example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/ # example email text from Django docs:
# https://docs.djangoproject.com/en/1.10/ref/unicode/
test_email_from = "Arnbjörg Ráðormsdóttir <arnbjorg@example.com>" test_email_from = "Arnbjörg Ráðormsdóttir <arnbjorg@example.com>"
test_email_subject = "My visit to Sør-Trøndelag" test_email_subject = "My visit to Sør-Trøndelag"
test_email_body = "Unicode helpdesk comment with an s-hat (ŝ) via email." test_email_body = "Unicode helpdesk comment with an s-hat (ŝ) via email."
test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \
"\nSubject: " + test_email_subject + "\n\n" + test_email_body
test_mail_len = len(test_email) test_mail_len = len(test_email)
if self.socks: if self.socks:
@ -184,38 +198,51 @@ class GetEmailParametricTemplate(object):
call_command('get_email') call_command('get_email')
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') mocked_listdir.assert_called_with(
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') '/var/lib/mail/helpdesk/')
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2') mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename1')
mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename2')
elif self.method == 'pop3': elif self.method == 'pop3':
# mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 # mock poplib.POP3's list and retr methods to provide responses
# as per RFC 1939
pop3_emails = { pop3_emails = {
'1': ("+OK", test_email.split('\n')), '1': ("+OK", test_email.split('\n')),
'2': ("+OK", test_email.split('\n')), '2': ("+OK", test_email.split('\n')),
} }
pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len)) pop3_mail_list = ("+OK 2 messages", ("1 %d" %
test_mail_len, "2 %d" % test_mail_len))
mocked_poplib_server = mock.Mock() mocked_poplib_server = mock.Mock()
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) mocked_poplib_server.list = mock.Mock(
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x]) return_value=pop3_mail_list)
mocked_poplib_server.retr = mock.Mock(
side_effect=lambda x: pop3_emails[x])
with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib:
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) mocked_poplib.POP3 = mock.Mock(
return_value=mocked_poplib_server)
call_command('get_email') call_command('get_email')
elif self.method == 'imap': elif self.method == 'imap':
# mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 # mock imaplib.IMAP4's search and fetch methods with responses
# from RFC 3501
imap_emails = { imap_emails = {
"1": ("OK", (("1", test_email),)), "1": ("OK", (("1", test_email),)),
"2": ("OK", (("2", test_email),)), "2": ("OK", (("2", test_email),)),
} }
imap_mail_list = ("OK", ("1 2",)) imap_mail_list = ("OK", ("1 2",))
mocked_imaplib_server = mock.Mock() mocked_imaplib_server = mock.Mock()
mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) mocked_imaplib_server.search = mock.Mock(
return_value=imap_mail_list)
# we ignore the second arg as the data item/mime-part is constant (RFC822) # we ignore the second arg as the data item/mime-part is
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) # constant (RFC822)
mocked_imaplib_server.fetch = mock.Mock(
side_effect=lambda x, _: imap_emails[x])
with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib:
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) mocked_imaplib.IMAP4 = mock.Mock(
return_value=mocked_imaplib_server)
call_command('get_email') call_command('get_email')
ticket1 = get_object_or_404(Ticket, pk=1) ticket1 = get_object_or_404(Ticket, pk=1)
@ -232,11 +259,13 @@ class GetEmailParametricTemplate(object):
"""Tests correctly decoding mail headers when a comma is encoded into """Tests correctly decoding mail headers when a comma is encoded into
UTF-8. See bug report #832.""" UTF-8. See bug report #832."""
# example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/ # example email text from Django docs:
# https://docs.djangoproject.com/en/1.10/ref/unicode/
test_email_from = "Bernard-Bouissières, Benjamin <bbb@example.com>" test_email_from = "Bernard-Bouissières, Benjamin <bbb@example.com>"
test_email_subject = "Commas in From lines" test_email_subject = "Commas in From lines"
test_email_body = "Testing commas in from email UTF-8." test_email_body = "Testing commas in from email UTF-8."
test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \
"\nSubject: " + test_email_subject + "\n\n" + test_email_body
test_mail_len = len(test_email) test_mail_len = len(test_email)
if self.socks: if self.socks:
@ -256,38 +285,51 @@ class GetEmailParametricTemplate(object):
call_command('get_email') call_command('get_email')
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') mocked_listdir.assert_called_with(
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') '/var/lib/mail/helpdesk/')
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2') mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename1')
mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename2')
elif self.method == 'pop3': elif self.method == 'pop3':
# mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 # mock poplib.POP3's list and retr methods to provide responses
# as per RFC 1939
pop3_emails = { pop3_emails = {
'1': ("+OK", test_email.split('\n')), '1': ("+OK", test_email.split('\n')),
'2': ("+OK", test_email.split('\n')), '2': ("+OK", test_email.split('\n')),
} }
pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len)) pop3_mail_list = ("+OK 2 messages", ("1 %d" %
test_mail_len, "2 %d" % test_mail_len))
mocked_poplib_server = mock.Mock() mocked_poplib_server = mock.Mock()
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) mocked_poplib_server.list = mock.Mock(
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x]) return_value=pop3_mail_list)
mocked_poplib_server.retr = mock.Mock(
side_effect=lambda x: pop3_emails[x])
with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib:
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) mocked_poplib.POP3 = mock.Mock(
return_value=mocked_poplib_server)
call_command('get_email') call_command('get_email')
elif self.method == 'imap': elif self.method == 'imap':
# mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 # mock imaplib.IMAP4's search and fetch methods with responses
# from RFC 3501
imap_emails = { imap_emails = {
"1": ("OK", (("1", test_email),)), "1": ("OK", (("1", test_email),)),
"2": ("OK", (("2", test_email),)), "2": ("OK", (("2", test_email),)),
} }
imap_mail_list = ("OK", ("1 2",)) imap_mail_list = ("OK", ("1 2",))
mocked_imaplib_server = mock.Mock() mocked_imaplib_server = mock.Mock()
mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) mocked_imaplib_server.search = mock.Mock(
return_value=imap_mail_list)
# we ignore the second arg as the data item/mime-part is constant (RFC822) # we ignore the second arg as the data item/mime-part is
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) # constant (RFC822)
mocked_imaplib_server.fetch = mock.Mock(
side_effect=lambda x, _: imap_emails[x])
with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib:
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) mocked_imaplib.IMAP4 = mock.Mock(
return_value=mocked_imaplib_server)
call_command('get_email') call_command('get_email')
ticket1 = get_object_or_404(Ticket, pk=1) ticket1 = get_object_or_404(Ticket, pk=1)
@ -308,11 +350,13 @@ class GetEmailParametricTemplate(object):
For each email source supported, we mock the backend to provide For each email source supported, we mock the backend to provide
authentically formatted responses containing our test data.""" authentically formatted responses containing our test data."""
# example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/ # example email text from Django docs:
# https://docs.djangoproject.com/en/1.10/ref/unicode/
test_email_from = "Arnbjörg Ráðormsdóttir <arnbjorg@example.com>" test_email_from = "Arnbjörg Ráðormsdóttir <arnbjorg@example.com>"
test_email_subject = "My visit to Sør-Trøndelag" test_email_subject = "My visit to Sør-Trøndelag"
test_email_body = "Reporting some issue with the template tag: {% if helpdesk %}." test_email_body = "Reporting some issue with the template tag: {% if helpdesk %}."
test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \
"\nSubject: " + test_email_subject + "\n\n" + test_email_body
test_mail_len = len(test_email) test_mail_len = len(test_email)
if self.socks: if self.socks:
@ -332,38 +376,51 @@ class GetEmailParametricTemplate(object):
call_command('get_email') call_command('get_email')
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') mocked_listdir.assert_called_with(
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') '/var/lib/mail/helpdesk/')
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2') mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename1')
mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename2')
elif self.method == 'pop3': elif self.method == 'pop3':
# mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 # mock poplib.POP3's list and retr methods to provide responses
# as per RFC 1939
pop3_emails = { pop3_emails = {
'1': ("+OK", test_email.split('\n')), '1': ("+OK", test_email.split('\n')),
'2': ("+OK", test_email.split('\n')), '2': ("+OK", test_email.split('\n')),
} }
pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len)) pop3_mail_list = ("+OK 2 messages", ("1 %d" %
test_mail_len, "2 %d" % test_mail_len))
mocked_poplib_server = mock.Mock() mocked_poplib_server = mock.Mock()
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) mocked_poplib_server.list = mock.Mock(
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x]) return_value=pop3_mail_list)
mocked_poplib_server.retr = mock.Mock(
side_effect=lambda x: pop3_emails[x])
with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib:
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) mocked_poplib.POP3 = mock.Mock(
return_value=mocked_poplib_server)
call_command('get_email') call_command('get_email')
elif self.method == 'imap': elif self.method == 'imap':
# mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 # mock imaplib.IMAP4's search and fetch methods with responses
# from RFC 3501
imap_emails = { imap_emails = {
"1": ("OK", (("1", test_email),)), "1": ("OK", (("1", test_email),)),
"2": ("OK", (("2", test_email),)), "2": ("OK", (("2", test_email),)),
} }
imap_mail_list = ("OK", ("1 2",)) imap_mail_list = ("OK", ("1 2",))
mocked_imaplib_server = mock.Mock() mocked_imaplib_server = mock.Mock()
mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) mocked_imaplib_server.search = mock.Mock(
return_value=imap_mail_list)
# we ignore the second arg as the data item/mime-part is constant (RFC822) # we ignore the second arg as the data item/mime-part is
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) # constant (RFC822)
mocked_imaplib_server.fetch = mock.Mock(
side_effect=lambda x, _: imap_emails[x])
with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib:
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) mocked_imaplib.IMAP4 = mock.Mock(
return_value=mocked_imaplib_server)
call_command('get_email') call_command('get_email')
ticket1 = get_object_or_404(Ticket, pk=1) ticket1 = get_object_or_404(Ticket, pk=1)
@ -382,7 +439,8 @@ class GetEmailParametricTemplate(object):
For each email source supported, we mock the backend to provide For each email source supported, we mock the backend to provide
authentically formatted responses containing our test data.""" authentically formatted responses containing our test data."""
# example email text from Python docs: https://docs.python.org/3/library/email-examples.html # example email text from Python docs:
# https://docs.python.org/3/library/email-examples.html
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
@ -396,7 +454,8 @@ class GetEmailParametricTemplate(object):
cc = cc_one + ", " + cc_two cc = cc_one + ", " + cc_two
subject = "Link" subject = "Link"
# Create message container - the correct MIME type is multipart/alternative. # Create message container - the correct MIME type is
# multipart/alternative.
msg = MIMEMultipart('alternative') msg = MIMEMultipart('alternative')
msg['Subject'] = subject msg['Subject'] = subject
msg['From'] = me msg['From'] = me
@ -446,38 +505,51 @@ class GetEmailParametricTemplate(object):
call_command('get_email') call_command('get_email')
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') mocked_listdir.assert_called_with(
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') '/var/lib/mail/helpdesk/')
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2') mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename1')
mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename2')
elif self.method == 'pop3': elif self.method == 'pop3':
# mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 # mock poplib.POP3's list and retr methods to provide responses
# as per RFC 1939
pop3_emails = { pop3_emails = {
'1': ("+OK", msg.as_string().split('\n')), '1': ("+OK", msg.as_string().split('\n')),
'2': ("+OK", msg.as_string().split('\n')), '2': ("+OK", msg.as_string().split('\n')),
} }
pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len)) pop3_mail_list = ("+OK 2 messages", ("1 %d" %
test_mail_len, "2 %d" % test_mail_len))
mocked_poplib_server = mock.Mock() mocked_poplib_server = mock.Mock()
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) mocked_poplib_server.list = mock.Mock(
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x]) return_value=pop3_mail_list)
mocked_poplib_server.retr = mock.Mock(
side_effect=lambda x: pop3_emails[x])
with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib:
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) mocked_poplib.POP3 = mock.Mock(
return_value=mocked_poplib_server)
call_command('get_email') call_command('get_email')
elif self.method == 'imap': elif self.method == 'imap':
# mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 # mock imaplib.IMAP4's search and fetch methods with responses
# from RFC 3501
imap_emails = { imap_emails = {
"1": ("OK", (("1", msg.as_string()),)), "1": ("OK", (("1", msg.as_string()),)),
"2": ("OK", (("2", msg.as_string()),)), "2": ("OK", (("2", msg.as_string()),)),
} }
imap_mail_list = ("OK", ("1 2",)) imap_mail_list = ("OK", ("1 2",))
mocked_imaplib_server = mock.Mock() mocked_imaplib_server = mock.Mock()
mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) mocked_imaplib_server.search = mock.Mock(
return_value=imap_mail_list)
# we ignore the second arg as the data item/mime-part is constant (RFC822) # we ignore the second arg as the data item/mime-part is
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) # constant (RFC822)
mocked_imaplib_server.fetch = mock.Mock(
side_effect=lambda x, _: imap_emails[x])
with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib:
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) mocked_imaplib.IMAP4 = mock.Mock(
return_value=mocked_imaplib_server)
call_command('get_email') call_command('get_email')
ticket1 = get_object_or_404(Ticket, pk=1) ticket1 = get_object_or_404(Ticket, pk=1)
@ -537,41 +609,54 @@ class GetEmailParametricTemplate(object):
call_command('get_email') call_command('get_email')
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') mocked_listdir.assert_called_with(
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') '/var/lib/mail/helpdesk/')
mocked_isfile.assert_any_call(
'/var/lib/mail/helpdesk/filename1')
elif self.method == 'pop3': elif self.method == 'pop3':
# mock poplib.POP3's list and retr methods to provide responses as per RFC 1939 # mock poplib.POP3's list and retr methods to provide responses
# as per RFC 1939
pop3_emails = { pop3_emails = {
'1': ("+OK", test_email.split('\n')), '1': ("+OK", test_email.split('\n')),
} }
pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len)) pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len))
mocked_poplib_server = mock.Mock() mocked_poplib_server = mock.Mock()
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list) mocked_poplib_server.list = mock.Mock(
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails['1']) return_value=pop3_mail_list)
mocked_poplib_server.retr = mock.Mock(
side_effect=lambda x: pop3_emails['1'])
with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib:
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server) mocked_poplib.POP3 = mock.Mock(
return_value=mocked_poplib_server)
call_command('get_email') call_command('get_email')
elif self.method == 'imap': elif self.method == 'imap':
# mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501 # mock imaplib.IMAP4's search and fetch methods with responses
# from RFC 3501
imap_emails = { imap_emails = {
"1": ("OK", (("1", test_email),)), "1": ("OK", (("1", test_email),)),
} }
imap_mail_list = ("OK", ("1",)) imap_mail_list = ("OK", ("1",))
mocked_imaplib_server = mock.Mock() mocked_imaplib_server = mock.Mock()
mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list) mocked_imaplib_server.search = mock.Mock(
return_value=imap_mail_list)
# we ignore the second arg as the data item/mime-part is constant (RFC822) # we ignore the second arg as the data item/mime-part is
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x]) # constant (RFC822)
mocked_imaplib_server.fetch = mock.Mock(
side_effect=lambda x, _: imap_emails[x])
with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib:
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server) mocked_imaplib.IMAP4 = mock.Mock(
return_value=mocked_imaplib_server)
call_command('get_email') call_command('get_email')
ticket1 = get_object_or_404(Ticket, pk=1) ticket1 = get_object_or_404(Ticket, pk=1)
self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id)
self.assertEqual(ticket1.title, "example email that crashes django-helpdesk get_email") self.assertEqual(
self.assertEqual(ticket1.description, """hi, thanks for looking into this :)\n\nhttps://github.com/django-helpdesk/django-helpdesk/issues/567#issuecomment-342954233""") ticket1.title, "example email that crashes django-helpdesk get_email")
self.assertEqual(
ticket1.description, """hi, thanks for looking into this :)\n\nhttps://github.com/django-helpdesk/django-helpdesk/issues/567#issuecomment-342954233""")
# MIME part should be attached to follow up # MIME part should be attached to follow up
followup1 = get_object_or_404(FollowUp, pk=1) followup1 = get_object_or_404(FollowUp, pk=1)
self.assertEqual(followup1.ticket.id, 1) self.assertEqual(followup1.ticket.id, 1)
@ -680,9 +765,11 @@ class GetEmailCCHandling(TestCase):
self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 1) self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 1)
ccstaff = get_object_or_404(TicketCC, pk=1) ccstaff = get_object_or_404(TicketCC, pk=1)
self.assertEqual(ccstaff.user, User.objects.get(username='staff')) self.assertEqual(ccstaff.user, User.objects.get(username='staff'))
self.assertEqual(ticket1.assigned_to, User.objects.get(username='assigned')) self.assertEqual(ticket1.assigned_to,
User.objects.get(username='assigned'))
# example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/ # example email text from Django docs:
# https://docs.djangoproject.com/en/1.10/ref/unicode/
test_email_from = "submitter@example.com" test_email_from = "submitter@example.com"
# NOTE: CC emails are in alphabetical order and must be tested as such! # NOTE: CC emails are in alphabetical order and must be tested as such!
# implementation uses sets, so only way to ensure tickets created # implementation uses sets, so only way to ensure tickets created
@ -694,7 +781,10 @@ class GetEmailCCHandling(TestCase):
ticket_user_emails = "assigned@example.com, staff@example.com, submitter@example.com, observer@example.com, queue@example.com" ticket_user_emails = "assigned@example.com, staff@example.com, submitter@example.com, observer@example.com, queue@example.com"
test_email_subject = "[CC-1] My visit to Sør-Trøndelag" test_email_subject = "[CC-1] My visit to Sør-Trøndelag"
test_email_body = "Unicode helpdesk comment with an s-hat (ŝ) via email." test_email_body = "Unicode helpdesk comment with an s-hat (ŝ) via email."
test_email = "To: queue@example.com\nCc: " + test_email_cc_one + ", " + test_email_cc_one + ", " + test_email_cc_two + ", " + test_email_cc_three + "\nCC: " + test_email_cc_one + ", " + test_email_cc_three + ", " + test_email_cc_four + ", " + ticket_user_emails + "\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body test_email = "To: queue@example.com\nCc: " + test_email_cc_one + ", " + test_email_cc_one + ", " + test_email_cc_two + ", " + test_email_cc_three + "\nCC: " + test_email_cc_one + \
", " + test_email_cc_three + ", " + test_email_cc_four + ", " + ticket_user_emails + \
"\nFrom: " + test_email_from + "\nSubject: " + \
test_email_subject + "\n\n" + test_email_body
test_mail_len = len(test_email) test_mail_len = len(test_email)
with mock.patch('os.listdir') as mocked_listdir, \ with mock.patch('os.listdir') as mocked_listdir, \
@ -755,5 +845,6 @@ for method, socks in case_matrix:
test_name = str( test_name = str(
"TestGetEmail%s%s" % (method.capitalize(), socks_str)) "TestGetEmail%s%s" % (method.capitalize(), socks_str))
cl = type(test_name, (GetEmailParametricTemplate, TestCase), {"method": method, "socks": socks}) cl = type(test_name, (GetEmailParametricTemplate, TestCase),
{"method": method, "socks": socks})
setattr(thismodule, test_name, cl) setattr(thismodule, test_name, cl)

View File

@ -4,7 +4,8 @@ from django.test import TestCase
from helpdesk.models import KBCategory, KBItem, Queue, Ticket from helpdesk.models import KBCategory, KBItem, Queue, Ticket
from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response) from helpdesk.tests.helpers import (
get_staff_user, reload_urlconf, User, create_ticket, print_response)
class KBTests(TestCase): class KBTests(TestCase):
@ -43,13 +44,16 @@ class KBTests(TestCase):
self.assertContains(response, 'This is a test category') self.assertContains(response, 'This is a test category')
def test_kb_category(self): def test_kb_category(self):
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", ))) response = self.client.get(
reverse('helpdesk:kb_category', args=("test_cat", )))
self.assertContains(response, 'This is a test category') self.assertContains(response, 'This is a test category')
self.assertContains(response, 'KBItem 1') self.assertContains(response, 'KBItem 1')
self.assertContains(response, 'KBItem 2') self.assertContains(response, 'KBItem 2')
self.assertContains(response, 'Create New Ticket Queue:') self.assertContains(response, 'Create New Ticket Queue:')
self.client.login(username=self.user.get_username(), password='password') self.client.login(username=self.user.get_username(),
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", ))) password='password')
response = self.client.get(
reverse('helpdesk:kb_category', args=("test_cat", )))
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>') self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')
self.assertContains(response, '0 open tickets') self.assertContains(response, '0 open tickets')
ticket = Ticket.objects.create( ticket = Ticket.objects.create(
@ -58,23 +62,30 @@ class KBTests(TestCase):
kbitem=self.kbitem1, kbitem=self.kbitem1,
) )
ticket.save() ticket.save()
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat",))) response = self.client.get(
reverse('helpdesk:kb_category', args=("test_cat",)))
self.assertContains(response, '1 open tickets') self.assertContains(response, '1 open tickets')
def test_kb_vote(self): def test_kb_vote(self):
self.client.login(username=self.user.get_username(), password='password') self.client.login(username=self.user.get_username(),
response = self.client.get(reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=up") password='password')
cat_url = reverse('helpdesk:kb_category', args=("test_cat",)) + "?kbitem=1" response = self.client.get(
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=up")
cat_url = reverse('helpdesk:kb_category',
args=("test_cat",)) + "?kbitem=1"
self.assertRedirects(response, cat_url) self.assertRedirects(response, cat_url)
response = self.client.get(cat_url) response = self.client.get(cat_url)
self.assertContains(response, '1 people found this answer useful of 1') self.assertContains(response, '1 people found this answer useful of 1')
response = self.client.get(reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=down") response = self.client.get(
reverse('helpdesk:kb_vote', args=(self.kbitem1.pk,)) + "?vote=down")
self.assertRedirects(response, cat_url) self.assertRedirects(response, cat_url)
response = self.client.get(cat_url) response = self.client.get(cat_url)
self.assertContains(response, '0 people found this answer useful of 1') self.assertContains(response, '0 people found this answer useful of 1')
def test_kb_category_iframe(self): def test_kb_category_iframe(self):
cat_url = reverse('helpdesk:kb_category', args=("test_cat",)) + "?kbitem=1&submitter_email=foo@bar.cz&title=lol&" cat_url = reverse('helpdesk:kb_category', args=(
"test_cat",)) + "?kbitem=1&submitter_email=foo@bar.cz&title=lol&"
response = self.client.get(cat_url) response = self.client.get(cat_url)
# Assert that query params are passed on to ticket submit form # Assert that query params are passed on to ticket submit form
self.assertContains(response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&amp;title=lol") self.assertContains(
response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&amp;title=lol")

View File

@ -6,7 +6,8 @@ from django.test import TestCase
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import Queue from helpdesk.models import Queue
from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response) from helpdesk.tests.helpers import (
get_staff_user, reload_urlconf, User, create_ticket, print_response)
from django.test.utils import override_settings from django.test.utils import override_settings
@ -26,13 +27,15 @@ class KBDisabledTestCase(TestCase):
"""Test proper rendering of navigation.html by accessing the dashboard""" """Test proper rendering of navigation.html by accessing the dashboard"""
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
self.client.login(username=get_staff_user().get_username(), password='password') self.client.login(username=get_staff_user(
).get_username(), password='password')
self.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index') self.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index')
try: try:
response = self.client.get(reverse('helpdesk:dashboard')) response = self.client.get(reverse('helpdesk:dashboard'))
except NoReverseMatch as e: except NoReverseMatch as e:
if 'helpdesk:kb_index' in e.message: if 'helpdesk:kb_index' in e.message:
self.fail("Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)") self.fail(
"Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)")
else: else:
raise raise
else: else:
@ -75,7 +78,8 @@ class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
""" """
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') user = User.objects.create_user(
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
self.assertTrue(is_helpdesk_staff(user)) self.assertTrue(is_helpdesk_staff(user))
@ -91,7 +95,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.non_staff_user_password = "gouda" self.non_staff_user_password = "gouda"
self.non_staff_user = User.objects.create_user(username='henry.wensleydale', password=self.non_staff_user_password, email='wensleydale@example.com') self.non_staff_user = User.objects.create_user(
username='henry.wensleydale', password=self.non_staff_user_password, email='wensleydale@example.com')
def test_staff_user_detection(self): def test_staff_user_detection(self):
"""Staff and non-staff users are correctly identified""" """Staff and non-staff users are correctly identified"""
@ -118,7 +123,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = self.non_staff_user user = self.non_staff_user
self.client.login(username=user.username, password=self.non_staff_user_password) self.client.login(username=user.username,
password=self.non_staff_user_password)
response = self.client.get(reverse('helpdesk:dashboard'), follow=True) response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
self.assertTemplateUsed(response, 'helpdesk/registration/login.html') self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
@ -128,7 +134,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
""" """
user = get_staff_user() user = get_staff_user()
self.client.login(username=user.username, password="password") self.client.login(username=user.username, password="password")
response = self.client.get(reverse('helpdesk:rss_unassigned'), follow=True) response = self.client.get(
reverse('helpdesk:rss_unassigned'), follow=True)
self.assertContains(response, 'Unassigned Open and Reopened tickets') self.assertContains(response, 'Unassigned Open and Reopened tickets')
@override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False) @override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False)
@ -137,21 +144,24 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
non-staff users should not be able to access rss feeds. non-staff users should not be able to access rss feeds.
""" """
user = self.non_staff_user user = self.non_staff_user
self.client.login(username=user.username, password=self.non_staff_user_password) self.client.login(username=user.username,
password=self.non_staff_user_password)
queue = Queue.objects.create( queue = Queue.objects.create(
title="Foo", title="Foo",
slug="test_queue", slug="test_queue",
) )
rss_urls = [ rss_urls = [
reverse('helpdesk:rss_user', args=[user.username]), reverse('helpdesk:rss_user', args=[user.username]),
reverse('helpdesk:rss_user_queue', args=[user.username, 'test_queue']), reverse('helpdesk:rss_user_queue', args=[
user.username, 'test_queue']),
reverse('helpdesk:rss_queue', args=['test_queue']), reverse('helpdesk:rss_queue', args=['test_queue']),
reverse('helpdesk:rss_unassigned'), reverse('helpdesk:rss_unassigned'),
reverse('helpdesk:rss_activity'), reverse('helpdesk:rss_activity'),
] ]
for rss_url in rss_urls: for rss_url in rss_urls:
response = self.client.get(rss_url, follow=True) response = self.client.get(rss_url, follow=True)
self.assertTemplateUsed(response, 'helpdesk/registration/login.html') self.assertTemplateUsed(
response, 'helpdesk/registration/login.html')
class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase): class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
@ -168,7 +178,8 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
""" """
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') user = User.objects.create_user(
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
self.assertTrue(is_helpdesk_staff(user)) self.assertTrue(is_helpdesk_staff(user))
@ -179,7 +190,8 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
def test_custom_staff_fail(self): def test_custom_staff_fail(self):
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user(username='terry.milton', password='frog', email='milton@example.com') user = User.objects.create_user(
username='terry.milton', password='frog', email='milton@example.com')
self.assertFalse(is_helpdesk_staff(user)) self.assertFalse(is_helpdesk_staff(user))
@ -264,7 +276,8 @@ class ReturnToTicketTestCase(TestCase):
def test_non_staff_user(self): def test_non_staff_user(self):
from helpdesk.views.staff import return_to_ticket from helpdesk.views.staff import return_to_ticket
user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') user = User.objects.create_user(
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
ticket = create_ticket() ticket = create_ticket()
response = return_to_ticket(user, helpdesk_settings, ticket) response = return_to_ticket(user, helpdesk_settings, ticket)
self.assertEqual(response['location'], ticket.ticket_url) self.assertEqual(response['location'], ticket.ticket_url)

View File

@ -56,11 +56,13 @@ class PerQueueStaffMembershipTestCase(TestCase):
for ticket_number in range(1, identifier + 1): for ticket_number in range(1, identifier + 1):
Ticket.objects.create( Ticket.objects.create(
title='Unassigned Ticket %d in Queue %d' % (ticket_number, identifier), title='Unassigned Ticket %d in Queue %d' % (
ticket_number, identifier),
queue=queue, queue=queue,
) )
Ticket.objects.create( Ticket.objects.create(
title='Ticket %d in Queue %d Assigned to User_%d' % (ticket_number, identifier, identifier), title='Ticket %d in Queue %d Assigned to User_%d' % (
ticket_number, identifier, identifier),
queue=queue, queue=queue,
assigned_to=user, assigned_to=user,
) )
@ -80,7 +82,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
# Regular users # Regular users
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % identifier, password=str(identifier)) self.client.login(username='User_%d' %
identifier, password=str(identifier))
response = self.client.get(reverse('helpdesk:dashboard')) response = self.client.get(reverse('helpdesk:dashboard'))
self.assertEqual( self.assertEqual(
len(response.context['unassigned_tickets']), len(response.context['unassigned_tickets']),
@ -117,7 +120,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
# Regular users # Regular users
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % identifier, password=str(identifier)) self.client.login(username='User_%d' %
identifier, password=str(identifier))
response = self.client.get(reverse('helpdesk:report_index')) response = self.client.get(reverse('helpdesk:report_index'))
self.assertEqual( self.assertEqual(
len(response.context['dash_tickets']), len(response.context['dash_tickets']),
@ -164,9 +168,11 @@ class PerQueueStaffMembershipTestCase(TestCase):
""" """
# Regular users # Regular users
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % identifier, password=str(identifier)) self.client.login(username='User_%d' %
identifier, password=str(identifier))
response = self.client.get(reverse('helpdesk:list')) response = self.client.get(reverse('helpdesk:list'))
tickets = __Query__(HelpdeskUser(self.identifier_users[identifier]), base64query=response.context['urlsafe_query']).get() tickets = __Query__(HelpdeskUser(
self.identifier_users[identifier]), base64query=response.context['urlsafe_query']).get()
self.assertEqual( self.assertEqual(
len(tickets), len(tickets),
identifier * 2, identifier * 2,
@ -186,7 +192,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
# Superuser # Superuser
self.client.login(username='superuser', password='superuser') self.client.login(username='superuser', password='superuser')
response = self.client.get(reverse('helpdesk:list')) response = self.client.get(reverse('helpdesk:list'))
tickets = __Query__(HelpdeskUser(self.superuser), base64query=response.context['urlsafe_query']).get() tickets = __Query__(HelpdeskUser(self.superuser),
base64query=response.context['urlsafe_query']).get()
self.assertEqual( self.assertEqual(
len(tickets), len(tickets),
6, 6,
@ -201,7 +208,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
""" """
# Regular users # Regular users
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % identifier, password=str(identifier)) self.client.login(username='User_%d' %
identifier, password=str(identifier))
response = self.client.get( response = self.client.get(
reverse('helpdesk:run_report', kwargs={'report': 'userqueue'}) reverse('helpdesk:run_report', kwargs={'report': 'userqueue'})
) )
@ -212,9 +220,11 @@ class PerQueueStaffMembershipTestCase(TestCase):
2, 2,
'Queues in report were not properly limited by queue membership' 'Queues in report were not properly limited by queue membership'
) )
# Each user should see a total number of tickets equal to twice their ID # Each user should see a total number of tickets equal to twice
# their ID
self.assertEqual( self.assertEqual(
sum([sum(user_tickets[1:]) for user_tickets in response.context['data']]), sum([sum(user_tickets[1:])
for user_tickets in response.context['data']]),
identifier * 2, identifier * 2,
'Tickets in report were not properly limited by queue membership' 'Tickets in report were not properly limited by queue membership'
) )
@ -224,7 +234,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
2, 2,
'Queue choices were not properly limited by queue membership' 'Queue choices were not properly limited by queue membership'
) )
# The queue each user can pick should be the queue named after their ID # The queue each user can pick should be the queue named after
# their ID
self.assertEqual( self.assertEqual(
response.context['headings'][1], response.context['headings'][1],
"Queue %d" % identifier, "Queue %d" % identifier,
@ -245,7 +256,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
) )
# Superuser should see the total ticket count of three tickets # Superuser should see the total ticket count of three tickets
self.assertEqual( self.assertEqual(
sum([sum(user_tickets[1:]) for user_tickets in response.context['data']]), sum([sum(user_tickets[1:])
for user_tickets in response.context['data']]),
6, 6,
'Tickets in report were improperly limited by queue membership for a superuser' 'Tickets in report were improperly limited by queue membership for a superuser'
) )

View File

@ -70,7 +70,8 @@ class PublicActionsTestCase(TestCase):
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html') self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
self.assertEqual(ticket.status, Ticket.CLOSED_STATUS) self.assertEqual(ticket.status, Ticket.CLOSED_STATUS)
self.assertEqual(ticket.resolution, resolution_text) self.assertEqual(ticket.resolution, resolution_text)
self.assertEqual(current_followups + 1, ticket.followup_set.all().count()) self.assertEqual(current_followups + 1,
ticket.followup_set.all().count())
ticket.resolution = old_resolution ticket.resolution = old_resolution
ticket.status = old_status ticket.status = old_status

View File

@ -5,7 +5,8 @@ from django.urls import reverse
from helpdesk.models import KBCategory, KBItem, Queue, Ticket from helpdesk.models import KBCategory, KBItem, Queue, Ticket
from helpdesk.query import query_to_base64 from helpdesk.query import query_to_base64
from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response) from helpdesk.tests.helpers import (
get_staff_user, reload_urlconf, User, create_ticket, print_response)
class QueryTests(TestCase): class QueryTests(TestCase):
@ -58,7 +59,8 @@ class QueryTests(TestCase):
def test_query_basic(self): def test_query_basic(self):
self.loginUser() self.loginUser()
query = query_to_base64({}) query = query_to_base64({})
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query])) response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
self.assertEqual( self.assertEqual(
response.json(), response.json(),
{ {
@ -76,12 +78,14 @@ class QueryTests(TestCase):
query = query_to_base64( query = query_to_base64(
{'filtering': {'kbitem__in': [self.kbitem1.pk]}} {'filtering': {'kbitem__in': [self.kbitem1.pk]}}
) )
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query])) response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
self.assertEqual( self.assertEqual(
response.json(), response.json(),
{ {
"data": "data":
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], [{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
"created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"recordsFiltered": 1, "recordsFiltered": 1,
"recordsTotal": 1, "recordsTotal": 1,
"draw": 0, "draw": 0,
@ -93,12 +97,14 @@ class QueryTests(TestCase):
query = query_to_base64( query = query_to_base64(
{'filtering_or': {'kbitem__in': [self.kbitem1.pk]}} {'filtering_or': {'kbitem__in': [self.kbitem1.pk]}}
) )
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query])) response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
self.assertEqual( self.assertEqual(
response.json(), response.json(),
{ {
"data": "data":
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}], [{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
"created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"recordsFiltered": 1, "recordsFiltered": 1,
"recordsTotal": 1, "recordsTotal": 1,
"draw": 0, "draw": 0,

View File

@ -78,10 +78,13 @@ class TicketActionsTestCase(TestCase):
ticket = Ticket.objects.create(**ticket_data) ticket = Ticket.objects.create(**ticket_data)
ticket_id = ticket.id ticket_id = ticket.id
response = self.client.get(reverse('helpdesk:delete', kwargs={'ticket_id': ticket_id}), follow=True) response = self.client.get(reverse('helpdesk:delete', kwargs={
self.assertContains(response, 'Are you sure you want to delete this ticket') 'ticket_id': ticket_id}), follow=True)
self.assertContains(
response, 'Are you sure you want to delete this ticket')
response = self.client.post(reverse('helpdesk:delete', kwargs={'ticket_id': ticket_id}), follow=True) response = self.client.post(reverse('helpdesk:delete', kwargs={
'ticket_id': ticket_id}), follow=True)
first_redirect = response.redirect_chain[0] first_redirect = response.redirect_chain[0]
first_redirect_url = first_redirect[0] first_redirect_url = first_redirect[0]
@ -123,7 +126,8 @@ class TicketActionsTestCase(TestCase):
post_data = { post_data = {
'owner': self.user2.id, 'owner': self.user2.id,
} }
response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) response = self.client.post(reverse('helpdesk:update', kwargs={
'ticket_id': ticket_id}), post_data, follow=True)
self.assertContains(response, 'Changed Owner from User_1 to User_2') self.assertContains(response, 'Changed Owner from User_1 to User_2')
# change status with users email assigned and submitter email assigned, # change status with users email assigned and submitter email assigned,
@ -142,14 +146,16 @@ class TicketActionsTestCase(TestCase):
# do this also to a newly assigned user (different from logged in one) # do this also to a newly assigned user (different from logged in one)
ticket.assigned_to = self.user ticket.assigned_to = self.user
response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) response = self.client.post(reverse('helpdesk:update', kwargs={
'ticket_id': ticket_id}), post_data, follow=True)
self.assertContains(response, 'Changed Status from Open to Closed') self.assertContains(response, 'Changed Status from Open to Closed')
post_data = { post_data = {
'new_status': Ticket.OPEN_STATUS, 'new_status': Ticket.OPEN_STATUS,
'owner': self.user2.id, 'owner': self.user2.id,
'public': True 'public': True
} }
response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) response = self.client.post(reverse('helpdesk:update', kwargs={
'ticket_id': ticket_id}), post_data, follow=True)
self.assertContains(response, 'Changed Status from Open to Closed') self.assertContains(response, 'Changed Status from Open to Closed')
def test_can_access_ticket(self): def test_can_access_ticket(self):
@ -175,8 +181,10 @@ class TicketActionsTestCase(TestCase):
# create ticket # create ticket
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
ticket = Ticket.objects.create(**initial_data) ticket = Ticket.objects.create(**initial_data)
self.assertEqual(HelpdeskUser(self.user).can_access_ticket(ticket), True) self.assertEqual(HelpdeskUser(
self.assertEqual(HelpdeskUser(self.user2).can_access_ticket(ticket), False) self.user).can_access_ticket(ticket), True)
self.assertEqual(HelpdeskUser(
self.user2).can_access_ticket(ticket), False)
def test_num_to_link(self): def test_num_to_link(self):
"""Test that we are correctly expanding links to tickets from IDs""" """Test that we are correctly expanding links to tickets from IDs"""
@ -197,10 +205,13 @@ class TicketActionsTestCase(TestCase):
# generate the URL text # generate the URL text
result = num_to_link('this is ticket#%s' % ticket_id) result = num_to_link('this is ticket#%s' % ticket_id)
self.assertEqual(result, "this is ticket <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a>" % (ticket_id, ticket_id)) self.assertEqual(
result, "this is ticket <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a>" % (ticket_id, ticket_id))
result2 = num_to_link('whoa another ticket is here #%s huh' % ticket_id) result2 = num_to_link(
self.assertEqual(result2, "whoa another ticket is here <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a> huh" % (ticket_id, ticket_id)) 'whoa another ticket is here #%s huh' % ticket_id)
self.assertEqual(
result2, "whoa another ticket is here <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a> huh" % (ticket_id, ticket_id))
def test_create_ticket_getform(self): def test_create_ticket_getform(self):
self.loginUser() self.loginUser()
@ -221,7 +232,8 @@ class TicketActionsTestCase(TestCase):
status=Ticket.RESOLVED_STATUS, status=Ticket.RESOLVED_STATUS,
resolution='Awesome resolution for ticket 1' resolution='Awesome resolution for ticket 1'
) )
ticket_1_follow_up = ticket_1.followup_set.create(title='Ticket 1 creation') ticket_1_follow_up = ticket_1.followup_set.create(
title='Ticket 1 creation')
ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user) ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user)
ticket_1_created = ticket_1.created ticket_1_created = ticket_1.created
due_date = timezone.now() due_date = timezone.now()
@ -233,7 +245,8 @@ class TicketActionsTestCase(TestCase):
due_date=due_date, due_date=due_date,
assigned_to=self.user assigned_to=self.user
) )
ticket_2_follow_up = ticket_1.followup_set.create(title='Ticket 2 creation') ticket_2_follow_up = ticket_1.followup_set.create(
title='Ticket 2 creation')
ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com') ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com')
# Create custom fields and set values for tickets # Create custom fields and set values for tickets
@ -243,16 +256,19 @@ class TicketActionsTestCase(TestCase):
data_type='varchar', data_type='varchar',
) )
ticket_1_field_1 = 'This is for the test field' ticket_1_field_1 = 'This is for the test field'
ticket_1.ticketcustomfieldvalue_set.create(field=custom_field_1, value=ticket_1_field_1) ticket_1.ticketcustomfieldvalue_set.create(
field=custom_field_1, value=ticket_1_field_1)
ticket_2_field_1 = 'Another test text' ticket_2_field_1 = 'Another test text'
ticket_2.ticketcustomfieldvalue_set.create(field=custom_field_1, value=ticket_2_field_1) ticket_2.ticketcustomfieldvalue_set.create(
field=custom_field_1, value=ticket_2_field_1)
custom_field_2 = CustomField.objects.create( custom_field_2 = CustomField.objects.create(
name='number', name='number',
label='Number', label='Number',
data_type='integer', data_type='integer',
) )
ticket_2_field_2 = '444' ticket_2_field_2 = '444'
ticket_2.ticketcustomfieldvalue_set.create(field=custom_field_2, value=ticket_2_field_2) ticket_2.ticketcustomfieldvalue_set.create(
field=custom_field_2, value=ticket_2_field_2)
# Check that it correctly redirects to the intermediate page # Check that it correctly redirects to the intermediate page
response = self.client.post( response = self.client.post(
@ -263,7 +279,8 @@ class TicketActionsTestCase(TestCase):
}, },
follow=True follow=True
) )
redirect_url = '%s?tickets=%s&tickets=%s' % (reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id) redirect_url = '%s?tickets=%s&tickets=%s' % (
reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id)
self.assertRedirects(response, redirect_url) self.assertRedirects(response, redirect_url)
self.assertContains(response, ticket_1.description) self.assertContains(response, ticket_1.description)
self.assertContains(response, ticket_1.resolution) self.assertContains(response, ticket_1.resolution)
@ -301,7 +318,11 @@ class TicketActionsTestCase(TestCase):
self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email) self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email)
self.assertEqual(ticket_1.description, ticket_2.description) self.assertEqual(ticket_1.description, ticket_2.description)
self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to) self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to)
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_1).value, ticket_1_field_1) self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_2).value, ticket_2_field_2) field=custom_field_1).value, ticket_1_field_1)
self.assertEqual(list(ticket_1.followup_set.all()), [ticket_1_follow_up, ticket_2_follow_up]) self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
self.assertEqual(list(ticket_1.ticketcc_set.all()), [ticket_1_cc, ticket_2_cc]) field=custom_field_2).value, ticket_2_field_2)
self.assertEqual(list(ticket_1.followup_set.all()), [
ticket_1_follow_up, ticket_2_follow_up])
self.assertEqual(list(ticket_1.ticketcc_set.all()),
[ticket_1_cc, ticket_2_cc])

View File

@ -57,7 +57,8 @@ class TestTicketLookupPublicEnabled(TestCase):
def test_add_email_to_ticketcc_if_not_in(self): def test_add_email_to_ticketcc_if_not_in(self):
staff_email = 'staff@mail.com' staff_email = 'staff@mail.com'
staff_user = User.objects.create(username='staff', email=staff_email, is_staff=True) staff_user = User.objects.create(
username='staff', email=staff_email, is_staff=True)
self.ticket.assigned_to = staff_user self.ticket.assigned_to = staff_user
self.ticket.save() self.ticket.save()
email_1 = 'user1@mail.com' email_1 = 'user1@mail.com'
@ -66,20 +67,25 @@ class TestTicketLookupPublicEnabled(TestCase):
# Add new email to CC # Add new email to CC
email_2 = 'user2@mail.com' email_2 = 'user2@mail.com'
ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2) ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2]) self.assertEqual(list(self.ticket.ticketcc_set.all()),
[ticketcc_1, ticketcc_2])
# Add existing email, doesn't change anything # Add existing email, doesn't change anything
self.ticket.add_email_to_ticketcc_if_not_in(email=email_1) self.ticket.add_email_to_ticketcc_if_not_in(email=email_1)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2]) self.assertEqual(list(self.ticket.ticketcc_set.all()),
[ticketcc_1, ticketcc_2])
# Add mail from assigned user, doesn't change anything # Add mail from assigned user, doesn't change anything
self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email) self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2]) self.assertEqual(list(self.ticket.ticketcc_set.all()),
[ticketcc_1, ticketcc_2])
self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user) self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2]) self.assertEqual(list(self.ticket.ticketcc_set.all()),
[ticketcc_1, ticketcc_2])
# Move a ticketCC from ticket 1 to ticket 2 # Move a ticketCC from ticket 1 to ticket 2
ticket_2 = Ticket.objects.create(queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2) ticket_2 = Ticket.objects.create(
queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2)
self.assertEqual(ticket_2.ticketcc_set.count(), 0) self.assertEqual(ticket_2.ticketcc_set.count(), 0)
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1) ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1)
self.assertEqual(ticketcc_1.ticket, ticket_2) self.assertEqual(ticketcc_1.ticket, ticket_2)

View File

@ -51,7 +51,6 @@ class TicketBasicsTestCase(TestCase):
self.client = Client() self.client = Client()
def test_create_ticket_instance_from_payload(self): def test_create_ticket_instance_from_payload(self):
""" """
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue. Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
""" """
@ -76,7 +75,8 @@ class TicketBasicsTestCase(TestCase):
'priority': 3, 'priority': 3,
} }
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]
@ -95,7 +95,6 @@ class TicketBasicsTestCase(TestCase):
# Follow up is anonymous # Follow up is anonymous
self.assertIsNone(ticket.followup_set.first().user) self.assertIsNone(ticket.followup_set.first().user)
def test_create_ticket_public_with_hidden_fields(self): def test_create_ticket_public_with_hidden_fields(self):
email_count = len(mail.outbox) email_count = len(mail.outbox)
@ -110,11 +109,11 @@ class TicketBasicsTestCase(TestCase):
'priority': 4, 'priority': 4,
} }
response = self.client.post(reverse('helpdesk:home') + "?_hide_fields_=priority", post_data, follow=True) response = self.client.post(
reverse('helpdesk:home') + "?_hide_fields_=priority", post_data, follow=True)
ticket = Ticket.objects.last() ticket = Ticket.objects.last()
self.assertEqual(ticket.priority, 4) self.assertEqual(ticket.priority, 4)
def test_create_ticket_authorized(self): def test_create_ticket_authorized(self):
email_count = len(mail.outbox) email_count = len(mail.outbox)
self.client.force_login(self.user) self.client.force_login(self.user)
@ -130,7 +129,8 @@ class TicketBasicsTestCase(TestCase):
'priority': 3, 'priority': 3,
} }
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]
@ -188,7 +188,8 @@ class TicketBasicsTestCase(TestCase):
'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]
@ -221,7 +222,8 @@ class TicketBasicsTestCase(TestCase):
'priority': 3, 'priority': 3,
} }
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]
@ -266,7 +268,6 @@ class EmailInteractionsTestCase(TestCase):
} }
def test_create_ticket_from_email_with_message_id(self): def test_create_ticket_from_email_with_message_id(self):
""" """
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue. Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id> Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id>
@ -302,7 +303,6 @@ class EmailInteractionsTestCase(TestCase):
self.assertIn(submitter_email, mail.outbox[0].to) self.assertIn(submitter_email, mail.outbox[0].to)
def test_create_ticket_from_email_without_message_id(self): def test_create_ticket_from_email_without_message_id(self):
""" """
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue. Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id> Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id>
@ -322,7 +322,8 @@ class EmailInteractionsTestCase(TestCase):
object_from_message(str(msg), self.queue_public, logger=logger) object_from_message(str(msg), self.queue_public, logger=logger)
ticket = Ticket.objects.get(title=self.ticket_data['title'], queue=self.queue_public, submitter_email=submitter_email) ticket = Ticket.objects.get(
title=self.ticket_data['title'], queue=self.queue_public, submitter_email=submitter_email)
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
@ -417,8 +418,10 @@ class EmailInteractionsTestCase(TestCase):
# Ensure that the submitter is notified # Ensure that the submitter is notified
self.assertIn(submitter_email, mail.outbox[0].to) self.assertIn(submitter_email, mail.outbox[0].to)
# Ensure that the queue's email was not subscribed to the event notifications. # Ensure that the queue's email was not subscribed to the event
self.assertRaises(TicketCC.DoesNotExist, TicketCC.objects.get, ticket=ticket, email=to_list[0]) # notifications.
self.assertRaises(TicketCC.DoesNotExist,
TicketCC.objects.get, ticket=ticket, email=to_list[0])
for cc_email in cc_list: for cc_email in cc_list:
@ -825,14 +828,16 @@ class EmailInteractionsTestCase(TestCase):
msg.__setitem__('Message-ID', message_id) msg.__setitem__('Message-ID', message_id)
msg.__setitem__('Subject', self.ticket_data['title']) msg.__setitem__('Subject', self.ticket_data['title'])
msg.__setitem__('From', submitter_email) msg.__setitem__('From', submitter_email)
msg.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) msg.__setitem__(
'To', self.queue_public_with_notifications_disabled.email_address)
msg.__setitem__('Cc', ','.join(cc_list)) msg.__setitem__('Cc', ','.join(cc_list))
msg.__setitem__('Content-Type', 'text/plain;') msg.__setitem__('Content-Type', 'text/plain;')
msg.set_payload(self.ticket_data['description']) msg.set_payload(self.ticket_data['description'])
email_count = len(mail.outbox) email_count = len(mail.outbox)
object_from_message(str(msg), self.queue_public_with_notifications_disabled, logger=logger) object_from_message(
str(msg), self.queue_public_with_notifications_disabled, logger=logger)
followup = FollowUp.objects.get(message_id=message_id) followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id) ticket = Ticket.objects.get(id=followup.ticket.id)
@ -954,14 +959,16 @@ class EmailInteractionsTestCase(TestCase):
msg.__setitem__('Message-ID', message_id) msg.__setitem__('Message-ID', message_id)
msg.__setitem__('Subject', self.ticket_data['title']) msg.__setitem__('Subject', self.ticket_data['title'])
msg.__setitem__('From', submitter_email) msg.__setitem__('From', submitter_email)
msg.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) msg.__setitem__(
'To', self.queue_public_with_notifications_disabled.email_address)
msg.__setitem__('Cc', ','.join(cc_list)) msg.__setitem__('Cc', ','.join(cc_list))
msg.__setitem__('Content-Type', 'text/plain;') msg.__setitem__('Content-Type', 'text/plain;')
msg.set_payload(self.ticket_data['description']) msg.set_payload(self.ticket_data['description'])
email_count = len(mail.outbox) email_count = len(mail.outbox)
object_from_message(str(msg), self.queue_public_with_notifications_disabled, logger=logger) object_from_message(
str(msg), self.queue_public_with_notifications_disabled, logger=logger)
followup = FollowUp.objects.get(message_id=message_id) followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id) ticket = Ticket.objects.get(id=followup.ticket.id)
@ -994,11 +1001,13 @@ class EmailInteractionsTestCase(TestCase):
reply.__setitem__('In-Reply-To', message_id) reply.__setitem__('In-Reply-To', message_id)
reply.__setitem__('Subject', self.ticket_data['title']) reply.__setitem__('Subject', self.ticket_data['title'])
reply.__setitem__('From', submitter_email) reply.__setitem__('From', submitter_email)
reply.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) reply.__setitem__(
'To', self.queue_public_with_notifications_disabled.email_address)
reply.__setitem__('Content-Type', 'text/plain;') reply.__setitem__('Content-Type', 'text/plain;')
reply.set_payload(self.ticket_data['description']) reply.set_payload(self.ticket_data['description'])
object_from_message(str(reply), self.queue_public_with_notifications_disabled, logger=logger) object_from_message(
str(reply), self.queue_public_with_notifications_disabled, logger=logger)
followup = FollowUp.objects.get(message_id=message_id) followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id) ticket = Ticket.objects.get(id=followup.ticket.id)
@ -1093,8 +1102,12 @@ class EmailInteractionsTestCase(TestCase):
answer="A KB Item", answer="A KB Item",
) )
self.kbitem1.save() self.kbitem1.save()
cat_url = reverse('helpdesk:submit') + "?kbitem=1&submitter_email=foo@bar.cz&title=lol" cat_url = reverse('helpdesk:submit') + \
"?kbitem=1&submitter_email=foo@bar.cz&title=lol"
response = self.client.get(cat_url) response = self.client.get(cat_url)
self.assertContains(response, '<option value="1" selected>KBItem 1</option>') self.assertContains(
self.assertContains(response, '<input type="email" name="submitter_email" value="foo@bar.cz" class="form-control form-control" required id="id_submitter_email">') response, '<option value="1" selected>KBItem 1</option>')
self.assertContains(response, '<input type="text" name="title" value="lol" class="form-control form-control" maxlength="100" required id="id_title">') self.assertContains(
response, '<input type="email" name="submitter_email" value="foo@bar.cz" class="form-control form-control" required id="id_submitter_email">')
self.assertContains(
response, '<input type="text" name="title" value="lol" class="form-control form-control" maxlength="100" required id="id_title">')

View File

@ -26,5 +26,6 @@ class TicketActionsTestCase(TestCase):
def test_get_user_settings(self): def test_get_user_settings(self):
response = self.client.get(reverse('helpdesk:user_settings'), follow=True) response = self.client.get(
reverse('helpdesk:user_settings'), follow=True)
self.assertContains(response, "Use the following options") self.assertContains(response, "Use the following options")

View File

@ -64,12 +64,16 @@ urlpatterns = [
name="followup_delete", name="followup_delete",
), ),
path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"), path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"),
path("tickets/<int:ticket_id>/update/", staff.update_ticket, name="update"), path("tickets/<int:ticket_id>/update/",
path("tickets/<int:ticket_id>/delete/", staff.delete_ticket, name="delete"), staff.update_ticket, name="update"),
path("tickets/<int:ticket_id>/delete/",
staff.delete_ticket, name="delete"),
path("tickets/<int:ticket_id>/hold/", staff.hold_ticket, name="hold"), path("tickets/<int:ticket_id>/hold/", staff.hold_ticket, name="hold"),
path("tickets/<int:ticket_id>/unhold/", staff.unhold_ticket, name="unhold"), path("tickets/<int:ticket_id>/unhold/",
staff.unhold_ticket, name="unhold"),
path("tickets/<int:ticket_id>/cc/", staff.ticket_cc, name="ticket_cc"), path("tickets/<int:ticket_id>/cc/", staff.ticket_cc, name="ticket_cc"),
path("tickets/<int:ticket_id>/cc/add/", staff.ticket_cc_add, name="ticket_cc_add"), path("tickets/<int:ticket_id>/cc/add/",
staff.ticket_cc_add, name="ticket_cc_add"),
path( path(
"tickets/<int:ticket_id>/cc/delete/<int:cc_id>/", "tickets/<int:ticket_id>/cc/delete/<int:cc_id>/",
staff.ticket_cc_del, staff.ticket_cc_del,
@ -93,13 +97,15 @@ urlpatterns = [
re_path(r"^raw/(?P<type>\w+)/$", staff.raw_details, name="raw"), re_path(r"^raw/(?P<type>\w+)/$", staff.raw_details, name="raw"),
path("rss/", staff.rss_list, name="rss_index"), path("rss/", staff.rss_list, name="rss_index"),
path("reports/", staff.report_index, name="report_index"), path("reports/", staff.report_index, name="report_index"),
re_path(r"^reports/(?P<report>\w+)/$", staff.run_report, name="run_report"), re_path(r"^reports/(?P<report>\w+)/$",
staff.run_report, name="run_report"),
path("save_query/", staff.save_query, name="savequery"), path("save_query/", staff.save_query, name="savequery"),
path("delete_query/<int:id>/", staff.delete_saved_query, name="delete_query"), path("delete_query/<int:id>/", staff.delete_saved_query, name="delete_query"),
path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"), path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"),
path("ignore/", staff.email_ignore, name="email_ignore"), path("ignore/", staff.email_ignore, name="email_ignore"),
path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"), path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"),
path("ignore/delete/<int:id>/", staff.email_ignore_del, name="email_ignore_del"), path("ignore/delete/<int:id>/",
staff.email_ignore_del, name="email_ignore_del"),
re_path( re_path(
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern), r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
staff.datatables_ticket_list, staff.datatables_ticket_list,
@ -140,7 +146,8 @@ urlpatterns += [
name="success_iframe", name="success_iframe",
), ),
path("view/", public.view_ticket, name="public_view"), path("view/", public.view_ticket, name="public_view"),
path("change_language/", public.change_language, name="public_change_language"), path("change_language/", public.change_language,
name="public_change_language"),
] ]
urlpatterns += [ urlpatterns += [
@ -209,7 +216,8 @@ urlpatterns += [
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
urlpatterns += [ urlpatterns += [
path("kb/", kb.index, name="kb_index"), path("kb/", kb.index, name="kb_index"),
re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$", kb.category, name="kb_category"), re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$",
kb.category, name="kb_category"),
path("kb/<int:item>/vote/", kb.vote, name="kb_vote"), path("kb/<int:item>/vote/", kb.vote, name="kb_vote"),
re_path( re_path(
r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$", r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$",
@ -227,7 +235,8 @@ urlpatterns += [
path( path(
"system_settings/", "system_settings/",
login_required( login_required(
DirectTemplateView.as_view(template_name="helpdesk/system_settings.html") DirectTemplateView.as_view(
template_name="helpdesk/system_settings.html")
), ),
name="system_settings", name="system_settings",
), ),

View File

@ -11,6 +11,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
KBItem KBItem
) )
def huser_from_request(req): def huser_from_request(req):
return HelpdeskUser(req.user) return HelpdeskUser(req.user)
@ -33,7 +34,8 @@ class HelpdeskUser:
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \ helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
and not user.is_superuser 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)]
id_list += public_ids id_list += public_ids
return all_queues.filter(pk__in=id_list) return all_queues.filter(pk__in=id_list)
else: else:

View File

@ -4,8 +4,11 @@
from django.conf import settings from django.conf import settings
#TODO: can we use the builtin Django validator instead? # TODO: can we use the builtin Django validator instead?
# see: https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator # see:
# https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator
def validate_file_extension(value): def validate_file_extension(value):
import os import os
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -19,9 +22,12 @@ def validate_file_extension(value):
if hasattr(settings, 'VALID_EXTENSIONS'): if hasattr(settings, 'VALID_EXTENSIONS'):
valid_extensions = settings.VALID_EXTENSIONS valid_extensions = settings.VALID_EXTENSIONS
else: else:
valid_extensions = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] valid_extensions = ['.txt', '.asc', '.htm', '.html',
'.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']
if not ext.lower() in valid_extensions: if not ext.lower() in valid_extensions:
# TODO: one more check in case it is a file with no extension; we should always allow that? # TODO: one more check in case it is a file with no extension; we
# should always allow that?
if not (ext.lower() == '' or ext.lower() == '.'): if not (ext.lower() == '' or ext.lower() == '.'):
raise ValidationError('Unsupported file extension: %s.' % ext.lower()) raise ValidationError(
'Unsupported file extension: %s.' % ext.lower())

View File

@ -6,15 +6,18 @@ class AbstractCreateTicketMixin():
initial_data = {} initial_data = {}
request = self.request request = self.request
try: try:
initial_data['queue'] = Queue.objects.get(slug=request.GET.get('queue', None)).id initial_data['queue'] = Queue.objects.get(
slug=request.GET.get('queue', None)).id
except Queue.DoesNotExist: except Queue.DoesNotExist:
pass pass
u = request.user u = request.user
if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email: if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email:
initial_data['submitter_email'] = u.email initial_data['submitter_email'] = u.email
query_param_fields = ['submitter_email', 'title', 'body', 'queue', 'kbitem'] query_param_fields = ['submitter_email',
custom_fields = ["custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)] 'title', 'body', 'queue', 'kbitem']
custom_fields = [
"custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]
query_param_fields += custom_fields query_param_fields += custom_fields
for qpf in query_param_fields: for qpf in query_param_fields:
initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, "")) initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, ""))
@ -29,7 +32,8 @@ class AbstractCreateTicketMixin():
) )
if kbitem: if kbitem:
try: try:
kwargs['kbcategory'] = KBItem.objects.get(pk=int(kbitem)).category kwargs['kbcategory'] = KBItem.objects.get(
pk=int(kbitem)).category
except (ValueError, KBItem.DoesNotExist): except (ValueError, KBItem.DoesNotExist):
pass pass
return kwargs return kwargs

View File

@ -123,7 +123,8 @@ class RecentFollowUps(Feed):
description_template = 'helpdesk/rss/recent_activity_description.html' description_template = 'helpdesk/rss/recent_activity_description.html'
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):

View File

@ -45,7 +45,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
def get_form_class(self): def get_form_class(self):
try: try:
the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(".", 1) the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(
".", 1)
the_module = import_module(the_module) the_module = import_module(the_module)
the_form_class = getattr(the_module, the_form_class) the_form_class = getattr(the_module, the_form_class)
except Exception as e: except Exception as e:
@ -87,7 +88,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
"Public queue '%s' is configured as default but can't be found", "Public queue '%s' is configured as default but can't be found",
settings.HELPDESK_PUBLIC_TICKET_QUEUE settings.HELPDESK_PUBLIC_TICKET_QUEUE
) )
raise ImproperlyConfigured("Wrong public queue configuration") from e raise ImproperlyConfigured(
"Wrong public queue configuration") from e
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'): if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'): if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
@ -97,8 +99,10 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
def get_form_kwargs(self, *args, **kwargs): def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs) kwargs = super().get_form_kwargs(*args, **kwargs)
if '_hide_fields_' in self.request.GET: if '_hide_fields_' in self.request.GET:
kwargs['hidden_fields'] = self.request.GET.get('_hide_fields_', '').split(',') kwargs['hidden_fields'] = self.request.GET.get(
kwargs['readonly_fields'] = self.request.GET.get('_readonly_fields_', '').split(',') '_hide_fields_', '').split(',')
kwargs['readonly_fields'] = self.request.GET.get(
'_readonly_fields_', '').split(',')
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
@ -107,7 +111,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
# 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(user=self.request.user if self.request.user.is_authenticated else None) ticket = form.save(
user=self.request.user if self.request.user.is_authenticated else None)
try: try:
return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % ( return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % (
reverse('helpdesk:public_view'), reverse('helpdesk:public_view'),
@ -146,7 +151,8 @@ class CreateTicketView(BaseCreateTicketView):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
# Add the CSS error class to the form in order to better see them in the page # Add the CSS error class to the form in order to better see them in
# the page
form.error_css_class = 'text-danger' form.error_css_class = 'text-danger'
return form return form
@ -156,7 +162,8 @@ class Homepage(CreateTicketView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['kb_categories'] = huser_from_request(self.request).get_allowed_kb_categories() context['kb_categories'] = huser_from_request(
self.request).get_allowed_kb_categories()
return context return context
@ -170,7 +177,8 @@ def search_for_ticket(request, error_message=None):
'helpdesk_settings': helpdesk_settings, 'helpdesk_settings': helpdesk_settings,
}) })
else: else:
raise PermissionDenied("Public viewing of tickets without a secret key is forbidden.") raise PermissionDenied(
"Public viewing of tickets without a secret key is forbidden.")
@protect_view @protect_view
@ -188,9 +196,11 @@ def view_ticket(request):
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req) queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
try: try:
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email) ticket = Ticket.objects.get(
id=ticket_id, submitter_email__iexact=email)
else: else:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key) ticket = Ticket.objects.get(
id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key)
except (ObjectDoesNotExist, ValueError): except (ObjectDoesNotExist, ValueError):
return search_for_ticket(request, _('Invalid ticket ID or e-mail address. Please try again.')) return search_for_ticket(request, _('Invalid ticket ID or e-mail address. Please try again.'))

View File

@ -110,7 +110,8 @@ def dashboard(request):
# page vars for the three ticket tables # page vars for the three ticket tables
user_tickets_page = request.GET.get(_('ut_page'), 1) user_tickets_page = request.GET.get(_('ut_page'), 1)
user_tickets_closed_resolved_page = request.GET.get(_('utcr_page'), 1) user_tickets_closed_resolved_page = request.GET.get(_('utcr_page'), 1)
all_tickets_reported_by_current_user_page = request.GET.get(_('atrbcu_page'), 1) all_tickets_reported_by_current_user_page = request.GET.get(
_('atrbcu_page'), 1)
huser = HelpdeskUser(request.user) huser = HelpdeskUser(request.user)
active_tickets = Ticket.objects.select_related('queue').exclude( active_tickets = Ticket.objects.select_related('queue').exclude(
@ -335,7 +336,8 @@ def view_ticket(request, ticket_id):
return update_ticket(request, ticket_id) return update_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.
ticket_cc, show_subscribe = \ ticket_cc, show_subscribe = \
return_ticketccstring_and_show_subscribe(request.user, ticket) return_ticketccstring_and_show_subscribe(request.user, ticket)
if show_subscribe: if show_subscribe:
@ -361,9 +363,11 @@ def view_ticket(request, ticket_id):
return update_ticket(request, ticket_id) return update_ticket(request, ticket_id)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
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)
queues = HelpdeskUser(request.user).get_queues() queues = HelpdeskUser(request.user).get_queues()
queue_choices = _get_queue_choices(queues) queue_choices = _get_queue_choices(queues)
@ -378,7 +382,8 @@ def view_ticket(request, ticket_id):
if submitter_userprofile is not None: if submitter_userprofile is not None:
content_type = ContentType.objects.get_for_model(submitter_userprofile) content_type = ContentType.objects.get_for_model(submitter_userprofile)
submitter_userprofile_url = reverse( submitter_userprofile_url = reverse(
'admin:{app}_{model}_change'.format(app=content_type.app_label, model=content_type.model), 'admin:{app}_{model}_change'.format(
app=content_type.app_label, model=content_type.model),
kwargs={'object_id': submitter_userprofile.id} kwargs={'object_id': submitter_userprofile.id}
) )
else: else:
@ -439,7 +444,8 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca
if ticket is not None: if ticket is not None:
queryset = TicketCC.objects.filter(ticket=ticket, user=user, email=email) queryset = TicketCC.objects.filter(
ticket=ticket, user=user, email=email)
# Don't create duplicate entries for subscribers # Don't create duplicate entries for subscribers
if queryset.count() > 0: if queryset.count() > 0:
@ -509,7 +515,8 @@ def update_ticket(request, ticket_id, public=False):
due_date_month = int(request.POST.get('due_date_month', 0)) due_date_month = int(request.POST.get('due_date_month', 0))
due_date_day = int(request.POST.get('due_date_day', 0)) due_date_day = int(request.POST.get('due_date_day', 0))
if request.POST.get("time_spent"): if request.POST.get("time_spent"):
(hours, minutes) = [int(f) for f in request.POST.get("time_spent").split(":")] (hours, minutes) = [int(f)
for f in request.POST.get("time_spent").split(":")]
time_spent = timedelta(hours=hours, minutes=minutes) time_spent = timedelta(hours=hours, minutes=minutes)
else: else:
time_spent = None time_spent = None
@ -530,12 +537,14 @@ def update_ticket(request, ticket_id, public=False):
if not (due_date_year and due_date_month and due_date_day): if not (due_date_year and due_date_month and due_date_day):
due_date = ticket.due_date due_date = ticket.due_date
else: else:
# NOTE: must be an easier way to create a new date than doing it this way? # NOTE: must be an easier way to create a new date than doing it
# this way?
if ticket.due_date: if ticket.due_date:
due_date = ticket.due_date due_date = ticket.due_date
else: else:
due_date = timezone.now() due_date = timezone.now()
due_date = due_date.replace(due_date_year, due_date_month, due_date_day) due_date = due_date.replace(
due_date_year, due_date_month, due_date_day)
no_changes = all([ no_changes = all([
not request.FILES, not request.FILES,
@ -559,7 +568,8 @@ def update_ticket(request, ticket_id, public=False):
# this prevents system from trying to render any template tags # this prevents system from trying to render any template tags
# broken into two stages to prevent changes from first replace being themselves # broken into two stages to prevent changes from first replace being themselves
# changed by the second replace due to conflicting syntax # changed by the second replace due to conflicting syntax
comment = comment.replace('{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM') comment = comment.replace(
'{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM')
comment = comment.replace( comment = comment.replace(
'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%' 'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%'
).replace( ).replace(
@ -699,7 +709,8 @@ def update_ticket(request, ticket_id, public=False):
} }
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change: if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change:
roles['assigned_to'] = (template + 'cc', context) roles['assigned_to'] = (template + 'cc', context)
messages_sent_to.update(ticket.send(roles, dont_send_to=messages_sent_to, fail_silently=True, files=files,)) messages_sent_to.update(ticket.send(
roles, dont_send_to=messages_sent_to, fail_silently=True, files=files,))
if reassigned: if reassigned:
template_staff = 'assigned_owner' template_staff = 'assigned_owner'
@ -741,7 +752,8 @@ def update_ticket(request, ticket_id, public=False):
# auto subscribe user if enabled # auto subscribe user if enabled
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated: if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated:
ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(request.user, ticket) ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(
request.user, ticket)
if SHOW_SUBSCRIBE: if SHOW_SUBSCRIBE:
subscribe_staff_member_to_ticket(ticket, request.user) subscribe_staff_member_to_ticket(ticket, request.user)
@ -779,9 +791,11 @@ def mass_update(request):
user = request.user user = request.user
action = 'assign' action = 'assign'
elif action == 'merge': elif action == 'merge':
# Redirect to the Merge View with selected tickets id in the GET request # Redirect to the Merge View with selected tickets id in the GET
# request
return redirect( return redirect(
reverse('helpdesk:merge_tickets') + '?' + '&'.join(['tickets=%s' % ticket_id for ticket_id in tickets]) reverse('helpdesk:merge_tickets') + '?' +
'&'.join(['tickets=%s' % ticket_id for ticket_id in tickets])
) )
huser = HelpdeskUser(request.user) huser = HelpdeskUser(request.user)
@ -871,7 +885,8 @@ def mass_update(request):
mass_update = staff_member_required(mass_update) mass_update = staff_member_required(mass_update)
# Prepare ticket attributes which will be displayed in the table to choose which value to keep when merging # Prepare ticket attributes which will be displayed in the table to choose
# which value to keep when merging
ticket_attributes = ( ticket_attributes = (
('created', _('Created date')), ('created', _('Created date')),
('due_date', _('Due on')), ('due_date', _('Due on')),
@ -914,7 +929,8 @@ def merge_tickets(request):
# Prepare the value for each custom fields of this ticket # Prepare the value for each custom fields of this ticket
for custom_field in custom_fields: for custom_field in custom_fields:
try: try:
value = ticket.ticketcustomfieldvalue_set.get(field=custom_field).value value = ticket.ticketcustomfieldvalue_set.get(
field=custom_field).value
except (TicketCustomFieldValue.DoesNotExist, ValueError): except (TicketCustomFieldValue.DoesNotExist, ValueError):
value = default value = default
ticket.values[custom_field.name] = { ticket.values[custom_field.name] = {
@ -925,11 +941,13 @@ def merge_tickets(request):
if request.method == 'POST': if request.method == 'POST':
# Find which ticket has been chosen to be the main one # Find which ticket has been chosen to be the main one
try: try:
chosen_ticket = tickets.get(id=request.POST.get('chosen_ticket')) chosen_ticket = tickets.get(
id=request.POST.get('chosen_ticket'))
except Ticket.DoesNotExist: except Ticket.DoesNotExist:
ticket_select_form.add_error( ticket_select_form.add_error(
field='tickets', field='tickets',
error=_('Please choose a ticket in which the others will be merged into.') error=_(
'Please choose a ticket in which the others will be merged into.')
) )
else: else:
# Save ticket fields values # Save ticket fields values
@ -945,7 +963,8 @@ def merge_tickets(request):
if attribute.startswith('get_') and attribute.endswith('_display'): if attribute.startswith('get_') and attribute.endswith('_display'):
# Keep only the FIELD part # Keep only the FIELD part
attribute = attribute[4:-8] attribute = attribute[4:-8]
# Get value from selected ticket and then save it on the chosen ticket # Get value from selected ticket and then save it on
# the chosen ticket
value = getattr(selected_ticket, attribute) value = getattr(selected_ticket, attribute)
setattr(chosen_ticket, attribute, value) setattr(chosen_ticket, attribute, value)
# Save custom fields values # Save custom fields values
@ -953,17 +972,21 @@ def merge_tickets(request):
id_for_custom_field = request.POST.get(custom_field.name) id_for_custom_field = request.POST.get(custom_field.name)
if id_for_custom_field != chosen_ticket.id: if id_for_custom_field != chosen_ticket.id:
try: try:
selected_ticket = tickets.get(id=id_for_custom_field) selected_ticket = tickets.get(
id=id_for_custom_field)
except (Ticket.DoesNotExist, ValueError): except (Ticket.DoesNotExist, ValueError):
continue continue
# Check if the value for this ticket custom field exists # Check if the value for this ticket custom field
# exists
try: try:
value = selected_ticket.ticketcustomfieldvalue_set.get(field=custom_field).value value = selected_ticket.ticketcustomfieldvalue_set.get(
field=custom_field).value
except TicketCustomFieldValue.DoesNotExist: except TicketCustomFieldValue.DoesNotExist:
continue continue
# Create the custom field value or update it with the value from the selected ticket # Create the custom field value or update it with the
# value from the selected ticket
custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create( custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create(
field=custom_field, field=custom_field,
defaults={'value': value} defaults={'value': value}
@ -981,31 +1004,39 @@ def merge_tickets(request):
ticket.status = Ticket.DUPLICATE_STATUS ticket.status = Ticket.DUPLICATE_STATUS
ticket.save() ticket.save()
# Send mail to submitter email and ticket CC to let them know ticket has been merged # Send mail to submitter email and ticket CC to let them
# know ticket has been merged
context = safe_template_context(ticket) context = safe_template_context(ticket)
if ticket.submitter_email: if ticket.submitter_email:
send_templated_mail( send_templated_mail(
template_name='merged', template_name='merged',
context=context, context=context,
recipients=[ticket.submitter_email], recipients=[ticket.submitter_email],
bcc=[cc.email_address for cc in ticket.ticketcc_set.select_related('user')], bcc=[
cc.email_address for cc in ticket.ticketcc_set.select_related('user')],
sender=ticket.queue.from_address, sender=ticket.queue.from_address,
fail_silently=True fail_silently=True
) )
# Move all followups and update their title to know they come from another ticket # Move all followups and update their title to know they
# come from another ticket
ticket.followup_set.update( ticket.followup_set.update(
ticket=chosen_ticket, ticket=chosen_ticket,
# Next might exceed maximum 200 characters limit # Next might exceed maximum 200 characters limit
title=_('[Merged from #%(id)d] %(title)s') % {'id': ticket.id, 'title': ticket.title} title=_('[Merged from #%(id)d] %(title)s') % {
'id': ticket.id, 'title': ticket.title}
) )
# Add submitter_email, assigned_to email and ticketcc to chosen ticket if necessary # Add submitter_email, assigned_to email and ticketcc to
chosen_ticket.add_email_to_ticketcc_if_not_in(email=ticket.submitter_email) # chosen ticket if necessary
chosen_ticket.add_email_to_ticketcc_if_not_in(
email=ticket.submitter_email)
if ticket.assigned_to and ticket.assigned_to.email: if ticket.assigned_to and ticket.assigned_to.email:
chosen_ticket.add_email_to_ticketcc_if_not_in(email=ticket.assigned_to.email) chosen_ticket.add_email_to_ticketcc_if_not_in(
email=ticket.assigned_to.email)
for ticketcc in ticket.ticketcc_set.all(): for ticketcc in ticket.ticketcc_set.all():
chosen_ticket.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc) chosen_ticket.add_email_to_ticketcc_if_not_in(
ticketcc=ticketcc)
return redirect(chosen_ticket) return redirect(chosen_ticket)
return render(request, 'helpdesk/ticket_merge.html', { return render(request, 'helpdesk/ticket_merge.html', {
@ -1134,7 +1165,8 @@ def ticket_list(request):
urlsafe_query = query_to_base64(query_params) urlsafe_query = query_to_base64(query_params)
user_saved_queries = SavedSearch.objects.filter(Q(user=request.user) | Q(shared__exact=True)) user_saved_queries = SavedSearch.objects.filter(
Q(user=request.user) | Q(shared__exact=True))
search_message = '' search_message = ''
if query_params['search_string'] and settings.DATABASES['default']['ENGINE'].endswith('sqlite'): if query_params['search_string'] and settings.DATABASES['default']['ENGINE'].endswith('sqlite'):
@ -1150,7 +1182,8 @@ def ticket_list(request):
kbitem = [] kbitem = []
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
kbitem_choices = [(item.pk, str(item)) for item in KBItem.objects.all()] kbitem_choices = [(item.pk, str(item))
for item in KBItem.objects.all()]
kbitem = KBItem.objects.all() kbitem = KBItem.objects.all()
return render(request, 'helpdesk/ticket_list.html', dict( return render(request, 'helpdesk/ticket_list.html', dict(
@ -1184,7 +1217,8 @@ def load_saved_query(request, query_params=None):
if request.GET.get('saved_query', None): if request.GET.get('saved_query', None):
try: try:
saved_query = SavedSearch.objects.get( saved_query = SavedSearch.objects.get(
Q(pk=request.GET.get('saved_query')) & (Q(shared=True) | Q(user=request.user)) Q(pk=request.GET.get('saved_query')) & (
Q(shared=True) | Q(user=request.user))
) )
except (SavedSearch.DoesNotExist, ValueError): except (SavedSearch.DoesNotExist, ValueError):
raise QueryLoadError() raise QueryLoadError()
@ -1253,7 +1287,8 @@ class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixi
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
self.ticket = form.save(user=self.request.user if self.request.user.is_authenticated else None) self.ticket = form.save(
user=self.request.user if self.request.user.is_authenticated else None)
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
@ -1580,7 +1615,8 @@ def save_query(request):
if not title or not query_encoded: if not title or not query_encoded:
return HttpResponseRedirect(reverse('helpdesk:list')) return HttpResponseRedirect(reverse('helpdesk:list'))
query = SavedSearch(title=title, shared=shared, query=query_encoded, user=request.user) query = SavedSearch(title=title, shared=shared,
query=query_encoded, user=request.user)
query.save() query.save()
return HttpResponseRedirect('%s?saved_query=%s' % (reverse('helpdesk:list'), query.id)) return HttpResponseRedirect('%s?saved_query=%s' % (reverse('helpdesk:list'), query.id))
@ -1679,9 +1715,11 @@ def ticket_cc_add(request, ticket_id):
user = form.cleaned_data.get('user') user = form.cleaned_data.get('user')
email = form.cleaned_data.get('email') email = form.cleaned_data.get('email')
if user and ticket.ticketcc_set.filter(user=user).exists(): if user and ticket.ticketcc_set.filter(user=user).exists():
form.add_error('user', _('Impossible to add twice the same user')) form.add_error(
'user', _('Impossible to add twice the same user'))
elif email and ticket.ticketcc_set.filter(email=email).exists(): elif email and ticket.ticketcc_set.filter(email=email).exists():
form.add_error('email', _('Impossible to add twice the same email address')) form.add_error('email', _(
'Impossible to add twice the same email address'))
else: else:
ticketcc = form.save(commit=False) ticketcc = form.save(commit=False)
ticketcc.ticket = ticket ticketcc.ticket = ticket
@ -1739,7 +1777,8 @@ ticket_dependency_add = staff_member_required(ticket_dependency_add)
@helpdesk_staff_member_required @helpdesk_staff_member_required
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]))
@ -1798,7 +1837,8 @@ def calc_basic_ticket_stats(Tickets):
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
@ -1822,7 +1862,8 @@ def calc_basic_ticket_stats(Tickets):
average_nbr_days_until_ticket_closed = \ average_nbr_days_until_ticket_closed = \
calc_average_nbr_days_until_ticket_resolved(all_closed_tickets) 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 = \ average_nbr_days_until_ticket_closed_last_60_days = \
calc_average_nbr_days_until_ticket_resolved(all_closed_last_60_days) calc_average_nbr_days_until_ticket_resolved(all_closed_last_60_days)

View File

@ -35,8 +35,8 @@ class QuickDjangoTest(object):
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'bootstrap4form', 'bootstrap4form',
## The following commented apps are optional, # The following commented apps are optional,
## related to teams functionalities # related to teams functionalities
#'account', #'account',
#'pinax.invitations', #'pinax.invitations',
#'pinax.teams', #'pinax.teams',
@ -102,11 +102,11 @@ class QuickDjangoTest(object):
TEMPLATES=self.TEMPLATES, TEMPLATES=self.TEMPLATES,
SITE_ID=1, SITE_ID=1,
SECRET_KEY='wowdonotusethisfakesecuritykeyyouneedarealsecure1', SECRET_KEY='wowdonotusethisfakesecuritykeyyouneedarealsecure1',
## The following settings disable teams # The following settings disable teams
HELPDESK_TEAMS_MODEL = 'auth.User', HELPDESK_TEAMS_MODEL='auth.User',
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [], HELPDESK_TEAMS_MIGRATION_DEPENDENCIES=[],
HELPDESK_KBITEM_TEAM_GETTER = lambda _: None, HELPDESK_KBITEM_TEAM_GETTER=lambda _: None,
## test the API # test the API
HELPDESK_ACTIVATE_API_ENDPOINT=True HELPDESK_ACTIVATE_API_ENDPOINT=True
) )