Merge branch 'unstable' into patch-1

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

View File

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

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

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

19
.isort.cfg Normal file
View File

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

View File

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

View File

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

View File

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

1
constraints-Django4.txt Normal file
View File

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

View File

@ -8,8 +8,10 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
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__)))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,14 +11,16 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import os
import sys
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# -- General configuration -----------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
@ -87,7 +89,7 @@ pygments_style = 'sphinx'
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# -- Options for HTML output ---------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
@ -167,7 +169,7 @@ html_static_path = ['_static']
htmlhelp_basename = 'django-helpdeskdoc'
# -- Options for LaTeX output --------------------------------------------------
# -- Options for LaTeX output --------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
@ -206,7 +208,7 @@ latex_documents = [
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# -- Options for manual page output --------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).

View File

@ -10,7 +10,7 @@ Prerequisites
Before getting started, ensure your system meets the following recommended dependencies:
* Python 3.8+
* Django 2.2 LTS or 3.2 LTS (strongly recommend migrating to 3.2 LTS as soon as possible)
* Django 3.2 LTS
Ensure any extra Django modules you wish to use are compatible before continuing.
@ -63,6 +63,7 @@ errors with trying to create User settings.
'pinax.teams', # Team support
'reversion', # Required by pinax-teams
'rest_framework', # required for the API
'django_cleanup.apps.CleanupConfig', # Remove this if you do NOT want to delete files on the file system when the associated record is deleted in the database
'helpdesk', # This is us!
)
@ -74,11 +75,12 @@ errors with trying to create User settings.
SITE_ID = 1
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following line to ``urls.py``::
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following lines to ``urls.py``::
from django.conf.urls import include
path('helpdesk/', include('helpdesk.urls')),
Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the line will be as follows::
Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the path line will be as follows::
path('', include('helpdesk.urls', namespace='helpdesk')),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,10 +4,10 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff.
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__)
@ -19,4 +19,5 @@ def helpdesk_staff(user):
try:
return is_helpdesk_staff(user)
except Exception:
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed")
logger.exception(
"'helpdesk_staff' template tag (django-helpdesk) crashed")

View File

