forked from extern/django-helpdesk
Merge branch 'unstable' into patch-1
This commit is contained in:
commit
318417f097
8
.flake8
8
.flake8
@ -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
42
.github/workflows/pythonpackage.yml
vendored
Normal 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
19
.isort.cfg
Normal 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
|
||||||
|
|
@ -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`::
|
||||||
|
@ -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'
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
Django >=2.2,<3
|
|
||||||
|
|
1
constraints-Django4.txt
Normal file
1
constraints-Django4.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Django >=4,<5
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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']
|
||||||
}
|
}
|
||||||
|
53
docs/api.rst
53
docs/api.rst
@ -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.
|
||||||
|
16
docs/conf.py
16
docs/conf.py
@ -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).
|
||||||
|
@ -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')),
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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')
|
||||||
|
@ -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'),
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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" %} »</label>
|
<label for='st_reopened' class='active radio-inline'><input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'>{% trans "Reopened" %} »</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" %} »</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" %} »</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" %} »</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" %} »</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" %} «</label>
|
<label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} «</label>
|
||||||
<label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} »</label>
|
<label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} »</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" %} «</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" %} «</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" %} «</label>
|
<label class="radio-inline" for='st_reopened'><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} «</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'>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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" %} »</label>
|
<label for='st_open' class='active radio-inline'><input type='radio' name='new_status' value='1' id='st_open' checked='checked'>{% trans "Open" %} »</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" %} »</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" %} »</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" %} »</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" %} »</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" %} »</label>
|
<label for='st_reopened' class='active radio-inline'><input type='radio' name='new_status' value='2' id='st_reopened' checked='checked'>{% trans "Reopened" %} »</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" %} »</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" %} »</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" %} »</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" %} »</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" %} «</label>
|
<label for='st_reopened' class="radio-inline"><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} «</label>
|
||||||
<label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} »</label>
|
<label for='st_resolved' class='active radio-inline'><input type='radio' name='new_status' value='3' id='st_resolved' checked='checked'>{% trans "Resolved" %} »</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" %} «</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" %} «</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" %} «</label>
|
<label class="radio-inline" for='st_reopened'><input type='radio' name='new_status' value='2' id='st_reopened'>{% trans "Reopened" %} «</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>
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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&title=lol")
|
self.assertContains(
|
||||||
|
response, "'/tickets/submit/?queue=1&_readonly_fields_=queue&kbitem=1&submitter_email=foo%40bar.cz&title=lol")
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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])
|
||||||
|
@ -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)
|
||||||
|
@ -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">')
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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),
|
||||||
]
|
]
|
||||||
|
356
helpdesk/urls.py
356
helpdesk/urls.py
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -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:
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
39
quicktest.py
Normal file → Executable 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)
|
||||||
|
@ -5,3 +5,5 @@ coverage
|
|||||||
argparse
|
argparse
|
||||||
pbr
|
pbr
|
||||||
mock
|
mock
|
||||||
|
freezegun
|
||||||
|
isort
|
||||||
|
@ -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
|
||||||
|
94
setup.py
94
setup.py
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user