Merge branch 'stable' of git@github.com:martin-marty/django-helpdesk.git

into HEAD
This commit is contained in:
Martin Whitehouse 2022-07-20 15:53:36 +02:00
commit 981cebfd84
51 changed files with 1275 additions and 712 deletions

View File

@ -6,7 +6,7 @@ django-helpdesk - A Django powered ticket tracker for small businesses.
.. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk
Copyright 2009-2021 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
Copyright 2009-2022 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
See LICENSE for details.
django-helpdesk was formerly known as Jutda Helpdesk, named after the
@ -52,7 +52,7 @@ Installation
`django-helpdesk` requires:
* Python 3.8+
* Django 3.2 LTS
* Django 3.2 LTS highly recommended (early adopters may test Django 4)
You can quickly install the latest stable version of `django-helpdesk`
app via `pip`::

View File

@ -17,18 +17,24 @@ pool:
vmImage: ubuntu-latest
strategy:
matrix:
Python38Django22:
PYTHON_VERSION: '3.8'
DJANGO_VERSION: '22'
Python39Django22:
PYTHON_VERSION: '3.9'
DJANGO_VERSION: '22'
Python38Django32:
PYTHON_VERSION: '3.8'
DJANGO_VERSION: '32'
Python39Django32:
PYTHON_VERSION: '3.9'
DJANGO_VERSION: '32'
Python310Django32:
PYTHON_VERSION: '3.10'
DJANGO_VERSION: '32'
Python38Django4:
PYTHON_VERSION: '3.8'
DJANGO_VERSION: '4'
Python39Django4:
PYTHON_VERSION: '3.9'
DJANGO_VERSION: '4'
Python310Django4:
PYTHON_VERSION: '3.10'
DJANGO_VERSION: '4'
maxParallel: 10
steps:
@ -56,6 +62,7 @@ steps:
- script: |
python -m pip install --upgrade pip setuptools wheel
pip install -c constraints-Django$(DJANGO_VERSION).txt -r requirements.txt
pip install -c constraints-Django$(DJANGO_VERSION).txt -r requirements-testing.txt
pip install unittest-xml-reporting
displayName: 'Install prerequisites'

View File

@ -1,2 +0,0 @@
Django >=2.2,<3

1
constraints-Django4.txt Normal file
View File

@ -0,0 +1 @@
Django >=4,<5

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -13,7 +13,7 @@ project_root = os.path.dirname(here)
NAME = 'django-helpdesk-demodesk'
DESCRIPTION = 'A demo Django project using django-helpdesk'
README = open(os.path.join(here, 'README.rst')).read()
VERSION = '0.5.0a1'
VERSION = '0.4.1'
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
AUTHOR = 'django-helpdesk team'
URL = 'https://github.com/django-helpdesk/django-helpdesk'
@ -28,7 +28,7 @@ KEYWORDS = []
PACKAGES = ['demodesk']
REQUIREMENTS = [
'django-helpdesk'
]
]
ENTRY_POINTS = {
'console_scripts': ['demodesk = demodesk.manage:main']
}

View File

