diff --git a/.flake8 b/.flake8
index 0daf1543..80233cdf 100644
--- a/.flake8
+++ b/.flake8
@@ -2,3 +2,11 @@
max-line-length = 120
exclude = .git,__pycache__,.tox,.eggs,*.egg,node_modules,.venv,migrations,docs,demo,tests,setup.py
import-order-style = pep8
+max-complexity = 20
+
+[pycodestyle]
+max-line-length = 120
+exclude = "migrations"
+in-place = true
+recursive = true
+
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
new file mode 100644
index 00000000..70b2482d
--- /dev/null
+++ b/.github/workflows/pythonpackage.yml
@@ -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
+
diff --git a/.isort.cfg b/.isort.cfg
new file mode 100644
index 00000000..d854b4fc
--- /dev/null
+++ b/.isort.cfg
@@ -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
+
diff --git a/README.rst b/README.rst
index c592559f..d7cb3237 100644
--- a/README.rst
+++ b/README.rst
@@ -1,13 +1,12 @@
django-helpdesk - A Django powered ticket tracker for small businesses.
=======================================================================
-.. image:: https://travis-ci.org/django-helpdesk/django-helpdesk.png?branch=develop
- :target: https://travis-ci.org/django-helpdesk/django-helpdesk
+[![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)
.. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/django-helpdesk/django-helpdesk
-Copyright 2009-2021 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
+Copyright 2009-2022 Ross Poulton and django-helpdesk contributors. All Rights Reserved.
See LICENSE for details.
django-helpdesk was formerly known as Jutda Helpdesk, named after the
@@ -53,7 +52,7 @@ Installation
`django-helpdesk` requires:
* 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`
app via `pip`::
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 214c9b74..2cc53fdd 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -4,26 +4,19 @@
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
trigger:
-- master
+- unstable
+- stable
- 0.3
pr:
-- master
+- unstable
+- stable
- 0.3
pool:
vmImage: ubuntu-latest
strategy:
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:
PYTHON_VERSION: '3.8'
DJANGO_VERSION: '32'
@@ -33,7 +26,16 @@ strategy:
Python310Django32:
PYTHON_VERSION: '3.10'
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:
- task: UsePythonVersion@0
@@ -42,29 +44,29 @@ steps:
architecture: 'x64'
- task: PythonScript@0
- displayName: 'Export project path'
+ displayName: 'Export quicktest.py path'
inputs:
scriptSource: 'inline'
script: |
- """Search all subdirectories for `manage.py`."""
+ """Search all subdirectories for `quicktest.py`."""
from glob import iglob
from os import path
# Python >= 3.5
- manage_py = next(iglob(path.join('**', 'manage.py'), recursive=True), None)
- if not manage_py:
- raise SystemExit('Could not find a Django project')
- project_location = path.dirname(path.abspath(manage_py))
- print('Found Django project in', project_location)
+ quicktest_py = next(iglob(path.join('**', 'quicktest.py'), recursive=True), None)
+ if not quicktest_py:
+ raise SystemExit('Could not find quicktest.py for django-helpdesk')
+ project_location = path.dirname(path.abspath(quicktest_py))
+ print('Found quicktest.py in', project_location)
print('##vso[task.setvariable variable=projectRoot]{}'.format(project_location))
- script: |
python -m pip install --upgrade pip setuptools wheel
pip install -c constraints-Django$(DJANGO_VERSION).txt -r requirements.txt
+ pip install -c constraints-Django$(DJANGO_VERSION).txt -r requirements-testing.txt
pip install unittest-xml-reporting
displayName: 'Install prerequisites'
- script: |
pushd '$(projectRoot)'
- #python manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input
python quicktest.py helpdesk
displayName: 'Run tests'
diff --git a/constraints-Django22.txt b/constraints-Django22.txt
deleted file mode 100644
index 1a728bf4..00000000
--- a/constraints-Django22.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Django >=2.2,<3
-
diff --git a/constraints-Django4.txt b/constraints-Django4.txt
new file mode 100644
index 00000000..1643cbe5
--- /dev/null
+++ b/constraints-Django4.txt
@@ -0,0 +1 @@
+Django >=4,<5
diff --git a/demo/demodesk/config/settings.py b/demo/demodesk/config/settings.py
index ec301a91..55ab94f2 100644
--- a/demo/demodesk/config/settings.py
+++ b/demo/demodesk/config/settings.py
@@ -8,8 +8,10 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
+
import os
+
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
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.
HELPDESK_DEFAULT_SETTINGS = {
- 'use_email_as_submitter': True,
- 'email_on_ticket_assign': True,
- 'email_on_ticket_change': True,
- 'login_view_ticketlist': True,
- 'email_on_ticket_apichange': True,
- 'preset_replies': True,
- 'tickets_per_page': 25
+ 'use_email_as_submitter': True,
+ 'email_on_ticket_assign': True,
+ 'email_on_ticket_change': True,
+ 'login_view_ticketlist': True,
+ 'email_on_ticket_apichange': True,
+ 'preset_replies': True,
+ 'tickets_per_page': 25
}
# Should the public web portal be enabled?
@@ -153,7 +155,7 @@ SITE_ID = 1
# Sessions
# https://docs.djangoproject.com/en/1.11/topics/http/sessions
-SESSION_COOKIE_AGE = 86400 # = 1 day
+SESSION_COOKIE_AGE = 86400 # = 1 day
# For better default security, set these cookie flags, but
# these are likely to cause problems when testing locally
diff --git a/demo/demodesk/config/urls.py b/demo/demodesk/config/urls.py
index 52f6f407..2fb2a3d7 100644
--- a/demo/demodesk/config/urls.py
+++ b/demo/demodesk/config/urls.py
@@ -13,10 +13,10 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
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.urls.static import static
+from django.contrib import admin
+from django.urls import include, path
# 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/
urlpatterns = [
- url(r'^admin/', admin.site.urls),
- url(r'^', include('helpdesk.urls', namespace='helpdesk')),
- url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework'))
+ path('admin/', admin.site.urls),
+ path('', include('helpdesk.urls', namespace='helpdesk')),
+ path('api/auth/', include('rest_framework.urls', namespace='rest_framework'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/demo/demodesk/config/wsgi.py b/demo/demodesk/config/wsgi.py
index f8e39f59..3fb82a46 100644
--- a/demo/demodesk/config/wsgi.py
+++ b/demo/demodesk/config/wsgi.py
@@ -7,9 +7,10 @@ For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
-import os
from django.core.wsgi import get_wsgi_application
+import os
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
diff --git a/demo/demodesk/manage.py b/demo/demodesk/manage.py
index 3427b7bc..0bc07500 100755
--- a/demo/demodesk/manage.py
+++ b/demo/demodesk/manage.py
@@ -2,6 +2,7 @@
import os
import sys
+
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
try:
@@ -21,5 +22,6 @@ def main():
raise
execute_from_command_line(sys.argv)
+
if __name__ == "__main__":
main()
diff --git a/demo/manage.py b/demo/manage.py
index 3427b7bc..0bc07500 100755
--- a/demo/manage.py
+++ b/demo/manage.py
@@ -2,6 +2,7 @@
import os
import sys
+
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
try:
@@ -21,5 +22,6 @@ def main():
raise
execute_from_command_line(sys.argv)
+
if __name__ == "__main__":
main()
diff --git a/demo/setup.py b/demo/setup.py
index 35822723..bacc93a2 100644
--- a/demo/setup.py
+++ b/demo/setup.py
@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
"""Python packaging."""
-
-from __future__ import unicode_literals
-
-from setuptools import setup
import os
+from setuptools import setup
+
here = os.path.abspath(os.path.dirname(__file__))
project_root = os.path.dirname(here)
@@ -13,7 +11,7 @@ project_root = os.path.dirname(here)
NAME = 'django-helpdesk-demodesk'
DESCRIPTION = 'A demo Django project using django-helpdesk'
README = open(os.path.join(here, 'README.rst')).read()
-VERSION = '0.3.5'
+VERSION = '0.5.0a1'
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
AUTHOR = 'django-helpdesk team'
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.9',
'Programming Language :: Python :: 3.10',
- 'Framework :: Django :: 2.2',
- 'Framework :: Django :: 3.2']
+ 'Framework :: Django :: 3.2',
+ 'Framework :: Django :: 4.0']
KEYWORDS = []
PACKAGES = ['demodesk']
REQUIREMENTS = [
'django-helpdesk'
- ]
+]
ENTRY_POINTS = {
'console_scripts': ['demodesk = demodesk.manage:main']
}
diff --git a/docs/api.rst b/docs/api.rst
index 234d4a5b..77e4b587 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -1,20 +1,25 @@
API
===
-A REST API (built with ``djangorestframework``) is available in order to list, create, update and delete tickets from other tools thanks to HTTP requests.
+A REST API (built with ``djangorestframework``) is available in order to list, create, update and delete tickets from
+other tools thanks to HTTP requests.
If you wish to use it, you have to add this line in your settings::
HELPDESK_ACTIVATE_API_ENDPOINT = True
-You must be authenticated to access the API, the URL endpoint is ``/api/tickets/``. You can configure how you wish to authenticate to the API by customizing the ``DEFAULT_AUTHENTICATION_CLASSES`` key in the ``REST_FRAMEWORK`` setting (more information on this page : https://www.django-rest-framework.org/api-guide/authentication/)
+You must be authenticated to access the API, the URL endpoint is ``/api/tickets/``.
+You can configure how you wish to authenticate to the API by customizing the ``DEFAULT_AUTHENTICATION_CLASSES`` key
+in the ``REST_FRAMEWORK`` setting (more information on this page : https://www.django-rest-framework.org/api-guide/authentication/)
GET
---
-Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the complete list of tickets.
+Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the complete list of tickets with their
+followups and their attachment files.
-Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the data of the ticket you provided the ID.
+Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the data of the ticket you
+provided the ID.
POST
----
@@ -35,7 +40,8 @@ You need to provide a JSON body with the following data :
- **due_date**: date representation for when the ticket is due
- **merged_to**: ID of the ticket to which it is merged
-Note that ``status`` will automatically be set to OPEN. Also, some fields are not configurable during creation: ``resolution``, ``on_hold`` and ``merged_to``.
+Note that ``status`` will automatically be set to OPEN. Also, some fields are not configurable during creation:
+``resolution``, ``on_hold`` and ``merged_to``.
Moreover, if you created custom fields, you can add them into the body with the key ``custom_``.
@@ -46,6 +52,34 @@ Here is an example of a cURL request to create a ticket (using Basic authenticat
--header 'Content-Type: application/json' \
--data-raw '{"queue": 1, "title": "Test Ticket API", "description": "Test create ticket from API", "submitter_email": "test@mail.com", "priority": 4}'
+Note that you can attach one file as attachment but in this case, you cannot use JSON for the request content type.
+Here is an example with form-data (curl default) ::
+
+ curl --location --request POST 'http://127.0.0.1:8000/api/tickets/' \
+ --header 'Authorization: Basic YWRtaW46YWRtaW4=' \
+ --form 'queue="1"' \
+ --form 'title="Test Ticket API with attachment"' \
+ --form 'description="Test create ticket from API avec attachment"' \
+ --form 'submitter_email="test@mail.com"' \
+ --form 'priority="2"' \
+ --form 'attachment=@"/C:/Users/benbb96/Documents/file.txt"'
+
+----
+
+Accessing the endpoint ``/api/followups/`` with a **POST** request will let you create a new followup on a ticket.
+
+This time, you can attach multiple files thanks to the `attachments` field. Here is an example ::
+
+ curl --location --request POST 'http://127.0.0.1:8000/api/followups/' \
+ --header 'Authorization: Basic YWRtaW46YWRtaW4=' \
+ --form 'ticket="44"' \
+ --form 'title="Test ticket answer"' \
+ --form 'comment="This answer contains multiple files as attachment."' \
+ --form 'attachments=@"/C:/Users/benbb96/Documents/doc.pdf"' \
+ --form 'attachments=@"/C:/Users/benbb96/Documents/image.png"'
+
+----
+
Accessing the endpoint ``/api/users/`` with a **POST** request will let you create a new user.
You need to provide a JSON body with the following data :
@@ -59,18 +93,21 @@ You need to provide a JSON body with the following data :
PUT
---
-Accessing the endpoint ``/api/tickets/`` with a **PUT** request will let you update the data of the ticket you provided the ID.
+Accessing the endpoint ``/api/tickets/`` with a **PUT** request will let you update the data of the ticket
+you provided the ID.
You must include all fields in the JSON body.
PATCH
-----
-Accessing the endpoint ``/api/tickets/`` 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/`` with a **PATCH** request will let you do a partial update of the
+data of the ticket you provided the ID.
You can include only the fields you need to update in the JSON body.
DELETE
------
-Accessing the endpoint ``/api/tickets/`` with a **DELETE** request will let you delete the ticket you provided the ID.
+Accessing the endpoint ``/api/tickets/`` with a **DELETE** request will let you delete the ticket you
+provided the ID.
diff --git a/docs/conf.py b/docs/conf.py
index 3d4ce533..f79715bb 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -11,14 +11,16 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os
+import os
+import sys
+
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
-# -- General configuration -----------------------------------------------------
+# -- General configuration -----------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
@@ -87,7 +89,7 @@ pygments_style = 'sphinx'
#modindex_common_prefix = []
-# -- Options for HTML output ---------------------------------------------------
+# -- Options for HTML output ---------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
@@ -167,7 +169,7 @@ html_static_path = ['_static']
htmlhelp_basename = 'django-helpdeskdoc'
-# -- Options for LaTeX output --------------------------------------------------
+# -- Options for LaTeX output --------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
@@ -178,8 +180,8 @@ htmlhelp_basename = 'django-helpdeskdoc'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index', 'django-helpdesk.tex', u'django-helpdesk Documentation',
- u'Ross Poulton + django-helpdesk Contributors', 'manual'),
+ ('index', 'django-helpdesk.tex', u'django-helpdesk Documentation',
+ u'Ross Poulton + django-helpdesk Contributors', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -206,7 +208,7 @@ latex_documents = [
#latex_domain_indices = True
-# -- Options for manual page output --------------------------------------------
+# -- Options for manual page output --------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
diff --git a/docs/install.rst b/docs/install.rst
index 6b34c3d4..249645d8 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -10,7 +10,7 @@ Prerequisites
Before getting started, ensure your system meets the following recommended dependencies:
* 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.
@@ -63,6 +63,7 @@ errors with trying to create User settings.
'pinax.teams', # Team support
'reversion', # Required by pinax-teams
'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!
)
@@ -74,11 +75,12 @@ errors with trying to create User settings.
SITE_ID = 1
-2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following line to ``urls.py``::
+2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following lines to ``urls.py``::
+ from django.conf.urls import include
path('helpdesk/', include('helpdesk.urls')),
- Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the line will be as follows::
+ Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the path line will be as follows::
path('', include('helpdesk.urls', namespace='helpdesk')),
diff --git a/docs/license.rst b/docs/license.rst
index e87f414f..bfeadd50 100644
--- a/docs/license.rst
+++ b/docs/license.rst
@@ -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.::
Copyright (c) 2008, Ross Poulton (Trading as Jutda)
+ Copyright (c) 2008-2021, django-helpdesk contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
diff --git a/docs/upgrade.rst b/docs/upgrade.rst
index 89e24ef4..1fd3d00f 100644
--- a/docs/upgrade.rst
+++ b/docs/upgrade.rst
@@ -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.
+0.3 -> 0.4
+----------
+
+- Under `INSTALLED_APPS`, `bootstrap4form` needs to be replaced with `bootstrap5form`
+
+
0.2 -> 0.3
----------
diff --git a/helpdesk/admin.py b/helpdesk/admin.py
index ad53346a..d51cf209 100644
--- a/helpdesk/admin.py
+++ b/helpdesk/admin.py
@@ -1,13 +1,25 @@
+
from django.contrib import admin
-from django.utils.translation import ugettext_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 django.utils.translation import gettext_lazy as _
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:
- from helpdesk.models import KBCategory
- from helpdesk.models import KBItem
+ from helpdesk.models import KBCategory, KBItem
+
@admin.register(Queue)
class QueueAdmin(admin.ModelAdmin):
@@ -74,15 +86,17 @@ class FollowUpAdmin(admin.ModelAdmin):
if helpdesk_settings.HELPDESK_KB_ENABLED:
@admin.register(KBItem)
class KBItemAdmin(admin.ModelAdmin):
- list_display = ('category', 'title', 'last_updated', 'team', 'order', 'enabled')
+ list_display = ('category', 'title', 'last_updated',
+ 'team', 'order', 'enabled')
inlines = [KBIAttachmentInline]
readonly_fields = ('voted_by', 'downvoted_by')
list_display_links = ('title',)
- @admin.register(KBCategory)
- class KBCategoryAdmin(admin.ModelAdmin):
- list_display = ('name', 'title', 'slug', 'public')
+ if helpdesk_settings.HELPDESK_KB_ENABLED:
+ @admin.register(KBCategory)
+ class KBCategoryAdmin(admin.ModelAdmin):
+ list_display = ('name', 'title', 'slug', 'public')
@admin.register(CustomField)
diff --git a/helpdesk/apps.py b/helpdesk/apps.py
index fff4c8f2..a3ed19d2 100644
--- a/helpdesk/apps.py
+++ b/helpdesk/apps.py
@@ -5,5 +5,6 @@ class HelpdeskConfig(AppConfig):
name = 'helpdesk'
verbose_name = "Helpdesk"
# for Django 3.2 support:
- # see: https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
+ # see:
+ # https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
default_auto_field = 'django.db.models.AutoField'
diff --git a/helpdesk/decorators.py b/helpdesk/decorators.py
index 1dedbc02..e003440a 100644
--- a/helpdesk/decorators.py
+++ b/helpdesk/decorators.py
@@ -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.http import Http404
from django.shortcuts import redirect
-
-from django.contrib.auth.decorators import user_passes_test
-
-
+from functools import wraps
from helpdesk import settings as helpdesk_settings
diff --git a/helpdesk/email.py b/helpdesk/email.py
index 998ebb96..1a0b7d50 100644
--- a/helpdesk/email.py
+++ b/helpdesk/email.py
@@ -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.
See LICENSE for details.
"""
+
# 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 datetime import timedelta
from django.conf import settings as django_settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q
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 helpdesk import settings
-from helpdesk.lib import safe_template_context, process_attachments
-from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
+from helpdesk.lib import process_attachments, safe_template_context
+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
@@ -72,7 +73,8 @@ def process_email(quiet=False):
# Log messages to specific file only if the queue has it configured
if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set
- log_file_handler = logging.FileHandler(join(q.logging_dir, q.slug + '_get_email.log'))
+ log_file_handler = logging.FileHandler(
+ join(q.logging_dir, q.slug + '_get_email.log'))
logger.addHandler(log_file_handler)
else:
log_file_handler = None
@@ -105,7 +107,8 @@ def pop3_sync(q, logger, server):
try:
server.stls()
except Exception:
- logger.warning("POP3 StartTLS failed or unsupported. Connection will be unencrypted.")
+ logger.warning(
+ "POP3 StartTLS failed or unsupported. Connection will be unencrypted.")
server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
@@ -127,16 +130,21 @@ def pop3_sync(q, logger, server):
raw_content = server.retr(msgNum)[1]
if type(raw_content[0]) is bytes:
- full_message = "\n".join([elm.decode('utf-8') for elm in raw_content])
+ full_message = "\n".join([elm.decode('utf-8')
+ for elm in raw_content])
else:
- full_message = encoding.force_text("\n".join(raw_content), errors='replace')
- ticket = object_from_message(message=full_message, queue=q, logger=logger)
+ full_message = encoding.force_str(
+ "\n".join(raw_content), errors='replace')
+ ticket = object_from_message(
+ message=full_message, queue=q, logger=logger)
if ticket:
server.dele(msgNum)
- logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum)
+ logger.info(
+ "Successfully processed message %s, deleted from POP3 server" % msgNum)
else:
- logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum)
+ logger.warn(
+ "Message %s was not successfully processed, and will be left on POP3 server" % msgNum)
server.quit()
@@ -146,7 +154,8 @@ def imap_sync(q, logger, server):
try:
server.starttls()
except Exception:
- logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
+ logger.warning(
+ "IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
server.login(q.email_box_user or
settings.QUEUE_EMAIL_BOX_USER,
q.email_box_pass or
@@ -168,23 +177,26 @@ def imap_sync(q, logger, server):
sys.exit()
try:
- status, data = server.search(None, 'NOT', 'DELETED')
+ data = server.search(None, 'NOT', 'DELETED')[1]
if data:
msgnums = data[0].split()
logger.info("Received %d messages from IMAP server" % len(msgnums))
for num in msgnums:
logger.info("Processing message %s" % num)
- status, data = server.fetch(num, '(RFC822)')
- full_message = encoding.force_text(data[0][1], errors='replace')
+ data = server.fetch(num, '(RFC822)')[1]
+ full_message = encoding.force_str(data[0][1], errors='replace')
try:
- ticket = object_from_message(message=full_message, queue=q, logger=logger)
+ ticket = object_from_message(
+ message=full_message, queue=q, logger=logger)
except TypeError:
ticket = None # hotfix. Need to work out WHY.
if ticket:
server.store(num, '+FLAGS', '\\Deleted')
- logger.info("Successfully processed message %s, deleted from IMAP server" % num)
+ logger.info(
+ "Successfully processed message %s, deleted from IMAP server" % num)
else:
- logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num)
+ logger.warn(
+ "Message %s was not successfully processed, and will be left on IMAP server" % num)
except imaplib.IMAP4.error:
logger.error(
"IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?",
@@ -261,25 +273,31 @@ def process_queue(q, logger):
elif email_box_type == 'local':
mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/'
- mail = [join(mail_dir, f) for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))]
+ mail = [join(mail_dir, f)
+ for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))]
logger.info("Found %d messages in local mailbox directory" % len(mail))
logger.info("Found %d messages in local mailbox directory" % len(mail))
for i, m in enumerate(mail, 1):
logger.info("Processing message %d" % i)
with open(m, 'r') as f:
- full_message = encoding.force_text(f.read(), errors='replace')
- ticket = object_from_message(message=full_message, queue=q, logger=logger)
+ full_message = encoding.force_str(f.read(), errors='replace')
+ ticket = object_from_message(
+ message=full_message, queue=q, logger=logger)
if ticket:
- logger.info("Successfully processed message %d, ticket/comment created.", i)
+ logger.info(
+ "Successfully processed message %d, ticket/comment created.", i)
try:
- os.unlink(m) # delete message file if ticket was successful
+ # delete message file if ticket was successful
+ os.unlink(m)
except OSError as e:
- logger.error("Unable to delete message %d (%s).", i, str(e))
+ logger.error(
+ "Unable to delete message %d (%s).", i, str(e))
else:
logger.info("Successfully deleted message %d.", i)
else:
- logger.warn("Message %d was not successfully processed, and will be left in local directory", i)
+ logger.warn(
+ "Message %d was not successfully processed, and will be left in local directory", i)
def decodeUnknown(charset, string):
@@ -309,8 +327,10 @@ def is_autoreply(message):
So we don't start mail loops
"""
any_if_this = [
- False if not message.get("Auto-Submitted") else message.get("Auto-Submitted").lower() != "no",
- True if message.get("X-Auto-Response-Suppress") in ("DR", "AutoReply", "All") else False,
+ False if not message.get(
+ "Auto-Submitted") else message.get("Auto-Submitted").lower() != "no",
+ True if message.get("X-Auto-Response-Suppress") in ("DR",
+ "AutoReply", "All") else False,
message.get("List-Id"),
message.get("List-Unsubscribe"),
]
@@ -323,10 +343,10 @@ def create_ticket_cc(ticket, cc_list):
return []
# 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 = []
- for cced_name, cced_email in cc_list:
+ for __, cced_email in cc_list:
cced_email = cced_email.strip()
if cced_email == ticket.queue.email_address:
@@ -335,12 +355,13 @@ def create_ticket_cc(ticket, cc_list):
user = None
try:
- user = User.objects.get(email=cced_email)
+ user = User.objects.get(email=cced_email) # @UndefinedVariable
except User.DoesNotExist:
pass
try:
- ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email)
+ ticket_cc = subscribe_to_ticket_updates(
+ ticket=ticket, user=user, email=cced_email)
new_ticket_ccs.append(ticket_cc)
except ValidationError:
pass
@@ -370,7 +391,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
if in_reply_to is not None:
try:
- queryset = FollowUp.objects.filter(message_id=in_reply_to).order_by('-date')
+ queryset = FollowUp.objects.filter(
+ message_id=in_reply_to).order_by('-date')
if queryset.count() > 0:
previous_followup = queryset.first()
ticket = previous_followup.ticket
@@ -386,7 +408,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
new = False
# Check if the ticket has been merged to another ticket
if ticket.merged_to:
- logger.info("Ticket has been merged to %s" % ticket.merged_to.ticket)
+ logger.info("Ticket has been merged to %s" %
+ ticket.merged_to.ticket)
# Use the ticket in which it was merged to for next operations
ticket = ticket.merged_to
@@ -402,7 +425,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
priority=payload['priority'],
)
ticket.save()
- logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id))
+ logger.debug("Created new ticket %s-%s" %
+ (ticket.queue.slug, ticket.id))
new = True
@@ -413,7 +437,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
f = FollowUp(
ticket=ticket,
- title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
+ title=_('E-Mail Received from %(sender_email)s' %
+ {'sender_email': sender_email}),
date=now,
public=True,
comment=payload.get('full_body', payload['body']) or "",
@@ -422,7 +447,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
if ticket.status == Ticket.REOPENED_STATUS:
f.new_status = Ticket.REOPENED_STATUS
- f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email})
+ f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' %
+ {'sender_email': sender_email})
f.save()
logger.debug("Created new FollowUp for Ticket")
@@ -441,18 +467,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
new_ticket_ccs = []
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)
if autoreply:
- logger.info("Message seems to be auto-reply, not sending any emails back to the sender")
+ logger.info(
+ "Message seems to be auto-reply, not sending any emails back to the sender")
else:
# send mail to appropriate people now depending on what objects
# were created and who was CC'd
@@ -489,12 +507,83 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
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\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 = email.message_from_string(message)
subject = message.get('subject', _('Comment from e-mail'))
- subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject))
+ subject = decode_mail_headers(
+ decodeUnknown(message.get_charset(), subject))
for affix in STRIPPED_SUBJECT_STRINGS:
subject = subject.replace(affix, "")
subject = subject.strip()
@@ -508,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,
# but the getaddresses() function seems to be able to handle just unclosed quotes
# correctly. Not ideal, but this seems to work for now.
- sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1]
-
- 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])
+ sender_email = email.utils.getaddresses(
+ ['\"' + sender.replace('<', '\" <')])[0][1]
for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)):
if ignore.test(sender_email):
- if ignore.keep_in_mailbox:
- # By returning 'False' the message will be kept in the mailbox,
- # and the 'True' will cause the message to be deleted.
- return False
- return True
+ # By returning 'False' the message will be kept in the mailbox,
+ # and the 'True' will cause the message to be deleted.
+ return not ignore.keep_in_mailbox
- matchobj = re.match(r".*\[" + queue.slug + r"-(?P\d+)\]", subject)
- if matchobj:
- # This is a reply or forward.
- ticket = matchobj.group('id')
- logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket))
- else:
- logger.info("No tracking ID matched.")
- ticket = None
+ ticket_id: typing.Optional[int] = get_ticket_id_from_subject_slug(
+ queue.slug,
+ subject,
+ logger
+ )
body = None
full_body = None
@@ -560,32 +634,27 @@ def object_from_message(message, queue, logger):
body = decodeUnknown(part.get_content_charset(), body)
# have to use django_settings here so overwritting it works in tests
# the default value is False anyway
- if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False):
- # first message in thread, we save full body to avoid losing forwards and things like that
- body_parts = []
- for f in EmailReplyParser.read(body).fragments:
- body_parts.append(f.content)
- full_body = '\n\n'.join(body_parts)
+ 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
+ full_body = get_body_from_fragments(body)
body = EmailReplyParser.parse_reply(body)
else:
- # second and other reply, save only first part of the message
+ # second and other reply, save only first part of the
+ # message
body = EmailReplyParser.parse_reply(body)
full_body = body
# workaround to get unicode text out rather than escaped text
- try:
- body = body.encode('ascii').decode('unicode_escape')
- except UnicodeEncodeError:
- body.encode('utf-8')
+ body = get_encoded_body(body)
logger.debug("Discovered plain text MIME part")
else:
- try:
- email_body = encoding.smart_text(part.get_payload(decode=True))
- except UnicodeDecodeError:
- email_body = encoding.smart_text(part.get_payload(decode=False))
+ email_body = get_email_body_from_part_payload(part)
if not body and not full_body:
- # no text has been parsed so far - try such deep parsing for some messages
- altered_body = email_body.replace("
", "\n").replace(" ", "\n").replace(" '
) % email_body
files.append(
- SimpleUploadedFile(_("email_html_body.html"), payload.encode("utf-8"), 'text/html')
+ SimpleUploadedFile(
+ _("email_html_body.html"), payload.encode("utf-8"), 'text/html')
)
logger.debug("Discovered HTML MIME part")
else:
@@ -627,7 +697,8 @@ def object_from_message(message, queue, logger):
# except non_b64_err:
# logger.debug("Payload was not base64 encoded, using raw bytes")
# # payloadToWrite = payload
- files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0]))
+ files.append(SimpleUploadedFile(name, part.get_payload(
+ decode=True), mimetypes.guess_type(name)[0]))
logger.debug("Found MIME attachment %s" % name)
counter += 1
@@ -644,23 +715,13 @@ def object_from_message(message, queue, logger):
if not body:
body = ""
- 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'
- )
- )
+ add_file_if_always_save_incoming_email_message(files, message)
smtp_priority = message.get('priority', '')
smtp_importance = message.get('importance', '')
high_priority_types = {'high', 'important', '1', 'urgent'}
- priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3
+ priority = 2 if high_priority_types & {
+ smtp_priority, smtp_importance} else 3
payload = {
'body': body,
@@ -672,4 +733,4 @@ def object_from_message(message, queue, logger):
'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)
diff --git a/helpdesk/forms.py b/helpdesk/forms.py
index 6b776bfc..11ec89ed 100644
--- a/helpdesk/forms.py
+++ b/helpdesk/forms.py
@@ -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
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.conf import settings
-from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils import timezone
-
-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 django.utils.translation import gettext_lazy as _
from helpdesk import settings as helpdesk_settings
-from helpdesk.settings import CUSTOMFIELD_TO_FIELD_DICT, CUSTOMFIELD_DATETIME_FORMAT, \
- CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT
+from helpdesk.lib import convert_value, process_attachments, safe_template_context
+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:
- from helpdesk.models import (KBItem)
+ from helpdesk.models import KBItem
logger = logging.getLogger(__name__)
User = get_user_model()
@@ -37,40 +50,49 @@ class CustomFieldMixin(object):
def customfield_to_field(self, field, instanceargs):
# Use TextInput widget by default
- instanceargs['widget'] = forms.TextInput(attrs={'class': 'form-control'})
+ instanceargs['widget'] = forms.TextInput(
+ attrs={'class': 'form-control'})
# if-elif branches start with special cases
if field.data_type == 'varchar':
fieldclass = forms.CharField
instanceargs['max_length'] = field.max_length
elif field.data_type == 'text':
fieldclass = forms.CharField
- instanceargs['widget'] = forms.Textarea(attrs={'class': 'form-control'})
+ instanceargs['widget'] = forms.Textarea(
+ attrs={'class': 'form-control'})
instanceargs['max_length'] = field.max_length
elif field.data_type == 'integer':
fieldclass = forms.IntegerField
- instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'})
+ instanceargs['widget'] = forms.NumberInput(
+ attrs={'class': 'form-control'})
elif field.data_type == 'decimal':
fieldclass = forms.DecimalField
instanceargs['decimal_places'] = field.decimal_places
instanceargs['max_digits'] = field.max_length
- instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'})
+ instanceargs['widget'] = forms.NumberInput(
+ attrs={'class': 'form-control'})
elif field.data_type == 'list':
fieldclass = forms.ChoiceField
instanceargs['choices'] = field.get_choices()
- instanceargs['widget'] = forms.Select(attrs={'class': 'form-control'})
+ instanceargs['widget'] = forms.Select(
+ attrs={'class': 'form-control'})
else:
# Try to use the immediate equivalences dictionary
try:
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
# Change widgets for the following classes
if fieldclass == forms.DateField:
- instanceargs['widget'] = forms.DateInput(attrs={'class': 'form-control date-field'})
+ instanceargs['widget'] = forms.DateInput(
+ attrs={'class': 'form-control date-field'})
elif fieldclass == forms.DateTimeField:
- instanceargs['widget'] = forms.DateTimeInput(attrs={'class': 'form-control datetime-field'})
+ instanceargs['widget'] = forms.DateTimeInput(
+ attrs={'class': 'form-control datetime-field'})
elif fieldclass == forms.TimeField:
- instanceargs['widget'] = forms.TimeInput(attrs={'class': 'form-control time-field'})
+ instanceargs['widget'] = forms.TimeInput(
+ attrs={'class': 'form-control time-field'})
elif fieldclass == forms.BooleanField:
- instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'})
+ instanceargs['widget'] = forms.CheckboxInput(
+ attrs={'class': 'form-control'})
except KeyError:
# The data_type was not found anywhere
@@ -83,10 +105,12 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
class Meta:
model = Ticket
- exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to')
+ exclude = ('created', 'modified', 'status', 'on_hold',
+ 'resolution', 'last_escalation', 'assigned_to')
class Media:
- js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js')
+ js = ('helpdesk/js/init_due_date.js',
+ 'helpdesk/js/init_datetime_classes.js')
def __init__(self, *args, **kwargs):
"""
@@ -96,21 +120,28 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
# Disable and add help_text to the merged_to field on this form
self.fields['merged_to'].disabled = True
- self.fields['merged_to'].help_text = _('This ticket is merged into the selected ticket.')
+ self.fields['merged_to'].help_text = _(
+ 'This ticket is merged into the selected ticket.')
for field in CustomField.objects.all():
initial_value = None
try:
- current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field)
+ current_value = TicketCustomFieldValue.objects.get(
+ ticket=self.instance, field=field)
initial_value = current_value.value
- # Attempt to convert from fixed format string to date/time data type
+ # Attempt to convert from fixed format string to date/time data
+ # type
if 'datetime' == current_value.field.data_type:
- initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATETIME_FORMAT)
+ initial_value = datetime.strptime(
+ initial_value, CUSTOMFIELD_DATETIME_FORMAT)
elif 'date' == current_value.field.data_type:
- initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATE_FORMAT)
+ initial_value = datetime.strptime(
+ initial_value, CUSTOMFIELD_DATE_FORMAT)
elif 'time' == current_value.field.data_type:
- initial_value = datetime.strptime(initial_value, CUSTOMFIELD_TIME_FORMAT)
- # If it is boolean field, transform the value to a real boolean instead of a string
+ initial_value = datetime.strptime(
+ initial_value, CUSTOMFIELD_TIME_FORMAT)
+ # If it is boolean field, transform the value to a real boolean
+ # instead of a string
elif 'boolean' == current_value.field.data_type:
initial_value = 'True' == initial_value
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
@@ -133,9 +164,11 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
field_name = field.replace('custom_', '', 1)
customfield = CustomField.objects.get(name=field_name)
try:
- cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
+ cfv = TicketCustomFieldValue.objects.get(
+ ticket=self.instance, field=customfield)
except ObjectDoesNotExist:
- cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
+ cfv = TicketCustomFieldValue(
+ ticket=self.instance, field=customfield)
cfv.value = convert_value(value)
cfv.save()
@@ -152,7 +185,8 @@ class EditFollowUpForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
"""Filter not openned tickets here."""
super(EditFollowUpForm, self).__init__(*args, **kwargs)
- self.fields["ticket"].queryset = Ticket.objects.filter(status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
+ self.fields["ticket"].queryset = Ticket.objects.filter(
+ status__in=(Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS))
class AbstractTicketForm(CustomFieldMixin, forms.Form):
@@ -178,7 +212,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.Textarea(attrs={'class': 'form-control'}),
label=_('Description of your issue'),
required=True,
- help_text=_('Please be as descriptive as possible and include all details'),
+ help_text=_(
+ 'Please be as descriptive as possible and include all details'),
)
priority = forms.ChoiceField(
@@ -187,13 +222,16 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
required=True,
initial=getattr(settings, 'HELPDESK_PUBLIC_TICKET_PRIORITY', '3'),
label=_('Priority'),
- help_text=_("Please select a priority carefully. If unsure, leave it as '3'."),
+ help_text=_(
+ "Please select a priority carefully. If unsure, leave it as '3'."),
)
due_date = forms.DateTimeField(
- widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}),
+ widget=forms.TextInput(
+ attrs={'class': 'form-control', 'autocomplete': 'off'}),
required=False,
- input_formats=[CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
+ input_formats=[CUSTOMFIELD_DATE_FORMAT,
+ CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
label=_('Due on'),
)
@@ -201,11 +239,15 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.FileInput(attrs={'class': 'form-control-file'}),
required=False,
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:
- js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js')
+ js = ('helpdesk/js/init_due_date.js',
+ 'helpdesk/js/init_datetime_classes.js')
def __init__(self, kbcategory=None, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -215,7 +257,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.Select(attrs={'class': 'form-control'}),
required=False,
label=_('Knowledge Base Item'),
- choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(category=kbcategory.pk, enabled=True)],
+ choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(
+ category=kbcategory.pk, enabled=True)],
)
def _add_form_custom_fields(self, staff_only_filter=None):
@@ -307,7 +350,8 @@ class TicketForm(AbstractTicketForm):
submitter_email = forms.EmailField(
required=False,
label=_('Submitter E-Mail Address'),
- widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}),
+ widget=forms.TextInput(
+ attrs={'class': 'form-control', 'type': 'email'}),
help_text=_('This e-mail address will receive copies of all public '
'updates to this ticket.'),
)
@@ -335,10 +379,13 @@ class TicketForm(AbstractTicketForm):
self.fields['queue'].choices = queue_choices
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS:
- assignable_users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
+ assignable_users = User.objects.filter(
+ is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
else:
- assignable_users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
- self.fields['assigned_to'].choices = [('', '--------')] + [(u.id, u.get_username()) for u in assignable_users]
+ assignable_users = User.objects.filter(
+ is_active=True).order_by(User.USERNAME_FIELD)
+ self.fields['assigned_to'].choices = [
+ ('', '--------')] + [(u.id, u.get_username()) for u in assignable_users]
self._add_form_custom_fields()
def save(self, user):
@@ -380,7 +427,8 @@ class PublicTicketForm(AbstractTicketForm):
Ticket Form creation for all users (public-facing).
"""
submitter_email = forms.EmailField(
- widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'email'}),
+ widget=forms.TextInput(
+ attrs={'class': 'form-control', 'type': 'email'}),
required=True,
label=_('Your E-Mail Address'),
help_text=_('We will e-mail you when your ticket is updated.'),
@@ -406,7 +454,8 @@ class PublicTicketForm(AbstractTicketForm):
}
for field_name, field_setting_key in field_deletion_table.items():
- has_settings_default_value = getattr(settings, field_setting_key, None)
+ has_settings_default_value = getattr(
+ settings, field_setting_key, None)
if has_settings_default_value is not None:
del self.fields[field_name]
@@ -485,9 +534,11 @@ class TicketCCForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TicketCCForm, self).__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
- users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
+ users = User.objects.filter(
+ is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
else:
- users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
+ users = User.objects.filter(
+ is_active=True).order_by(User.USERNAME_FIELD)
self.fields['user'].queryset = users
@@ -497,9 +548,11 @@ class TicketCCUserForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TicketCCUserForm, self).__init__(*args, **kwargs)
if helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC:
- users = User.objects.filter(is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
+ users = User.objects.filter(
+ is_active=True, is_staff=True).order_by(User.USERNAME_FIELD)
else:
- users = User.objects.filter(is_active=True).order_by(User.USERNAME_FIELD)
+ users = User.objects.filter(
+ is_active=True).order_by(User.USERNAME_FIELD)
self.fields['user'].queryset = users
class Meta:
@@ -538,8 +591,11 @@ class MultipleTicketSelectForm(forms.Form):
if len(tickets) < 2:
raise ValidationError(_('Please choose at least 2 tickets.'))
if len(tickets) > 4:
- raise ValidationError(_('Impossible to merge more than 4 tickets...'))
- queues = tickets.order_by('queue').distinct().values_list('queue', flat=True)
+ raise ValidationError(
+ _('Impossible to merge more than 4 tickets...'))
+ queues = tickets.order_by('queue').distinct(
+ ).values_list('queue', flat=True)
if len(queues) != 1:
- raise ValidationError(_('All selected tickets must share the same queue in order to be merged.'))
+ raise ValidationError(
+ _('All selected tickets must share the same queue in order to be merged.'))
return tickets
diff --git a/helpdesk/lib.py b/helpdesk/lib.py
index f9adb174..028d6353 100644
--- a/helpdesk/lib.py
+++ b/helpdesk/lib.py
@@ -6,14 +6,14 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
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 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')
@@ -117,26 +117,30 @@ def text_is_spam(text, request):
if ak.verify_key():
ak_data = {
'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
- 'user_agent': request.META.get('HTTP_USER_AGENT', ''),
- 'referrer': request.META.get('HTTP_REFERER', ''),
+ 'user_agent': request.headers.get('User-Agent', ''),
+ 'referrer': request.headers.get('Referer', ''),
'comment_type': 'comment',
'comment_author': '',
}
- return ak.comment_check(smart_text(text), data=ak_data)
+ return ak.comment_check(smart_str(text), data=ak_data)
return False
def process_attachments(followup, attached_files):
- max_email_attachment_size = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
+ max_email_attachment_size = getattr(
+ settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
attachments = []
for attached in attached_files:
if attached.size:
- filename = smart_text(attached.name)
- att = followup.followupattachment_set.create(
+ from helpdesk.models import FollowUpAttachment
+
+ filename = smart_str(attached.name)
+ att = FollowUpAttachment(
+ followup=followup,
file=attached,
filename=filename,
mime_type=attached.content_type or
@@ -149,7 +153,8 @@ def process_attachments(followup, attached_files):
if attached.size < max_email_attachment_size:
# Only files smaller than 512kb (or as defined in
- # settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via email.
+ # settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via
+ # email.
attachments.append([filename, att.file])
return attachments
diff --git a/helpdesk/management/commands/create_escalation_exclusions.py b/helpdesk/management/commands/create_escalation_exclusions.py
index 8801e0f0..6d3e249f 100644
--- a/helpdesk/management/commands/create_escalation_exclusions.py
+++ b/helpdesk/management/commands/create_escalation_exclusions.py
@@ -8,12 +8,11 @@ scripts/create_escalation_exclusion.py - Easy way to routinely add particular
days to the list of days on which no
escalation should take place.
"""
+
+from datetime import date, timedelta
from django.core.management.base import BaseCommand, CommandError
-
-from helpdesk.models import EscalationExclusion, Queue
-
-from datetime import timedelta, date
import getopt
+from helpdesk.models import EscalationExclusion, Queue
from optparse import make_option
import sys
@@ -66,7 +65,8 @@ class Command(BaseCommand):
raise CommandError("Queue %s does not exist." % queue)
queues.append(q)
- create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
+ create_exclusions(days=days, occurrences=occurrences,
+ verbose=verbose, queues=queues)
day_names = {
@@ -90,11 +90,13 @@ def create_exclusions(days, occurrences, verbose, queues):
while i < occurrences:
if day == workdate.weekday():
if EscalationExclusion.objects.filter(date=workdate).count() == 0:
- esc = EscalationExclusion(name='Auto Exclusion for %s' % day_name, date=workdate)
+ esc = EscalationExclusion(
+ name='Auto Exclusion for %s' % day_name, date=workdate)
esc.save()
if verbose:
- print("Created exclusion for %s %s" % (day_name, workdate))
+ print("Created exclusion for %s %s" %
+ (day_name, workdate))
for q in queues:
esc.queues.add(q)
@@ -116,7 +118,8 @@ def usage():
if __name__ == '__main__':
# This script can be run from the command-line or via Django's manage.py.
try:
- opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', ['days=', 'occurrences=', 'verbose', 'queues='])
+ opts, args = getopt.getopt(sys.argv[1:], 'd:o:q:v', [
+ 'days=', 'occurrences=', 'verbose', 'queues='])
except getopt.GetoptError:
usage()
sys.exit(2)
@@ -151,4 +154,5 @@ if __name__ == '__main__':
sys.exit(2)
queues.append(q)
- create_exclusions(days=days, occurrences=occurrences, verbose=verbose, queues=queues)
+ create_exclusions(days=days, occurrences=occurrences,
+ verbose=verbose, queues=queues)
diff --git a/helpdesk/management/commands/create_queue_permissions.py b/helpdesk/management/commands/create_queue_permissions.py
index 50e980c3..1da97851 100644
--- a/helpdesk/management/commands/create_queue_permissions.py
+++ b/helpdesk/management/commands/create_queue_permissions.py
@@ -13,15 +13,13 @@ scripts/create_queue_permissions.py -
existing permissions.
"""
-from optparse import make_option
-
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError
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 optparse import make_option
class Command(BaseCommand):
@@ -55,14 +53,17 @@ class Command(BaseCommand):
self.stdout.write("Preparing Queue %s [%s]" % (q.title, q.slug))
if q.permission_name:
- self.stdout.write(" .. already has `permission_name=%s`" % q.permission_name)
+ self.stdout.write(
+ " .. already has `permission_name=%s`" % q.permission_name)
basename = q.permission_name[9:]
else:
basename = q.generate_permission_name()
- self.stdout.write(" .. generated `permission_name=%s`" % q.permission_name)
+ self.stdout.write(
+ " .. generated `permission_name=%s`" % q.permission_name)
q.save()
- self.stdout.write(" .. checking permission codename `%s`" % basename)
+ self.stdout.write(
+ " .. checking permission codename `%s`" % basename)
try:
Permission.objects.create(
diff --git a/helpdesk/management/commands/create_usersettings.py b/helpdesk/management/commands/create_usersettings.py
index e27cea16..7f5203cf 100644
--- a/helpdesk/management/commands/create_usersettings.py
+++ b/helpdesk/management/commands/create_usersettings.py
@@ -8,12 +8,12 @@ create_usersettings.py - Easy way to create helpdesk-specific settings for
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.core.management.base import BaseCommand
+from django.utils.translation import gettext as _
from helpdesk.models import UserSettings
+
User = get_user_model()
diff --git a/helpdesk/management/commands/escalate_tickets.py b/helpdesk/management/commands/escalate_tickets.py
index edbf7307..d0a920a8 100644
--- a/helpdesk/management/commands/escalate_tickets.py
+++ b/helpdesk/management/commands/escalate_tickets.py
@@ -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.
"""
-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.db.models import Q
-from django.utils.translation import ugettext as _
from django.utils import timezone
-
-from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
+from django.utils.translation import gettext as _
+import getopt
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):
@@ -62,7 +61,8 @@ class Command(BaseCommand):
def escalate_tickets(queues, verbose):
""" Only include queues with escalation configured """
- queryset = Queue.objects.filter(escalate_days__isnull=False).exclude(escalate_days=0)
+ queryset = Queue.objects.filter(
+ escalate_days__isnull=False).exclude(escalate_days=0)
if queues:
queryset = queryset.filter(slug__in=queues)
@@ -143,7 +143,8 @@ def usage():
if __name__ == '__main__':
try:
- opts, args = getopt.getopt(sys.argv[1:], ['queues=', 'verboseescalation'])
+ opts, args = getopt.getopt(
+ sys.argv[1:], ['queues=', 'verboseescalation'])
except getopt.GetoptError:
usage()
sys.exit(2)
diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py
index ce344bd5..0111b93b 100755
--- a/helpdesk/management/commands/get_email.py
+++ b/helpdesk/management/commands/get_email.py
@@ -11,7 +11,6 @@ scripts/get_email.py - Designed to be run from cron, this script checks the
adding to existing tickets if needed)
"""
from django.core.management.base import BaseCommand
-
from helpdesk.email import process_email
diff --git a/helpdesk/migrations/0009_migrate_queuemembership.py b/helpdesk/migrations/0009_migrate_queuemembership.py
index 09e2a60f..318b24ab 100644
--- a/helpdesk/migrations/0009_migrate_queuemembership.py
+++ b/helpdesk/migrations/0009_migrate_queuemembership.py
@@ -2,7 +2,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations
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):
diff --git a/helpdesk/models.py b/helpdesk/models.py
index cb3d4149..73b2ba7c 100644
--- a/helpdesk/models.py
+++ b/helpdesk/models.py
@@ -7,35 +7,29 @@ models.py - Model (and hence database) definitions. This is the core of the
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.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models
-from django.conf import settings
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.translation import gettext, gettext_lazy as _
+from helpdesk import settings as helpdesk_settings
+from io import StringIO
from markdown import markdown
from markdown.extensions import Extension
-
-
-import uuid
-
+import mimetypes
+import os
+import re
from rest_framework import serializers
-
-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
+import uuid
def format_time_spent(time_spent):
@@ -50,16 +44,17 @@ def format_time_spent(time_spent):
class EscapeHtml(Extension):
- def extendMarkdown(self, md, md_globals):
- del md.preprocessors['html_block']
- del md.inlinePatterns['html']
+ def extendMarkdown(self, md, md_globals=None):
+ # del md.preprocessors['html_block']
+ # del md.inlinePatterns['html']
+ pass
def get_markdown(text):
if not text:
return ""
- pattern = fr'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)'
+ pattern = r'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)'
# Regex check
if re.match(pattern, text):
# get get value of group regex
@@ -128,7 +123,8 @@ class Queue(models.Model):
_('Allow Public Submission?'),
blank=True,
default=False,
- help_text=_('Should this queue be listed on the public submission form?'),
+ help_text=_(
+ 'Should this queue be listed on the public submission form?'),
)
allow_email_submission = models.BooleanField(
@@ -180,7 +176,8 @@ class Queue(models.Model):
email_box_type = models.CharField(
_('E-Mail Box Type'),
max_length=5,
- choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), ('local', _('Local Directory'))),
+ choices=(('pop3', _('POP 3')), ('imap', _('IMAP')),
+ ('local', _('Local Directory'))),
blank=True,
null=True,
help_text=_('E-Mail server type for creating tickets automatically '
@@ -262,7 +259,8 @@ class Queue(models.Model):
email_box_interval = models.IntegerField(
_('E-Mail Check Interval'),
- help_text=_('How often do you wish to check this mailbox? (in Minutes)'),
+ help_text=_(
+ 'How often do you wish to check this mailbox? (in Minutes)'),
blank=True,
null=True,
default='5',
@@ -281,7 +279,8 @@ class Queue(models.Model):
choices=(('socks4', _('SOCKS4')), ('socks5', _('SOCKS5'))),
blank=True,
null=True,
- help_text=_('SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'),
+ help_text=_(
+ 'SOCKS4 or SOCKS5 allows you to proxy your connections through a SOCKS server.'),
)
socks_proxy_host = models.GenericIPAddressField(
@@ -295,7 +294,8 @@ class Queue(models.Model):
_('Socks Proxy Port'),
blank=True,
null=True,
- help_text=_('Socks proxy port number. Default: 9150 (default TOR port)'),
+ help_text=_(
+ 'Socks proxy port number. Default: 9150 (default TOR port)'),
)
logging_type = models.CharField(
@@ -356,7 +356,8 @@ class Queue(models.Model):
"""
if not self.email_address:
# must check if given in format "Foo "
- default_email = re.match(".*<(?P.*@*.)>", settings.DEFAULT_FROM_EMAIL)
+ default_email = re.match(
+ ".*<(?P.*@*.)>", settings.DEFAULT_FROM_EMAIL)
if default_email is not None:
# already in the right format, so just include it here
return u'NO QUEUE EMAIL ADDRESS DEFINED %s' % settings.DEFAULT_FROM_EMAIL
@@ -532,7 +533,8 @@ class Ticket(models.Model):
_('On Hold'),
blank=True,
default=False,
- help_text=_('If a ticket is on hold, it will not automatically be escalated.'),
+ help_text=_(
+ 'If a ticket is on hold, it will not automatically be escalated.'),
)
description = models.TextField(
@@ -582,7 +584,8 @@ class Ticket(models.Model):
blank=True,
null=True,
on_delete=models.CASCADE,
- verbose_name=_('Knowledge base item the user was viewing when they created this ticket.'),
+ verbose_name=_(
+ 'Knowledge base item the user was viewing when they created this ticket.'),
)
merged_to = models.ForeignKey(
@@ -648,7 +651,8 @@ class Ticket(models.Model):
def send(role, recipient):
if recipient and recipient not in recipients and role in roles:
template, context = roles[role]
- send_templated_mail(template, context, recipient, sender=self.queue.from_address, **kwargs)
+ send_templated_mail(
+ template, context, recipient, sender=self.queue.from_address, **kwargs)
recipients.add(recipient)
send('submitter', self.submitter_email)
@@ -844,7 +848,8 @@ class Ticket(models.Model):
# Ignore if user has no email address
return
elif not email:
- raise ValueError('You must provide at least one parameter to get the email from')
+ raise ValueError(
+ 'You must provide at least one parameter to get the email from')
# Prepare all emails already into the ticket
ticket_emails = [x.display for x in self.ticketcc_set.all()]
@@ -1031,11 +1036,11 @@ class TicketChange(models.Model):
def __str__(self):
out = '%s ' % self.field
if not self.new_value:
- out += ugettext('removed')
+ out += gettext('removed')
elif not self.old_value:
- out += ugettext('set to %s') % self.new_value
+ out += gettext('set to %s') % self.new_value
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,
'new_value': self.new_value
}
@@ -1280,7 +1285,8 @@ class EmailTemplate(models.Model):
html = models.TextField(
_('HTML'),
- help_text=_('The same context is available here as in plain_text, above.'),
+ help_text=_(
+ 'The same context is available here as in plain_text, above.'),
)
locale = models.CharField(
@@ -1329,7 +1335,8 @@ class KBCategory(models.Model):
blank=True,
null=True,
on_delete=models.CASCADE,
- verbose_name=_('Default queue when creating a ticket after viewing this category.'),
+ verbose_name=_(
+ 'Default queue when creating a ticket after viewing this category.'),
)
public = models.BooleanField(
@@ -1396,7 +1403,8 @@ class KBItem(models.Model):
last_updated = models.DateTimeField(
_('Last Updated'),
- help_text=_('The date on which this question was most recently changed.'),
+ help_text=_(
+ 'The date on which this question was most recently changed.'),
blank=True,
)
@@ -1555,7 +1563,8 @@ class UserSettings(models.Model):
login_view_ticketlist = models.BooleanField(
verbose_name=_('Show Ticket List on Login?'),
- help_text=_('Display the ticket list upon login? Otherwise, the dashboard is shown.'),
+ help_text=_(
+ 'Display the ticket list upon login? Otherwise, the dashboard is shown.'),
default=login_view_ticketlist_default,
)
@@ -1570,13 +1579,15 @@ class UserSettings(models.Model):
email_on_ticket_assign = models.BooleanField(
verbose_name=_('E-mail me when assigned a ticket?'),
- help_text=_('If you are assigned a ticket via the web, do you want to receive an e-mail?'),
+ help_text=_(
+ 'If you are assigned a ticket via the web, do you want to receive an e-mail?'),
default=email_on_ticket_assign_default,
)
tickets_per_page = models.IntegerField(
verbose_name=_('Number of tickets to show per page'),
- help_text=_('How many tickets do you want to see on the Ticket List page?'),
+ help_text=_(
+ 'How many tickets do you want to see on the Ticket List page?'),
default=tickets_per_page_default,
choices=PAGE_SIZES,
)
@@ -1611,7 +1622,8 @@ def create_usersettings(sender, instance, created, **kwargs):
UserSettings.objects.create(user=instance)
-models.signals.post_save.connect(create_usersettings, sender=settings.AUTH_USER_MODEL)
+models.signals.post_save.connect(
+ create_usersettings, sender=settings.AUTH_USER_MODEL)
class IgnoreEmail(models.Model):
@@ -1851,14 +1863,16 @@ class CustomField(models.Model):
ordering = models.IntegerField(
_('Ordering'),
- help_text=_('Lower numbers are displayed first; higher numbers are listed later'),
+ help_text=_(
+ 'Lower numbers are displayed first; higher numbers are listed later'),
blank=True,
null=True,
)
def _choices_as_array(self):
valuebuffer = StringIO(self.list_values)
- choices = [[item.strip(), item.strip()] for item in valuebuffer.readlines()]
+ choices = [[item.strip(), item.strip()]
+ for item in valuebuffer.readlines()]
valuebuffer.close()
return choices
choices_as_array = property(_choices_as_array)
@@ -1912,10 +1926,10 @@ class CustomField(models.Model):
# Prepare attributes for each types
attributes = {
- 'label': self.label,
- 'help_text': self.help_text,
- 'required': self.required,
- }
+ 'label': self.label,
+ 'help_text': self.help_text,
+ 'required': self.required,
+ }
if self.data_type in ('varchar', 'text'):
attributes['max_length'] = self.max_length
if self.data_type == 'text':
@@ -1950,6 +1964,10 @@ class TicketCustomFieldValue(models.Model):
def __str__(self):
return '%s / %s' % (self.ticket, self.field)
+ @property
+ def default_value(self) -> str:
+ return _("Not defined")
+
class Meta:
unique_together = (('ticket', 'field'),)
verbose_name = _('Ticket custom field value')
diff --git a/helpdesk/query.py b/helpdesk/query.py
index 36d7eca2..9ebef3e9 100644
--- a/helpdesk/query.py
+++ b/helpdesk/query.py
@@ -1,15 +1,12 @@
+
+from base64 import b64decode, b64encode
from django.db.models import Q
from django.urls import reverse
from django.utils.html import escape
-from django.utils.translation import ugettext as _
-
-from base64 import b64encode
-from base64 import b64decode
-import json
-
-from model_utils import Choices
-
+from django.utils.translation import gettext as _
from helpdesk.serializers import DatatablesTicketSerializer
+import json
+from model_utils import Choices
def query_to_base64(query):
@@ -103,8 +100,10 @@ def get_query_class():
class __Query__:
def __init__(self, huser, base64query=None, query_params=None):
self.huser = huser
- self.params = query_params if query_params else query_from_base64(base64query)
- self.base64 = base64query if base64query else query_to_base64(query_params)
+ self.params = query_params if query_params else query_from_base64(
+ base64query)
+ self.base64 = base64query if base64query else query_to_base64(
+ query_params)
self.result = None
def get_search_filter_args(self):
@@ -128,7 +127,8 @@ class __Query__:
"""
filter = self.params.get('filtering', {})
filter_or = self.params.get('filtering_or', {})
- queryset = queryset.filter((Q(**filter) | Q(**filter_or)) & self.get_search_filter_args())
+ queryset = queryset.filter(
+ (Q(**filter) | Q(**filter_or)) & self.get_search_filter_args())
sorting = self.params.get('sorting', None)
if sorting:
sortreverse = self.params.get('sortreverse', None)
@@ -191,11 +191,13 @@ class __Query__:
'text': {
'headline': ticket.title + ' - ' + followup.title,
'text': (
- (escape(followup.comment) if followup.comment else _('No text'))
+ (escape(followup.comment)
+ if followup.comment else _('No text'))
+
' %s'
%
- (reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket"))
+ (reverse('helpdesk:view', kwargs={
+ 'ticket_id': ticket.pk}), _("View ticket"))
),
},
'group': _('Messages'),
diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py
index b5734c49..6474060d 100644
--- a/helpdesk/serializers.py
+++ b/helpdesk/serializers.py
@@ -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.humanize.templatetags import humanize
+from rest_framework import serializers
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):
"""
@@ -71,12 +70,45 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
return obj.kbitem.title if obj.kbitem else ""
+class FollowUpAttachmentSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FollowUpAttachment
+ fields = ('id', 'followup', 'file', 'filename', 'mime_type', 'size')
+
+
+class FollowUpSerializer(serializers.ModelSerializer):
+ followupattachment_set = FollowUpAttachmentSerializer(
+ many=True, read_only=True)
+ attachments = serializers.ListField(
+ child=serializers.FileField(),
+ write_only=True,
+ required=False
+ )
+
+ class Meta:
+ model = FollowUp
+ fields = (
+ 'id', 'ticket', 'date', 'title', 'comment', 'public', 'user', 'new_status', 'message_id',
+ 'time_spent', 'followupattachment_set', 'attachments'
+ )
+
+ def create(self, validated_data):
+ attachments = validated_data.pop('attachments', None)
+ followup = super().create(validated_data)
+ if attachments:
+ process_attachments(followup, attachments)
+ return followup
+
+
class TicketSerializer(serializers.ModelSerializer):
+ followup_set = FollowUpSerializer(many=True, read_only=True)
+ attachment = serializers.FileField(write_only=True, required=False)
+
class Meta:
model = Ticket
fields = (
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold',
- 'priority', 'due_date', 'merged_to'
+ 'priority', 'due_date', 'merged_to', 'attachment', 'followup_set'
)
def __init__(self, *args, **kwargs):
@@ -99,7 +131,10 @@ class TicketSerializer(serializers.ModelSerializer):
if data.get('merged_to'):
data['merged_to'] = data['merged_to'].id
- ticket_form = TicketForm(data=data, queue_choices=queue_choices)
+ files = {'attachment': data.pop('attachment', None)}
+
+ ticket_form = TicketForm(
+ data=data, files=files, queue_choices=queue_choices)
if ticket_form.is_valid():
ticket = ticket_form.save(user=self.context['request'].user)
ticket.set_custom_field_values()
diff --git a/helpdesk/settings.py b/helpdesk/settings.py
index 4b5fb7bb..4594a510 100644
--- a/helpdesk/settings.py
+++ b/helpdesk/settings.py
@@ -2,12 +2,13 @@
Default settings for django-helpdesk.
"""
-import os
-import warnings
from django import forms
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
+import os
+import warnings
+
DEFAULT_USER_SETTINGS = {
'login_view_ticketlist': True,
@@ -25,6 +26,9 @@ except AttributeError:
HAS_TAG_SUPPORT = False
+# Use international timezones
+USE_TZ: bool = True
+
# check for secure cookie support
if os.environ.get('SECURE_PROXY_SSL_HEADER'):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
@@ -43,13 +47,13 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
# Enable the Dependencies field on ticket view
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings,
- 'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
- True)
+ 'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
+ True)
# Enable the Time spent on field on ticket view
HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings,
- 'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET',
- True)
+ 'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET',
+ True)
# raises a 404 to anon users. It's like it was invisible
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
@@ -60,10 +64,13 @@ HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
# Disable Timeline on ticket list
-HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True)
+HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(
+ settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True)
-# show extended navigation by default, to all users, irrespective of staff status?
-HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False)
+# show extended navigation by default, to all users, irrespective of staff
+# status?
+HELPDESK_NAVIGATION_ENABLED = getattr(
+ settings, 'HELPDESK_NAVIGATION_ENABLED', False)
# use public CDNs to serve jquery and other javascript by default?
# otherwise, use built-in static copy
@@ -81,7 +88,8 @@ HELPDESK_TRANSLATE_TICKET_COMMENTS_LANG = getattr(settings,
["en", "de", "es", "fr", "it", "ru"])
# show link to 'change password' on 'User Settings' page?
-HELPDESK_SHOW_CHANGE_PASSWORD = getattr(settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False)
+HELPDESK_SHOW_CHANGE_PASSWORD = getattr(
+ settings, 'HELPDESK_SHOW_CHANGE_PASSWORD', False)
# allow user to override default layout for 'followups' - work in progress.
HELPDESK_FOLLOWUP_MOD = getattr(settings, 'HELPDESK_FOLLOWUP_MOD', False)
@@ -93,17 +101,19 @@ HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE = getattr(settings,
# URL schemes that are allowed within links
ALLOWED_URL_SCHEMES = getattr(settings, 'ALLOWED_URL_SCHEMES', (
- 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
+ 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
))
############################
# options for public pages #
############################
# show 'view a ticket' section on public page?
-HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True)
+HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(
+ settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC', True)
# show 'submit a ticket' section on public page?
-HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
+HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(
+ settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
# change that to custom class to have extra fields or validation (like captcha)
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
@@ -134,8 +144,10 @@ CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M"
''' options for update_ticket views '''
# allow non-staff users to interact with tickets?
-# can be True/False or a callable accepting the active user and returning True if they must be considered helpdesk staff
-HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
+# can be True/False or a callable accepting the active user and returning
+# True if they must be considered helpdesk staff
+HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(
+ settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)):
warnings.warn(
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
@@ -151,14 +163,18 @@ HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP = getattr(
settings, 'HELPDESK_SHOW_DELETE_BUTTON_SUPERUSER_FOLLOW_UP', False)
-# make all updates public by default? this will hide the 'is this update public' checkbox
-HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
+# make all updates public by default? this will hide the 'is this update
+# public' checkbox
+HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(
+ settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)
# only show staff users in ticket owner drop-downs
-HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False)
+HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(
+ settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False)
# only show staff users in ticket cc drop-down
-HELPDESK_STAFF_ONLY_TICKET_CC = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False)
+HELPDESK_STAFF_ONLY_TICKET_CC = getattr(
+ settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False)
# allow the subject to have a configurable template.
HELPDESK_EMAIL_SUBJECT_TEMPLATE = getattr(
@@ -170,11 +186,13 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
raise ImproperlyConfigured
# default fallback locale when queue locale not found
-HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en')
+HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(
+ settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en')
# default maximum email attachment size, in bytes
# only attachments smaller than this size will be sent via email
-HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
+HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(
+ settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
########################################
@@ -186,7 +204,8 @@ HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
# Activate the API endpoint to manage tickets thanks to Django REST Framework
-HELPDESK_ACTIVATE_API_ENDPOINT = getattr(settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False)
+HELPDESK_ACTIVATE_API_ENDPOINT = getattr(
+ settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False)
#################
@@ -201,25 +220,32 @@ QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None)
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None)
# only process emails with a valid tracking ID? (throws away all other mail)
-QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False)
+QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(
+ settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', False)
# only allow users to access queues that they are members of?
HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr(
settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False)
# use https in the email links
-HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False)
+HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(
+ settings, 'HELPDESK_USE_HTTPS_IN_EMAIL_LINK', False)
-HELPDESK_TEAMS_MODEL = getattr(settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team')
-HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [('pinax_teams', '0004_auto_20170511_0856')])
-HELPDESK_KBITEM_TEAM_GETTER = getattr(settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team)
+HELPDESK_TEAMS_MODEL = getattr(
+ settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team')
+HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [
+ ('pinax_teams', '0004_auto_20170511_0856')])
+HELPDESK_KBITEM_TEAM_GETTER = getattr(
+ settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team)
# Include all signatures and forwards in the first ticket message if set
-# Useful if you get forwards dropped from them while they are useful part of request
-HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
+# Useful if you get forwards dropped from them while they are useful part
+# of request
+HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(
+ settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
# If set then we always save incoming emails as .eml attachments
# which is quite noisy but very helpful for complicated markup, forwards and so on
# (which gets stripped/corrupted otherwise)
-HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False)
-
+HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(
+ settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False)
diff --git a/helpdesk/tasks.py b/helpdesk/tasks.py
index 2fdee3e4..d7ce1d68 100644
--- a/helpdesk/tasks.py
+++ b/helpdesk/tasks.py
@@ -1,6 +1,5 @@
-from celery import shared_task
-
from .email import process_email
+from celery import shared_task
@shared_task
diff --git a/helpdesk/templated_email.py b/helpdesk/templated_email.py
index 4b20fc83..5c98cdc7 100644
--- a/helpdesk/templated_email.py
+++ b/helpdesk/templated_email.py
@@ -1,9 +1,10 @@
-import os
-import logging
-from smtplib import SMTPException
from django.conf import settings
from django.utils.safestring import mark_safe
+import logging
+import os
+from smtplib import SMTPException
+
logger = logging.getLogger('helpdesk')
@@ -50,20 +51,22 @@ def send_templated_mail(template_name,
from_string = engines['django'].from_string
from helpdesk.models import EmailTemplate
- from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \
- HELPDESK_EMAIL_FALLBACK_LOCALE
+ from helpdesk.settings import HELPDESK_EMAIL_FALLBACK_LOCALE, HELPDESK_EMAIL_SUBJECT_TEMPLATE
headers = extra_headers or {}
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
try:
- t = EmailTemplate.objects.get(template_name__iexact=template_name, locale=locale)
+ t = EmailTemplate.objects.get(
+ template_name__iexact=template_name, locale=locale)
except EmailTemplate.DoesNotExist:
try:
- t = EmailTemplate.objects.get(template_name__iexact=template_name, locale__isnull=True)
+ t = EmailTemplate.objects.get(
+ template_name__iexact=template_name, locale__isnull=True)
except EmailTemplate.DoesNotExist:
- logger.warning('template "%s" does not exist, no mail sent', template_name)
+ logger.warning(
+ 'template "%s" does not exist, no mail sent', template_name)
return # just ignore if template doesn't exist
subject_part = from_string(
@@ -77,10 +80,12 @@ def send_templated_mail(template_name,
"%s\n\n{%% include '%s' %%}" % (t.plain_text, footer_file)
).render(context)
- email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html')
+ email_html_base_file = os.path.join(
+ 'helpdesk', locale, 'email_html_base.html')
# keep new lines in html emails
if 'comment' in context:
- context['comment'] = mark_safe(context['comment'].replace('\r\n', ' '))
+ context['comment'] = mark_safe(
+ context['comment'].replace('\r\n', ' '))
html_part = from_string(
"{%% extends '%s' %%}"
@@ -112,7 +117,8 @@ def send_templated_mail(template_name,
try:
return msg.send()
except SMTPException as e:
- logger.exception('SMTPException raised while sending email to {}'.format(recipients))
+ logger.exception(
+ 'SMTPException raised while sending email to {}'.format(recipients))
if not fail_silently:
raise e
return 0
diff --git a/helpdesk/templates/helpdesk/debug.html b/helpdesk/templates/helpdesk/debug.html
index e182dcc2..85c19e8c 100644
--- a/helpdesk/templates/helpdesk/debug.html
+++ b/helpdesk/templates/helpdesk/debug.html
@@ -3,9 +3,9 @@