Merge branch 'unstable' into patch-1

This commit is contained in:
Pouria Mousavizadeh Tehrani 2022-08-03 18:05:13 +04:30 committed by GitHub
commit 318417f097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 2288 additions and 1444 deletions

View File

@ -2,3 +2,11 @@
max-line-length = 120 max-line-length = 120
exclude = .git,__pycache__,.tox,.eggs,*.egg,node_modules,.venv,migrations,docs,demo,tests,setup.py exclude = .git,__pycache__,.tox,.eggs,*.egg,node_modules,.venv,migrations,docs,demo,tests,setup.py
import-order-style = pep8 import-order-style = pep8
max-complexity = 20
[pycodestyle]
max-line-length = 120
exclude = "migrations"
in-place = true
recursive = true

42
.github/workflows/pythonpackage.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Python package
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django32.txt
- name: Format style check with 'autopep8'
run: |
pip install autopep8
autopep8 --exit-code --global-config .flake8 helpdesk
- name: Lint with flake8
run: |
pip install flake8
# stop the build if there are Python syntax errors or undefined names
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 helpdesk --count --show-source --statistics --exit-zero --max-complexity=20
- name: Sort style check with 'isort'
run: |
isort --line-length=120 --src helpdesk . --check
- name: Test with pytest
run: |
pip install pytest
cd ${GITHUB_WORKSPACE} && python quicktest.py
env:
DJANGO_SETTINGS_MODULE: helpdesk.settings

19
.isort.cfg Normal file
View File