@ -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()
@ -22,13 +21,16 @@ def datetime_string_format(value):
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
"""
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
except (TypeError, ValueError):
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
except (TypeError, ValueError):
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
new_value = date_filter(datetime.strptime(
value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
except (TypeError, ValueError):
# If NoneType return empty string, else return original value
new_value = "" if value is None else value

View File

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

View File

@ -10,12 +10,11 @@ templatetags/ticket_to_link.py - Used in ticket comments to allow wiki-style
to show the status of that ticket (eg a closed
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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):
@ -70,7 +70,8 @@ class PublicActionsTestCase(TestCase):
self.assertTemplateNotUsed(response, 'helpdesk/public_view_form.html')
self.assertEqual(ticket.status, Ticket.CLOSED_STATUS)
self.assertEqual(ticket.resolution, resolution_text)
self.assertEqual(current_followups + 1, ticket.followup_set.all().count())
self.assertEqual(current_followups + 1,
ticket.followup_set.all().count())
ticket.resolution = old_resolution
ticket.status = old_status

View File

@ -1,11 +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):
@ -58,7 +56,8 @@ class QueryTests(TestCase):
def test_query_basic(self):
self.loginUser()
query = query_to_base64({})
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query]))
response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
self.assertEqual(
response.json(),
{
@ -76,12 +75,14 @@ class QueryTests(TestCase):
query = query_to_base64(
{'filtering': {'kbitem__in': [self.kbitem1.pk]}}
)
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query]))
response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
self.assertEqual(
response.json(),
{
"data":
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
"created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"recordsFiltered": 1,
"recordsTotal": 1,
"draw": 0,
@ -93,12 +94,14 @@ class QueryTests(TestCase):
query = query_to_base64(
{'filtering_or': {'kbitem__in': [self.kbitem1.pk]}}
)
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query]))
response = self.client.get(
reverse('helpdesk:datatables_ticket_list', args=[query]))
self.assertEqual(
response.json(),
{
"data":
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open",
"created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
"recordsFiltered": 1,
"recordsTotal": 1,
"draw": 0,

View File

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

View File

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

View File

@ -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()
@ -57,7 +57,8 @@ class TestTicketLookupPublicEnabled(TestCase):
def test_add_email_to_ticketcc_if_not_in(self):
staff_email = 'staff@mail.com'
staff_user = User.objects.create(username='staff', email=staff_email, is_staff=True)
staff_user = User.objects.create(
username='staff', email=staff_email, is_staff=True)
self.ticket.assigned_to = staff_user
self.ticket.save()
email_1 = 'user1@mail.com'
@ -66,20 +67,25 @@ class TestTicketLookupPublicEnabled(TestCase):
# Add new email to CC
email_2 = 'user2@mail.com'
ticketcc_2 = self.ticket.add_email_to_ticketcc_if_not_in(email=email_2)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
self.assertEqual(list(self.ticket.ticketcc_set.all()),
[ticketcc_1, ticketcc_2])
# Add existing email, doesn't change anything
self.ticket.add_email_to_ticketcc_if_not_in(email=email_1)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
self.assertEqual(list(self.ticket.ticketcc_set.all()),
[ticketcc_1, ticketcc_2])
# Add mail from assigned user, doesn't change anything
self.ticket.add_email_to_ticketcc_if_not_in(email=staff_email)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
self.assertEqual(list(self.ticket.ticketcc_set.all()),
[ticketcc_1, ticketcc_2])
self.ticket.add_email_to_ticketcc_if_not_in(user=staff_user)
self.assertEqual(list(self.ticket.ticketcc_set.all()), [ticketcc_1, ticketcc_2])
self.assertEqual(list(self.ticket.ticketcc_set.all()),
[ticketcc_1, ticketcc_2])
# Move a ticketCC from ticket 1 to ticket 2
ticket_2 = Ticket.objects.create(queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2)
ticket_2 = Ticket.objects.create(
queue=self.ticket.queue, title='Ticket 2', submitter_email=email_2)
self.assertEqual(ticket_2.ticketcc_set.count(), 0)
ticket_2.add_email_to_ticketcc_if_not_in(ticketcc=ticketcc_1)
self.assertEqual(ticketcc_1.ticket, ticket_2)

View File

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

View File

@ -1,23 +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):

View File

@ -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
@ -26,5 +27,6 @@ class TicketActionsTestCase(TestCase):
def test_get_user_settings(self):
response = self.client.get(reverse('helpdesk:user_settings'), follow=True)
response = self.client.get(
reverse('helpdesk:user_settings'), follow=True)
self.assertContains(response, "Use the following options")

View File

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

View File

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

View File

@ -1,15 +1,11 @@
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):
return HelpdeskUser(req.user)
@ -33,7 +29,8 @@ class HelpdeskUser:
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION \
and not user.is_superuser
if limit_queues_by_user:
id_list = [q.pk for q in all_queues if user.has_perm(q.permission_name)]
id_list = [q.pk for q in all_queues if user.has_perm(
q.permission_name)]
id_list += public_ids
return all_queues.filter(pk__in=id_list)
else:

View File

@ -2,13 +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
# 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.
@ -19,9 +24,12 @@ def validate_file_extension(value):
if hasattr(settings, 'VALID_EXTENSIONS'):
valid_extensions = settings.VALID_EXTENSIONS
else:
valid_extensions = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']
valid_extensions = ['.txt', '.asc', '.htm', '.html',
'.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']
if not ext.lower() in valid_extensions:
# TODO: one more check in case it is a file with no extension; we should always allow that?
# TODO: one more check in case it is a file with no extension; we
# should always allow that?
if not (ext.lower() == '' or ext.lower() == '.'):
raise ValidationError('Unsupported file extension: %s.' % ext.lower())
raise ValidationError(
'Unsupported file extension: %s.' % ext.lower())

View File

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

View File

@ -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
from helpdesk.serializers import TicketSerializer, UserSerializer
class TicketViewSet(viewsets.ModelViewSet):
@ -28,6 +27,18 @@ class TicketViewSet(viewsets.ModelViewSet):
return ticket
class FollowUpViewSet(viewsets.ModelViewSet):
queryset = FollowUp.objects.all()
serializer_class = FollowUpSerializer
permission_classes = [IsAdminUser]
class FollowUpAttachmentViewSet(viewsets.ModelViewSet):
queryset = FollowUpAttachment.objects.all()
serializer_class = FollowUpAttachmentSerializer
permission_classes = [IsAdminUser]
class CreateUserView(CreateModelMixin, GenericViewSet):
queryset = get_user_model().objects.all()
serializer_class = UserSerializer

View File

@ -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 ugettext 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()
@ -123,7 +123,8 @@ class RecentFollowUps(Feed):
description_template = 'helpdesk/rss/recent_activity_description.html'
title = _('Helpdesk: Recent Followups')
description = _('Recent FollowUps, such as e-mail replies, comments, attachments and resolutions')
description = _(
'Recent FollowUps, such as e-mail replies, comments, attachments and resolutions')
link = '/tickets/' # reverse('helpdesk:list')
def items(self):

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

39
quicktest.py Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env python
"""
Usage:
$ python -m venv .venv
@ -5,15 +6,15 @@ $ 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):
class QuickDjangoTest:
"""
A quick way to run the Django test suite without a fully-configured project.
@ -35,14 +36,14 @@ class QuickDjangoTest(object):
'django.contrib.sites',
'django.contrib.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',
@ -77,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):
@ -98,20 +100,20 @@ class QuickDjangoTest(object):
MIDDLEWARE=self.MIDDLEWARE,
ROOT_URLCONF='helpdesk.tests.urls',
STATIC_URL='/static/',
LOGIN_URL='/helpdesk/login/',
LOGIN_URL='/login/',
TEMPLATES=self.TEMPLATES,
SITE_ID=1,
SECRET_KEY='wowdonotusethisfakesecuritykeyyouneedarealsecure1',
## The following settings disable teams
HELPDESK_TEAMS_MODEL = 'auth.User',
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [],
HELPDESK_KBITEM_TEAM_GETTER = lambda _: None,
## test the API
# The following settings disable teams
HELPDESK_TEAMS_MODEL='auth.User',
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES=[],
HELPDESK_KBITEM_TEAM_GETTER=lambda _: None,
# test the API
HELPDESK_ACTIVATE_API_ENDPOINT=True
)
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)
@ -133,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)

View File

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

View File

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

View File

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

9
tox.ini Normal file
View File

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