mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-11-25 01:13:31 +01:00
Fix. Add test markdown and rewrite for last markdown is working
This commit is contained in:
commit
3a13bbfeef
8
.flake8
8
.flake8
@ -2,3 +2,11 @@
|
||||
max-line-length = 120
|
||||
exclude = .git,__pycache__,.tox,.eggs,*.egg,node_modules,.venv,migrations,docs,demo,tests,setup.py
|
||||
import-order-style = pep8
|
||||
max-complexity = 20
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length = 120
|
||||
exclude = "migrations"
|
||||
in-place = true
|
||||
recursive = true
|
||||
|
||||
|
53
.github/workflows/pythonpackage.yml
vendored
Normal file
53
.github/workflows/pythonpackage.yml
vendored
Normal 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
19
.isort.cfg
Normal file
@ -0,0 +1,19 @@
|
||||
[settings]
|
||||
src_paths=helpdesk
|
||||
skip_glob=migrations/*
|
||||
skip=migrations
|
||||
multi_line_output=3
|
||||
line_length=120
|
||||
use_parentheses=true
|
||||
sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
||||
lines_after_imports=2
|
||||
lines_before_imports=1
|
||||
balanced_wrapping=true
|
||||
lines_between_types=1
|
||||
combine_as_imports=true
|
||||
force_alphabetical_sort=true
|
||||
skip_gitignore=true
|
||||
force_sort_within_sections=true
|
||||
group_by_package=false
|
||||
from_first=true
|
||||
|
@ -8,8 +8,10 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -13,10 +13,10 @@ Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.urls import include, path
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
|
||||
# The following uses the static() helper function,
|
||||
|
@ -7,9 +7,10 @@ For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
import os
|
||||
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Python packaging."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from setuptools import setup
|
||||
import os
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
project_root = os.path.dirname(here)
|
||||
@ -13,7 +11,7 @@ project_root = os.path.dirname(here)
|
||||
NAME = 'django-helpdesk-demodesk'
|
||||
DESCRIPTION = 'A demo Django project using django-helpdesk'
|
||||
README = open(os.path.join(here, 'README.rst')).read()
|
||||
VERSION = '0.4.1'
|
||||
VERSION = '0.5.0a1'
|
||||
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
|
||||
AUTHOR = 'django-helpdesk team'
|
||||
URL = 'https://github.com/django-helpdesk/django-helpdesk'
|
||||
|
@ -11,8 +11,9 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
|
@ -63,6 +63,7 @@ errors with trying to create User settings.
|
||||
'pinax.teams', # Team support
|
||||
'reversion', # Required by pinax-teams
|
||||
'rest_framework', # required for the API
|
||||
'django_cleanup.apps.CleanupConfig', # Remove this if you do NOT want to delete files on the file system when the associated record is deleted in the database
|
||||
'helpdesk', # This is us!
|
||||
)
|
||||
|
||||
|
@ -13,4 +13,4 @@ You can assign a knowledge-base item to a team on the Helpdesk admin page.
|
||||
|
||||
Once you have set up teams. Unassigned tickets which are associated with a knowledge-base item will only be shown on the dashboard to those users who are members of the team which is associated with that knowledge-base item.
|
||||
|
||||
Note: It is possible that pinax-teams will interfere with other packages that you already use in your project. If you do not wish to use team functionality, you can dissable teams by setting the following settings: ``HELPDESK_TEAMS_MODEL`` to any random model, ``HELPDESK_TEAMS_MIGRATION_DEPENDENCIES`` to ``[]``, and ``HELPDESK_KBITEM_TEAM_GETTER`` to ``lambda _: None``. You can also use a different library in place of pinax teams by setting those settings appropriately. ``HELPDESK_KBITEM_TEAM_GETTER`` should take a ``kbitem`` and return a team object with a ``name`` property and a method ``is_member(self, user)`` which returns true if user is a member of the team.
|
||||
Note: It is possible that pinax-teams will interfere with other packages that you already use in your project. If you do not wish to use team functionality, you can disable teams by setting the following settings: ``HELPDESK_TEAMS_MODEL`` to any random model, ``HELPDESK_TEAMS_MIGRATION_DEPENDENCIES`` to ``[]``, and ``HELPDESK_KBITEM_TEAM_GETTER`` to ``lambda _: None``. You can also use a different library in place of pinax teams by setting those settings appropriately. ``HELPDESK_KBITEM_TEAM_GETTER`` should take a ``kbitem`` and return a team object with a ``name`` property and a method ``is_member(self, user)`` which returns true if user is a member of the team.
|
||||
|
@ -1,13 +1,24 @@
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, 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.models import (
|
||||
CustomField,
|
||||
EmailTemplate,
|
||||
EscalationExclusion,
|
||||
FollowUp,
|
||||
FollowUpAttachment,
|
||||
IgnoreEmail,
|
||||
KBIAttachment,
|
||||
PreSetReply,
|
||||
Queue,
|
||||
Ticket,
|
||||
TicketChange
|
||||
)
|
||||
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.models import KBCategory
|
||||
from helpdesk.models import KBItem
|
||||
from helpdesk.models import KBCategory, KBItem
|
||||
|
||||
|
||||
@admin.register(Queue)
|
||||
@ -82,6 +93,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
|
||||
list_display_links = ('title',)
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
@admin.register(KBCategory)
|
||||
class KBCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'title', 'slug', 'public')
|
||||
|
@ -1,12 +1,8 @@
|
||||
from functools import wraps
|
||||
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
|
||||
|
||||
from functools import wraps
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
|
||||
|
@ -4,23 +4,11 @@ Django Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
(c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved.
|
||||
See LICENSE for details.
|
||||
"""
|
||||
|
||||
# import base64
|
||||
import email
|
||||
import imaplib
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import poplib
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from email.utils import getaddresses
|
||||
from os.path import isfile, join
|
||||
from time import ctime
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import timedelta
|
||||
from django.conf import settings as django_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -28,11 +16,24 @@ from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.db.models import Q
|
||||
from django.utils import encoding, timezone
|
||||
from django.utils.translation import gettext as _
|
||||
import email
|
||||
from email.utils import getaddresses
|
||||
from email_reply_parser import EmailReplyParser
|
||||
|
||||
from helpdesk import settings
|
||||
from helpdesk.lib import safe_template_context, process_attachments
|
||||
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
|
||||
from helpdesk.lib import process_attachments, safe_template_context
|
||||
from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket
|
||||
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
|
||||
@ -176,13 +177,13 @@ def imap_sync(q, logger, server):
|
||||
sys.exit()
|
||||
|
||||
try:
|
||||
status, data = server.search(None, 'NOT', 'DELETED')
|
||||
data = server.search(None, 'NOT', 'DELETED')[1]
|
||||
if data:
|
||||
msgnums = data[0].split()
|
||||
logger.info("Received %d messages from IMAP server" % len(msgnums))
|
||||
for num in msgnums:
|
||||
logger.info("Processing message %s" % num)
|
||||
status, data = server.fetch(num, '(RFC822)')
|
||||
data = server.fetch(num, '(RFC822)')[1]
|
||||
full_message = encoding.force_str(data[0][1], errors='replace')
|
||||
try:
|
||||
ticket = object_from_message(
|
||||
@ -342,10 +343,10 @@ def create_ticket_cc(ticket, cc_list):
|
||||
return []
|
||||
|
||||
# Local import to deal with non-defined / circular reference problem
|
||||
from helpdesk.views.staff import User, subscribe_to_ticket_updates
|
||||
from helpdesk.views.staff import subscribe_to_ticket_updates, User
|
||||
|
||||
new_ticket_ccs = []
|
||||
for cced_name, cced_email in cc_list:
|
||||
for __, cced_email in cc_list:
|
||||
|
||||
cced_email = cced_email.strip()
|
||||
if cced_email == ticket.queue.email_address:
|
||||
@ -354,7 +355,7 @@ def create_ticket_cc(ticket, cc_list):
|
||||
user = None
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=cced_email)
|
||||
user = User.objects.get(email=cced_email) # @UndefinedVariable
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
|
||||
@ -466,16 +467,6 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
new_ticket_ccs = []
|
||||
new_ticket_ccs.append(create_ticket_cc(ticket, to_list + cc_list))
|
||||
|
||||
notifications_to_be_sent = [sender_email]
|
||||
|
||||
if queue.enable_notifications_on_email_events and len(notifications_to_be_sent):
|
||||
|
||||
ticket_cc_list = TicketCC.objects.filter(
|
||||
ticket=ticket).all().values_list('email', flat=True)
|
||||
|
||||
for email_address in ticket_cc_list:
|
||||
notifications_to_be_sent.append(email_address)
|
||||
|
||||
autoreply = is_autoreply(message)
|
||||
if autoreply:
|
||||
logger.info(
|
||||
@ -516,7 +507,77 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
return ticket
|
||||
|
||||
|
||||
def object_from_message(message, queue, logger):
|
||||
def get_ticket_id_from_subject_slug(
|
||||
queue_slug: str,
|
||||
subject: str,
|
||||
logger: logging.Logger
|
||||
) -> typing.Optional[int]:
|
||||
"""Get a ticket id from the subject string
|
||||
|
||||
Performs a match on the subject using the queue_slug as reference,
|
||||
returning the ticket id if a match is found.
|
||||
"""
|
||||
matchobj = re.match(r".*\[" + queue_slug + r"-(?P<id>\d+)\]", subject)
|
||||
ticket_id = None
|
||||
if matchobj:
|
||||
# This is a reply or forward.
|
||||
ticket_id = matchobj.group('id')
|
||||
logger.info("Matched tracking ID %s-%s" % (queue_slug, ticket_id))
|
||||
else:
|
||||
logger.info("No tracking ID matched.")
|
||||
return ticket_id
|
||||
|
||||
|
||||
def add_file_if_always_save_incoming_email_message(
|
||||
files_,
|
||||
message: str
|
||||
) -> None:
|
||||
"""When `settings.HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE` is `True`
|
||||
add a file to the files_ list"""
|
||||
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False):
|
||||
# save message as attachment in case of some complex markup renders
|
||||
# wrong
|
||||
files_.append(
|
||||
SimpleUploadedFile(
|
||||
_("original_message.eml").replace(
|
||||
".eml",
|
||||
timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml"
|
||||
),
|
||||
str(message).encode("utf-8"),
|
||||
'text/plain'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_encoded_body(body: str) -> str:
|
||||
try:
|
||||
return body.encode('ascii').decode('unicode_escape')
|
||||
except UnicodeEncodeError:
|
||||
return body
|
||||
|
||||
|
||||
def get_body_from_fragments(body) -> str:
|
||||
"""Gets a body from the fragments, joined by a double line break"""
|
||||
return "\n\n".join(f.content for f in EmailReplyParser.read(body).fragments)
|
||||
|
||||
|
||||
def get_email_body_from_part_payload(part) -> str:
|
||||
"""Gets an decoded body from the payload part, if the decode fails,
|
||||
returns without encoding"""
|
||||
try:
|
||||
return encoding.smart_str(
|
||||
part.get_payload(decode=True)
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
return encoding.smart_str(
|
||||
part.get_payload(decode=False)
|
||||
)
|
||||
|
||||
|
||||
def object_from_message(message: str,
|
||||
queue: Queue,
|
||||
logger: logging.Logger
|
||||
) -> Ticket:
|
||||
# 'message' must be an RFC822 formatted message.
|
||||
message = email.message_from_string(message)
|
||||
|
||||
@ -539,35 +600,17 @@ def object_from_message(message, queue, logger):
|
||||
sender_email = email.utils.getaddresses(
|
||||
['\"' + sender.replace('<', '\" <')])[0][1]
|
||||
|
||||
cc = message.get_all('cc', None)
|
||||
if cc:
|
||||
# first, fixup the encoding if necessary
|
||||
cc = [decode_mail_headers(decodeUnknown(
|
||||
message.get_charset(), x)) for x in cc]
|
||||
# get_all checks if multiple CC headers, but individual emails may be
|
||||
# comma separated too
|
||||
tempcc = []
|
||||
for hdr in cc:
|
||||
tempcc.extend(hdr.split(','))
|
||||
# use a set to ensure no duplicates
|
||||
cc = set([x.strip() for x in tempcc])
|
||||
|
||||
for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)):
|
||||
if ignore.test(sender_email):
|
||||
if ignore.keep_in_mailbox:
|
||||
# By returning 'False' the message will be kept in the mailbox,
|
||||
# and the 'True' will cause the message to be deleted.
|
||||
return False
|
||||
return True
|
||||
return not ignore.keep_in_mailbox
|
||||
|
||||
matchobj = re.match(r".*\[" + queue.slug + r"-(?P<id>\d+)\]", subject)
|
||||
if matchobj:
|
||||
# This is a reply or forward.
|
||||
ticket = matchobj.group('id')
|
||||
logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket))
|
||||
else:
|
||||
logger.info("No tracking ID matched.")
|
||||
ticket = None
|
||||
ticket_id: typing.Optional[int] = get_ticket_id_from_subject_slug(
|
||||
queue.slug,
|
||||
subject,
|
||||
logger
|
||||
)
|
||||
|
||||
body = None
|
||||
full_body = None
|
||||
@ -591,13 +634,10 @@ def object_from_message(message, queue, logger):
|
||||
body = decodeUnknown(part.get_content_charset(), body)
|
||||
# have to use django_settings here so overwritting it works in tests
|
||||
# the default value is False anyway
|
||||
if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False):
|
||||
if ticket_id is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False):
|
||||
# first message in thread, we save full body to avoid
|
||||
# losing forwards and things like that
|
||||
body_parts = []
|
||||
for f in EmailReplyParser.read(body).fragments:
|
||||
body_parts.append(f.content)
|
||||
full_body = '\n\n'.join(body_parts)
|
||||
full_body = get_body_from_fragments(body)
|
||||
body = EmailReplyParser.parse_reply(body)
|
||||
else:
|
||||
# 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)
|
||||
full_body = body
|
||||
# workaround to get unicode text out rather than escaped text
|
||||
try:
|
||||
body = body.encode('ascii').decode('unicode_escape')
|
||||
except UnicodeEncodeError:
|
||||
body.encode('utf-8')
|
||||
body = get_encoded_body(body)
|
||||
logger.debug("Discovered plain text MIME part")
|
||||
else:
|
||||
try:
|
||||
email_body = encoding.smart_str(
|
||||
part.get_payload(decode=True))
|
||||
except UnicodeDecodeError:
|
||||
email_body = encoding.smart_str(
|
||||
part.get_payload(decode=False))
|
||||
email_body = get_email_body_from_part_payload(part)
|
||||
|
||||
if not body and not full_body:
|
||||
# no text has been parsed so far - try such deep parsing
|
||||
@ -683,19 +715,7 @@ def object_from_message(message, queue, logger):
|
||||
if not body:
|
||||
body = ""
|
||||
|
||||
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False):
|
||||
# save message as attachment in case of some complex markup renders
|
||||
# wrong
|
||||
files.append(
|
||||
SimpleUploadedFile(
|
||||
_("original_message.eml").replace(
|
||||
".eml",
|
||||
timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml"
|
||||
),
|
||||
str(message).encode("utf-8"),
|
||||
'text/plain'
|
||||
)
|
||||
)
|
||||
add_file_if_always_save_incoming_email_message(files, message)
|
||||
|
||||
smtp_priority = message.get('priority', '')
|
||||
smtp_importance = message.get('importance', '')
|
||||
@ -713,4 +733,4 @@ def object_from_message(message, queue, logger):
|
||||
'files': files,
|
||||
}
|
||||
|
||||
return create_object_from_email_message(message, ticket, payload, files, logger=logger)
|
||||
return create_object_from_email_message(message, ticket_id, payload, files, logger=logger)
|
||||
|
@ -6,25 +6,38 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
forms.py - Definitions of newforms-based forms for creating and maintaining
|
||||
tickets.
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, date, time
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from datetime import datetime
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk.lib import safe_template_context, process_attachments, convert_value
|
||||
from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC,
|
||||
CustomField, TicketCustomFieldValue, TicketDependency, UserSettings)
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.settings import CUSTOMFIELD_TO_FIELD_DICT, CUSTOMFIELD_DATETIME_FORMAT, \
|
||||
CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
||||
from helpdesk.lib import convert_value, process_attachments, safe_template_context
|
||||
from helpdesk.models import (
|
||||
CustomField,
|
||||
FollowUp,
|
||||
IgnoreEmail,
|
||||
Queue,
|
||||
Ticket,
|
||||
TicketCC,
|
||||
TicketCustomFieldValue,
|
||||
TicketDependency,
|
||||
UserSettings
|
||||
)
|
||||
from helpdesk.settings import (
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
CUSTOMFIELD_DATETIME_FORMAT,
|
||||
CUSTOMFIELD_TIME_FORMAT,
|
||||
CUSTOMFIELD_TO_FIELD_DICT
|
||||
)
|
||||
import logging
|
||||
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.models import (KBItem)
|
||||
from helpdesk.models import KBItem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
@ -226,7 +239,10 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
widget=forms.FileInput(attrs={'class': 'form-control-file'}),
|
||||
required=False,
|
||||
label=_('Attach File'),
|
||||
help_text=_('You can attach a file to this ticket. Only file types such as plain text (.txt), a document (.pdf, .docx, or .odt), or screenshot (.png or .jpg) may be uploaded.'),
|
||||
help_text=_('You can attach a file to this ticket. '
|
||||
'Only file types such as plain text (.txt), '
|
||||
'a document (.pdf, .docx, or .odt), '
|
||||
'or screenshot (.png or .jpg) may be uploaded.'),
|
||||
)
|
||||
|
||||
class Media:
|
||||
|
@ -6,14 +6,14 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
lib.py - Common functions (eg multipart e-mail)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
from datetime import datetime, date, time
|
||||
|
||||
from datetime import date, datetime, time
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import smart_str
|
||||
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
||||
import logging
|
||||
import mimetypes
|
||||
|
||||
from helpdesk.settings import CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
||||
|
||||
logger = logging.getLogger('helpdesk')
|
||||
|
||||
|
@ -8,12 +8,11 @@ scripts/create_escalation_exclusion.py - Easy way to routinely add particular
|
||||
days to the list of days on which no
|
||||
escalation should take place.
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
|
||||
from datetime import timedelta, date
|
||||
import getopt
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
from optparse import make_option
|
||||
import sys
|
||||
|
||||
|
@ -13,15 +13,13 @@ scripts/create_queue_permissions.py -
|
||||
existing permissions.
|
||||
"""
|
||||
|
||||
from optparse import make_option
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from helpdesk.models import Queue
|
||||
from optparse import make_option
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -8,12 +8,12 @@ create_usersettings.py - Easy way to create helpdesk-specific settings for
|
||||
users who don't yet have them.
|
||||
"""
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import gettext as _
|
||||
from helpdesk.models import UserSettings
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
|
@ -8,18 +8,17 @@ scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
|
||||
designed to be run from Cron or similar.
|
||||
"""
|
||||
|
||||
from datetime import timedelta, date
|
||||
import getopt
|
||||
from optparse import make_option
|
||||
import sys
|
||||
|
||||
from datetime import date, timedelta
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk.models import Queue, Ticket, FollowUp, EscalationExclusion, TicketChange
|
||||
from django.utils.translation import gettext as _
|
||||
import getopt
|
||||
from helpdesk.lib import safe_template_context
|
||||
from helpdesk.models import EscalationExclusion, FollowUp, Queue, Ticket, TicketChange
|
||||
from optparse import make_option
|
||||
import sys
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -11,7 +11,6 @@ scripts/get_email.py - Designed to be run from cron, this script checks the
|
||||
adding to existing tickets if needed)
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from helpdesk.email import process_email
|
||||
|
||||
|
||||
|
@ -7,35 +7,29 @@ models.py - Model (and hence database) definitions. This is the core of the
|
||||
helpdesk structure.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
from .lib import convert_value
|
||||
from .templated_email import send_templated_mail
|
||||
from .validators import validate_file_extension
|
||||
import datetime
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _, gettext
|
||||
from io import StringIO
|
||||
import re
|
||||
import os
|
||||
import mimetypes
|
||||
import datetime
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from io import StringIO
|
||||
from markdown import markdown
|
||||
from markdown.extensions import Extension
|
||||
|
||||
|
||||
import uuid
|
||||
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
from rest_framework import serializers
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from .lib import convert_value
|
||||
|
||||
from .validators import validate_file_extension
|
||||
|
||||
from .templated_email import send_templated_mail
|
||||
import uuid
|
||||
|
||||
|
||||
def format_time_spent(time_spent):
|
||||
@ -59,7 +53,7 @@ def get_markdown(text):
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
pattern = fr'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)'
|
||||
pattern = r'([\[\s\S\]]*?)\(([\s\S]*?):([\s\S]*?)\)'
|
||||
# Regex check
|
||||
if re.match(pattern, text):
|
||||
# get get value of group regex
|
||||
@ -1969,6 +1963,10 @@ class TicketCustomFieldValue(models.Model):
|
||||
def __str__(self):
|
||||
return '%s / %s' % (self.ticket, self.field)
|
||||
|
||||
@property
|
||||
def default_value(self) -> str:
|
||||
return _("Not defined")
|
||||
|
||||
class Meta:
|
||||
unique_together = (('ticket', 'field'),)
|
||||
verbose_name = _('Ticket custom field value')
|
||||
|
@ -1,15 +1,12 @@
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from base64 import b64encode
|
||||
from base64 import b64decode
|
||||
import json
|
||||
|
||||
from model_utils import Choices
|
||||
|
||||
from helpdesk.serializers import DatatablesTicketSerializer
|
||||
import json
|
||||
from model_utils import Choices
|
||||
|
||||
|
||||
def query_to_base64(query):
|
||||
|
@ -1,13 +1,12 @@
|
||||
from rest_framework import serializers
|
||||
from .forms import TicketForm
|
||||
from .lib import format_time_spent, process_attachments
|
||||
from .models import CustomField, FollowUp, FollowUpAttachment, Ticket
|
||||
from .user import HelpdeskUser
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.humanize.templatetags import humanize
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from .forms import TicketForm
|
||||
from .models import Ticket, CustomField, FollowUp, FollowUpAttachment
|
||||
from .lib import format_time_spent, process_attachments
|
||||
from .user import HelpdeskUser
|
||||
|
||||
|
||||
class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
|
@ -2,12 +2,13 @@
|
||||
Default settings for django-helpdesk.
|
||||
|
||||
"""
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import os
|
||||
import warnings
|
||||
|
||||
|
||||
DEFAULT_USER_SETTINGS = {
|
||||
'login_view_ticketlist': True,
|
||||
|
@ -1,6 +1,5 @@
|
||||
from celery import shared_task
|
||||
|
||||
from .email import process_email
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task
|
||||
|
@ -1,9 +1,10 @@
|
||||
import os
|
||||
import logging
|
||||
from smtplib import SMTPException
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
import logging
|
||||
import os
|
||||
from smtplib import SMTPException
|
||||
|
||||
|
||||
logger = logging.getLogger('helpdesk')
|
||||
|
||||
@ -50,8 +51,7 @@ def send_templated_mail(template_name,
|
||||
from_string = engines['django'].from_string
|
||||
|
||||
from helpdesk.models import EmailTemplate
|
||||
from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \
|
||||
HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||
from helpdesk.settings import HELPDESK_EMAIL_FALLBACK_LOCALE, HELPDESK_EMAIL_SUBJECT_TEMPLATE
|
||||
|
||||
headers = extra_headers or {}
|
||||
|
||||
|
@ -4,10 +4,10 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff.
|
||||
templatetags/helpdesk_staff.py
|
||||
"""
|
||||
import logging
|
||||
from django.template import Library
|
||||
|
||||
from django.template import Library
|
||||
from helpdesk.decorators import is_helpdesk_staff
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -1,10 +1,9 @@
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.template import Library
|
||||
from django.template.defaultfilters import date as date_filter
|
||||
from django.conf import settings
|
||||
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT, CUSTOMFIELD_DATETIME_FORMAT
|
||||
|
||||
register = Library()
|
||||
|
||||
|
@ -7,7 +7,6 @@ templatetags/saved_queries.py - This template tag returns previously saved
|
||||
"""
|
||||
from django import template
|
||||
from django.db.models import Q
|
||||
|
||||
from helpdesk.models import SavedSearch
|
||||
|
||||
|
||||
|
@ -10,12 +10,11 @@ templatetags/ticket_to_link.py - Used in ticket comments to allow wiki-style
|
||||
to show the status of that ticket (eg a closed
|
||||
ticket would have a strikethrough).
|
||||
"""
|
||||
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from helpdesk.models import Ticket
|
||||
|
||||
import re
|
||||
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from helpdesk.models import Ticket, Queue, UserSettings
|
||||
from django.contrib.auth import get_user_model
|
||||
from helpdesk.models import Queue, Ticket, UserSettings
|
||||
import sys
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -1,18 +1,22 @@
|
||||
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from freezegun import freeze_time
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from helpdesk.models import CustomField, Queue, Ticket
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework.exceptions import ErrorDetail
|
||||
from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK,
|
||||
HTTP_201_CREATED,
|
||||
HTTP_204_NO_CONTENT,
|
||||
HTTP_400_BAD_REQUEST,
|
||||
HTTP_403_FORBIDDEN
|
||||
)
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from helpdesk.models import Queue, Ticket, CustomField
|
||||
|
||||
|
||||
class TicketTest(APITestCase):
|
||||
due_date = datetime(2022, 4, 10, 15, 6)
|
||||
|
@ -1,15 +1,13 @@
|
||||
# vim: set fileencoding=utf-8 :
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.urls import reverse
|
||||
from django.test import override_settings, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
from helpdesk import lib, models
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from tempfile import gettempdir
|
||||
|
||||
from unittest import mock
|
||||
from unittest.case import skip
|
||||
|
||||
|
@ -1,24 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management import call_command
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, FollowUpAttachment
|
||||
from helpdesk.management.commands.get_email import Command
|
||||
from django.test import override_settings, TestCase
|
||||
import helpdesk.email
|
||||
|
||||
import six
|
||||
from helpdesk.management.commands.get_email import Command
|
||||
from helpdesk.models import FollowUp, FollowUpAttachment, Queue, Ticket, TicketCC
|
||||
import itertools
|
||||
from shutil import rmtree
|
||||
import sys
|
||||
import os
|
||||
from tempfile import mkdtemp
|
||||
import logging
|
||||
|
||||
import os
|
||||
from shutil import rmtree
|
||||
import six
|
||||
import sys
|
||||
from tempfile import mkdtemp
|
||||
from unittest import mock
|
||||
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# class A addresses can't have first octet of 0
|
||||
|
@ -1,11 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import KBCategory, KBItem, Queue, Ticket
|
||||
|
||||
from helpdesk.tests.helpers import (
|
||||
get_staff_user, reload_urlconf, User, create_ticket, print_response)
|
||||
from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User
|
||||
|
||||
|
||||
class KBTests(TestCase):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test import override_settings, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
from importlib import reload
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import Queue
|
||||
from helpdesk.tests.helpers import (
|
||||
get_staff_user, reload_urlconf, User, create_ticket, print_response)
|
||||
from django.test.utils import override_settings
|
||||
from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User
|
||||
from importlib import reload
|
||||
import sys
|
||||
|
||||
|
||||
class KBDisabledTestCase(TestCase):
|
||||
|
@ -1,11 +1,10 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
|
||||
from helpdesk.models import Queue, Ticket
|
||||
from django.urls import reverse
|
||||
from helpdesk import settings
|
||||
from helpdesk.models import Queue, Ticket
|
||||
from helpdesk.query import __Query__
|
||||
from helpdesk.user import HelpdeskUser
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from helpdesk.models import Queue, Ticket
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
|
||||
class PublicActionsTestCase(TestCase):
|
||||
|
@ -1,12 +1,9 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from helpdesk.models import KBCategory, KBItem, Queue, Ticket
|
||||
from helpdesk.query import query_to_base64
|
||||
|
||||
from helpdesk.tests.helpers import (
|
||||
get_staff_user, reload_urlconf, User, create_ticket, print_response)
|
||||
from helpdesk.tests.helpers import create_ticket, get_staff_user, print_response, reload_urlconf, User
|
||||
|
||||
|
||||
class QueryTests(TestCase):
|
||||
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import Queue
|
||||
from helpdesk.tests.helpers import get_user
|
||||
|
||||
|
@ -1,22 +1,21 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import mail
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk.models import CustomField, Queue, Ticket
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import CustomField, Queue, Ticket
|
||||
from helpdesk.templatetags.ticket_to_link import num_to_link
|
||||
from helpdesk.user import HelpdeskUser
|
||||
|
||||
|
||||
try: # python 3
|
||||
from urllib.parse import urlparse
|
||||
except ImportError: # python 2
|
||||
from urlparse import urlparse
|
||||
|
||||
from helpdesk.templatetags.ticket_to_link import num_to_link
|
||||
from helpdesk.user import HelpdeskUser
|
||||
|
||||
|
||||
class TicketActionsTestCase(TestCase):
|
||||
fixtures = ['emailtemplate.json']
|
||||
|
@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from helpdesk.models import Ticket, Queue
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -1,22 +1,19 @@
|
||||
|
||||
import email
|
||||
import uuid
|
||||
|
||||
from helpdesk.models import Queue, CustomField, FollowUp, Ticket, TicketCC, KBCategory, KBItem
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import mail
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.forms import ValidationError
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
|
||||
from helpdesk.email import object_from_message, create_ticket_cc
|
||||
import email
|
||||
from helpdesk.email import create_ticket_cc, object_from_message
|
||||
from helpdesk.models import CustomField, FollowUp, KBCategory, KBItem, Queue, Ticket, TicketCC
|
||||
from helpdesk.tests.helpers import print_response
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
import uuid
|
||||
|
||||
|
||||
logger = logging.getLogger('helpdesk')
|
||||
|
@ -1,23 +1,24 @@
|
||||
|
||||
import datetime
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import mail
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from helpdesk.models import Queue, Ticket, FollowUp
|
||||
from django.urls import reverse
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from helpdesk.models import FollowUp, Queue, Ticket
|
||||
from helpdesk.templatetags.ticket_to_link import num_to_link
|
||||
import uuid
|
||||
import datetime
|
||||
|
||||
|
||||
try: # python 3
|
||||
from urllib.parse import urlparse
|
||||
except ImportError: # python 2
|
||||
from urlparse import urlparse
|
||||
|
||||
from helpdesk.templatetags.ticket_to_link import num_to_link
|
||||
|
||||
|
||||
class TimeSpentTestCase(TestCase):
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import mail
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import CustomField, Queue, Ticket
|
||||
|
||||
|
||||
try: # python 3
|
||||
from urllib.parse import urlparse
|
||||
except ImportError: # python 2
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.urls import include, path
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', include('helpdesk.urls', namespace='helpdesk')),
|
||||
|
@ -7,17 +7,16 @@ urls.py - Mapping of URL's to our various views. Note we always used NAMED
|
||||
views for simplicity in linking later on.
|
||||
"""
|
||||
|
||||
from django.urls import path, re_path
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.urls import include
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import include, path, re_path
|
||||
from django.views.generic import TemplateView
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
|
||||
from helpdesk.views import feeds, login, public, staff
|
||||
from helpdesk.views.api import CreateUserView, FollowUpAttachmentViewSet, FollowUpViewSet, TicketViewSet
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
|
||||
from helpdesk.views import feeds, staff, public, login
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.views.api import TicketViewSet, CreateUserView, FollowUpViewSet, FollowUpAttachmentViewSet
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.views import kb
|
||||
|
@ -1,15 +1,10 @@
|
||||
from helpdesk.models import (
|
||||
Ticket,
|
||||
Queue
|
||||
)
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.models import (
|
||||
KBCategory,
|
||||
KBItem
|
||||
)
|
||||
from helpdesk.models import KBCategory, KBItem
|
||||
|
||||
|
||||
def huser_from_request(req):
|
||||
|
@ -2,16 +2,18 @@
|
||||
#
|
||||
# validators for file uploads, etc.
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
# TODO: can we use the builtin Django validator instead?
|
||||
# see:
|
||||
# https://docs.djangoproject.com/en/4.0/ref/validators/#fileextensionvalidator
|
||||
|
||||
|
||||
def validate_file_extension(value):
|
||||
import os
|
||||
from django.core.exceptions import ValidationError
|
||||
import os
|
||||
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
|
||||
# TODO: we might improve this with more thorough checks of file types
|
||||
# rather than just the extensions.
|
||||
|
@ -1,11 +1,10 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket
|
||||
from helpdesk.serializers import FollowUpAttachmentSerializer, FollowUpSerializer, TicketSerializer, UserSerializer
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from helpdesk.models import Ticket, FollowUp, FollowUpAttachment
|
||||
from helpdesk.serializers import TicketSerializer, UserSerializer, FollowUpSerializer, FollowUpAttachmentSerializer
|
||||
|
||||
|
||||
class TicketViewSet(viewsets.ModelViewSet):
|
||||
|
@ -9,12 +9,12 @@ views/feeds.py - A handful of staff-only RSS feeds to provide ticket details
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from helpdesk.models import FollowUp, Queue, Ticket
|
||||
|
||||
from helpdesk.models import Ticket, FollowUp, Queue
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -8,12 +8,10 @@ views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
|
||||
resolutions to common problems.
|
||||
"""
|
||||
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk import user
|
||||
from helpdesk import settings as helpdesk_settings, user
|
||||
from helpdesk.models import KBCategory, KBItem
|
||||
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
|
||||
from helpdesk.decorators import is_helpdesk_staff
|
||||
|
||||
|
||||
|
@ -6,30 +6,29 @@ django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
views/public.py - All public facing views, eg non-staff (no authentication
|
||||
required) views.
|
||||
"""
|
||||
import logging
|
||||
from importlib import import_module
|
||||
|
||||
from django.core.exceptions import (
|
||||
ObjectDoesNotExist, PermissionDenied, ImproperlyConfigured,
|
||||
)
|
||||
from django.urls import reverse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from urllib.parse import quote
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.conf import settings
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.decorators import protect_view, is_helpdesk_staff
|
||||
import helpdesk.views.staff as staff
|
||||
import helpdesk.views.abstract_views as abstract_views
|
||||
from helpdesk.decorators import is_helpdesk_staff, protect_view
|
||||
from helpdesk.lib import text_is_spam
|
||||
from helpdesk.models import Ticket, Queue, UserSettings
|
||||
from helpdesk.models import Queue, Ticket, UserSettings
|
||||
from helpdesk.user import huser_from_request
|
||||
import helpdesk.views.abstract_views as abstract_views
|
||||
import helpdesk.views.staff as staff
|
||||
from importlib import import_module
|
||||
import logging
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -212,6 +211,7 @@ def view_ticket(request):
|
||||
|
||||
if 'close' in request.GET and ticket.status == Ticket.RESOLVED_STATUS:
|
||||
from helpdesk.views.staff import update_ticket
|
||||
|
||||
# Trick the update_ticket() view into thinking it's being called with
|
||||
# a valid POST.
|
||||
request.POST = {
|
||||
|
@ -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
|
||||
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
|
||||
import json
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.core.exceptions import ValidationError, PermissionDenied
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect, Http404, HttpResponse, JsonResponse
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.html import escape
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.csrf import requires_csrf_token
|
||||
from django.views.generic.edit import FormView, UpdateView
|
||||
|
||||
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT
|
||||
from helpdesk.query import (
|
||||
get_query_class,
|
||||
query_to_base64,
|
||||
query_from_base64,
|
||||
)
|
||||
|
||||
from helpdesk.user import HelpdeskUser
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.decorators import (
|
||||
helpdesk_staff_member_required, helpdesk_superuser_required,
|
||||
is_helpdesk_staff
|
||||
helpdesk_staff_member_required,
|
||||
helpdesk_superuser_required,
|
||||
is_helpdesk_staff,
|
||||
superuser_required
|
||||
)
|
||||
from helpdesk.forms import (
|
||||
TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm,
|
||||
TicketCCEmailForm, TicketCCUserForm, EditFollowUpForm, TicketDependencyForm, MultipleTicketSelectForm
|
||||
)
|
||||
from helpdesk.decorators import superuser_required
|
||||
from helpdesk.lib import (
|
||||
safe_template_context,
|
||||
process_attachments,
|
||||
queue_template_context,
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
EditFollowUpForm,
|
||||
EditTicketForm,
|
||||
EmailIgnoreForm,
|
||||
MultipleTicketSelectForm,
|
||||
TicketCCEmailForm,
|
||||
TicketCCForm,
|
||||
TicketCCUserForm,
|
||||
TicketDependencyForm,
|
||||
TicketForm,
|
||||
UserSettingsForm
|
||||
)
|
||||
from helpdesk.lib import process_attachments, queue_template_context, safe_template_context
|
||||
from helpdesk.models import (
|
||||
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
|
||||
IgnoreEmail, TicketCC, TicketDependency, UserSettings, CustomField, TicketCustomFieldValue,
|
||||
CustomField,
|
||||
FollowUp,
|
||||
FollowUpAttachment,
|
||||
IgnoreEmail,
|
||||
PreSetReply,
|
||||
Queue,
|
||||
SavedSearch,
|
||||
Ticket,
|
||||
TicketCC,
|
||||
TicketChange,
|
||||
TicketCustomFieldValue,
|
||||
TicketDependency,
|
||||
UserSettings
|
||||
)
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.models import (KBItem)
|
||||
|
||||
from helpdesk.query import get_query_class, query_from_base64, query_to_base64
|
||||
from helpdesk.user import HelpdeskUser
|
||||
import helpdesk.views.abstract_views as abstract_views
|
||||
from helpdesk.views.permissions import MustBeStaffMixin
|
||||
from ..lib import format_time_spent
|
||||
|
||||
import json
|
||||
import re
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
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()
|
||||
Query = get_query_class()
|
||||
@ -260,7 +273,7 @@ def followup_edit(request, ticket_id, followup_id):
|
||||
'time_spent': format_time_spent(followup.time_spent),
|
||||
})
|
||||
|
||||
ticketcc_string, show_subscribe = \
|
||||
ticketcc_string, __ = \
|
||||
return_ticketccstring_and_show_subscribe(request.user, ticket)
|
||||
|
||||
return render(request, 'helpdesk/followup_edit.html', {
|
||||
@ -338,8 +351,10 @@ def view_ticket(request, ticket_id):
|
||||
if 'subscribe' in request.GET:
|
||||
# Allow the user to subscribe him/herself to the ticket whilst viewing
|
||||
# it.
|
||||
ticket_cc, show_subscribe = \
|
||||
return_ticketccstring_and_show_subscribe(request.user, ticket)
|
||||
show_subscribe = return_ticketccstring_and_show_subscribe(
|
||||
request.user, ticket
|
||||
)[1]
|
||||
|
||||
if show_subscribe:
|
||||
subscribe_staff_member_to_ticket(ticket, request.user)
|
||||
return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket.id]))
|
||||
@ -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)
|
||||
|
||||
|
||||
def update_ticket(request, ticket_id, public=False):
|
||||
def get_ticket_from_request_with_authorisation(
|
||||
request: WSGIRequest,
|
||||
ticket_id: str,
|
||||
public: bool
|
||||
) -> typing.Union[
|
||||
Ticket, typing.NoReturn
|
||||
]:
|
||||
"""Gets a ticket from the public status and if the user is authenticated and
|
||||
has permissions to update tickets
|
||||
|
||||
ticket = None
|
||||
Raises:
|
||||
Http404 when the ticket can not be found or the user lacks permission
|
||||
|
||||
"""
|
||||
if not (public or (
|
||||
request.user.is_authenticated and
|
||||
request.user.is_active and (
|
||||
@ -498,41 +523,29 @@ def update_ticket(request, ticket_id, public=False):
|
||||
'%s?next=%s' % (reverse('helpdesk:login'), request.path)
|
||||
)
|
||||
|
||||
if not ticket:
|
||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||
return get_object_or_404(Ticket, id=ticket_id)
|
||||
|
||||
date_re = re.compile(
|
||||
r'(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<year>\d{4})$'
|
||||
)
|
||||
|
||||
comment = request.POST.get('comment', '')
|
||||
new_status = int(request.POST.get('new_status', ticket.status))
|
||||
title = request.POST.get('title', '')
|
||||
public = request.POST.get('public', False)
|
||||
owner = int(request.POST.get('owner', -1))
|
||||
priority = int(request.POST.get('priority', ticket.priority))
|
||||
due_date_year = int(request.POST.get('due_date_year', 0))
|
||||
due_date_month = int(request.POST.get('due_date_month', 0))
|
||||
due_date_day = int(request.POST.get('due_date_day', 0))
|
||||
if request.POST.get("time_spent"):
|
||||
(hours, minutes) = [int(f)
|
||||
for f in request.POST.get("time_spent").split(":")]
|
||||
time_spent = timedelta(hours=hours, minutes=minutes)
|
||||
else:
|
||||
time_spent = None
|
||||
# NOTE: jQuery's default for dates is mm/dd/yy
|
||||
# very US-centric but for now that's the only format supported
|
||||
# until we clean up code to internationalize a little more
|
||||
def get_due_date_from_request_or_ticket(
|
||||
request: WSGIRequest,
|
||||
ticket: Ticket
|
||||
) -> typing.Optional[datetime.date]:
|
||||
"""Tries to locate the due date for a ticket from the `request.POST`
|
||||
'due_date' parameter or the `due_date_*` paramaters.
|
||||
"""
|
||||
due_date = request.POST.get('due_date', None) or None
|
||||
|
||||
if due_date is not None:
|
||||
# based on Django code to parse dates:
|
||||
# https://docs.djangoproject.com/en/2.0/_modules/django/utils/dateparse/
|
||||
match = date_re.match(due_date)
|
||||
match = DATE_RE.match(due_date)
|
||||
if match:
|
||||
kw = {k: int(v) for k, v in match.groupdict().items()}
|
||||
due_date = date(**kw)
|
||||
else:
|
||||
due_date_year = int(request.POST.get('due_date_year', 0))
|
||||
due_date_month = int(request.POST.get('due_date_month', 0))
|
||||
due_date_day = int(request.POST.get('due_date_day', 0))
|
||||
# old way, probably deprecated?
|
||||
if not (due_date_year and due_date_month and due_date_day):
|
||||
due_date = ticket.due_date
|
||||
@ -545,7 +558,144 @@ def update_ticket(request, ticket_id, public=False):
|
||||
due_date = timezone.now()
|
||||
due_date = due_date.replace(
|
||||
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([
|
||||
not request.FILES,
|
||||
not comment,
|
||||
@ -605,28 +755,9 @@ def update_ticket(request, ticket_id, public=False):
|
||||
f.title = _('Unassigned')
|
||||
ticket.assigned_to = None
|
||||
|
||||
old_status_str = ticket.get_status_display()
|
||||
old_status = ticket.status
|
||||
if new_status != ticket.status:
|
||||
ticket.status = new_status
|
||||
ticket.save()
|
||||
f.new_status = new_status
|
||||
if f.title:
|
||||
f.title += ' and %s' % ticket.get_status_display()
|
||||
else:
|
||||
f.title = '%s' % ticket.get_status_display()
|
||||
old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)
|
||||
|
||||
if not f.title:
|
||||
if f.comment:
|
||||
f.title = _('Comment')
|
||||
else:
|
||||
f.title = _('Updated')
|
||||
|
||||
f.save()
|
||||
|
||||
files = []
|
||||
if request.FILES:
|
||||
files = process_attachments(f, request.FILES.getlist('attachment'))
|
||||
files = process_attachments(f, request.FILES.getlist('attachment')) if request.FILES else []
|
||||
|
||||
if title and title != ticket.title:
|
||||
c = TicketChange(
|
||||
@ -676,8 +807,11 @@ def update_ticket(request, ticket_id, public=False):
|
||||
c.save()
|
||||
ticket.due_date = due_date
|
||||
|
||||
if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS):
|
||||
if new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None:
|
||||
if new_status in (
|
||||
Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS
|
||||
) and (
|
||||
new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
|
||||
):
|
||||
ticket.resolution = comment
|
||||
|
||||
# ticket might have changed above, so we re-instantiate context with the
|
||||
@ -693,34 +827,16 @@ def update_ticket(request, ticket_id, public=False):
|
||||
messages_sent_to.add(request.user.email)
|
||||
except AttributeError:
|
||||
pass
|
||||
if public and (f.comment or (
|
||||
f.new_status in (Ticket.RESOLVED_STATUS,
|
||||
Ticket.CLOSED_STATUS))):
|
||||
if f.new_status == Ticket.RESOLVED_STATUS:
|
||||
template = 'resolved_'
|
||||
elif f.new_status == Ticket.CLOSED_STATUS:
|
||||
template = 'closed_'
|
||||
else:
|
||||
template = 'updated_'
|
||||
|
||||
roles = {
|
||||
'submitter': (template + 'submitter', context),
|
||||
'ticket_cc': (template + 'cc', context),
|
||||
}
|
||||
if ticket.assigned_to and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change:
|
||||
roles['assigned_to'] = (template + 'cc', context)
|
||||
messages_sent_to.update(ticket.send(
|
||||
roles, dont_send_to=messages_sent_to, fail_silently=True, files=files,))
|
||||
|
||||
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'
|
||||
ticket = update_messages_sent_to_by_public_and_status(
|
||||
public,
|
||||
ticket,
|
||||
f,
|
||||
context,
|
||||
messages_sent_to,
|
||||
files
|
||||
)
|
||||
|
||||
template_staff, template_cc = get_template_staff_and_template_cc(reassigned, f)
|
||||
if ticket.assigned_to and (
|
||||
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
|
||||
or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign)
|
||||
@ -732,30 +848,16 @@ def update_ticket(request, ticket_id, public=False):
|
||||
files=files,
|
||||
))
|
||||
|
||||
if reassigned:
|
||||
template_cc = 'assigned_cc'
|
||||
elif f.new_status == Ticket.RESOLVED_STATUS:
|
||||
template_cc = 'resolved_cc'
|
||||
elif f.new_status == Ticket.CLOSED_STATUS:
|
||||
template_cc = 'closed_cc'
|
||||
else:
|
||||
template_cc = 'updated_cc'
|
||||
|
||||
messages_sent_to.update(ticket.send(
|
||||
{'ticket_cc': (template_cc, context)},
|
||||
dont_send_to=messages_sent_to,
|
||||
fail_silently=True,
|
||||
files=files,
|
||||
))
|
||||
|
||||
ticket.save()
|
||||
|
||||
# auto subscribe user if enabled
|
||||
if helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE and request.user.is_authenticated:
|
||||
ticketcc_string, SHOW_SUBSCRIBE = return_ticketccstring_and_show_subscribe(
|
||||
request.user, ticket)
|
||||
if SHOW_SUBSCRIBE:
|
||||
subscribe_staff_member_to_ticket(ticket, request.user)
|
||||
add_staff_subscription(request, 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
|
||||
# which value to keep when merging
|
||||
ticket_attributes = (
|
||||
TICKET_ATTRIBUTES = (
|
||||
('created', _('Created date')),
|
||||
('due_date', _('Due on')),
|
||||
('get_status_display', _('Status')),
|
||||
@ -898,30 +1000,20 @@ ticket_attributes = (
|
||||
)
|
||||
|
||||
|
||||
@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()
|
||||
default = _('Not defined')
|
||||
def merge_ticket_values(
|
||||
request: WSGIRequest,
|
||||
tickets: typing.List[Ticket],
|
||||
custom_fields
|
||||
) -> None:
|
||||
for ticket in tickets:
|
||||
ticket.values = {}
|
||||
# Prepare the value for each attributes of this ticket
|
||||
for attribute, display_name in ticket_attributes:
|
||||
value = getattr(ticket, attribute, default)
|
||||
for attribute, __ in TICKET_ATTRIBUTES:
|
||||
value = getattr(ticket, attribute, TicketCustomFieldValue.default_value)
|
||||
# Check if attr is a get_FIELD_display
|
||||
if attribute.startswith('get_') and attribute.endswith('_display'):
|
||||
# Hack to call methods like get_FIELD_display()
|
||||
value = getattr(ticket, attribute, default)()
|
||||
value = getattr(ticket, attribute, TicketCustomFieldValue.default_value)()
|
||||
ticket.values[attribute] = {
|
||||
'value': value,
|
||||
'checked': str(ticket.id) == request.POST.get(attribute)
|
||||
@ -932,26 +1024,21 @@ def merge_tickets(request):
|
||||
value = ticket.ticketcustomfieldvalue_set.get(
|
||||
field=custom_field).value
|
||||
except (TicketCustomFieldValue.DoesNotExist, ValueError):
|
||||
value = default
|
||||
value = TicketCustomFieldValue.default_value
|
||||
ticket.values[custom_field.name] = {
|
||||
'value': value,
|
||||
'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
|
||||
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:
|
||||
|
||||
def redirect_from_chosen_ticket(
|
||||
request,
|
||||
chosen_ticket,
|
||||
tickets,
|
||||
custom_fields
|
||||
) -> HttpResponseRedirect:
|
||||
# Save ticket fields values
|
||||
for attribute, display_name in ticket_attributes:
|
||||
for attribute, __ in TICKET_ATTRIBUTES:
|
||||
id_for_attribute = request.POST.get(attribute)
|
||||
if id_for_attribute != chosen_ticket.id:
|
||||
try:
|
||||
@ -1039,14 +1126,88 @@ def merge_tickets(request):
|
||||
ticketcc=ticketcc)
|
||||
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', {
|
||||
'tickets': tickets,
|
||||
'ticket_attributes': ticket_attributes,
|
||||
'ticket_attributes': TICKET_ATTRIBUTES,
|
||||
'custom_fields': custom_fields,
|
||||
'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
|
||||
def ticket_list(request):
|
||||
context = {}
|
||||
@ -1071,40 +1232,10 @@ def ticket_list(request):
|
||||
'sortreverse': False,
|
||||
}
|
||||
|
||||
# If the user is coming from the header/navigation search box, lets' first
|
||||
# look at their query to see if they have entered a valid ticket number. If
|
||||
# they have, just redirect to that ticket number. Otherwise, we treat it as
|
||||
# a keyword search.
|
||||
|
||||
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
|
||||
|
||||
#: check for a redirect, see function doc for details
|
||||
redirect = check_redirect_on_user_query(request, huser)
|
||||
if redirect:
|
||||
return redirect
|
||||
try:
|
||||
saved_query, query_params = load_saved_query(request, query_params)
|
||||
except QueryLoadError:
|
||||
@ -1300,15 +1431,15 @@ class CreateTicketView(MustBeStaffMixin, abstract_views.AbstractCreateTicketMixi
|
||||
|
||||
|
||||
@helpdesk_staff_member_required
|
||||
def raw_details(request, type):
|
||||
def raw_details(request, type_):
|
||||
# TODO: This currently only supports spewing out 'PreSetReply' objects,
|
||||
# in the future it needs to be expanded to include other items. All it
|
||||
# does is return a plain-text representation of an object.
|
||||
|
||||
if type not in ('preset',):
|
||||
if type_ not in ('preset',):
|
||||
raise Http404
|
||||
|
||||
if type == 'preset' and request.GET.get('id', False):
|
||||
if type_ == 'preset' and request.GET.get('id', False):
|
||||
try:
|
||||
preset = PreSetReply.objects.get(id=request.GET.get('id'))
|
||||
return HttpResponse(preset.body)
|
||||
@ -1408,12 +1539,18 @@ def report_index(request):
|
||||
report_index = staff_member_required(report_index)
|
||||
|
||||
|
||||
@helpdesk_staff_member_required
|
||||
def run_report(request, report):
|
||||
def get_report_queryset_or_redirect(request, report):
|
||||
if Ticket.objects.all().count() == 0 or report not in (
|
||||
'queuemonth', 'usermonth', 'queuestatus', 'queuepriority', 'userstatus',
|
||||
'userpriority', 'userqueue', 'daysuntilticketclosedbymonth'):
|
||||
return HttpResponseRedirect(reverse("helpdesk:report_index"))
|
||||
"queuemonth",
|
||||
"usermonth",
|
||||
"queuestatus",
|
||||
"queuepriority",
|
||||
"userstatus",
|
||||
"userpriority",
|
||||
"userqueue",
|
||||
"daysuntilticketclosedbymonth"
|
||||
):
|
||||
return None, None, HttpResponseRedirect(reverse("helpdesk:report_index"))
|
||||
|
||||
report_queryset = Ticket.objects.all().select_related().filter(
|
||||
queue__in=HelpdeskUser(request.user).get_queues()
|
||||
@ -1422,12 +1559,79 @@ def run_report(request, report):
|
||||
try:
|
||||
saved_query, query_params = load_saved_query(request)
|
||||
except QueryLoadError:
|
||||
return HttpResponseRedirect(reverse('helpdesk:report_index'))
|
||||
return None, HttpResponseRedirect(reverse('helpdesk:report_index'))
|
||||
return report_queryset, query_params, saved_query, None
|
||||
|
||||
|
||||
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):
|
||||
Query(report_queryset, query_to_base64(query_params))
|
||||
|
||||
from collections import defaultdict
|
||||
summarytable = defaultdict(int)
|
||||
# a second table for more complex queries
|
||||
summarytable2 = defaultdict(int)
|
||||
@ -1502,50 +1706,7 @@ def run_report(request, report):
|
||||
col1heading = _('Queue')
|
||||
possible_options = periods
|
||||
charttype = 'date'
|
||||
|
||||
metric3 = False
|
||||
for ticket in report_queryset:
|
||||
if report == 'userpriority':
|
||||
metric1 = u'%s' % ticket.get_assigned_to
|
||||
metric2 = u'%s' % ticket.get_priority_display()
|
||||
|
||||
elif report == 'userqueue':
|
||||
metric1 = u'%s' % ticket.get_assigned_to
|
||||
metric2 = u'%s' % ticket.queue.title
|
||||
|
||||
elif report == 'userstatus':
|
||||
metric1 = u'%s' % ticket.get_assigned_to
|
||||
metric2 = u'%s' % ticket.get_status_display()
|
||||
|
||||
elif report == 'usermonth':
|
||||
metric1 = u'%s' % ticket.get_assigned_to
|
||||
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
|
||||
|
||||
elif report == 'queuepriority':
|
||||
metric1 = u'%s' % ticket.queue.title
|
||||
metric2 = u'%s' % ticket.get_priority_display()
|
||||
|
||||
elif report == 'queuestatus':
|
||||
metric1 = u'%s' % ticket.queue.title
|
||||
metric2 = u'%s' % ticket.get_status_display()
|
||||
|
||||
elif report == 'queuemonth':
|
||||
metric1 = u'%s' % ticket.queue.title
|
||||
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
|
||||
|
||||
elif report == 'daysuntilticketclosedbymonth':
|
||||
metric1 = u'%s' % ticket.queue.title
|
||||
metric2 = u'%s-%s' % (ticket.created.year, ticket.created.month)
|
||||
metric3 = ticket.modified - ticket.created
|
||||
metric3 = metric3.days
|
||||
|
||||
summarytable[metric1, metric2] += 1
|
||||
if metric3:
|
||||
if report == 'daysuntilticketclosedbymonth':
|
||||
summarytable2[metric1, metric2] += metric3
|
||||
|
||||
table = []
|
||||
|
||||
update_summary_tables(report_queryset, report, summarytable, summarytable2)
|
||||
if report == 'daysuntilticketclosedbymonth':
|
||||
for key in summarytable2.keys():
|
||||
summarytable[key] = summarytable2[key] / summarytable[key]
|
||||
@ -1555,18 +1716,9 @@ def run_report(request, report):
|
||||
column_headings = [col1heading] + possible_options
|
||||
|
||||
# Prepare a dict to store totals for each possible option
|
||||
totals = {}
|
||||
table, totals = get_report_table_and_totals(header1, summarytable, possible_options)
|
||||
# Pivot the data so that 'header1' fields are always first column
|
||||
# in the row, and 'possible_options' are always the 2nd - nth columns.
|
||||
for item in header1:
|
||||
data = []
|
||||
for hdr in possible_options:
|
||||
if hdr not in totals.keys():
|
||||
totals[hdr] = summarytable[item, hdr]
|
||||
else:
|
||||
totals[hdr] += summarytable[item, hdr]
|
||||
data.append(summarytable[item, hdr])
|
||||
table.append([item] + data)
|
||||
|
||||
# Zip data and headers together in one list for Morris.js charts
|
||||
# will get a list like [(Header1, Data1), (Header2, Data2)...]
|
||||
@ -1626,8 +1778,8 @@ save_query = staff_member_required(save_query)
|
||||
|
||||
|
||||
@helpdesk_staff_member_required
|
||||
def delete_saved_query(request, id):
|
||||
query = get_object_or_404(SavedSearch, id=id, user=request.user)
|
||||
def delete_saved_query(request, pk):
|
||||
query = get_object_or_404(SavedSearch, id=pk, user=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
query.delete()
|
||||
@ -1676,8 +1828,8 @@ email_ignore_add = superuser_required(email_ignore_add)
|
||||
|
||||
|
||||
@helpdesk_superuser_required
|
||||
def email_ignore_del(request, id):
|
||||
ignore = get_object_or_404(IgnoreEmail, id=id)
|
||||
def email_ignore_del(request, pk):
|
||||
ignore = get_object_or_404(IgnoreEmail, id=pk)
|
||||
if request.method == 'POST':
|
||||
ignore.delete()
|
||||
return HttpResponseRedirect(reverse('helpdesk:email_ignore'))
|
||||
|
23
quicktest.py
Normal file → Executable file
23
quicktest.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Usage:
|
||||
$ python -m venv .venv
|
||||
@ -5,15 +6,15 @@ $ source .venv/bin/activate
|
||||
$ pip install -r requirements-testing.txt -r requirements.txt
|
||||
$ python ./quicktest.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
import argparse
|
||||
import django
|
||||
from django.conf import settings
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
class QuickDjangoTest(object):
|
||||
class QuickDjangoTest:
|
||||
"""
|
||||
A quick way to run the Django test suite without a fully-configured project.
|
||||
|
||||
@ -37,12 +38,12 @@ class QuickDjangoTest(object):
|
||||
'bootstrap4form',
|
||||
# The following commented apps are optional,
|
||||
# related to teams functionalities
|
||||
#'account',
|
||||
#'pinax.invitations',
|
||||
#'pinax.teams',
|
||||
# 'account',
|
||||
# 'pinax.invitations',
|
||||
# 'pinax.teams',
|
||||
'rest_framework',
|
||||
'helpdesk',
|
||||
#'reversion',
|
||||
# 'reversion',
|
||||
)
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
@ -77,6 +78,7 @@ class QuickDjangoTest(object):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tests = args
|
||||
self.kwargs = kwargs or {"verbosity": 1}
|
||||
self._tests()
|
||||
|
||||
def _tests(self):
|
||||
@ -111,7 +113,7 @@ class QuickDjangoTest(object):
|
||||
)
|
||||
|
||||
from django.test.runner import DiscoverRunner
|
||||
test_runner = DiscoverRunner(verbosity=1)
|
||||
test_runner = DiscoverRunner(verbosity=self.kwargs["verbosity"])
|
||||
django.setup()
|
||||
|
||||
failures = test_runner.run_tests(self.tests)
|
||||
@ -133,7 +135,8 @@ if __name__ == '__main__':
|
||||
description="Run Django tests."
|
||||
)
|
||||
parser.add_argument('tests', nargs="*", type=str)
|
||||
parser.add_argument("--verbosity", "-v", nargs="?", type=int, default=1)
|
||||
args = parser.parse_args()
|
||||
if not args.tests:
|
||||
args.tests = ['helpdesk']
|
||||
QuickDjangoTest(*args.tests)
|
||||
QuickDjangoTest(*args.tests, verbosity=args.verbosity)
|
||||
|
@ -6,3 +6,4 @@ argparse
|
||||
pbr
|
||||
mock
|
||||
freezegun
|
||||
isort
|
||||
|
@ -1,4 +1,4 @@
|
||||
Django>=2.2
|
||||
Django>=3.2
|
||||
django-bootstrap4-form
|
||||
celery
|
||||
email-reply-parser
|
||||
@ -12,3 +12,4 @@ six
|
||||
pinax_teams
|
||||
djangorestframework
|
||||
django-model-utils
|
||||
django-cleanup
|
||||
|
11
setup.py
11
setup.py
@ -1,10 +1,13 @@
|
||||
import os
|
||||
import sys
|
||||
"""django-helpdesk setup"""
|
||||
from distutils.util import convert_path
|
||||
from fnmatch import fnmatchcase
|
||||
from setuptools import setup, find_packages
|
||||
import os
|
||||
from setuptools import find_packages, setup
|
||||
import sys
|
||||
|
||||
|
||||
version = '0.5.0a1'
|
||||
|
||||
version = '0.4.1'
|
||||
|
||||
# Provided as an attribute, so you can append to these instead
|
||||
# of replicating them:
|
||||
|
Loading…
Reference in New Issue
Block a user