@ -1,20 +1,25 @@
API
===
A REST API (built with ``djangorestframework``) is available in order to list, create, update and delete tickets from other tools thanks to HTTP requests.
A REST API (built with ``djangorestframework``) is available in order to list, create, update and delete tickets from
other tools thanks to HTTP requests.
If you wish to use it, you have to add this line in your settings::
HELPDESK_ACTIVATE_API_ENDPOINT = True
You must be authenticated to access the API, the URL endpoint is ``/api/tickets/``. You can configure how you wish to authenticate to the API by customizing the ``DEFAULT_AUTHENTICATION_CLASSES`` key in the ``REST_FRAMEWORK`` setting (more information on this page : https://www.django-rest-framework.org/api-guide/authentication/)
You must be authenticated to access the API, the URL endpoint is ``/api/tickets/``.
You can configure how you wish to authenticate to the API by customizing the ``DEFAULT_AUTHENTICATION_CLASSES`` key
in the ``REST_FRAMEWORK`` setting (more information on this page : https://www.django-rest-framework.org/api-guide/authentication/)
GET
---
Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the complete list of tickets.
Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the complete list of tickets with their
followups and their attachment files.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **GET** request will return you the data of the ticket you provided the ID.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **GET** request will return you the data of the ticket you
provided the ID.
POST
----
@ -35,7 +40,8 @@ You need to provide a JSON body with the following data :
- **due_date**: date representation for when the ticket is due
- **merged_to**: ID of the ticket to which it is merged
Note that ``status`` will automatically be set to OPEN. Also, some fields are not configurable during creation: ``resolution``, ``on_hold`` and ``merged_to``.
Note that ``status`` will automatically be set to OPEN. Also, some fields are not configurable during creation:
``resolution``, ``on_hold`` and ``merged_to``.
Moreover, if you created custom fields, you can add them into the body with the key ``custom_<custom-field-slug>``.
@ -46,6 +52,34 @@ Here is an example of a cURL request to create a ticket (using Basic authenticat
--header 'Content-Type: application/json' \
--data-raw '{"queue": 1, "title": "Test Ticket API", "description": "Test create ticket from API", "submitter_email": "test@mail.com", "priority": 4}'
Note that you can attach one file as attachment but in this case, you cannot use JSON for the request content type.
Here is an example with form-data (curl default) ::
curl --location --request POST 'http://127.0.0.1:8000/api/tickets/' \
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
--form 'queue="1"' \
--form 'title="Test Ticket API with attachment"' \
--form 'description="Test create ticket from API avec attachment"' \
--form 'submitter_email="test@mail.com"' \
--form 'priority="2"' \
--form 'attachment=@"/C:/Users/benbb96/Documents/file.txt"'
----
Accessing the endpoint ``/api/followups/`` with a **POST** request will let you create a new followup on a ticket.
This time, you can attach multiple files thanks to the `attachments` field. Here is an example ::
curl --location --request POST 'http://127.0.0.1:8000/api/followups/' \
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
--form 'ticket="44"' \
--form 'title="Test ticket answer"' \
--form 'comment="This answer contains multiple files as attachment."' \
--form 'attachments=@"/C:/Users/benbb96/Documents/doc.pdf"' \
--form 'attachments=@"/C:/Users/benbb96/Documents/image.png"'
----
Accessing the endpoint ``/api/users/`` with a **POST** request will let you create a new user.
You need to provide a JSON body with the following data :
@ -59,18 +93,21 @@ You need to provide a JSON body with the following data :
PUT
---
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PUT** request will let you update the data of the ticket you provided the ID.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PUT** request will let you update the data of the ticket
you provided the ID.
You must include all fields in the JSON body.
PATCH
-----
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PATCH** request will let you do a partial update of the data of the ticket you provided the ID.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PATCH** request will let you do a partial update of the
data of the ticket you provided the ID.
You can include only the fields you need to update in the JSON body.
DELETE
------
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **DELETE** request will let you delete the ticket you provided the ID.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **DELETE** request will let you delete the ticket you
provided the ID.

View File

@ -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).

View File

@ -74,11 +74,12 @@ errors with trying to create User settings.
SITE_ID = 1
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following line to ``urls.py``::
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following lines to ``urls.py``::
from django.conf.urls import include
path('helpdesk/', include('helpdesk.urls')),
Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the line will be as follows::
Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the path line will be as follows::
path('', include('helpdesk.urls', namespace='helpdesk')),

View File

@ -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')

View File

@ -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'

View File

@ -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,

View File

@ -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

View File

@ -129,12 +129,15 @@ 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:
if attached.size:
from helpdesk.models import FollowUpAttachment
filename = smart_str(attached.name)
att = FollowUpAttachment(
followup=followup,
@ -150,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

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -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':

View File

@ -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'),

View File

@ -4,8 +4,8 @@ from django.contrib.humanize.templatetags import humanize
from rest_framework.exceptions import ValidationError
from .forms import TicketForm
from .models import Ticket, CustomField
from .lib import format_time_spent
from .models import Ticket, CustomField, FollowUp, FollowUpAttachment
from .lib import format_time_spent, process_attachments
from .user import HelpdeskUser
@ -71,12 +71,45 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
return obj.kbitem.title if obj.kbitem else ""
class FollowUpAttachmentSerializer(serializers.ModelSerializer):
class Meta:
model = FollowUpAttachment
fields = ('id', 'followup', 'file', 'filename', 'mime_type', 'size')
class FollowUpSerializer(serializers.ModelSerializer):
followupattachment_set = FollowUpAttachmentSerializer(
many=True, read_only=True)
attachments = serializers.ListField(
child=serializers.FileField(),
write_only=True,
required=False
)
class Meta:
model = FollowUp
fields = (
'id', 'ticket', 'date', 'title', 'comment', 'public', 'user', 'new_status', 'message_id',
'time_spent', 'followupattachment_set', 'attachments'
)
def create(self, validated_data):
attachments = validated_data.pop('attachments', None)
followup = super().create(validated_data)
if attachments:
process_attachments(followup, attachments)
return followup
class TicketSerializer(serializers.ModelSerializer):
followup_set = FollowUpSerializer(many=True, read_only=True)
attachment = serializers.FileField(write_only=True, required=False)
class Meta:
model = Ticket
fields = (
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold',
'priority', 'due_date', 'merged_to'
'priority', 'due_date', 'merged_to', 'attachment', 'followup_set'
)
def __init__(self, *args, **kwargs):
@ -99,7 +132,10 @@ class TicketSerializer(serializers.ModelSerializer):
if data.get('merged_to'):
data['merged_to'] = data['merged_to'].id
ticket_form = TicketForm(data=data, queue_choices=queue_choices)
files = {'attachment': data.pop('attachment', None)}
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()

View File

@ -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)

View File

@ -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

View File

@ -29,7 +29,7 @@
<label for='saved_query'>{% trans "Select Query:" %}</label>
<select name='saved_query'>
<option value="">--------</option>{% for q in user_saved_queries_ %}
<option value="{{ q.id }}"{% if saved_query==q %} selected{% endif %}>{{ q.title }}</option>{% endfor %}
<option value="{{ q.id }}"{% if saved_query == q %} selected{% endif %}>{{ q.title }}</option>{% endfor %}
</select>
<input class="btn btn-primary" type='submit' value='{% trans "Filter Report" %}'>
</form>

View File

@ -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")

View File

@ -22,13 +22,16 @@ def datetime_string_format(value):
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
"""
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

View File

@ -1,8 +1,11 @@
import base64
from collections import OrderedDict
from datetime import datetime
from django.core.files.uploadedfile import SimpleUploadedFile
from freezegun import freeze_time
from django.contrib.auth.models import User
from pytz import UTC
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework.exceptions import ErrorDetail
from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN
@ -12,6 +15,8 @@ from helpdesk.models import Queue, Ticket, CustomField
class TicketTest(APITestCase):
due_date = datetime(2022, 4, 10, 15, 6)
@classmethod
def setUpTestData(cls):
cls.queue = Queue.objects.create(
@ -67,23 +72,29 @@ 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)
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/',
{
@ -96,7 +107,7 @@ class TicketTest(APITestCase):
'status': Ticket.RESOLVED_STATUS,
'priority': 1,
'on_hold': True,
'due_date': datetime(2022, 4, 10, 15, 6),
'due_date': self.due_date,
'merged_to': merge_ticket.id
}
)
@ -105,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
self.assertEqual(created_ticket.due_date, datetime(2022, 4, 10, 15, 6, tzinfo=UTC))
self.assertIsNone(created_ticket.merged_to) # merged_to can not be set 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)
# 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(
@ -134,7 +151,7 @@ class TicketTest(APITestCase):
'status': Ticket.RESOLVED_STATUS,
'priority': 1,
'on_hold': True,
'due_date': datetime(2022, 4, 10, 15, 6),
'due_date': self.due_date,
'merged_to': merge_ticket.id
}
)
@ -149,12 +166,13 @@ class TicketTest(APITestCase):
self.assertEqual(test_ticket.priority, 1)
self.assertTrue(test_ticket.on_hold)
self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS)
self.assertEqual(test_ticket.due_date, datetime(2022, 4, 10, 15, 6, tzinfo=UTC))
self.assertEqual(test_ticket.due_date, self.due_date)
self.assertEqual(test_ticket.merged_to, merge_ticket)
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(
@ -170,12 +188,14 @@ 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)
self.assertFalse(Ticket.objects.exists())
@freeze_time('2022-06-30 23:09:44')
def test_create_api_ticket_with_custom_fields(self):
# Create custom fields
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
@ -193,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)
@ -207,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/', {
@ -245,6 +267,19 @@ class TicketTest(APITestCase):
'priority': 4,
'due_date': None,
'merged_to': None,
'followup_set': [OrderedDict([
('id', 1),
('ticket', 1),
('date', '2022-06-30T23:09:44'),
('title', 'Ticket Opened'),
('comment', 'Test description\nMulti lines'),
('public', True),
('user', 1),
('new_status', None),
('message_id', None),
('time_spent', None),
('followupattachment_set', [])
])],
'custom_varchar': 'test',
'custom_text': 'multi\nline',
'custom_integer': 1,
@ -260,3 +295,63 @@ class TicketTest(APITestCase):
'custom_slug': 'test-slug'
})
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')
response = self.client.post('/api/tickets/', {
'queue': self.queue.id,
'title': 'Test title',
'description': 'Test description\nMulti lines',
'submitter_email': 'test@mail.com',
'priority': 4,
'attachment': test_file
})
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.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)
attachment = created_ticket.followup_set.get().followupattachment_set.get()
self.assertEqual(
attachment.file.name,
f'helpdesk/attachments/test-queue-1-{created_ticket.secret_key}/1/file.jpg'
)
def test_create_follow_up_with_attachments(self):
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')
response = self.client.post('/api/followups/', {
'ticket': ticket.id,
'title': 'Test',
'comment': 'Test answer\nMulti lines',
'attachments': [
test_file_1,
test_file_2
]
})
self.assertEqual(response.status_code, HTTP_201_CREATED)
created_followup = ticket.followup_set.last()
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')

View File

@ -11,6 +11,7 @@ import shutil
from tempfile import gettempdir
from unittest import mock
from unittest.case import skip
MEDIA_DIR = os.path.join(gettempdir(), 'helpdesk_test_media')
@ -46,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,
@ -54,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,
@ -72,17 +77,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 = smart_str(file_on_disk.read(), 'utf-8')
self.assertEqual(disk_content, 'โจ')
@mock.patch.object(models.FollowUp, 'save', autospec=True)
@mock.patch.object(models.FollowUpAttachment, 'save', autospec=True)
@mock.patch.object(models.Ticket, 'save', autospec=True)
@mock.patch.object(models.Queue, 'save', autospec=True)
class AttachmentUnitTests(TestCase):
@ -100,10 +108,11 @@ class AttachmentUnitTests(TestCase):
)
)
@mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
@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'],
@ -113,18 +122,18 @@ class AttachmentUnitTests(TestCase):
)
self.assertEqual(filename, self.file_attrs['filename'])
@mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" check utf-8 data is parsed correctly """
obj = models.FollowUpAttachment.objects.create(
followup=self.follow_up,
file=self.test_file
)
self.assertEqual(obj.filename, self.file_attrs['filename'])
self.assertEqual(obj.size, len(self.file_attrs['content']))
self.assertEqual(obj.mime_type, "text/plain")
obj.save()
self.assertEqual(obj.file.name, self.file_attrs['filename'])
self.assertEqual(obj.file.size, len(self.file_attrs['content']))
self.assertEqual(obj.file.file.content_type, "text/utf8")
def test_kbi_attachment(self, mock_att_save, mock_queue_save, mock_ticket_save):
def test_kbi_attachment(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" check utf-8 data is parsed correctly """
kbcategory = models.KBCategory.objects.create(
@ -143,17 +152,20 @@ class AttachmentUnitTests(TestCase):
kbitem=kbitem,
file=self.test_file
)
obj.save()
self.assertEqual(obj.filename, self.file_attrs['filename'])
self.assertEqual(obj.size, len(self.file_attrs['content']))
self.assertEqual(obj.file.size, len(self.file_attrs['content']))
self.assertEqual(obj.mime_type, "text/plain")
@mock.patch.object('helpdesk.lib.FollowUpAttachment', 'save', autospec=True)
@skip("model in lib not patched")
@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
attachment_obj = mock_att_save.call_args[0][0]
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)
self.assertIsInstance(attachment_obj, models.FollowUpAttachment)

View File

@ -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)

View File

@ -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, "'/helpdesk/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&amp;title=lol")
self.assertContains(
response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&amp;title=lol")

View File

@ -6,7 +6,9 @@ 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
class KBDisabledTestCase(TestCase):
@ -25,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:
@ -74,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))
@ -89,7 +94,9 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
def setUp(self):
super().setUp()
self.non_staff_user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
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')
def test_staff_user_detection(self):
"""Staff and non-staff users are correctly identified"""
@ -116,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=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')
@ -125,30 +133,35 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
staff users should be able to access rss feeds.
"""
user = get_staff_user()
self.client.login(username=user.username, password='password')
response = self.client.get(reverse('helpdesk:rss_unassigned'), follow=True)
self.client.login(username=user.username, password="password")
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)
def test_non_staff_cannot_rss(self):
"""If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
non-staff users should not be able to access rss feeds.
"""
user = self.non_staff_user
self.client.login(username=user.username, password='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):
@ -165,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))
@ -176,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))
@ -261,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)

View File

@ -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'
)

View File

@ -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

View File

@ -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,

View File

@ -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='/helpdesk/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='/helpdesk/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])

View File

@ -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)

View File

@ -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:
@ -649,17 +652,18 @@ class EmailInteractionsTestCase(TestCase):
# the new and update queues (+2)
# Ensure that the submitter is notified
self.assertIn(submitter_email, mail.outbox[expected_email_count - 1].to)
# Ensure that contacts on cc_list will be notified on the same email (index 0)
for cc_email in cc_list:
self.assertIn(cc_email, mail.outbox[expected_email_count - 1].to)
# Even after 2 messages with the same cc_list,
# <get> MUST return only one object
ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
self.assertTrue(ticket_cc.ticket, ticket)
self.assertTrue(ticket_cc.email, cc_email)
# DISABLED, iterating a cc_list against a mailbox list can not work
# self.assertIn(submitter_email, mail.outbox[expected_email_count - 1].to)
#
# # Ensure that contacts on cc_list will be notified on the same email (index 0)
# for cc_email in cc_list:
# self.assertIn(cc_email, mail.outbox[expected_email_count - 1].to)
#
# # Even after 2 messages with the same cc_list,
# # <get> MUST return only one object
# ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
# self.assertTrue(ticket_cc.ticket, ticket)
# self.assertTrue(ticket_cc.email, cc_email)
def test_create_followup_from_email_with_invalid_message_id(self):
"""
@ -824,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)
@ -953,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)
@ -993,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)
@ -1092,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">')

