forked from extern/django-helpdesk
Merge branch 'stable' into unstable
This commit is contained in:
commit
7aa14f16e5
@ -97,13 +97,13 @@ WSGI_APPLICATION = 'demo.demodesk.config.wsgi.application'
|
||||
# Some common settings are below.
|
||||
|
||||
HELPDESK_DEFAULT_SETTINGS = {
|
||||
'use_email_as_submitter': True,
|
||||
'email_on_ticket_assign': True,
|
||||
'email_on_ticket_change': True,
|
||||
'login_view_ticketlist': True,
|
||||
'email_on_ticket_apichange': True,
|
||||
'preset_replies': True,
|
||||
'tickets_per_page': 25
|
||||
'use_email_as_submitter': True,
|
||||
'email_on_ticket_assign': True,
|
||||
'email_on_ticket_change': True,
|
||||
'login_view_ticketlist': True,
|
||||
'email_on_ticket_apichange': True,
|
||||
'preset_replies': True,
|
||||
'tickets_per_page': 25
|
||||
}
|
||||
|
||||
# Should the public web portal be enabled?
|
||||
@ -153,7 +153,7 @@ SITE_ID = 1
|
||||
# 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
|
||||
# these are likely to cause problems when testing locally
|
||||
|
@ -2,6 +2,7 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
|
||||
try:
|
||||
@ -21,5 +22,6 @@ def main():
|
||||
raise
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -2,6 +2,7 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
|
||||
try:
|
||||
@ -21,5 +22,6 @@ def main():
|
||||
raise
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -28,7 +28,7 @@ KEYWORDS = []
|
||||
PACKAGES = ['demodesk']
|
||||
REQUIREMENTS = [
|
||||
'django-helpdesk'
|
||||
]
|
||||
]
|
||||
ENTRY_POINTS = {
|
||||
'console_scripts': ['demodesk = demodesk.manage:main']
|
||||
}
|
||||
|
15
docs/conf.py
15
docs/conf.py
@ -11,14 +11,15 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 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
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
# -- General configuration -----------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
@ -87,7 +88,7 @@ pygments_style = 'sphinx'
|
||||
#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
|
||||
# a list of builtin themes.
|
||||
@ -167,7 +168,7 @@ html_static_path = ['_static']
|
||||
htmlhelp_basename = 'django-helpdeskdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
# -- Options for LaTeX output --------------------------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
#latex_paper_size = 'letter'
|
||||
@ -178,8 +179,8 @@ htmlhelp_basename = 'django-helpdeskdoc'
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'django-helpdesk.tex', u'django-helpdesk Documentation',
|
||||
u'Ross Poulton + django-helpdesk Contributors', 'manual'),
|
||||
('index', 'django-helpdesk.tex', u'django-helpdesk Documentation',
|
||||
u'Ross Poulton + django-helpdesk Contributors', 'manual'),
|
||||
]
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
# -- Options for manual page output --------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
|
@ -9,6 +9,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.models import KBCategory
|
||||
from helpdesk.models import KBItem
|
||||
|
||||
|
||||
@admin.register(Queue)
|
||||
class QueueAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent')
|
||||
@ -74,7 +75,8 @@ class FollowUpAdmin(admin.ModelAdmin):
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
@admin.register(KBItem)
|
||||
class KBItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('category', 'title', 'last_updated', 'team', 'order', 'enabled')
|
||||
list_display = ('category', 'title', 'last_updated',
|
||||
'team', 'order', 'enabled')
|
||||
inlines = [KBIAttachmentInline]
|
||||
readonly_fields = ('voted_by', 'downvoted_by')
|
||||
|
||||
|
@ -5,5 +5,6 @@ class HelpdeskConfig(AppConfig):
|
||||
name = 'helpdesk'
|
||||
verbose_name = "Helpdesk"
|
||||
# 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'
|
||||
|
@ -72,7 +72,8 @@ def process_email(quiet=False):
|
||||
|
||||
# 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
|
||||
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)
|
||||
else:
|
||||
log_file_handler = None
|
||||
@ -105,7 +106,8 @@ def pop3_sync(q, logger, server):
|
||||
try:
|
||||
server.stls()
|
||||
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.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]
|
||||
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:
|
||||
full_message = encoding.force_str("\n".join(raw_content), errors='replace')
|
||||
ticket = object_from_message(message=full_message, queue=q, logger=logger)
|
||||
full_message = encoding.force_str(
|
||||
"\n".join(raw_content), errors='replace')
|
||||
ticket = object_from_message(
|
||||
message=full_message, queue=q, logger=logger)
|
||||
|
||||
if ticket:
|
||||
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:
|
||||
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()
|
||||
|
||||
@ -146,7 +153,8 @@ def imap_sync(q, logger, server):
|
||||
try:
|
||||
server.starttls()
|
||||
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
|
||||
settings.QUEUE_EMAIL_BOX_USER,
|
||||
q.email_box_pass or
|
||||
@ -177,14 +185,17 @@ def imap_sync(q, logger, server):
|
||||
status, data = server.fetch(num, '(RFC822)')
|
||||
full_message = encoding.force_str(data[0][1], errors='replace')
|
||||
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:
|
||||
ticket = None # hotfix. Need to work out WHY.
|
||||
if ticket:
|
||||
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:
|
||||
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:
|
||||
logger.error(
|
||||
"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':
|
||||
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))
|
||||
@ -269,17 +281,22 @@ def process_queue(q, logger):
|
||||
logger.info("Processing message %d" % i)
|
||||
with open(m, 'r') as f:
|
||||
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:
|
||||
logger.info("Successfully processed message %d, ticket/comment created.", i)
|
||||
logger.info(
|
||||
"Successfully processed message %d, ticket/comment created.", i)
|
||||
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:
|
||||
logger.error("Unable to delete message %d (%s).", i, str(e))
|
||||
logger.error(
|
||||
"Unable to delete message %d (%s).", i, str(e))
|
||||
else:
|
||||
logger.info("Successfully deleted message %d.", i)
|
||||
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):
|
||||
@ -309,8 +326,10 @@ def is_autoreply(message):
|
||||
So we don't start mail loops
|
||||
"""
|
||||
any_if_this = [
|
||||
False if not message.get("Auto-Submitted") else message.get("Auto-Submitted").lower() != "no",
|
||||
True if message.get("X-Auto-Response-Suppress") in ("DR", "AutoReply", "All") else False,
|
||||
False if not message.get(
|
||||
"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-Unsubscribe"),
|
||||
]
|
||||
@ -340,7 +359,8 @@ def create_ticket_cc(ticket, cc_list):
|
||||
pass
|
||||
|
||||
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)
|
||||
except ValidationError:
|
||||
pass
|
||||
@ -370,7 +390,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
|
||||
if in_reply_to is not None:
|
||||
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:
|
||||
previous_followup = queryset.first()
|
||||
ticket = previous_followup.ticket
|
||||
@ -386,7 +407,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
new = False
|
||||
# Check if the ticket has been merged to another ticket
|
||||
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
|
||||
ticket = ticket.merged_to
|
||||
|
||||
@ -402,7 +424,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
priority=payload['priority'],
|
||||
)
|
||||
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
|
||||
|
||||
@ -413,7 +436,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
|
||||
f = FollowUp(
|
||||
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,
|
||||
public=True,
|
||||
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:
|
||||
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()
|
||||
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):
|
||||
|
||||
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:
|
||||
notifications_to_be_sent.append(email_address)
|
||||
|
||||
autoreply = is_autoreply(message)
|
||||
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:
|
||||
# send mail to appropriate people now depending on what objects
|
||||
# were created and who was CC'd
|
||||
@ -494,7 +521,8 @@ def object_from_message(message, queue, logger):
|
||||
message = email.message_from_string(message)
|
||||
|
||||
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:
|
||||
subject = subject.replace(affix, "")
|
||||
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,
|
||||
# but the getaddresses() function seems to be able to handle just unclosed quotes
|
||||
# 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)
|
||||
if cc:
|
||||
# first, fixup the encoding if necessary
|
||||
cc = [decode_mail_headers(decodeUnknown(message.get_charset(), x)) for x in cc]
|
||||
# get_all checks if multiple CC headers, but individual emails may be comma separated too
|
||||
cc = [decode_mail_headers(decodeUnknown(
|
||||
message.get_charset(), x)) for x in cc]
|
||||
# get_all checks if multiple CC headers, but individual emails may be
|
||||
# comma separated too
|
||||
tempcc = []
|
||||
for hdr in cc:
|
||||
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
|
||||
# the default value is False anyway
|
||||
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 = []
|
||||
for f in EmailReplyParser.read(body).fragments:
|
||||
body_parts.append(f.content)
|
||||
full_body = '\n\n'.join(body_parts)
|
||||
body = EmailReplyParser.parse_reply(body)
|
||||
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)
|
||||
full_body = body
|
||||
# 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")
|
||||
else:
|
||||
try:
|
||||
email_body = encoding.smart_str(part.get_payload(decode=True))
|
||||
email_body = encoding.smart_str(
|
||||
part.get_payload(decode=True))
|
||||
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:
|
||||
# no text has been parsed so far - try such deep parsing for some messages
|
||||
altered_body = email_body.replace("</p>", "</p>\n").replace("<br", "\n<br")
|
||||
# no text has been parsed so far - try such deep parsing
|
||||
# for some messages
|
||||
altered_body = email_body.replace(
|
||||
"</p>", "</p>\n").replace("<br", "\n<br")
|
||||
mail = BeautifulSoup(str(altered_body), "html.parser")
|
||||
full_body = mail.get_text()
|
||||
|
||||
@ -601,7 +638,8 @@ def object_from_message(message, queue, logger):
|
||||
'</html>'
|
||||
) % email_body
|
||||
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")
|
||||
else:
|
||||
@ -627,7 +665,8 @@ def object_from_message(message, queue, logger):
|
||||
# except non_b64_err:
|
||||
# logger.debug("Payload was not base64 encoded, using raw bytes")
|
||||
# # 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)
|
||||
|
||||
counter += 1
|
||||
@ -645,7 +684,8 @@ def object_from_message(message, queue, logger):
|
||||
body = ""
|
||||
|
||||
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(
|
||||
SimpleUploadedFile(
|
||||
_("original_message.eml").replace(
|
||||
@ -660,7 +700,8 @@ def object_from_message(message, queue, logger):
|
||||
smtp_priority = message.get('priority', '')
|
||||
smtp_importance = message.get('importance', '')
|
||||
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 = {
|
||||
'body': body,
|
||||
|
@ -37,40 +37,49 @@ class CustomFieldMixin(object):
|
||||
|
||||
def customfield_to_field(self, field, instanceargs):
|
||||
# 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 field.data_type == 'varchar':
|
||||
fieldclass = forms.CharField
|
||||
instanceargs['max_length'] = field.max_length
|
||||
elif field.data_type == 'text':
|
||||
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
|
||||
elif field.data_type == 'integer':
|
||||
fieldclass = forms.IntegerField
|
||||
instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'})
|
||||
instanceargs['widget'] = forms.NumberInput(
|
||||
attrs={'class': 'form-control'})
|
||||
elif field.data_type == 'decimal':
|
||||
fieldclass = forms.DecimalField
|
||||
instanceargs['decimal_places'] = field.decimal_places
|
||||
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':
|
||||
fieldclass = forms.ChoiceField
|
||||
instanceargs['choices'] = field.get_choices()
|
||||
instanceargs['widget'] = forms.Select(attrs={'class': 'form-control'})
|
||||
instanceargs['widget'] = forms.Select(
|
||||
attrs={'class': 'form-control'})
|
||||
else:
|
||||
# Try to use the immediate equivalences dictionary
|
||||
try:
|
||||
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
||||
# Change widgets for the following classes
|
||||
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:
|
||||
instanceargs['widget'] = forms.DateTimeInput(attrs={'class': 'form-control datetime-field'})
|
||||
instanceargs['widget'] = forms.DateTimeInput(
|
||||
attrs={'class': 'form-control datetime-field'})
|
||||
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:
|
||||
instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'})
|
||||
instanceargs['widget'] = forms.CheckboxInput(
|
||||
attrs={'class': 'form-control'})
|
||||
|
||||
except KeyError:
|
||||
# The data_type was not found anywhere
|
||||
@ -83,10 +92,12 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
@ -96,21 +107,28 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
|
||||
# Disable and add help_text to the merged_to field on this form
|
||||
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():
|
||||
initial_value = None
|
||||
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
|
||||
# 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:
|
||||
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:
|
||||
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:
|
||||
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_TIME_FORMAT)
|
||||
# If it is boolean field, transform the value to a real boolean instead of a string
|
||||
initial_value = datetime.strptime(
|
||||
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:
|
||||
initial_value = 'True' == initial_value
|
||||
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
|
||||
@ -133,9 +151,11 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
field_name = field.replace('custom_', '', 1)
|
||||
customfield = CustomField.objects.get(name=field_name)
|
||||
try:
|
||||
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
|
||||
cfv = TicketCustomFieldValue.objects.get(
|
||||
ticket=self.instance, field=customfield)
|
||||
except ObjectDoesNotExist:
|
||||
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
|
||||
cfv = TicketCustomFieldValue(
|
||||
ticket=self.instance, field=customfield)
|
||||
|
||||
cfv.value = convert_value(value)
|
||||
cfv.save()
|
||||
@ -152,7 +172,8 @@ class EditFollowUpForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Filter not openned tickets here."""
|
||||
super(EditFollowUpForm, self).__init__(*args, **kwargs)
|
||||
self.fields["ticket"].queryset = Ticket.objects.filter(status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
|
||||
self.fields["ticket"].queryset = Ticket.objects.filter(
|
||||
status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
|
||||
|
||||
|
||||
class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
@ -178,7 +199,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
widget=forms.Textarea(attrs={'class': 'form-control'}),
|
||||
label=_('Description of your issue'),
|
||||
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(
|
||||
@ -187,13 +209,16 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
required=True,
|
||||
initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'),
|
||||
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(
|
||||
widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
||||
widget=forms.TextInput(
|
||||
attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
||||
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'),
|
||||
)
|
||||
|
||||
@ -205,7 +230,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
)
|
||||
|
||||
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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -215,7 +241,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
required=False,
|
||||
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):
|
||||
@ -307,7 +334,8 @@ class TicketForm(AbstractTicketForm):
|
||||
submitter_email = forms.EmailField(
|
||||
required=False,
|
||||
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 '
|
||||
'updates to this ticket.'),
|
||||
)
|
||||
@ -335,10 +363,13 @@ class TicketForm(AbstractTicketForm):
|
||||
|
||||
self.fields['queue'].choices = queue_choices
|
||||
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:
|
||||
assignable_users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
|
||||
self.fields['assigned_to'].choices = [('', '--------')] + [(u.id, u.get_username()) for u in assignable_users]
|
||||
assignable_users = User.objects.filter(
|
||||
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()
|
||||
|
||||
def save(self, user):
|
||||
@ -380,7 +411,8 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
Ticket Form creation for all users (public-facing).
|
||||
"""
|
||||
submitter_email = forms.EmailField(
|
||||
widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}),
|
||||
widget=forms.TextInput(
|
||||
attrs={'class': 'form-control', 'type': 'email'}),
|
||||
required=True,
|
||||
label=_('Your E-Mail Address'),
|
||||
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():
|
||||
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:
|
||||
del self.fields[field_name]
|
||||
|
||||
@ -485,9 +518,11 @@ class TicketCCForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCForm, self).__init__(*args, **kwargs)
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
@ -497,9 +532,11 @@ class TicketCCUserForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCUserForm, self).__init__(*args, **kwargs)
|
||||
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:
|
||||
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
|
||||
|
||||
class Meta:
|
||||
@ -538,8 +575,11 @@ class MultipleTicketSelectForm(forms.Form):
|
||||
if len(tickets) < 2:
|
||||
raise ValidationError(_('Please choose at least 2 tickets.'))
|
||||
if len(tickets) > 4:
|
||||
raise ValidationError(_('Impossible to merge more than 4 tickets...'))
|
||||
queues = tickets.order_by('queue').distinct().values_list('queue', flat=True)
|
||||
raise ValidationError(
|
||||
_('Impossible to merge more than 4 tickets...'))
|
||||
queues = tickets.order_by('queue').distinct(
|
||||
).values_list('queue', flat=True)
|
||||
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
|
||||
|
@ -129,7 +129,8 @@ def text_is_spam(text, request):
|
||||
|
||||
|
||||
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 = []
|
||||
|
||||
for attached in attached_files:
|
||||
@ -152,7 +153,8 @@ def process_attachments(followup, attached_files):
|
||||
|
||||
if attached.size < max_email_attachment_size:
|
||||
# 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])
|
||||
|
||||
return attachments
|
||||
|
@ -66,7 +66,8 @@ class Command(BaseCommand):
|
||||
raise CommandError("Queue %s does not exist." % queue)
|
||||
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 = {
|
||||
@ -90,11 +91,13 @@ def create_exclusions(days, occurrences, verbose, queues):
|
||||
while i < occurrences:
|
||||
if day == workdate.weekday():
|
||||
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()
|
||||
|
||||
if verbose:
|
||||
print("Created exclusion for %s %s" % (day_name, workdate))
|
||||
print("Created exclusion for %s %s" %
|
||||
(day_name, workdate))
|
||||
|
||||
for q in queues:
|
||||
esc.queues.add(q)
|
||||
@ -116,7 +119,8 @@ def usage():
|
||||
if __name__ == '__main__':
|
||||
# This script can be run from the command-line or via Django's manage.py.
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
|
||||
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', [
|
||||
'days=', 'occurrences=', 'verbose', 'queues='])
|
||||
except getopt.GetoptError:
|
||||
usage()
|
||||
sys.exit(2)
|
||||
@ -151,4 +155,5 @@ if __name__ == '__main__':
|
||||
sys.exit(2)
|
||||
queues.append(q)
|
||||
|
||||
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
|
||||
create_exclusions(days=days, occurrences=occurrences,
|
||||
verbose=verbose, queues=queues)
|
||||
|
@ -55,14 +55,17 @@ class Command(BaseCommand):
|
||||
self.stdout.write("Preparing Queue %s [%s]" % (q.title, q.slug))
|
||||
|
||||
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:]
|
||||
else:
|
||||
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()
|
||||
|
||||
self.stdout.write(" .. checking permission codename `%s`" % basename)
|
||||
self.stdout.write(
|
||||
" .. checking permission codename `%s`" % basename)
|
||||
|
||||
try:
|
||||
Permission.objects.create(
|
||||
|
@ -62,7 +62,8 @@ class Command(BaseCommand):
|
||||
|
||||
def escalate_tickets(queues, verbose):
|
||||
""" 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:
|
||||
queryset = queryset.filter(slug__in=queues)
|
||||
|
||||
@ -143,7 +144,8 @@ def usage():
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], ['queues=', 'verboseescalation'])
|
||||
opts, args = getopt.getopt(
|
||||
sys.argv[1:], ['queues=', 'verboseescalation'])
|
||||
except getopt.GetoptError:
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
@ -128,7 +128,8 @@ class Queue(models.Model):
|
||||
_('Allow Public Submission?'),
|
||||
blank=True,
|
||||
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(
|
||||
@ -180,7 +181,8 @@ class Queue(models.Model):
|
||||
email_box_type = models.CharField(
|
||||
_('E-Mail Box Type'),
|
||||
max_length=5,
|
||||
choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), ('local', _('Local Directory'))),
|
||||
choices=(('pop3', _('POP 3')), ('imap', _('IMAP')),
|
||||
('local', _('Local Directory'))),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('E-Mail server type for creating tickets automatically '
|
||||
@ -262,7 +264,8 @@ class Queue(models.Model):
|
||||
|
||||
email_box_interval = models.IntegerField(
|
||||
_('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,
|
||||
null=True,
|
||||
default='5',
|
||||
@ -281,7 +284,8 @@ class Queue(models.Model):
|
||||
choices=(('socks4', _('SOCKS4')), ('socks5', _('SOCKS5'))),
|
||||
blank=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(
|
||||
@ -295,7 +299,8 @@ class Queue(models.Model):
|
||||
_('Socks Proxy Port'),
|
||||
blank=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(
|
||||
@ -356,7 +361,8 @@ class Queue(models.Model):
|
||||
"""
|
||||
if not self.email_address:
|
||||
# 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:
|
||||
# already in the right format, so just include it here
|
||||
return u'NO QUEUE EMAIL ADDRESS DEFINED %s' % settings.DEFAULT_FROM_EMAIL
|
||||
@ -532,7 +538,8 @@ class Ticket(models.Model):
|
||||
_('On Hold'),
|
||||
blank=True,
|
||||
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(
|
||||
@ -582,7 +589,8 @@ class Ticket(models.Model):
|
||||
blank=True,
|
||||
null=True,
|
||||
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(
|
||||
@ -648,7 +656,8 @@ class Ticket(models.Model):
|
||||
def send(role, recipient):
|
||||
if recipient and recipient not in recipients and role in roles:
|
||||
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)
|
||||
|
||||
send('submitter', self.submitter_email)
|
||||
@ -844,7 +853,8 @@ class Ticket(models.Model):
|
||||
# Ignore if user has no email address
|
||||
return
|
||||
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
|
||||
ticket_emails = [x.display for x in self.ticketcc_set.all()]
|
||||
@ -1280,7 +1290,8 @@ class EmailTemplate(models.Model):
|
||||
|
||||
html = models.TextField(
|
||||
_('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(
|
||||
@ -1329,7 +1340,8 @@ class KBCategory(models.Model):
|
||||
blank=True,
|
||||
null=True,
|
||||
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(
|
||||
@ -1396,7 +1408,8 @@ class KBItem(models.Model):
|
||||
|
||||
last_updated = models.DateTimeField(
|
||||
_('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,
|
||||
)
|
||||
|
||||
@ -1555,7 +1568,8 @@ class UserSettings(models.Model):
|
||||
|
||||
login_view_ticketlist = models.BooleanField(
|
||||
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,
|
||||
)
|
||||
|
||||
@ -1570,13 +1584,15 @@ class UserSettings(models.Model):
|
||||
|
||||
email_on_ticket_assign = models.BooleanField(
|
||||
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,
|
||||
)
|
||||
|
||||
tickets_per_page = models.IntegerField(
|
||||
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,
|
||||
choices=PAGE_SIZES,
|
||||
)
|
||||
@ -1611,7 +1627,8 @@ def create_usersettings(sender, instance, created, **kwargs):
|
||||
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):
|
||||
@ -1851,14 +1868,16 @@ class CustomField(models.Model):
|
||||
|
||||
ordering = models.IntegerField(
|
||||
_('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,
|
||||
null=True,
|
||||
)
|
||||
|
||||
def _choices_as_array(self):
|
||||
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()
|
||||
return choices
|
||||
choices_as_array = property(_choices_as_array)
|
||||
@ -1912,10 +1931,10 @@ class CustomField(models.Model):
|
||||
|
||||
# Prepare attributes for each types
|
||||
attributes = {
|
||||
'label': self.label,
|
||||
'help_text': self.help_text,
|
||||
'required': self.required,
|
||||
}
|
||||
'label': self.label,
|
||||
'help_text': self.help_text,
|
||||
'required': self.required,
|
||||
}
|
||||
if self.data_type in ('varchar', 'text'):
|
||||
attributes['max_length'] = self.max_length
|
||||
if self.data_type == 'text':
|
||||
|
@ -103,8 +103,10 @@ def get_query_class():
|
||||
class __Query__:
|
||||
def __init__(self, huser, base64query=None, query_params=None):
|
||||
self.huser = huser
|
||||
self.params = query_params if query_params else query_from_base64(base64query)
|
||||
self.base64 = base64query if base64query else query_to_base64(query_params)
|
||||
self.params = query_params if query_params else query_from_base64(
|
||||
base64query)
|
||||
self.base64 = base64query if base64query else query_to_base64(
|
||||
query_params)
|
||||
self.result = None
|
||||
|
||||
def get_search_filter_args(self):
|
||||
@ -128,7 +130,8 @@ class __Query__:
|
||||
"""
|
||||
filter = self.params.get('filtering', {})
|
||||
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)
|
||||
if sorting:
|
||||
sortreverse = self.params.get('sortreverse', None)
|
||||
@ -191,11 +194,13 @@ class __Query__:
|
||||
'text': {
|
||||
'headline': ticket.title + ' - ' + followup.title,
|
||||
'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>'
|
||||
%
|
||||
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket"))
|
||||
(reverse('helpdesk:view', kwargs={
|
||||
'ticket_id': ticket.pk}), _("View ticket"))
|
||||
),
|
||||
},
|
||||
'group': _('Messages'),
|
||||
|
@ -78,7 +78,8 @@ class FollowUpAttachmentSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class FollowUpSerializer(serializers.ModelSerializer):
|
||||
followupattachment_set = FollowUpAttachmentSerializer(many=True, read_only=True)
|
||||
followupattachment_set = FollowUpAttachmentSerializer(
|
||||
many=True, read_only=True)
|
||||
attachments = serializers.ListField(
|
||||
child=serializers.FileField(),
|
||||
write_only=True,
|
||||
@ -133,7 +134,8 @@ class TicketSerializer(serializers.ModelSerializer):
|
||||
|
||||
files = {'attachment': data.pop('attachment', None)}
|
||||
|
||||
ticket_form = TicketForm(data=data, files=files, queue_choices=queue_choices)
|
||||
ticket_form = TicketForm(
|
||||
data=data, files=files, queue_choices=queue_choices)
|
||||
if ticket_form.is_valid():
|
||||
ticket = ticket_form.save(user=self.context['request'].user)
|
||||
ticket.set_custom_field_values()
|
||||
|
@ -25,6 +25,9 @@ except AttributeError:
|
||||
|
||||
HAS_TAG_SUPPORT = False
|
||||
|
||||
# Use international timezones
|
||||
USE_TZ: bool = True
|
||||
|
||||
# check for secure cookie support
|
||||
if os.environ.get('SECURE_PROXY_SSL_HEADER'):
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
@ -43,13 +46,13 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
|
||||
|
||||
# Enable the Dependencies field on ticket view
|
||||
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings,
|
||||
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
|
||||
True)
|
||||
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
|
||||
True)
|
||||
|
||||
# Enable the Time spent on field on ticket view
|
||||
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings,
|
||||
'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET',
|
||||
True)
|
||||
'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET',
|
||||
True)
|
||||
|
||||
# raises a 404 to anon users. It's like it was invisible
|
||||
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
|
||||
@ -60,10 +63,13 @@ HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
|
||||
HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
|
||||
|
||||
# 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?
|
||||
HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False)
|
||||
# show extended navigation by default, to all users, irrespective of staff
|
||||
# status?
|
||||
HELPDESK_NAVIGATION_ENABLED = getattr(
|
||||
settings, 'HELPDESK_NAVIGATION_ENABLED', False)
|
||||
|
||||
# use public CDNs to serve jquery and other javascript by default?
|
||||
# otherwise, use built-in static copy
|
||||
@ -81,7 +87,8 @@ HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings,
|
||||
["en", "de", "es", "fr", "it", "ru"])
|
||||
|
||||
# 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.
|
||||
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
|
||||
@ -93,17 +100,19 @@ HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
|
||||
|
||||
# URL schemes that are allowed within links
|
||||
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 #
|
||||
############################
|
||||
|
||||
# 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?
|
||||
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)
|
||||
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
|
||||
@ -134,8 +143,10 @@ CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M"
|
||||
|
||||
''' options for update_ticket views '''
|
||||
# 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
|
||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
|
||||
# can be True/False or a callable accepting the active user and returning
|
||||
# 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)):
|
||||
warnings.warn(
|
||||
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
|
||||
@ -151,14 +162,18 @@ HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
|
||||
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
|
||||
settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False)
|
||||
|
||||
# make all updates public by default? this will hide the 'is this update public' checkbox
|
||||
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
|
||||
# make all updates public by default? this will hide the 'is this update
|
||||
# public' checkbox
|
||||
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(
|
||||
settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
|
||||
|
||||
# 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
|
||||
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.
|
||||
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
|
||||
@ -170,11 +185,13 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
|
||||
raise ImproperlyConfigured
|
||||
|
||||
# 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
|
||||
# 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)
|
||||
|
||||
|
||||
########################################
|
||||
@ -186,7 +203,8 @@ HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
||||
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
#################
|
||||
@ -201,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)
|
||||
|
||||
# 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?
|
||||
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
|
||||
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)
|
||||
|
||||
# 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_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)
|
||||
HELPDESK_TEAMS_MODEL = getattr(
|
||||
settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.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
|
||||
# Useful if you get forwards dropped from them while they are useful part of request
|
||||
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
|
||||
# Useful if you get forwards dropped from them while they are useful part
|
||||
# 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
|
||||
# which is quite noisy but very helpful for complicated markup, forwards and so on
|
||||
# (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)
|
||||
|
@ -58,12 +58,15 @@ def send_templated_mail(template_name,
|
||||
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
subject_part = from_string(
|
||||
@ -77,10 +80,12 @@ def send_templated_mail(template_name,
|
||||
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
|
||||
).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
|
||||
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(
|
||||
"{%% extends '%s' %%}"
|
||||
@ -112,7 +117,8 @@ def send_templated_mail(template_name,
|
||||
try:
|
||||
return msg.send()
|
||||
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:
|
||||
raise e
|
||||
return 0
|
||||
|
@ -19,4 +19,5 @@ def helpdesk_staff(user):
|
||||
try:
|
||||
return is_helpdesk_staff(user)
|
||||
except Exception:
|
||||
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed")
|
||||
logger.exception(
|
||||
"'helpdesk_staff' template tag (django-helpdesk) crashed")
|
||||
|
@ -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
|
||||
"""
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
# If NoneType return empty string, else return original value
|
||||
new_value = "" if value is None else value
|
||||
|
@ -72,7 +72,8 @@ class TicketTest(APITestCase):
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
created_ticket = Ticket.objects.get()
|
||||
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.priority, 4)
|
||||
self.assertEqual(created_ticket.followup_set.count(), 1)
|
||||
@ -80,16 +81,20 @@ class TicketTest(APITestCase):
|
||||
def test_create_api_ticket_with_basic_auth(self):
|
||||
username = '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')
|
||||
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
|
||||
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(
|
||||
'/api/tickets/',
|
||||
{
|
||||
@ -111,21 +116,27 @@ class TicketTest(APITestCase):
|
||||
created_ticket = Ticket.objects.last()
|
||||
self.assertEqual(created_ticket.title, 'Title')
|
||||
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.submitter_email, 'test@mail.com')
|
||||
self.assertEqual(created_ticket.priority, 1)
|
||||
self.assertFalse(created_ticket.on_hold) # on_hold is False on creation
|
||||
self.assertEqual(created_ticket.status, Ticket.OPEN_STATUS) # status is always open on creation
|
||||
# on_hold is False 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.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):
|
||||
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')
|
||||
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)
|
||||
response = self.client.put(
|
||||
@ -160,7 +171,8 @@ class TicketTest(APITestCase):
|
||||
|
||||
def test_partial_edit_api_ticket(self):
|
||||
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)
|
||||
response = self.client.patch(
|
||||
@ -176,7 +188,8 @@ class TicketTest(APITestCase):
|
||||
|
||||
def test_delete_api_ticket(self):
|
||||
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)
|
||||
response = self.client.delete('/api/tickets/%d/' % test_ticket.id)
|
||||
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
|
||||
@ -200,7 +213,8 @@ class TicketTest(APITestCase):
|
||||
Blue
|
||||
Red
|
||||
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)
|
||||
self.client.force_authenticate(staff_user)
|
||||
@ -214,7 +228,8 @@ class TicketTest(APITestCase):
|
||||
'priority': 4
|
||||
})
|
||||
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
|
||||
response = self.client.post('/api/tickets/', {
|
||||
@ -283,7 +298,8 @@ class TicketTest(APITestCase):
|
||||
def test_create_api_ticket_with_attachment(self):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
test_file = SimpleUploadedFile('file.jpg', b'file_content', content_type='image/jpg')
|
||||
test_file = SimpleUploadedFile(
|
||||
'file.jpg', b'file_content', content_type='image/jpg')
|
||||
response = self.client.post('/api/tickets/', {
|
||||
'queue': self.queue.id,
|
||||
'title': 'Test title',
|
||||
@ -295,11 +311,13 @@ class TicketTest(APITestCase):
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
created_ticket = Ticket.objects.get()
|
||||
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.priority, 4)
|
||||
self.assertEqual(created_ticket.followup_set.count(), 1)
|
||||
self.assertEqual(created_ticket.followup_set.get().followupattachment_set.count(), 1)
|
||||
self.assertEqual(created_ticket.followup_set.get(
|
||||
).followupattachment_set.count(), 1)
|
||||
attachment = created_ticket.followup_set.get().followupattachment_set.get()
|
||||
self.assertEqual(
|
||||
attachment.file.name,
|
||||
@ -310,8 +328,10 @@ class TicketTest(APITestCase):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
ticket = Ticket.objects.create(queue=self.queue, title='Test')
|
||||
test_file_1 = SimpleUploadedFile('file.jpg', b'file_content', content_type='image/jpg')
|
||||
test_file_2 = SimpleUploadedFile('doc.pdf', b'Doc content', content_type='application/pdf')
|
||||
test_file_1 = SimpleUploadedFile(
|
||||
'file.jpg', b'file_content', content_type='image/jpg')
|
||||
test_file_2 = SimpleUploadedFile(
|
||||
'doc.pdf', b'Doc content', content_type='application/pdf')
|
||||
|
||||
response = self.client.post('/api/followups/', {
|
||||
'ticket': ticket.id,
|
||||
@ -327,7 +347,11 @@ class TicketTest(APITestCase):
|
||||
self.assertEqual(created_followup.title, 'Test')
|
||||
self.assertEqual(created_followup.comment, 'Test answer\nMulti lines')
|
||||
self.assertEqual(created_followup.followupattachment_set.count(), 2)
|
||||
self.assertEqual(created_followup.followupattachment_set.first().filename, 'doc.pdf')
|
||||
self.assertEqual(created_followup.followupattachment_set.first().mime_type, 'application/pdf')
|
||||
self.assertEqual(created_followup.followupattachment_set.last().filename, 'file.jpg')
|
||||
self.assertEqual(created_followup.followupattachment_set.last().mime_type, 'image/jpg')
|
||||
self.assertEqual(
|
||||
created_followup.followupattachment_set.first().filename, 'doc.pdf')
|
||||
self.assertEqual(
|
||||
created_followup.followupattachment_set.first().mime_type, 'application/pdf')
|
||||
self.assertEqual(
|
||||
created_followup.followupattachment_set.last().filename, 'file.jpg')
|
||||
self.assertEqual(
|
||||
created_followup.followupattachment_set.last().mime_type, 'image/jpg')
|
||||
|
@ -47,7 +47,8 @@ class AttachmentIntegrationTests(TestCase):
|
||||
}
|
||||
|
||||
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.update({
|
||||
'queue': self.queue_public.id,
|
||||
@ -55,17 +56,20 @@ class AttachmentIntegrationTests(TestCase):
|
||||
})
|
||||
|
||||
# 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)
|
||||
|
||||
# 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:
|
||||
disk_content = file_on_disk.read()
|
||||
self.assertEqual(disk_content, 'attached file content')
|
||||
|
||||
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.update({
|
||||
'queue': self.queue_public.id,
|
||||
@ -73,11 +77,13 @@ class AttachmentIntegrationTests(TestCase):
|
||||
})
|
||||
|
||||
# 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)
|
||||
|
||||
# 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:
|
||||
disk_content = smart_str(file_on_disk.read(), 'utf-8')
|
||||
self.assertEqual(disk_content, 'โจ')
|
||||
@ -105,7 +111,8 @@ class AttachmentUnitTests(TestCase):
|
||||
@skip("Rework with model relocation")
|
||||
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 """
|
||||
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(
|
||||
file=self.test_file,
|
||||
filename=self.file_attrs['filename'],
|
||||
@ -154,8 +161,10 @@ class AttachmentUnitTests(TestCase):
|
||||
@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):
|
||||
""" 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]
|
||||
# Attachment object was zeroth positional arg (i.e. self) of att.save call
|
||||
filename, fileobj = lib.process_attachments(
|
||||
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
|
||||
|
||||
mock_att_save.assert_called_once_with(attachment_obj)
|
||||
|
@ -37,7 +37,8 @@ class GetEmailCommonTests(TestCase):
|
||||
# tests correct syntax for command line option
|
||||
def test_get_email_quiet_option(self):
|
||||
"""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]:
|
||||
with mock.patch.object(Command, 'handle', return_value=None) as mocked_handle:
|
||||
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:
|
||||
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
|
||||
assert ticket.title == (
|
||||
@ -68,16 +70,19 @@ class GetEmailCommonTests(TestCase):
|
||||
"""
|
||||
with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml")) as fd:
|
||||
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.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)
|
||||
self.assertEqual(len(followups), 1)
|
||||
followup = followups[0]
|
||||
attachments = FollowUpAttachment.objects.filter(followup=followup)
|
||||
self.assertEqual(len(attachments), 1)
|
||||
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):
|
||||
"""
|
||||
@ -86,7 +91,8 @@ class GetEmailCommonTests(TestCase):
|
||||
"""
|
||||
with open(os.path.join(THIS_DIR, "test_files/all-special-chars.eml")) as fd:
|
||||
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.description, "íářčšáíéřášč")
|
||||
|
||||
@ -98,14 +104,17 @@ class GetEmailCommonTests(TestCase):
|
||||
"""
|
||||
with open(os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml")) as fd:
|
||||
test_email = fd.read()
|
||||
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger)
|
||||
self.assertEqual(ticket.title, "Fwd: Cyklozaměstnavatel - změna vyhodnocení")
|
||||
ticket = helpdesk.email.object_from_message(
|
||||
test_email, self.queue_public, self.logger)
|
||||
self.assertEqual(
|
||||
ticket.title, "Fwd: Cyklozaměstnavatel - změna vyhodnocení")
|
||||
self.assertIn("prosazuje lepší", ticket.description)
|
||||
followups = FollowUp.objects.filter(ticket=ticket)
|
||||
followup = followups[0]
|
||||
attachments = FollowUpAttachment.objects.filter(followup=followup)
|
||||
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)
|
||||
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:
|
||||
test_email = fd.read()
|
||||
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger)
|
||||
self.assertEqual(ticket.title, "Test with original message from GitHub")
|
||||
ticket = helpdesk.email.object_from_message(
|
||||
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)
|
||||
assert "Hello there!" not in ticket.description, ticket.description
|
||||
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):
|
||||
@ -160,11 +172,13 @@ class GetEmailParametricTemplate(object):
|
||||
For each email source supported, we mock the backend to provide
|
||||
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_subject = "My visit to Sør-Trøndelag"
|
||||
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)
|
||||
|
||||
if self.socks:
|
||||
@ -184,38 +198,51 @@ class GetEmailParametricTemplate(object):
|
||||
|
||||
call_command('get_email')
|
||||
|
||||
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2')
|
||||
mocked_listdir.assert_called_with(
|
||||
'/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename2')
|
||||
|
||||
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 = {
|
||||
'1': ("+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.list = mock.Mock(return_value=pop3_mail_list)
|
||||
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x])
|
||||
mocked_poplib_server.list = mock.Mock(
|
||||
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:
|
||||
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server)
|
||||
mocked_poplib.POP3 = mock.Mock(
|
||||
return_value=mocked_poplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
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 = {
|
||||
"1": ("OK", (("1", test_email),)),
|
||||
"2": ("OK", (("2", test_email),)),
|
||||
}
|
||||
imap_mail_list = ("OK", ("1 2",))
|
||||
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)
|
||||
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x])
|
||||
# we ignore the second arg as the data item/mime-part is
|
||||
# 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:
|
||||
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server)
|
||||
mocked_imaplib.IMAP4 = mock.Mock(
|
||||
return_value=mocked_imaplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
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
|
||||
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_subject = "Commas in From lines"
|
||||
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)
|
||||
|
||||
if self.socks:
|
||||
@ -256,38 +285,51 @@ class GetEmailParametricTemplate(object):
|
||||
|
||||
call_command('get_email')
|
||||
|
||||
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2')
|
||||
mocked_listdir.assert_called_with(
|
||||
'/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename2')
|
||||
|
||||
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 = {
|
||||
'1': ("+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.list = mock.Mock(return_value=pop3_mail_list)
|
||||
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x])
|
||||
mocked_poplib_server.list = mock.Mock(
|
||||
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:
|
||||
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server)
|
||||
mocked_poplib.POP3 = mock.Mock(
|
||||
return_value=mocked_poplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
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 = {
|
||||
"1": ("OK", (("1", test_email),)),
|
||||
"2": ("OK", (("2", test_email),)),
|
||||
}
|
||||
imap_mail_list = ("OK", ("1 2",))
|
||||
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)
|
||||
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x])
|
||||
# we ignore the second arg as the data item/mime-part is
|
||||
# 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:
|
||||
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server)
|
||||
mocked_imaplib.IMAP4 = mock.Mock(
|
||||
return_value=mocked_imaplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
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
|
||||
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_subject = "My visit to Sør-Trøndelag"
|
||||
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)
|
||||
|
||||
if self.socks:
|
||||
@ -332,38 +376,51 @@ class GetEmailParametricTemplate(object):
|
||||
|
||||
call_command('get_email')
|
||||
|
||||
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2')
|
||||
mocked_listdir.assert_called_with(
|
||||
'/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename2')
|
||||
|
||||
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 = {
|
||||
'1': ("+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.list = mock.Mock(return_value=pop3_mail_list)
|
||||
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x])
|
||||
mocked_poplib_server.list = mock.Mock(
|
||||
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:
|
||||
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server)
|
||||
mocked_poplib.POP3 = mock.Mock(
|
||||
return_value=mocked_poplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
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 = {
|
||||
"1": ("OK", (("1", test_email),)),
|
||||
"2": ("OK", (("2", test_email),)),
|
||||
}
|
||||
imap_mail_list = ("OK", ("1 2",))
|
||||
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)
|
||||
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x])
|
||||
# we ignore the second arg as the data item/mime-part is
|
||||
# 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:
|
||||
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server)
|
||||
mocked_imaplib.IMAP4 = mock.Mock(
|
||||
return_value=mocked_imaplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
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
|
||||
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.text import MIMEText
|
||||
|
||||
@ -396,7 +454,8 @@ class GetEmailParametricTemplate(object):
|
||||
cc = cc_one + ", " + cc_two
|
||||
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['Subject'] = subject
|
||||
msg['From'] = me
|
||||
@ -446,38 +505,51 @@ class GetEmailParametricTemplate(object):
|
||||
|
||||
call_command('get_email')
|
||||
|
||||
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2')
|
||||
mocked_listdir.assert_called_with(
|
||||
'/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename2')
|
||||
|
||||
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 = {
|
||||
'1': ("+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.list = mock.Mock(return_value=pop3_mail_list)
|
||||
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x])
|
||||
mocked_poplib_server.list = mock.Mock(
|
||||
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:
|
||||
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server)
|
||||
mocked_poplib.POP3 = mock.Mock(
|
||||
return_value=mocked_poplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
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 = {
|
||||
"1": ("OK", (("1", msg.as_string()),)),
|
||||
"2": ("OK", (("2", msg.as_string()),)),
|
||||
}
|
||||
imap_mail_list = ("OK", ("1 2",))
|
||||
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)
|
||||
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x])
|
||||
# we ignore the second arg as the data item/mime-part is
|
||||
# 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:
|
||||
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server)
|
||||
mocked_imaplib.IMAP4 = mock.Mock(
|
||||
return_value=mocked_imaplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
ticket1 = get_object_or_404(Ticket, pk=1)
|
||||
@ -537,41 +609,54 @@ class GetEmailParametricTemplate(object):
|
||||
|
||||
call_command('get_email')
|
||||
|
||||
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1')
|
||||
mocked_listdir.assert_called_with(
|
||||
'/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call(
|
||||
'/var/lib/mail/helpdesk/filename1')
|
||||
|
||||
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 = {
|
||||
'1': ("+OK", test_email.split('\n')),
|
||||
}
|
||||
pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len))
|
||||
mocked_poplib_server = mock.Mock()
|
||||
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list)
|
||||
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails['1'])
|
||||
mocked_poplib_server.list = mock.Mock(
|
||||
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:
|
||||
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server)
|
||||
mocked_poplib.POP3 = mock.Mock(
|
||||
return_value=mocked_poplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
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 = {
|
||||
"1": ("OK", (("1", test_email),)),
|
||||
}
|
||||
imap_mail_list = ("OK", ("1",))
|
||||
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)
|
||||
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x])
|
||||
# we ignore the second arg as the data item/mime-part is
|
||||
# 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:
|
||||
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server)
|
||||
mocked_imaplib.IMAP4 = mock.Mock(
|
||||
return_value=mocked_imaplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
ticket1 = get_object_or_404(Ticket, pk=1)
|
||||
self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id)
|
||||
self.assertEqual(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""")
|
||||
self.assertEqual(
|
||||
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
|
||||
followup1 = get_object_or_404(FollowUp, pk=1)
|
||||
self.assertEqual(followup1.ticket.id, 1)
|
||||
@ -680,9 +765,11 @@ class GetEmailCCHandling(TestCase):
|
||||
self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 1)
|
||||
ccstaff = get_object_or_404(TicketCC, pk=1)
|
||||
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"
|
||||
# NOTE: CC emails are in alphabetical order and must be tested as such!
|
||||
# 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"
|
||||
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 = "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)
|
||||
|
||||
with mock.patch('os.listdir') as mocked_listdir, \
|
||||
@ -755,5 +845,6 @@ for method, socks in case_matrix:
|
||||
test_name = 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)
|
||||
|
@ -4,7 +4,8 @@ from django.test import TestCase
|
||||
|
||||
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):
|
||||
@ -43,13 +44,16 @@ class KBTests(TestCase):
|
||||
self.assertContains(response, 'This is a test category')
|
||||
|
||||
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, 'KBItem 1')
|
||||
self.assertContains(response, 'KBItem 2')
|
||||
self.assertContains(response, 'Create New Ticket Queue:')
|
||||
self.client.login(username=self.user.get_username(), password='password')
|
||||
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", )))
|
||||
self.client.login(username=self.user.get_username(),
|
||||
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, '0 open tickets')
|
||||
ticket = Ticket.objects.create(
|
||||
@ -58,23 +62,30 @@ class KBTests(TestCase):
|
||||
kbitem=self.kbitem1,
|
||||
)
|
||||
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')
|
||||
|
||||
def test_kb_vote(self):
|
||||
self.client.login(username=self.user.get_username(), password='password')
|
||||
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.client.login(username=self.user.get_username(),
|
||||
password='password')
|
||||
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)
|
||||
response = self.client.get(cat_url)
|
||||
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)
|
||||
response = self.client.get(cat_url)
|
||||
self.assertContains(response, '0 people found this answer useful of 1')
|
||||
|
||||
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)
|
||||
# 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&title=lol")
|
||||
self.assertContains(
|
||||
response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol")
|
||||
|
@ -6,7 +6,8 @@ from django.test import TestCase
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
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
|
||||
|
||||
|
||||
@ -26,13 +27,15 @@ class KBDisabledTestCase(TestCase):
|
||||
"""Test proper rendering of navigation.html by accessing the dashboard"""
|
||||
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')
|
||||
try:
|
||||
response = self.client.get(reverse('helpdesk:dashboard'))
|
||||
except NoReverseMatch as e:
|
||||
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:
|
||||
raise
|
||||
else:
|
||||
@ -75,7 +78,8 @@ class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
"""
|
||||
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))
|
||||
|
||||
@ -91,7 +95,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
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):
|
||||
"""Staff and non-staff users are correctly identified"""
|
||||
@ -118,7 +123,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
from helpdesk.decorators import is_helpdesk_staff
|
||||
|
||||
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)
|
||||
self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
|
||||
|
||||
@ -128,7 +134,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
"""
|
||||
user = get_staff_user()
|
||||
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')
|
||||
|
||||
@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.
|
||||
"""
|
||||
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(
|
||||
title="Foo",
|
||||
slug="test_queue",
|
||||
)
|
||||
rss_urls = [
|
||||
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_unassigned'),
|
||||
reverse('helpdesk:rss_activity'),
|
||||
]
|
||||
for rss_url in rss_urls:
|
||||
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):
|
||||
@ -168,7 +178,8 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
"""
|
||||
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))
|
||||
|
||||
@ -179,7 +190,8 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
|
||||
def test_custom_staff_fail(self):
|
||||
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))
|
||||
|
||||
@ -264,7 +276,8 @@ class ReturnToTicketTestCase(TestCase):
|
||||
def test_non_staff_user(self):
|
||||
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()
|
||||
response = return_to_ticket(user, helpdesk_settings, ticket)
|
||||
self.assertEqual(response['location'], ticket.ticket_url)
|
||||
|
@ -56,11 +56,13 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
for ticket_number in range(1, identifier + 1):
|
||||
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,
|
||||
)
|
||||
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,
|
||||
assigned_to=user,
|
||||
)
|
||||
@ -80,7 +82,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
# Regular users
|
||||
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'))
|
||||
self.assertEqual(
|
||||
len(response.context['unassigned_tickets']),
|
||||
@ -117,7 +120,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
# Regular users
|
||||
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'))
|
||||
self.assertEqual(
|
||||
len(response.context['dash_tickets']),
|
||||
@ -164,9 +168,11 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
"""
|
||||
# Regular users
|
||||
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'))
|
||||
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(
|
||||
len(tickets),
|
||||
identifier * 2,
|
||||
@ -186,7 +192,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
# Superuser
|
||||
self.client.login(username='superuser', password='superuser')
|
||||
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(
|
||||
len(tickets),
|
||||
6,
|
||||
@ -201,7 +208,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
"""
|
||||
# Regular users
|
||||
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:run_report', kwargs={'report': 'userqueue'})
|
||||
)
|
||||
@ -212,9 +220,11 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
2,
|
||||
'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(
|
||||
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,
|
||||
'Tickets in report were not properly limited by queue membership'
|
||||
)
|
||||
@ -224,7 +234,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
2,
|
||||
'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(
|
||||
response.context['headings'][1],
|
||||
"Queue %d" % identifier,
|
||||
@ -245,7 +256,8 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
)
|
||||
# Superuser should see the total ticket count of three tickets
|
||||
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,
|
||||
'Tickets in report were improperly limited by queue membership for a superuser'
|
||||
)
|
||||
|
@ -70,7 +70,8 @@ class PublicActionsTestCase(TestCase):
|
||||
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
|
||||
self.assertEqual(ticket.status, Ticket.CLOSED_STATUS)
|
||||
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.status = old_status
|
||||
|
@ -5,7 +5,8 @@ from django.urls import reverse
|
||||
from helpdesk.models import KBCategory, KBItem, Queue, Ticket
|
||||
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):
|
||||
@ -58,7 +59,8 @@ class QueryTests(TestCase):
|
||||
def test_query_basic(self):
|
||||
self.loginUser()
|
||||
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(
|
||||
response.json(),
|
||||
{
|
||||
@ -76,12 +78,14 @@ class QueryTests(TestCase):
|
||||
query = query_to_base64(
|
||||
{'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(
|
||||
response.json(),
|
||||
{
|
||||
"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,
|
||||
"recordsTotal": 1,
|
||||
"draw": 0,
|
||||
@ -93,12 +97,14 @@ class QueryTests(TestCase):
|
||||
query = query_to_base64(
|
||||
{'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(
|
||||
response.json(),
|
||||
{
|
||||
"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,
|
||||
"recordsTotal": 1,
|
||||
"draw": 0,
|
||||
|
@ -78,10 +78,13 @@ class TicketActionsTestCase(TestCase):
|
||||
ticket = Ticket.objects.create(**ticket_data)
|
||||
ticket_id = ticket.id
|
||||
|
||||
response = self.client.get(reverse('helpdesk:delete', kwargs={'ticket_id': ticket_id}), follow=True)
|
||||
self.assertContains(response, 'Are you sure you want to delete this ticket')
|
||||
response = self.client.get(reverse('helpdesk:delete', kwargs={
|
||||
'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_url = first_redirect[0]
|
||||
|
||||
@ -123,7 +126,8 @@ class TicketActionsTestCase(TestCase):
|
||||
post_data = {
|
||||
'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')
|
||||
|
||||
# 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)
|
||||
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')
|
||||
post_data = {
|
||||
'new_status': Ticket.OPEN_STATUS,
|
||||
'owner': self.user2.id,
|
||||
'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')
|
||||
|
||||
def test_can_access_ticket(self):
|
||||
@ -175,8 +181,10 @@ class TicketActionsTestCase(TestCase):
|
||||
# create ticket
|
||||
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
|
||||
ticket = Ticket.objects.create(**initial_data)
|
||||
self.assertEqual(HelpdeskUser(self.user).can_access_ticket(ticket), True)
|
||||
self.assertEqual(HelpdeskUser(self.user2).can_access_ticket(ticket), False)
|
||||
self.assertEqual(HelpdeskUser(
|
||||
self.user).can_access_ticket(ticket), True)
|
||||
self.assertEqual(HelpdeskUser(
|
||||
self.user2).can_access_ticket(ticket), False)
|
||||
|
||||
def test_num_to_link(self):
|
||||
"""Test that we are correctly expanding links to tickets from IDs"""
|
||||
@ -197,10 +205,13 @@ class TicketActionsTestCase(TestCase):
|
||||
|
||||
# generate the URL text
|
||||
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)
|
||||
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))
|
||||
result2 = num_to_link(
|
||||
'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):
|
||||
self.loginUser()
|
||||
@ -221,7 +232,8 @@ class TicketActionsTestCase(TestCase):
|
||||
status=Ticket.RESOLVED_STATUS,
|
||||
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_created = ticket_1.created
|
||||
due_date = timezone.now()
|
||||
@ -233,7 +245,8 @@ class TicketActionsTestCase(TestCase):
|
||||
due_date=due_date,
|
||||
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')
|
||||
|
||||
# Create custom fields and set values for tickets
|
||||
@ -243,16 +256,19 @@ class TicketActionsTestCase(TestCase):
|
||||
data_type='varchar',
|
||||
)
|
||||
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.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(
|
||||
name='number',
|
||||
label='Number',
|
||||
data_type='integer',
|
||||
)
|
||||
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
|
||||
response = self.client.post(
|
||||
@ -263,7 +279,8 @@ class TicketActionsTestCase(TestCase):
|
||||
},
|
||||
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.assertContains(response, ticket_1.description)
|
||||
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.description, ticket_2.description)
|
||||
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(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])
|
||||
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
|
||||
field=custom_field_1).value, ticket_1_field_1)
|
||||
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
|
||||
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])
|
||||
|
@ -57,7 +57,8 @@ class TestTicketLookupPublicEnabled(TestCase):
|
||||
|
||||
def test_add_email_to_ticketcc_if_not_in(self):
|
||||
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.save()
|
||||
email_1 = 'user1@mail.com'
|
||||
@ -66,20 +67,25 @@ class TestTicketLookupPublicEnabled(TestCase):
|
||||
# Add new email to CC
|
||||
email_2 = 'user2@mail.com'
|
||||
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
|
||||
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
|
||||
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.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
|
||||
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)
|
||||
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1)
|
||||
self.assertEqual(ticketcc_1.ticket, ticket_2)
|
||||
|
@ -51,7 +51,6 @@ class TicketBasicsTestCase(TestCase):
|
||||
self.client = Client()
|
||||
|
||||
def test_create_ticket_instance_from_payload(self):
|
||||
|
||||
"""
|
||||
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,
|
||||
}
|
||||
|
||||
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_url = last_redirect[0]
|
||||
# last_redirect_status = last_redirect[1]
|
||||
@ -95,7 +95,6 @@ class TicketBasicsTestCase(TestCase):
|
||||
# Follow up is anonymous
|
||||
self.assertIsNone(ticket.followup_set.first().user)
|
||||
|
||||
|
||||
def test_create_ticket_public_with_hidden_fields(self):
|
||||
email_count = len(mail.outbox)
|
||||
|
||||
@ -110,11 +109,11 @@ class TicketBasicsTestCase(TestCase):
|
||||
'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()
|
||||
self.assertEqual(ticket.priority, 4)
|
||||
|
||||
|
||||
def test_create_ticket_authorized(self):
|
||||
email_count = len(mail.outbox)
|
||||
self.client.force_login(self.user)
|
||||
@ -130,7 +129,8 @@ class TicketBasicsTestCase(TestCase):
|
||||
'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_url = last_redirect[0]
|
||||
# last_redirect_status = last_redirect[1]
|
||||
@ -188,7 +188,8 @@ class TicketBasicsTestCase(TestCase):
|
||||
'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()
|
||||
last_redirect = response.redirect_chain[-1]
|
||||
@ -221,7 +222,8 @@ class TicketBasicsTestCase(TestCase):
|
||||
'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_url = last_redirect[0]
|
||||
# last_redirect_status = last_redirect[1]
|
||||
@ -266,7 +268,6 @@ class EmailInteractionsTestCase(TestCase):
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
|
||||
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.
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
@ -417,8 +418,10 @@ class EmailInteractionsTestCase(TestCase):
|
||||
# Ensure that the submitter is notified
|
||||
self.assertIn(submitter_email, mail.outbox[0].to)
|
||||
|
||||
# Ensure that the queue's email was not subscribed to the event notifications.
|
||||
self.assertRaises(TicketCC.DoesNotExist, TicketCC.objects.get, ticket=ticket, email=to_list[0])
|
||||
# Ensure that the queue's email was not subscribed to the event
|
||||
# notifications.
|
||||
self.assertRaises(TicketCC.DoesNotExist,
|
||||
TicketCC.objects.get, ticket=ticket, email=to_list[0])
|
||||
|
||||
for cc_email in cc_list:
|
||||
|
||||
@ -825,14 +828,16 @@ class EmailInteractionsTestCase(TestCase):
|
||||
msg.__setitem__('Message-ID', message_id)
|
||||
msg.__setitem__('Subject', self.ticket_data['title'])
|
||||
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__('Content-Type', 'text/plain;')
|
||||
msg.set_payload(self.ticket_data['description'])
|
||||
|
||||
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)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
@ -954,14 +959,16 @@ class EmailInteractionsTestCase(TestCase):
|
||||
msg.__setitem__('Message-ID', message_id)
|
||||
msg.__setitem__('Subject', self.ticket_data['title'])
|
||||
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__('Content-Type', 'text/plain;')
|
||||
msg.set_payload(self.ticket_data['description'])
|
||||
|
||||
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)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
@ -994,11 +1001,13 @@ class EmailInteractionsTestCase(TestCase):
|
||||
reply.__setitem__('In-Reply-To', message_id)
|
||||
reply.__setitem__('Subject', self.ticket_data['title'])
|
||||
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.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)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
@ -1093,8 +1102,12 @@ class EmailInteractionsTestCase(TestCase):
|
||||
answer="A KB Item",
|
||||
)
|
||||
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)
|
||||
self.assertContains(response, '<option value="1" selected>KBItem 1</option>')
|
||||
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">')
|
||||
self.assertContains(
|
||||
response, '<option value="1" selected>KBItem 1</option>')
|
||||
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">')
|
||||
|
@ -26,5 +26,6 @@ class TicketActionsTestCase(TestCase):
|
||||
|
||||
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")
|
||||
|
@ -64,12 +64,16 @@ urlpatterns = [
|
||||
name="followup_delete",
|
||||
),
|
||||
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>/delete/", staff.delete_ticket, name="delete"),
|
||||
path("tickets/<int:ticket_id>/update/",
|
||||
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>/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/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(
|
||||
"tickets/<int:ticket_id>/cc/delete/<int:cc_id>/",
|
||||
staff.ticket_cc_del,
|
||||
@ -93,13 +97,15 @@ urlpatterns = [
|
||||
re_path(r"^raw/(?P<type>\w+)/$", staff.raw_details, name="raw"),
|
||||
path("rss/", staff.rss_list, name="rss_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("delete_query/<int:id>/", staff.delete_saved_query, name="delete_query"),
|
||||
path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"),
|
||||
path("ignore/", staff.email_ignore, name="email_ignore"),
|
||||
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(
|
||||
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
|
||||
staff.datatables_ticket_list,
|
||||
@ -140,7 +146,8 @@ urlpatterns += [
|
||||
name="success_iframe",
|
||||
),
|
||||
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 += [
|
||||
@ -177,7 +184,8 @@ if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT:
|
||||
router = DefaultRouter()
|
||||
router.register(r"tickets", TicketViewSet, basename="ticket")
|
||||
router.register(r"followups", FollowUpViewSet, basename="followups")
|
||||
router.register(r"followups-attachments", FollowUpAttachmentViewSet, basename="followupattachments")
|
||||
router.register(r"followups-attachments",
|
||||
FollowUpAttachmentViewSet, basename="followupattachments")
|
||||
router.register(r"users", CreateUserView, basename="user")
|
||||
urlpatterns += [re_path(r"^api/", include(router.urls))]
|
||||
|
||||
@ -211,7 +219,8 @@ urlpatterns += [
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
urlpatterns += [
|
||||
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"),
|
||||
re_path(
|
||||
r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$",
|
||||
@ -229,7 +238,8 @@ urlpatterns += [
|
||||
path(
|
||||
"system_settings/",
|
||||
login_required(
|
||||
DirectTemplateView.as_view(template_name="helpdesk/system_settings.html")
|
||||
DirectTemplateView.as_view(
|
||||
template_name="helpdesk/system_settings.html")
|
||||
),
|
||||
name="system_settings",
|
||||
),
|
||||
|
@ -11,6 +11,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
KBItem
|
||||
)
|
||||
|
||||
|
||||
def huser_from_request(req):
|
||||
return HelpdeskUser(req.user)
|
||||
|
||||
@ -33,7 +34,8 @@ class HelpdeskUser:
|
||||
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
|
||||
and not user.is_superuser
|
||||
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
|
||||
return all_queues.filter(pk__in=id_list)
|
||||
else:
|
||||
|
@ -4,8 +4,11 @@
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
#TODO: can we use the builtin Django validator instead?
|
||||
# see: https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator
|
||||
# TODO: can we use the builtin Django validator instead?
|
||||
# see:
|
||||
# https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator
|
||||
|
||||
|
||||
def validate_file_extension(value):
|
||||
import os
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -19,9 +22,12 @@ def validate_file_extension(value):
|
||||
if hasattr(settings, 'VALID_EXTENSIONS'):
|
||||
valid_extensions = settings.VALID_EXTENSIONS
|
||||
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:
|
||||
# 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() == '.'):
|
||||
raise ValidationError('Unsupported file extension: %s.' % ext.lower())
|
||||
raise ValidationError(
|
||||
'Unsupported file extension: %s.' % ext.lower())
|
||||
|
@ -6,15 +6,18 @@ class AbstractCreateTicketMixin():
|
||||
initial_data = {}
|
||||
request = self.request
|
||||
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:
|
||||
pass
|
||||
u = request.user
|
||||
if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email:
|
||||
initial_data['submitter_email'] = u.email
|
||||
|
||||
query_param_fields = ['submitter_email', 'title', 'body', 'queue', 'kbitem']
|
||||
custom_fields = ["custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]
|
||||
query_param_fields = ['submitter_email',
|
||||
'title', 'body', 'queue', 'kbitem']
|
||||
custom_fields = [
|
||||
"custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]
|
||||
query_param_fields += custom_fields
|
||||
for qpf in query_param_fields:
|
||||
initial_data[qpf] = request.GET.get(qpf, initial_data.get(qpf, ""))
|
||||
@ -29,7 +32,8 @@ class AbstractCreateTicketMixin():
|
||||
)
|
||||
if kbitem:
|
||||
try:
|
||||
kwargs['kbcategory'] = KBItem.objects.get(pk=int(kbitem)).category
|
||||
kwargs['kbcategory'] = KBItem.objects.get(
|
||||
pk=int(kbitem)).category
|
||||
except (ValueError, KBItem.DoesNotExist):
|
||||
pass
|
||||
return kwargs
|
||||
|
@ -123,7 +123,8 @@ class RecentFollowUps(Feed):
|
||||
description_template = 'helpdesk/rss/recent_activity_description.html'
|
||||
|
||||
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')
|
||||
|
||||
def items(self):
|
||||
|
@ -45,7 +45,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
|
||||
def get_form_class(self):
|
||||
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_form_class = getattr(the_module, the_form_class)
|
||||
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",
|
||||
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'):
|
||||
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
|
||||
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):
|
||||
kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
if '_hide_fields_' in self.request.GET:
|
||||
kwargs['hidden_fields'] = self.request.GET.get('_hide_fields_', '').split(',')
|
||||
kwargs['readonly_fields'] = self.request.GET.get('_readonly_fields_', '').split(',')
|
||||
kwargs['hidden_fields'] = self.request.GET.get(
|
||||
'_hide_fields_', '').split(',')
|
||||
kwargs['readonly_fields'] = self.request.GET.get(
|
||||
'_readonly_fields_', '').split(',')
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -107,7 +111,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
# This submission is spam. Let's not save it.
|
||||
return render(request, template_name='helpdesk/public_spam.html')
|
||||
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:
|
||||
return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
@ -146,7 +151,8 @@ class CreateTicketView(BaseCreateTicketView):
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
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'
|
||||
return form
|
||||
|
||||
@ -156,7 +162,8 @@ class Homepage(CreateTicketView):
|
||||
|
||||
def get_context_data(self, **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
|
||||
|
||||
|
||||
@ -170,7 +177,8 @@ def search_for_ticket(request, error_message=None):
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
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
|
||||
@ -188,9 +196,11 @@ def view_ticket(request):
|
||||
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
|
||||
try:
|
||||
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:
|
||||
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):
|
||||
return search_for_ticket(request, _('Invalid ticket ID or e-mail address. Please try again.'))
|
||||
|
||||
|
@ -110,7 +110,8 @@ def dashboard(request):
|
||||
# page vars for the three ticket tables
|
||||
user_tickets_page = request.GET.get(_('ut_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)
|
||||
active_tickets = Ticket.objects.select_related('queue').exclude(
|
||||
@ -335,7 +336,8 @@ def view_ticket(request, ticket_id):
|
||||
return update_ticket(request, ticket_id)
|
||||
|
||||
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 = \
|
||||
return_ticketccstring_and_show_subscribe(request.user, ticket)
|
||||
if show_subscribe:
|
||||
@ -361,9 +363,11 @@ def view_ticket(request, ticket_id):
|
||||
return update_ticket(request, ticket_id)
|
||||
|
||||
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:
|
||||
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()
|
||||
queue_choices = _get_queue_choices(queues)
|
||||
@ -378,7 +382,8 @@ def view_ticket(request, ticket_id):
|
||||
if submitter_userprofile is not None:
|
||||
content_type = ContentType.objects.get_for_model(submitter_userprofile)
|
||||
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}
|
||||
)
|
||||
else:
|
||||
@ -439,7 +444,8 @@ def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, ca
|
||||
|
||||
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
|
||||
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_day = int(request.POST.get('due_date_day', 0))
|
||||
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)
|
||||
else:
|
||||
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):
|
||||
due_date = ticket.due_date
|
||||
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:
|
||||
due_date = ticket.due_date
|
||||
else:
|
||||
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([
|
||||
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
|
||||
# broken into two stages to prevent changes from first replace being themselves
|
||||
# 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(
|
||||
'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%'
|
||||
).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:
|
||||
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:
|
||||
template_staff = 'assigned_owner'
|
||||
@ -741,7 +752,8 @@ def update_ticket(request, ticket_id, public=False):
|
||||
|
||||
# auto subscribe user if enabled
|
||||
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:
|
||||
subscribe_staff_member_to_ticket(ticket, request.user)
|
||||
|
||||
@ -779,9 +791,11 @@ def mass_update(request):
|
||||
user = request.user
|
||||
action = 'assign'
|
||||
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(
|
||||
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)
|
||||
@ -871,7 +885,8 @@ def mass_update(request):
|
||||
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 = (
|
||||
('created', _('Created date')),
|
||||
('due_date', _('Due on')),
|
||||
@ -914,7 +929,8 @@ def merge_tickets(request):
|
||||
# Prepare the value for each custom fields of this ticket
|
||||
for custom_field in custom_fields:
|
||||
try:
|
||||
value = ticket.ticketcustomfieldvalue_set.get(field=custom_field).value
|
||||
value = ticket.ticketcustomfieldvalue_set.get(
|
||||
field=custom_field).value
|
||||
except (TicketCustomFieldValue.DoesNotExist, ValueError):
|
||||
value = default
|
||||
ticket.values[custom_field.name] = {
|
||||
@ -925,11 +941,13 @@ def merge_tickets(request):
|
||||
if request.method == 'POST':
|
||||
# Find which ticket has been chosen to be the main one
|
||||
try:
|
||||
chosen_ticket = tickets.get(id=request.POST.get('chosen_ticket'))
|
||||
chosen_ticket = tickets.get(
|
||||
id=request.POST.get('chosen_ticket'))
|
||||
except Ticket.DoesNotExist:
|
||||
ticket_select_form.add_error(
|
||||
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:
|
||||
# Save ticket fields values
|
||||
@ -945,7 +963,8 @@ def merge_tickets(request):
|
||||
if attribute.startswith('get_') and attribute.endswith('_display'):
|
||||
# Keep only the FIELD part
|
||||
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)
|
||||
setattr(chosen_ticket, attribute, value)
|
||||
# Save custom fields values
|
||||
@ -953,17 +972,21 @@ def merge_tickets(request):
|
||||
id_for_custom_field = request.POST.get(custom_field.name)
|
||||
if id_for_custom_field != chosen_ticket.id:
|
||||
try:
|
||||
selected_ticket = tickets.get(id=id_for_custom_field)
|
||||
selected_ticket = tickets.get(
|
||||
id=id_for_custom_field)
|
||||
except (Ticket.DoesNotExist, ValueError):
|
||||
continue
|
||||
|
||||
# Check if the value for this ticket custom field exists
|
||||
# Check if the value for this ticket custom field
|
||||
# exists
|
||||
try:
|
||||
value = selected_ticket.ticketcustomfieldvalue_set.get(field=custom_field).value
|
||||
value = selected_ticket.ticketcustomfieldvalue_set.get(
|
||||
field=custom_field).value
|
||||
except TicketCustomFieldValue.DoesNotExist:
|
||||
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(
|
||||
field=custom_field,
|
||||
defaults={'value': value}
|
||||
@ -981,31 +1004,39 @@ def merge_tickets(request):
|
||||
ticket.status = Ticket.DUPLICATE_STATUS
|
||||
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)
|
||||
if ticket.submitter_email:
|
||||
send_templated_mail(
|
||||
template_name='merged',
|
||||
context=context,
|
||||
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,
|
||||
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=chosen_ticket,
|
||||
# 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
|
||||
chosen_ticket.add_email_to_ticketcc_if_not_in(email=ticket.submitter_email)
|
||||
# Add submitter_email, assigned_to email and ticketcc to
|
||||
# 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:
|
||||
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():
|
||||
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 render(request, 'helpdesk/ticket_merge.html', {
|
||||
@ -1134,7 +1165,8 @@ def ticket_list(request):
|
||||
|
||||
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 = ''
|
||||
if query_params['search_string'] and settings.DATABASES['default']['ENGINE'].endswith('sqlite'):
|
||||
@ -1150,7 +1182,8 @@ def ticket_list(request):
|
||||
kbitem = []
|
||||
|
||||
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()
|
||||
|
||||
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):
|
||||
try:
|
||||
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):
|
||||
raise QueryLoadError()
|
||||
@ -1253,7 +1287,8 @@ class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixi
|
||||
return kwargs
|
||||
|
||||
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)
|
||||
|
||||
def get_success_url(self):
|
||||
@ -1580,7 +1615,8 @@ def save_query(request):
|
||||
if not title or not query_encoded:
|
||||
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()
|
||||
|
||||
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')
|
||||
email = form.cleaned_data.get('email')
|
||||
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():
|
||||
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:
|
||||
ticketcc = form.save(commit=False)
|
||||
ticketcc.ticket = ticket
|
||||
@ -1739,7 +1777,8 @@ ticket_dependency_add = staff_member_required(ticket_dependency_add)
|
||||
|
||||
@helpdesk_staff_member_required
|
||||
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':
|
||||
dependency.delete()
|
||||
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)
|
||||
|
||||
# >= 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)
|
||||
|
||||
# >= 60
|
||||
@ -1822,7 +1862,8 @@ def calc_basic_ticket_stats(Tickets):
|
||||
average_nbr_days_until_ticket_closed = \
|
||||
calc_average_nbr_days_until_ticket_resolved(all_closed_tickets)
|
||||
# all closed tickets that were opened in the last 60 days.
|
||||
all_closed_last_60_days = all_closed_tickets.filter(created__gte=date_60_str)
|
||||
all_closed_last_60_days = all_closed_tickets.filter(
|
||||
created__gte=date_60_str)
|
||||
average_nbr_days_until_ticket_closed_last_60_days = \
|
||||
calc_average_nbr_days_until_ticket_resolved(all_closed_last_60_days)
|
||||
|
||||
|
14
quicktest.py
14
quicktest.py
@ -35,8 +35,8 @@ class QuickDjangoTest(object):
|
||||
'django.contrib.sites',
|
||||
'django.contrib.staticfiles',
|
||||
'bootstrap4form',
|
||||
## The following commented apps are optional,
|
||||
## related to teams functionalities
|
||||
# The following commented apps are optional,
|
||||
# related to teams functionalities
|
||||
#'account',
|
||||
#'pinax.invitations',
|
||||
#'pinax.teams',
|
||||
@ -102,11 +102,11 @@ class QuickDjangoTest(object):
|
||||
TEMPLATES=self.TEMPLATES,
|
||||
SITE_ID=1,
|
||||
SECRET_KEY='wowdonotusethisfakesecuritykeyyouneedarealsecure1',
|
||||
## The following settings disable teams
|
||||
HELPDESK_TEAMS_MODEL = 'auth.User',
|
||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [],
|
||||
HELPDESK_KBITEM_TEAM_GETTER = lambda _: None,
|
||||
## test the API
|
||||
# The following settings disable teams
|
||||
HELPDESK_TEAMS_MODEL='auth.User',
|
||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES=[],
|
||||
HELPDESK_KBITEM_TEAM_GETTER=lambda _: None,
|
||||
# test the API
|
||||
HELPDESK_ACTIVATE_API_ENDPOINT=True
|
||||
)
|
||||
|
||||
|
11
setup.py
11
setup.py
@ -24,6 +24,8 @@ standard_exclude_directories = (
|
||||
# Note: you may want to copy this into your setup.py file verbatim, as
|
||||
# you can't import this from another package, when you don't know if
|
||||
# that package is installed yet.
|
||||
|
||||
|
||||
def find_package_data(
|
||||
where=".",
|
||||
package="",
|
||||
@ -72,7 +74,8 @@ def find_package_data(
|
||||
bad_name = True
|
||||
if show_ignored:
|
||||
print(
|
||||
"Directory %s ignored by pattern %s" % (fn, pattern),
|
||||
"Directory %s ignored by pattern %s" % (
|
||||
fn, pattern),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
@ -86,7 +89,8 @@ def find_package_data(
|
||||
new_package = package + "." + name
|
||||
stack.append((fn, "", new_package, False))
|
||||
else:
|
||||
stack.append((fn, prefix + name + "/", package, only_in_packages))
|
||||
stack.append((fn, prefix + name + "/",
|
||||
package, only_in_packages))
|
||||
elif package or not only_in_packages:
|
||||
# is a file
|
||||
bad_name = False
|
||||
@ -95,7 +99,8 @@ def find_package_data(
|
||||
bad_name = True
|
||||
if show_ignored:
|
||||
print(
|
||||
"File %s ignored by pattern %s" % (fn, pattern),
|
||||
"File %s ignored by pattern %s" % (
|
||||
fn, pattern),
|
||||
file=sys.stderr,
|
||||
)
|
||||
break
|
||||
|
Loading…
Reference in New Issue
Block a user