@ -0,0 +1,19 @@
[settings]
src_paths=helpdesk
skip_glob=migrations/*
skip=migrations
multi_line_output=3
line_length=120
use_parentheses=true
sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
lines_after_imports=2
lines_before_imports=1
balanced_wrapping=true
lines_between_types=1
combine_as_imports=true
force_alphabetical_sort=true
skip_gitignore=true
force_sort_within_sections=true
group_by_package=false
from_first=true

View File

@ -1,13 +1,12 @@
django-helpdesk - A Django powered ticket tracker for small businesses. django-helpdesk - A Django powered ticket tracker for small businesses.
======================================================================= =======================================================================
.. image:: https://travis-ci.org/django-helpdesk/django-helpdesk.png?branch=develop [![Build Status](https://dev.azure.com/django-helpdesk/django-helpdesk/_apis/build/status/django-helpdesk.django-helpdesk?branchName=master)](https://dev.azure.com/django-helpdesk/django-helpdesk/_build/latest?definitionId=1&branchName=master)
:target: https://travis-ci.org/django-helpdesk/django-helpdesk
.. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg .. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk :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. See LICENSE for details.
django-helpdesk was formerly known as Jutda Helpdesk, named after the django-helpdesk was formerly known as Jutda Helpdesk, named after the
@ -53,7 +52,7 @@ Installation
`django-helpdesk` requires: `django-helpdesk` requires:
* Python 3.8+ * Python 3.8+
* Django 2.2 LTS or 3.2 LTS (recommend migration to 3.2 as soon as possible) * Django 3.2 LTS highly recommended (early adopters may test Django 4)
You can quickly install the latest stable version of `django-helpdesk` You can quickly install the latest stable version of `django-helpdesk`
app via `pip`:: app via `pip`::

View File

@ -4,26 +4,19 @@
# https://docs.microsoft.com/azure/devops/pipelines/languages/python # https://docs.microsoft.com/azure/devops/pipelines/languages/python
trigger: trigger:
- master - unstable
- stable
- 0.3 - 0.3
pr: pr:
- master - unstable
- stable
- 0.3 - 0.3
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest
strategy: strategy:
matrix: matrix:
Python38Django22:
PYTHON_VERSION: '3.8'
DJANGO_VERSION: '22'
Python39Django22:
PYTHON_VERSION: '3.9'
DJANGO_VERSION: '22'
Python310Django22:
PYTHON_VERSION: '3.10'
DJANGO_VERSION: '22'
Python38Django32: Python38Django32:
PYTHON_VERSION: '3.8' PYTHON_VERSION: '3.8'
DJANGO_VERSION: '32' DJANGO_VERSION: '32'
@ -33,7 +26,16 @@ strategy:
Python310Django32: Python310Django32:
PYTHON_VERSION: '3.10' PYTHON_VERSION: '3.10'
DJANGO_VERSION: '32' DJANGO_VERSION: '32'
maxParallel: 1 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: steps:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
@ -42,29 +44,29 @@ steps:
architecture: 'x64' architecture: 'x64'
- task: PythonScript@0 - task: PythonScript@0
displayName: 'Export project path' displayName: 'Export quicktest.py path'
inputs: inputs:
scriptSource: 'inline' scriptSource: 'inline'
script: | script: |
"""Search all subdirectories for `manage.py`.""" """Search all subdirectories for `quicktest.py`."""
from glob import iglob from glob import iglob
from os import path from os import path
# Python >= 3.5 # Python >= 3.5
manage_py = next(iglob(path.join('**', 'manage.py'), recursive=True), None) quicktest_py = next(iglob(path.join('**', 'quicktest.py'), recursive=True), None)
if not manage_py: if not quicktest_py:
raise SystemExit('Could not find a Django project') raise SystemExit('Could not find quicktest.py for django-helpdesk')
project_location = path.dirname(path.abspath(manage_py)) project_location = path.dirname(path.abspath(quicktest_py))
print('Found Django project in', project_location) print('Found quicktest.py in', project_location)
print('##vso[task.setvariable variable=projectRoot]{}'.format(project_location)) print('##vso[task.setvariable variable=projectRoot]{}'.format(project_location))
- script: | - script: |
python -m pip install --upgrade pip setuptools wheel 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.txt
pip install -c constraints-Django$(DJANGO_VERSION).txt -r requirements-testing.txt
pip install unittest-xml-reporting pip install unittest-xml-reporting
displayName: 'Install prerequisites' displayName: 'Install prerequisites'
- script: | - script: |
pushd '$(projectRoot)' pushd '$(projectRoot)'
#python manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input
python quicktest.py helpdesk python quicktest.py helpdesk
displayName: 'Run tests' displayName: 'Run tests'

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

@ -8,8 +8,10 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/ https://docs.djangoproject.com/en/1.11/ref/settings/
""" """
import os import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -97,13 +99,13 @@ WSGI_APPLICATION = 'demo.demodesk.config.wsgi.application'
# Some common settings are below. # Some common settings are below.
HELPDESK_DEFAULT_SETTINGS = { HELPDESK_DEFAULT_SETTINGS = {
'use_email_as_submitter': True, 'use_email_as_submitter': True,
'email_on_ticket_assign': True, 'email_on_ticket_assign': True,
'email_on_ticket_change': True, 'email_on_ticket_change': True,
'login_view_ticketlist': True, 'login_view_ticketlist': True,
'email_on_ticket_apichange': True, 'email_on_ticket_apichange': True,
'preset_replies': True, 'preset_replies': True,
'tickets_per_page': 25 'tickets_per_page': 25
} }
# Should the public web portal be enabled? # Should the public web portal be enabled?
@ -153,7 +155,7 @@ SITE_ID = 1
# Sessions # Sessions
# https://docs.djangoproject.com/en/1.11/topics/http/sessions # https://docs.djangoproject.com/en/1.11/topics/http/sessions
SESSION_COOKIE_AGE = 86400 # = 1 day SESSION_COOKIE_AGE = 86400 # = 1 day
# For better default security, set these cookie flags, but # For better default security, set these cookie flags, but
# these are likely to cause problems when testing locally # these are likely to cause problems when testing locally

View File

@ -13,10 +13,10 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include 1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
from django.conf.urls import url, include
from django.contrib import admin
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
# The following uses the static() helper function, # The following uses the static() helper function,
@ -26,7 +26,7 @@ from django.conf.urls.static import static
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls), path('admin/', admin.site.urls),
url(r'^', include('helpdesk.urls', namespace='helpdesk')), path('', include('helpdesk.urls', namespace='helpdesk')),
url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework')) path('api/auth/', include('rest_framework.urls', namespace='rest_framework'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -7,9 +7,10 @@ For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
""" """
import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")

View File

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

View File

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

View File

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Python packaging.""" """Python packaging."""
from __future__ import unicode_literals
from setuptools import setup
import os import os
from setuptools import setup
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
project_root = os.path.dirname(here) project_root = os.path.dirname(here)
@ -13,7 +11,7 @@ project_root = os.path.dirname(here)
NAME = 'django-helpdesk-demodesk' NAME = 'django-helpdesk-demodesk'
DESCRIPTION = 'A demo Django project using django-helpdesk' DESCRIPTION = 'A demo Django project using django-helpdesk'
README = open(os.path.join(here, 'README.rst')).read() README = open(os.path.join(here, 'README.rst')).read()
VERSION = '0.3.5' VERSION = '0.5.0a1'
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip() #VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
AUTHOR = 'django-helpdesk team' AUTHOR = 'django-helpdesk team'
URL = 'https://github.com/django-helpdesk/django-helpdesk' URL = 'https://github.com/django-helpdesk/django-helpdesk'
@ -22,13 +20,13 @@ CLASSIFIERS = ['Development Status :: 4 - Beta',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Framework :: Django :: 2.2', 'Framework :: Django :: 3.2',
'Framework :: Django :: 3.2'] 'Framework :: Django :: 4.0']
KEYWORDS = [] KEYWORDS = []
PACKAGES = ['demodesk'] PACKAGES = ['demodesk']
REQUIREMENTS = [ REQUIREMENTS = [
'django-helpdesk' 'django-helpdesk'
] ]
ENTRY_POINTS = { ENTRY_POINTS = {
'console_scripts': ['demodesk = demodesk.manage:main'] 'console_scripts': ['demodesk = demodesk.manage:main']
} }

View File

@ -1,20 +1,25 @@
API 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:: If you wish to use it, you have to add this line in your settings::
HELPDESK_ACTIVATE_API_ENDPOINT = True 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 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 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 - **due_date**: date representation for when the ticket is due
- **merged_to**: ID of the ticket to which it is merged - **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>``. 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' \ --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}' --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. 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 : 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 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. You must include all fields in the JSON body.
PATCH 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. You can include only the fields you need to update in the JSON body.
DELETE 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,16 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys, os import os
import sys
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.')) #sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0' #needs_sphinx = '1.0'
@ -87,7 +89,7 @@ pygments_style = 'sphinx'
#modindex_common_prefix = [] #modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
@ -167,7 +169,7 @@ html_static_path = ['_static']
htmlhelp_basename = 'django-helpdeskdoc' htmlhelp_basename = 'django-helpdeskdoc'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------
# The paper size ('letter' or 'a4'). # The paper size ('letter' or 'a4').
#latex_paper_size = 'letter' #latex_paper_size = 'letter'
@ -178,8 +180,8 @@ htmlhelp_basename = 'django-helpdeskdoc'
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'django-helpdesk.tex', u'django-helpdesk Documentation', ('index', 'django-helpdesk.tex', u'django-helpdesk Documentation',
u'Ross Poulton + django-helpdesk Contributors', 'manual'), u'Ross Poulton + django-helpdesk Contributors', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -206,7 +208,7 @@ latex_documents = [
#latex_domain_indices = True #latex_domain_indices = True
# -- Options for manual page output -------------------------------------------- # -- Options for manual page output --------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).

View File

@ -10,7 +10,7 @@ Prerequisites
Before getting started, ensure your system meets the following recommended dependencies: Before getting started, ensure your system meets the following recommended dependencies:
* Python 3.8+ * Python 3.8+
* Django 2.2 LTS or 3.2 LTS (strongly recommend migrating to 3.2 LTS as soon as possible) * Django 3.2 LTS
Ensure any extra Django modules you wish to use are compatible before continuing. Ensure any extra Django modules you wish to use are compatible before continuing.
@ -63,6 +63,7 @@ errors with trying to create User settings.
'pinax.teams', # Team support 'pinax.teams', # Team support
'reversion', # Required by pinax-teams 'reversion', # Required by pinax-teams
'rest_framework', # required for the API 'rest_framework', # required for the API
'django_cleanup.apps.CleanupConfig', # Remove this if you do NOT want to delete files on the file system when the associated record is deleted in the database
'helpdesk', # This is us! 'helpdesk', # This is us!
) )
@ -74,11 +75,12 @@ errors with trying to create User settings.
SITE_ID = 1 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')), 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')), path('', include('helpdesk.urls', namespace='helpdesk')),

View File

@ -4,6 +4,7 @@ License
django-helpdesk is released under the terms of the BSD license. You must agree to these terms before installing or using django-helpdesk.:: django-helpdesk is released under the terms of the BSD license. You must agree to these terms before installing or using django-helpdesk.::
Copyright (c) 2008, Ross Poulton (Trading as Jutda) Copyright (c) 2008, Ross Poulton (Trading as Jutda)
Copyright (c) 2008-2021, django-helpdesk contributors
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,

View File

@ -11,6 +11,12 @@ Please consult the Installation instructions for general instructions and tips.
The tips below are based on modifications of the original installation instructions. The tips below are based on modifications of the original installation instructions.
0.3 -> 0.4
----------
- Under `INSTALLED_APPS`, `bootstrap4form` needs to be replaced with `bootstrap5form`
0.2 -> 0.3 0.2 -> 0.3
---------- ----------

View File

@ -1,13 +1,25 @@
from django.contrib import admin from django.contrib import admin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply
from helpdesk.models import EscalationExclusion, EmailTemplate
from helpdesk.models import TicketChange, KBIAttachment, FollowUpAttachment, IgnoreEmail
from helpdesk.models import CustomField
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import (
CustomField,
EmailTemplate,
EscalationExclusion,
FollowUp,
FollowUpAttachment,
IgnoreEmail,
KBIAttachment,
PreSetReply,
Queue,
Ticket,
TicketChange
)
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import KBCategory from helpdesk.models import KBCategory, KBItem
from helpdesk.models import KBItem
@admin.register(Queue) @admin.register(Queue)
class QueueAdmin(admin.ModelAdmin): class QueueAdmin(admin.ModelAdmin):
@ -74,15 +86,17 @@ class FollowUpAdmin(admin.ModelAdmin):
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
@admin.register(KBItem) @admin.register(KBItem)
class KBItemAdmin(admin.ModelAdmin): class KBItemAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'last_updated', 'team', 'order', 'enabled') list_display = ('category', 'title', 'last_updated',
'team', 'order', 'enabled')
inlines = [KBIAttachmentInline] inlines = [KBIAttachmentInline]
readonly_fields = ('voted_by', 'downvoted_by') readonly_fields = ('voted_by', 'downvoted_by')
list_display_links = ('title',) list_display_links = ('title',)
@admin.register(KBCategory) if helpdesk_settings.HELPDESK_KB_ENABLED:
class KBCategoryAdmin(admin.ModelAdmin): @admin.register(KBCategory)
list_display = ('name', 'title', 'slug', 'public') class KBCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'title', 'slug', 'public')
@admin.register(CustomField) @admin.register(CustomField)

View File

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

View File

@ -1,12 +1,8 @@
from functools import wraps from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from functools import wraps
from django.contrib.auth.decorators import user_passes_test
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings

View File

@ -4,35 +4,36 @@ Django Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved. (c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved.
See LICENSE for details. See LICENSE for details.
""" """
# import base64 # import base64
import email
import imaplib
import logging
import mimetypes
import os
import poplib
import re
import socket
import ssl
import sys
from datetime import timedelta
from email.utils import getaddresses
from os.path import isfile, join
from time import ctime
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from datetime import timedelta
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q from django.db.models import Q
from django.utils import encoding, timezone from django.utils import encoding, timezone
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
import email
from email.utils import getaddresses
from email_reply_parser import EmailReplyParser from email_reply_parser import EmailReplyParser
from helpdesk import settings from helpdesk import settings
from helpdesk.lib import safe_template_context, process_attachments from helpdesk.lib import process_attachments, safe_template_context
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket
import imaplib
import logging
import mimetypes
import os
from os.path import isfile, join
import poplib
import re
import socket
import ssl
import sys
from time import ctime
import typing
# import User model, which may be a custom model # import User model, which may be a custom model
@ -72,7 +73,8 @@ def process_email(quiet=False):
# Log messages to specific file only if the queue has it configured # Log messages to specific file only if the queue has it configured
if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set
log_file_handler = logging.FileHandler(join(q.logging_dir, q.slug + '_get_email.log')) log_file_handler = logging.FileHandler(
join(q.logging_dir, q.slug + '_get_email.log'))
logger.addHandler(log_file_handler) logger.addHandler(log_file_handler)
else: else:
log_file_handler = None log_file_handler = None
@ -105,7 +107,8 @@ def pop3_sync(q, logger, server):
try: try:
server.stls() server.stls()
except Exception: except Exception:
logger.warning("POP3 StartTLS failed or unsupported. Connection will be unencrypted.") logger.warning(
"POP3 StartTLS failed or unsupported. Connection will be unencrypted.")
server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER) server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
@ -127,16 +130,21 @@ def pop3_sync(q, logger, server):
raw_content = server.retr(msgNum)[1] raw_content = server.retr(msgNum)[1]
if type(raw_content[0]) is bytes: if type(raw_content[0]) is bytes:
full_message = "\n".join([elm.decode('utf-8') for elm in raw_content]) full_message = "\n".join([elm.decode('utf-8')
for elm in raw_content])
else: else:
full_message = encoding.force_text("\n".join(raw_content), errors='replace') full_message = encoding.force_str(
ticket = object_from_message(message=full_message, queue=q, logger=logger) "\n".join(raw_content), errors='replace')
ticket = object_from_message(
message=full_message, queue=q, logger=logger)
if ticket: if ticket:
server.dele(msgNum) server.dele(msgNum)
logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum) logger.info(
"Successfully processed message %s, deleted from POP3 server" % msgNum)
else: else:
logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum) logger.warn(
"Message %s was not successfully processed, and will be left on POP3 server" % msgNum)
server.quit() server.quit()
@ -146,7 +154,8 @@ def imap_sync(q, logger, server):
try: try:
server.starttls() server.starttls()
except Exception: except Exception:
logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.") logger.warning(
"IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
server.login(q.email_box_user or server.login(q.email_box_user or
settings.QUEUE_EMAIL_BOX_USER, settings.QUEUE_EMAIL_BOX_USER,
q.email_box_pass or q.email_box_pass or
@ -168,23 +177,26 @@ def imap_sync(q, logger, server):
sys.exit() sys.exit()
try: try:
status, data = server.search(None, 'NOT', 'DELETED') data = server.search(None, 'NOT', 'DELETED')[1]
if data: if data:
msgnums = data[0].split() msgnums = data[0].split()
logger.info("Received %d messages from IMAP server" % len(msgnums)) logger.info("Received %d messages from IMAP server" % len(msgnums))
for num in msgnums: for num in msgnums:
logger.info("Processing message %s" % num) logger.info("Processing message %s" % num)
status, data = server.fetch(num, '(RFC822)') data = server.fetch(num, '(RFC822)')[1]
full_message = encoding.force_text(data[0][1], errors='replace') full_message = encoding.force_str(data[0][1], errors='replace')
try: try:
ticket = object_from_message(message=full_message, queue=q, logger=logger) ticket = object_from_message(
message=full_message, queue=q, logger=logger)
except TypeError: except TypeError:
ticket = None # hotfix. Need to work out WHY. ticket = None # hotfix. Need to work out WHY.
if ticket: if ticket:
server.store(num, '+FLAGS', '\\Deleted') server.store(num, '+FLAGS', '\\Deleted')
logger.info("Successfully processed message %s, deleted from IMAP server" % num) logger.info(
"Successfully processed message %s, deleted from IMAP server" % num)
else: else:
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num) logger.warn(
"Message %s was not successfully processed, and will be left on IMAP server" % num)
except imaplib.IMAP4.error: except imaplib.IMAP4.error:
logger.error( logger.error(
"IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?", "IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?",
@ -261,25 +273,31 @@ def process_queue(q, logger):
elif email_box_type == 'local': elif email_box_type == 'local':
mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/' mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/'
mail = [join(mail_dir, f) for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))] mail = [join(mail_dir, f)
for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))]
logger.info("Found %d messages in local mailbox directory" % len(mail)) logger.info("Found %d messages in local mailbox directory" % len(mail))
logger.info("Found %d messages in local mailbox directory" % len(mail)) logger.info("Found %d messages in local mailbox directory" % len(mail))
for i, m in enumerate(mail, 1): for i, m in enumerate(mail, 1):
logger.info("Processing message %d" % i) logger.info("Processing message %d" % i)
with open(m, 'r') as f: with open(m, 'r') as f:
full_message = encoding.force_text(f.read(), errors='replace') full_message = encoding.force_str(f.read(), errors='replace')
ticket = object_from_message(message=full_message, queue=q, logger=logger) ticket = object_from_message(
message=full_message, queue=q, logger=logger)
if ticket: if ticket:
logger.info("Successfully processed message %d, ticket/comment created.", i) logger.info(
"Successfully processed message %d, ticket/comment created.", i)
try: try:
os.unlink(m) # delete message file if ticket was successful # delete message file if ticket was successful
os.unlink(m)
except OSError as e: except OSError as e:
logger.error("Unable to delete message %d (%s).", i, str(e)) logger.error(
"Unable to delete message %d (%s).", i, str(e))
else: else:
logger.info("Successfully deleted message %d.", i) logger.info("Successfully deleted message %d.", i)
else: else:
logger.warn("Message %d was not successfully processed, and will be left in local directory", i) logger.warn(
"Message %d was not successfully processed, and will be left in local directory", i)
def decodeUnknown(charset, string): def decodeUnknown(charset, string):
@ -309,8 +327,10 @@ def is_autoreply(message):
So we don't start mail loops So we don't start mail loops
""" """
any_if_this = [ any_if_this = [
False if not message.get("Auto-Submitted") else message.get("Auto-Submitted").lower() != "no", False if not message.get(
True if message.get("X-Auto-Response-Suppress") in ("DR", "AutoReply", "All") else False, "Auto-Submitted") else message.get("Auto-Submitted").lower() != "no",
True if message.get("X-Auto-Response-Suppress") in ("DR",
"AutoReply", "All") else False,
message.get("List-Id"), message.get("List-Id"),
message.get("List-Unsubscribe"), message.get("List-Unsubscribe"),
] ]
@ -323,10 +343,10 @@ def create_ticket_cc(ticket, cc_list):
return [] return []
# Local import to deal with non-defined / circular reference problem # Local import to deal with non-defined / circular reference problem
from helpdesk.views.staff import User, subscribe_to_ticket_updates from helpdesk.views.staff import subscribe_to_ticket_updates, User
new_ticket_ccs = [] new_ticket_ccs = []
for cced_name, cced_email in cc_list: for __, cced_email in cc_list:
cced_email = cced_email.strip() cced_email = cced_email.strip()
if cced_email == ticket.queue.email_address: if cced_email == ticket.queue.email_address:
@ -335,12 +355,13 @@ def create_ticket_cc(ticket, cc_list):
user = None user = None
try: try:
user = User.objects.get(email=cced_email) user = User.objects.get(email=cced_email) # @UndefinedVariable
except User.DoesNotExist: except User.DoesNotExist:
pass pass
try: try:
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email) ticket_cc = subscribe_to_ticket_updates(
ticket=ticket, user=user, email=cced_email)
new_ticket_ccs.append(ticket_cc) new_ticket_ccs.append(ticket_cc)
except ValidationError: except ValidationError:
pass pass
@ -370,7 +391,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
if in_reply_to is not None: if in_reply_to is not None:
try: try:
queryset = FollowUp.objects.filter(message_id=in_reply_to).order_by('-date') queryset = FollowUp.objects.filter(
message_id=in_reply_to).order_by('-date')
if queryset.count() > 0: if queryset.count() > 0:
previous_followup = queryset.first() previous_followup = queryset.first()
ticket = previous_followup.ticket ticket = previous_followup.ticket
@ -386,7 +408,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
new = False new = False
# Check if the ticket has been merged to another ticket # Check if the ticket has been merged to another ticket
if ticket.merged_to: if ticket.merged_to:
logger.info("Ticket has been merged to %s" % ticket.merged_to.ticket) logger.info("Ticket has been merged to %s" %
ticket.merged_to.ticket)
# Use the ticket in which it was merged to for next operations # Use the ticket in which it was merged to for next operations
ticket = ticket.merged_to ticket = ticket.merged_to
@ -402,7 +425,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
priority=payload['priority'], priority=payload['priority'],
) )
ticket.save() ticket.save()
logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id)) logger.debug("Created new ticket %s-%s" %
(ticket.queue.slug, ticket.id))
new = True new = True
@ -413,7 +437,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
f = FollowUp( f = FollowUp(
ticket=ticket, ticket=ticket,
title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}), title=_('E-Mail Received from %(sender_email)s' %
{'sender_email': sender_email}),
date=now, date=now,
public=True, public=True,
comment=payload.get('full_body', payload['body']) or "", comment=payload.get('full_body', payload['body']) or "",
@ -422,7 +447,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
if ticket.status == Ticket.REOPENED_STATUS: if ticket.status == Ticket.REOPENED_STATUS:
f.new_status = Ticket.REOPENED_STATUS f.new_status = Ticket.REOPENED_STATUS
f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}) f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' %
{'sender_email': sender_email})
f.save() f.save()
logger.debug("Created new FollowUp for Ticket") logger.debug("Created new FollowUp for Ticket")
@ -441,18 +467,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
new_ticket_ccs = [] new_ticket_ccs = []
new_ticket_ccs.append(create_ticket_cc(ticket, to_list + cc_list)) new_ticket_ccs.append(create_ticket_cc(ticket, to_list + cc_list))
notifications_to_be_sent = [sender_email]
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)
for email_address in ticket_cc_list:
notifications_to_be_sent.append(email_address)
autoreply = is_autoreply(message) autoreply = is_autoreply(message)
if autoreply: if autoreply:
logger.info("Message seems to be auto-reply, not sending any emails back to the sender") logger.info(
"Message seems to be auto-reply, not sending any emails back to the sender")
else: else:
# send mail to appropriate people now depending on what objects # send mail to appropriate people now depending on what objects
# were created and who was CC'd # were created and who was CC'd
@ -489,12 +507,83 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
return ticket return ticket
def object_from_message(message, queue, logger): def get_ticket_id_from_subject_slug(
queue_slug: str,
subject: str,
logger: logging.Logger
) -> typing.Optional[int]:
"""Get a ticket id from the subject string
Performs a match on the subject using the queue_slug as reference,
returning the ticket id if a match is found.
"""
matchobj = re.match(r".*\[" + queue_slug + r"-(?P<id>\d+)\]", subject)
ticket_id = None
if matchobj:
# This is a reply or forward.
ticket_id = matchobj.group('id')
logger.info("Matched tracking ID %s-%s" % (queue_slug, ticket_id))
else:
logger.info("No tracking ID matched.")
return ticket_id
def add_file_if_always_save_incoming_email_message(
files_,
message: str
) -> None:
"""When `settings.HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE` is `True`
add a file to the files_ list"""
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False):
# save message as attachment in case of some complex markup renders
# wrong
files_.append(
SimpleUploadedFile(
_("original_message.eml").replace(
".eml",
timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml"
),
str(message).encode("utf-8"),
'text/plain'
)
)
def get_encoded_body(body: str) -> str:
try:
return body.encode('ascii').decode('unicode_escape')
except UnicodeEncodeError:
return body
def get_body_from_fragments(body) -> str:
"""Gets a body from the fragments, joined by a double line break"""
return "\n\n".join(f.content for f in EmailReplyParser.read(body).fragments)
def get_email_body_from_part_payload(part) -> str:
"""Gets an decoded body from the payload part, if the decode fails,
returns without encoding"""
try:
return encoding.smart_str(
part.get_payload(decode=True)
)
except UnicodeDecodeError:
return encoding.smart_str(
part.get_payload(decode=False)
)
def object_from_message(message: str,
queue: Queue,
logger: logging.Logger
) -> Ticket:
# 'message' must be an RFC822 formatted message. # 'message' must be an RFC822 formatted message.
message = email.message_from_string(message) message = email.message_from_string(message)
subject = message.get('subject', _('Comment from e-mail')) subject = message.get('subject', _('Comment from e-mail'))
subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) subject = decode_mail_headers(
decodeUnknown(message.get_charset(), subject))
for affix in STRIPPED_SUBJECT_STRINGS: for affix in STRIPPED_SUBJECT_STRINGS:
subject = subject.replace(affix, "") subject = subject.replace(affix, "")
subject = subject.strip() subject = subject.strip()
@ -508,35 +597,20 @@ def object_from_message(message, queue, logger):
# Note that the replace won't work on just an email with no real name, # Note that the replace won't work on just an email with no real name,
# but the getaddresses() function seems to be able to handle just unclosed quotes # but the getaddresses() function seems to be able to handle just unclosed quotes
# correctly. Not ideal, but this seems to work for now. # correctly. Not ideal, but this seems to work for now.
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1] sender_email = email.utils.getaddresses(
['\"' + sender.replace('<', '\" <')])[0][1]
cc = message.get_all('cc', None)
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
tempcc = []
for hdr in cc:
tempcc.extend(hdr.split(','))
# use a set to ensure no duplicates
cc = set([x.strip() for x in tempcc])
for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)): for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)):
if ignore.test(sender_email): if ignore.test(sender_email):
if ignore.keep_in_mailbox: # By returning 'False' the message will be kept in the mailbox,
# By returning 'False' the message will be kept in the mailbox, # and the 'True' will cause the message to be deleted.
# and the 'True' will cause the message to be deleted. return not ignore.keep_in_mailbox
return False
return True
matchobj = re.match(r".*\[" + queue.slug + r"-(?P<id>\d+)\]", subject) ticket_id: typing.Optional[int] = get_ticket_id_from_subject_slug(
if matchobj: queue.slug,
# This is a reply or forward. subject,
ticket = matchobj.group('id') logger
logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket)) )
else:
logger.info("No tracking ID matched.")
ticket = None
body = None body = None
full_body = None full_body = None
@ -560,32 +634,27 @@ def object_from_message(message, queue, logger):
body = decodeUnknown(part.get_content_charset(), body) body = decodeUnknown(part.get_content_charset(), body)
# have to use django_settings here so overwritting it works in tests # have to use django_settings here so overwritting it works in tests
# the default value is False anyway # the default value is False anyway
if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False): if ticket_id 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
body_parts = [] # losing forwards and things like that
for f in EmailReplyParser.read(body).fragments: full_body = get_body_from_fragments(body)
body_parts.append(f.content)
full_body = '\n\n'.join(body_parts)
body = EmailReplyParser.parse_reply(body) body = EmailReplyParser.parse_reply(body)
else: else:
# second and other reply, save only first part of the message # second and other reply, save only first part of the
# message
body = EmailReplyParser.parse_reply(body) body = EmailReplyParser.parse_reply(body)
full_body = body full_body = body
# workaround to get unicode text out rather than escaped text # workaround to get unicode text out rather than escaped text
try: body = get_encoded_body(body)
body = body.encode('ascii').decode('unicode_escape')
except UnicodeEncodeError:
body.encode('utf-8')
logger.debug("Discovered plain text MIME part") logger.debug("Discovered plain text MIME part")
else: else:
try: email_body = get_email_body_from_part_payload(part)
email_body = encoding.smart_text(part.get_payload(decode=True))
except UnicodeDecodeError:
email_body = encoding.smart_text(part.get_payload(decode=False))
if not body and not full_body: if not body and not full_body:
# no text has been parsed so far - try such deep parsing for some messages # no text has been parsed so far - try such deep parsing
altered_body = email_body.replace("</p>", "</p>\n").replace("<br", "\n<br") # for some messages
altered_body = email_body.replace(
"</p>", "</p>\n").replace("<br", "\n<br")
mail = BeautifulSoup(str(altered_body), "html.parser") mail = BeautifulSoup(str(altered_body), "html.parser")
full_body = mail.get_text() full_body = mail.get_text()
@ -601,7 +670,8 @@ def object_from_message(message, queue, logger):
'</html>' '</html>'
) % email_body ) % email_body
files.append( files.append(
SimpleUploadedFile(_("email_html_body.html"), payload.encode("utf-8"), 'text/html') SimpleUploadedFile(
_("email_html_body.html"), payload.encode("utf-8"), 'text/html')
) )
logger.debug("Discovered HTML MIME part") logger.debug("Discovered HTML MIME part")
else: else:
@ -627,7 +697,8 @@ def object_from_message(message, queue, logger):
# except non_b64_err: # except non_b64_err:
# logger.debug("Payload was not base64 encoded, using raw bytes") # logger.debug("Payload was not base64 encoded, using raw bytes")
# # payloadToWrite = payload # # payloadToWrite = payload
files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) files.append(SimpleUploadedFile(name, part.get_payload(
decode=True), mimetypes.guess_type(name)[0]))
logger.debug("Found MIME attachment %s" % name) logger.debug("Found MIME attachment %s" % name)
counter += 1 counter += 1
@ -644,23 +715,13 @@ def object_from_message(message, queue, logger):
if not body: if not body:
body = "" body = ""
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False): add_file_if_always_save_incoming_email_message(files, message)
# save message as attachment in case of some complex markup renders wrong
files.append(
SimpleUploadedFile(
_("original_message.eml").replace(
".eml",
timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml"
),
str(message).encode("utf-8"),
'text/plain'
)
)
smtp_priority = message.get('priority', '') smtp_priority = message.get('priority', '')
smtp_importance = message.get('importance', '') smtp_importance = message.get('importance', '')
high_priority_types = {'high', 'important', '1', 'urgent'} high_priority_types = {'high', 'important', '1', 'urgent'}
priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3 priority = 2 if high_priority_types & {
smtp_priority, smtp_importance} else 3
payload = { payload = {
'body': body, 'body': body,
@ -672,4 +733,4 @@ def object_from_message(message, queue, logger):
'files': files, 'files': files,
} }
return create_object_from_email_message(message, ticket, payload, files, logger=logger) return create_object_from_email_message(message, ticket_id, payload, files, logger=logger)

View File

@ -6,25 +6,38 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
forms.py - Definitions of newforms-based forms for creating and maintaining forms.py - Definitions of newforms-based forms for creating and maintaining
tickets. tickets.
""" """
import logging
from datetime import datetime, date, time
from django.core.exceptions import ObjectDoesNotExist, ValidationError from datetime import datetime
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from helpdesk.lib import safe_template_context, process_attachments, convert_value
from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC,
CustomField, TicketCustomFieldValue, TicketDependency, UserSettings)
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.settings import CUSTOMFIELD_TO_FIELD_DICT, CUSTOMFIELD_DATETIME_FORMAT, \ from helpdesk.lib import convert_value, process_attachments, safe_template_context
CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT from helpdesk.models import (
CustomField,
FollowUp,
IgnoreEmail,
Queue,
Ticket,
TicketCC,
TicketCustomFieldValue,
TicketDependency,
UserSettings
)
from helpdesk.settings import (
CUSTOMFIELD_DATE_FORMAT,
CUSTOMFIELD_DATETIME_FORMAT,
CUSTOMFIELD_TIME_FORMAT,
CUSTOMFIELD_TO_FIELD_DICT
)
import logging
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import (KBItem) from helpdesk.models import KBItem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
@ -37,40 +50,49 @@ class CustomFieldMixin(object):
def customfield_to_field(self, field, instanceargs): def customfield_to_field(self, field, instanceargs):
# Use TextInput widget by default # Use TextInput widget by default
instanceargs['widget'] = forms.TextInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.TextInput(
attrs={'class': 'form-control'})
# if-elif branches start with special cases # if-elif branches start with special cases
if field.data_type == 'varchar': if field.data_type == 'varchar':
fieldclass = forms.CharField fieldclass = forms.CharField
instanceargs['max_length'] = field.max_length instanceargs['max_length'] = field.max_length
elif field.data_type == 'text': elif field.data_type == 'text':
fieldclass = forms.CharField fieldclass = forms.CharField
instanceargs['widget'] = forms.Textarea(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.Textarea(
attrs={'class': 'form-control'})
instanceargs['max_length'] = field.max_length instanceargs['max_length'] = field.max_length
elif field.data_type == 'integer': elif field.data_type == 'integer':
fieldclass = forms.IntegerField fieldclass = forms.IntegerField
instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.NumberInput(
attrs={'class': 'form-control'})
elif field.data_type == 'decimal': elif field.data_type == 'decimal':
fieldclass = forms.DecimalField fieldclass = forms.DecimalField
instanceargs['decimal_places'] = field.decimal_places instanceargs['decimal_places'] = field.decimal_places
instanceargs['max_digits'] = field.max_length instanceargs['max_digits'] = field.max_length
instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.NumberInput(
attrs={'class': 'form-control'})
elif field.data_type == 'list': elif field.data_type == 'list':
fieldclass = forms.ChoiceField fieldclass = forms.ChoiceField
instanceargs['choices'] = field.get_choices() instanceargs['choices'] = field.get_choices()
instanceargs['widget'] = forms.Select(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.Select(
attrs={'class': 'form-control'})
else: else:
# Try to use the immediate equivalences dictionary # Try to use the immediate equivalences dictionary
try: try:
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type] fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
# Change widgets for the following classes # Change widgets for the following classes
if fieldclass == forms.DateField: if fieldclass == forms.DateField:
instanceargs['widget'] = forms.DateInput(attrs={'class': 'form-control date-field'}) instanceargs['widget'] = forms.DateInput(
attrs={'class': 'form-control date-field'})
elif fieldclass == forms.DateTimeField: elif fieldclass == forms.DateTimeField:
instanceargs['widget'] = forms.DateTimeInput(attrs={'class': 'form-control datetime-field'}) instanceargs['widget'] = forms.DateTimeInput(
attrs={'class': 'form-control datetime-field'})
elif fieldclass == forms.TimeField: elif fieldclass == forms.TimeField:
instanceargs['widget'] = forms.TimeInput(attrs={'class': 'form-control time-field'}) instanceargs['widget'] = forms.TimeInput(
attrs={'class': 'form-control time-field'})
elif fieldclass == forms.BooleanField: elif fieldclass == forms.BooleanField:
instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.CheckboxInput(
attrs={'class': 'form-control'})
except KeyError: except KeyError:
# The data_type was not found anywhere # The data_type was not found anywhere
@ -83,10 +105,12 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
class Meta: class Meta:
model = Ticket model = Ticket
exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to') exclude = ('created', 'modified', 'status', 'on_hold',
'resolution', 'last_escalation', 'assigned_to')
class Media: class Media:
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js') js = ('helpdesk/js/init_due_date.js',
'helpdesk/js/init_datetime_classes.js')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
@ -96,21 +120,28 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
# Disable and add help_text to the merged_to field on this form # Disable and add help_text to the merged_to field on this form
self.fields['merged_to'].disabled = True self.fields['merged_to'].disabled = True
self.fields['merged_to'].help_text = _('This ticket is merged into the selected ticket.') self.fields['merged_to'].help_text = _(
'This ticket is merged into the selected ticket.')
for field in CustomField.objects.all(): for field in CustomField.objects.all():
initial_value = None initial_value = None
try: try:
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field) current_value = TicketCustomFieldValue.objects.get(
ticket=self.instance, field=field)
initial_value = current_value.value initial_value = current_value.value
# Attempt to convert from fixed format string to date/time data type # Attempt to convert from fixed format string to date/time data
# type
if 'datetime' == current_value.field.data_type: if 'datetime' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATETIME_FORMAT) initial_value = datetime.strptime(
initial_value, CUSTOMFIELD_DATETIME_FORMAT)
elif 'date' == current_value.field.data_type: elif 'date' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATE_FORMAT) initial_value = datetime.strptime(
initial_value, CUSTOMFIELD_DATE_FORMAT)
elif 'time' == current_value.field.data_type: elif 'time' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_TIME_FORMAT) initial_value = datetime.strptime(
# If it is boolean field, transform the value to a real boolean instead of a string initial_value, CUSTOMFIELD_TIME_FORMAT)
# If it is boolean field, transform the value to a real boolean
# instead of a string
elif 'boolean' == current_value.field.data_type: elif 'boolean' == current_value.field.data_type:
initial_value = 'True' == initial_value initial_value = 'True' == initial_value
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError): except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
@ -133,9 +164,11 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
field_name = field.replace('custom_', '', 1) field_name = field.replace('custom_', '', 1)
customfield = CustomField.objects.get(name=field_name) customfield = CustomField.objects.get(name=field_name)
try: try:
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue.objects.get(
ticket=self.instance, field=customfield)
except ObjectDoesNotExist: except ObjectDoesNotExist:
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue(
ticket=self.instance, field=customfield)
cfv.value = convert_value(value) cfv.value = convert_value(value)
cfv.save() cfv.save()
@ -152,7 +185,8 @@ class EditFollowUpForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Filter not openned tickets here.""" """Filter not openned tickets here."""
super(EditFollowUpForm, self).__init__(*args, **kwargs) super(EditFollowUpForm, self).__init__(*args, **kwargs)
self.fields["ticket"].queryset = Ticket.objects.filter(status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS)) self.fields["ticket"].queryset = Ticket.objects.filter(
status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
class AbstractTicketForm(CustomFieldMixin, forms.Form): class AbstractTicketForm(CustomFieldMixin, forms.Form):
@ -178,7 +212,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.Textarea(attrs={'class': 'form-control'}), widget=forms.Textarea(attrs={'class': 'form-control'}),
label=_('Description of your issue'), label=_('Description of your issue'),
required=True, required=True,
help_text=_('Please be as descriptive as possible and include all details'), help_text=_(
'Please be as descriptive as possible and include all details'),
) )
priority = forms.ChoiceField( priority = forms.ChoiceField(
@ -187,13 +222,16 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
required=True, required=True,
initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'), initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'),
label=_('Priority'), label=_('Priority'),
help_text=_("Please select a priority carefully. If unsure, leave it as '3'."), help_text=_(
"Please select a priority carefully. If unsure, leave it as '3'."),
) )
due_date = forms.DateTimeField( due_date = forms.DateTimeField(
widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}), widget=forms.TextInput(
attrs={'class': 'form-control', 'autocomplete': 'off'}),
required=False, required=False,
input_formats=[CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"], input_formats=[CUSTOMFIELD_DATE_FORMAT,
CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
label=_('Due on'), label=_('Due on'),
) )
@ -201,11 +239,15 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.FileInput(attrs={'class': 'form-control-file'}), widget=forms.FileInput(attrs={'class': 'form-control-file'}),
required=False, required=False,
label=_('Attach File'), label=_('Attach File'),
help_text=_('You can attach a file to this ticket. Only file types such as plain text (.txt), a document (.pdf, .docx, or .odt), or screenshot (.png or .jpg) may be uploaded.'), help_text=_('You can attach a file to this ticket. '
'Only file types such as plain text (.txt), '
'a document (.pdf, .docx, or .odt), '
'or screenshot (.png or .jpg) may be uploaded.'),
) )
class Media: class Media:
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js') js = ('helpdesk/js/init_due_date.js',
'helpdesk/js/init_datetime_classes.js')
def __init__(self, kbcategory=None, *args, **kwargs): def __init__(self, kbcategory=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -215,7 +257,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.Select(attrs={'class': 'form-control'}), widget=forms.Select(attrs={'class': 'form-control'}),
required=False, required=False,
label=_('Knowledge Base Item'), label=_('Knowledge Base Item'),
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(category=kbcategory.pk, enabled=True)], choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(
category=kbcategory.pk, enabled=True)],
) )
def _add_form_custom_fields(self, staff_only_filter=None): def _add_form_custom_fields(self, staff_only_filter=None):
@ -307,7 +350,8 @@ class TicketForm(AbstractTicketForm):
submitter_email = forms.EmailField( submitter_email = forms.EmailField(
required=False, required=False,
label=_('Submitter E-Mail Address'), label=_('Submitter E-Mail Address'),
widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}), widget=forms.TextInput(
attrs={'class': 'form-control', 'type': 'email'}),
help_text=_('This e-mail address will receive copies of all public ' help_text=_('This e-mail address will receive copies of all public '
'updates to this ticket.'), 'updates to this ticket.'),
) )
@ -335,10 +379,13 @@ class TicketForm(AbstractTicketForm):
self.fields['queue'].choices = queue_choices self.fields['queue'].choices = queue_choices
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
assignable_users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) assignable_users = User.objects.filter(
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
else: else:
assignable_users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) assignable_users = User.objects.filter(
self.fields['assigned_to'].choices = [('', '--------')] + [(u.id, u.get_username()) for u in assignable_users] is_active=True).order_by(User.USERNAME_FIELD)
self.fields['assigned_to'].choices = [
('', '--------')] + [(u.id, u.get_username()) for u in assignable_users]
self._add_form_custom_fields() self._add_form_custom_fields()
def save(self, user): def save(self, user):
@ -380,7 +427,8 @@ class PublicTicketForm(AbstractTicketForm):
Ticket Form creation for all users (public-facing). Ticket Form creation for all users (public-facing).
""" """
submitter_email = forms.EmailField( submitter_email = forms.EmailField(
widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}), widget=forms.TextInput(
attrs={'class': 'form-control', 'type': 'email'}),
required=True, required=True,
label=_('Your E-Mail Address'), label=_('Your E-Mail Address'),
help_text=_('We will e-mail you when your ticket is updated.'), help_text=_('We will e-mail you when your ticket is updated.'),
@ -406,7 +454,8 @@ class PublicTicketForm(AbstractTicketForm):
} }
for field_name, field_setting_key in field_deletion_table.items(): for field_name, field_setting_key in field_deletion_table.items():
has_settings_default_value = getattr(settings, field_setting_key, None) has_settings_default_value = getattr(
settings, field_setting_key, None)
if has_settings_default_value is not None: if has_settings_default_value is not None:
del self.fields[field_name] del self.fields[field_name]
@ -485,9 +534,11 @@ class TicketCCForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TicketCCForm, self).__init__(*args, **kwargs) super(TicketCCForm, self).__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
else: else:
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(
is_active=True).order_by(User.USERNAME_FIELD)
self.fields['user'].queryset = users self.fields['user'].queryset = users
@ -497,9 +548,11 @@ class TicketCCUserForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TicketCCUserForm, self).__init__(*args, **kwargs) super(TicketCCUserForm, self).__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC: if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(
is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
else: else:
users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD) users = User.objects.filter(
is_active=True).order_by(User.USERNAME_FIELD)
self.fields['user'].queryset = users self.fields['user'].queryset = users
class Meta: class Meta:
@ -538,8 +591,11 @@ class MultipleTicketSelectForm(forms.Form):
if len(tickets) < 2: if len(tickets) < 2:
raise ValidationError(_('Please choose at least 2 tickets.')) raise ValidationError(_('Please choose at least 2 tickets.'))
if len(tickets) > 4: if len(tickets) > 4:
raise ValidationError(_('Impossible to merge more than 4 tickets...')) raise ValidationError(
queues = tickets.order_by('queue').distinct().values_list('queue', flat=True) _('Impossible to merge more than 4 tickets...'))
queues = tickets.order_by('queue').distinct(
).values_list('queue', flat=True)
if len(queues) != 1: if len(queues) != 1:
raise ValidationError(_('All selected tickets must share the same queue in order to be merged.')) raise ValidationError(
_('All selected tickets must share the same queue in order to be merged.'))
return tickets return tickets

View File

@ -6,14 +6,14 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
lib.py - Common functions (eg multipart e-mail) lib.py - Common functions (eg multipart e-mail)
""" """
from datetime import date, datetime, time
from django.conf import settings
from django.utils.encoding import smart_str
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
import logging import logging
import mimetypes import mimetypes
from datetime import datetime, date, time
from django.conf import settings
from django.utils.encoding import smart_text
from helpdesk.settings import CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT
logger = logging.getLogger('helpdesk') logger = logging.getLogger('helpdesk')
@ -117,26 +117,30 @@ def text_is_spam(text, request):
if ak.verify_key(): if ak.verify_key():
ak_data = { ak_data = {
'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'), 'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
'user_agent': request.META.get('HTTP_USER_AGENT', ''), 'user_agent': request.headers.get('User-Agent', ''),
'referrer': request.META.get('HTTP_REFERER', ''), 'referrer': request.headers.get('Referer', ''),
'comment_type': 'comment', 'comment_type': 'comment',
'comment_author': '', 'comment_author': '',
} }
return ak.comment_check(smart_text(text), data=ak_data) return ak.comment_check(smart_str(text), data=ak_data)
return False return False
def process_attachments(followup, attached_files): def process_attachments(followup, attached_files):
max_email_attachment_size = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) max_email_attachment_size = getattr(
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
attachments = [] attachments = []
for attached in attached_files: for attached in attached_files:
if attached.size: if attached.size:
filename = smart_text(attached.name) from helpdesk.models import FollowUpAttachment
att = followup.followupattachment_set.create(
filename = smart_str(attached.name)
att = FollowUpAttachment(
followup=followup,
file=attached, file=attached,
filename=filename, filename=filename,
mime_type=attached.content_type or mime_type=attached.content_type or
@ -149,7 +153,8 @@ def process_attachments(followup, attached_files):
if attached.size < max_email_attachment_size: if attached.size < max_email_attachment_size:
# Only files smaller than 512kb (or as defined in # Only files smaller than 512kb (or as defined in
# settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via email. # settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via
# email.
attachments.append([filename, att.file]) attachments.append([filename, att.file])
return attachments return attachments

View File

@ -8,12 +8,11 @@ scripts/create_escalation_exclusion.py - Easy way to routinely add particular
days to the list of days on which no days to the list of days on which no
escalation should take place. escalation should take place.
""" """
from datetime import date, timedelta
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from helpdesk.models import EscalationExclusion, Queue
from datetime import timedelta, date
import getopt import getopt
from helpdesk.models import EscalationExclusion, Queue
from optparse import make_option from optparse import make_option
import sys import sys
@ -66,7 +65,8 @@ class Command(BaseCommand):
raise CommandError("Queue %s does not exist." % queue) raise CommandError("Queue %s does not exist." % queue)
queues.append(q) queues.append(q)
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues) create_exclusions(days=days, occurrences=occurrences,
verbose=verbose, queues=queues)
day_names = { day_names = {
@ -90,11 +90,13 @@ def create_exclusions(days, occurrences, verbose, queues):
while i < occurrences: while i < occurrences:
if day == workdate.weekday(): if day == workdate.weekday():
if EscalationExclusion.objects.filter(date=workdate).count() == 0: if EscalationExclusion.objects.filter(date=workdate).count() == 0:
esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate) esc = EscalationExclusion(
name='Auto Exclusion for %s' % day_name, date=workdate)
esc.save() esc.save()
if verbose: if verbose:
print("Created exclusion for %s %s" % (day_name, workdate)) print("Created exclusion for %s %s" %
(day_name, workdate))
for q in queues: for q in queues:
esc.queues.add(q) esc.queues.add(q)
@ -116,7 +118,8 @@ def usage():
if __name__ == '__main__': if __name__ == '__main__':
# This script can be run from the command-line or via Django's manage.py. # This script can be run from the command-line or via Django's manage.py.
try: try:
opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues=']) opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', [
'days=', 'occurrences=', 'verbose', 'queues='])
except getopt.GetoptError: except getopt.GetoptError:
usage() usage()
sys.exit(2) sys.exit(2)
@ -151,4 +154,5 @@ if __name__ == '__main__':
sys.exit(2) sys.exit(2)
queues.append(q) queues.append(q)
create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues) create_exclusions(days=days, occurrences=occurrences,
verbose=verbose, queues=queues)

View File

@ -13,15 +13,13 @@ scripts/create_queue_permissions.py -
existing permissions. existing permissions.
""" """
from optparse import make_option
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from helpdesk.models import Queue from helpdesk.models import Queue
from optparse import make_option
class Command(BaseCommand): class Command(BaseCommand):
@ -55,14 +53,17 @@ class Command(BaseCommand):
self.stdout.write("Preparing Queue %s [%s]" % (q.title, q.slug)) self.stdout.write("Preparing Queue %s [%s]" % (q.title, q.slug))
if q.permission_name: if q.permission_name:
self.stdout.write(" .. already has `permission_name=%s`" % q.permission_name) self.stdout.write(
" .. already has `permission_name=%s`" % q.permission_name)
basename = q.permission_name[9:] basename = q.permission_name[9:]
else: else:
basename = q.generate_permission_name() basename = q.generate_permission_name()
self.stdout.write(" .. generated `permission_name=%s`" % q.permission_name) self.stdout.write(
" .. generated `permission_name=%s`" % q.permission_name)
q.save() q.save()
self.stdout.write(" .. checking permission codename `%s`" % basename) self.stdout.write(
" .. checking permission codename `%s`" % basename)
try: try:
Permission.objects.create( Permission.objects.create(

View File

@ -8,12 +8,12 @@ create_usersettings.py - Easy way to create helpdesk-specific settings for
users who don't yet have them. users who don't yet have them.
""" """
from django.utils.translation import ugettext as _
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.utils.translation import gettext as _
from helpdesk.models import UserSettings from helpdesk.models import UserSettings
User = get_user_model() User = get_user_model()

View File

@ -8,18 +8,17 @@ scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
designed to be run from Cron or similar. designed to be run from Cron or similar.
""" """
from datetime import timedelta, date
import getopt
from optparse import make_option
import sys
from datetime import date, timedelta
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext as _
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange import getopt
from helpdesk.lib import safe_template_context from helpdesk.lib import safe_template_context
from helpdesk.models import EscalationExclusion, FollowUp, Queue, Ticket, TicketChange
from optparse import make_option
import sys
class Command(BaseCommand): class Command(BaseCommand):
@ -62,7 +61,8 @@ class Command(BaseCommand):
def escalate_tickets(queues, verbose): def escalate_tickets(queues, verbose):
""" Only include queues with escalation configured """ """ Only include queues with escalation configured """
queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0) queryset = Queue.objects.filter(
escalate_days__isnull=False).exclude(escalate_days=0)
if queues: if queues:
queryset = queryset.filter(slug__in=queues) queryset = queryset.filter(slug__in=queues)
@ -143,7 +143,8 @@ def usage():
if __name__ == '__main__': if __name__ == '__main__':
try: try:
opts, args = getopt.getopt(sys.argv[1:], ['queues=', 'verboseescalation']) opts, args = getopt.getopt(
sys.argv[1:], ['queues=', 'verboseescalation'])
except getopt.GetoptError: except getopt.GetoptError:
usage() usage()
sys.exit(2) sys.exit(2)

View File

@ -11,7 +11,6 @@ scripts/get_email.py - Designed to be run from cron, this script checks the
adding to existing tickets if needed) adding to existing tickets if needed)
""" """
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from helpdesk.email import process_email from helpdesk.email import process_email

View File

@ -2,7 +2,7 @@
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations from django.db import migrations
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
def create_and_assign_permissions(apps, schema_editor): def create_and_assign_permissions(apps, schema_editor):

View File

@ -7,35 +7,29 @@ models.py - Model (and hence database) definitions. This is the core of the
helpdesk structure. helpdesk structure.
""" """
from django.contrib.auth.models import Permission
from .lib import convert_value
from .templated_email import send_templated_mail
from .validators import validate_file_extension
import datetime
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models from django.db import models
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext
from io import StringIO
import re
import os
import mimetypes
import datetime
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext, gettext_lazy as _
from helpdesk import settings as helpdesk_settings
from io import StringIO
from markdown import markdown from markdown import markdown
from markdown.extensions import Extension from markdown.extensions import Extension
import mimetypes
import os
import uuid import re
from rest_framework import serializers from rest_framework import serializers
import uuid
from helpdesk import settings as helpdesk_settings
from .lib import convert_value
from .validators import validate_file_extension
from .templated_email import send_templated_mail
def format_time_spent(time_spent): def format_time_spent(time_spent):
@ -50,16 +44,17 @@ def format_time_spent(time_spent):
class EscapeHtml(Extension): class EscapeHtml(Extension):
def extendMarkdown(self, md, md_globals): def extendMarkdown(self, md, md_globals=None):
del md.preprocessors['html_block'] # del md.preprocessors['html_block']
del md.inlinePatterns['html'] # del md.inlinePatterns['html']
pass
def get_markdown(text): def get_markdown(text):
if not text: if not text:
return "" return ""
pattern = fr'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)' pattern = r'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)'
# Regex check # Regex check
if re.match(pattern, text): if re.match(pattern, text):
# get get value of group regex # get get value of group regex
@ -128,7 +123,8 @@ class Queue(models.Model):
_('Allow Public Submission?'), _('Allow Public Submission?'),
blank=True, blank=True,
default=False, default=False,
help_text=_('Should this queue be listed on the public submission form?'), help_text=_(
'Should this queue be listed on the public submission form?'),
) )
allow_email_submission = models.BooleanField( allow_email_submission = models.BooleanField(
@ -180,7 +176,8 @@ class Queue(models.Model):
email_box_type = models.CharField( email_box_type = models.CharField(
_('E-Mail Box Type'), _('E-Mail Box Type'),
max_length=5, max_length=5,
choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), ('local', _('Local Directory'))), choices=(('pop3', _('POP 3')), ('imap', _('IMAP')),
('local', _('Local Directory'))),
blank=True, blank=True,
null=True, null=True,
help_text=_('E-Mail server type for creating tickets automatically ' help_text=_('E-Mail server type for creating tickets automatically '
@ -262,7 +259,8 @@ class Queue(models.Model):
email_box_interval = models.IntegerField( email_box_interval = models.IntegerField(
_('E-Mail Check Interval'), _('E-Mail Check Interval'),
help_text=_('How often do you wish to check this mailbox? (in Minutes)'), help_text=_(
'How often do you wish to check this mailbox? (in Minutes)'),
blank=True, blank=True,
null=True, null=True,
default='5', default='5',
@ -281,7 +279,8 @@ class Queue(models.Model):
choices=(('socks4', _('SOCKS4')), ('socks5', _('SOCKS5'))), choices=(('socks4', _('SOCKS4')), ('socks5', _('SOCKS5'))),
blank=True, blank=True,
null=True, null=True,
help_text=_('SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'), help_text=_(
'SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'),
) )
socks_proxy_host = models.GenericIPAddressField( socks_proxy_host = models.GenericIPAddressField(
@ -295,7 +294,8 @@ class Queue(models.Model):
_('Socks Proxy Port'), _('Socks Proxy Port'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Socks proxy port number. Default: 9150 (default TOR port)'), help_text=_(
'Socks proxy port number. Default: 9150 (default TOR port)'),
) )
logging_type = models.CharField( logging_type = models.CharField(
@ -356,7 +356,8 @@ class Queue(models.Model):
""" """
if not self.email_address: if not self.email_address:
# must check if given in format "Foo <foo@example.com>" # must check if given in format "Foo <foo@example.com>"
default_email = re.match(".*<(?P<email>.*@*.)>", settings.DEFAULT_FROM_EMAIL) default_email = re.match(
".*<(?P<email>.*@*.)>", settings.DEFAULT_FROM_EMAIL)
if default_email is not None: if default_email is not None:
# already in the right format, so just include it here # already in the right format, so just include it here
return u'NO QUEUE EMAIL ADDRESS DEFINED %s' % settings.DEFAULT_FROM_EMAIL return u'NO QUEUE EMAIL ADDRESS DEFINED %s' % settings.DEFAULT_FROM_EMAIL
@ -532,7 +533,8 @@ class Ticket(models.Model):
_('On Hold'), _('On Hold'),
blank=True, blank=True,
default=False, default=False,
help_text=_('If a ticket is on hold, it will not automatically be escalated.'), help_text=_(
'If a ticket is on hold, it will not automatically be escalated.'),
) )
description = models.TextField( description = models.TextField(
@ -582,7 +584,8 @@ class Ticket(models.Model):
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('Knowledge base item the user was viewing when they created this ticket.'), verbose_name=_(
'Knowledge base item the user was viewing when they created this ticket.'),
) )
merged_to = models.ForeignKey( merged_to = models.ForeignKey(
@ -648,7 +651,8 @@ class Ticket(models.Model):
def send(role, recipient): def send(role, recipient):
if recipient and recipient not in recipients and role in roles: if recipient and recipient not in recipients and role in roles:
template, context = roles[role] template, context = roles[role]
send_templated_mail(template, context, recipient, sender=self.queue.from_address, **kwargs) send_templated_mail(
template, context, recipient, sender=self.queue.from_address, **kwargs)
recipients.add(recipient) recipients.add(recipient)
send('submitter', self.submitter_email) send('submitter', self.submitter_email)
@ -844,7 +848,8 @@ class Ticket(models.Model):
# Ignore if user has no email address # Ignore if user has no email address
return return
elif not email: elif not email:
raise ValueError('You must provide at least one parameter to get the email from') raise ValueError(
'You must provide at least one parameter to get the email from')
# Prepare all emails already into the ticket # Prepare all emails already into the ticket
ticket_emails = [x.display for x in self.ticketcc_set.all()] ticket_emails = [x.display for x in self.ticketcc_set.all()]
@ -1031,11 +1036,11 @@ class TicketChange(models.Model):
def __str__(self): def __str__(self):
out = '%s ' % self.field out = '%s ' % self.field
if not self.new_value: if not self.new_value:
out += ugettext('removed') out += gettext('removed')
elif not self.old_value: elif not self.old_value:
out += ugettext('set to %s') % self.new_value out += gettext('set to %s') % self.new_value
else: else:
out += ugettext('changed from "%(old_value)s" to "%(new_value)s"') % { out += gettext('changed from "%(old_value)s" to "%(new_value)s"') % {
'old_value': self.old_value, 'old_value': self.old_value,
'new_value': self.new_value 'new_value': self.new_value
} }
@ -1280,7 +1285,8 @@ class EmailTemplate(models.Model):
html = models.TextField( html = models.TextField(
_('HTML'), _('HTML'),
help_text=_('The same context is available here as in plain_text, above.'), help_text=_(
'The same context is available here as in plain_text, above.'),
) )
locale = models.CharField( locale = models.CharField(
@ -1329,7 +1335,8 @@ class KBCategory(models.Model):
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('Default queue when creating a ticket after viewing this category.'), verbose_name=_(
'Default queue when creating a ticket after viewing this category.'),
) )
public = models.BooleanField( public = models.BooleanField(
@ -1396,7 +1403,8 @@ class KBItem(models.Model):
last_updated = models.DateTimeField( last_updated = models.DateTimeField(
_('Last Updated'), _('Last Updated'),
help_text=_('The date on which this question was most recently changed.'), help_text=_(
'The date on which this question was most recently changed.'),
blank=True, blank=True,
) )
@ -1555,7 +1563,8 @@ class UserSettings(models.Model):
login_view_ticketlist = models.BooleanField( login_view_ticketlist = models.BooleanField(
verbose_name=_('Show Ticket List on Login?'), verbose_name=_('Show Ticket List on Login?'),
help_text=_('Display the ticket list upon login? Otherwise, the dashboard is shown.'), help_text=_(
'Display the ticket list upon login? Otherwise, the dashboard is shown.'),
default=login_view_ticketlist_default, default=login_view_ticketlist_default,
) )
@ -1570,13 +1579,15 @@ class UserSettings(models.Model):
email_on_ticket_assign = models.BooleanField( email_on_ticket_assign = models.BooleanField(
verbose_name=_('E-mail me when assigned a ticket?'), verbose_name=_('E-mail me when assigned a ticket?'),
help_text=_('If you are assigned a ticket via the web, do you want to receive an e-mail?'), help_text=_(
'If you are assigned a ticket via the web, do you want to receive an e-mail?'),
default=email_on_ticket_assign_default, default=email_on_ticket_assign_default,
) )
tickets_per_page = models.IntegerField( tickets_per_page = models.IntegerField(
verbose_name=_('Number of tickets to show per page'), verbose_name=_('Number of tickets to show per page'),
help_text=_('How many tickets do you want to see on the Ticket List page?'), help_text=_(
'How many tickets do you want to see on the Ticket List page?'),
default=tickets_per_page_default, default=tickets_per_page_default,
choices=PAGE_SIZES, choices=PAGE_SIZES,
) )
@ -1611,7 +1622,8 @@ def create_usersettings(sender, instance, created, **kwargs):
UserSettings.objects.create(user=instance) UserSettings.objects.create(user=instance)
models.signals.post_save.connect(create_usersettings, sender=settings.AUTH_USER_MODEL) models.signals.post_save.connect(
create_usersettings, sender=settings.AUTH_USER_MODEL)
class IgnoreEmail(models.Model): class IgnoreEmail(models.Model):
@ -1851,14 +1863,16 @@ class CustomField(models.Model):
ordering = models.IntegerField( ordering = models.IntegerField(
_('Ordering'), _('Ordering'),
help_text=_('Lower numbers are displayed first; higher numbers are listed later'), help_text=_(
'Lower numbers are displayed first; higher numbers are listed later'),
blank=True, blank=True,
null=True, null=True,
) )
def _choices_as_array(self): def _choices_as_array(self):
valuebuffer = StringIO(self.list_values) valuebuffer = StringIO(self.list_values)
choices = [[item.strip(), item.strip()] for item in valuebuffer.readlines()] choices = [[item.strip(), item.strip()]
for item in valuebuffer.readlines()]
valuebuffer.close() valuebuffer.close()
return choices return choices
choices_as_array = property(_choices_as_array) choices_as_array = property(_choices_as_array)
@ -1912,10 +1926,10 @@ class CustomField(models.Model):
# Prepare attributes for each types # Prepare attributes for each types
attributes = { attributes = {
'label': self.label, 'label': self.label,
'help_text': self.help_text, 'help_text': self.help_text,
'required': self.required, 'required': self.required,
} }
if self.data_type in ('varchar', 'text'): if self.data_type in ('varchar', 'text'):
attributes['max_length'] = self.max_length attributes['max_length'] = self.max_length
if self.data_type == 'text': if self.data_type == 'text':
@ -1950,6 +1964,10 @@ class TicketCustomFieldValue(models.Model):
def __str__(self): def __str__(self):
return '%s / %s' % (self.ticket, self.field) return '%s / %s' % (self.ticket, self.field)
@property
def default_value(self) -> str:
return _("Not defined")
class Meta: class Meta:
unique_together = (('ticket', 'field'),) unique_together = (('ticket', 'field'),)
verbose_name = _('Ticket custom field value') verbose_name = _('Ticket custom field value')

View File

@ -1,15 +1,12 @@
from base64 import b64decode, b64encode
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from base64 import b64encode
from base64 import b64decode
import json
from model_utils import Choices
from helpdesk.serializers import DatatablesTicketSerializer from helpdesk.serializers import DatatablesTicketSerializer
import json
from model_utils import Choices
def query_to_base64(query): def query_to_base64(query):
@ -103,8 +100,10 @@ def get_query_class():
class __Query__: class __Query__:
def __init__(self, huser, base64query=None, query_params=None): def __init__(self, huser, base64query=None, query_params=None):
self.huser = huser self.huser = huser
self.params = query_params if query_params else query_from_base64(base64query) self.params = query_params if query_params else query_from_base64(
self.base64 = base64query if base64query else query_to_base64(query_params) base64query)
self.base64 = base64query if base64query else query_to_base64(
query_params)
self.result = None self.result = None
def get_search_filter_args(self): def get_search_filter_args(self):
@ -128,7 +127,8 @@ class __Query__:
""" """
filter = self.params.get('filtering', {}) filter = self.params.get('filtering', {})
filter_or = self.params.get('filtering_or', {}) filter_or = self.params.get('filtering_or', {})
queryset = queryset.filter((Q(**filter) | Q(**filter_or)) & self.get_search_filter_args()) queryset = queryset.filter(
(Q(**filter) | Q(**filter_or)) & self.get_search_filter_args())
sorting = self.params.get('sorting', None) sorting = self.params.get('sorting', None)
if sorting: if sorting:
sortreverse = self.params.get('sortreverse', None) sortreverse = self.params.get('sortreverse', None)
@ -191,11 +191,13 @@ class __Query__:
'text': { 'text': {
'headline': ticket.title + ' - ' + followup.title, 'headline': ticket.title + ' - ' + followup.title,
'text': ( 'text': (
(escape(followup.comment) if followup.comment else _('No text')) (escape(followup.comment)
if followup.comment else _('No text'))
+ +
'<br/> <a href="%s" class="btn" role="button">%s</a>' '<br/> <a href="%s" class="btn" role="button">%s</a>'
% %
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")) (reverse('helpdesk:view', kwargs={
'ticket_id': ticket.pk}), _("View ticket"))
), ),
}, },
'group': _('Messages'), 'group': _('Messages'),

View File

@ -1,13 +1,12 @@
from rest_framework import serializers from .forms import TicketForm
from .lib import format_time_spent, process_attachments
from .models import CustomField, FollowUp, FollowUpAttachment, Ticket
from .user import HelpdeskUser
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.humanize.templatetags import humanize from django.contrib.humanize.templatetags import humanize
from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from .forms import TicketForm
from .models import Ticket, CustomField
from .lib import format_time_spent
from .user import HelpdeskUser
class DatatablesTicketSerializer(serializers.ModelSerializer): class DatatablesTicketSerializer(serializers.ModelSerializer):
""" """
@ -71,12 +70,45 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
return obj.kbitem.title if obj.kbitem else "" 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): class TicketSerializer(serializers.ModelSerializer):
followup_set = FollowUpSerializer(many=True, read_only=True)
attachment = serializers.FileField(write_only=True, required=False)
class Meta: class Meta:
model = Ticket model = Ticket
fields = ( fields = (
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold', '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): def __init__(self, *args, **kwargs):
@ -99,7 +131,10 @@ class TicketSerializer(serializers.ModelSerializer):
if data.get('merged_to'): if data.get('merged_to'):
data['merged_to'] = data['merged_to'].id 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(): if ticket_form.is_valid():
ticket = ticket_form.save(user=self.context['request'].user) ticket = ticket_form.save(user=self.context['request'].user)
ticket.set_custom_field_values() ticket.set_custom_field_values()

View File

@ -2,12 +2,13 @@
Default settings for django-helpdesk. Default settings for django-helpdesk.
""" """
import os
import warnings
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
import os
import warnings
DEFAULT_USER_SETTINGS = { DEFAULT_USER_SETTINGS = {
'login_view_ticketlist': True, 'login_view_ticketlist': True,
@ -25,6 +26,9 @@ except AttributeError:
HAS_TAG_SUPPORT = False HAS_TAG_SUPPORT = False
# Use international timezones
USE_TZ: bool = True
# check for secure cookie support # check for secure cookie support
if os.environ.get('SECURE_PROXY_SSL_HEADER'): if os.environ.get('SECURE_PROXY_SSL_HEADER'):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
@ -43,13 +47,13 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
# Enable the Dependencies field on ticket view # Enable the Dependencies field on ticket view
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings, HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings,
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET', 'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
True) True)
# Enable the Time spent on field on ticket view # Enable the Time spent on field on ticket view
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings, HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings,
'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET', 'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET',
True) True)
# raises a 404 to anon users. It's like it was invisible # raises a 404 to anon users. It's like it was invisible
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings, HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
@ -60,10 +64,13 @@ HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True) HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
# Disable Timeline on ticket list # Disable Timeline on ticket list
HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True) HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(
settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True)
# show extended navigation by default, to all users, irrespective of staff status? # show extended navigation by default, to all users, irrespective of staff
HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False) # status?
HELPDESK_NAVIGATION_ENABLED = getattr(
settings, 'HELPDESK_NAVIGATION_ENABLED', False)
# use public CDNs to serve jquery and other javascript by default? # use public CDNs to serve jquery and other javascript by default?
# otherwise, use built-in static copy # otherwise, use built-in static copy
@ -81,7 +88,8 @@ HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings,
["en", "de", "es", "fr", "it", "ru"]) ["en", "de", "es", "fr", "it", "ru"])
# show link to 'change password' on 'User Settings' page? # show link to 'change password' on 'User Settings' page?
HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False) HELPDESK_SHOW_CHANGE_PASSWORD = getattr(
settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False)
# allow user to override default layout for 'followups' - work in progress. # allow user to override default layout for 'followups' - work in progress.
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False) HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
@ -93,17 +101,19 @@ HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
# URL schemes that are allowed within links # URL schemes that are allowed within links
ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', ( ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', (
'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
)) ))
############################ ############################
# options for public pages # # options for public pages #
############################ ############################
# show 'view a ticket' section on public page? # show 'view a ticket' section on public page?
HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True) HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(
settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True)
# show 'submit a ticket' section on public page? # show 'submit a ticket' section on public page?
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True) HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(
settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
# change that to custom class to have extra fields or validation (like captcha) # change that to custom class to have extra fields or validation (like captcha)
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr( HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
@ -134,8 +144,10 @@ CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M"
''' options for update_ticket views ''' ''' options for update_ticket views '''
# allow non-staff users to interact with tickets? # allow non-staff users to interact with tickets?
# can be True/False or a callable accepting the active user and returning True if they must be considered helpdesk staff # can be True/False or a callable accepting the active user and returning
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False) # True if they must be considered helpdesk staff
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(
settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)): if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)):
warnings.warn( warnings.warn(
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.", "HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
@ -151,14 +163,18 @@ HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr( HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False) settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False)
# make all updates public by default? this will hide the 'is this update public' checkbox # make all updates public by default? this will hide the 'is this update
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False) # public' checkbox
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(
settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
# only show staff users in ticket owner drop-downs # only show staff users in ticket owner drop-downs
HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False) HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(
settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False)
# only show staff users in ticket cc drop-down # only show staff users in ticket cc drop-down
HELPDESK_STAFF_ONLY_TICKET_CC = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False) HELPDESK_STAFF_ONLY_TICKET_CC = getattr(
settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False)
# allow the subject to have a configurable template. # allow the subject to have a configurable template.
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr( HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
@ -170,11 +186,13 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
raise ImproperlyConfigured raise ImproperlyConfigured
# default fallback locale when queue locale not found # default fallback locale when queue locale not found
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en') HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(
settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en')
# default maximum email attachment size, in bytes # default maximum email attachment size, in bytes
# only attachments smaller than this size will be sent via email # only attachments smaller than this size will be sent via email
HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000) HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(
settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
######################################## ########################################
@ -186,7 +204,8 @@ HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False) settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
# Activate the API endpoint to manage tickets thanks to Django REST Framework # Activate the API endpoint to manage tickets thanks to Django REST Framework
HELPDESK_ACTIVATE_API_ENDPOINT = getattr(settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False) HELPDESK_ACTIVATE_API_ENDPOINT = getattr(
settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False)
################# #################
@ -201,25 +220,32 @@ QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None)
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None) QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None)
# only process emails with a valid tracking ID? (throws away all other mail) # only process emails with a valid tracking ID? (throws away all other mail)
QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False) QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(
settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False)
# only allow users to access queues that they are members of? # only allow users to access queues that they are members of?
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr( HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False) settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)
# use https in the email links # use https in the email links
HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False) HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(
settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False)
HELPDESK_TEAMS_MODEL = getattr(settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team') HELPDESK_TEAMS_MODEL = getattr(
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [('pinax_teams', '0004_auto_20170511_0856')]) settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team')
HELPDESK_KBITEM_TEAM_GETTER = getattr(settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team) HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [
('pinax_teams', '0004_auto_20170511_0856')])
HELPDESK_KBITEM_TEAM_GETTER = getattr(
settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team)
# Include all signatures and forwards in the first ticket message if set # Include all signatures and forwards in the first ticket message if set
# Useful if you get forwards dropped from them while they are useful part of request # Useful if you get forwards dropped from them while they are useful part
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False) # of request
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(
settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
# If set then we always save incoming emails as .eml attachments # If set then we always save incoming emails as .eml attachments
# which is quite noisy but very helpful for complicated markup, forwards and so on # which is quite noisy but very helpful for complicated markup, forwards and so on
# (which gets stripped/corrupted otherwise) # (which gets stripped/corrupted otherwise)
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False) HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(
settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False)

View File

@ -1,6 +1,5 @@
from celery import shared_task
from .email import process_email from .email import process_email
from celery import shared_task
@shared_task @shared_task

View File

@ -1,9 +1,10 @@
import os
import logging
from smtplib import SMTPException
from django.conf import settings from django.conf import settings
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
import logging
import os
from smtplib import SMTPException
logger = logging.getLogger('helpdesk') logger = logging.getLogger('helpdesk')
@ -50,20 +51,22 @@ def send_templated_mail(template_name,
from_string = engines['django'].from_string from_string = engines['django'].from_string
from helpdesk.models import EmailTemplate from helpdesk.models import EmailTemplate
from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \ from helpdesk.settings import HELPDESK_EMAIL_FALLBACK_LOCALE, HELPDESK_EMAIL_SUBJECT_TEMPLATE
HELPDESK_EMAIL_FALLBACK_LOCALE
headers = extra_headers or {} headers = extra_headers or {}
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
try: try:
t = EmailTemplate.objects.get(template_name__iexact=template_name, locale=locale) t = EmailTemplate.objects.get(
template_name__iexact=template_name, locale=locale)
except EmailTemplate.DoesNotExist: except EmailTemplate.DoesNotExist:
try: try:
t = EmailTemplate.objects.get(template_name__iexact=template_name, locale__isnull=True) t = EmailTemplate.objects.get(
template_name__iexact=template_name, locale__isnull=True)
except EmailTemplate.DoesNotExist: except EmailTemplate.DoesNotExist:
logger.warning('template "%s" does not exist, no mail sent', template_name) logger.warning(
'template "%s" does not exist, no mail sent', template_name)
return # just ignore if template doesn't exist return # just ignore if template doesn't exist
subject_part = from_string( subject_part = from_string(
@ -77,10 +80,12 @@ def send_templated_mail(template_name,
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file) "%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
).render(context) ).render(context)
email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html') email_html_base_file = os.path.join(
'helpdesk', locale, 'email_html_base.html')
# keep new lines in html emails # keep new lines in html emails
if 'comment' in context: if 'comment' in context:
context['comment'] = mark_safe(context['comment'].replace('\r\n', '<br>')) context['comment'] = mark_safe(
context['comment'].replace('\r\n', '<br>'))
html_part = from_string( html_part = from_string(
"{%% extends '%s' %%}" "{%% extends '%s' %%}"
@ -112,7 +117,8 @@ def send_templated_mail(template_name,
try: try:
return msg.send() return msg.send()
except SMTPException as e: except SMTPException as e:
logger.exception('SMTPException raised while sending email to {}'.format(recipients)) logger.exception(
'SMTPException raised while sending email to {}'.format(recipients))
if not fail_silently: if not fail_silently:
raise e raise e
return 0 return 0

View File

@ -3,9 +3,9 @@
<h2>Queries</h2> <h2>Queries</h2>
<p> <p>
{{ sql_queries|length }} Quer{{ sql_queries|pluralize:"y,ies" }} {{ sql_queries|length }} Quer{{ sql_queries|pluralize:"y,ies" }}
{% ifnotequal sql_queries|length 0 %} {% if sql_queries|length != 0 %}
(<span style="cursor: pointer;" onclick="var s=document.getElementById('debugQueryTable').style;s.display=s.display=='none'?'':'none';this.innerHTML=this.innerHTML=='Show'?'Hide':'Show';">Show</span>) (<span style="cursor: pointer;" onclick="var s=document.getElementById('debugQueryTable').style;s.display=s.display=='none'?'':'none';this.innerHTML=this.innerHTML=='Show'?'Hide':'Show';">Show</span>)
{% endifnotequal %} {% endif %}
</p> </p>
<table id="debugQueryTable" style="display: none;"> <table id="debugQueryTable" style="display: none;">
<col width="1"></col> <col width="1"></col>

View File

@ -14,7 +14,7 @@
{% endwith %} {% endwith %}
{% for u in user_choices %} {% for u in user_choices %}
<option value='{{ u.id }}'{% if u.id|in_list:query_params.filtering.assigned_to__id__in %} selected='selected'{% endif %}> <option value='{{ u.id }}'{% if u.id|in_list:query_params.filtering.assigned_to__id__in %} selected='selected'{% endif %}>
{{ u.get_username }}{% ifequal u user %} {% trans "(ME)" %}{% endifequal %} {{ u.get_username }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -6,22 +6,22 @@
</div> </div>
<div class="col col-sm-3"> <div class="col col-sm-3">
<select id='id_sortx' name='sortx' class=""> <select id='id_sortx' name='sortx' class="">
<option value='created'{% ifequal query_params.sorting "created"%} selected='selected'{% endifequal %}> <option value='created'{% if query_params.sorting == "created"%} selected='selected'{% endif %}>
{% trans "Created" %} {% trans "Created" %}
</option> </option>
<option value='title'{% ifequal query_params.sorting "title"%} selected='selected'{% endifequal %}> <option value='title'{% if query_params.sorting == "title"%} selected='selected'{% endif %}>
{% trans "Title" %} {% trans "Title" %}
</option> </option>
<option value='queue'{% ifequal query_params.sorting "queue"%} selected='selected'{% endifequal %}> <option value='queue'{% if query_params.sorting == "queue"%} selected='selected'{% endif %}>
{% trans "Queue" %} {% trans "Queue" %}
</option> </option>
<option value='status'{% ifequal query_params.sorting "status"%} selected='selected'{% endifequal %}> <option value='status'{% if query_params.sorting == "status"%} selected='selected'{% endif %}>
{% trans "Status" %} {% trans "Status" %}
</option> </option>
<option value='priority'{% ifequal query_params.sorting "priority"%} selected='selected'{% endifequal %}> <option value='priority'{% if query_params.sorting == "priority"%} selected='selected'{% endif %}>
{% trans "Priority" %} {% trans "Priority" %}
</option> </option>
<option value='assigned_to'{% ifequal query_params.sorting "assigned_to"%} selected='selected'{% endifequal %}> <option value='assigned_to'{% if query_params.sorting == "assigned_to"%} selected='selected'{% endif %}>
{% trans "Owner" %} {% trans "Owner" %}
</option> </option>
</select> </select>

View File

@ -34,7 +34,7 @@
{% for q in user_saved_queries_ %} {% for q in user_saved_queries_ %}
<a class="dropdown-item" href="{% url 'helpdesk:list' %}?saved_query={{ q.id }}">{{ q.title }} <a class="dropdown-item" href="{% url 'helpdesk:list' %}?saved_query={{ q.id }}">{{ q.title }}
{% if q.shared %} {% if q.shared %}
(Shared{% ifnotequal user q.user %} by {{ q.user.get_username }}{% endifnotequal %}) (Shared{% if user != q.user %} by {{ q.user.get_username }}{% endif %})
{% endif %} {% endif %}
</a> </a>
{% endfor %} {% endfor %}

View File

@ -25,7 +25,7 @@
{% for q in user_saved_queries_ %} {% for q in user_saved_queries_ %}
<a class="dropdown-item small" href="{% url 'helpdesk:list' %}?saved_query={{ q.id }}">{{ q.title }} <a class="dropdown-item small" href="{% url 'helpdesk:list' %}?saved_query={{ q.id }}">{{ q.title }}
{% if q.shared %} {% if q.shared %}
(Shared{% ifnotequal user q.user %} by {{ q.user.get_username }}{% endifnotequal %}) (Shared{% if user != q.user %} by {{ q.user.get_username }}{% endif %})
{% endif %} {% endif %}
</a> </a>
{% endfor %} {% endfor %}

View File

@ -99,34 +99,34 @@
<dd class='form_help_text'>{% trans "You can insert ticket and queue details in your message. For more information, see the <a href='../../help/context/'>context help page</a>." %}</dd> <dd class='form_help_text'>{% trans "You can insert ticket and queue details in your message. For more information, see the <a href='../../help/context/'>context help page</a>." %}</dd>
{% if not ticket.can_be_resolved %}<dd>{% trans "This ticket cannot be resolved or closed until the tickets it depends on are resolved." %}</dd>{% endif %} {% if not ticket.can_be_resolved %}<dd>{% trans "This ticket cannot be resolved or closed until the tickets it depends on are resolved." %}</dd>{% endif %}
{% ifequal ticket.status 1 %} {% if ticket.status == 1 %}
<input type="hidden" name="new_status" value="{{ ticket.status }}" /> <input type="hidden" name="new_status" value="{{ ticket.status }}" />
{% endifequal %} {% endif %}
{% ifequal ticket.status 2 %} {% if ticket.status == 2 %}
<dd><div class="form-group"> <dd><div class="form-group">
<label for='st_reopened' class='active radio-inline'><input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'>{% trans "Reopened" %} &raquo;</label> <label for='st_reopened' class='active radio-inline'><input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'>{% trans "Reopened" %} &raquo;</label>
<label class="radio-inline" for='st_resolved'><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label> <label class="radio-inline" for='st_resolved'><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label> <label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label> <label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label>
</div></dd> </div></dd>
{% endifequal %} {% endif %}
{% ifequal ticket.status 3 %} {% if ticket.status == 3 %}
<dd><div class="form-group"> <dd><div class="form-group">
<label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label> <label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} &raquo;</label> <label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} &raquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'>{% trans "Closed" %}</label> <label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'>{% trans "Closed" %}</label>
</div></dd> </div></dd>
{% endifequal %} {% endif %}
{% ifequal ticket.status 4 %} {% if ticket.status == 4 %}
<dd><div class="form-group"><label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label> <dd><div class="form-group"><label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed' checked='checked'>{% trans "Closed" %}</label></div></dd> <label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed' checked='checked'>{% trans "Closed" %}</label></div></dd>
{% endifequal %} {% endif %}
{% ifequal ticket.status 5 %} {% if ticket.status == 5 %}
<dd><div class="form-group"> <dd><div class="form-group">
<label class="radio-inline" for='st_reopened'><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label> <label class="radio-inline" for='st_reopened'><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate' checked='checked'>{% trans "Duplicate" %}</label> <label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate' checked='checked'>{% trans "Duplicate" %}</label>
</div></dd> </div></dd>
{% endifequal %} {% endif %}
<input type='hidden' name='public' value='1'> <input type='hidden' name='public' value='1'>

View File

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

View File

@ -112,39 +112,39 @@
<dt><label>{% trans "New Status" %}</label></dt> <dt><label>{% trans "New Status" %}</label></dt>
{% if not ticket.can_be_resolved %}<dd>{% trans "This ticket cannot be resolved or closed until the tickets it depends on are resolved." %}</dd>{% endif %} {% if not ticket.can_be_resolved %}<dd>{% trans "This ticket cannot be resolved or closed until the tickets it depends on are resolved." %}</dd>{% endif %}
{% ifequal ticket.status 1 %} {% if ticket.status == 1 %}
<dd><div class="form-group"> <dd><div class="form-group">
<label for='st_open' class='active radio-inline'><input type='radio' name='new_status' value='1' id='st_open' checked='checked'>{% trans "Open" %} &raquo;</label> <label for='st_open' class='active radio-inline'><input type='radio' name='new_status' value='1' id='st_open' checked='checked'>{% trans "Open" %} &raquo;</label>
<label for='st_resolved' class="radio-inline"><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label> <label for='st_resolved' class="radio-inline"><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label>
<label for='st_closed' class="radio-inline"><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label> <label for='st_closed' class="radio-inline"><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label> <label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label>
</div></dd> </div></dd>
{% endifequal %} {% endif %}
{% ifequal ticket.status 2 %} {% if ticket.status == 2 %}
<dd><div class="form-group"> <dd><div class="form-group">
<label for='st_reopened' class='active radio-inline'><input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'>{% trans "Reopened" %} &raquo;</label> <label for='st_reopened' class='active radio-inline'><input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'>{% trans "Reopened" %} &raquo;</label>
<label class="radio-inline" for='st_resolved'><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label> <label class="radio-inline" for='st_resolved'><input type='radio' name='new_status' value='3' id='st_resolved'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Resolved" %} &raquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label> <label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'{% if not ticket.can_be_resolved %} disabled='disabled'{% endif %}>{% trans "Closed" %} &raquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label> <label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate'>{% trans "Duplicate" %}</label>
</div></dd> </div></dd>
{% endifequal %} {% endif %}
{% ifequal ticket.status 3 %} {% if ticket.status == 3 %}
<dd><div class="form-group"> <dd><div class="form-group">
<label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label> <label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} &raquo;</label> <label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} &raquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'>{% trans "Closed" %}</label> <label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed'>{% trans "Closed" %}</label>
</div></dd> </div></dd>
{% endifequal %} {% endif %}
{% ifequal ticket.status 4 %} {% if ticket.status == 4 %}
<dd><div class="form-group"><label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label> <dd><div class="form-group"><label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed' checked='checked'>{% trans "Closed" %}</label></div></dd> <label class="radio-inline" for='st_closed'><input type='radio' name='new_status' value='4' id='st_closed' checked='checked'>{% trans "Closed" %}</label></div></dd>
{% endifequal %} {% endif %}
{% ifequal ticket.status 5 %} {% if ticket.status == 5 %}
<dd><div class="form-group"> <dd><div class="form-group">
<label class="radio-inline" for='st_reopened'><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label> <label class="radio-inline" for='st_reopened'><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} &laquo;</label>
<label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate' checked='checked'>{% trans "Duplicate" %}</label> <label class="radio-inline" for='st_duplicate'><input type='radio' name='new_status' value='5' id='st_duplicate' checked='checked'>{% trans "Duplicate" %}</label>
</div></dd> </div></dd>
{% endifequal %} {% endif %}
{% if helpdesk_settings.HELPDESK_UPDATE_PUBLIC_DEFAULT %} {% if helpdesk_settings.HELPDESK_UPDATE_PUBLIC_DEFAULT %}
<input type='hidden' name='public' value='1'> <input type='hidden' name='public' value='1'>
@ -176,10 +176,10 @@
<dd><input type='text' name='title' value='{{ ticket.title|escape }}' /></dd> <dd><input type='text' name='title' value='{{ ticket.title|escape }}' /></dd>
<dt><label for='id_owner'>{% trans "Owner" %}</label></dt> <dt><label for='id_owner'>{% trans "Owner" %}</label></dt>
<dd><select id='id_owner' name='owner'><option value='0'>{% trans "Unassign" %}</option>{% for u in active_users %}<option value='{{ u.id }}' {% ifequal u.id ticket.assigned_to.id %}selected{% endifequal %}>{{ u }}</option>{% endfor %}</select></dd> <dd><select id='id_owner' name='owner'><option value='0'>{% trans "Unassign" %}</option>{% for u in active_users %}{% if u.id == ticket.assigned_to.id %}<option value='{{ u.id }}' selected>{{ u }}</option>{% else %}<option value='{{ u.id }}'>{{ u }}</option>{% endif %}{% endfor %}</select></dd>
<dt><label for='id_priority'>{% trans "Priority" %}</label></dt> <dt><label for='id_priority'>{% trans "Priority" %}</label></dt>
<dd><select id='id_priority' name='priority'>{% for p in priorities %}<option value='{{ p.0 }}'{% ifequal p.0 ticket.priority %} selected='selected'{% endifequal %}>{{ p.1 }}</option>{% endfor %}</select></dd> <dd><select id='id_priority' name='priority'>{% for p in priorities %}{% if p.0 == ticket.priority %}<option value='{{ p.0 }}' selected='selected'>{{ p.1 }}</option>{% else %}<option value='{{ p.0 }}'>{{ p.1 }}</option>{% endif %}{% endfor %}</select></dd>
<dt><label for='id_due_date'>{% trans "Due on" %}</label></dt> <dt><label for='id_due_date'>{% trans "Due on" %}</label></dt>
<dd>{{ form.due_date }}</dd> <dd>{{ form.due_date }}</dd>

View File

@ -4,10 +4,10 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff. The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff.
templatetags/helpdesk_staff.py templatetags/helpdesk_staff.py
""" """
import logging
from django.template import Library
from django.template import Library
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -19,4 +19,5 @@ def helpdesk_staff(user):
try: try:
return is_helpdesk_staff(user) return is_helpdesk_staff(user)
except Exception: except Exception:
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed") logger.exception(
"'helpdesk_staff' template tag (django-helpdesk) crashed")

View File

@ -1,10 +1,9 @@
from datetime import datetime
from django.conf import settings
from django.template import Library from django.template import Library
from django.template.defaultfilters import date as date_filter from django.template.defaultfilters import date as date_filter
from django.conf import settings from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
from datetime import datetime
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT, CUSTOMFIELD_DATETIME_FORMAT
register = Library() register = Library()
@ -22,13 +21,16 @@ def datetime_string_format(value):
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats :return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
""" """
try: try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT) new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
except (TypeError, ValueError): except (TypeError, ValueError):
try: try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT) new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
except (TypeError, ValueError): except (TypeError, ValueError):
try: try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT) new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
except (TypeError, ValueError): except (TypeError, ValueError):
# If NoneType return empty string, else return original value # If NoneType return empty string, else return original value
new_value = "" if value is None else value new_value = "" if value is None else value

View File

@ -7,7 +7,6 @@ templatetags/saved_queries.py - This template tag returns previously saved
""" """
from django import template from django import template
from django.db.models import Q from django.db.models import Q
from helpdesk.models import SavedSearch from helpdesk.models import SavedSearch

View File

@ -10,12 +10,11 @@ templatetags/ticket_to_link.py - Used in ticket comments to allow wiki-style
to show the status of that ticket (eg a closed to show the status of that ticket (eg a closed
ticket would have a strikethrough). ticket would have a strikethrough).
""" """
from django import template from django import template
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from helpdesk.models import Ticket from helpdesk.models import Ticket
import re import re

View File

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
from django.contrib.auth import get_user_model
from helpdesk.models import Ticket, Queue, UserSettings from django.contrib.auth import get_user_model
from helpdesk.models import Queue, Ticket, UserSettings
import sys
User = get_user_model() User = get_user_model()

View File

@ -1,17 +1,26 @@
import base64
from datetime import datetime
import base64
from collections import OrderedDict
from datetime import datetime
from django.contrib.auth.models import User from django.contrib.auth.models import User
from pytz import UTC from django.core.files.uploadedfile import SimpleUploadedFile
from freezegun import freeze_time
from helpdesk.models import CustomField, Queue, Ticket
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework.exceptions import ErrorDetail 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 from rest_framework.status import (
HTTP_200_OK,
HTTP_201_CREATED,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_403_FORBIDDEN
)
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from helpdesk.models import Queue, Ticket, CustomField
class TicketTest(APITestCase): class TicketTest(APITestCase):
due_date = datetime(2022, 4, 10, 15, 6)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.queue = Queue.objects.create( cls.queue = Queue.objects.create(
@ -67,23 +76,29 @@ class TicketTest(APITestCase):
self.assertEqual(response.status_code, HTTP_201_CREATED) self.assertEqual(response.status_code, HTTP_201_CREATED)
created_ticket = Ticket.objects.get() created_ticket = Ticket.objects.get()
self.assertEqual(created_ticket.title, 'Test title') self.assertEqual(created_ticket.title, 'Test title')
self.assertEqual(created_ticket.description, 'Test description\nMulti lines') self.assertEqual(created_ticket.description,
'Test description\nMulti lines')
self.assertEqual(created_ticket.submitter_email, 'test@mail.com') self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 4) self.assertEqual(created_ticket.priority, 4)
self.assertEqual(created_ticket.followup_set.count(), 1)
def test_create_api_ticket_with_basic_auth(self): def test_create_api_ticket_with_basic_auth(self):
username = 'admin' username = 'admin'
password = 'admin' password = 'admin'
User.objects.create_user(username=username, password=password, is_staff=True) User.objects.create_user(
username=username, password=password, is_staff=True)
test_user = User.objects.create_user(username='test') test_user = User.objects.create_user(username='test')
merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket') merge_ticket = Ticket.objects.create(
queue=self.queue, title='merge ticket')
# Generate base64 credentials string # Generate base64 credentials string
credentials = f"{username}:{password}" credentials = f"{username}:{password}"
base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) base64_credentials = base64.b64encode(credentials.encode(
HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}") self.client.credentials(
HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
response = self.client.post( response = self.client.post(
'/api/tickets/', '/api/tickets/',
{ {
@ -96,7 +111,7 @@ class TicketTest(APITestCase):
'status': Ticket.RESOLVED_STATUS, 'status': Ticket.RESOLVED_STATUS,
'priority': 1, 'priority': 1,
'on_hold': True, 'on_hold': True,
'due_date': datetime(2022, 4, 10, 15, 6), 'due_date': self.due_date,
'merged_to': merge_ticket.id 'merged_to': merge_ticket.id
} }
) )
@ -105,21 +120,27 @@ class TicketTest(APITestCase):
created_ticket = Ticket.objects.last() created_ticket = Ticket.objects.last()
self.assertEqual(created_ticket.title, 'Title') self.assertEqual(created_ticket.title, 'Title')
self.assertEqual(created_ticket.description, 'Description') self.assertEqual(created_ticket.description, 'Description')
self.assertIsNone(created_ticket.resolution) # resolution can not be set on creation # resolution can not be set on creation
self.assertIsNone(created_ticket.resolution)
self.assertEqual(created_ticket.assigned_to, test_user) self.assertEqual(created_ticket.assigned_to, test_user)
self.assertEqual(created_ticket.submitter_email, 'test@mail.com') self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 1) self.assertEqual(created_ticket.priority, 1)
self.assertFalse(created_ticket.on_hold) # on_hold is False on creation # on_hold is False on creation
self.assertEqual(created_ticket.status, Ticket.OPEN_STATUS) # status is always open on creation self.assertFalse(created_ticket.on_hold)
self.assertEqual(created_ticket.due_date, datetime(2022, 4, 10, 15, 6, tzinfo=UTC)) # status is always open on creation
self.assertIsNone(created_ticket.merged_to) # merged_to can not be set 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): def test_edit_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') test_ticket = Ticket.objects.create(
queue=self.queue, title='Test ticket')
test_user = User.objects.create_user(username='test') test_user = User.objects.create_user(username='test')
merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket') merge_ticket = Ticket.objects.create(
queue=self.queue, title='merge ticket')
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
response = self.client.put( response = self.client.put(
@ -134,7 +155,7 @@ class TicketTest(APITestCase):
'status': Ticket.RESOLVED_STATUS, 'status': Ticket.RESOLVED_STATUS,
'priority': 1, 'priority': 1,
'on_hold': True, 'on_hold': True,
'due_date': datetime(2022, 4, 10, 15, 6), 'due_date': self.due_date,
'merged_to': merge_ticket.id 'merged_to': merge_ticket.id
} }
) )
@ -149,12 +170,13 @@ class TicketTest(APITestCase):
self.assertEqual(test_ticket.priority, 1) self.assertEqual(test_ticket.priority, 1)
self.assertTrue(test_ticket.on_hold) self.assertTrue(test_ticket.on_hold)
self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS) 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) self.assertEqual(test_ticket.merged_to, merge_ticket)
def test_partial_edit_api_ticket(self): def test_partial_edit_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') test_ticket = Ticket.objects.create(
queue=self.queue, title='Test ticket')
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
response = self.client.patch( response = self.client.patch(
@ -170,12 +192,14 @@ class TicketTest(APITestCase):
def test_delete_api_ticket(self): def test_delete_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True) staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket') test_ticket = Ticket.objects.create(
queue=self.queue, title='Test ticket')
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
response = self.client.delete('/api/tickets/%d/' % test_ticket.id) response = self.client.delete('/api/tickets/%d/' % test_ticket.id)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertFalse(Ticket.objects.exists()) self.assertFalse(Ticket.objects.exists())
@freeze_time('2022-06-30 23:09:44')
def test_create_api_ticket_with_custom_fields(self): def test_create_api_ticket_with_custom_fields(self):
# Create custom fields # Create custom fields
for field_type, field_display in CustomField.DATA_TYPE_CHOICES: for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
@ -193,7 +217,8 @@ class TicketTest(APITestCase):
Blue Blue
Red Red
Yellow''' Yellow'''
CustomField.objects.create(name=field_type, label=field_display, data_type=field_type, **extra_data) CustomField.objects.create(
name=field_type, label=field_display, data_type=field_type, **extra_data)
staff_user = User.objects.create_user(username='test', is_staff=True) staff_user = User.objects.create_user(username='test', is_staff=True)
self.client.force_authenticate(staff_user) self.client.force_authenticate(staff_user)
@ -207,7 +232,8 @@ class TicketTest(APITestCase):
'priority': 4 'priority': 4
}) })
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'custom_integer': [ErrorDetail(string='This field is required.', code='required')]}) self.assertEqual(response.data, {'custom_integer': [ErrorDetail(
string='This field is required.', code='required')]})
# Test creation with custom field values # Test creation with custom field values
response = self.client.post('/api/tickets/', { response = self.client.post('/api/tickets/', {
@ -245,6 +271,19 @@ class TicketTest(APITestCase):
'priority': 4, 'priority': 4,
'due_date': None, 'due_date': None,
'merged_to': 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_varchar': 'test',
'custom_text': 'multi\nline', 'custom_text': 'multi\nline',
'custom_integer': 1, 'custom_integer': 1,
@ -260,3 +299,63 @@ class TicketTest(APITestCase):
'custom_slug': 'test-slug' '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

@ -1,16 +1,15 @@
# vim: set fileencoding=utf-8 : # vim: set fileencoding=utf-8 :
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from django.test import override_settings, TestCase from django.test import override_settings, TestCase
from django.utils.encoding import smart_text from django.urls import reverse
from django.utils.encoding import smart_str
from helpdesk import lib, models from helpdesk import lib, models
import os import os
import shutil import shutil
from tempfile import gettempdir from tempfile import gettempdir
from unittest import mock from unittest import mock
from unittest.case import skip
MEDIA_DIR = os.path.join(gettempdir(), 'helpdesk_test_media') MEDIA_DIR = os.path.join(gettempdir(), 'helpdesk_test_media')
@ -46,7 +45,8 @@ class AttachmentIntegrationTests(TestCase):
} }
def test_create_pub_ticket_with_attachment(self): def test_create_pub_ticket_with_attachment(self):
test_file = SimpleUploadedFile('test_att.txt', b'attached file content', 'text/plain') test_file = SimpleUploadedFile(
'test_att.txt', b'attached file content', 'text/plain')
post_data = self.ticket_data.copy() post_data = self.ticket_data.copy()
post_data.update({ post_data.update({
'queue': self.queue_public.id, 'queue': self.queue_public.id,
@ -54,17 +54,20 @@ class AttachmentIntegrationTests(TestCase):
}) })
# Ensure ticket form submits with attachment successfully # Ensure ticket form submits with attachment successfully
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) response = self.client.post(
reverse('helpdesk:home'), post_data, follow=True)
self.assertContains(response, test_file.name) self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content # Ensure attachment is available with correct content
att = models.FollowUpAttachment.objects.get(followup__ticket=response.context['ticket']) att = models.FollowUpAttachment.objects.get(
followup__ticket=response.context['ticket'])
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk: with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = file_on_disk.read() disk_content = file_on_disk.read()
self.assertEqual(disk_content, 'attached file content') self.assertEqual(disk_content, 'attached file content')
def test_create_pub_ticket_with_attachment_utf8(self): def test_create_pub_ticket_with_attachment_utf8(self):
test_file = SimpleUploadedFile('ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8') test_file = SimpleUploadedFile(
'ß°äöü.txt', 'โจ'.encode('utf-8'), 'text/utf-8')
post_data = self.ticket_data.copy() post_data = self.ticket_data.copy()
post_data.update({ post_data.update({
'queue': self.queue_public.id, 'queue': self.queue_public.id,
@ -72,17 +75,20 @@ class AttachmentIntegrationTests(TestCase):
}) })
# Ensure ticket form submits with attachment successfully # Ensure ticket form submits with attachment successfully
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) response = self.client.post(
reverse('helpdesk:home'), post_data, follow=True)
self.assertContains(response, test_file.name) self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content # Ensure attachment is available with correct content
att = models.FollowUpAttachment.objects.get(followup__ticket=response.context['ticket']) att = models.FollowUpAttachment.objects.get(
followup__ticket=response.context['ticket'])
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk: with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = smart_text(file_on_disk.read(), 'utf-8') disk_content = smart_str(file_on_disk.read(), 'utf-8')
self.assertEqual(disk_content, 'โจ') self.assertEqual(disk_content, 'โจ')
@mock.patch.object(models.FollowUp, 'save', autospec=True) @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.Ticket, 'save', autospec=True)
@mock.patch.object(models.Queue, 'save', autospec=True) @mock.patch.object(models.Queue, 'save', autospec=True)
class AttachmentUnitTests(TestCase): class AttachmentUnitTests(TestCase):
@ -100,10 +106,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): def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" check utf-8 data is parsed correctly """ """ check utf-8 data is parsed correctly """
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0] filename, fileobj = lib.process_attachments(
self.follow_up, [self.test_file])[0]
mock_att_save.assert_called_with( mock_att_save.assert_called_with(
file=self.test_file, file=self.test_file,
filename=self.file_attrs['filename'], filename=self.file_attrs['filename'],
@ -113,18 +120,18 @@ class AttachmentUnitTests(TestCase):
) )
self.assertEqual(filename, self.file_attrs['filename']) 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): def test_autofill(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" check utf-8 data is parsed correctly """ """ check utf-8 data is parsed correctly """
obj = models.FollowUpAttachment.objects.create( obj = models.FollowUpAttachment.objects.create(
followup=self.follow_up, followup=self.follow_up,
file=self.test_file file=self.test_file
) )
self.assertEqual(obj.filename, self.file_attrs['filename']) obj.save()
self.assertEqual(obj.size, len(self.file_attrs['content'])) self.assertEqual(obj.file.name, self.file_attrs['filename'])
self.assertEqual(obj.mime_type, "text/plain") 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 """ """ check utf-8 data is parsed correctly """
kbcategory = models.KBCategory.objects.create( kbcategory = models.KBCategory.objects.create(
@ -143,17 +150,20 @@ class AttachmentUnitTests(TestCase):
kbitem=kbitem, kbitem=kbitem,
file=self.test_file file=self.test_file
) )
obj.save()
self.assertEqual(obj.filename, self.file_attrs['filename']) 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") 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) @override_settings(MEDIA_ROOT=MEDIA_DIR)
def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" don't mock saving to filesystem to test file renames caused by storage layer """ """ don't mock saving to filesystem to test file renames caused by storage layer """
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0] filename, fileobj = lib.process_attachments(
# Attachment object was zeroth positional arg (i.e. self) of att.save call self.follow_up, [self.test_file])[0]
attachment_obj = mock_att_save.call_args[0][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) mock_att_save.assert_called_once_with(attachment_obj)
self.assertIsInstance(attachment_obj, models.FollowUpAttachment) self.assertIsInstance(attachment_obj, models.FollowUpAttachment)

View File

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

View File

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

@ -1,4 +1,4 @@
from django.test import TestCase, override_settings from django.test import override_settings, TestCase
from django.urls import reverse from django.urls import reverse

View File

@ -1,12 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
from importlib import reload
from django.urls import reverse
from django.test import TestCase
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import Queue from helpdesk.models import Queue
from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response) from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User
from importlib import reload
import sys
class KBDisabledTestCase(TestCase): class KBDisabledTestCase(TestCase):
@ -25,13 +27,15 @@ class KBDisabledTestCase(TestCase):
"""Test proper rendering of navigation.html by accessing the dashboard""" """Test proper rendering of navigation.html by accessing the dashboard"""
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
self.client.login(username=get_staff_user().get_username(), password='password') self.client.login(username=get_staff_user(
).get_username(), password='password')
self.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index') self.assertRaises(NoReverseMatch, reverse, 'helpdesk:kb_index')
try: try:
response = self.client.get(reverse('helpdesk:dashboard')) response = self.client.get(reverse('helpdesk:dashboard'))
except NoReverseMatch as e: except NoReverseMatch as e:
if 'helpdesk:kb_index' in e.message: if 'helpdesk:kb_index' in e.message:
self.fail("Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)") self.fail(
"Please verify any unchecked references to helpdesk_kb_index (start with navigation.html)")
else: else:
raise raise
else: else:
@ -74,7 +78,8 @@ class NonStaffUsersAllowedTestCase(StaffUserTestCaseMixin, TestCase):
""" """
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') user = User.objects.create_user(
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
self.assertTrue(is_helpdesk_staff(user)) self.assertTrue(is_helpdesk_staff(user))
@ -89,7 +94,9 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
def setUp(self): def setUp(self):
super().setUp() 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): def test_staff_user_detection(self):
"""Staff and non-staff users are correctly identified""" """Staff and non-staff users are correctly identified"""
@ -116,7 +123,8 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = self.non_staff_user user = self.non_staff_user
self.client.login(username=user.username, password=user.password) self.client.login(username=user.username,
password=self.non_staff_user_password)
response = self.client.get(reverse('helpdesk:dashboard'), follow=True) response = self.client.get(reverse('helpdesk:dashboard'), follow=True)
self.assertTemplateUsed(response, 'helpdesk/registration/login.html') self.assertTemplateUsed(response, 'helpdesk/registration/login.html')
@ -125,30 +133,35 @@ class StaffUsersOnlyTestCase(StaffUserTestCaseMixin, TestCase):
staff users should be able to access rss feeds. staff users should be able to access rss feeds.
""" """
user = get_staff_user() user = get_staff_user()
self.client.login(username=user.username, password='password') self.client.login(username=user.username, password="password")
response = self.client.get(reverse('helpdesk:rss_unassigned'), follow=True) response = self.client.get(
reverse('helpdesk:rss_unassigned'), follow=True)
self.assertContains(response, 'Unassigned Open and Reopened tickets') self.assertContains(response, 'Unassigned Open and Reopened tickets')
@override_settings(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE=False)
def test_non_staff_cannot_rss(self): def test_non_staff_cannot_rss(self):
"""If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False, """If HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE is False,
non-staff users should not be able to access rss feeds. non-staff users should not be able to access rss feeds.
""" """
user = self.non_staff_user user = self.non_staff_user
self.client.login(username=user.username, password='password') self.client.login(username=user.username,
password=self.non_staff_user_password)
queue = Queue.objects.create( queue = Queue.objects.create(
title="Foo", title="Foo",
slug="test_queue", slug="test_queue",
) )
rss_urls = [ rss_urls = [
reverse('helpdesk:rss_user', args=[user.username]), reverse('helpdesk:rss_user', args=[user.username]),
reverse('helpdesk:rss_user_queue', args=[user.username, 'test_queue']), reverse('helpdesk:rss_user_queue', args=[
user.username, 'test_queue']),
reverse('helpdesk:rss_queue', args=['test_queue']), reverse('helpdesk:rss_queue', args=['test_queue']),
reverse('helpdesk:rss_unassigned'), reverse('helpdesk:rss_unassigned'),
reverse('helpdesk:rss_activity'), reverse('helpdesk:rss_activity'),
] ]
for rss_url in rss_urls: for rss_url in rss_urls:
response = self.client.get(rss_url, follow=True) response = self.client.get(rss_url, follow=True)
self.assertTemplateUsed(response, 'helpdesk/registration/login.html') self.assertTemplateUsed(
response, 'helpdesk/registration/login.html')
class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase): class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
@ -165,7 +178,8 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
""" """
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') user = User.objects.create_user(
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
self.assertTrue(is_helpdesk_staff(user)) self.assertTrue(is_helpdesk_staff(user))
@ -176,7 +190,8 @@ class CustomStaffUserTestCase(StaffUserTestCaseMixin, TestCase):
def test_custom_staff_fail(self): def test_custom_staff_fail(self):
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
user = User.objects.create_user(username='terry.milton', password='frog', email='milton@example.com') user = User.objects.create_user(
username='terry.milton', password='frog', email='milton@example.com')
self.assertFalse(is_helpdesk_staff(user)) self.assertFalse(is_helpdesk_staff(user))
@ -261,7 +276,8 @@ class ReturnToTicketTestCase(TestCase):
def test_non_staff_user(self): def test_non_staff_user(self):
from helpdesk.views.staff import return_to_ticket from helpdesk.views.staff import return_to_ticket
user = User.objects.create_user(username='henry.wensleydale', password='gouda', email='wensleydale@example.com') user = User.objects.create_user(
username='henry.wensleydale', password='gouda', email='wensleydale@example.com')
ticket = create_ticket() ticket = create_ticket()
response = return_to_ticket(user, helpdesk_settings, ticket) response = return_to_ticket(user, helpdesk_settings, ticket)
self.assertEqual(response['location'], ticket.ticket_url) self.assertEqual(response['location'], ticket.ticket_url)

View File

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

View File

@ -1,7 +1,7 @@
from helpdesk.models import Queue, Ticket
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse from django.urls import reverse
from helpdesk.models import Queue, Ticket
class PublicActionsTestCase(TestCase): class PublicActionsTestCase(TestCase):
@ -70,7 +70,8 @@ class PublicActionsTestCase(TestCase):
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html') self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
self.assertEqual(ticket.status, Ticket.CLOSED_STATUS) self.assertEqual(ticket.status, Ticket.CLOSED_STATUS)
self.assertEqual(ticket.resolution, resolution_text) self.assertEqual(ticket.resolution, resolution_text)
self.assertEqual(current_followups + 1, ticket.followup_set.all().count()) self.assertEqual(current_followups + 1,
ticket.followup_set.all().count())
ticket.resolution = old_resolution ticket.resolution = old_resolution
ticket.status = old_status ticket.status = old_status

View File

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

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from helpdesk.models import Queue from helpdesk.models import Queue
from helpdesk.tests.helpers import get_user from helpdesk.tests.helpers import get_user

View File

@ -1,22 +1,21 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import mail from django.core import mail
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from helpdesk.models import CustomField, Queue, Ticket
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import CustomField, Queue, Ticket
from helpdesk.templatetags.ticket_to_link import num_to_link
from helpdesk.user import HelpdeskUser
try: # python 3 try: # python 3
from urllib.parse import urlparse from urllib.parse import urlparse
except ImportError: # python 2 except ImportError: # python 2
from urlparse import urlparse from urlparse import urlparse
from helpdesk.templatetags.ticket_to_link import num_to_link
from helpdesk.user import HelpdeskUser
class TicketActionsTestCase(TestCase): class TicketActionsTestCase(TestCase):
fixtures = ['emailtemplate.json'] fixtures = ['emailtemplate.json']
@ -78,10 +77,13 @@ class TicketActionsTestCase(TestCase):
ticket = Ticket.objects.create(**ticket_data) ticket = Ticket.objects.create(**ticket_data)
ticket_id = ticket.id ticket_id = ticket.id
response = self.client.get(reverse('helpdesk:delete', kwargs={'ticket_id': ticket_id}), follow=True) response = self.client.get(reverse('helpdesk:delete', kwargs={
self.assertContains(response, 'Are you sure you want to delete this ticket') 'ticket_id': ticket_id}), follow=True)
self.assertContains(
response, 'Are you sure you want to delete this ticket')
response = self.client.post(reverse('helpdesk:delete', kwargs={'ticket_id': ticket_id}), follow=True) response = self.client.post(reverse('helpdesk:delete', kwargs={
'ticket_id': ticket_id}), follow=True)
first_redirect = response.redirect_chain[0] first_redirect = response.redirect_chain[0]
first_redirect_url = first_redirect[0] first_redirect_url = first_redirect[0]
@ -123,7 +125,8 @@ class TicketActionsTestCase(TestCase):
post_data = { post_data = {
'owner': self.user2.id, 'owner': self.user2.id,
} }
response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) response = self.client.post(reverse('helpdesk:update', kwargs={
'ticket_id': ticket_id}), post_data, follow=True)
self.assertContains(response, 'Changed Owner from User_1 to User_2') self.assertContains(response, 'Changed Owner from User_1 to User_2')
# change status with users email assigned and submitter email assigned, # change status with users email assigned and submitter email assigned,
@ -142,14 +145,16 @@ class TicketActionsTestCase(TestCase):
# do this also to a newly assigned user (different from logged in one) # do this also to a newly assigned user (different from logged in one)
ticket.assigned_to = self.user ticket.assigned_to = self.user
response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) response = self.client.post(reverse('helpdesk:update', kwargs={
'ticket_id': ticket_id}), post_data, follow=True)
self.assertContains(response, 'Changed Status from Open to Closed') self.assertContains(response, 'Changed Status from Open to Closed')
post_data = { post_data = {
'new_status': Ticket.OPEN_STATUS, 'new_status': Ticket.OPEN_STATUS,
'owner': self.user2.id, 'owner': self.user2.id,
'public': True 'public': True
} }
response = self.client.post(reverse('helpdesk:update', kwargs={'ticket_id': ticket_id}), post_data, follow=True) response = self.client.post(reverse('helpdesk:update', kwargs={
'ticket_id': ticket_id}), post_data, follow=True)
self.assertContains(response, 'Changed Status from Open to Closed') self.assertContains(response, 'Changed Status from Open to Closed')
def test_can_access_ticket(self): def test_can_access_ticket(self):
@ -175,8 +180,10 @@ class TicketActionsTestCase(TestCase):
# create ticket # create ticket
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = True
ticket = Ticket.objects.create(**initial_data) ticket = Ticket.objects.create(**initial_data)
self.assertEqual(HelpdeskUser(self.user).can_access_ticket(ticket), True) self.assertEqual(HelpdeskUser(
self.assertEqual(HelpdeskUser(self.user2).can_access_ticket(ticket), False) self.user).can_access_ticket(ticket), True)
self.assertEqual(HelpdeskUser(
self.user2).can_access_ticket(ticket), False)
def test_num_to_link(self): def test_num_to_link(self):
"""Test that we are correctly expanding links to tickets from IDs""" """Test that we are correctly expanding links to tickets from IDs"""
@ -197,10 +204,13 @@ class TicketActionsTestCase(TestCase):
# generate the URL text # generate the URL text
result = num_to_link('this is ticket#%s' % ticket_id) result = num_to_link('this is ticket#%s' % ticket_id)
self.assertEqual(result, "this is ticket <a href='/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) result2 = num_to_link(
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)) 'whoa another ticket is here #%s huh' % ticket_id)
self.assertEqual(
result2, "whoa another ticket is here <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a> huh" % (ticket_id, ticket_id))
def test_create_ticket_getform(self): def test_create_ticket_getform(self):
self.loginUser() self.loginUser()
@ -221,7 +231,8 @@ class TicketActionsTestCase(TestCase):
status=Ticket.RESOLVED_STATUS, status=Ticket.RESOLVED_STATUS,
resolution='Awesome resolution for ticket 1' resolution='Awesome resolution for ticket 1'
) )
ticket_1_follow_up = ticket_1.followup_set.create(title='Ticket 1 creation') ticket_1_follow_up = ticket_1.followup_set.create(
title='Ticket 1 creation')
ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user) ticket_1_cc = ticket_1.ticketcc_set.create(user=self.user)
ticket_1_created = ticket_1.created ticket_1_created = ticket_1.created
due_date = timezone.now() due_date = timezone.now()
@ -233,7 +244,8 @@ class TicketActionsTestCase(TestCase):
due_date=due_date, due_date=due_date,
assigned_to=self.user assigned_to=self.user
) )
ticket_2_follow_up = ticket_1.followup_set.create(title='Ticket 2 creation') ticket_2_follow_up = ticket_1.followup_set.create(
title='Ticket 2 creation')
ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com') ticket_2_cc = ticket_2.ticketcc_set.create(email='random@mail.com')
# Create custom fields and set values for tickets # Create custom fields and set values for tickets
@ -243,16 +255,19 @@ class TicketActionsTestCase(TestCase):
data_type='varchar', data_type='varchar',
) )
ticket_1_field_1 = 'This is for the test field' ticket_1_field_1 = 'This is for the test field'
ticket_1.ticketcustomfieldvalue_set.create(field=custom_field_1, value=ticket_1_field_1) ticket_1.ticketcustomfieldvalue_set.create(
field=custom_field_1, value=ticket_1_field_1)
ticket_2_field_1 = 'Another test text' ticket_2_field_1 = 'Another test text'
ticket_2.ticketcustomfieldvalue_set.create(field=custom_field_1, value=ticket_2_field_1) ticket_2.ticketcustomfieldvalue_set.create(
field=custom_field_1, value=ticket_2_field_1)
custom_field_2 = CustomField.objects.create( custom_field_2 = CustomField.objects.create(
name='number', name='number',
label='Number', label='Number',
data_type='integer', data_type='integer',
) )
ticket_2_field_2 = '444' ticket_2_field_2 = '444'
ticket_2.ticketcustomfieldvalue_set.create(field=custom_field_2, value=ticket_2_field_2) ticket_2.ticketcustomfieldvalue_set.create(
field=custom_field_2, value=ticket_2_field_2)
# Check that it correctly redirects to the intermediate page # Check that it correctly redirects to the intermediate page
response = self.client.post( response = self.client.post(
@ -263,7 +278,8 @@ class TicketActionsTestCase(TestCase):
}, },
follow=True follow=True
) )
redirect_url = '%s?tickets=%s&tickets=%s' % (reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id) redirect_url = '%s?tickets=%s&tickets=%s' % (
reverse('helpdesk:merge_tickets'), ticket_1.id, ticket_2.id)
self.assertRedirects(response, redirect_url) self.assertRedirects(response, redirect_url)
self.assertContains(response, ticket_1.description) self.assertContains(response, ticket_1.description)
self.assertContains(response, ticket_1.resolution) self.assertContains(response, ticket_1.resolution)
@ -301,7 +317,11 @@ class TicketActionsTestCase(TestCase):
self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email) self.assertEqual(ticket_1.submitter_email, ticket_2.submitter_email)
self.assertEqual(ticket_1.description, ticket_2.description) self.assertEqual(ticket_1.description, ticket_2.description)
self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to) self.assertEqual(ticket_1.assigned_to, ticket_2.assigned_to)
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_1).value, ticket_1_field_1) self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(field=custom_field_2).value, ticket_2_field_2) field=custom_field_1).value, ticket_1_field_1)
self.assertEqual(list(ticket_1.followup_set.all()), [ticket_1_follow_up, ticket_2_follow_up]) self.assertEqual(ticket_1.ticketcustomfieldvalue_set.get(
self.assertEqual(list(ticket_1.ticketcc_set.all()), [ticket_1_cc, ticket_2_cc]) field=custom_field_2).value, ticket_2_field_2)
self.assertEqual(list(ticket_1.followup_set.all()), [
ticket_1_follow_up, ticket_2_follow_up])
self.assertEqual(list(ticket_1.ticketcc_set.all()),
[ticket_1_cc, ticket_2_cc])

View File

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

View File

@ -1,22 +1,19 @@
import email
import uuid
from helpdesk.models import Queue, CustomField, FollowUp, Ticket, TicketCC, KBCategory, KBItem
from django.test import TestCase
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail from django.core import mail
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.forms import ValidationError from django.forms import ValidationError
from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse from django.urls import reverse
import email
from helpdesk.email import object_from_message, create_ticket_cc from helpdesk.email import create_ticket_cc, object_from_message
from helpdesk.models import CustomField, FollowUp, KBCategory, KBItem, Queue, Ticket, TicketCC
from helpdesk.tests.helpers import print_response from helpdesk.tests.helpers import print_response
from urllib.parse import urlparse
import logging import logging
from urllib.parse import urlparse
import uuid
logger = logging.getLogger('helpdesk') logger = logging.getLogger('helpdesk')
@ -51,7 +48,6 @@ class TicketBasicsTestCase(TestCase):
self.client = Client() self.client = Client()
def test_create_ticket_instance_from_payload(self): def test_create_ticket_instance_from_payload(self):
""" """
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue. Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
""" """
@ -76,7 +72,8 @@ class TicketBasicsTestCase(TestCase):
'priority': 3, 'priority': 3,
} }
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) response = self.client.post(
reverse('helpdesk:home'), post_data, follow=True)
last_redirect = response.redirect_chain[-1] last_redirect = response.redirect_chain[-1]
last_redirect_url = last_redirect[0] last_redirect_url = last_redirect[0]
# last_redirect_status = last_redirect[1] # last_redirect_status = last_redirect[1]
@ -95,7 +92,6 @@ class TicketBasicsTestCase(TestCase):
# Follow up is anonymous # Follow up is anonymous
self.assertIsNone(ticket.followup_set.first().user) self.assertIsNone(ticket.followup_set.first().user)
def test_create_ticket_public_with_hidden_fields(self): def test_create_ticket_public_with_hidden_fields(self):
email_count = len(mail.outbox) email_count = len(mail.outbox)
@ -110,11 +106,11 @@ class TicketBasicsTestCase(TestCase):
'priority': 4, 'priority': 4,
} }
response = self.client.post(reverse('helpdesk:home') + "?_hide_fields_=priority", post_data, follow=True) response = self.client.post(
reverse('helpdesk:home') + "?_hide_fields_=priority", post_data, follow=True)
ticket = Ticket.objects.last() ticket = Ticket.objects.last()
self.assertEqual(ticket.priority, 4) self.assertEqual(ticket.priority, 4)
def test_create_ticket_authorized(self): def test_create_ticket_authorized(self):
email_count = len(mail.outbox) email_count = len(mail.outbox)
self.client.force_login(self.user) self.client.force_login(self.user)
@ -130,7 +126,8 @@ class TicketBasicsTestCase(TestCase):
'priority': 3, 'priority': 3,
} }
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) response = self.client.post(
reverse('helpdesk:home'), post_data, follow=True)
last_redirect = response.redirect_chain[-1] last_redirect = response.redirect_chain[-1]
last_redirect_url = last_redirect[0] last_redirect_url = last_redirect[0]
# last_redirect_status = last_redirect[1] # last_redirect_status = last_redirect[1]
@ -188,7 +185,8 @@ class TicketBasicsTestCase(TestCase):
'custom_textfield': 'This is my custom text.', 'custom_textfield': 'This is my custom text.',
} }
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) response = self.client.post(
reverse('helpdesk:home'), post_data, follow=True)
custom_field_1.delete() custom_field_1.delete()
last_redirect = response.redirect_chain[-1] last_redirect = response.redirect_chain[-1]
@ -221,7 +219,8 @@ class TicketBasicsTestCase(TestCase):
'priority': 3, 'priority': 3,
} }
response = self.client.post(reverse('helpdesk:home'), post_data, follow=True) response = self.client.post(
reverse('helpdesk:home'), post_data, follow=True)
last_redirect = response.redirect_chain[-1] last_redirect = response.redirect_chain[-1]
last_redirect_url = last_redirect[0] last_redirect_url = last_redirect[0]
# last_redirect_status = last_redirect[1] # last_redirect_status = last_redirect[1]
@ -266,7 +265,6 @@ class EmailInteractionsTestCase(TestCase):
} }
def test_create_ticket_from_email_with_message_id(self): def test_create_ticket_from_email_with_message_id(self):
""" """
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue. Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id> Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id>
@ -302,7 +300,6 @@ class EmailInteractionsTestCase(TestCase):
self.assertIn(submitter_email, mail.outbox[0].to) self.assertIn(submitter_email, mail.outbox[0].to)
def test_create_ticket_from_email_without_message_id(self): def test_create_ticket_from_email_without_message_id(self):
""" """
Ensure that a <Ticket> instance is created whenever an email is sent to a public queue. Ensure that a <Ticket> instance is created whenever an email is sent to a public queue.
Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id> Also, make sure that the RFC 2822 field "message-id" is stored on the <Ticket.submitter_email_id>
@ -322,7 +319,8 @@ class EmailInteractionsTestCase(TestCase):
object_from_message(str(msg), self.queue_public, logger=logger) object_from_message(str(msg), self.queue_public, logger=logger)
ticket = Ticket.objects.get(title=self.ticket_data['title'], queue=self.queue_public, submitter_email=submitter_email) ticket = Ticket.objects.get(
title=self.ticket_data['title'], queue=self.queue_public, submitter_email=submitter_email)
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
@ -417,8 +415,10 @@ class EmailInteractionsTestCase(TestCase):
# Ensure that the submitter is notified # Ensure that the submitter is notified
self.assertIn(submitter_email, mail.outbox[0].to) self.assertIn(submitter_email, mail.outbox[0].to)
# Ensure that the queue's email was not subscribed to the event notifications. # Ensure that the queue's email was not subscribed to the event
self.assertRaises(TicketCC.DoesNotExist, TicketCC.objects.get, ticket=ticket, email=to_list[0]) # notifications.
self.assertRaises(TicketCC.DoesNotExist,
TicketCC.objects.get, ticket=ticket, email=to_list[0])
for cc_email in cc_list: for cc_email in cc_list:
@ -649,17 +649,18 @@ class EmailInteractionsTestCase(TestCase):
# the new and update queues (+2) # the new and update queues (+2)
# Ensure that the submitter is notified # Ensure that the submitter is notified
self.assertIn(submitter_email, mail.outbox[expected_email_count - 1].to) # 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: # # Ensure that contacts on cc_list will be notified on the same email (index 0)
self.assertIn(cc_email, mail.outbox[expected_email_count - 1].to) # 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 # # Even after 2 messages with the same cc_list,
ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email) # # <get> MUST return only one object
self.assertTrue(ticket_cc.ticket, ticket) # ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email)
self.assertTrue(ticket_cc.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): def test_create_followup_from_email_with_invalid_message_id(self):
""" """
@ -824,14 +825,16 @@ class EmailInteractionsTestCase(TestCase):
msg.__setitem__('Message-ID', message_id) msg.__setitem__('Message-ID', message_id)
msg.__setitem__('Subject', self.ticket_data['title']) msg.__setitem__('Subject', self.ticket_data['title'])
msg.__setitem__('From', submitter_email) msg.__setitem__('From', submitter_email)
msg.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) msg.__setitem__(
'To', self.queue_public_with_notifications_disabled.email_address)
msg.__setitem__('Cc', ','.join(cc_list)) msg.__setitem__('Cc', ','.join(cc_list))
msg.__setitem__('Content-Type', 'text/plain;') msg.__setitem__('Content-Type', 'text/plain;')
msg.set_payload(self.ticket_data['description']) msg.set_payload(self.ticket_data['description'])
email_count = len(mail.outbox) email_count = len(mail.outbox)
object_from_message(str(msg), self.queue_public_with_notifications_disabled, logger=logger) object_from_message(
str(msg), self.queue_public_with_notifications_disabled, logger=logger)
followup = FollowUp.objects.get(message_id=message_id) followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id) ticket = Ticket.objects.get(id=followup.ticket.id)
@ -953,14 +956,16 @@ class EmailInteractionsTestCase(TestCase):
msg.__setitem__('Message-ID', message_id) msg.__setitem__('Message-ID', message_id)
msg.__setitem__('Subject', self.ticket_data['title']) msg.__setitem__('Subject', self.ticket_data['title'])
msg.__setitem__('From', submitter_email) msg.__setitem__('From', submitter_email)
msg.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) msg.__setitem__(
'To', self.queue_public_with_notifications_disabled.email_address)
msg.__setitem__('Cc', ','.join(cc_list)) msg.__setitem__('Cc', ','.join(cc_list))
msg.__setitem__('Content-Type', 'text/plain;') msg.__setitem__('Content-Type', 'text/plain;')
msg.set_payload(self.ticket_data['description']) msg.set_payload(self.ticket_data['description'])
email_count = len(mail.outbox) email_count = len(mail.outbox)
object_from_message(str(msg), self.queue_public_with_notifications_disabled, logger=logger) object_from_message(
str(msg), self.queue_public_with_notifications_disabled, logger=logger)
followup = FollowUp.objects.get(message_id=message_id) followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id) ticket = Ticket.objects.get(id=followup.ticket.id)
@ -993,11 +998,13 @@ class EmailInteractionsTestCase(TestCase):
reply.__setitem__('In-Reply-To', message_id) reply.__setitem__('In-Reply-To', message_id)
reply.__setitem__('Subject', self.ticket_data['title']) reply.__setitem__('Subject', self.ticket_data['title'])
reply.__setitem__('From', submitter_email) reply.__setitem__('From', submitter_email)
reply.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) reply.__setitem__(
'To', self.queue_public_with_notifications_disabled.email_address)
reply.__setitem__('Content-Type', 'text/plain;') reply.__setitem__('Content-Type', 'text/plain;')
reply.set_payload(self.ticket_data['description']) reply.set_payload(self.ticket_data['description'])
object_from_message(str(reply), self.queue_public_with_notifications_disabled, logger=logger) object_from_message(
str(reply), self.queue_public_with_notifications_disabled, logger=logger)
followup = FollowUp.objects.get(message_id=message_id) followup = FollowUp.objects.get(message_id=message_id)
ticket = Ticket.objects.get(id=followup.ticket.id) ticket = Ticket.objects.get(id=followup.ticket.id)
@ -1092,8 +1099,12 @@ class EmailInteractionsTestCase(TestCase):
answer="A KB Item", answer="A KB Item",
) )
self.kbitem1.save() self.kbitem1.save()
cat_url = reverse('helpdesk:submit') + "?kbitem=1&submitter_email=foo@bar.cz&title=lol" cat_url = reverse('helpdesk:submit') + \
"?kbitem=1&submitter_email=foo@bar.cz&title=lol"
response = self.client.get(cat_url) response = self.client.get(cat_url)
self.assertContains(response, '<option value="1" selected>KBItem 1</option>') self.assertContains(
self.assertContains(response, '<input type="email" name="submitter_email" value="foo@bar.cz" class="form-control form-control" required id="id_submitter_email">') response, '<option value="1" selected>KBItem 1</option>')
self.assertContains(response, '<input type="text" name="title" value="lol" class="form-control form-control" maxlength="100" required id="id_title">') self.assertContains(
response, '<input type="email" name="submitter_email" value="foo@bar.cz" class="form-control form-control" required id="id_submitter_email">')
self.assertContains(
response, '<input type="text" name="title" value="lol" class="form-control form-control" maxlength="100" required id="id_title">')

View File

@ -1,23 +1,24 @@
import datetime
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import mail from django.core import mail
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from helpdesk.models import Queue, Ticket, FollowUp from django.urls import reverse
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from django.contrib.auth.models import User from helpdesk.models import FollowUp, Queue, Ticket
from django.contrib.auth.hashers import make_password from helpdesk.templatetags.ticket_to_link import num_to_link
import uuid import uuid
import datetime
try: # python 3 try: # python 3
from urllib.parse import urlparse from urllib.parse import urlparse
except ImportError: # python 2 except ImportError: # python 2
from urlparse import urlparse from urlparse import urlparse
from helpdesk.templatetags.ticket_to_link import num_to_link
class TimeSpentTestCase(TestCase): class TimeSpentTestCase(TestCase):

View File

@ -1,10 +1,11 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail from django.core import mail
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse
from helpdesk.models import CustomField, Queue, Ticket from helpdesk.models import CustomField, Queue, Ticket
try: # python 3 try: # python 3
from urllib.parse import urlparse from urllib.parse import urlparse
except ImportError: # python 2 except ImportError: # python 2
@ -26,5 +27,6 @@ class TicketActionsTestCase(TestCase):
def test_get_user_settings(self): def test_get_user_settings(self):
response = self.client.get(reverse('helpdesk:user_settings'), follow=True) response = self.client.get(
reverse('helpdesk:user_settings'), follow=True)
self.assertContains(response, "Use the following options") self.assertContains(response, "Use the following options")

View File

@ -1,7 +1,8 @@
from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.urls import include, path
urlpatterns = [ urlpatterns = [
url(r'^helpdesk/', include('helpdesk.urls', namespace='helpdesk')), path('', include('helpdesk.urls', namespace='helpdesk')),
url(r'^admin/', admin.site.urls), path('admin/', admin.site.urls),
] ]

View File

@ -7,17 +7,16 @@ urls.py - Mapping of URL's to our various views. Note we always used NAMED
views for simplicity in linking later on. views for simplicity in linking later on.
""" """
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import include from django.contrib.auth.decorators import login_required
from django.urls import include, path, re_path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from helpdesk import settings as helpdesk_settings
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
from helpdesk.views import feeds, login, public, staff
from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet
from rest_framework.routers import DefaultRouter 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
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.views import kb from helpdesk.views import kb
@ -43,243 +42,204 @@ class DirectTemplateView(TemplateView):
return context 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 = [ urlpatterns = [
url(r'^dashboard/$', path("dashboard/", staff.dashboard, name="dashboard"),
staff.dashboard, path("tickets/", staff.ticket_list, name="list"),
name='dashboard'), path("tickets/update/", staff.mass_update, name="mass_update"),
path("tickets/merge", staff.merge_tickets, name="merge_tickets"),
url(r'^tickets/$', path("tickets/<int:ticket_id>/", staff.view_ticket, name="view"),
staff.ticket_list, path(
name='list'), "tickets/<int:ticket_id>/followup_edit/<int:followup_id>/",
url(r'^tickets/update/$',
staff.mass_update,
name='mass_update'),
url(r'^tickets/merge$',
staff.merge_tickets,
name='merge_tickets'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/$',
staff.view_ticket,
name='view'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/followup_edit/(?P<followup_id>[0-9]+)/$',
staff.followup_edit, staff.followup_edit,
name='followup_edit'), name="followup_edit",
),
url(r'^tickets/(?P<ticket_id>[0-9]+)/followup_delete/(?P<followup_id>[0-9]+)/$', path(
"tickets/<int:ticket_id>/followup_delete/<int:followup_id>/",
staff.followup_delete, staff.followup_delete,
name='followup_delete'), name="followup_delete",
),
url(r'^tickets/(?P<ticket_id>[0-9]+)/edit/$', path("tickets/<int:ticket_id>/edit/", staff.edit_ticket, name="edit"),
staff.edit_ticket, path("tickets/<int:ticket_id>/update/",
name='edit'), staff.update_ticket, name="update"),
path("tickets/<int:ticket_id>/delete/",
url(r'^tickets/(?P<ticket_id>[0-9]+)/update/$', staff.delete_ticket, name="delete"),
staff.update_ticket, path("tickets/<int:ticket_id>/hold/", staff.hold_ticket, name="hold"),
name='update'), path("tickets/<int:ticket_id>/unhold/",
staff.unhold_ticket, name="unhold"),
url(r'^tickets/(?P<ticket_id>[0-9]+)/delete/$', path("tickets/<int:ticket_id>/cc/", staff.ticket_cc, name="ticket_cc"),
staff.delete_ticket, path("tickets/<int:ticket_id>/cc/add/",
name='delete'), staff.ticket_cc_add, name="ticket_cc_add"),
path(
url(r'^tickets/(?P<ticket_id>[0-9]+)/hold/$', "tickets/<int:ticket_id>/cc/delete/<int:cc_id>/",
staff.hold_ticket,
name='hold'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/unhold/$',
staff.unhold_ticket,
name='unhold'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/cc/$',
staff.ticket_cc,
name='ticket_cc'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/cc/add/$',
staff.ticket_cc_add,
name='ticket_cc_add'),
url(r'^tickets/(?P<ticket_id>[0-9]+)/cc/delete/(?P<cc_id>[0-9]+)/$',
staff.ticket_cc_del, staff.ticket_cc_del,
name='ticket_cc_del'), name="ticket_cc_del",
),
url(r'^tickets/(?P<ticket_id>[0-9]+)/attachment_delete/(?P<attachment_id>[0-9]+)/$', 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>/",
staff.ticket_dependency_del,
name="ticket_dependency_del",
),
path(
"tickets/<int:ticket_id>/attachment_delete/<int:attachment_id>/",
staff.attachment_del, staff.attachment_del,
name='attachment_del'), name="attachment_del",
),
url(r'^raw/(?P<type>\w+)/$', re_path(r"^raw/(?P<type>\w+)/$", staff.raw_details, name="raw"),
staff.raw_details, path("rss/", staff.rss_list, name="rss_index"),
name='raw'), path("reports/", staff.report_index, name="report_index"),
re_path(r"^reports/(?P<report>\w+)/$",
url(r'^rss/$', staff.run_report, name="run_report"),
staff.rss_list, path("save_query/", staff.save_query, name="savequery"),
name='rss_index'), path("delete_query/<int:id>/", staff.delete_saved_query, name="delete_query"),
path("settings/", staff.EditUserSettingsView.as_view(), name="user_settings"),
url(r'^reports/$', path("ignore/", staff.email_ignore, name="email_ignore"),
staff.report_index, path("ignore/add/", staff.email_ignore_add, name="email_ignore_add"),
name='report_index'), path("ignore/delete/<int:id>/",
staff.email_ignore_del, name="email_ignore_del"),
url(r'^reports/(?P<report>\w+)/$', re_path(
staff.run_report, r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
name='run_report'),
url(r'^save_query/$',
staff.save_query,
name='savequery'),
url(r'^delete_query/(?P<id>[0-9]+)/$',
staff.delete_saved_query,
name='delete_query'),
url(r'^settings/$',
staff.EditUserSettingsView.as_view(),
name='user_settings'),
url(r'^ignore/$',
staff.email_ignore,
name='email_ignore'),
url(r'^ignore/add/$',
staff.email_ignore_add,
name='email_ignore_add'),
url(r'^ignore/delete/(?P<id>[0-9]+)/$',
staff.email_ignore_del,
name='email_ignore_del'),
url(r'^datatables_ticket_list/(?P<query>{})$'.format(base64_pattern),
staff.datatables_ticket_list, staff.datatables_ticket_list,
name="datatables_ticket_list"), name="datatables_ticket_list",
),
url(r'^timeline_ticket_list/(?P<query>{})$'.format(base64_pattern), re_path(
r"^timeline_ticket_list/(?P<query>{})$".format(base64_pattern),
staff.timeline_ticket_list, staff.timeline_ticket_list,
name="timeline_ticket_list"), name="timeline_ticket_list",
),
] ]
if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET: if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET:
urlpatterns += [ 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, staff.ticket_dependency_add,
name='ticket_dependency_add'), name="ticket_dependency_add",
),
url(r'^tickets/(?P<ticket_id>[0-9]+)/dependency/delete/(?P<dependency_id>[0-9]+)/$', re_path(
r"^tickets/(?P<ticket_id>[0-9]+)/dependency/delete/(?P<dependency_id>[0-9]+)/$",
staff.ticket_dependency_del, staff.ticket_dependency_del,
name='ticket_dependency_del'), name="ticket_dependency_del",
),
] ]
urlpatterns += [ urlpatterns += [
url(r'^$', path("", protect_view(public.Homepage.as_view()), name="home"),
protect_view(public.Homepage.as_view()), path("tickets/submit/", public.create_ticket, name="submit"),
name='home'), path(
"tickets/submit_iframe/",
url(r'^tickets/submit/$',
public.create_ticket,
name='submit'),
url(r'^tickets/submit_iframe/$',
public.CreateTicketIframeView.as_view(), public.CreateTicketIframeView.as_view(),
name='submit_iframe'), name="submit_iframe",
),
url(r'^tickets/success_iframe/$', # Ticket was submitted successfully path(
"tickets/success_iframe/", # Ticket was submitted successfully
public.SuccessIframeView.as_view(), public.SuccessIframeView.as_view(),
name='success_iframe'), name="success_iframe",
),
url(r'^view/$', path("view/", public.view_ticket, name="public_view"),
public.view_ticket, path("change_language/", public.change_language,
name='public_view'), name="public_change_language"),
url(r'^change_language/$',
public.change_language,
name='public_change_language'),
] ]
urlpatterns += [ urlpatterns += [
url(r'^rss/user/(?P<user_name>[^/]+)/$', re_path(
r"^rss/user/(?P<user_name>[a-zA-Z0-9\_\.]+)/",
helpdesk_staff_member_required(feeds.OpenTicketsByUser()), helpdesk_staff_member_required(feeds.OpenTicketsByUser()),
name='rss_user'), name="rss_user",
),
url(r'^rss/user/(?P<user_name>[^/]+)/(?P<queue_slug>[A-Za-z0-9_-]+)/$', re_path(
r"^rss/user/(?P<user_name>[^/]+)/(?P<queue_slug>[A-Za-z0-9_-]+)/$",
helpdesk_staff_member_required(feeds.OpenTicketsByUser()), helpdesk_staff_member_required(feeds.OpenTicketsByUser()),
name='rss_user_queue'), name="rss_user_queue",
),
url(r'^rss/queue/(?P<queue_slug>[A-Za-z0-9_-]+)/$', re_path(
r"^rss/queue/(?P<queue_slug>[A-Za-z0-9_-]+)/$",
helpdesk_staff_member_required(feeds.OpenTicketsByQueue()), helpdesk_staff_member_required(feeds.OpenTicketsByQueue()),
name='rss_queue'), name="rss_queue",
),
url(r'^rss/unassigned/$', path(
"rss/unassigned/",
helpdesk_staff_member_required(feeds.UnassignedTickets()), helpdesk_staff_member_required(feeds.UnassignedTickets()),
name='rss_unassigned'), name="rss_unassigned",
),
url(r'^rss/recent_activity/$', path(
"rss/recent_activity/",
helpdesk_staff_member_required(feeds.RecentFollowUps()), 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) # API is added to url conf based on the setting (False by default)
if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT: if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT:
router = DefaultRouter() router = DefaultRouter()
router.register(r'tickets', TicketViewSet, basename='ticket') router.register(r"tickets", TicketViewSet, basename="ticket")
router.register(r'users', CreateUserView, basename='user') router.register(r"followups", FollowUpViewSet, basename="followups")
urlpatterns += [ router.register(r"followups-attachments",
url(r'^api/', include(router.urls)) FollowUpAttachmentViewSet, basename="followupattachments")
] router.register(r"users", CreateUserView, basename="user")
urlpatterns += [re_path(r"^api/", include(router.urls))]
urlpatterns += [ urlpatterns += [
url(r'^login/$', path("login/", login.login, name="login"),
login.login, path(
name='login'), "logout/",
url(r'^logout/$',
auth_views.LogoutView.as_view( auth_views.LogoutView.as_view(
template_name='helpdesk/registration/login.html', template_name="helpdesk/registration/login.html", next_page="../"
next_page='../'), ),
name='logout'), name="logout",
),
url(r'^password_change/$', path(
"password_change/",
auth_views.PasswordChangeView.as_view( auth_views.PasswordChangeView.as_view(
template_name='helpdesk/registration/change_password.html', template_name="helpdesk/registration/change_password.html",
success_url='./done'), success_url="./done",
name='password_change'), ),
name="password_change",
url(r'^password_change/done$', ),
path(
"password_change/done",
auth_views.PasswordChangeDoneView.as_view( auth_views.PasswordChangeDoneView.as_view(
template_name='helpdesk/registration/change_password_done.html',), template_name="helpdesk/registration/change_password_done.html",
name='password_change_done'), ),
name="password_change_done",
),
] ]
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
urlpatterns += [ urlpatterns += [
url(r'^kb/$', path("kb/", kb.index, name="kb_index"),
kb.index, re_path(r"^kb/(?P<slug>[A-Za-z0-9_-]+)/$",
name='kb_index'), kb.category, name="kb_category"),
path("kb/<int:item>/vote/", kb.vote, name="kb_vote"),
url(r'^kb/(?P<slug>[A-Za-z0-9_-]+)/$', re_path(
kb.category, r"^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$",
name='kb_category'),
url(r'^kb/(?P<item>[0-9]+)/vote/$',
kb.vote,
name='kb_vote'),
url(r'^kb_iframe/(?P<slug>[A-Za-z0-9_-]+)/$',
kb.category_iframe, kb.category_iframe,
name='kb_category_iframe'), name="kb_category_iframe",
),
] ]
urlpatterns += [ urlpatterns += [
url(r'^help/context/$', path(
TemplateView.as_view(template_name='helpdesk/help_context.html'), "help/context/",
name='help_context'), TemplateView.as_view(template_name="helpdesk/help_context.html"),
name="help_context",
url(r'^system_settings/$', ),
login_required(DirectTemplateView.as_view(template_name='helpdesk/system_settings.html')), path(
name='system_settings'), "system_settings/",
login_required(
DirectTemplateView.as_view(
template_name="helpdesk/system_settings.html")
),
name="system_settings",
),
] ]

View File

@ -1,15 +1,11 @@
from helpdesk.models import (
Ticket,
Queue
)
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import Queue, Ticket
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import ( from helpdesk.models import KBCategory, KBItem
KBCategory,
KBItem
)
def huser_from_request(req): def huser_from_request(req):
return HelpdeskUser(req.user) return HelpdeskUser(req.user)
@ -33,7 +29,8 @@ class HelpdeskUser:
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \ helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
and not user.is_superuser and not user.is_superuser
if limit_queues_by_user: if limit_queues_by_user:
id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)] id_list = [q.pk for q in all_queues if user.has_perm(
q.permission_name)]
id_list += public_ids id_list += public_ids
return all_queues.filter(pk__in=id_list) return all_queues.filter(pk__in=id_list)
else: else:

View File

@ -2,13 +2,18 @@
# #
# validators for file uploads, etc. # validators for file uploads, etc.
from django.conf import settings 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): def validate_file_extension(value):
import os
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import os
ext = os.path.splitext(value.name)[1] # [0] returns path+filename ext = os.path.splitext(value.name)[1] # [0] returns path+filename
# TODO: we might improve this with more thorough checks of file types # TODO: we might improve this with more thorough checks of file types
# rather than just the extensions. # rather than just the extensions.
@ -19,9 +24,12 @@ def validate_file_extension(value):
if hasattr(settings, 'VALID_EXTENSIONS'): if hasattr(settings, 'VALID_EXTENSIONS'):
valid_extensions = settings.VALID_EXTENSIONS valid_extensions = settings.VALID_EXTENSIONS
else: else:
valid_extensions = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] valid_extensions = ['.txt', '.asc', '.htm', '.html',
'.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']
if not ext.lower() in valid_extensions: if not ext.lower() in valid_extensions:
# TODO: one more check in case it is a file with no extension; we should always allow that? # TODO: one more check in case it is a file with no extension; we
# should always allow that?
if not (ext.lower() == '' or ext.lower() == '.'): if not (ext.lower() == '' or ext.lower() == '.'):
raise ValidationError('Unsupported file extension: %s.' % ext.lower()) raise ValidationError(
'Unsupported file extension: %s.' % ext.lower())

View File

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

View File

@ -1,11 +1,10 @@
from django.contrib.auth import get_user_model
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket
from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.viewsets import GenericViewSet 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
class TicketViewSet(viewsets.ModelViewSet): class TicketViewSet(viewsets.ModelViewSet):
@ -28,6 +27,18 @@ class TicketViewSet(viewsets.ModelViewSet):
return ticket 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): class CreateUserView(CreateModelMixin, GenericViewSet):
queryset = get_user_model().objects.all() queryset = get_user_model().objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer

View File

@ -9,12 +9,12 @@ views/feeds.py - A handful of staff-only RSS feeds to provide ticket details
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.urls import reverse
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext as _
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext as _
from helpdesk.models import FollowUp, Queue, Ticket
from helpdesk.models import Ticket, FollowUp, Queue
User = get_user_model() User = get_user_model()
@ -123,7 +123,8 @@ class RecentFollowUps(Feed):
description_template = 'helpdesk/rss/recent_activity_description.html' description_template = 'helpdesk/rss/recent_activity_description.html'
title = _('Helpdesk: Recent Followups') title = _('Helpdesk: Recent Followups')
description = _('Recent FollowUps, such as e-mail replies, comments, attachments and resolutions') description = _(
'Recent FollowUps, such as e-mail replies, comments, attachments and resolutions')
link = '/tickets/' # reverse('helpdesk:list') link = '/tickets/' # reverse('helpdesk:list')
def items(self): def items(self):

View File

@ -8,12 +8,10 @@ views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
resolutions to common problems. resolutions to common problems.
""" """
from django.http import HttpResponseRedirect, Http404 from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404 from django.shortcuts import get_object_or_404, render
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from helpdesk import settings as helpdesk_settings, user
from helpdesk import settings as helpdesk_settings
from helpdesk import user
from helpdesk.models import KBCategory, KBItem from helpdesk.models import KBCategory, KBItem

View File

@ -1,5 +1,4 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff

View File

@ -6,30 +6,29 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
views/public.py - All public facing views, eg non-staff (no authentication views/public.py - All public facing views, eg non-staff (no authentication
required) views. required) views.
""" """
import logging
from importlib import import_module
from django.core.exceptions import (
ObjectDoesNotExist, PermissionDenied, ImproperlyConfigured, from django.conf import settings
) from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
from django.urls import reverse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.utils.http import urlquote from django.urls import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from django.conf import settings
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.decorators import protect_view, is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff, protect_view
import helpdesk.views.staff as staff
import helpdesk.views.abstract_views as abstract_views
from helpdesk.lib import text_is_spam from helpdesk.lib import text_is_spam
from helpdesk.models import Ticket, Queue, UserSettings from helpdesk.models import Queue, Ticket, UserSettings
from helpdesk.user import huser_from_request from helpdesk.user import huser_from_request
import helpdesk.views.abstract_views as abstract_views
import helpdesk.views.staff as staff
from importlib import import_module
import logging
from urllib.parse import quote
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -45,7 +44,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
def get_form_class(self): def get_form_class(self):
try: try:
the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(".", 1) the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(
".", 1)
the_module = import_module(the_module) the_module = import_module(the_module)
the_form_class = getattr(the_module, the_form_class) the_form_class = getattr(the_module, the_form_class)
except Exception as e: except Exception as e:
@ -87,7 +87,8 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
"Public queue '%s' is configured as default but can't be found", "Public queue '%s' is configured as default but can't be found",
settings.HELPDESK_PUBLIC_TICKET_QUEUE settings.HELPDESK_PUBLIC_TICKET_QUEUE
) )
raise ImproperlyConfigured("Wrong public queue configuration") from e raise ImproperlyConfigured(
"Wrong public queue configuration") from e
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'): if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY'):
initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY initial_data['priority'] = settings.HELPDESK_PUBLIC_TICKET_PRIORITY
if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'): if hasattr(settings, 'HELPDESK_PUBLIC_TICKET_DUE_DATE'):
@ -97,8 +98,10 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
def get_form_kwargs(self, *args, **kwargs): def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs) kwargs = super().get_form_kwargs(*args, **kwargs)
if '_hide_fields_' in self.request.GET: if '_hide_fields_' in self.request.GET:
kwargs['hidden_fields'] = self.request.GET.get('_hide_fields_', '').split(',') kwargs['hidden_fields'] = self.request.GET.get(
kwargs['readonly_fields'] = self.request.GET.get('_readonly_fields_', '').split(',') '_hide_fields_', '').split(',')
kwargs['readonly_fields'] = self.request.GET.get(
'_readonly_fields_', '').split(',')
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
@ -107,12 +110,13 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
# This submission is spam. Let's not save it. # This submission is spam. Let's not save it.
return render(request, template_name='helpdesk/public_spam.html') return render(request, template_name='helpdesk/public_spam.html')
else: else:
ticket = form.save(user=self.request.user if self.request.user.is_authenticated else None) ticket = form.save(
user=self.request.user if self.request.user.is_authenticated else None)
try: try:
return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % ( return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % (
reverse('helpdesk:public_view'), reverse('helpdesk:public_view'),
ticket.ticket_for_url, ticket.ticket_for_url,
urlquote(ticket.submitter_email), quote(ticket.submitter_email),
ticket.secret_key) ticket.secret_key)
) )
except ValueError: except ValueError:
@ -146,7 +150,8 @@ class CreateTicketView(BaseCreateTicketView):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
# Add the CSS error class to the form in order to better see them in the page # Add the CSS error class to the form in order to better see them in
# the page
form.error_css_class = 'text-danger' form.error_css_class = 'text-danger'
return form return form
@ -156,7 +161,8 @@ class Homepage(CreateTicketView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['kb_categories'] = huser_from_request(self.request).get_allowed_kb_categories() context['kb_categories'] = huser_from_request(
self.request).get_allowed_kb_categories()
return context return context
@ -170,7 +176,8 @@ def search_for_ticket(request, error_message=None):
'helpdesk_settings': helpdesk_settings, 'helpdesk_settings': helpdesk_settings,
}) })
else: else:
raise PermissionDenied("Public viewing of tickets without a secret key is forbidden.") raise PermissionDenied(
"Public viewing of tickets without a secret key is forbidden.")
@protect_view @protect_view
@ -188,9 +195,11 @@ def view_ticket(request):
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req) queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
try: try:
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email) ticket = Ticket.objects.get(
id=ticket_id, submitter_email__iexact=email)
else: else:
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key) ticket = Ticket.objects.get(
id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key)
except (ObjectDoesNotExist, ValueError): except (ObjectDoesNotExist, ValueError):
return search_for_ticket(request, _('Invalid ticket ID or e-mail address. Please try again.')) return search_for_ticket(request, _('Invalid ticket ID or e-mail address. Please try again.'))
@ -202,6 +211,7 @@ def view_ticket(request):
if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS: if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
from helpdesk.views.staff import update_ticket from helpdesk.views.staff import update_ticket
# Trick the update_ticket() view into thinking it's being called with # Trick the update_ticket() view into thinking it's being called with
# a valid POST. # a valid POST.
request.POST = { request.POST = {

File diff suppressed because it is too large Load Diff

39
quicktest.py Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env python
""" """
Usage: Usage:
$ python -m venv .venv $ python -m venv .venv
@ -5,15 +6,15 @@ $ source .venv/bin/activate
$ pip install -r requirements-testing.txt -r requirements.txt $ pip install -r requirements-testing.txt -r requirements.txt
$ python ./quicktest.py $ python ./quicktest.py
""" """
import os
import sys
import argparse
import argparse
import django import django
from django.conf import settings from django.conf import settings
import os
import sys
class QuickDjangoTest(object): class QuickDjangoTest:
""" """
A quick way to run the Django test suite without a fully-configured project. A quick way to run the Django test suite without a fully-configured project.
@ -35,14 +36,14 @@ class QuickDjangoTest(object):
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'bootstrap4form', 'bootstrap4form',
## The following commented apps are optional, # The following commented apps are optional,
## related to teams functionalities # related to teams functionalities
#'account', # 'account',
#'pinax.invitations', # 'pinax.invitations',
#'pinax.teams', # 'pinax.teams',
'rest_framework', 'rest_framework',
'helpdesk', 'helpdesk',
#'reversion', # 'reversion',
) )
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
@ -77,6 +78,7 @@ class QuickDjangoTest(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.tests = args self.tests = args
self.kwargs = kwargs or {"verbosity": 1}
self._tests() self._tests()
def _tests(self): def _tests(self):
@ -98,20 +100,20 @@ class QuickDjangoTest(object):
MIDDLEWARE=self.MIDDLEWARE, MIDDLEWARE=self.MIDDLEWARE,
ROOT_URLCONF='helpdesk.tests.urls', ROOT_URLCONF='helpdesk.tests.urls',
STATIC_URL='/static/', STATIC_URL='/static/',
LOGIN_URL='/helpdesk/login/', LOGIN_URL='/login/',
TEMPLATES=self.TEMPLATES, TEMPLATES=self.TEMPLATES,
SITE_ID=1, SITE_ID=1,
SECRET_KEY='wowdonotusethisfakesecuritykeyyouneedarealsecure1', SECRET_KEY='wowdonotusethisfakesecuritykeyyouneedarealsecure1',
## The following settings disable teams # The following settings disable teams
HELPDESK_TEAMS_MODEL = 'auth.User', HELPDESK_TEAMS_MODEL='auth.User',
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [], HELPDESK_TEAMS_MIGRATION_DEPENDENCIES=[],
HELPDESK_KBITEM_TEAM_GETTER = lambda _: None, HELPDESK_KBITEM_TEAM_GETTER=lambda _: None,
## test the API # test the API
HELPDESK_ACTIVATE_API_ENDPOINT=True HELPDESK_ACTIVATE_API_ENDPOINT=True
) )
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner
test_runner = DiscoverRunner(verbosity=1) test_runner = DiscoverRunner(verbosity=self.kwargs["verbosity"])
django.setup() django.setup()
failures = test_runner.run_tests(self.tests) failures = test_runner.run_tests(self.tests)
@ -133,7 +135,8 @@ if __name__ == '__main__':
description="Run Django tests." description="Run Django tests."
) )
parser.add_argument('tests', nargs="*", type=str) parser.add_argument('tests', nargs="*", type=str)
parser.add_argument("--verbosity", "-v", nargs="?", type=int, default=1)
args = parser.parse_args() args = parser.parse_args()
if not args.tests: if not args.tests:
args.tests = ['helpdesk'] args.tests = ['helpdesk']
QuickDjangoTest(*args.tests) QuickDjangoTest(*args.tests, verbosity=args.verbosity)

View File

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

View File

@ -1,7 +1,6 @@
Django>=2.2,<4 Django>=2.2
django-bootstrap4-form django-bootstrap4-form
celery celery
django-celery-beat
email-reply-parser email-reply-parser
akismet akismet
markdown markdown
@ -13,3 +12,4 @@ six
pinax_teams pinax_teams
djangorestframework djangorestframework
django-model-utils django-model-utils
django-cleanup

View File

@ -1,28 +1,42 @@
import os """django-helpdesk setup"""
import sys
from distutils.util import convert_path from distutils.util import convert_path
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from setuptools import setup, find_packages import os
from setuptools import find_packages, setup
import sys
version = '0.5.0a1'
version = '0.3.5'
# Provided as an attribute, so you can append to these instead # Provided as an attribute, so you can append to these instead
# of replicating them: # of replicating them:
standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak') standard_exclude = ("*.py", "*.pyc", "*$py.class", "*~", ".*", "*.bak")
standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', standard_exclude_directories = (
'./dist', 'EGG-INFO', '*.egg-info') ".*",
"CVS",
"_darcs",
"./build",
"./dist",
"EGG-INFO",
"*.egg-info",
)
# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
# Note: you may want to copy this into your setup.py file verbatim, as # 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 # you can't import this from another package, when you don't know if
# that package is installed yet. # that package is installed yet.
def find_package_data( def find_package_data(
where='.', package='', where=".",
package="",
exclude=standard_exclude, exclude=standard_exclude,
exclude_directories=standard_exclude_directories, exclude_directories=standard_exclude_directories,
only_in_packages=True, only_in_packages=True,
show_ignored=False): show_ignored=False,
):
""" """
Return a dictionary suitable for use in ``package_data`` Return a dictionary suitable for use in ``package_data``
in a distutils ``setup.py`` file. in a distutils ``setup.py`` file.
@ -51,7 +65,7 @@ def find_package_data(
""" """
out = {} out = {}
stack = [(convert_path(where), '', package, only_in_packages)] stack = [(convert_path(where), "", package, only_in_packages)]
while stack: while stack:
where, prefix, package, only_in_packages = stack.pop(0) where, prefix, package, only_in_packages = stack.pop(0)
for name in os.listdir(where): for name in os.listdir(where):
@ -59,43 +73,43 @@ def find_package_data(
if os.path.isdir(fn): if os.path.isdir(fn):
bad_name = False bad_name = False
for pattern in exclude_directories: for pattern in exclude_directories:
if (fnmatchcase(name, pattern) if fnmatchcase(name, pattern) or fn.lower() == pattern.lower():
or fn.lower() == pattern.lower()):
bad_name = True bad_name = True
if show_ignored: if show_ignored:
print( print(
"Directory %s ignored by pattern %s" % (fn, pattern), "Directory %s ignored by pattern %s" % (
file=sys.stderr fn, pattern),
file=sys.stderr,
) )
break break
if bad_name: if bad_name:
continue continue
if (os.path.isfile(os.path.join(fn, '__init__.py')) if os.path.isfile(os.path.join(fn, "__init__.py")) and not prefix:
and not prefix):
if not package: if not package:
new_package = name new_package = name
else: else:
new_package = package + '.' + name new_package = package + "." + name
stack.append((fn, '', new_package, False)) stack.append((fn, "", new_package, False))
else: 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: elif package or not only_in_packages:
# is a file # is a file
bad_name = False bad_name = False
for pattern in exclude: for pattern in exclude:
if (fnmatchcase(name, pattern) if fnmatchcase(name, pattern) or fn.lower() == pattern.lower():
or fn.lower() == pattern.lower()):
bad_name = True bad_name = True
if show_ignored: if show_ignored:
print( print(
"File %s ignored by pattern %s" % (fn, pattern), "File %s ignored by pattern %s" % (
file=sys.stderr fn, pattern),
) file=sys.stderr,
)
break break
if bad_name: if bad_name:
continue continue
out.setdefault(package, []).append(prefix+name) out.setdefault(package, []).append(prefix + name)
return out return out
@ -116,7 +130,7 @@ def get_long_description():
setup( setup(
name='django-helpdesk', name="django-helpdesk",
version=version, version=version,
description="Django-powered ticket tracker for your helpdesk", description="Django-powered ticket tracker for your helpdesk",
long_description=get_long_description(), long_description=get_long_description(),
@ -128,8 +142,8 @@ setup(
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Framework :: Django", "Framework :: Django",
'Framework :: Django :: 2.2',
"Framework :: Django :: 3.2", "Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Environment :: Web Environment", "Environment :: Web Environment",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Intended Audience :: Customer Service", "Intended Audience :: Customer Service",
@ -139,18 +153,26 @@ setup(
"Topic :: Office/Business", "Topic :: Office/Business",
"Natural Language :: English", "Natural Language :: English",
], ],
keywords=['django', 'helpdesk', 'django-helpdesk', 'tickets', 'incidents', keywords=[
'cases', 'bugs', 'track', 'support'], "django",
author='Ross Poulton', "helpdesk",
author_email='ross@rossp.org', "django-helpdesk",
maintainer='Garret Wassermann', "tickets",
maintainer_email='gwasser@gmail.com', "incidents",
url='https://github.com/django-helpdesk/django-helpdesk', "cases",
license='BSD', "bugs",
"track",
"support",
],
author="Ross Poulton",
author_email="ross@rossp.org",
maintainer="Garret Wassermann",
maintainer_email="gwasser@gmail.com",
url="https://github.com/django-helpdesk/django-helpdesk",
license="BSD",
packages=find_packages(), packages=find_packages(),
package_data=find_package_data("helpdesk", only_in_packages=False), package_data=find_package_data("helpdesk", only_in_packages=False),
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=get_requirements(), install_requires=get_requirements(),
) )

9
tox.ini Normal file
View File

@ -0,0 +1,9 @@
[tox]
minversion = 3.25.1
requires = pytest
freezegun
[testenv:release]
commands = pip install -r requirements-testing.txt
python quicktest.py