View File

@ -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")

View File

@ -2,6 +2,6 @@ from django.urls import include, path
from django.contrib import admin
urlpatterns = [
path('helpdesk/', include('helpdesk.urls', namespace='helpdesk')),
path('', include('helpdesk.urls', namespace='helpdesk')),
path('admin/', admin.site.urls),
]

View File

@ -17,7 +17,7 @@ from rest_framework.routers import DefaultRouter
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
from helpdesk.views import feeds, staff, public, login
from helpdesk import settings as helpdesk_settings
from helpdesk.views.api import TicketViewSet, CreateUserView
from helpdesk.views.api import TicketViewSet, CreateUserView, FollowUpViewSet, FollowUpAttachmentViewSet
if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.views import kb
@ -43,251 +43,204 @@ class DirectTemplateView(TemplateView):
return context
app_name = 'helpdesk'
app_name = "helpdesk"
base64_pattern = r'(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$'
base64_pattern = r"(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
urlpatterns = [
path('dashboard/',
staff.dashboard,
name='dashboard'),
path('tickets/',
staff.ticket_list,
name='list'),
path('tickets/update/',
staff.mass_update,
name='mass_update'),
path('tickets/merge',
staff.merge_tickets,
name='merge_tickets'),
path('tickets/<int:ticket_id>/',
staff.view_ticket,
name='view'),
path('tickets/<int:ticket_id>/followup_edit/<int:followup_id>/',
path("dashboard/", staff.dashboard, name="dashboard"),
path("tickets/", staff.ticket_list, name="list"),
path("tickets/update/", staff.mass_update, name="mass_update"),
path("tickets/merge", staff.merge_tickets, name="merge_tickets"),
path("tickets/<int:ticket_id>/", staff.view_ticket, name="view"),
path(
"tickets/<int:ticket_id>/followup_edit/<int:followup_id>/",
staff.followup_edit,
name='followup_edit'),
path('tickets/<int:ticket_id>/followup_delete/<int:followup_id>/',
name="followup_edit",
),
path(
"tickets/<int:ticket_id>/followup_delete/<int:followup_id>/",
staff.followup_delete,
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>/hold/',
staff.hold_ticket,
name='hold'),
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/delete/<int:cc_id>/',
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>/hold/", staff.hold_ticket, name="hold"),
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/delete/<int:cc_id>/",
staff.ticket_cc_del,
name='ticket_cc_del'),
path('tickets/<int:ticket_id>/dependency/add/',
name="ticket_cc_del",
),
path(
"tickets/<int:ticket_id>/dependency/add/",
staff.ticket_dependency_add,
name='ticket_dependency_add'),
path('tickets/<int:ticket_id>/dependency/delete/<int:dependency_id>/',
name="ticket_dependency_add",
),
path(
"tickets/<int:ticket_id>/dependency/delete/<int:dependency_id>/",
staff.ticket_dependency_del,
name='ticket_dependency_del'),
path('tickets/<int:ticket_id>/attachment_delete/<int:attachment_id>/',
name="ticket_dependency_del",
),
path(
"tickets/<int:ticket_id>/attachment_delete/<int:attachment_id>/",
staff.attachment_del,
name='attachment_del'),
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'),
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'),
re_path(r'^datatables_ticket_list/(?P<query>{})$'.format(base64_pattern),
name="attachment_del",
),
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"),
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"),
re_path(
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
staff.datatables_ticket_list,
name="datatables_ticket_list"),
re_path(r'^timeline_ticket_list/(?P<query>{})$'.format(base64_pattern),
name="datatables_ticket_list",
),
re_path(
r"^timeline_ticket_list/(?P<query>{})$".format(base64_pattern),
staff.timeline_ticket_list,
name="timeline_ticket_list"),
name="timeline_ticket_list",
),
]
if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET:
urlpatterns += [
url(r'^tickets/(?P<ticket_id>[0-9]+)/dependency/add/$',
re_path(
r"^tickets/(?P<ticket_id>[0-9]+)/dependency/add/$",
staff.ticket_dependency_add,
name='ticket_dependency_add'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/dependency/delete/(?P<dependency_id>[0-9]+)/$',
name="ticket_dependency_add",
),
re_path(
r"^tickets/(?P<ticket_id>[0-9]+)/dependency/delete/(?P<dependency_id>[0-9]+)/$",
staff.ticket_dependency_del,
name='ticket_dependency_del'),
name="ticket_dependency_del",
),
]
urlpatterns += [
path('',
protect_view(public.Homepage.as_view()),
name='home'),
path('tickets/submit/',
public.create_ticket,
name='submit'),
path('tickets/submit_iframe/',
path("", protect_view(public.Homepage.as_view()), name="home"),
path("tickets/submit/", public.create_ticket, name="submit"),
path(
"tickets/submit_iframe/",
public.CreateTicketIframeView.as_view(),
name='submit_iframe'),
path('tickets/success_iframe/', # Ticket was submitted successfully
name="submit_iframe",
),
path(
"tickets/success_iframe/", # Ticket was submitted successfully
public.SuccessIframeView.as_view(),
name='success_iframe'),
path('view/',
public.view_ticket,
name='public_view'),
path('change_language/',
public.change_language,
name='public_change_language'),
name="success_iframe",
),
path("view/", public.view_ticket, name="public_view"),
path("change_language/", public.change_language,
name="public_change_language"),
]
urlpatterns += [
path('rss/user/<str:user_name>/',
re_path(
r"^rss/user/(?P<user_name>[a-zA-Z0-9\_\.]+)/",
helpdesk_staff_member_required(feeds.OpenTicketsByUser()),
name='rss_user'),
re_path(r'^rss/user/(?P<user_name>[^/]+)/(?P<queue_slug>[A-Za-z0-9_-]+)/$',
name="rss_user",
),
re_path(
r"^rss/user/(?P<user_name>[^/]+)/(?P<queue_slug>[A-Za-z0-9_-]+)/$",
helpdesk_staff_member_required(feeds.OpenTicketsByUser()),
name='rss_user_queue'),
re_path(r'^rss/queue/(?P<queue_slug>[A-Za-z0-9_-]+)/$',
name="rss_user_queue",
),
re_path(
r"^rss/queue/(?P<queue_slug>[A-Za-z0-9_-]+)/$",
helpdesk_staff_member_required(feeds.OpenTicketsByQueue()),
name='rss_queue'),
path('rss/unassigned/',
name="rss_queue",
),
path(
"rss/unassigned/",
helpdesk_staff_member_required(feeds.UnassignedTickets()),
name='rss_unassigned'),
path('rss/recent_activity/',
name="rss_unassigned",
),
path(
"rss/recent_activity/",
helpdesk_staff_member_required(feeds.RecentFollowUps()),
name='rss_activity'),
name="rss_activity",
),
]
# API is added to url conf based on the setting (False by default)
if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT:
router = DefaultRouter()
router.register(r'tickets', TicketViewSet, basename='ticket')
router.register(r'users', CreateUserView, basename='user')
urlpatterns += [
url(r'^api/', include(router.urls))
]
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"users", CreateUserView, basename="user")
urlpatterns += [re_path(r"^api/", include(router.urls))]
urlpatterns += [
path('login/',
login.login,
name='login'),
path('logout/',
path("login/", login.login, name="login"),
path(
"logout/",
auth_views.LogoutView.as_view(
template_name='helpdesk/registration/login.html',
next_page='../'),
name='logout'),
path('password_change/',
template_name="helpdesk/registration/login.html", next_page="../"
),
name="logout",
),
path(
"password_change/",
auth_views.PasswordChangeView.as_view(
template_name='helpdesk/registration/change_password.html',
success_url='./done'),
name='password_change'),
path('password_change/done',
template_name="helpdesk/registration/change_password.html",
success_url="./done",
),
name="password_change",
),
path(
"password_change/done",
auth_views.PasswordChangeDoneView.as_view(
template_name='helpdesk/registration/change_password_done.html',),
name='password_change_done'),
template_name="helpdesk/registration/change_password_done.html",
),
name="password_change_done",
),
]
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'),
path('kb/<int:item>/vote/',
kb.vote,
name='kb_vote'),
re_path(r'^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$',
path("kb/", kb.index, name="kb_index"),
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_-]+)/$",
kb.category_iframe,
name='kb_category_iframe'),
name="kb_category_iframe",
),
]
urlpatterns += [
path('help/context/',
TemplateView.as_view(template_name='helpdesk/help_context.html'),
name='help_context'),
path('system_settings/',
login_required(DirectTemplateView.as_view(template_name='helpdesk/system_settings.html')),
name='system_settings'),
path(
"help/context/",
TemplateView.as_view(template_name="helpdesk/help_context.html"),
name="help_context",
),
path(
"system_settings/",
login_required(
DirectTemplateView.as_view(
template_name="helpdesk/system_settings.html")
),
name="system_settings",
),
]

