Merge branch 'unstable' into correct_requirements

This commit is contained in:
IvanovIvan1900 2022-08-08 15:48:34 +07:00 committed by GitHub
commit 1d450c01db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 955 additions and 687 deletions

View File

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

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

@ -0,0 +1,53 @@
name: Python package
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
django-version: ["32","4"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-testing.txt')}}-${{ hashFiles('tox.ini') }}-${{ matrix.python-version }}-${{ matrix.django-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-testing.txt -c constraints-Django${{ matrix.django-version }}.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

@ -8,8 +8,10 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/ https://docs.djangoproject.com/en/1.11/ref/settings/
""" """
import os import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

View File

@ -13,10 +13,10 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include 1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
from django.urls import include, path
from django.contrib import admin
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
# The following uses the static() helper function, # The following uses the static() helper function,

View File

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

View File

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Python packaging.""" """Python packaging."""
from __future__ import unicode_literals
from setuptools import setup
import os import os
from setuptools import setup
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
project_root = os.path.dirname(here) project_root = os.path.dirname(here)
@ -13,7 +11,7 @@ project_root = os.path.dirname(here)
NAME = 'django-helpdesk-demodesk' NAME = 'django-helpdesk-demodesk'
DESCRIPTION = 'A demo Django project using django-helpdesk' DESCRIPTION = 'A demo Django project using django-helpdesk'
README = open(os.path.join(here, 'README.rst')).read() README = open(os.path.join(here, 'README.rst')).read()
VERSION = '0.4.1' VERSION = '0.5.0a1'
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip() #VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
AUTHOR = 'django-helpdesk team' AUTHOR = 'django-helpdesk team'
URL = 'https://github.com/django-helpdesk/django-helpdesk' URL = 'https://github.com/django-helpdesk/django-helpdesk'

View File

@ -11,8 +11,9 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys
import os import os
import sys
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the

View File

@ -63,6 +63,7 @@ errors with trying to create User settings.
'pinax.teams', # Team support 'pinax.teams', # Team support
'reversion', # Required by pinax-teams 'reversion', # Required by pinax-teams
'rest_framework', # required for the API 'rest_framework', # required for the API
'django_cleanup.apps.CleanupConfig', # Remove this if you do NOT want to delete files on the file system when the associated record is deleted in the database
'helpdesk', # This is us! 'helpdesk', # This is us!
) )

View File

@ -13,4 +13,4 @@ You can assign a knowledge-base item to a team on the Helpdesk admin page.
Once you have set up teams. Unassigned tickets which are associated with a knowledge-base item will only be shown on the dashboard to those users who are members of the team which is associated with that knowledge-base item. Once you have set up teams. Unassigned tickets which are associated with a knowledge-base item will only be shown on the dashboard to those users who are members of the team which is associated with that knowledge-base item.
Note: It is possible that pinax-teams will interfere with other packages that you already use in your project. If you do not wish to use team functionality, you can dissable teams by setting the following settings: ``HELPDESK_TEAMS_MODEL`` to any random model, ``HELPDESK_TEAMS_MIGRATION_DEPENDENCIES`` to ``[]``, and ``HELPDESK_KBITEM_TEAM_GETTER`` to ``lambda _: None``. You can also use a different library in place of pinax teams by setting those settings appropriately. ``HELPDESK_KBITEM_TEAM_GETTER`` should take a ``kbitem`` and return a team object with a ``name`` property and a method ``is_member(self, user)`` which returns true if user is a member of the team. Note: It is possible that pinax-teams will interfere with other packages that you already use in your project. If you do not wish to use team functionality, you can disable teams by setting the following settings: ``HELPDESK_TEAMS_MODEL`` to any random model, ``HELPDESK_TEAMS_MIGRATION_DEPENDENCIES`` to ``[]``, and ``HELPDESK_KBITEM_TEAM_GETTER`` to ``lambda _: None``. You can also use a different library in place of pinax teams by setting those settings appropriately. ``HELPDESK_KBITEM_TEAM_GETTER`` should take a ``kbitem`` and return a team object with a ``name`` property and a method ``is_member(self, user)`` which returns true if user is a member of the team.

View File

@ -1,13 +1,24 @@
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory
from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem
from helpdesk.models import TicketChange, KBIAttachment, FollowUpAttachment, IgnoreEmail
from helpdesk.models import CustomField
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import (
CustomField,
EmailTemplate,
EscalationExclusion,
FollowUp,
FollowUpAttachment,
IgnoreEmail,
KBIAttachment,
PreSetReply,
Queue,
Ticket,
TicketChange
)
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import KBCategory from helpdesk.models import KBCategory, KBItem
from helpdesk.models import KBItem
@admin.register(Queue) @admin.register(Queue)
@ -82,6 +93,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
list_display_links = ('title',) list_display_links = ('title',)
if helpdesk_settings.HELPDESK_KB_ENABLED:
@admin.register(KBCategory) @admin.register(KBCategory)
class KBCategoryAdmin(admin.ModelAdmin): class KBCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'title', 'slug', 'public') list_display = ('name', 'title', 'slug', 'public')

View File

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

View File

@ -4,23 +4,11 @@ Django Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved. (c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved.
See LICENSE for details. See LICENSE for details.
""" """
# import base64 # import base64
import email
import imaplib
import logging
import mimetypes
import os
import poplib
import re
import socket
import ssl
import sys
from datetime import timedelta
from email.utils import getaddresses
from os.path import isfile, join
from time import ctime
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from datetime import timedelta
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -28,11 +16,24 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q from django.db.models import Q
from django.utils import encoding, timezone from django.utils import encoding, timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
import email
from email.utils import getaddresses
from email_reply_parser import EmailReplyParser from email_reply_parser import EmailReplyParser
from helpdesk import settings from helpdesk import settings
from helpdesk.lib import safe_template_context, process_attachments from helpdesk.lib import process_attachments, safe_template_context
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket
import imaplib
import logging
import mimetypes
import os
from os.path import isfile, join
import poplib
import re
import socket
import ssl
import sys
from time import ctime
import typing
# import User model, which may be a custom model # import User model, which may be a custom model
@ -176,13 +177,13 @@ def imap_sync(q, logger, server):
sys.exit() sys.exit()
try: try:
status, data = server.search(None, 'NOT', 'DELETED') data = server.search(None, 'NOT', 'DELETED')[1]
if data: if data:
msgnums = data[0].split() msgnums = data[0].split()
logger.info("Received %d messages from IMAP server" % len(msgnums)) logger.info("Received %d messages from IMAP server" % len(msgnums))
for num in msgnums: for num in msgnums:
logger.info("Processing message %s" % num) logger.info("Processing message %s" % num)
status, data = server.fetch(num, '(RFC822)') data = server.fetch(num, '(RFC822)')[1]
full_message = encoding.force_str(data[0][1], errors='replace') full_message = encoding.force_str(data[0][1], errors='replace')
try: try:
ticket = object_from_message( ticket = object_from_message(
@ -342,10 +343,10 @@ def create_ticket_cc(ticket, cc_list):
return [] return []
# Local import to deal with non-defined / circular reference problem # Local import to deal with non-defined / circular reference problem
from helpdesk.views.staff import User, subscribe_to_ticket_updates from helpdesk.views.staff import subscribe_to_ticket_updates, User
new_ticket_ccs = [] new_ticket_ccs = []
for cced_name, cced_email in cc_list: for __, cced_email in cc_list:
cced_email = cced_email.strip() cced_email = cced_email.strip()
if cced_email == ticket.queue.email_address: if cced_email == ticket.queue.email_address:
@ -354,7 +355,7 @@ def create_ticket_cc(ticket, cc_list):
user = None user = None
try: try:
user = User.objects.get(email=cced_email) user = User.objects.get(email=cced_email) # @UndefinedVariable
except User.DoesNotExist: except User.DoesNotExist:
pass pass
@ -466,16 +467,6 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
new_ticket_ccs = [] new_ticket_ccs = []
new_ticket_ccs.append(create_ticket_cc(ticket, to_list + cc_list)) new_ticket_ccs.append(create_ticket_cc(ticket, to_list + cc_list))
notifications_to_be_sent = [sender_email]
if queue.enable_notifications_on_email_events and len(notifications_to_be_sent):
ticket_cc_list = TicketCC.objects.filter(
ticket=ticket).all().values_list('email', flat=True)
for email_address in ticket_cc_list:
notifications_to_be_sent.append(email_address)
autoreply = is_autoreply(message) autoreply = is_autoreply(message)
if autoreply: if autoreply:
logger.info( logger.info(
@ -516,7 +507,77 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
return ticket return ticket
def object_from_message(message, queue, logger): def get_ticket_id_from_subject_slug(
queue_slug: str,
subject: str,
logger: logging.Logger
) -> typing.Optional[int]:
"""Get a ticket id from the subject string
Performs a match on the subject using the queue_slug as reference,
returning the ticket id if a match is found.
"""
matchobj = re.match(r".*\[" + queue_slug + r"-(?P<id>\d+)\]", subject)
ticket_id = None
if matchobj:
# This is a reply or forward.
ticket_id = matchobj.group('id')
logger.info("Matched tracking ID %s-%s" % (queue_slug, ticket_id))
else:
logger.info("No tracking ID matched.")
return ticket_id
def add_file_if_always_save_incoming_email_message(
files_,
message: str
) -> None:
"""When `settings.HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE` is `True`
add a file to the files_ list"""
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False):
# save message as attachment in case of some complex markup renders
# wrong
files_.append(
SimpleUploadedFile(
_("original_message.eml").replace(
".eml",
timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml"
),
str(message).encode("utf-8"),
'text/plain'
)
)
def get_encoded_body(body: str) -> str:
try:
return body.encode('ascii').decode('unicode_escape')
except UnicodeEncodeError:
return body
def get_body_from_fragments(body) -> str:
"""Gets a body from the fragments, joined by a double line break"""
return "\n\n".join(f.content for f in EmailReplyParser.read(body).fragments)
def get_email_body_from_part_payload(part) -> str:
"""Gets an decoded body from the payload part, if the decode fails,
returns without encoding"""
try:
return encoding.smart_str(
part.get_payload(decode=True)
)
except UnicodeDecodeError:
return encoding.smart_str(
part.get_payload(decode=False)
)
def object_from_message(message: str,
queue: Queue,
logger: logging.Logger
) -> Ticket:
# 'message' must be an RFC822 formatted message. # 'message' must be an RFC822 formatted message.
message = email.message_from_string(message) message = email.message_from_string(message)
@ -539,35 +600,17 @@ def object_from_message(message, queue, logger):
sender_email = email.utils.getaddresses( sender_email = email.utils.getaddresses(
['\"' + sender.replace('<', '\" <')])[0][1] ['\"' + sender.replace('<', '\" <')])[0][1]
cc = message.get_all('cc', None)
if cc:
# first, fixup the encoding if necessary
cc = [decode_mail_headers(decodeUnknown(
message.get_charset(), x)) for x in cc]
# get_all checks if multiple CC headers, but individual emails may be
# comma separated too
tempcc = []
for hdr in cc:
tempcc.extend(hdr.split(','))
# use a set to ensure no duplicates
cc = set([x.strip() for x in tempcc])
for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)): for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)):
if ignore.test(sender_email): if ignore.test(sender_email):
if ignore.keep_in_mailbox:
# By returning 'False' the message will be kept in the mailbox, # By returning 'False' the message will be kept in the mailbox,
# and the 'True' will cause the message to be deleted. # and the 'True' will cause the message to be deleted.
return False return not ignore.keep_in_mailbox
return True
matchobj = re.match(r".*\[" + queue.slug + r"-(?P<id>\d+)\]", subject) ticket_id: typing.Optional[int] = get_ticket_id_from_subject_slug(
if matchobj: queue.slug,
# This is a reply or forward. subject,
ticket = matchobj.group('id') logger
logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket)) )
else:
logger.info("No tracking ID matched.")
ticket = None
body = None body = None
full_body = None full_body = None
@ -591,13 +634,10 @@ def object_from_message(message, queue, logger):
body = decodeUnknown(part.get_content_charset(), body) body = decodeUnknown(part.get_content_charset(), body)
# have to use django_settings here so overwritting it works in tests # have to use django_settings here so overwritting it works in tests
# the default value is False anyway # the default value is False anyway
if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False): if ticket_id is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False):
# first message in thread, we save full body to avoid # first message in thread, we save full body to avoid
# losing forwards and things like that # losing forwards and things like that
body_parts = [] full_body = get_body_from_fragments(body)
for f in EmailReplyParser.read(body).fragments:
body_parts.append(f.content)
full_body = '\n\n'.join(body_parts)
body = EmailReplyParser.parse_reply(body) body = EmailReplyParser.parse_reply(body)
else: else:
# second and other reply, save only first part of the # second and other reply, save only first part of the
@ -605,18 +645,10 @@ def object_from_message(message, queue, logger):
body = EmailReplyParser.parse_reply(body) body = EmailReplyParser.parse_reply(body)
full_body = body full_body = body
# workaround to get unicode text out rather than escaped text # workaround to get unicode text out rather than escaped text
try: body = get_encoded_body(body)
body = body.encode('ascii').decode('unicode_escape')
except UnicodeEncodeError:
body.encode('utf-8')
logger.debug("Discovered plain text MIME part") logger.debug("Discovered plain text MIME part")
else: else:
try: email_body = get_email_body_from_part_payload(part)
email_body = encoding.smart_str(
part.get_payload(decode=True))
except UnicodeDecodeError:
email_body = encoding.smart_str(
part.get_payload(decode=False))
if not body and not full_body: if not body and not full_body:
# no text has been parsed so far - try such deep parsing # no text has been parsed so far - try such deep parsing
@ -683,19 +715,7 @@ def object_from_message(message, queue, logger):
if not body: if not body:
body = "" body = ""
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False): add_file_if_always_save_incoming_email_message(files, message)
# save message as attachment in case of some complex markup renders
# wrong
files.append(
SimpleUploadedFile(
_("original_message.eml").replace(
".eml",
timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml"
),
str(message).encode("utf-8"),
'text/plain'
)
)
smtp_priority = message.get('priority', '') smtp_priority = message.get('priority', '')
smtp_importance = message.get('importance', '') smtp_importance = message.get('importance', '')
@ -713,4 +733,4 @@ def object_from_message(message, queue, logger):
'files': files, 'files': files,
} }
return create_object_from_email_message(message, ticket, payload, files, logger=logger) return create_object_from_email_message(message, ticket_id, payload, files, logger=logger)

View File

@ -6,25 +6,38 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
forms.py - Definitions of newforms-based forms for creating and maintaining forms.py - Definitions of newforms-based forms for creating and maintaining
tickets. tickets.
""" """
import logging
from datetime import datetime, date, time
from django.core.exceptions import ObjectDoesNotExist, ValidationError from datetime import datetime
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from helpdesk.lib import safe_template_context, process_attachments, convert_value
from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC,
CustomField, TicketCustomFieldValue, TicketDependency, UserSettings)
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.settings import CUSTOMFIELD_TO_FIELD_DICT, CUSTOMFIELD_DATETIME_FORMAT, \ from helpdesk.lib import convert_value, process_attachments, safe_template_context
CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT from helpdesk.models import (
CustomField,
FollowUp,
IgnoreEmail,
Queue,
Ticket,
TicketCC,
TicketCustomFieldValue,
TicketDependency,
UserSettings
)
from helpdesk.settings import (
CUSTOMFIELD_DATE_FORMAT,
CUSTOMFIELD_DATETIME_FORMAT,
CUSTOMFIELD_TIME_FORMAT,
CUSTOMFIELD_TO_FIELD_DICT
)
import logging
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import (KBItem) from helpdesk.models import KBItem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
@ -226,7 +239,10 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
widget=forms.FileInput(attrs={'class': 'form-control-file'}), widget=forms.FileInput(attrs={'class': 'form-control-file'}),
required=False, required=False,
label=_('Attach File'), label=_('Attach File'),
help_text=_('You can attach a file to this ticket. Only file types such as plain text (.txt), a document (.pdf, .docx, or .odt), or screenshot (.png or .jpg) may be uploaded.'), help_text=_('You can attach a file to this ticket. '
'Only file types such as plain text (.txt), '
'a document (.pdf, .docx, or .odt), '
'or screenshot (.png or .jpg) may be uploaded.'),
) )
class Media: class Media:

View File

@ -6,14 +6,14 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
lib.py - Common functions (eg multipart e-mail) lib.py - Common functions (eg multipart e-mail)
""" """
import logging
import mimetypes
from datetime import datetime, date, time
from datetime import date, datetime, time
from django.conf import settings from django.conf import settings
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
import logging
import mimetypes
from helpdesk.settings import CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT
logger = logging.getLogger('helpdesk') logger = logging.getLogger('helpdesk')

View File

@ -8,12 +8,11 @@ scripts/create_escalation_exclusion.py - Easy way to routinely add particular
days to the list of days on which no days to the list of days on which no
escalation should take place. escalation should take place.
""" """
from datetime import date, timedelta
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from helpdesk.models import EscalationExclusion, Queue
from datetime import timedelta, date
import getopt import getopt
from helpdesk.models import EscalationExclusion, Queue
from optparse import make_option from optparse import make_option
import sys import sys

View File

@ -13,15 +13,13 @@ scripts/create_queue_permissions.py -
existing permissions. existing permissions.
""" """
from optparse import make_option
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from helpdesk.models import Queue from helpdesk.models import Queue
from optparse import make_option
class Command(BaseCommand): class Command(BaseCommand):

View File

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

View File

@ -8,18 +8,17 @@ scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
designed to be run from Cron or similar. designed to be run from Cron or similar.
""" """
from datetime import timedelta, date
import getopt
from optparse import make_option
import sys
from datetime import date, timedelta
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext as _
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange import getopt
from helpdesk.lib import safe_template_context from helpdesk.lib import safe_template_context
from helpdesk.models import EscalationExclusion, FollowUp, Queue, Ticket, TicketChange
from optparse import make_option
import sys
class Command(BaseCommand): class Command(BaseCommand):

View File

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

View File

@ -7,35 +7,29 @@ models.py - Model (and hence database) definitions. This is the core of the
helpdesk structure. helpdesk structure.
""" """
from django.contrib.auth.models import Permission
from .lib import convert_value
from .templated_email import send_templated_mail
from .validators import validate_file_extension
import datetime
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models from django.db import models
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _, gettext
from io import StringIO
import re
import os
import mimetypes
import datetime
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext, gettext_lazy as _
from helpdesk import settings as helpdesk_settings
from io import StringIO
from markdown import markdown from markdown import markdown
from markdown.extensions import Extension from markdown.extensions import Extension
import mimetypes
import os
import uuid import re
from rest_framework import serializers from rest_framework import serializers
import uuid
from helpdesk import settings as helpdesk_settings
from .lib import convert_value
from .validators import validate_file_extension
from .templated_email import send_templated_mail
def format_time_spent(time_spent): def format_time_spent(time_spent):
@ -59,7 +53,7 @@ def get_markdown(text):
if not text: if not text:
return "" return ""
pattern = fr'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)' pattern = r'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)'
# Regex check # Regex check
if re.match(pattern, text): if re.match(pattern, text):
# get get value of group regex # get get value of group regex
@ -1969,6 +1963,10 @@ class TicketCustomFieldValue(models.Model):
def __str__(self): def __str__(self):
return '%s / %s' % (self.ticket, self.field) return '%s / %s' % (self.ticket, self.field)
@property
def default_value(self) -> str:
return _("Not defined")
class Meta: class Meta:
unique_together = (('ticket', 'field'),) unique_together = (('ticket', 'field'),)
verbose_name = _('Ticket custom field value') verbose_name = _('Ticket custom field value')

View File

@ -1,15 +1,12 @@
from base64 import b64decode, b64encode
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from base64 import b64encode
from base64 import b64decode
import json
from model_utils import Choices
from helpdesk.serializers import DatatablesTicketSerializer from helpdesk.serializers import DatatablesTicketSerializer
import json
from model_utils import Choices
def query_to_base64(query): def query_to_base64(query):

View File

@ -1,13 +1,12 @@
from rest_framework import serializers from .forms import TicketForm
from .lib import format_time_spent, process_attachments
from .models import CustomField, FollowUp, FollowUpAttachment, Ticket
from .user import HelpdeskUser
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.humanize.templatetags import humanize from django.contrib.humanize.templatetags import humanize
from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from .forms import TicketForm
from .models import Ticket, CustomField, FollowUp, FollowUpAttachment
from .lib import format_time_spent, process_attachments
from .user import HelpdeskUser
class DatatablesTicketSerializer(serializers.ModelSerializer): class DatatablesTicketSerializer(serializers.ModelSerializer):
""" """

View File

@ -2,12 +2,13 @@
Default settings for django-helpdesk. Default settings for django-helpdesk.
""" """
import os
import warnings
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
import os
import warnings
DEFAULT_USER_SETTINGS = { DEFAULT_USER_SETTINGS = {
'login_view_ticketlist': True, 'login_view_ticketlist': True,

View File

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

View File

@ -1,9 +1,10 @@
import os
import logging
from smtplib import SMTPException
from django.conf import settings from django.conf import settings
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
import logging
import os
from smtplib import SMTPException
logger = logging.getLogger('helpdesk') logger = logging.getLogger('helpdesk')
@ -50,8 +51,7 @@ def send_templated_mail(template_name,
from_string = engines['django'].from_string from_string = engines['django'].from_string
from helpdesk.models import EmailTemplate from helpdesk.models import EmailTemplate
from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \ from helpdesk.settings import HELPDESK_EMAIL_FALLBACK_LOCALE, HELPDESK_EMAIL_SUBJECT_TEMPLATE
HELPDESK_EMAIL_FALLBACK_LOCALE
headers = extra_headers or {} headers = extra_headers or {}

View File

@ -4,10 +4,10 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff. The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff.
templatetags/helpdesk_staff.py templatetags/helpdesk_staff.py
""" """
import logging
from django.template import Library
from django.template import Library
from helpdesk.decorators import is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff
import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,10 +1,9 @@
from datetime import datetime
from django.conf import settings
from django.template import Library from django.template import Library
from django.template.defaultfilters import date as date_filter from django.template.defaultfilters import date as date_filter
from django.conf import settings from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
from datetime import datetime
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT, CUSTOMFIELD_DATETIME_FORMAT
register = Library() register = Library()

View File

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

View File

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

View File

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

View File

@ -1,18 +1,22 @@
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime from datetime import datetime
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from freezegun import freeze_time from freezegun import freeze_time
from helpdesk.models import CustomField, Queue, Ticket
from django.contrib.auth.models import User
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework.exceptions import ErrorDetail from rest_framework.exceptions import ErrorDetail
from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN from rest_framework.status import (
HTTP_200_OK,
HTTP_201_CREATED,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_403_FORBIDDEN
)
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from helpdesk.models import Queue, Ticket, CustomField
class TicketTest(APITestCase): class TicketTest(APITestCase):
due_date = datetime(2022, 4, 10, 15, 6) due_date = datetime(2022, 4, 10, 15, 6)

View File

@ -1,15 +1,13 @@
# vim: set fileencoding=utf-8 : # vim: set fileencoding=utf-8 :
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from django.test import override_settings, TestCase from django.test import override_settings, TestCase
from django.urls import reverse
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from helpdesk import lib, models from helpdesk import lib, models
import os import os
import shutil import shutil
from tempfile import gettempdir from tempfile import gettempdir
from unittest import mock from unittest import mock
from unittest.case import skip from unittest.case import skip

View File

@ -1,24 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.test import TestCase, override_settings
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User
from django.core.management import call_command from django.core.management import call_command
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User from django.test import override_settings, TestCase
from django.contrib.auth.hashers import make_password
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, FollowUpAttachment
from helpdesk.management.commands.get_email import Command
import helpdesk.email import helpdesk.email
from helpdesk.management.commands.get_email import Command
import six from helpdesk.models import FollowUp, FollowUpAttachment, Queue, Ticket, TicketCC
import itertools import itertools
from shutil import rmtree
import sys
import os
from tempfile import mkdtemp
import logging import logging
import os
from shutil import rmtree
import six
import sys
from tempfile import mkdtemp
from unittest import mock from unittest import mock
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) THIS_DIR = os.path.dirname(os.path.abspath(__file__))
# class A addresses can't have first octet of 0 # class A addresses can't have first octet of 0

View File

@ -1,11 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from helpdesk.models import KBCategory, KBItem, Queue, Ticket from helpdesk.models import KBCategory, KBItem, Queue, Ticket
from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User
from helpdesk.tests.helpers import (
get_staff_user, reload_urlconf, User, create_ticket, print_response)
class KBTests(TestCase): class KBTests(TestCase):

View File

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

View File

@ -1,14 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
from importlib import reload
from django.urls import reverse
from django.test import TestCase
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import Queue from helpdesk.models import Queue
from helpdesk.tests.helpers import ( from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User
get_staff_user, reload_urlconf, User, create_ticket, print_response) from importlib import reload
from django.test.utils import override_settings import sys
class KBDisabledTestCase(TestCase): class KBDisabledTestCase(TestCase):

View File

@ -1,11 +1,10 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse
from helpdesk.models import Queue, Ticket
from helpdesk import settings from helpdesk import settings
from helpdesk.models import Queue, Ticket
from helpdesk.query import __Query__ from helpdesk.query import __Query__
from helpdesk.user import HelpdeskUser from helpdesk.user import HelpdeskUser

View File

@ -1,7 +1,7 @@
from helpdesk.models import Queue, Ticket
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse from django.urls import reverse
from helpdesk.models import Queue, Ticket
class PublicActionsTestCase(TestCase): class PublicActionsTestCase(TestCase):

View File

@ -1,12 +1,9 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from helpdesk.models import KBCategory, KBItem, Queue, Ticket from helpdesk.models import KBCategory, KBItem, Queue, Ticket
from helpdesk.query import query_to_base64 from helpdesk.query import query_to_base64
from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User
from helpdesk.tests.helpers import (
get_staff_user, reload_urlconf, User, create_ticket, print_response)
class QueryTests(TestCase): class QueryTests(TestCase):

View File

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

View File

@ -1,22 +1,21 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import mail from django.core import mail
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from helpdesk.models import CustomField, Queue, Ticket
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import CustomField, Queue, Ticket
from helpdesk.templatetags.ticket_to_link import num_to_link
from helpdesk.user import HelpdeskUser
try: # python 3 try: # python 3
from urllib.parse import urlparse from urllib.parse import urlparse
except ImportError: # python 2 except ImportError: # python 2
from urlparse import urlparse from urlparse import urlparse
from helpdesk.templatetags.ticket_to_link import num_to_link
from helpdesk.user import HelpdeskUser
class TicketActionsTestCase(TestCase): class TicketActionsTestCase(TestCase):
fixtures = ['emailtemplate.json'] fixtures = ['emailtemplate.json']

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from helpdesk.models import Ticket, Queue
from django.test.utils import override_settings from django.test.utils import override_settings
from django.urls import reverse
from helpdesk.models import Queue, Ticket
User = get_user_model() User = get_user_model()

View File

@ -1,22 +1,19 @@
import email
import uuid
from helpdesk.models import Queue, CustomField, FollowUp, Ticket, TicketCC, KBCategory, KBItem
from django.test import TestCase
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail from django.core import mail
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.forms import ValidationError from django.forms import ValidationError
from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse from django.urls import reverse
import email
from helpdesk.email import object_from_message, create_ticket_cc from helpdesk.email import create_ticket_cc, object_from_message
from helpdesk.models import CustomField, FollowUp, KBCategory, KBItem, Queue, Ticket, TicketCC
from helpdesk.tests.helpers import print_response from helpdesk.tests.helpers import print_response
from urllib.parse import urlparse
import logging import logging
from urllib.parse import urlparse
import uuid
logger = logging.getLogger('helpdesk') logger = logging.getLogger('helpdesk')

View File

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

View File

@ -1,10 +1,11 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail from django.core import mail
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.urls import reverse
from helpdesk.models import CustomField, Queue, Ticket from helpdesk.models import CustomField, Queue, Ticket
try: # python 3 try: # python 3
from urllib.parse import urlparse from urllib.parse import urlparse
except ImportError: # python 2 except ImportError: # python 2

View File

@ -1,5 +1,6 @@
from django.urls import include, path
from django.contrib import admin from django.contrib import admin
from django.urls import include, path
urlpatterns = [ urlpatterns = [
path('', include('helpdesk.urls', namespace='helpdesk')), path('', include('helpdesk.urls', namespace='helpdesk')),

View File

@ -7,17 +7,16 @@ urls.py - Mapping of URL's to our various views. Note we always used NAMED
views for simplicity in linking later on. views for simplicity in linking later on.
""" """
from django.urls import path, re_path
from django.contrib.auth.decorators import login_required
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import include from django.contrib.auth.decorators import login_required
from django.urls import include, path, re_path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from helpdesk import settings as helpdesk_settings
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
from helpdesk.views import feeds, login, public, staff
from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
from helpdesk.views import feeds, staff, public, login
from helpdesk import settings as helpdesk_settings
from helpdesk.views.api import TicketViewSet, CreateUserView, FollowUpViewSet, FollowUpAttachmentViewSet
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.views import kb from helpdesk.views import kb

View File

@ -1,15 +1,10 @@
from helpdesk.models import (
Ticket,
Queue
)
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.models import Queue, Ticket
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import ( from helpdesk.models import KBCategory, KBItem
KBCategory,
KBItem
)
def huser_from_request(req): def huser_from_request(req):

View File

@ -2,16 +2,18 @@
# #
# validators for file uploads, etc. # validators for file uploads, etc.
from django.conf import settings from django.conf import settings
# TODO: can we use the builtin Django validator instead? # TODO: can we use the builtin Django validator instead?
# see: # see:
# https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator # https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator
def validate_file_extension(value): def validate_file_extension(value):
import os
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import os
ext = os.path.splitext(value.name)[1] # [0] returns path+filename ext = os.path.splitext(value.name)[1] # [0] returns path+filename
# TODO: we might improve this with more thorough checks of file types # TODO: we might improve this with more thorough checks of file types
# rather than just the extensions. # rather than just the extensions.

View File

@ -1,11 +1,10 @@
from django.contrib.auth import get_user_model
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket
from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from django.contrib.auth import get_user_model
from helpdesk.models import Ticket, FollowUp, FollowUpAttachment
from helpdesk.serializers import TicketSerializer, UserSerializer, FollowUpSerializer, FollowUpAttachmentSerializer
class TicketViewSet(viewsets.ModelViewSet): class TicketViewSet(viewsets.ModelViewSet):

View File

@ -9,12 +9,12 @@ views/feeds.py - A handful of staff-only RSS feeds to provide ticket details
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.urls import reverse
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext as _
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext as _
from helpdesk.models import FollowUp, Queue, Ticket
from helpdesk.models import Ticket, FollowUp, Queue
User = get_user_model() User = get_user_model()

View File

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

View File

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

View File

@ -6,30 +6,29 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
views/public.py - All public facing views, eg non-staff (no authentication views/public.py - All public facing views, eg non-staff (no authentication
required) views. required) views.
""" """
import logging
from importlib import import_module
from django.core.exceptions import (
ObjectDoesNotExist, PermissionDenied, ImproperlyConfigured, from django.conf import settings
) from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
from django.urls import reverse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from urllib.parse import quote from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.conf import settings
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.decorators import protect_view, is_helpdesk_staff from helpdesk.decorators import is_helpdesk_staff, protect_view
import helpdesk.views.staff as staff
import helpdesk.views.abstract_views as abstract_views
from helpdesk.lib import text_is_spam from helpdesk.lib import text_is_spam
from helpdesk.models import Ticket, Queue, UserSettings from helpdesk.models import Queue, Ticket, UserSettings
from helpdesk.user import huser_from_request from helpdesk.user import huser_from_request
import helpdesk.views.abstract_views as abstract_views
import helpdesk.views.staff as staff
from importlib import import_module
import logging
from urllib.parse import quote
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -212,6 +211,7 @@ def view_ticket(request):
if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS: if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
from helpdesk.views.staff import update_ticket from helpdesk.views.staff import update_ticket
# Trick the update_ticket() view into thinking it's being called with # Trick the update_ticket() view into thinking it's being called with
# a valid POST. # a valid POST.
request.POST = { request.POST = {

View File

@ -6,67 +6,80 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
views/staff.py - The bulk of the application - provides most business logic and views/staff.py - The bulk of the application - provides most business logic and
renders all staff-facing views. renders all staff-facing views.
""" """
from ..lib import format_time_spent
from ..templated_email import send_templated_mail
from collections import defaultdict
from copy import deepcopy from copy import deepcopy
import json from datetime import date, datetime, timedelta
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, reverse_lazy from django.core.exceptions import PermissionDenied, ValidationError
from django.core.exceptions import ValidationError, PermissionDenied from django.core.handlers.wsgi import WSGIRequest
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect, Http404, HttpResponse, JsonResponse from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext as _ from django.urls import reverse, reverse_lazy
from django.utils.html import escape
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape
from django.utils.translation import gettext as _
from django.views.decorators.csrf import requires_csrf_token from django.views.decorators.csrf import requires_csrf_token
from django.views.generic.edit import FormView, UpdateView from django.views.generic.edit import FormView, UpdateView
from helpdesk import settings as helpdesk_settings
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT
from helpdesk.query import (
get_query_class,
query_to_base64,
query_from_base64,
)
from helpdesk.user import HelpdeskUser
from helpdesk.decorators import ( from helpdesk.decorators import (
helpdesk_staff_member_required, helpdesk_superuser_required, helpdesk_staff_member_required,
is_helpdesk_staff helpdesk_superuser_required,
is_helpdesk_staff,
superuser_required
) )
from helpdesk.forms import ( from helpdesk.forms import (
TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm, CUSTOMFIELD_DATE_FORMAT,
TicketCCEmailForm, TicketCCUserForm, EditFollowUpForm, TicketDependencyForm, MultipleTicketSelectForm EditFollowUpForm,
) EditTicketForm,
from helpdesk.decorators import superuser_required EmailIgnoreForm,
from helpdesk.lib import ( MultipleTicketSelectForm,
safe_template_context, TicketCCEmailForm,
process_attachments, TicketCCForm,
queue_template_context, TicketCCUserForm,
TicketDependencyForm,
TicketForm,
UserSettingsForm
) )
from helpdesk.lib import process_attachments, queue_template_context, safe_template_context
from helpdesk.models import ( from helpdesk.models import (
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch, CustomField,
IgnoreEmail, TicketCC, TicketDependency, UserSettings, CustomField, TicketCustomFieldValue, FollowUp,
FollowUpAttachment,
IgnoreEmail,
PreSetReply,
Queue,
SavedSearch,
Ticket,
TicketCC,
TicketChange,
TicketCustomFieldValue,
TicketDependency,
UserSettings
) )
from helpdesk import settings as helpdesk_settings from helpdesk.query import get_query_class, query_from_base64, query_to_base64
if helpdesk_settings.HELPDESK_KB_ENABLED: from helpdesk.user import HelpdeskUser
from helpdesk.models import (KBItem)
import helpdesk.views.abstract_views as abstract_views import helpdesk.views.abstract_views as abstract_views
from helpdesk.views.permissions import MustBeStaffMixin from helpdesk.views.permissions import MustBeStaffMixin
from ..lib import format_time_spent import json
import re
from rest_framework import status from rest_framework import status
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
import typing
from datetime import date, datetime, timedelta
import re
from ..templated_email import send_templated_mail if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import KBItem
DATE_RE: re.Pattern = re.compile(
r'(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<year>\d{4})$'
)
User = get_user_model() User = get_user_model()
Query = get_query_class() Query = get_query_class()
@ -260,7 +273,7 @@ def followup_edit(request, ticket_id, followup_id):
'time_spent': format_time_spent(followup.time_spent), 'time_spent': format_time_spent(followup.time_spent),
}) })
ticketcc_string, show_subscribe = \ ticketcc_string, __ = \
return_ticketccstring_and_show_subscribe(request.user, ticket) return_ticketccstring_and_show_subscribe(request.user, ticket)
return render(request, 'helpdesk/followup_edit.html', { return render(request, 'helpdesk/followup_edit.html', {
@ -338,8 +351,10 @@ def view_ticket(request, ticket_id):
if 'subscribe' in request.GET: if 'subscribe' in request.GET:
# Allow the user to subscribe him/herself to the ticket whilst viewing # Allow the user to subscribe him/herself to the ticket whilst viewing
# it. # it.
ticket_cc, show_subscribe = \ show_subscribe = return_ticketccstring_and_show_subscribe(
return_ticketccstring_and_show_subscribe(request.user, ticket) request.user, ticket
)[1]
if show_subscribe: if show_subscribe:
subscribe_staff_member_to_ticket(ticket, request.user) subscribe_staff_member_to_ticket(ticket, request.user)
return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id])) return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id]))
@ -473,10 +488,20 @@ def subscribe_staff_member_to_ticket(ticket, user, email='', can_view=True, can_
return subscribe_to_ticket_updates(ticket=ticket, user=user, email=email, can_view=can_view, can_update=can_update) return subscribe_to_ticket_updates(ticket=ticket, user=user, email=email, can_view=can_view, can_update=can_update)
def update_ticket(request, ticket_id, public=False): def get_ticket_from_request_with_authorisation(
request: WSGIRequest,
ticket_id: str,
public: bool
) -> typing.Union[
Ticket, typing.NoReturn
]:
"""Gets a ticket from the public status and if the user is authenticated and
has permissions to update tickets
ticket = None Raises:
Http404 when the ticket can not be found or the user lacks permission
"""
if not (public or ( if not (public or (
request.user.is_authenticated and request.user.is_authenticated and
request.user.is_active and ( request.user.is_active and (
@ -498,41 +523,29 @@ def update_ticket(request, ticket_id, public=False):
'%s?next=%s' % (reverse('helpdesk:login'), request.path) '%s?next=%s' % (reverse('helpdesk:login'), request.path)
) )
if not ticket: return get_object_or_404(Ticket, id=ticket_id)
ticket = get_object_or_404(Ticket, id=ticket_id)
date_re = re.compile(
r'(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<year>\d{4})$'
)
comment = request.POST.get('comment', '') def get_due_date_from_request_or_ticket(
new_status = int(request.POST.get('new_status', ticket.status)) request: WSGIRequest,
title = request.POST.get('title', '') ticket: Ticket
public = request.POST.get('public', False) ) -> typing.Optional[datetime.date]:
owner = int(request.POST.get('owner', -1)) """Tries to locate the due date for a ticket from the `request.POST`
priority = int(request.POST.get('priority', ticket.priority)) 'due_date' parameter or the `due_date_*` paramaters.
due_date_year = int(request.POST.get('due_date_year', 0)) """
due_date_month = int(request.POST.get('due_date_month', 0))
due_date_day = int(request.POST.get('due_date_day', 0))
if request.POST.get("time_spent"):
(hours, minutes) = [int(f)
for f in request.POST.get("time_spent").split(":")]
time_spent = timedelta(hours=hours, minutes=minutes)
else:
time_spent = None
# NOTE: jQuery's default for dates is mm/dd/yy
# very US-centric but for now that's the only format supported
# until we clean up code to internationalize a little more
due_date = request.POST.get('due_date', None) or None due_date = request.POST.get('due_date', None) or None
if due_date is not None: if due_date is not None:
# based on Django code to parse dates: # based on Django code to parse dates:
# https://docs.djangoproject.com/en/2.0/_modules/django/utils/dateparse/ # https://docs.djangoproject.com/en/2.0/_modules/django/utils/dateparse/
match = date_re.match(due_date) match = DATE_RE.match(due_date)
if match: if match:
kw = {k: int(v) for k, v in match.groupdict().items()} kw = {k: int(v) for k, v in match.groupdict().items()}
due_date = date(**kw) due_date = date(**kw)
else: else:
due_date_year = int(request.POST.get('due_date_year', 0))
due_date_month = int(request.POST.get('due_date_month', 0))
due_date_day = int(request.POST.get('due_date_day', 0))
# old way, probably deprecated? # old way, probably deprecated?
if not (due_date_year and due_date_month and due_date_day): if not (due_date_year and due_date_month and due_date_day):
due_date = ticket.due_date due_date = ticket.due_date
@ -545,7 +558,144 @@ def update_ticket(request, ticket_id, public=False):
due_date = timezone.now() due_date = timezone.now()
due_date = due_date.replace( due_date = due_date.replace(
due_date_year, due_date_month, due_date_day) due_date_year, due_date_month, due_date_day)
return due_date
def get_and_set_ticket_status(
new_status: str,
ticket: Ticket,
follow_up: FollowUp
) -> typing.Tuple[str, str]:
"""Performs comparision on previous status to new status,
updating the title as required.
Returns:
The old status as a display string, old status code string
"""
old_status_str = ticket.get_status_display()
old_status = ticket.status
if new_status != ticket.status:
ticket.status = new_status
ticket.save()
follow_up.new_status = new_status
if follow_up.title:
follow_up.title += ' and %s' % ticket.get_status_display()
else:
follow_up.title = '%s' % ticket.get_status_display()
if not follow_up.title:
if follow_up.comment:
follow_up.title = _('Comment')
else:
follow_up.title = _('Updated')
follow_up.save()
return (old_status_str, old_status)
def get_time_spent_from_request(request: WSGIRequest) -> typing.Optional[timedelta]:
if request.POST.get("time_spent"):
(hours, minutes) = [int(f)
for f in request.POST.get("time_spent").split(":")]
return timedelta(hours=hours, minutes=minutes)
return None
def update_messages_sent_to_by_public_and_status(
public: bool,
ticket: Ticket,
follow_up: FollowUp,
context: str,
messages_sent_to: typing.List[str],
files: typing.List[typing.Tuple[str, str]]
) -> Ticket:
"""Sets the status of the ticket"""
if public and (
follow_up.comment or (
follow_up.new_status in (
Ticket.RESOLVED_STATUS,
Ticket.CLOSED_STATUS
)
)
):
if follow_up.new_status == Ticket.RESOLVED_STATUS:
template = 'resolved_'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template = 'closed_'
else:
template = 'updated_'
roles = {
'submitter': (template + 'submitter', context),
'ticket_cc': (template + 'cc', context),
}
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change:
roles['assigned_to'] = (template + 'cc', context)
messages_sent_to.update(
ticket.send(
roles,
dont_send_to=messages_sent_to,
fail_silently=True,
files=files
)
)
return ticket
def add_staff_subscription(
request: WSGIRequest,
ticket: Ticket
) -> None:
"""Auto subscribe the staff member if that's what the settigs say and the
user is authenticated and a staff member"""
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated:
SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(
request.user, ticket
)[1]
if SHOW_SUBSCRIBE:
subscribe_staff_member_to_ticket(ticket, request.user)
def get_template_staff_and_template_cc(
reassigned, follow_up: FollowUp
) -> typing.Tuple[str, str]:
if reassigned:
template_staff = 'assigned_owner'
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
template_staff = 'resolved_owner'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template_staff = 'closed_owner'
else:
template_staff = 'updated_owner'
if reassigned:
template_cc = 'assigned_cc'
elif follow_up.new_status == Ticket.RESOLVED_STATUS:
template_cc = 'resolved_cc'
elif follow_up.new_status == Ticket.CLOSED_STATUS:
template_cc = 'closed_cc'
else:
template_cc = 'updated_cc'
return template_staff, template_cc
def update_ticket(request, ticket_id, public=False):
ticket = get_ticket_from_request_with_authorisation(request, ticket_id, public)
comment = request.POST.get('comment', '')
new_status = int(request.POST.get('new_status', ticket.status))
title = request.POST.get('title', '')
public = request.POST.get('public', False)
owner = int(request.POST.get('owner', -1))
priority = int(request.POST.get('priority', ticket.priority))
time_spent = get_time_spent_from_request(request)
# NOTE: jQuery's default for dates is mm/dd/yy
# very US-centric but for now that's the only format supported
# until we clean up code to internationalize a little more
due_date = get_due_date_from_request_or_ticket(request, ticket)
no_changes = all([ no_changes = all([
not request.FILES, not request.FILES,
not comment, not comment,
@ -605,28 +755,9 @@ def update_ticket(request, ticket_id, public=False):
f.title = _('Unassigned') f.title = _('Unassigned')
ticket.assigned_to = None ticket.assigned_to = None
old_status_str = ticket.get_status_display() old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)
old_status = ticket.status
if new_status != ticket.status:
ticket.status = new_status
ticket.save()
f.new_status = new_status
if f.title:
f.title += ' and %s' % ticket.get_status_display()
else:
f.title = '%s' % ticket.get_status_display()
if not f.title: files = process_attachments(f, request.FILES.getlist('attachment')) if request.FILES else []
if f.comment:
f.title = _('Comment')
else:
f.title = _('Updated')
f.save()
files = []
if request.FILES:
files = process_attachments(f, request.FILES.getlist('attachment'))
if title and title != ticket.title: if title and title != ticket.title:
c = TicketChange( c = TicketChange(
@ -676,8 +807,11 @@ def update_ticket(request, ticket_id, public=False):
c.save() c.save()
ticket.due_date = due_date ticket.due_date = due_date
if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS): if new_status in (
if new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None: Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
) and (
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
):
ticket.resolution = comment ticket.resolution = comment
# ticket might have changed above, so we re-instantiate context with the # ticket might have changed above, so we re-instantiate context with the
@ -693,34 +827,16 @@ def update_ticket(request, ticket_id, public=False):
messages_sent_to.add(request.user.email) messages_sent_to.add(request.user.email)
except AttributeError: except AttributeError:
pass pass
if public and (f.comment or ( ticket = update_messages_sent_to_by_public_and_status(
f.new_status in (Ticket.RESOLVED_STATUS, public,
Ticket.CLOSED_STATUS))): ticket,
if f.new_status == Ticket.RESOLVED_STATUS: f,
template = 'resolved_' context,
elif f.new_status == Ticket.CLOSED_STATUS: messages_sent_to,
template = 'closed_' files
else: )
template = 'updated_'
roles = {
'submitter': (template + 'submitter', context),
'ticket_cc': (template + 'cc', context),
}
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change:
roles['assigned_to'] = (template + 'cc', context)
messages_sent_to.update(ticket.send(
roles, dont_send_to=messages_sent_to, fail_silently=True, files=files,))
if reassigned:
template_staff = 'assigned_owner'
elif f.new_status == Ticket.RESOLVED_STATUS:
template_staff = 'resolved_owner'
elif f.new_status == Ticket.CLOSED_STATUS:
template_staff = 'closed_owner'
else:
template_staff = 'updated_owner'
template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f)
if ticket.assigned_to and ( if ticket.assigned_to and (
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign) or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign)
@ -732,30 +848,16 @@ def update_ticket(request, ticket_id, public=False):
files=files, files=files,
)) ))
if reassigned:
template_cc = 'assigned_cc'
elif f.new_status == Ticket.RESOLVED_STATUS:
template_cc = 'resolved_cc'
elif f.new_status == Ticket.CLOSED_STATUS:
template_cc = 'closed_cc'
else:
template_cc = 'updated_cc'
messages_sent_to.update(ticket.send( messages_sent_to.update(ticket.send(
{'ticket_cc': (template_cc, context)}, {'ticket_cc': (template_cc, context)},
dont_send_to=messages_sent_to, dont_send_to=messages_sent_to,
fail_silently=True, fail_silently=True,
files=files, files=files,
)) ))
ticket.save() ticket.save()
# auto subscribe user if enabled # auto subscribe user if enabled
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated: add_staff_subscription(request, ticket)
ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(
request.user, ticket)
if SHOW_SUBSCRIBE:
subscribe_staff_member_to_ticket(ticket, request.user)
return return_to_ticket(request.user, helpdesk_settings, ticket) return return_to_ticket(request.user, helpdesk_settings, ticket)
@ -887,7 +989,7 @@ mass_update = staff_member_required(mass_update)
# Prepare ticket attributes which will be displayed in the table to choose # Prepare ticket attributes which will be displayed in the table to choose
# which value to keep when merging # which value to keep when merging
ticket_attributes = ( TICKET_ATTRIBUTES = (
('created', _('Created date')), ('created', _('Created date')),
('due_date', _('Due on')), ('due_date', _('Due on')),
('get_status_display', _('Status')), ('get_status_display', _('Status')),
@ -898,30 +1000,20 @@ ticket_attributes = (
) )
@staff_member_required def merge_ticket_values(
def merge_tickets(request): request: WSGIRequest,
""" tickets: typing.List[Ticket],
An intermediate view to merge up to 3 tickets in one main ticket. custom_fields
The user has to first select which ticket will receive the other tickets information and can also choose which ) -> None:
data to keep per attributes as well as custom fields.
Follow-ups and ticketCC will be moved to the main ticket and other tickets won't be able to receive new answers.
"""
ticket_select_form = MultipleTicketSelectForm(request.GET or None)
tickets = custom_fields = None
if ticket_select_form.is_valid():
tickets = ticket_select_form.cleaned_data.get('tickets')
custom_fields = CustomField.objects.all()
default = _('Not defined')
for ticket in tickets: for ticket in tickets:
ticket.values = {} ticket.values = {}
# Prepare the value for each attributes of this ticket # Prepare the value for each attributes of this ticket
for attribute, display_name in ticket_attributes: for attribute, __ in TICKET_ATTRIBUTES:
value = getattr(ticket, attribute, default) value = getattr(ticket, attribute, TicketCustomFieldValue.default_value)
# Check if attr is a get_FIELD_display # Check if attr is a get_FIELD_display
if attribute.startswith('get_') and attribute.endswith('_display'): if attribute.startswith('get_') and attribute.endswith('_display'):
# Hack to call methods like get_FIELD_display() # Hack to call methods like get_FIELD_display()
value = getattr(ticket, attribute, default)() value = getattr(ticket, attribute, TicketCustomFieldValue.default_value)()
ticket.values[attribute] = { ticket.values[attribute] = {
'value': value, 'value': value,
'checked': str(ticket.id) == request.POST.get(attribute) 'checked': str(ticket.id) == request.POST.get(attribute)
@ -932,26 +1024,21 @@ def merge_tickets(request):
value = ticket.ticketcustomfieldvalue_set.get( value = ticket.ticketcustomfieldvalue_set.get(
field=custom_field).value field=custom_field).value
except (TicketCustomFieldValue.DoesNotExist, ValueError): except (TicketCustomFieldValue.DoesNotExist, ValueError):
value = default value = TicketCustomFieldValue.default_value
ticket.values[custom_field.name] = { ticket.values[custom_field.name] = {
'value': value, 'value': value,
'checked': str(ticket.id) == request.POST.get(custom_field.name) 'checked': str(ticket.id) == request.POST.get(custom_field.name)
} }
if request.method == 'POST':
# Find which ticket has been chosen to be the main one def redirect_from_chosen_ticket(
try: request,
chosen_ticket = tickets.get( chosen_ticket,
id=request.POST.get('chosen_ticket')) tickets,
except Ticket.DoesNotExist: custom_fields
ticket_select_form.add_error( ) -> HttpResponseRedirect:
field='tickets',
error=_(
'Please choose a ticket in which the others will be merged into.')
)
else:
# Save ticket fields values # Save ticket fields values
for attribute, display_name in ticket_attributes: for attribute, __ in TICKET_ATTRIBUTES:
id_for_attribute = request.POST.get(attribute) id_for_attribute = request.POST.get(attribute)
if id_for_attribute != chosen_ticket.id: if id_for_attribute != chosen_ticket.id:
try: try:
@ -1039,14 +1126,88 @@ def merge_tickets(request):
ticketcc=ticketcc) ticketcc=ticketcc)
return redirect(chosen_ticket) return redirect(chosen_ticket)
@staff_member_required
def merge_tickets(request):
"""
An intermediate view to merge up to 3 tickets in one main ticket.
The user has to first select which ticket will receive the other tickets information and can also choose which
data to keep per attributes as well as custom fields.
Follow-ups and ticketCC will be moved to the main ticket and other tickets won't be able to receive new answers.
"""
ticket_select_form = MultipleTicketSelectForm(request.GET or None)
tickets = custom_fields = None
if ticket_select_form.is_valid():
tickets = ticket_select_form.cleaned_data.get('tickets')
custom_fields = CustomField.objects.all()
merge_ticket_values(request, tickets, custom_fields)
if request.method == 'POST':
# Find which ticket has been chosen to be the main one
try:
chosen_ticket = tickets.get(
id=request.POST.get('chosen_ticket'))
except Ticket.DoesNotExist:
ticket_select_form.add_error(
field='tickets',
error=_(
'Please choose a ticket in which the others will be merged into.')
)
else:
return redirect_from_chosen_ticket(
request,
chosen_ticket,
tickets,
custom_fields
)
return render(request, 'helpdesk/ticket_merge.html', { return render(request, 'helpdesk/ticket_merge.html', {
'tickets': tickets, 'tickets': tickets,
'ticket_attributes': ticket_attributes, 'ticket_attributes': TICKET_ATTRIBUTES,
'custom_fields': custom_fields, 'custom_fields': custom_fields,
'ticket_select_form': ticket_select_form 'ticket_select_form': ticket_select_form
}) })
def check_redirect_on_user_query(request, huser):
"""If the user is coming from the header/navigation search box, lets' first
look at their query to see if they have entered a valid ticket number. If
they have, just redirect to that ticket number. Otherwise, we treat it as
a keyword search.
"""
if request.GET.get('search_type', None) == 'header':
query = request.GET.get('q')
filter_ = None
if query.find('-') > 0:
try:
queue, id_ = Ticket.queue_and_id_from_query(query)
id_ = int(id)
except ValueError:
id_ = None
if id_:
filter_ = {'queue__slug': queue, 'id': id_}
else:
try:
query = int(query)
except ValueError:
query = None
if query:
filter_ = {'id': int(query)}
if filter_:
try:
ticket = huser.get_tickets_in_queues().get(**filter_)
return HttpResponseRedirect(ticket.staff_url)
except Ticket.DoesNotExist:
# Go on to standard keyword searching
pass
return None
@helpdesk_staff_member_required @helpdesk_staff_member_required
def ticket_list(request): def ticket_list(request):
context = {} context = {}
@ -1071,40 +1232,10 @@ def ticket_list(request):
'sortreverse': False, 'sortreverse': False,
} }
# If the user is coming from the header/navigation search box, lets' first #: check for a redirect, see function doc for details
# look at their query to see if they have entered a valid ticket number. If redirect = check_redirect_on_user_query(request, huser)
# they have, just redirect to that ticket number. Otherwise, we treat it as if redirect:
# a keyword search. return redirect
if request.GET.get('search_type', None) == 'header':
query = request.GET.get('q')
filter = None
if query.find('-') > 0:
try:
queue, id = Ticket.queue_and_id_from_query(query)
id = int(id)
except ValueError:
id = None
if id:
filter = {'queue__slug': queue, 'id': id}
else:
try:
query = int(query)
except ValueError:
query = None
if query:
filter = {'id': int(query)}
if filter:
try:
ticket = huser.get_tickets_in_queues().get(**filter)
return HttpResponseRedirect(ticket.staff_url)
except Ticket.DoesNotExist:
# Go on to standard keyword searching
pass
try: try:
saved_query, query_params = load_saved_query(request, query_params) saved_query, query_params = load_saved_query(request, query_params)
except QueryLoadError: except QueryLoadError:
@ -1300,15 +1431,15 @@ class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixi
@helpdesk_staff_member_required @helpdesk_staff_member_required
def raw_details(request, type): def raw_details(request, type_):
# TODO: This currently only supports spewing out 'PreSetReply' objects, # TODO: This currently only supports spewing out 'PreSetReply' objects,
# in the future it needs to be expanded to include other items. All it # in the future it needs to be expanded to include other items. All it
# does is return a plain-text representation of an object. # does is return a plain-text representation of an object.
if type not in ('preset',): if type_ not in ('preset',):
raise Http404 raise Http404
if type == 'preset' and request.GET.get('id', False): if type_ == 'preset' and request.GET.get('id', False):
try: try:
preset = PreSetReply.objects.get(id=request.GET.get('id')) preset = PreSetReply.objects.get(id=request.GET.get('id'))
return HttpResponse(preset.body) return HttpResponse(preset.body)
@ -1408,12 +1539,18 @@ def report_index(request):
report_index = staff_member_required(report_index) report_index = staff_member_required(report_index)
@helpdesk_staff_member_required def get_report_queryset_or_redirect(request, report):
def run_report(request, report):
if Ticket.objects.all().count() == 0 or report not in ( if Ticket.objects.all().count() == 0 or report not in (
'queuemonth', 'usermonth', 'queuestatus', 'queuepriority', 'userstatus', "queuemonth",
'userpriority', 'userqueue', 'daysuntilticketclosedbymonth'): "usermonth",
return HttpResponseRedirect(reverse("helpdesk:report_index")) "queuestatus",
"queuepriority",
"userstatus",
"userpriority",
"userqueue",
"daysuntilticketclosedbymonth"
):
return None, None, HttpResponseRedirect(reverse("helpdesk:report_index"))
report_queryset = Ticket.objects.all().select_related().filter( report_queryset = Ticket.objects.all().select_related().filter(
queue__in=HelpdeskUser(request.user).get_queues() queue__in=HelpdeskUser(request.user).get_queues()
@ -1422,12 +1559,79 @@ def run_report(request, report):
try: try:
saved_query, query_params = load_saved_query(request) saved_query, query_params = load_saved_query(request)
except QueryLoadError: except QueryLoadError:
return HttpResponseRedirect(reverse('helpdesk:report_index')) return None, HttpResponseRedirect(reverse('helpdesk:report_index'))
return report_queryset, query_params, saved_query, None
def get_report_table_and_totals(header1, summarytable, possible_options):
table = []
totals = {}
for item in header1:
data = []
for hdr in possible_options:
if hdr not in totals.keys():
totals[hdr] = summarytable[item, hdr]
else:
totals[hdr] += summarytable[item, hdr]
data.append(summarytable[item, hdr])
table.append([item] + data)
return table, totals
def update_summary_tables(report_queryset, report, summarytable, summarytable2):
metric3 = False
for ticket in report_queryset:
if report == 'userpriority':
metric1 = u'%s' % ticket.get_assigned_to
metric2 = u'%s' % ticket.get_priority_display()
elif report == 'userqueue':
metric1 = u'%s' % ticket.get_assigned_to
metric2 = u'%s' % ticket.queue.title
elif report == 'userstatus':
metric1 = u'%s' % ticket.get_assigned_to
metric2 = u'%s' % ticket.get_status_display()
elif report == 'usermonth':
metric1 = u'%s' % ticket.get_assigned_to
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
elif report == 'queuepriority':
metric1 = u'%s' % ticket.queue.title
metric2 = u'%s' % ticket.get_priority_display()
elif report == 'queuestatus':
metric1 = u'%s' % ticket.queue.title
metric2 = u'%s' % ticket.get_status_display()
elif report == 'queuemonth':
metric1 = u'%s' % ticket.queue.title
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
elif report == 'daysuntilticketclosedbymonth':
metric1 = u'%s' % ticket.queue.title
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
metric3 = ticket.modified - ticket.created
metric3 = metric3.days
summarytable[metric1, metric2] += 1
if metric3:
if report == 'daysuntilticketclosedbymonth':
summarytable2[metric1, metric2] += metric3
@helpdesk_staff_member_required
def run_report(request, report):
report_queryset, query_params, saved_query, redirect = get_report_queryset_or_redirect(
request, report
)
if redirect:
return redirect
if request.GET.get('saved_query', None): if request.GET.get('saved_query', None):
Query(report_queryset, query_to_base64(query_params)) Query(report_queryset, query_to_base64(query_params))
from collections import defaultdict
summarytable = defaultdict(int) summarytable = defaultdict(int)
# a second table for more complex queries # a second table for more complex queries
summarytable2 = defaultdict(int) summarytable2 = defaultdict(int)
@ -1502,50 +1706,7 @@ def run_report(request, report):
col1heading = _('Queue') col1heading = _('Queue')
possible_options = periods possible_options = periods
charttype = 'date' charttype = 'date'
update_summary_tables(report_queryset, report, summarytable, summarytable2)
metric3 = False
for ticket in report_queryset:
if report == 'userpriority':
metric1 = u'%s' % ticket.get_assigned_to
metric2 = u'%s' % ticket.get_priority_display()
elif report == 'userqueue':
metric1 = u'%s' % ticket.get_assigned_to
metric2 = u'%s' % ticket.queue.title
elif report == 'userstatus':
metric1 = u'%s' % ticket.get_assigned_to
metric2 = u'%s' % ticket.get_status_display()
elif report == 'usermonth':
metric1 = u'%s' % ticket.get_assigned_to
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
elif report == 'queuepriority':
metric1 = u'%s' % ticket.queue.title
metric2 = u'%s' % ticket.get_priority_display()
elif report == 'queuestatus':
metric1 = u'%s' % ticket.queue.title
metric2 = u'%s' % ticket.get_status_display()
elif report == 'queuemonth':
metric1 = u'%s' % ticket.queue.title
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
elif report == 'daysuntilticketclosedbymonth':
metric1 = u'%s' % ticket.queue.title
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
metric3 = ticket.modified - ticket.created
metric3 = metric3.days
summarytable[metric1, metric2] += 1
if metric3:
if report == 'daysuntilticketclosedbymonth':
summarytable2[metric1, metric2] += metric3
table = []
if report == 'daysuntilticketclosedbymonth': if report == 'daysuntilticketclosedbymonth':
for key in summarytable2.keys(): for key in summarytable2.keys():
summarytable[key] = summarytable2[key] / summarytable[key] summarytable[key] = summarytable2[key] / summarytable[key]
@ -1555,18 +1716,9 @@ def run_report(request, report):
column_headings = [col1heading] + possible_options column_headings = [col1heading] + possible_options
# Prepare a dict to store totals for each possible option # Prepare a dict to store totals for each possible option
totals = {} table, totals = get_report_table_and_totals(header1, summarytable, possible_options)
# Pivot the data so that 'header1' fields are always first column # Pivot the data so that 'header1' fields are always first column
# in the row, and 'possible_options' are always the 2nd - nth columns. # in the row, and 'possible_options' are always the 2nd - nth columns.
for item in header1:
data = []
for hdr in possible_options:
if hdr not in totals.keys():
totals[hdr] = summarytable[item, hdr]
else:
totals[hdr] += summarytable[item, hdr]
data.append(summarytable[item, hdr])
table.append([item] + data)
# Zip data and headers together in one list for Morris.js charts # Zip data and headers together in one list for Morris.js charts
# will get a list like [(Header1, Data1), (Header2, Data2)...] # will get a list like [(Header1, Data1), (Header2, Data2)...]
@ -1626,8 +1778,8 @@ save_query = staff_member_required(save_query)
@helpdesk_staff_member_required @helpdesk_staff_member_required
def delete_saved_query(request, id): def delete_saved_query(request, pk):
query = get_object_or_404(SavedSearch, id=id, user=request.user) query = get_object_or_404(SavedSearch, id=pk, user=request.user)
if request.method == 'POST': if request.method == 'POST':
query.delete() query.delete()
@ -1676,8 +1828,8 @@ email_ignore_add = superuser_required(email_ignore_add)
@helpdesk_superuser_required @helpdesk_superuser_required
def email_ignore_del(request, id): def email_ignore_del(request, pk):
ignore = get_object_or_404(IgnoreEmail, id=id) ignore = get_object_or_404(IgnoreEmail, id=pk)
if request.method == 'POST': if request.method == 'POST':
ignore.delete() ignore.delete()
return HttpResponseRedirect(reverse('helpdesk:email_ignore')) return HttpResponseRedirect(reverse('helpdesk:email_ignore'))

23
quicktest.py Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env python
""" """
Usage: Usage:
$ python -m venv .venv $ python -m venv .venv
@ -5,15 +6,15 @@ $ source .venv/bin/activate
$ pip install -r requirements-testing.txt -r requirements.txt $ pip install -r requirements-testing.txt -r requirements.txt
$ python ./quicktest.py $ python ./quicktest.py
""" """
import os
import sys
import argparse
import argparse
import django import django
from django.conf import settings from django.conf import settings
import os
import sys
class QuickDjangoTest(object): class QuickDjangoTest:
""" """
A quick way to run the Django test suite without a fully-configured project. A quick way to run the Django test suite without a fully-configured project.
@ -37,12 +38,12 @@ class QuickDjangoTest(object):
'bootstrap4form', 'bootstrap4form',
# The following commented apps are optional, # The following commented apps are optional,
# related to teams functionalities # related to teams functionalities
#'account', # 'account',
#'pinax.invitations', # 'pinax.invitations',
#'pinax.teams', # 'pinax.teams',
'rest_framework', 'rest_framework',
'helpdesk', 'helpdesk',
#'reversion', # 'reversion',
) )
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
@ -77,6 +78,7 @@ class QuickDjangoTest(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.tests = args self.tests = args
self.kwargs = kwargs or {"verbosity": 1}
self._tests() self._tests()
def _tests(self): def _tests(self):
@ -111,7 +113,7 @@ class QuickDjangoTest(object):
) )
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner
test_runner = DiscoverRunner(verbosity=1) test_runner = DiscoverRunner(verbosity=self.kwargs["verbosity"])
django.setup() django.setup()
failures = test_runner.run_tests(self.tests) failures = test_runner.run_tests(self.tests)
@ -133,7 +135,8 @@ if __name__ == '__main__':
description="Run Django tests." description="Run Django tests."
) )
parser.add_argument('tests', nargs="*", type=str) parser.add_argument('tests', nargs="*", type=str)
parser.add_argument("--verbosity", "-v", nargs="?", type=int, default=1)
args = parser.parse_args() args = parser.parse_args()
if not args.tests: if not args.tests:
args.tests = ['helpdesk'] args.tests = ['helpdesk']
QuickDjangoTest(*args.tests) QuickDjangoTest(*args.tests, verbosity=args.verbosity)

View File

@ -6,3 +6,4 @@ argparse
pbr pbr
mock mock
freezegun freezegun
isort

View File

@ -1,4 +1,4 @@
Django>=2.2,<4.0 Django>=3.2,<4.0
django-bootstrap4-form django-bootstrap4-form
celery celery
email-reply-parser email-reply-parser
@ -12,3 +12,4 @@ six
pinax_teams pinax_teams
djangorestframework djangorestframework
django-model-utils django-model-utils
django-cleanup

View File

@ -1,10 +1,13 @@
import os """django-helpdesk setup"""
import sys
from distutils.util import convert_path from distutils.util import convert_path
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from setuptools import setup, find_packages import os
from setuptools import find_packages, setup
import sys
version = '0.5.0a1'
version = '0.4.1'
# Provided as an attribute, so you can append to these instead # Provided as an attribute, so you can append to these instead
# of replicating them: # of replicating them:

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