From 5ad04166577dda3e239985594dc7777cd0db61f9 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Sun, 12 Jun 2022 03:23:21 -0400 Subject: [PATCH 01/67] Unstable is now the 0.5.0 testing ground --- demo/setup.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/setup.py b/demo/setup.py index 463a15e0..a950c330 100644 --- a/demo/setup.py +++ b/demo/setup.py @@ -13,7 +13,7 @@ project_root = os.path.dirname(here) NAME = 'django-helpdesk-demodesk' DESCRIPTION = 'A demo Django project using django-helpdesk' README = open(os.path.join(here, 'README.rst')).read() -VERSION = '0.4.0' +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' diff --git a/setup.py b/setup.py index 3b71a534..c16d9b8a 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.util import convert_path from fnmatch import fnmatchcase from setuptools import setup, find_packages -version = '0.4.0' +version = '0.5.0a1' # Provided as an attribute, so you can append to these instead # of replicating them: From 86d3e1cb4b2aa249a502140e9418fc721fe9c275 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Wed, 20 Jul 2022 15:00:14 +0200 Subject: [PATCH 02/67] Initial github workflow Basic template to run quicktest with python versions - 3.8 - 3.9 - 3.10 --- .github/workflows/pythonpackage.yml | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/pythonpackage.yml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 00000000..6c3568c9 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,36 @@ +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 + # - name: Lint with flake8 + # run: | + # pip install flake8 + # stop the build if there are Python syntax errors or undefined names + # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pip install pytest + cd ${GITHUB_WORKSPACE} && python quicktest.py + env: + DJANGO_SETTINGS_MODULE: helpdesk.settings + From 3423627f2d7054bee6ceb3ffcf5cf2282edbcc1a Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Wed, 20 Jul 2022 15:59:28 +0200 Subject: [PATCH 03/67] Add shebang to 'quicktest.py' --- quicktest.py | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 quicktest.py diff --git a/quicktest.py b/quicktest.py old mode 100644 new mode 100755 index dc576596..0e4bc791 --- a/quicktest.py +++ b/quicktest.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python """ Usage: $ python -m venv .venv From f6caebc661aaaa1b23bc4175ec27539a5f5be74a Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Wed, 20 Jul 2022 16:01:07 +0200 Subject: [PATCH 04/67] Add requirements-testing to workflow --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6c3568c9..4d50085c 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install -r requirements.txt -r requirements-testing.txt # - name: Lint with flake8 # run: | # pip install flake8 From 70206b8f8f2e94400a58193d1220e57e802ef4ab Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Wed, 20 Jul 2022 16:10:18 +0200 Subject: [PATCH 05/67] Add django<4 constraint to workflow --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4d50085c..23add7ee 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt -r requirements-testing.txt + pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django32.txt # - name: Lint with flake8 # run: | # pip install flake8 From 08654bd9c4d092afde2c63ecf2bbeaf21ec96a13 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Wed, 20 Jul 2022 16:21:28 +0200 Subject: [PATCH 06/67] Remove 3.10 from test matrix Working through to error cause --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 23add7ee..30a3bf33 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} From fa91620bf904b07407a47528b831b23e56315ba6 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Wed, 20 Jul 2022 16:32:51 +0200 Subject: [PATCH 07/67] EscapeHtml.extendMarkdown disabled Updated markdown caused failures. Not sure what is going on with this class, 0 comments :-( If someone could give me a hint as to what this is trying to achieve, I can investigate further --- .github/workflows/pythonpackage.yml | 2 +- helpdesk/models.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 30a3bf33..23add7ee 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.8", "3.9"] + python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/helpdesk/models.py b/helpdesk/models.py index 7ef1b9c9..dcc0d9d8 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -50,9 +50,10 @@ 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): From 7e2b95b8c48c57a00b4b2fab76703c1e724b96e2 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:06:23 +0200 Subject: [PATCH 08/67] pycodestyle section added Will format with, max line length 120, recursively; inplace ignoring migrations --- .flake8 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.flake8 b/.flake8 index 0daf1543..4bbce879 100644 --- a/.flake8 +++ b/.flake8 @@ -2,3 +2,10 @@ max-line-length = 120 exclude = .git,__pycache__,.tox,.eggs,*.egg,node_modules,.venv,migrations,docs,demo,tests,setup.py import-order-style = pep8 + +[pycodestyle] +max-line-length = 120 +exclude = "migrations" +in-place = true +recursive = true + From 3fdcb41d6849927b77694b6ca58130513fd8e3a0 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:06:42 +0200 Subject: [PATCH 09/67] Add autopep8 to build pipeline --- .github/workflows/pythonpackage.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 23add7ee..dd729284 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -20,6 +20,10 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django32.txt + name: autopep8 + run: | + pip install autopep8 + autopep8 --exit-code --global-config .flake8 helpdesk # - name: Lint with flake8 # run: | # pip install flake8 From f6810865b6e32439160274026903413dae0e0999 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:08:17 +0200 Subject: [PATCH 10/67] Missing - --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index dd729284..3e998f23 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -20,7 +20,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django32.txt - name: autopep8 + - name: autopep8 run: | pip install autopep8 autopep8 --exit-code --global-config .flake8 helpdesk From 5314b55f96cae84d9d254505bdb60c54cfe6a445 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:11:31 +0200 Subject: [PATCH 11/67] Verbose step name --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3e998f23..8ed8cf34 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -20,7 +20,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django32.txt - - name: autopep8 + - name: Format style check with 'autopep8' run: | pip install autopep8 autopep8 --exit-code --global-config .flake8 helpdesk From 8d658b4c08ce4e75668a27fd4b9eb373584c976c Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:16:44 +0200 Subject: [PATCH 12/67] Add missing space after '#' --- quicktest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/quicktest.py b/quicktest.py index 0e4bc791..184b17bf 100755 --- a/quicktest.py +++ b/quicktest.py @@ -36,14 +36,14 @@ class QuickDjangoTest(object): 'django.contrib.sites', 'django.contrib.staticfiles', 'bootstrap4form', - # The following commented apps are optional, - # related to teams functionalities - #'account', - #'pinax.invitations', - #'pinax.teams', + # The following commented apps are optional, + # related to teams functionalities + # 'account', + # 'pinax.invitations', + # 'pinax.teams', 'rest_framework', 'helpdesk', - #'reversion', + # 'reversion', ) MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', From 280462c2fe7e6e0460042bbc5642cc7c9ef7c57e Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:18:45 +0200 Subject: [PATCH 13/67] Remove 'f-string', no place holders present --- helpdesk/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/models.py b/helpdesk/models.py index dcc0d9d8..0f5db4cf 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -60,7 +60,7 @@ 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 From fb21d9bcdc01f6d7e11df7053578a3072bd76575 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:20:10 +0200 Subject: [PATCH 14/67] Check for model enabled before registering --- helpdesk/admin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/helpdesk/admin.py b/helpdesk/admin.py index 6fbd352d..8beb35f1 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ -from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory -from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem +from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply +from helpdesk.models import EscalationExclusion, EmailTemplate from helpdesk.models import TicketChange, KBIAttachment, FollowUpAttachment, IgnoreEmail from helpdesk.models import CustomField from helpdesk import settings as helpdesk_settings @@ -82,9 +82,10 @@ if helpdesk_settings.HELPDESK_KB_ENABLED: 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) From e2a8b974dd82766ed4050f7d17de3b21f4273064 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:21:25 +0200 Subject: [PATCH 15/67] Removed unused imports, format long line --- helpdesk/forms.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 9aa32a69..0db3016c 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -7,7 +7,7 @@ forms.py - Definitions of newforms-based forms for creating and maintaining tickets. """ import logging -from datetime import datetime, date, time +from datetime import datetime from django.core.exceptions import ObjectDoesNotExist, ValidationError from django import forms @@ -226,7 +226,10 @@ 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: From 12bb68d5ee1a3d2f46e07a1535eec539ed2fd7d2 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 01:23:50 +0200 Subject: [PATCH 16/67] Add flake8 to workflow --- .github/workflows/pythonpackage.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 8ed8cf34..f604ea30 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -24,13 +24,13 @@ jobs: run: | pip install autopep8 autopep8 --exit-code --global-config .flake8 helpdesk - # - name: Lint with flake8 - # run: | - # pip install flake8 + - name: Lint with flake8 + run: | + pip install flake8 # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 helpdesk --count --show-source --statistics --exit-zero # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + # flake8 . --count --exit-zero --max-complexity=10 --statistics - name: Test with pytest run: | pip install pytest From b867cf5680065cb86a7aa2f894315b2419b872ef Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 02:06:36 +0200 Subject: [PATCH 17/67] Add initial tox.ini `make release` will now create a test environment free from the source. Gives isolated testing. --- tox.ini | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..beb62696 --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +minversion = 3.25.1 +requires = pytest + freezegun + +[testenv:release] +commands = pip install -r requirements-testing.txt + python quicktest.py From 25542f929ed32012900ca2103dfef7d7dbb016ad Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 03:26:41 +0200 Subject: [PATCH 18/67] Initial isort configuration Will fail build if imports are not sorted correctly --- .github/workflows/pythonpackage.yml | 3 + .isort.cfg | 19 ++++ demo/demodesk/config/settings.py | 2 + demo/demodesk/config/urls.py | 4 +- demo/demodesk/config/wsgi.py | 3 +- demo/setup.py | 12 ++- docs/conf.py | 3 +- helpdesk/admin.py | 23 +++-- helpdesk/decorators.py | 8 +- helpdesk/email.py | 36 ++++---- helpdesk/forms.py | 35 ++++--- helpdesk/lib.py | 8 +- .../commands/create_escalation_exclusions.py | 7 +- .../commands/create_queue_permissions.py | 4 +- .../commands/create_usersettings.py | 6 +- .../management/commands/escalate_tickets.py | 13 ++- helpdesk/management/commands/get_email.py | 1 - helpdesk/models.py | 34 +++---- helpdesk/query.py | 11 +-- helpdesk/serializers.py | 11 +-- helpdesk/settings.py | 5 +- helpdesk/tasks.py | 3 +- helpdesk/templated_email.py | 10 +- helpdesk/templatetags/helpdesk_staff.py | 4 +- helpdesk/templatetags/helpdesk_util.py | 7 +- helpdesk/templatetags/saved_queries.py | 1 - helpdesk/templatetags/ticket_to_link.py | 3 +- helpdesk/tests/helpers.py | 7 +- helpdesk/tests/test_api.py | 16 ++-- helpdesk/tests/test_attachments.py | 6 +- helpdesk/tests/test_get_email.py | 25 +++-- helpdesk/tests/test_kb.py | 7 +- helpdesk/tests/test_login.py | 2 +- helpdesk/tests/test_navigation.py | 14 +-- .../tests/test_per_queue_staff_permission.py | 5 +- helpdesk/tests/test_public_actions.py | 2 +- helpdesk/tests/test_query.py | 5 +- helpdesk/tests/test_savequery.py | 2 +- helpdesk/tests/test_ticket_actions.py | 10 +- helpdesk/tests/test_ticket_lookup.py | 4 +- helpdesk/tests/test_ticket_submission.py | 15 ++- helpdesk/tests/test_time_spent.py | 14 +-- helpdesk/tests/test_usersettings.py | 3 +- helpdesk/tests/urls.py | 3 +- helpdesk/urls.py | 13 ++- helpdesk/user.py | 11 +-- helpdesk/validators.py | 4 +- helpdesk/views/api.py | 9 +- helpdesk/views/feeds.py | 6 +- helpdesk/views/kb.py | 8 +- helpdesk/views/permissions.py | 1 - helpdesk/views/public.py | 26 +++--- helpdesk/views/staff.py | 91 +++++++++++-------- quicktest.py | 6 +- requirements-testing.txt | 1 + setup.py | 9 +- tox.ini | 9 ++ 57 files changed, 330 insertions(+), 280 deletions(-) create mode 100644 .isort.cfg create mode 100644 tox.ini diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index f604ea30..83ba7e3b 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -31,6 +31,9 @@ jobs: flake8 helpdesk --count --show-source --statistics --exit-zero # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # flake8 . --count --exit-zero --max-complexity=10 --statistics + - name: Sort style check with 'isort' + run: | + isort --line-length=120 --src helpdesk . --check - name: Test with pytest run: | pip install pytest 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/demo/demodesk/config/settings.py b/demo/demodesk/config/settings.py index 5aaccfbf..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__))) diff --git a/demo/demodesk/config/urls.py b/demo/demodesk/config/urls.py index 22d4f803..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.urls import include, path -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, 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/setup.py b/demo/setup.py index 310cdae1..b866210c 100644 --- a/demo/setup.py +++ b/demo/setup.py @@ -1,10 +1,20 @@ # -*- 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) diff --git a/docs/conf.py b/docs/conf.py index 6e384cb3..f79715bb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,8 +11,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys 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 diff --git a/helpdesk/admin.py b/helpdesk/admin.py index 8beb35f1..d51cf209 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -1,13 +1,24 @@ + from django.contrib import admin from django.utils.translation import gettext_lazy as _ -from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply -from helpdesk.models import EscalationExclusion, EmailTemplate -from helpdesk.models import TicketChange, KBIAttachment, FollowUpAttachment, IgnoreEmail -from helpdesk.models import CustomField from helpdesk import settings as helpdesk_settings +from helpdesk.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) 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 6fe347ac..8f92b521 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -4,23 +4,11 @@ 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 @@ -28,11 +16,23 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import Q from django.utils import encoding, timezone 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, TicketCC +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 User model, which may be a custom model @@ -342,7 +342,7 @@ 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: diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 0db3016c..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 -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 gettext_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() diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 911c7a96..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) """ -import logging -import mimetypes -from datetime import datetime, date, time +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 helpdesk.settings import CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT logger = logging.getLogger('helpdesk') diff --git a/helpdesk/management/commands/create_escalation_exclusions.py b/helpdesk/management/commands/create_escalation_exclusions.py index 9e2148d5..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 diff --git a/helpdesk/management/commands/create_queue_permissions.py b/helpdesk/management/commands/create_queue_permissions.py index 91f064dc..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 gettext_lazy as _ - from helpdesk.models import Queue +from optparse import make_option class Command(BaseCommand): diff --git a/helpdesk/management/commands/create_usersettings.py b/helpdesk/management/commands/create_usersettings.py index 2ed628a1..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 gettext 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 020a3b78..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 gettext 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): 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/models.py b/helpdesk/models.py index 0f5db4cf..291e7323 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 gettext_lazy as _, gettext -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): diff --git a/helpdesk/query.py b/helpdesk/query.py index 14da72a8..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 gettext as _ - -from base64 import b64encode -from base64 import b64decode -import json - -from model_utils import Choices - from helpdesk.serializers import DatatablesTicketSerializer +import json +from model_utils import Choices def query_to_base64(query): diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py index c4134fa2..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, FollowUp, FollowUpAttachment -from .lib import format_time_spent, process_attachments -from .user import HelpdeskUser - class DatatablesTicketSerializer(serializers.ModelSerializer): """ diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 2724c01b..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, 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 2fc83973..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,8 +51,7 @@ 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 {} diff --git a/helpdesk/templatetags/helpdesk_staff.py b/helpdesk/templatetags/helpdesk_staff.py index ab1ea3cf..621398cc 100644 --- a/helpdesk/templatetags/helpdesk_staff.py +++ b/helpdesk/templatetags/helpdesk_staff.py @@ -4,10 +4,10 @@ django-helpdesk - A Django powered ticket tracker for small enterprise. The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff. templatetags/helpdesk_staff.py """ -import logging -from django.template import Library +from django.template import Library from helpdesk.decorators import is_helpdesk_staff +import logging logger = logging.getLogger(__name__) diff --git a/helpdesk/templatetags/helpdesk_util.py b/helpdesk/templatetags/helpdesk_util.py index b096824c..2498e2e5 100644 --- a/helpdesk/templatetags/helpdesk_util.py +++ b/helpdesk/templatetags/helpdesk_util.py @@ -1,10 +1,9 @@ +from datetime import datetime +from django.conf import settings from django.template import Library from django.template.defaultfilters import date as date_filter -from django.conf import settings +from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT -from datetime import datetime - -from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT, CUSTOMFIELD_DATETIME_FORMAT register = Library() diff --git a/helpdesk/templatetags/saved_queries.py b/helpdesk/templatetags/saved_queries.py index 533e13a6..5956fc86 100644 --- a/helpdesk/templatetags/saved_queries.py +++ b/helpdesk/templatetags/saved_queries.py @@ -7,7 +7,6 @@ templatetags/saved_queries.py - This template tag returns previously saved """ from django import template from django.db.models import Q - from helpdesk.models import SavedSearch diff --git a/helpdesk/templatetags/ticket_to_link.py b/helpdesk/templatetags/ticket_to_link.py index 39683bfb..9111ff11 100644 --- a/helpdesk/templatetags/ticket_to_link.py +++ b/helpdesk/templatetags/ticket_to_link.py @@ -10,12 +10,11 @@ templatetags/ticket_to_link.py - Used in ticket comments to allow wiki-style to show the status of that ticket (eg a closed ticket would have a strikethrough). """ + from django import template from django.urls import reverse from django.utils.safestring import mark_safe - from helpdesk.models import Ticket - import re diff --git a/helpdesk/tests/helpers.py b/helpdesk/tests/helpers.py index 57775c76..fe4d65de 100644 --- a/helpdesk/tests/helpers.py +++ b/helpdesk/tests/helpers.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -import sys -from django.contrib.auth import get_user_model -from helpdesk.models import Ticket, Queue, UserSettings +from django.contrib.auth import get_user_model +from helpdesk.models import Queue, Ticket, UserSettings +import sys + User = get_user_model() diff --git a/helpdesk/tests/test_api.py b/helpdesk/tests/test_api.py index 45c4bcfe..75bf895e 100644 --- a/helpdesk/tests/test_api.py +++ b/helpdesk/tests/test_api.py @@ -1,18 +1,22 @@ + import base64 from collections import OrderedDict from datetime import datetime - +from django.contrib.auth.models import User from django.core.files.uploadedfile import SimpleUploadedFile from freezegun import freeze_time - -from django.contrib.auth.models import User +from helpdesk.models import CustomField, Queue, Ticket from rest_framework import HTTP_HEADER_ENCODING from rest_framework.exceptions import ErrorDetail -from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN +from rest_framework.status import ( + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_204_NO_CONTENT, + HTTP_400_BAD_REQUEST, + HTTP_403_FORBIDDEN +) from rest_framework.test import APITestCase -from helpdesk.models import Queue, Ticket, CustomField - class TicketTest(APITestCase): due_date = datetime(2022, 4, 10, 15, 6) diff --git a/helpdesk/tests/test_attachments.py b/helpdesk/tests/test_attachments.py index 46e7151d..b8f16b28 100644 --- a/helpdesk/tests/test_attachments.py +++ b/helpdesk/tests/test_attachments.py @@ -1,15 +1,13 @@ # vim: set fileencoding=utf-8 : + from django.core.files.uploadedfile import SimpleUploadedFile -from django.urls import reverse from django.test import override_settings, TestCase +from django.urls import reverse from django.utils.encoding import smart_str - from helpdesk import lib, models - import os import shutil from tempfile import gettempdir - from unittest import mock from unittest.case import skip diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index f1cf010e..89afa95d 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -1,24 +1,23 @@ # -*- coding: utf-8 -*- -from django.test import TestCase, override_settings + +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import User from django.core.management import call_command from django.shortcuts import get_object_or_404 -from django.contrib.auth.models import User -from django.contrib.auth.hashers import make_password - -from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, FollowUpAttachment -from helpdesk.management.commands.get_email import Command +from django.test import override_settings, TestCase import helpdesk.email - -import six +from helpdesk.management.commands.get_email import Command +from helpdesk.models import FollowUp, FollowUpAttachment, Queue, Ticket, TicketCC import itertools -from shutil import rmtree -import sys -import os -from tempfile import mkdtemp import logging - +import os +from shutil import rmtree +import six +import sys +from tempfile import mkdtemp from unittest import mock + THIS_DIR = os.path.dirname(os.path.abspath(__file__)) # class A addresses can't have first octet of 0 diff --git a/helpdesk/tests/test_kb.py b/helpdesk/tests/test_kb.py index ed1ab7ba..511aae67 100644 --- a/helpdesk/tests/test_kb.py +++ b/helpdesk/tests/test_kb.py @@ -1,11 +1,8 @@ # -*- coding: utf-8 -*- -from django.urls import reverse from django.test import TestCase - +from django.urls import reverse from helpdesk.models import KBCategory, KBItem, Queue, Ticket - -from helpdesk.tests.helpers import ( - get_staff_user, reload_urlconf, User, create_ticket, print_response) +from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User class KBTests(TestCase): diff --git a/helpdesk/tests/test_login.py b/helpdesk/tests/test_login.py index 800ae71d..ad0ebddc 100644 --- a/helpdesk/tests/test_login.py +++ b/helpdesk/tests/test_login.py @@ -1,4 +1,4 @@ -from django.test import TestCase, override_settings +from django.test import override_settings, TestCase from django.urls import reverse diff --git a/helpdesk/tests/test_navigation.py b/helpdesk/tests/test_navigation.py index 21e51ab9..fef74fce 100644 --- a/helpdesk/tests/test_navigation.py +++ b/helpdesk/tests/test_navigation.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -import sys -from importlib import reload -from django.urls import reverse -from django.test import TestCase + +from django.test import TestCase +from django.test.utils import override_settings +from django.urls import reverse from helpdesk import settings as helpdesk_settings from helpdesk.models import Queue -from helpdesk.tests.helpers import ( - get_staff_user, reload_urlconf, User, create_ticket, print_response) -from django.test.utils import override_settings +from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User +from importlib import reload +import sys class KBDisabledTestCase(TestCase): diff --git a/helpdesk/tests/test_per_queue_staff_permission.py b/helpdesk/tests/test_per_queue_staff_permission.py index c16dd7a2..b04f53bc 100644 --- a/helpdesk/tests/test_per_queue_staff_permission.py +++ b/helpdesk/tests/test_per_queue_staff_permission.py @@ -1,11 +1,10 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission -from django.urls import reverse from django.test import TestCase from django.test.client import Client - -from helpdesk.models import Queue, Ticket +from django.urls import reverse from helpdesk import settings +from helpdesk.models import Queue, Ticket from helpdesk.query import __Query__ from helpdesk.user import HelpdeskUser diff --git a/helpdesk/tests/test_public_actions.py b/helpdesk/tests/test_public_actions.py index feba1a66..f07c2fdf 100644 --- a/helpdesk/tests/test_public_actions.py +++ b/helpdesk/tests/test_public_actions.py @@ -1,7 +1,7 @@ -from helpdesk.models import Queue, Ticket from django.test import TestCase from django.test.client import Client from django.urls import reverse +from helpdesk.models import Queue, Ticket class PublicActionsTestCase(TestCase): diff --git a/helpdesk/tests/test_query.py b/helpdesk/tests/test_query.py index 43ede439..9e7d8842 100644 --- a/helpdesk/tests/test_query.py +++ b/helpdesk/tests/test_query.py @@ -1,12 +1,9 @@ from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse - from helpdesk.models import KBCategory, KBItem, Queue, Ticket from helpdesk.query import query_to_base64 - -from helpdesk.tests.helpers import ( - get_staff_user, reload_urlconf, User, create_ticket, print_response) +from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User class QueryTests(TestCase): diff --git a/helpdesk/tests/test_savequery.py b/helpdesk/tests/test_savequery.py index 5916782b..b7130d25 100644 --- a/helpdesk/tests/test_savequery.py +++ b/helpdesk/tests/test_savequery.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from django.urls import reverse from django.test import TestCase +from django.urls import reverse from helpdesk.models import Queue from helpdesk.tests.helpers import get_user diff --git a/helpdesk/tests/test_ticket_actions.py b/helpdesk/tests/test_ticket_actions.py index b0b9daec..1836f429 100644 --- a/helpdesk/tests/test_ticket_actions.py +++ b/helpdesk/tests/test_ticket_actions.py @@ -1,21 +1,21 @@ from django.contrib.auth import get_user_model from django.contrib.sites.models import Site from django.core import mail -from django.urls import reverse from django.test import TestCase from django.test.client import Client +from django.urls import reverse from django.utils import timezone - -from helpdesk.models import CustomField, Queue, Ticket from helpdesk import settings as helpdesk_settings +from helpdesk.models import CustomField, Queue, Ticket +from helpdesk.templatetags.ticket_to_link import num_to_link +from helpdesk.user import HelpdeskUser + try: # python 3 from urllib.parse import urlparse except ImportError: # python 2 from urlparse import urlparse -from helpdesk.templatetags.ticket_to_link import num_to_link -from helpdesk.user import HelpdeskUser class TicketActionsTestCase(TestCase): diff --git a/helpdesk/tests/test_ticket_lookup.py b/helpdesk/tests/test_ticket_lookup.py index e2891f95..8b3fcaba 100644 --- a/helpdesk/tests/test_ticket_lookup.py +++ b/helpdesk/tests/test_ticket_lookup.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from django.contrib.auth import get_user_model -from django.urls import reverse from django.test import TestCase -from helpdesk.models import Ticket, Queue from django.test.utils import override_settings +from django.urls import reverse +from helpdesk.models import Queue, Ticket User = get_user_model() diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 90923967..2e1da9ac 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -1,22 +1,19 @@ -import email -import uuid -from helpdesk.models import Queue, CustomField, FollowUp, Ticket, TicketCC, KBCategory, KBItem -from django.test import TestCase from django.contrib.auth import get_user_model from django.core import mail from django.core.exceptions import ObjectDoesNotExist from django.forms import ValidationError +from django.test import TestCase from django.test.client import Client from django.urls import reverse - -from helpdesk.email import object_from_message, create_ticket_cc +import email +from helpdesk.email import create_ticket_cc, object_from_message +from helpdesk.models import CustomField, FollowUp, KBCategory, KBItem, Queue, Ticket, TicketCC from helpdesk.tests.helpers import print_response - -from urllib.parse import urlparse - import logging +from urllib.parse import urlparse +import uuid logger = logging.getLogger('helpdesk') diff --git a/helpdesk/tests/test_time_spent.py b/helpdesk/tests/test_time_spent.py index 5caf7df5..ad1749c4 100644 --- a/helpdesk/tests/test_time_spent.py +++ b/helpdesk/tests/test_time_spent.py @@ -1,22 +1,24 @@ + +import datetime from django.contrib.auth import get_user_model +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core import mail -from django.urls import reverse from django.test import TestCase from django.test.client import Client -from helpdesk.models import Queue, Ticket, FollowUp +from django.urls import reverse from helpdesk import settings as helpdesk_settings -from django.contrib.auth.models import User -from django.contrib.auth.hashers import make_password +from helpdesk.models import FollowUp, Queue, Ticket +from helpdesk.templatetags.ticket_to_link import num_to_link import uuid -import datetime + try: # python 3 from urllib.parse import urlparse except ImportError: # python 2 from urlparse import urlparse -from helpdesk.templatetags.ticket_to_link import num_to_link class TimeSpentTestCase(TestCase): diff --git a/helpdesk/tests/test_usersettings.py b/helpdesk/tests/test_usersettings.py index 293adbf6..b7d72866 100644 --- a/helpdesk/tests/test_usersettings.py +++ b/helpdesk/tests/test_usersettings.py @@ -1,10 +1,11 @@ from django.contrib.auth import get_user_model from django.core import mail -from django.urls import reverse from django.test import TestCase from django.test.client import Client +from django.urls import reverse from helpdesk.models import CustomField, Queue, Ticket + try: # python 3 from urllib.parse import urlparse except ImportError: # python 2 diff --git a/helpdesk/tests/urls.py b/helpdesk/tests/urls.py index 9a96671c..e07fd6c8 100644 --- a/helpdesk/tests/urls.py +++ b/helpdesk/tests/urls.py @@ -1,5 +1,6 @@ -from django.urls import include, path from django.contrib import admin +from django.urls import include, path + urlpatterns = [ path('', include('helpdesk.urls', namespace='helpdesk')), diff --git a/helpdesk/urls.py b/helpdesk/urls.py index 9ccd4d38..f0fcceab 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -7,17 +7,16 @@ urls.py - Mapping of URL's to our various views. Note we always used NAMED views for simplicity in linking later on. """ -from django.urls import path, re_path -from django.contrib.auth.decorators import login_required from django.contrib.auth import views as auth_views -from django.urls import include +from django.contrib.auth.decorators import login_required +from django.urls import include, path, re_path from django.views.generic import TemplateView +from helpdesk import settings as helpdesk_settings +from helpdesk.decorators import helpdesk_staff_member_required, protect_view +from helpdesk.views import feeds, login, public, staff +from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet from rest_framework.routers import DefaultRouter -from helpdesk.decorators import helpdesk_staff_member_required, protect_view -from helpdesk.views import feeds, staff, public, login -from helpdesk import settings as helpdesk_settings -from helpdesk.views.api import TicketViewSet, CreateUserView, FollowUpViewSet, FollowUpAttachmentViewSet if helpdesk_settings.HELPDESK_KB_ENABLED: from helpdesk.views import kb diff --git a/helpdesk/user.py b/helpdesk/user.py index eb11d5d0..e21fd064 100644 --- a/helpdesk/user.py +++ b/helpdesk/user.py @@ -1,15 +1,10 @@ -from helpdesk.models import ( - Ticket, - Queue -) from helpdesk import settings as helpdesk_settings +from helpdesk.models import Queue, Ticket + if helpdesk_settings.HELPDESK_KB_ENABLED: - from helpdesk.models import ( - KBCategory, - KBItem - ) + from helpdesk.models import KBCategory, KBItem def huser_from_request(req): diff --git a/helpdesk/validators.py b/helpdesk/validators.py index bd2cd522..08086e1f 100644 --- a/helpdesk/validators.py +++ b/helpdesk/validators.py @@ -2,16 +2,18 @@ # # validators for file uploads, etc. + from django.conf import settings + # TODO: can we use the builtin Django validator instead? # see: # https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator def validate_file_extension(value): - import os from django.core.exceptions import ValidationError + import os ext = os.path.splitext(value.name)[1] # [0] returns path+filename # TODO: we might improve this with more thorough checks of file types # rather than just the extensions. diff --git a/helpdesk/views/api.py b/helpdesk/views/api.py index d217a1a4..7e3f0448 100644 --- a/helpdesk/views/api.py +++ b/helpdesk/views/api.py @@ -1,11 +1,10 @@ +from django.contrib.auth import get_user_model +from helpdesk.models import FollowUp, FollowUpAttachment, Ticket +from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer from rest_framework import viewsets +from rest_framework.mixins import CreateModelMixin from rest_framework.permissions import IsAdminUser from rest_framework.viewsets import GenericViewSet -from rest_framework.mixins import CreateModelMixin -from django.contrib.auth import get_user_model - -from helpdesk.models import Ticket, FollowUp, FollowUpAttachment -from helpdesk.serializers import TicketSerializer, UserSerializer, FollowUpSerializer, FollowUpAttachmentSerializer class TicketViewSet(viewsets.ModelViewSet): diff --git a/helpdesk/views/feeds.py b/helpdesk/views/feeds.py index 7975fa4e..cccf4b19 100644 --- a/helpdesk/views/feeds.py +++ b/helpdesk/views/feeds.py @@ -9,12 +9,12 @@ views/feeds.py - A handful of staff-only RSS feeds to provide ticket details from django.contrib.auth import get_user_model from django.contrib.syndication.views import Feed -from django.urls import reverse from django.db.models import Q -from django.utils.translation import gettext as _ from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext as _ +from helpdesk.models import FollowUp, Queue, Ticket -from helpdesk.models import Ticket, FollowUp, Queue User = get_user_model() diff --git a/helpdesk/views/kb.py b/helpdesk/views/kb.py index 55b3424d..1f619a65 100644 --- a/helpdesk/views/kb.py +++ b/helpdesk/views/kb.py @@ -8,12 +8,10 @@ views/kb.py - Public-facing knowledgebase views. The knowledgebase is a resolutions to common problems. """ -from django.http import HttpResponseRedirect, Http404 -from django.shortcuts import render, get_object_or_404 +from django.http import Http404, HttpResponseRedirect +from django.shortcuts import get_object_or_404, render from django.views.decorators.clickjacking import xframe_options_exempt - -from helpdesk import settings as helpdesk_settings -from helpdesk import user +from helpdesk import settings as helpdesk_settings, user from helpdesk.models import KBCategory, KBItem diff --git a/helpdesk/views/permissions.py b/helpdesk/views/permissions.py index 955e71c9..3ebd556c 100644 --- a/helpdesk/views/permissions.py +++ b/helpdesk/views/permissions.py @@ -1,5 +1,4 @@ from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin - from helpdesk.decorators import is_helpdesk_staff diff --git a/helpdesk/views/public.py b/helpdesk/views/public.py index 7a0b28bf..dfe42e53 100644 --- a/helpdesk/views/public.py +++ b/helpdesk/views/public.py @@ -6,30 +6,29 @@ django-helpdesk - A Django powered ticket tracker for small enterprise. views/public.py - All public facing views, eg non-staff (no authentication required) views. """ -import logging -from importlib import import_module -from django.core.exceptions import ( - ObjectDoesNotExist, PermissionDenied, ImproperlyConfigured, -) -from django.urls import reverse + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied from django.http import HttpResponseRedirect from django.shortcuts import render -from urllib.parse import quote +from django.urls import reverse from django.utils.translation import gettext as _ -from django.conf import settings from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt from django.views.generic.base import TemplateView from django.views.generic.edit import FormView - from helpdesk import settings as helpdesk_settings -from helpdesk.decorators import protect_view, is_helpdesk_staff -import helpdesk.views.staff as staff -import helpdesk.views.abstract_views as abstract_views +from helpdesk.decorators import is_helpdesk_staff, protect_view from helpdesk.lib import text_is_spam -from helpdesk.models import Ticket, Queue, UserSettings +from helpdesk.models import Queue, Ticket, UserSettings from helpdesk.user import huser_from_request +import helpdesk.views.abstract_views as abstract_views +import helpdesk.views.staff as staff +from importlib import import_module +import logging +from urllib.parse import quote + logger = logging.getLogger(__name__) @@ -212,6 +211,7 @@ def view_ticket(request): if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS: from helpdesk.views.staff import update_ticket + # Trick the update_ticket() view into thinking it's being called with # a valid POST. request.POST = { diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 94117637..38ba3743 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -6,67 +6,78 @@ django-helpdesk - A Django powered ticket tracker for small enterprise. views/staff.py - The bulk of the application - provides most business logic and renders all staff-facing views. """ -from copy import deepcopy -import json +from ..lib import format_time_spent +from ..templated_email import send_templated_mail +from copy import deepcopy +from datetime import date, datetime, timedelta from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.decorators import user_passes_test from django.contrib.contenttypes.models import ContentType -from django.urls import reverse, reverse_lazy -from django.core.exceptions import ValidationError, PermissionDenied -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.exceptions import PermissionDenied, ValidationError +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Q -from django.http import HttpResponseRedirect, Http404, HttpResponse, JsonResponse -from django.shortcuts import render, get_object_or_404, redirect -from django.utils.translation import gettext as _ -from django.utils.html import escape +from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse, reverse_lazy from django.utils import timezone +from django.utils.html import escape +from django.utils.translation import gettext as _ from django.views.decorators.csrf import requires_csrf_token from django.views.generic.edit import FormView, UpdateView - -from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT -from helpdesk.query import ( - get_query_class, - query_to_base64, - query_from_base64, -) - -from helpdesk.user import HelpdeskUser - +from helpdesk import settings as helpdesk_settings from helpdesk.decorators import ( - helpdesk_staff_member_required, helpdesk_superuser_required, - is_helpdesk_staff + helpdesk_staff_member_required, + helpdesk_superuser_required, + is_helpdesk_staff, + superuser_required ) from helpdesk.forms import ( - TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm, - TicketCCEmailForm, TicketCCUserForm, EditFollowUpForm, TicketDependencyForm, MultipleTicketSelectForm -) -from helpdesk.decorators import superuser_required -from helpdesk.lib import ( - safe_template_context, - process_attachments, - queue_template_context, + CUSTOMFIELD_DATE_FORMAT, + EditFollowUpForm, + EditTicketForm, + EmailIgnoreForm, + MultipleTicketSelectForm, + TicketCCEmailForm, + TicketCCForm, + TicketCCUserForm, + TicketDependencyForm, + TicketForm, + UserSettingsForm ) +from helpdesk.lib import process_attachments, queue_template_context, safe_template_context from helpdesk.models import ( - Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch, - IgnoreEmail, TicketCC, TicketDependency, UserSettings, CustomField, TicketCustomFieldValue, + CustomField, + FollowUp, + FollowUpAttachment, + IgnoreEmail, + PreSetReply, + Queue, + SavedSearch, + Ticket, + TicketCC, + TicketChange, + TicketCustomFieldValue, + TicketDependency, + UserSettings ) -from helpdesk import settings as helpdesk_settings -if helpdesk_settings.HELPDESK_KB_ENABLED: - from helpdesk.models import (KBItem) - +from helpdesk.query import get_query_class, query_from_base64, query_to_base64 +from helpdesk.user import HelpdeskUser import helpdesk.views.abstract_views as abstract_views from helpdesk.views.permissions import MustBeStaffMixin -from ..lib import format_time_spent - +import json +import re from rest_framework import status from rest_framework.decorators import api_view -from datetime import date, datetime, timedelta -import re -from ..templated_email import send_templated_mail +if helpdesk_settings.HELPDESK_KB_ENABLED: + from helpdesk.models import KBItem + + + + User = get_user_model() Query = get_query_class() diff --git a/quicktest.py b/quicktest.py index 184b17bf..c5001732 100755 --- a/quicktest.py +++ b/quicktest.py @@ -6,12 +6,12 @@ $ source .venv/bin/activate $ pip install -r requirements-testing.txt -r requirements.txt $ python ./quicktest.py """ -import os -import sys -import argparse +import argparse import django from django.conf import settings +import os +import sys class QuickDjangoTest(object): diff --git a/requirements-testing.txt b/requirements-testing.txt index c07a8ced..12adeae7 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -6,3 +6,4 @@ argparse pbr mock freezegun +isort diff --git a/setup.py b/setup.py index 1aac84d1..720de9da 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,14 @@ -import os -import sys +"""django-helpdesk setup""" from distutils.util import convert_path from fnmatch import fnmatchcase -from setuptools import setup, find_packages +import os +from setuptools import find_packages, setup +import sys + version = '0.5.0a1' + # Provided as an attribute, so you can append to these instead # of replicating them: standard_exclude = ("*.py", "*.pyc", "*$py.class", "*~", ".*", "*.bak") diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..05309446 --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[tox] +minversion = 3.25.1 +requires = pytest + freezegun + + +[testenv:release] +commands = pip install -r requirements-testing.txt + python quicktest.py From c2e933b1fbc3edbb1f5c7ffefbad0f07a08b5102 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 03:28:46 +0200 Subject: [PATCH 19/67] Removed future import --- demo/setup.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/demo/setup.py b/demo/setup.py index b866210c..bacc93a2 100644 --- a/demo/setup.py +++ b/demo/setup.py @@ -1,17 +1,5 @@ # -*- coding: utf-8 -*- """Python packaging.""" - - - - - - - - - - -from __future__ import unicode_literals - import os from setuptools import setup From 44f068d1b8c405cf79b295f6ea2de1804158b7ba Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 03:30:35 +0200 Subject: [PATCH 20/67] re-apply formatting --- helpdesk/tests/test_ticket_actions.py | 1 - helpdesk/tests/test_time_spent.py | 1 - helpdesk/views/staff.py | 3 --- 3 files changed, 5 deletions(-) diff --git a/helpdesk/tests/test_ticket_actions.py b/helpdesk/tests/test_ticket_actions.py index 1836f429..d3809005 100644 --- a/helpdesk/tests/test_ticket_actions.py +++ b/helpdesk/tests/test_ticket_actions.py @@ -17,7 +17,6 @@ except ImportError: # python 2 from urlparse import urlparse - class TicketActionsTestCase(TestCase): fixtures = ['emailtemplate.json'] diff --git a/helpdesk/tests/test_time_spent.py b/helpdesk/tests/test_time_spent.py index ad1749c4..f89214e3 100644 --- a/helpdesk/tests/test_time_spent.py +++ b/helpdesk/tests/test_time_spent.py @@ -20,7 +20,6 @@ except ImportError: # python 2 from urlparse import urlparse - class TimeSpentTestCase(TestCase): def setUp(self): diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 38ba3743..21e25d92 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -76,9 +76,6 @@ if helpdesk_settings.HELPDESK_KB_ENABLED: from helpdesk.models import KBItem - - - User = get_user_model() Query = get_query_class() From 0f0b0a59957b642f2d62f56895c83692ca339360 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 22 Jul 2022 16:27:38 +0200 Subject: [PATCH 21/67] Remove left over 'cc' code --- helpdesk/email.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 8f92b521..983ff644 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -539,19 +539,6 @@ def object_from_message(message, queue, logger): sender_email = email.utils.getaddresses( ['\"' + sender.replace('<', '\" <')])[0][1] - cc = message.get_all('cc', None) - if cc: - # first, fixup the encoding if necessary - cc = [decode_mail_headers(decodeUnknown( - message.get_charset(), x)) for x in cc] - # get_all checks if multiple CC headers, but individual emails may be - # comma separated too - tempcc = [] - for hdr in cc: - tempcc.extend(hdr.split(',')) - # use a set to ensure no duplicates - cc = set([x.strip() for x in tempcc]) - for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)): if ignore.test(sender_email): if ignore.keep_in_mailbox: From 58ed521a2c19552edbccf91650cc04f43ef4666f Mon Sep 17 00:00:00 2001 From: chrisbroderick Date: Sat, 23 Jul 2022 11:58:28 +0100 Subject: [PATCH 22/67] Add the file system deletion app for attachments into INSTALLED_APPS --- docs/install.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/install.rst b/docs/install.rst index 743d34e8..054398af 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -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 fiels on the file system when the associated record is deleted in the database 'helpdesk', # This is us! ) From 0d85acc46006985bc9a745ef6cd340099e4a942c Mon Sep 17 00:00:00 2001 From: chrisbroderick Date: Sat, 23 Jul 2022 12:14:36 +0100 Subject: [PATCH 23/67] Add Django cleanup app for attachment file system deletion --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 97472289..7cc1c79c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ six pinax_teams djangorestframework django-model-utils +django-cleanup From bdd8e394219cae97222ccfbe9a8deb35d3d4b0ff Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Sat, 23 Jul 2022 17:18:01 -0400 Subject: [PATCH 24/67] Update install.rst Fix a docs misspelling --- docs/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index 054398af..249645d8 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -63,7 +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 fiels on the file system when the associated record is deleted in the database + '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! ) From cf804a586aca06be8dad141ef611e263862c568d Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 01:16:46 +0200 Subject: [PATCH 25/67] Add verbosity argument to quicktest Enables verbose output for analysis on what is happening --- quicktest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/quicktest.py b/quicktest.py index c5001732..e387be6e 100755 --- a/quicktest.py +++ b/quicktest.py @@ -14,7 +14,7 @@ import os import sys -class QuickDjangoTest(object): +class QuickDjangoTest: """ A quick way to run the Django test suite without a fully-configured project. @@ -78,6 +78,7 @@ class QuickDjangoTest(object): def __init__(self, *args, **kwargs): self.tests = args + self.kwargs = kwargs or {"verbosity": 1} self._tests() def _tests(self): @@ -112,7 +113,7 @@ class QuickDjangoTest(object): ) from django.test.runner import DiscoverRunner - test_runner = DiscoverRunner(verbosity=1) + test_runner = DiscoverRunner(verbosity=self.kwargs["verbosity"]) django.setup() failures = test_runner.run_tests(self.tests) @@ -134,7 +135,8 @@ if __name__ == '__main__': description="Run Django tests." ) parser.add_argument('tests', nargs="*", type=str) + parser.add_argument("--verbosity", "-v", nargs="?", type=int, default=1) args = parser.parse_args() if not args.tests: args.tests = ['helpdesk'] - QuickDjangoTest(*args.tests) + QuickDjangoTest(*args.tests, verbosity=args.verbosity) From e863609cbecb64183c423ca66702f7d7255a9ab5 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 01:17:12 +0200 Subject: [PATCH 26/67] Add complexity to flake8 configuration Set to maximum value 20 --- .flake8 | 1 + 1 file changed, 1 insertion(+) diff --git a/.flake8 b/.flake8 index 4bbce879..80233cdf 100644 --- a/.flake8 +++ b/.flake8 @@ -2,6 +2,7 @@ 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 From 1ac78955c03c218c73db50fb04ec262d905e51e4 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 01:22:05 +0200 Subject: [PATCH 27/67] Removed `notifications_to_be_sent` list The whole loop appeared to be doing nothing other than appending email addresses to a list, which was never used. --- helpdesk/email.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 8f92b521..54b592ca 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -466,16 +466,6 @@ 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( From 0b1de1eeadaff998ca22a37a3ac83e8258952b48 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 01:23:12 +0200 Subject: [PATCH 28/67] Removed unused import Result of previous code removal --- helpdesk/email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index ce27e093..2606f766 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -21,7 +21,7 @@ from email.utils import getaddresses from email_reply_parser import EmailReplyParser from helpdesk import settings from helpdesk.lib import process_attachments, safe_template_context -from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket, TicketCC +from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket import imaplib import logging import mimetypes From 80f415230174db7af88ac68e72ad9fb47fa9c0e9 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 01:43:43 +0200 Subject: [PATCH 29/67] Simplify return statement Rename `ticket` to `ticket_id` for clarity --- helpdesk/email.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 2606f766..c7ab39ce 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -531,20 +531,18 @@ def object_from_message(message, queue, logger): 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)) + ticket_id = matchobj.group('id') + logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket_id)) else: logger.info("No tracking ID matched.") - ticket = None + ticket_id = None body = None full_body = None @@ -568,7 +566,7 @@ 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): + 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 body_parts = [] @@ -690,4 +688,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) From 283f052c0e72ce9d282eb290707b7bac221cd6ee Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 01:47:00 +0200 Subject: [PATCH 30/67] Annotate function signature --- helpdesk/email.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index c7ab39ce..8f998823 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -506,7 +506,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) return ticket -def object_from_message(message, queue, logger): +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) From a5e74d6449a2a78690e75e276efa0e021b643ec1 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 01:56:13 +0200 Subject: [PATCH 31/67] Extract getting ticket_id from subject to helper function --- helpdesk/email.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 8f998823..43ff0195 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -33,6 +33,7 @@ import socket import ssl import sys from time import ctime +import typing # import User model, which may be a custom model @@ -506,6 +507,27 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) return ticket +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 object_from_message(message: str, queue: Queue, logger: logging.Logger @@ -538,14 +560,11 @@ def object_from_message(message: str, # 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_id = matchobj.group('id') - logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket_id)) - else: - logger.info("No tracking ID matched.") - ticket_id = None + ticket_id: typing.Optional[int] = get_ticket_id_from_subject_slug( + queue.slug, + subject, + logger + ) body = None full_body = None From 4e2b7deefb9d709a08f25a1fce47c284a78d9686 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 02:22:32 +0200 Subject: [PATCH 32/67] Reduces complexity of `object_from_message` Helper functions created to help break up the flow --- helpdesk/email.py | 78 ++++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 43ff0195..9a3a7076 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -528,6 +528,52 @@ def get_ticket_id_from_subject_slug( return ticket_id +def add_file_if_always_save_incoming_email_message( + files_: list[SimpleUploadedFile], + 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.encode('utf-8') + + +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 @@ -591,29 +637,17 @@ def object_from_message(message: str, 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 - body_parts = [] - for f in EmailReplyParser.read(body).fragments: - body_parts.append(f.content) - full_body = '\n\n'.join(body_parts) - body = EmailReplyParser.parse_reply(body) + body = EmailReplyParser.parse_reply(get_body_from_fragments(body)) else: # 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_str( - part.get_payload(decode=True)) - except UnicodeDecodeError: - email_body = encoding.smart_str( - 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 @@ -680,19 +714,7 @@ def object_from_message(message: str, 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', '') From 8d63d65a7d5e0ab94a5a95ae4be319c4a1363720 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 02:41:40 +0200 Subject: [PATCH 33/67] Removed encoding to 'utf-8', breaks tests. This needs to be looked into further. --- helpdesk/email.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 9a3a7076..cac0d788 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -553,7 +553,7 @@ def get_encoded_body(body: str) -> str: try: return body.encode('ascii').decode('unicode_escape') except UnicodeEncodeError: - return body.encode('utf-8') + return body def get_body_from_fragments(body) -> str: @@ -637,7 +637,8 @@ def object_from_message(message: str, 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 - body = EmailReplyParser.parse_reply(get_body_from_fragments(body)) + full_body = get_body_from_fragments(body) + body = EmailReplyParser.parse_reply(body) else: # second and other reply, save only first part of the # message From 574395ee2814486e6bdca54dad83081279cb20b7 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 02:46:33 +0200 Subject: [PATCH 34/67] Easy pickings Simple code violations of reserved symbols etc. --- helpdesk/views/staff.py | 50 ++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 21e25d92..e7d8a12a 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -268,7 +268,7 @@ def followup_edit(request, ticket_id, followup_id): 'time_spent': format_time_spent(followup.time_spent), }) - ticketcc_string, show_subscribe = \ + ticketcc_string, __ = \ return_ticketccstring_and_show_subscribe(request.user, ticket) return render(request, 'helpdesk/followup_edit.html', { @@ -346,8 +346,10 @@ def view_ticket(request, ticket_id): if 'subscribe' in request.GET: # Allow the user to subscribe him/herself to the ticket whilst viewing # it. - ticket_cc, show_subscribe = \ - return_ticketccstring_and_show_subscribe(request.user, ticket) + show_subscribe = return_ticketccstring_and_show_subscribe( + request.user, ticket + )[1] + if show_subscribe: subscribe_staff_member_to_ticket(ticket, request.user) return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) @@ -760,8 +762,10 @@ def update_ticket(request, ticket_id, public=False): # auto subscribe user if enabled if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated: - ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe( - request.user, ticket) + SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe( + request.user, ticket + )[1] + if SHOW_SUBSCRIBE: subscribe_staff_member_to_ticket(ticket, request.user) @@ -924,7 +928,7 @@ def merge_tickets(request): for ticket in tickets: ticket.values = {} # Prepare the value for each attributes of this ticket - for attribute, display_name in ticket_attributes: + for attribute, __ in ticket_attributes: value = getattr(ticket, attribute, default) # Check if attr is a get_FIELD_display if attribute.startswith('get_') and attribute.endswith('_display'): @@ -959,7 +963,7 @@ def merge_tickets(request): ) else: # Save ticket fields values - for attribute, display_name in ticket_attributes: + for attribute, __ in ticket_attributes: id_for_attribute = request.POST.get(attribute) if id_for_attribute != chosen_ticket.id: try: @@ -1086,16 +1090,16 @@ def ticket_list(request): if request.GET.get('search_type', None) == 'header': query = request.GET.get('q') - filter = None + filter_ = None if query.find('-') > 0: try: - queue, id = Ticket.queue_and_id_from_query(query) - id = int(id) + queue, id_ = Ticket.queue_and_id_from_query(query) + id_ = int(id) except ValueError: - id = None + id_ = None - if id: - filter = {'queue__slug': queue, 'id': id} + if id_: + filter_ = {'queue__slug': queue, 'id': id_} else: try: query = int(query) @@ -1103,11 +1107,11 @@ def ticket_list(request): query = None if query: - filter = {'id': int(query)} + filter_ = {'id': int(query)} - if filter: + if filter_: try: - ticket = huser.get_tickets_in_queues().get(**filter) + ticket = huser.get_tickets_in_queues().get(**filter_) return HttpResponseRedirect(ticket.staff_url) except Ticket.DoesNotExist: # Go on to standard keyword searching @@ -1308,15 +1312,15 @@ class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixi @helpdesk_staff_member_required -def raw_details(request, type): +def raw_details(request, type_): # TODO: This currently only supports spewing out 'PreSetReply' objects, # in the future it needs to be expanded to include other items. All it # does is return a plain-text representation of an object. - if type not in ('preset',): + if type_ not in ('preset',): raise Http404 - if type == 'preset' and request.GET.get('id', False): + if type_ == 'preset' and request.GET.get('id', False): try: preset = PreSetReply.objects.get(id=request.GET.get('id')) return HttpResponse(preset.body) @@ -1634,8 +1638,8 @@ save_query = staff_member_required(save_query) @helpdesk_staff_member_required -def delete_saved_query(request, id): - query = get_object_or_404(SavedSearch, id=id, user=request.user) +def delete_saved_query(request, pk): + query = get_object_or_404(SavedSearch, id=pk, user=request.user) if request.method == 'POST': query.delete() @@ -1684,8 +1688,8 @@ email_ignore_add = superuser_required(email_ignore_add) @helpdesk_superuser_required -def email_ignore_del(request, id): - ignore = get_object_or_404(IgnoreEmail, id=id) +def email_ignore_del(request, pk): + ignore = get_object_or_404(IgnoreEmail, id=pk) if request.method == 'POST': ignore.delete() return HttpResponseRedirect(reverse('helpdesk:email_ignore')) From ecefd5e407355d41fbacbb028c43d3fbf7f42491 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:01:50 +0200 Subject: [PATCH 35/67] Extract the `due_date` to helper function --- helpdesk/views/staff.py | 81 +++++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index e7d8a12a..fae230a6 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -16,6 +16,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.decorators import user_passes_test from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied, ValidationError +from django.core.handlers.wsgi import WSGIRequest from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Q from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse @@ -70,11 +71,15 @@ import json import re from rest_framework import status from rest_framework.decorators import api_view +import typing if helpdesk_settings.HELPDESK_KB_ENABLED: from helpdesk.models import KBItem +DATE_RE: re.Pattern = re.compile( + r'(?P\d{1,2})/(?P\d{1,2})/(?P\d{4})$' +) User = get_user_model() Query = get_query_class() @@ -483,10 +488,20 @@ def subscribe_staff_member_to_ticket(ticket, user, email='', can_view=True, can_ return subscribe_to_ticket_updates(ticket=ticket, user=user, email=email, can_view=can_view, can_update=can_update) -def update_ticket(request, ticket_id, public=False): +def get_ticket_from_request_with_authorisation( + request: WSGIRequest, + ticket_id: str, + public: bool +) -> typing.Union[ + Ticket, typing.NoReturn +]: + """Gets a ticket from the public status and if the user is authenticated and + has permissions to update tickets - ticket = None + Raises: + Http404 when the ticket can not be found or the user lacks permission + """ if not (public or ( request.user.is_authenticated and request.user.is_active and ( @@ -508,41 +523,29 @@ def update_ticket(request, ticket_id, public=False): '%s?next=%s' % (reverse('helpdesk:login'), request.path) ) - if not ticket: - ticket = get_object_or_404(Ticket, id=ticket_id) + return get_object_or_404(Ticket, id=ticket_id) - date_re = re.compile( - r'(?P\d{1,2})/(?P\d{1,2})/(?P\d{4})$' - ) - comment = request.POST.get('comment', '') - new_status = int(request.POST.get('new_status', ticket.status)) - title = request.POST.get('title', '') - public = request.POST.get('public', False) - owner = int(request.POST.get('owner', -1)) - priority = int(request.POST.get('priority', ticket.priority)) - due_date_year = int(request.POST.get('due_date_year', 0)) - due_date_month = int(request.POST.get('due_date_month', 0)) - due_date_day = int(request.POST.get('due_date_day', 0)) - if request.POST.get("time_spent"): - (hours, minutes) = [int(f) - for f in request.POST.get("time_spent").split(":")] - time_spent = timedelta(hours=hours, minutes=minutes) - else: - time_spent = None - # NOTE: jQuery's default for dates is mm/dd/yy - # very US-centric but for now that's the only format supported - # until we clean up code to internationalize a little more +def get_due_date_from_request_or_ticket( + request: WSGIRequest, + ticket: Ticket +) -> typing.Optional[datetime.date]: + """Tries to locate the due date for a ticket from the `request.POST` + 'due_date' parameter or the `due_date_*` paramaters. + """ due_date = request.POST.get('due_date', None) or None if due_date is not None: # based on Django code to parse dates: # https://docs.djangoproject.com/en/2.0/_modules/django/utils/dateparse/ - match = date_re.match(due_date) + match = DATE_RE.match(due_date) if match: kw = {k: int(v) for k, v in match.groupdict().items()} due_date = date(**kw) else: + due_date_year = int(request.POST.get('due_date_year', 0)) + due_date_month = int(request.POST.get('due_date_month', 0)) + due_date_day = int(request.POST.get('due_date_day', 0)) # old way, probably deprecated? if not (due_date_year and due_date_month and due_date_day): due_date = ticket.due_date @@ -553,9 +556,31 @@ def update_ticket(request, ticket_id, public=False): due_date = ticket.due_date else: due_date = timezone.now() - due_date = due_date.replace( - due_date_year, due_date_month, due_date_day) + due_date = due_date.replace( + due_date_year, due_date_month, due_date_day) + return due_date + +def update_ticket(request, ticket_id, public=False): + + ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public) + + comment = request.POST.get('comment', '') + new_status = int(request.POST.get('new_status', ticket.status)) + title = request.POST.get('title', '') + public = request.POST.get('public', False) + owner = int(request.POST.get('owner', -1)) + priority = int(request.POST.get('priority', ticket.priority)) + if request.POST.get("time_spent"): + (hours, minutes) = [int(f) + for f in request.POST.get("time_spent").split(":")] + time_spent = timedelta(hours=hours, minutes=minutes) + else: + time_spent = None + # NOTE: jQuery's default for dates is mm/dd/yy + # very US-centric but for now that's the only format supported + # until we clean up code to internationalize a little more + due_date = get_due_date_from_request_or_ticket(request, ticket) no_changes = all([ not request.FILES, not comment, From 9294eca5d6417d590cf7c698c40072e954527fd2 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:11:30 +0200 Subject: [PATCH 36/67] Add `get_and_set_ticket_status` helper Extracts some futher code from `update_ticket` --- helpdesk/views/staff.py | 51 ++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index fae230a6..cec32bf4 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -561,6 +561,38 @@ def get_due_date_from_request_or_ticket( return due_date +def get_and_set_ticket_status( + new_status: str, + ticket: Ticket, + follow_up: FollowUp +) -> tuple[str, str]: + """Performs comparision on previous status to new status, + updating the title as required. + + Returns: + The old status as a display string, old status code string + """ + old_status_str = ticket.get_status_display() + old_status = ticket.status + if new_status != ticket.status: + ticket.status = new_status + ticket.save() + follow_up.new_status = new_status + if follow_up.title: + follow_up.title += ' and %s' % ticket.get_status_display() + else: + follow_up.title = '%s' % ticket.get_status_display() + + if not follow_up.title: + if follow_up.comment: + follow_up.title = _('Comment') + else: + follow_up.title = _('Updated') + + follow_up.save() + return (old_status_str, old_status) + + def update_ticket(request, ticket_id, public=False): ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public) @@ -640,24 +672,7 @@ def update_ticket(request, ticket_id, public=False): f.title = _('Unassigned') ticket.assigned_to = None - old_status_str = ticket.get_status_display() - old_status = ticket.status - if new_status != ticket.status: - ticket.status = new_status - ticket.save() - f.new_status = new_status - if f.title: - f.title += ' and %s' % ticket.get_status_display() - else: - f.title = '%s' % ticket.get_status_display() - - if not f.title: - if f.comment: - f.title = _('Comment') - else: - f.title = _('Updated') - - f.save() + old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f) files = [] if request.FILES: From f815ebbb5c73e5af339bf6d688cb9712658eaa9b Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:17:10 +0200 Subject: [PATCH 37/67] Add `get_time_spent_from_request` helper Extracts further code --- helpdesk/views/staff.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index cec32bf4..635f5352 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -593,6 +593,14 @@ def get_and_set_ticket_status( return (old_status_str, old_status) +def get_time_spent_from_request(request: WSGIRequest) -> typing.Optional[timedelta]: + if request.POST.get("time_spent"): + (hours, minutes) = [int(f) + for f in request.POST.get("time_spent").split(":")] + return timedelta(hours=hours, minutes=minutes) + return None + + def update_ticket(request, ticket_id, public=False): ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public) @@ -603,12 +611,8 @@ def update_ticket(request, ticket_id, public=False): public = request.POST.get('public', False) owner = int(request.POST.get('owner', -1)) priority = int(request.POST.get('priority', ticket.priority)) - if request.POST.get("time_spent"): - (hours, minutes) = [int(f) - for f in request.POST.get("time_spent").split(":")] - time_spent = timedelta(hours=hours, minutes=minutes) - else: - time_spent = None + + time_spent = get_time_spent_from_request(request) # NOTE: jQuery's default for dates is mm/dd/yy # very US-centric but for now that's the only format supported # until we clean up code to internationalize a little more @@ -674,9 +678,7 @@ def update_ticket(request, ticket_id, public=False): old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f) - files = [] - if request.FILES: - files = process_attachments(f, request.FILES.getlist('attachment')) + files = process_attachments(f, request.FILES.getlist('attachment')) if request.FILES else [] if title and title != ticket.title: c = TicketChange( From fe619b5ff2183504ef45d7191b6aea514c45977e Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:22:59 +0200 Subject: [PATCH 38/67] Combine conditionals to single line --- helpdesk/views/staff.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 635f5352..28a170ff 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -728,9 +728,12 @@ def update_ticket(request, ticket_id, public=False): c.save() ticket.due_date = due_date - if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS): - if new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None: - ticket.resolution = comment + if new_status in ( + Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS + ) and ( + new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None + ): + ticket.resolution = comment # ticket might have changed above, so we re-instantiate context with the # (possibly) updated ticket. From f678c63496653e0903361451f9b56df3ca45204f Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:30:07 +0200 Subject: [PATCH 39/67] Add `update_messages_sent_to_by_public_and_status` helper function Handles updating ticket and sending ticket reply --- helpdesk/views/staff.py | 67 ++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 28a170ff..ac2365a5 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -601,6 +601,47 @@ def get_time_spent_from_request(request: WSGIRequest) -> typing.Optional[timedel return None +def update_messages_sent_to_by_public_and_status( + public: bool, + ticket: Ticket, + follow_up: FollowUp, + context: str, + messages_sent_to: list[str], + files: list[str, str] +) -> Ticket: + """Sets the status of the ticket""" + if public and ( + follow_up.comment or ( + follow_up.new_status in ( + Ticket.RESOLVED_STATUS, + Ticket.CLOSED_STATUS + ) + ) + ): + if follow_up.new_status == Ticket.RESOLVED_STATUS: + template = 'resolved_' + elif follow_up.new_status == Ticket.CLOSED_STATUS: + template = 'closed_' + else: + template = 'updated_' + + roles = { + 'submitter': (template + 'submitter', context), + 'ticket_cc': (template + 'cc', context), + } + if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change: + roles['assigned_to'] = (template + 'cc', context) + messages_sent_to.update( + ticket.send( + roles, + dont_send_to=messages_sent_to, + fail_silently=True, + files=files + ) + ) + return ticket + + def update_ticket(request, ticket_id, public=False): ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public) @@ -748,24 +789,14 @@ def update_ticket(request, ticket_id, public=False): messages_sent_to.add(request.user.email) except AttributeError: pass - if public and (f.comment or ( - f.new_status in (Ticket.RESOLVED_STATUS, - Ticket.CLOSED_STATUS))): - if f.new_status == Ticket.RESOLVED_STATUS: - template = 'resolved_' - elif f.new_status == Ticket.CLOSED_STATUS: - template = 'closed_' - else: - template = 'updated_' - - roles = { - 'submitter': (template + 'submitter', context), - 'ticket_cc': (template + 'cc', context), - } - if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change: - roles['assigned_to'] = (template + 'cc', context) - messages_sent_to.update(ticket.send( - roles, dont_send_to=messages_sent_to, fail_silently=True, files=files,)) + ticket = update_messages_sent_to_by_public_and_status( + public, + ticket, + f, + context, + messages_sent_to, + files + ) if reassigned: template_staff = 'assigned_owner' From a2f33c97998d9b94defb8ec5a16a7f64d06dcdff Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:32:45 +0200 Subject: [PATCH 40/67] Add `add_staff_subscription` helper Further reduces complexity by checking for subscription in helper function --- helpdesk/views/staff.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index ac2365a5..6ea6cd2f 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -642,6 +642,19 @@ def update_messages_sent_to_by_public_and_status( return ticket +def add_staff_subscription( + request: WSGIRequest, + ticket: Ticket +) -> None: + if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated: + SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe( + request.user, ticket + )[1] + + if SHOW_SUBSCRIBE: + subscribe_staff_member_to_ticket(ticket, request.user) + + def update_ticket(request, ticket_id, public=False): ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public) @@ -833,17 +846,10 @@ def update_ticket(request, ticket_id, public=False): fail_silently=True, files=files, )) - + add_staff_subscription(request, ticket) ticket.save() # auto subscribe user if enabled - if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated: - SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe( - request.user, ticket - )[1] - - if SHOW_SUBSCRIBE: - subscribe_staff_member_to_ticket(ticket, request.user) return return_to_ticket(request.user, helpdesk_settings, ticket) From 256af24daa78da5900885e59a98e40a002d91072 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:34:03 +0200 Subject: [PATCH 41/67] Comment function --- helpdesk/views/staff.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 6ea6cd2f..e1de3326 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -646,6 +646,8 @@ def add_staff_subscription( request: WSGIRequest, ticket: Ticket ) -> None: + """Auto subscribe the staff member if that's what the settigs say and the + user is authenticated and a staff member""" if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated: SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe( request.user, ticket @@ -846,10 +848,10 @@ def update_ticket(request, ticket_id, public=False): fail_silently=True, files=files, )) - add_staff_subscription(request, ticket) ticket.save() # auto subscribe user if enabled + add_staff_subscription(request, ticket) return return_to_ticket(request.user, helpdesk_settings, ticket) From 595dae1cf7d913592fb5ed3969c0d3c368caae2c Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:38:16 +0200 Subject: [PATCH 42/67] Add `get_template_staff_and_template_cc` function Furhter reduxes complexity by combining creation of templates --- helpdesk/views/staff.py | 42 +++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index e1de3326..074141c9 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -657,6 +657,29 @@ def add_staff_subscription( subscribe_staff_member_to_ticket(ticket, request.user) +def get_template_staff_and_template_cc( + reassigned, follow_up: FollowUp +) -> tuple[str, str]: + if reassigned: + template_staff = 'assigned_owner' + elif follow_up.new_status == Ticket.RESOLVED_STATUS: + template_staff = 'resolved_owner' + elif follow_up.new_status == Ticket.CLOSED_STATUS: + template_staff = 'closed_owner' + else: + template_staff = 'updated_owner' + if reassigned: + template_cc = 'assigned_cc' + elif follow_up.new_status == Ticket.RESOLVED_STATUS: + template_cc = 'resolved_cc' + elif follow_up.new_status == Ticket.CLOSED_STATUS: + template_cc = 'closed_cc' + else: + template_cc = 'updated_cc' + + return template_staff, template_cc + + def update_ticket(request, ticket_id, public=False): ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public) @@ -813,15 +836,7 @@ def update_ticket(request, ticket_id, public=False): files ) - if reassigned: - template_staff = 'assigned_owner' - elif f.new_status == Ticket.RESOLVED_STATUS: - template_staff = 'resolved_owner' - elif f.new_status == Ticket.CLOSED_STATUS: - template_staff = 'closed_owner' - else: - template_staff = 'updated_owner' - + template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f) if ticket.assigned_to and ( ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign) @@ -833,15 +848,6 @@ def update_ticket(request, ticket_id, public=False): files=files, )) - if reassigned: - template_cc = 'assigned_cc' - elif f.new_status == Ticket.RESOLVED_STATUS: - template_cc = 'resolved_cc' - elif f.new_status == Ticket.CLOSED_STATUS: - template_cc = 'closed_cc' - else: - template_cc = 'updated_cc' - messages_sent_to.update(ticket.send( {'ticket_cc': (template_cc, context)}, dont_send_to=messages_sent_to, From 749ebbe16be87a128069f673a9cbf62a961d5d3b Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:42:16 +0200 Subject: [PATCH 43/67] Fix annotations for py3.8 --- helpdesk/views/staff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 074141c9..981091bb 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -565,7 +565,7 @@ def get_and_set_ticket_status( new_status: str, ticket: Ticket, follow_up: FollowUp -) -> tuple[str, str]: +) -> typing.Tuple[str, str]: """Performs comparision on previous status to new status, updating the title as required. @@ -659,7 +659,7 @@ def add_staff_subscription( def get_template_staff_and_template_cc( reassigned, follow_up: FollowUp -) -> tuple[str, str]: +) -> typing.Tuple[str, str]: if reassigned: template_staff = 'assigned_owner' elif follow_up.new_status == Ticket.RESOLVED_STATUS: From 7b4d53cfc040bf7bb13de45eb51dcd56db995f7a Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:44:04 +0200 Subject: [PATCH 44/67] Fix 'list' annoation for py3.8 --- helpdesk/views/staff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 981091bb..450ffac8 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -606,8 +606,8 @@ def update_messages_sent_to_by_public_and_status( ticket: Ticket, follow_up: FollowUp, context: str, - messages_sent_to: list[str], - files: list[str, str] + messages_sent_to: typing.List[str], + files: typing.List[str, str] ) -> Ticket: """Sets the status of the ticket""" if public and ( From 45e47846fe35ef63dbcc6774078fef8f2c418315 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:45:36 +0200 Subject: [PATCH 45/67] py3.8 annotation fix --- helpdesk/views/staff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 450ffac8..ac405b6d 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -607,7 +607,7 @@ def update_messages_sent_to_by_public_and_status( follow_up: FollowUp, context: str, messages_sent_to: typing.List[str], - files: typing.List[str, str] + files: typing.List[typing.Tuple[str, str]] ) -> Ticket: """Sets the status of the ticket""" if public and ( From 57cd2f147121112c1cc9939ceed2d4d855bbac96 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:47:57 +0200 Subject: [PATCH 46/67] Remove annoation for py3.8 --- helpdesk/email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index cac0d788..03f26e9a 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -529,7 +529,7 @@ def get_ticket_id_from_subject_slug( def add_file_if_always_save_incoming_email_message( - files_: list[SimpleUploadedFile], + files_, message: str ) -> None: """When `settings.HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE` is `True` From b326103d825cf286b4b77e9ecd6c2f6d9619c0f8 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:48:06 +0200 Subject: [PATCH 47/67] Fix spacing --- helpdesk/views/staff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index ac405b6d..8702e0b2 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -563,8 +563,8 @@ def get_due_date_from_request_or_ticket( def get_and_set_ticket_status( new_status: str, - ticket: Ticket, - follow_up: FollowUp + ticket: Ticket, + follow_up: FollowUp ) -> typing.Tuple[str, str]: """Performs comparision on previous status to new status, updating the title as required. From 14689820433c01015f393b8e576382727e45662c Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:50:49 +0200 Subject: [PATCH 48/67] Remove unused variables, extract correct index --- helpdesk/email.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 03f26e9a..60e476a9 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -177,13 +177,13 @@ 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)') + data = server.fetch(num, '(RFC822)')[1] full_message = encoding.force_str(data[0][1], errors='replace') try: ticket = object_from_message( @@ -346,7 +346,7 @@ def create_ticket_cc(ticket, cc_list): 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: From 46f8e9d21f07c6282f60baac884ef1658dadf26e Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 03:52:58 +0200 Subject: [PATCH 49/67] Clear error Use.objects.get causes undefined variable when using get_user_model --- helpdesk/email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 60e476a9..1a0b7d50 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -355,7 +355,7 @@ 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 From bed7f0e493d389f414a3934bf33f79ba2a048f41 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:00:57 +0200 Subject: [PATCH 50/67] Add default value property to TicketCustomField Property of the model, so add it there to keep consistency --- helpdesk/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helpdesk/models.py b/helpdesk/models.py index 291e7323..73b2ba7c 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -1964,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') From a783156b611111f367cd1a3b677fdf3c708e3f2a Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:01:27 +0200 Subject: [PATCH 51/67] Add `merge_ticket_values` helper Extract a large portion of code from `merge_tickets` --- helpdesk/views/staff.py | 58 +++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 8702e0b2..88f1e130 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -1000,6 +1000,37 @@ ticket_attributes = ( ) +def merge_ticket_values( + request: WSGIRequest, + tickets: typing.List[Ticket], + custom_fields +) -> None: + for ticket in tickets: + ticket.values = {} + # Prepare the value for each attributes of this ticket + for attribute, __ in ticket_attributes: + value = getattr(ticket, attribute, TicketCustomFieldValue.default_value) + # Check if attr is a get_FIELD_display + if attribute.startswith('get_') and attribute.endswith('_display'): + # Hack to call methods like get_FIELD_display() + value = getattr(ticket, attribute, TicketCustomFieldValue.default_value)() + ticket.values[attribute] = { + 'value': value, + 'checked': str(ticket.id) == request.POST.get(attribute) + } + # Prepare the value for each custom fields of this ticket + for custom_field in custom_fields: + try: + value = ticket.ticketcustomfieldvalue_set.get( + field=custom_field).value + except (TicketCustomFieldValue.DoesNotExist, ValueError): + value = TicketCustomFieldValue.default_value + ticket.values[custom_field.name] = { + 'value': value, + 'checked': str(ticket.id) == request.POST.get(custom_field.name) + } + + @staff_member_required def merge_tickets(request): """ @@ -1014,31 +1045,8 @@ def merge_tickets(request): tickets = ticket_select_form.cleaned_data.get('tickets') custom_fields = CustomField.objects.all() - default = _('Not defined') - for ticket in tickets: - ticket.values = {} - # Prepare the value for each attributes of this ticket - for attribute, __ in ticket_attributes: - value = getattr(ticket, attribute, default) - # Check if attr is a get_FIELD_display - if attribute.startswith('get_') and attribute.endswith('_display'): - # Hack to call methods like get_FIELD_display() - value = getattr(ticket, attribute, default)() - ticket.values[attribute] = { - 'value': value, - 'checked': str(ticket.id) == request.POST.get(attribute) - } - # Prepare the value for each custom fields of this ticket - for custom_field in custom_fields: - try: - value = ticket.ticketcustomfieldvalue_set.get( - field=custom_field).value - except (TicketCustomFieldValue.DoesNotExist, ValueError): - value = default - ticket.values[custom_field.name] = { - 'value': value, - 'checked': str(ticket.id) == request.POST.get(custom_field.name) - } + + merge_ticket_values(request, tickets, custom_fields) if request.method == 'POST': # Find which ticket has been chosen to be the main one From eb11c4fe0ee78bf0c37c96d329d3945be355ed20 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:03:12 +0200 Subject: [PATCH 52/67] Rename `ticket_attriubtes` to upper, module level constant. --- helpdesk/views/staff.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 88f1e130..82b4c5d3 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -989,7 +989,7 @@ mass_update = staff_member_required(mass_update) # Prepare ticket attributes which will be displayed in the table to choose # which value to keep when merging -ticket_attributes = ( +TICKET_ATTRIBUTES = ( ('created', _('Created date')), ('due_date', _('Due on')), ('get_status_display', _('Status')), @@ -1008,7 +1008,7 @@ def merge_ticket_values( for ticket in tickets: ticket.values = {} # Prepare the value for each attributes of this ticket - for attribute, __ in ticket_attributes: + for attribute, __ in TICKET_ATTRIBUTES: value = getattr(ticket, attribute, TicketCustomFieldValue.default_value) # Check if attr is a get_FIELD_display if attribute.startswith('get_') and attribute.endswith('_display'): @@ -1061,7 +1061,7 @@ def merge_tickets(request): ) else: # Save ticket fields values - for attribute, __ in ticket_attributes: + for attribute, __ in TICKET_ATTRIBUTES: id_for_attribute = request.POST.get(attribute) if id_for_attribute != chosen_ticket.id: try: @@ -1151,7 +1151,7 @@ def merge_tickets(request): return render(request, 'helpdesk/ticket_merge.html', { 'tickets': tickets, - 'ticket_attributes': ticket_attributes, + 'TICKET_ATTRIBUTES': TICKET_ATTRIBUTES, 'custom_fields': custom_fields, 'ticket_select_form': ticket_select_form }) From a248181857a2ff9f43428cc621a3cdcd004070d8 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:08:16 +0200 Subject: [PATCH 53/67] Add `redirect_from_chosen_ticket` helper function Moves the whole handling to own block, reducing complexity greatly. --- helpdesk/views/staff.py | 190 +++++++++++++++++++++------------------- 1 file changed, 102 insertions(+), 88 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 82b4c5d3..3ced684e 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -1031,6 +1031,102 @@ def merge_ticket_values( } +def redirect_from_chosen_ticket( + request, + chosen_ticket, + tickets, + custom_fields +) -> HttpResponseRedirect: + # Save ticket fields values + for attribute, __ in TICKET_ATTRIBUTES: + id_for_attribute = request.POST.get(attribute) + if id_for_attribute != chosen_ticket.id: + try: + selected_ticket = tickets.get(id=id_for_attribute) + except (Ticket.DoesNotExist, ValueError): + continue + + # Check if attr is a get_FIELD_display + if attribute.startswith('get_') and attribute.endswith('_display'): + # Keep only the FIELD part + attribute = attribute[4:-8] + # Get value from selected ticket and then save it on + # the chosen ticket + value = getattr(selected_ticket, attribute) + setattr(chosen_ticket, attribute, value) + # Save custom fields values + for custom_field in custom_fields: + id_for_custom_field = request.POST.get(custom_field.name) + if id_for_custom_field != chosen_ticket.id: + try: + selected_ticket = tickets.get( + id=id_for_custom_field) + except (Ticket.DoesNotExist, ValueError): + continue + + # Check if the value for this ticket custom field + # exists + try: + value = selected_ticket.ticketcustomfieldvalue_set.get( + field=custom_field).value + except TicketCustomFieldValue.DoesNotExist: + continue + + # Create the custom field value or update it with the + # value from the selected ticket + custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create( + field=custom_field, + defaults={'value': value} + ) + if not created: + custom_field_value.value = value + custom_field_value.save(update_fields=['value']) + # Save changes + chosen_ticket.save() + + # For other tickets, save the link to the ticket in which they have been merged to + # and set status to DUPLICATE + for ticket in tickets.exclude(id=chosen_ticket.id): + ticket.merged_to = chosen_ticket + ticket.status = Ticket.DUPLICATE_STATUS + ticket.save() + + # Send mail to submitter email and ticket CC to let them + # know ticket has been merged + context = safe_template_context(ticket) + if ticket.submitter_email: + send_templated_mail( + template_name='merged', + context=context, + recipients=[ticket.submitter_email], + bcc=[ + cc.email_address for cc in ticket.ticketcc_set.select_related('user')], + sender=ticket.queue.from_address, + fail_silently=True + ) + + # Move all followups and update their title to know they + # come from another ticket + ticket.followup_set.update( + ticket=chosen_ticket, + # Next might exceed maximum 200 characters limit + title=_('[Merged from #%(id)d] %(title)s') % { + 'id': ticket.id, 'title': ticket.title} + ) + + # Add submitter_email, assigned_to email and ticketcc to + # chosen ticket if necessary + chosen_ticket.add_email_to_ticketcc_if_not_in( + email=ticket.submitter_email) + if ticket.assigned_to and ticket.assigned_to.email: + chosen_ticket.add_email_to_ticketcc_if_not_in( + email=ticket.assigned_to.email) + for ticketcc in ticket.ticketcc_set.all(): + chosen_ticket.add_email_to_ticketcc_if_not_in( + ticketcc=ticketcc) + return redirect(chosen_ticket) + + @staff_member_required def merge_tickets(request): """ @@ -1060,94 +1156,12 @@ def merge_tickets(request): 'Please choose a ticket in which the others will be merged into.') ) else: - # Save ticket fields values - for attribute, __ in TICKET_ATTRIBUTES: - id_for_attribute = request.POST.get(attribute) - if id_for_attribute != chosen_ticket.id: - try: - selected_ticket = tickets.get(id=id_for_attribute) - except (Ticket.DoesNotExist, ValueError): - continue - - # Check if attr is a get_FIELD_display - if attribute.startswith('get_') and attribute.endswith('_display'): - # Keep only the FIELD part - attribute = attribute[4:-8] - # Get value from selected ticket and then save it on - # the chosen ticket - value = getattr(selected_ticket, attribute) - setattr(chosen_ticket, attribute, value) - # Save custom fields values - for custom_field in custom_fields: - id_for_custom_field = request.POST.get(custom_field.name) - if id_for_custom_field != chosen_ticket.id: - try: - selected_ticket = tickets.get( - id=id_for_custom_field) - except (Ticket.DoesNotExist, ValueError): - continue - - # Check if the value for this ticket custom field - # exists - try: - value = selected_ticket.ticketcustomfieldvalue_set.get( - field=custom_field).value - except TicketCustomFieldValue.DoesNotExist: - continue - - # Create the custom field value or update it with the - # value from the selected ticket - custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create( - field=custom_field, - defaults={'value': value} - ) - if not created: - custom_field_value.value = value - custom_field_value.save(update_fields=['value']) - # Save changes - chosen_ticket.save() - - # For other tickets, save the link to the ticket in which they have been merged to - # and set status to DUPLICATE - for ticket in tickets.exclude(id=chosen_ticket.id): - ticket.merged_to = chosen_ticket - ticket.status = Ticket.DUPLICATE_STATUS - ticket.save() - - # Send mail to submitter email and ticket CC to let them - # know ticket has been merged - context = safe_template_context(ticket) - if ticket.submitter_email: - send_templated_mail( - template_name='merged', - context=context, - recipients=[ticket.submitter_email], - bcc=[ - cc.email_address for cc in ticket.ticketcc_set.select_related('user')], - sender=ticket.queue.from_address, - fail_silently=True - ) - - # Move all followups and update their title to know they - # come from another ticket - ticket.followup_set.update( - ticket=chosen_ticket, - # Next might exceed maximum 200 characters limit - title=_('[Merged from #%(id)d] %(title)s') % { - 'id': ticket.id, 'title': ticket.title} - ) - - # Add submitter_email, assigned_to email and ticketcc to - # chosen ticket if necessary - chosen_ticket.add_email_to_ticketcc_if_not_in( - email=ticket.submitter_email) - if ticket.assigned_to and ticket.assigned_to.email: - chosen_ticket.add_email_to_ticketcc_if_not_in( - email=ticket.assigned_to.email) - for ticketcc in ticket.ticketcc_set.all(): - chosen_ticket.add_email_to_ticketcc_if_not_in( - ticketcc=ticketcc) - return redirect(chosen_ticket) + return redirect_from_chosen_ticket( + request, + chosen_ticket, + tickets, + custom_fields + ) return render(request, 'helpdesk/ticket_merge.html', { 'tickets': tickets, From 40a243c23b51cb883c7f6dd8818031d88e1f6eb5 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:15:53 +0200 Subject: [PATCH 54/67] Revert changes, updating objects missed somewhere --- helpdesk/views/staff.py | 196 +++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 105 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 3ced684e..88f1e130 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -989,7 +989,7 @@ mass_update = staff_member_required(mass_update) # Prepare ticket attributes which will be displayed in the table to choose # which value to keep when merging -TICKET_ATTRIBUTES = ( +ticket_attributes = ( ('created', _('Created date')), ('due_date', _('Due on')), ('get_status_display', _('Status')), @@ -1008,7 +1008,7 @@ def merge_ticket_values( for ticket in tickets: ticket.values = {} # Prepare the value for each attributes of this ticket - for attribute, __ in TICKET_ATTRIBUTES: + for attribute, __ in ticket_attributes: value = getattr(ticket, attribute, TicketCustomFieldValue.default_value) # Check if attr is a get_FIELD_display if attribute.startswith('get_') and attribute.endswith('_display'): @@ -1031,102 +1031,6 @@ def merge_ticket_values( } -def redirect_from_chosen_ticket( - request, - chosen_ticket, - tickets, - custom_fields -) -> HttpResponseRedirect: - # Save ticket fields values - for attribute, __ in TICKET_ATTRIBUTES: - id_for_attribute = request.POST.get(attribute) - if id_for_attribute != chosen_ticket.id: - try: - selected_ticket = tickets.get(id=id_for_attribute) - except (Ticket.DoesNotExist, ValueError): - continue - - # Check if attr is a get_FIELD_display - if attribute.startswith('get_') and attribute.endswith('_display'): - # Keep only the FIELD part - attribute = attribute[4:-8] - # Get value from selected ticket and then save it on - # the chosen ticket - value = getattr(selected_ticket, attribute) - setattr(chosen_ticket, attribute, value) - # Save custom fields values - for custom_field in custom_fields: - id_for_custom_field = request.POST.get(custom_field.name) - if id_for_custom_field != chosen_ticket.id: - try: - selected_ticket = tickets.get( - id=id_for_custom_field) - except (Ticket.DoesNotExist, ValueError): - continue - - # Check if the value for this ticket custom field - # exists - try: - value = selected_ticket.ticketcustomfieldvalue_set.get( - field=custom_field).value - except TicketCustomFieldValue.DoesNotExist: - continue - - # Create the custom field value or update it with the - # value from the selected ticket - custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create( - field=custom_field, - defaults={'value': value} - ) - if not created: - custom_field_value.value = value - custom_field_value.save(update_fields=['value']) - # Save changes - chosen_ticket.save() - - # For other tickets, save the link to the ticket in which they have been merged to - # and set status to DUPLICATE - for ticket in tickets.exclude(id=chosen_ticket.id): - ticket.merged_to = chosen_ticket - ticket.status = Ticket.DUPLICATE_STATUS - ticket.save() - - # Send mail to submitter email and ticket CC to let them - # know ticket has been merged - context = safe_template_context(ticket) - if ticket.submitter_email: - send_templated_mail( - template_name='merged', - context=context, - recipients=[ticket.submitter_email], - bcc=[ - cc.email_address for cc in ticket.ticketcc_set.select_related('user')], - sender=ticket.queue.from_address, - fail_silently=True - ) - - # Move all followups and update their title to know they - # come from another ticket - ticket.followup_set.update( - ticket=chosen_ticket, - # Next might exceed maximum 200 characters limit - title=_('[Merged from #%(id)d] %(title)s') % { - 'id': ticket.id, 'title': ticket.title} - ) - - # Add submitter_email, assigned_to email and ticketcc to - # chosen ticket if necessary - chosen_ticket.add_email_to_ticketcc_if_not_in( - email=ticket.submitter_email) - if ticket.assigned_to and ticket.assigned_to.email: - chosen_ticket.add_email_to_ticketcc_if_not_in( - email=ticket.assigned_to.email) - for ticketcc in ticket.ticketcc_set.all(): - chosen_ticket.add_email_to_ticketcc_if_not_in( - ticketcc=ticketcc) - return redirect(chosen_ticket) - - @staff_member_required def merge_tickets(request): """ @@ -1156,16 +1060,98 @@ def merge_tickets(request): 'Please choose a ticket in which the others will be merged into.') ) else: - return redirect_from_chosen_ticket( - request, - chosen_ticket, - tickets, - custom_fields - ) + # Save ticket fields values + for attribute, __ in ticket_attributes: + id_for_attribute = request.POST.get(attribute) + if id_for_attribute != chosen_ticket.id: + try: + selected_ticket = tickets.get(id=id_for_attribute) + except (Ticket.DoesNotExist, ValueError): + continue + + # Check if attr is a get_FIELD_display + if attribute.startswith('get_') and attribute.endswith('_display'): + # Keep only the FIELD part + attribute = attribute[4:-8] + # Get value from selected ticket and then save it on + # the chosen ticket + value = getattr(selected_ticket, attribute) + setattr(chosen_ticket, attribute, value) + # Save custom fields values + for custom_field in custom_fields: + id_for_custom_field = request.POST.get(custom_field.name) + if id_for_custom_field != chosen_ticket.id: + try: + selected_ticket = tickets.get( + id=id_for_custom_field) + except (Ticket.DoesNotExist, ValueError): + continue + + # Check if the value for this ticket custom field + # exists + try: + value = selected_ticket.ticketcustomfieldvalue_set.get( + field=custom_field).value + except TicketCustomFieldValue.DoesNotExist: + continue + + # Create the custom field value or update it with the + # value from the selected ticket + custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create( + field=custom_field, + defaults={'value': value} + ) + if not created: + custom_field_value.value = value + custom_field_value.save(update_fields=['value']) + # Save changes + chosen_ticket.save() + + # For other tickets, save the link to the ticket in which they have been merged to + # and set status to DUPLICATE + for ticket in tickets.exclude(id=chosen_ticket.id): + ticket.merged_to = chosen_ticket + ticket.status = Ticket.DUPLICATE_STATUS + ticket.save() + + # Send mail to submitter email and ticket CC to let them + # know ticket has been merged + context = safe_template_context(ticket) + if ticket.submitter_email: + send_templated_mail( + template_name='merged', + context=context, + recipients=[ticket.submitter_email], + bcc=[ + cc.email_address for cc in ticket.ticketcc_set.select_related('user')], + sender=ticket.queue.from_address, + fail_silently=True + ) + + # Move all followups and update their title to know they + # come from another ticket + ticket.followup_set.update( + ticket=chosen_ticket, + # Next might exceed maximum 200 characters limit + title=_('[Merged from #%(id)d] %(title)s') % { + 'id': ticket.id, 'title': ticket.title} + ) + + # Add submitter_email, assigned_to email and ticketcc to + # chosen ticket if necessary + chosen_ticket.add_email_to_ticketcc_if_not_in( + email=ticket.submitter_email) + if ticket.assigned_to and ticket.assigned_to.email: + chosen_ticket.add_email_to_ticketcc_if_not_in( + email=ticket.assigned_to.email) + for ticketcc in ticket.ticketcc_set.all(): + chosen_ticket.add_email_to_ticketcc_if_not_in( + ticketcc=ticketcc) + return redirect(chosen_ticket) return render(request, 'helpdesk/ticket_merge.html', { 'tickets': tickets, - 'TICKET_ATTRIBUTES': TICKET_ATTRIBUTES, + 'ticket_attributes': ticket_attributes, 'custom_fields': custom_fields, 'ticket_select_form': ticket_select_form }) From f89f5b91da72264c0d5f0ed4a97aa6e644ba545d Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:23:03 +0200 Subject: [PATCH 55/67] Reinstate changes, fixed missing update --- helpdesk/views/staff.py | 196 +++++++++++++++++++++------------------- 1 file changed, 105 insertions(+), 91 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 88f1e130..dac7ed42 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -989,7 +989,7 @@ mass_update = staff_member_required(mass_update) # Prepare ticket attributes which will be displayed in the table to choose # which value to keep when merging -ticket_attributes = ( +TICKET_ATTRIBUTES = ( ('created', _('Created date')), ('due_date', _('Due on')), ('get_status_display', _('Status')), @@ -1008,7 +1008,7 @@ def merge_ticket_values( for ticket in tickets: ticket.values = {} # Prepare the value for each attributes of this ticket - for attribute, __ in ticket_attributes: + for attribute, __ in TICKET_ATTRIBUTES: value = getattr(ticket, attribute, TicketCustomFieldValue.default_value) # Check if attr is a get_FIELD_display if attribute.startswith('get_') and attribute.endswith('_display'): @@ -1031,6 +1031,102 @@ def merge_ticket_values( } +def redirect_from_chosen_ticket( + request, + chosen_ticket, + tickets, + custom_fields +) -> HttpResponseRedirect: + # Save ticket fields values + for attribute, __ in TICKET_ATTRIBUTES: + id_for_attribute = request.POST.get(attribute) + if id_for_attribute != chosen_ticket.id: + try: + selected_ticket = tickets.get(id=id_for_attribute) + except (Ticket.DoesNotExist, ValueError): + continue + + # Check if attr is a get_FIELD_display + if attribute.startswith('get_') and attribute.endswith('_display'): + # Keep only the FIELD part + attribute = attribute[4:-8] + # Get value from selected ticket and then save it on + # the chosen ticket + value = getattr(selected_ticket, attribute) + setattr(chosen_ticket, attribute, value) + # Save custom fields values + for custom_field in custom_fields: + id_for_custom_field = request.POST.get(custom_field.name) + if id_for_custom_field != chosen_ticket.id: + try: + selected_ticket = tickets.get( + id=id_for_custom_field) + except (Ticket.DoesNotExist, ValueError): + continue + + # Check if the value for this ticket custom field + # exists + try: + value = selected_ticket.ticketcustomfieldvalue_set.get( + field=custom_field).value + except TicketCustomFieldValue.DoesNotExist: + continue + + # Create the custom field value or update it with the + # value from the selected ticket + custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create( + field=custom_field, + defaults={'value': value} + ) + if not created: + custom_field_value.value = value + custom_field_value.save(update_fields=['value']) + # Save changes + chosen_ticket.save() + + # For other tickets, save the link to the ticket in which they have been merged to + # and set status to DUPLICATE + for ticket in tickets.exclude(id=chosen_ticket.id): + ticket.merged_to = chosen_ticket + ticket.status = Ticket.DUPLICATE_STATUS + ticket.save() + + # Send mail to submitter email and ticket CC to let them + # know ticket has been merged + context = safe_template_context(ticket) + if ticket.submitter_email: + send_templated_mail( + template_name='merged', + context=context, + recipients=[ticket.submitter_email], + bcc=[ + cc.email_address for cc in ticket.ticketcc_set.select_related('user')], + sender=ticket.queue.from_address, + fail_silently=True + ) + + # Move all followups and update their title to know they + # come from another ticket + ticket.followup_set.update( + ticket=chosen_ticket, + # Next might exceed maximum 200 characters limit + title=_('[Merged from #%(id)d] %(title)s') % { + 'id': ticket.id, 'title': ticket.title} + ) + + # Add submitter_email, assigned_to email and ticketcc to + # chosen ticket if necessary + chosen_ticket.add_email_to_ticketcc_if_not_in( + email=ticket.submitter_email) + if ticket.assigned_to and ticket.assigned_to.email: + chosen_ticket.add_email_to_ticketcc_if_not_in( + email=ticket.assigned_to.email) + for ticketcc in ticket.ticketcc_set.all(): + chosen_ticket.add_email_to_ticketcc_if_not_in( + ticketcc=ticketcc) + return redirect(chosen_ticket) + + @staff_member_required def merge_tickets(request): """ @@ -1060,98 +1156,16 @@ def merge_tickets(request): 'Please choose a ticket in which the others will be merged into.') ) else: - # Save ticket fields values - for attribute, __ in ticket_attributes: - id_for_attribute = request.POST.get(attribute) - if id_for_attribute != chosen_ticket.id: - try: - selected_ticket = tickets.get(id=id_for_attribute) - except (Ticket.DoesNotExist, ValueError): - continue - - # Check if attr is a get_FIELD_display - if attribute.startswith('get_') and attribute.endswith('_display'): - # Keep only the FIELD part - attribute = attribute[4:-8] - # Get value from selected ticket and then save it on - # the chosen ticket - value = getattr(selected_ticket, attribute) - setattr(chosen_ticket, attribute, value) - # Save custom fields values - for custom_field in custom_fields: - id_for_custom_field = request.POST.get(custom_field.name) - if id_for_custom_field != chosen_ticket.id: - try: - selected_ticket = tickets.get( - id=id_for_custom_field) - except (Ticket.DoesNotExist, ValueError): - continue - - # Check if the value for this ticket custom field - # exists - try: - value = selected_ticket.ticketcustomfieldvalue_set.get( - field=custom_field).value - except TicketCustomFieldValue.DoesNotExist: - continue - - # Create the custom field value or update it with the - # value from the selected ticket - custom_field_value, created = chosen_ticket.ticketcustomfieldvalue_set.get_or_create( - field=custom_field, - defaults={'value': value} - ) - if not created: - custom_field_value.value = value - custom_field_value.save(update_fields=['value']) - # Save changes - chosen_ticket.save() - - # For other tickets, save the link to the ticket in which they have been merged to - # and set status to DUPLICATE - for ticket in tickets.exclude(id=chosen_ticket.id): - ticket.merged_to = chosen_ticket - ticket.status = Ticket.DUPLICATE_STATUS - ticket.save() - - # Send mail to submitter email and ticket CC to let them - # know ticket has been merged - context = safe_template_context(ticket) - if ticket.submitter_email: - send_templated_mail( - template_name='merged', - context=context, - recipients=[ticket.submitter_email], - bcc=[ - cc.email_address for cc in ticket.ticketcc_set.select_related('user')], - sender=ticket.queue.from_address, - fail_silently=True - ) - - # Move all followups and update their title to know they - # come from another ticket - ticket.followup_set.update( - ticket=chosen_ticket, - # Next might exceed maximum 200 characters limit - title=_('[Merged from #%(id)d] %(title)s') % { - 'id': ticket.id, 'title': ticket.title} - ) - - # Add submitter_email, assigned_to email and ticketcc to - # chosen ticket if necessary - chosen_ticket.add_email_to_ticketcc_if_not_in( - email=ticket.submitter_email) - if ticket.assigned_to and ticket.assigned_to.email: - chosen_ticket.add_email_to_ticketcc_if_not_in( - email=ticket.assigned_to.email) - for ticketcc in ticket.ticketcc_set.all(): - chosen_ticket.add_email_to_ticketcc_if_not_in( - ticketcc=ticketcc) - return redirect(chosen_ticket) + return redirect_from_chosen_ticket( + request, + chosen_ticket, + tickets, + custom_fields + ) return render(request, 'helpdesk/ticket_merge.html', { 'tickets': tickets, - 'ticket_attributes': ticket_attributes, + 'ticket_attributes': TICKET_ATTRIBUTES, 'custom_fields': custom_fields, 'ticket_select_form': ticket_select_form }) From d858c40416ef0e6db3e640bb126ca472c4b011f4 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:29:43 +0200 Subject: [PATCH 56/67] Add `check_redirect_on_user_query` helper function Extract the checking for a redirect to reduce complexity --- helpdesk/views/staff.py | 65 +++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index dac7ed42..586ccadd 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -1171,35 +1171,12 @@ def merge_tickets(request): }) -@helpdesk_staff_member_required -def ticket_list(request): - context = {} - - huser = HelpdeskUser(request.user) - - # Query_params will hold a dictionary of parameters relating to - # a query, to be saved if needed: - query_params = { - 'filtering': {}, - 'filtering_or': {}, - 'sorting': None, - 'sortreverse': False, - 'search_string': '', - } - default_query_params = { - 'filtering': { - 'status__in': [1, 2], - }, - 'sorting': 'created', - 'search_string': '', - 'sortreverse': False, - } - - # If the user is coming from the header/navigation search box, lets' first - # look at their query to see if they have entered a valid ticket number. If - # they have, just redirect to that ticket number. Otherwise, we treat it as - # a keyword search. - +def check_redirect_on_user_query(request, huser): + """If the user is coming from the header/navigation search box, lets' first + look at their query to see if they have entered a valid ticket number. If + they have, just redirect to that ticket number. Otherwise, we treat it as + a keyword search. + """ if request.GET.get('search_type', None) == 'header': query = request.GET.get('q') filter_ = None @@ -1228,7 +1205,37 @@ def ticket_list(request): except Ticket.DoesNotExist: # Go on to standard keyword searching pass + return None + +@helpdesk_staff_member_required +def ticket_list(request): + context = {} + + huser = HelpdeskUser(request.user) + + # Query_params will hold a dictionary of parameters relating to + # a query, to be saved if needed: + query_params = { + 'filtering': {}, + 'filtering_or': {}, + 'sorting': None, + 'sortreverse': False, + 'search_string': '', + } + default_query_params = { + 'filtering': { + 'status__in': [1, 2], + }, + 'sorting': 'created', + 'search_string': '', + 'sortreverse': False, + } + + #: check for a redirect, see function doc for details + redirect = check_redirect_on_user_query(request, huser) + if redirect: + return redirect try: saved_query, query_params = load_saved_query(request, query_params) except QueryLoadError: From 50bd72ac7a0fc75981fa9346902762bf5336de65 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:31:17 +0200 Subject: [PATCH 57/67] Move import to top --- helpdesk/views/staff.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 586ccadd..783135ff 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -6,9 +6,9 @@ django-helpdesk - A Django powered ticket tracker for small enterprise. views/staff.py - The bulk of the application - provides most business logic and renders all staff-facing views. """ - from ..lib import format_time_spent from ..templated_email import send_templated_mail +from collections import defaultdict from copy import deepcopy from datetime import date, datetime, timedelta from django.conf import settings @@ -1558,7 +1558,6 @@ def run_report(request, report): if request.GET.get('saved_query', None): Query(report_queryset, query_to_base64(query_params)) - from collections import defaultdict summarytable = defaultdict(int) # a second table for more complex queries summarytable2 = defaultdict(int) From b1bf2cab46a99251965e5a7d17009a23d70c45b1 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:35:49 +0200 Subject: [PATCH 58/67] Add `get_report_queryset_or_redirect` helper Gets required objects or redirects --- helpdesk/views/staff.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 783135ff..eb850223 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -1539,12 +1539,18 @@ def report_index(request): report_index = staff_member_required(report_index) -@helpdesk_staff_member_required -def run_report(request, report): +def get_report_queryset_or_redirect(request, report): if Ticket.objects.all().count() == 0 or report not in ( - 'queuemonth', 'usermonth', 'queuestatus', 'queuepriority', 'userstatus', - 'userpriority', 'userqueue', 'daysuntilticketclosedbymonth'): - return HttpResponseRedirect(reverse("helpdesk:report_index")) + "queuemonth", + "usermonth", + "queuestatus", + "queuepriority", + "userstatus", + "userpriority", + "userqueue", + "daysuntilticketclosedbymonth" + ): + return None, None, HttpResponseRedirect(reverse("helpdesk:report_index")) report_queryset = Ticket.objects.all().select_related().filter( queue__in=HelpdeskUser(request.user).get_queues() @@ -1553,8 +1559,18 @@ def run_report(request, report): try: saved_query, query_params = load_saved_query(request) except QueryLoadError: - return HttpResponseRedirect(reverse('helpdesk:report_index')) + return None, HttpResponseRedirect(reverse('helpdesk:report_index')) + return report_queryset, query_params, saved_query, None + +@helpdesk_staff_member_required +def run_report(request, report): + + report_queryset, query_params, saved_query, redirect = get_report_queryset_or_redirect( + request, report + ) + if redirect: + return redirect if request.GET.get('saved_query', None): Query(report_queryset, query_to_base64(query_params)) From 205c69b539537b7b581fea3a9fff85323c0ae6d4 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:41:01 +0200 Subject: [PATCH 59/67] Add `get_report_table_and_totals` helper function Extracts a large portion of run_report handling --- helpdesk/views/staff.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index eb850223..30dc88a1 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -1563,6 +1563,21 @@ def get_report_queryset_or_redirect(request, report): return report_queryset, query_params, saved_query, None +def get_report_table_and_totals(header1, summarytable, possible_options): + table = [] + totals = {} + for item in header1: + data = [] + for hdr in possible_options: + if hdr not in totals.keys(): + totals[hdr] = summarytable[item, hdr] + else: + totals[hdr] += summarytable[item, hdr] + data.append(summarytable[item, hdr]) + table.append([item] + data) + return table, totals + + @helpdesk_staff_member_required def run_report(request, report): @@ -1690,8 +1705,6 @@ def run_report(request, report): if report == 'daysuntilticketclosedbymonth': summarytable2[metric1, metric2] += metric3 - table = [] - if report == 'daysuntilticketclosedbymonth': for key in summarytable2.keys(): summarytable[key] = summarytable2[key] / summarytable[key] @@ -1701,18 +1714,9 @@ def run_report(request, report): column_headings = [col1heading] + possible_options # Prepare a dict to store totals for each possible option - totals = {} + table, totals = get_report_table_and_totals(header1, summarytable, possible_options) # Pivot the data so that 'header1' fields are always first column # in the row, and 'possible_options' are always the 2nd - nth columns. - for item in header1: - data = [] - for hdr in possible_options: - if hdr not in totals.keys(): - totals[hdr] = summarytable[item, hdr] - else: - totals[hdr] += summarytable[item, hdr] - data.append(summarytable[item, hdr]) - table.append([item] + data) # Zip data and headers together in one list for Morris.js charts # will get a list like [(Header1, Data1), (Header2, Data2)...] From 72392a3f500f5f1cc575d3f8f11549a092c3e328 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:44:46 +0200 Subject: [PATCH 60/67] Add `update_summary_tables` helper function Reduces complexity of 'run_report' and handles updating summary table in own function --- helpdesk/views/staff.py | 86 +++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 30dc88a1..f31b89f6 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -1578,6 +1578,49 @@ def get_report_table_and_totals(header1, summarytable, possible_options): return table, totals +def update_summary_tables(report_queryset, report, summarytable, summarytable2): + metric3 = False + for ticket in report_queryset: + if report == 'userpriority': + metric1 = u'%s' % ticket.get_assigned_to + metric2 = u'%s' % ticket.get_priority_display() + + elif report == 'userqueue': + metric1 = u'%s' % ticket.get_assigned_to + metric2 = u'%s' % ticket.queue.title + + elif report == 'userstatus': + metric1 = u'%s' % ticket.get_assigned_to + metric2 = u'%s' % ticket.get_status_display() + + elif report == 'usermonth': + metric1 = u'%s' % ticket.get_assigned_to + metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) + + elif report == 'queuepriority': + metric1 = u'%s' % ticket.queue.title + metric2 = u'%s' % ticket.get_priority_display() + + elif report == 'queuestatus': + metric1 = u'%s' % ticket.queue.title + metric2 = u'%s' % ticket.get_status_display() + + elif report == 'queuemonth': + metric1 = u'%s' % ticket.queue.title + metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) + + elif report == 'daysuntilticketclosedbymonth': + metric1 = u'%s' % ticket.queue.title + metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) + metric3 = ticket.modified - ticket.created + metric3 = metric3.days + + summarytable[metric1, metric2] += 1 + if metric3: + if report == 'daysuntilticketclosedbymonth': + summarytable2[metric1, metric2] += metric3 + + @helpdesk_staff_member_required def run_report(request, report): @@ -1663,48 +1706,7 @@ def run_report(request, report): col1heading = _('Queue') possible_options = periods charttype = 'date' - - metric3 = False - for ticket in report_queryset: - if report == 'userpriority': - metric1 = u'%s' % ticket.get_assigned_to - metric2 = u'%s' % ticket.get_priority_display() - - elif report == 'userqueue': - metric1 = u'%s' % ticket.get_assigned_to - metric2 = u'%s' % ticket.queue.title - - elif report == 'userstatus': - metric1 = u'%s' % ticket.get_assigned_to - metric2 = u'%s' % ticket.get_status_display() - - elif report == 'usermonth': - metric1 = u'%s' % ticket.get_assigned_to - metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) - - elif report == 'queuepriority': - metric1 = u'%s' % ticket.queue.title - metric2 = u'%s' % ticket.get_priority_display() - - elif report == 'queuestatus': - metric1 = u'%s' % ticket.queue.title - metric2 = u'%s' % ticket.get_status_display() - - elif report == 'queuemonth': - metric1 = u'%s' % ticket.queue.title - metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) - - elif report == 'daysuntilticketclosedbymonth': - metric1 = u'%s' % ticket.queue.title - metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month) - metric3 = ticket.modified - ticket.created - metric3 = metric3.days - - summarytable[metric1, metric2] += 1 - if metric3: - if report == 'daysuntilticketclosedbymonth': - summarytable2[metric1, metric2] += metric3 - + update_summary_tables(report_queryset, report, summarytable, summarytable2) if report == 'daysuntilticketclosedbymonth': for key in summarytable2.keys(): summarytable[key] = summarytable2[key] / summarytable[key] From 4a429f5498bdc6669a79ab98719e42e532843552 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Mon, 25 Jul 2022 04:47:59 +0200 Subject: [PATCH 61/67] Add flake8 exit failure to workflow --- .github/workflows/pythonpackage.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 83ba7e3b..70b2482d 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -28,9 +28,8 @@ jobs: run: | pip install flake8 # stop the build if there are Python syntax errors or undefined names - flake8 helpdesk --count --show-source --statistics --exit-zero # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --statistics + 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 From 47f0bf8df68df7052691d96b3270cc92461f5b0a Mon Sep 17 00:00:00 2001 From: Pouria Mousavizadeh Tehrani Date: Mon, 1 Aug 2022 16:13:59 +0430 Subject: [PATCH 62/67] Dissable to disable --- docs/teams.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/teams.rst b/docs/teams.rst index 5fe88437..64be9747 100644 --- a/docs/teams.rst +++ b/docs/teams.rst @@ -13,4 +13,4 @@ You can assign a knowledge-base item to a team on the Helpdesk admin page. Once you have set up teams. Unassigned tickets which are associated with a knowledge-base item will only be shown on the dashboard to those users who are members of the team which is associated with that knowledge-base item. -Note: It is possible that pinax-teams will interfere with other packages that you already use in your project. If you do not wish to use team functionality, you can dissable teams by setting the following settings: ``HELPDESK_TEAMS_MODEL`` to any random model, ``HELPDESK_TEAMS_MIGRATION_DEPENDENCIES`` to ``[]``, and ``HELPDESK_KBITEM_TEAM_GETTER`` to ``lambda _: None``. You can also use a different library in place of pinax teams by setting those settings appropriately. ``HELPDESK_KBITEM_TEAM_GETTER`` should take a ``kbitem`` and return a team object with a ``name`` property and a method ``is_member(self, user)`` which returns true if user is a member of the team. +Note: It is possible that pinax-teams will interfere with other packages that you already use in your project. If you do not wish to use team functionality, you can disable teams by setting the following settings: ``HELPDESK_TEAMS_MODEL`` to any random model, ``HELPDESK_TEAMS_MIGRATION_DEPENDENCIES`` to ``[]``, and ``HELPDESK_KBITEM_TEAM_GETTER`` to ``lambda _: None``. You can also use a different library in place of pinax teams by setting those settings appropriately. ``HELPDESK_KBITEM_TEAM_GETTER`` should take a ``kbitem`` and return a team object with a ``name`` property and a method ``is_member(self, user)`` which returns true if user is a member of the team. From 6b0400dbcd51499a7d0b3a7294a1d3c4d1d35fa2 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 5 Aug 2022 10:04:50 +0200 Subject: [PATCH 63/67] Add cache to workflow --- .github/workflows/pythonpackage.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 70b2482d..db0adfd6 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -12,27 +12,38 @@ jobs: 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 }} + + - uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-testing.txt')}}-${{ hashFiles('tox.ini') }} + - 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 + + - 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 From cb35578b70a078897e32cb11c81d51aa9c1f74c8 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 5 Aug 2022 10:09:00 +0200 Subject: [PATCH 64/67] Add python version to cache key Prevents conflicts --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index db0adfd6..f46e197f 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-testing.txt')}}-${{ hashFiles('tox.ini') }} + key: ${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-testing.txt')}}-${{ hashFiles('tox.ini') }}-${{ matrix.python-version }} - name: Install dependencies run: | From a51933f9bff6533de4cb5ba9896a05f6283c7f4d Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 5 Aug 2022 10:28:31 +0200 Subject: [PATCH 65/67] Bump django 2.2 -> 3.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7cc1c79c..f49a88f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django>=2.2 +Django>=3.2 django-bootstrap4-form celery email-reply-parser From cd480b2750e7bf94f5e26d66e35bc87be508a832 Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 5 Aug 2022 14:27:29 +0200 Subject: [PATCH 66/67] Add Djagno versions to test matrix 32 and 4 used for constraints --- .github/workflows/pythonpackage.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index f46e197f..751a03a4 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -10,6 +10,7 @@ jobs: max-parallel: 4 matrix: python-version: ["3.8", "3.9", "3.10"] + django-version: ["32","4"] steps: - uses: actions/checkout@v3 @@ -21,12 +22,12 @@ jobs: - uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-testing.txt')}}-${{ hashFiles('tox.ini') }}-${{ matrix.python-version }} + key: ${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-testing.txt')}}-${{ hashFiles('tox.ini') }}-${{ matrix.python-version }}-${{ matrix.django-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django32.txt + pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django${{ matrix.django-version }}.txt - name: Format style check with 'autopep8' run: | From 8ab7599bea0fc7af825a1e0f2cb80b9513e299ef Mon Sep 17 00:00:00 2001 From: Martin Whitehouse Date: Fri, 5 Aug 2022 14:43:53 +0200 Subject: [PATCH 67/67] Remove parallel constraint --- .github/workflows/pythonpackage.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 751a03a4..dfd82e51 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -7,7 +7,6 @@ jobs: runs-on: ubuntu-latest strategy: - max-parallel: 4 matrix: python-version: ["3.8", "3.9", "3.10"] django-version: ["32","4"]