View File

@ -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:

View File

@ -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())

View File

@ -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

View File

@ -4,8 +4,8 @@ from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from django.contrib.auth import get_user_model
from helpdesk.models import Ticket
from helpdesk.serializers import TicketSerializer, UserSerializer
from helpdesk.models import Ticket, FollowUp, FollowUpAttachment
from helpdesk.serializers import TicketSerializer, UserSerializer, FollowUpSerializer, FollowUpAttachmentSerializer
class TicketViewSet(viewsets.ModelViewSet):
@ -28,6 +28,18 @@ class TicketViewSet(viewsets.ModelViewSet):
return ticket
class FollowUpViewSet(viewsets.ModelViewSet):
queryset = FollowUp.objects.all()
serializer_class = FollowUpSerializer
permission_classes = [IsAdminUser]
class FollowUpAttachmentViewSet(viewsets.ModelViewSet):
queryset = FollowUpAttachment.objects.all()
serializer_class = FollowUpAttachmentSerializer
permission_classes = [IsAdminUser]
class CreateUserView(CreateModelMixin, GenericViewSet):
queryset = get_user_model().objects.all()
serializer_class = UserSerializer

View File

@ -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):

View File

@ -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.'))

View File

@ -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)

View File

@ -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',
@ -98,15 +98,15 @@ class QuickDjangoTest(object):
MIDDLEWARE=self.MIDDLEWARE,
ROOT_URLCONF='helpdesk.tests.urls',
STATIC_URL='/static/',
LOGIN_URL='/helpdesk/login/',
LOGIN_URL='/login/',
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
)

View File

@ -5,3 +5,4 @@ coverage
argparse
pbr
mock
freezegun

View File

@ -4,7 +4,7 @@ from distutils.util import convert_path
from fnmatch import fnmatchcase
from setuptools import setup, find_packages
version = '0.5.0a1'
version = '0.4.1'
# Provided as an attribute, so you can append to these instead
# of replicating them:
@ -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