Ruff 0.16.0 updates
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.9
|
||||
rev: v0.16.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -5,7 +5,6 @@ Django settings for django-helpdesk demodesk project.
|
||||
|
||||
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__)))
|
||||
|
||||
@@ -241,6 +240,6 @@ FIXTURE_DIRS = [os.path.join(BASE_DIR, "fixtures")]
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
try:
|
||||
from .local_settings import * # noqa
|
||||
from .local_settings import *
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -19,7 +19,6 @@ 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,
|
||||
# which only works when in development mode (using DEBUG).
|
||||
# For a real deployment, you'd have to properly configure a media server.
|
||||
|
||||
@@ -7,9 +7,9 @@ For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demodesk.config.settings")
|
||||
|
||||
|
||||
10
quicktest.py
10
quicktest.py
@@ -8,10 +8,12 @@ $ python ./quicktest.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import django
|
||||
from django.conf import settings
|
||||
import os
|
||||
import sys
|
||||
from typing import ClassVar
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
print(f"Running tests using Django version {django.get_version()}...")
|
||||
|
||||
@@ -48,7 +50,7 @@ class QuickDjangoTest:
|
||||
"helpdesk",
|
||||
# 'reversion',
|
||||
)
|
||||
MIDDLEWARE = [
|
||||
MIDDLEWARE: ClassVar[list] = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
@@ -58,7 +60,7 @@ class QuickDjangoTest:
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
TEMPLATES = [
|
||||
TEMPLATES: ClassVar[list] = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"APP_DIRS": True,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import (
|
||||
Checklist,
|
||||
@@ -18,7 +21,6 @@ from helpdesk.models import (
|
||||
TicketChange,
|
||||
)
|
||||
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.models import KBCategory, KBItem
|
||||
|
||||
@@ -26,11 +28,11 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
@admin.register(Queue)
|
||||
class QueueAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "slug", "email_address", "locale", "time_spent")
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
prepopulated_fields: ClassVar[dict] = {"slug": ("title",)}
|
||||
|
||||
def time_spent(self, q):
|
||||
if q.dedicated_time:
|
||||
return "{} / {}".format(q.time_spent, q.dedicated_time)
|
||||
return f"{q.time_spent} / {q.dedicated_time}"
|
||||
elif q.time_spent:
|
||||
return q.time_spent
|
||||
else:
|
||||
@@ -61,7 +63,7 @@ class TicketAdmin(admin.ModelAdmin):
|
||||
username, domain = ticket.submitter_email.split("@")
|
||||
username = username[:2] + "*" * (len(username) - 2)
|
||||
domain = domain[:1] + "*" * (len(domain) - 2) + domain[-1:]
|
||||
return "%s@%s" % (username, domain)
|
||||
return f"{username}@{domain}"
|
||||
else:
|
||||
return ticket.submitter_email
|
||||
|
||||
@@ -86,7 +88,7 @@ class KBIAttachmentInline(admin.StackedInline):
|
||||
|
||||
@admin.register(FollowUp)
|
||||
class FollowUpAdmin(admin.ModelAdmin):
|
||||
inlines = [TicketChangeInline, FollowUpAttachmentInline]
|
||||
inlines: ClassVar[list] = [TicketChangeInline, FollowUpAttachmentInline]
|
||||
list_display = (
|
||||
"ticket_get_ticket_for_url",
|
||||
"title",
|
||||
@@ -108,7 +110,7 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
@admin.register(KBItem)
|
||||
class KBItemAdmin(admin.ModelAdmin):
|
||||
list_display = ("category", "title", "last_updated", "team", "order", "enabled")
|
||||
inlines = [KBIAttachmentInline]
|
||||
inlines: ClassVar[list] = [KBIAttachmentInline]
|
||||
readonly_fields = ("voted_by", "downvoted_by")
|
||||
|
||||
list_display_links = ("title",)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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 functools import wraps
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,29 @@ See LICENSE for details.
|
||||
"""
|
||||
|
||||
# import base64
|
||||
from bs4 import BeautifulSoup
|
||||
from __future__ import annotations
|
||||
|
||||
import email
|
||||
import imaplib
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import poplib
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import timedelta
|
||||
from email import policy
|
||||
from email.message import EmailMessage, MIMEPart
|
||||
from email.utils import getaddresses
|
||||
from os.path import isfile, join
|
||||
from time import ctime
|
||||
|
||||
import oauthlib.oauth2 as oauth2lib
|
||||
import requests_oauthlib
|
||||
from bs4 import BeautifulSoup
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -15,33 +36,13 @@ 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 import policy
|
||||
from email.message import EmailMessage, MIMEPart
|
||||
from email.utils import getaddresses
|
||||
from email_reply_parser import EmailReplyParser
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException
|
||||
from helpdesk.lib import process_attachments, safe_template_context
|
||||
from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket
|
||||
from helpdesk.signals import new_ticket_done, update_ticket_done
|
||||
import imaplib
|
||||
import logging
|
||||
import mimetypes
|
||||
import oauthlib.oauth2 as oauth2lib
|
||||
import os
|
||||
from os.path import isfile, join
|
||||
import poplib
|
||||
import re
|
||||
import requests_oauthlib
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
from time import ctime
|
||||
import traceback
|
||||
import typing
|
||||
from typing import List
|
||||
|
||||
|
||||
# import User model, which may be a custom model
|
||||
User = get_user_model()
|
||||
@@ -72,7 +73,7 @@ def process_email(quiet: bool = False, debug_to_stdout: bool = False):
|
||||
logger = logging.getLogger("django.helpdesk.queue." + q.slug)
|
||||
logging_types = {
|
||||
"info": logging.INFO,
|
||||
"warn": logging.WARN,
|
||||
"warn": logging.WARNING,
|
||||
"error": logging.ERROR,
|
||||
"crit": logging.CRITICAL,
|
||||
"debug": logging.DEBUG,
|
||||
@@ -109,8 +110,8 @@ def process_email(quiet: bool = False, debug_to_stdout: bool = False):
|
||||
logger.info(log_msg)
|
||||
if debug_to_stdout:
|
||||
print(log_msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Queue processing failed: {q.slug} -- {e}", exc_info=True)
|
||||
except Exception:
|
||||
logger.exception(f"Queue processing failed: {q.slug}")
|
||||
if debug_to_stdout:
|
||||
print(f"Queue processing failed: {q.slug}")
|
||||
print("-" * 60)
|
||||
@@ -120,13 +121,13 @@ def process_email(quiet: bool = False, debug_to_stdout: bool = False):
|
||||
try:
|
||||
if log_file_handler:
|
||||
log_file_handler.close()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
except Exception:
|
||||
logger.exception("Failed to close log file handler")
|
||||
try:
|
||||
if log_file_handler:
|
||||
logger.removeHandler(log_file_handler)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
except Exception:
|
||||
logger.exception("Failed to remove log file handler")
|
||||
if debug_to_stdout:
|
||||
print("Email extraction into queues completed.")
|
||||
|
||||
@@ -135,7 +136,7 @@ def pop3_sync(q, logger, server):
|
||||
server.getwelcome()
|
||||
try:
|
||||
server.stls()
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 - StartTLS support/failure varies by server; fall back to unencrypted
|
||||
logger.warning(
|
||||
"POP3 StartTLS failed or unsupported. Connection will be unencrypted."
|
||||
)
|
||||
@@ -143,7 +144,7 @@ def pop3_sync(q, logger, server):
|
||||
server.pass_(q.email_box_pass or helpdesk_settings.QUEUE_EMAIL_BOX_PASSWORD)
|
||||
|
||||
messagesInfo = server.list()[1]
|
||||
logger.info("Received %d messages from POP3 server" % len(messagesInfo))
|
||||
logger.info(f"Received {len(messagesInfo)} messages from POP3 server")
|
||||
|
||||
for msgRaw in messagesInfo:
|
||||
if type(msgRaw) is bytes:
|
||||
@@ -156,7 +157,7 @@ def pop3_sync(q, logger, server):
|
||||
# already a str
|
||||
msg = msgRaw
|
||||
msgNum = msg.split(" ")[0]
|
||||
logger.info("Processing message %s" % msgNum)
|
||||
logger.info(f"Processing message {msgNum}")
|
||||
|
||||
raw_content = server.retr(msgNum)[1]
|
||||
if type(raw_content[0]) is bytes:
|
||||
@@ -169,24 +170,20 @@ def pop3_sync(q, logger, server):
|
||||
)
|
||||
except IgnoreTicketException:
|
||||
logger.warning(
|
||||
"Message %s was ignored and will be left on POP3 server" % msgNum
|
||||
f"Message {msgNum} was ignored and will be left on POP3 server"
|
||||
)
|
||||
except DeleteIgnoredTicketException:
|
||||
logger.warning(
|
||||
"Message %s was ignored and deleted from POP3 server" % msgNum
|
||||
)
|
||||
logger.warning(f"Message {msgNum} was ignored and deleted from POP3 server")
|
||||
server.dele(msgNum)
|
||||
else:
|
||||
if ticket:
|
||||
server.dele(msgNum)
|
||||
logger.info(
|
||||
"Successfully processed message %s, deleted from POP3 server"
|
||||
% msgNum
|
||||
f"Successfully processed message {msgNum}, deleted from POP3 server"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Message %s was not successfully processed, and will be left on POP3 server"
|
||||
% msgNum
|
||||
f"Message {msgNum} was not successfully processed, and will be left on POP3 server"
|
||||
)
|
||||
|
||||
server.quit()
|
||||
@@ -196,7 +193,7 @@ def imap_sync(q, logger, server):
|
||||
try:
|
||||
try:
|
||||
server.starttls()
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 - StartTLS support/failure varies by server; fall back to unencrypted
|
||||
logger.warning(
|
||||
"IMAP4 StartTLS unsupported or failed. Connection will be unencrypted."
|
||||
)
|
||||
@@ -224,9 +221,9 @@ def imap_sync(q, logger, server):
|
||||
data = server.search(None, "NOT", "DELETED")[1]
|
||||
if data:
|
||||
msgnums = data[0].split()
|
||||
logger.info("Received %d messages from IMAP server" % len(msgnums))
|
||||
logger.info(f"Received {len(msgnums)} messages from IMAP server")
|
||||
for num in msgnums:
|
||||
logger.info("Processing message %s" % num)
|
||||
logger.info(f"Processing message {num}")
|
||||
data = server.fetch(num, "(RFC822)")[1]
|
||||
full_message = encoding.force_str(data[0][1], errors="replace")
|
||||
try:
|
||||
@@ -235,29 +232,25 @@ def imap_sync(q, logger, server):
|
||||
)
|
||||
except IgnoreTicketException:
|
||||
logger.warning(
|
||||
"Message %s was ignored and will be left on IMAP server" % num
|
||||
f"Message {num} was ignored and will be left on IMAP server"
|
||||
)
|
||||
except DeleteIgnoredTicketException:
|
||||
server.store(num, "+FLAGS", "\\Deleted")
|
||||
logger.warning(
|
||||
"Message %s was ignored and deleted from IMAP server" % num
|
||||
f"Message {num} was ignored and deleted from IMAP server"
|
||||
)
|
||||
except TypeError as te:
|
||||
except TypeError:
|
||||
# Log the error with stacktrace to help identify what went wrong
|
||||
logger.error(
|
||||
f"Unexpected error processing message: {te}", exc_info=True
|
||||
)
|
||||
logger.exception(f"Unexpected error processing message {num}")
|
||||
else:
|
||||
if ticket:
|
||||
server.store(num, "+FLAGS", "\\Deleted")
|
||||
logger.info(
|
||||
"Successfully processed message %s, deleted from IMAP server"
|
||||
% num
|
||||
f"Successfully processed message {num}, deleted from IMAP server"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Message %s was not successfully processed, and will be left on IMAP server"
|
||||
% num
|
||||
f"Message {num} was not successfully processed, and will be left on IMAP server"
|
||||
)
|
||||
except imaplib.IMAP4.error:
|
||||
logger.error(
|
||||
@@ -306,15 +299,14 @@ def imap_oauth_sync(q, logger, server):
|
||||
# Select the Inbound Mailbox folder
|
||||
server.select(q.email_box_imap_folder)
|
||||
|
||||
except imaplib.IMAP4.abort as e1:
|
||||
logger.error(f"IMAP authentication failed in OAUTH: {e1}", exc_info=True)
|
||||
except imaplib.IMAP4.abort:
|
||||
logger.exception("IMAP authentication failed in OAUTH")
|
||||
server.logout()
|
||||
sys.exit()
|
||||
|
||||
except ssl.SSLError as e2:
|
||||
logger.error(
|
||||
f"IMAP login failed due to SSL error. (This is often due to a timeout): {e2}",
|
||||
exc_info=True,
|
||||
except ssl.SSLError:
|
||||
logger.exception(
|
||||
"IMAP login failed due to SSL error. (This is often due to a timeout)"
|
||||
)
|
||||
server.logout()
|
||||
sys.exit()
|
||||
@@ -341,27 +333,23 @@ def imap_oauth_sync(q, logger, server):
|
||||
server.store(num, "+FLAGS", "\\Deleted")
|
||||
server.expunge()
|
||||
logger.warning(
|
||||
"Message %s was ignored and deleted from IMAP server" % num
|
||||
f"Message {num} was ignored and deleted from IMAP server"
|
||||
)
|
||||
|
||||
except TypeError as te:
|
||||
except TypeError:
|
||||
# Log the error with stacktrace to help identify what went wrong
|
||||
logger.error(
|
||||
f"Unexpected error processing message: {te}", exc_info=True
|
||||
)
|
||||
logger.exception(f"Unexpected error processing message {num}")
|
||||
|
||||
else:
|
||||
if ticket:
|
||||
server.store(num, "+FLAGS", "\\Deleted")
|
||||
server.expunge()
|
||||
logger.info(
|
||||
"Successfully processed message %s, deleted from IMAP server"
|
||||
% num
|
||||
f"Successfully processed message {num}, deleted from IMAP server"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Message %s was not successfully processed, and will be left on IMAP server"
|
||||
% num
|
||||
f"Message {num} was not successfully processed, and will be left on IMAP server"
|
||||
)
|
||||
|
||||
except imaplib.IMAP4.error:
|
||||
@@ -450,7 +438,7 @@ def process_queue(q, logger):
|
||||
q.email_box_host or helpdesk_settings.QUEUE_EMAIL_BOX_HOST,
|
||||
int(q.email_box_port),
|
||||
)
|
||||
logger.info("Attempting %s server login" % email_box_type.upper())
|
||||
logger.info(f"Attempting {email_box_type.upper()} server login")
|
||||
mail_defaults[email_box_type]["sync"](q, logger, server)
|
||||
|
||||
elif email_box_type == "local":
|
||||
@@ -458,11 +446,11 @@ def process_queue(q, logger):
|
||||
mail = [
|
||||
join(mail_dir, f) for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))
|
||||
]
|
||||
logger.info("Found %d messages in local mailbox directory" % len(mail))
|
||||
logger.info(f"Found {len(mail)} messages in local mailbox directory")
|
||||
|
||||
logger.info("Found %d messages in local mailbox directory" % len(mail))
|
||||
logger.info(f"Found {len(mail)} messages in local mailbox directory")
|
||||
for i, m in enumerate(mail, 1):
|
||||
logger.info("Processing message %d" % i)
|
||||
logger.info(f"Processing message {i}")
|
||||
with open(m, "r") as f:
|
||||
full_message = encoding.force_str(f.read(), errors="replace")
|
||||
try:
|
||||
@@ -529,9 +517,7 @@ def is_autoreply(message):
|
||||
False
|
||||
if not message.get("Auto-Submitted")
|
||||
else message.get("Auto-Submitted").lower() != "no",
|
||||
True
|
||||
if message.get("X-Auto-Response-Suppress") in ("DR", "AutoReply", "All")
|
||||
else False,
|
||||
message.get("X-Auto-Response-Suppress") in ("DR", "AutoReply", "All"),
|
||||
message.get("List-Id"),
|
||||
message.get("List-Unsubscribe"),
|
||||
]
|
||||
@@ -544,7 +530,7 @@ def create_ticket_cc(ticket, cc_list, logger):
|
||||
# Local import to deal with non-defined / circular reference problem
|
||||
|
||||
new_ticket_ccs = []
|
||||
from helpdesk.views.staff import subscribe_to_ticket_updates, User
|
||||
from helpdesk.views.staff import User, subscribe_to_ticket_updates
|
||||
|
||||
for __, cced_email in cc_list:
|
||||
cced_email = cced_email.strip()
|
||||
@@ -622,7 +608,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
new = False
|
||||
# Check if the ticket has been merged to another ticket
|
||||
if ticket.merged_to:
|
||||
logger.info("Ticket has been merged to %s" % ticket.merged_to.ticket)
|
||||
logger.info(f"Ticket has been merged to {ticket.merged_to.ticket}")
|
||||
# Use the ticket in which it was merged to for next operations
|
||||
ticket = ticket.merged_to
|
||||
# New issue, create a new <Ticket> instance
|
||||
@@ -637,7 +623,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
priority=payload["priority"],
|
||||
)
|
||||
ticket.save()
|
||||
logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id))
|
||||
logger.debug(f"Created new ticket {ticket.queue.slug}-{ticket.id}")
|
||||
new = True
|
||||
else:
|
||||
# Possibly an email with no body but has an attachment
|
||||
@@ -652,8 +638,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
|
||||
f = FollowUp(
|
||||
ticket=ticket,
|
||||
title=_(
|
||||
"E-Mail Received from %(sender_email)s" % {"sender_email": sender_email}
|
||||
title=_("E-Mail Received from {sender_email}").format(
|
||||
sender_email=sender_email
|
||||
),
|
||||
date=now,
|
||||
public=True,
|
||||
@@ -663,22 +649,14 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
|
||||
if ticket.status == Ticket.REOPENED_STATUS:
|
||||
f.new_status = Ticket.REOPENED_STATUS
|
||||
f.title = _(
|
||||
"Ticket Re-Opened by E-Mail Received from %(sender_email)s"
|
||||
% {"sender_email": sender_email}
|
||||
f.title = _("Ticket Re-Opened by E-Mail Received from {sender_email}").format(
|
||||
sender_email=sender_email
|
||||
)
|
||||
|
||||
f.save()
|
||||
logger.debug("Created new FollowUp for Ticket")
|
||||
|
||||
logger.info(
|
||||
"[%s-%s] %s"
|
||||
% (
|
||||
ticket.queue.slug,
|
||||
ticket.id,
|
||||
ticket.title,
|
||||
)
|
||||
)
|
||||
logger.info(f"[{ticket.queue.slug}-{ticket.id}] {ticket.title}")
|
||||
|
||||
if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
|
||||
try:
|
||||
@@ -756,7 +734,7 @@ def send_info_email(
|
||||
|
||||
def get_ticket_id_from_subject_slug(
|
||||
queue_slug: str, subject: str, logger: logging.Logger
|
||||
) -> typing.Optional[int]:
|
||||
) -> int | None:
|
||||
"""Get a ticket id from the subject string
|
||||
|
||||
Performs a match on the subject using the queue_slug as reference,
|
||||
@@ -767,7 +745,7 @@ def get_ticket_id_from_subject_slug(
|
||||
if matchobj:
|
||||
# This is a reply or forward.
|
||||
ticket_id = matchobj.group("id")
|
||||
logger.info("Matched tracking ID %s-%s" % (queue_slug, ticket_id))
|
||||
logger.info(f"Matched tracking ID {queue_slug}-{ticket_id}")
|
||||
else:
|
||||
logger.info("No tracking ID matched.")
|
||||
return ticket_id
|
||||
@@ -857,7 +835,7 @@ def parse_email_content(mime_content: str, is_extract_full_email_msg: bool) -> s
|
||||
|
||||
def extract_email_message_content(
|
||||
part: MIMEPart,
|
||||
files: List,
|
||||
files: list,
|
||||
include_chained_msgs: bool,
|
||||
) -> (str, str):
|
||||
"""
|
||||
@@ -944,7 +922,7 @@ def extract_email_message_content(
|
||||
|
||||
|
||||
def process_as_attachment(
|
||||
part: MIMEPart, counter: int, files: List, logger: logging.Logger
|
||||
part: MIMEPart, counter: int, files: list, logger: logging.Logger
|
||||
):
|
||||
name = part.get_filename()
|
||||
if name:
|
||||
@@ -959,7 +937,6 @@ def process_as_attachment(
|
||||
files.append(SimpleUploadedFile(name, payload_bytes, mimetypes.guess_type(name)[0]))
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Processed MIME as attachment: %s", name)
|
||||
return
|
||||
|
||||
|
||||
def extract_email_subject(
|
||||
@@ -974,7 +951,7 @@ def extract_email_subject(
|
||||
|
||||
def extract_attachments(
|
||||
target_part: MIMEPart,
|
||||
files: List,
|
||||
files: list,
|
||||
logger: logging.Logger,
|
||||
counter: int = 1,
|
||||
content_parts_excluded: bool = False,
|
||||
@@ -1093,9 +1070,7 @@ def extract_email_metadata(
|
||||
)
|
||||
|
||||
# First try to get ticket ID from the current queue's slug
|
||||
ticket_id: typing.Optional[int] = get_ticket_id_from_subject_slug(
|
||||
queue.slug, subject, logger
|
||||
)
|
||||
ticket_id: int | None = get_ticket_id_from_subject_slug(queue.slug, subject, logger)
|
||||
|
||||
# If no ticket ID found in the current queue, check all other queues
|
||||
original_queue = queue # Store the original queue in case we need to revert
|
||||
@@ -1127,11 +1102,9 @@ def extract_email_metadata(
|
||||
|
||||
files = []
|
||||
# first message in thread, we save full body to avoid losing forwards and things like that
|
||||
include_chained_msgs = (
|
||||
True
|
||||
if ticket_id is None
|
||||
include_chained_msgs = bool(
|
||||
ticket_id is None
|
||||
and getattr(settings, "HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL", False)
|
||||
else False
|
||||
)
|
||||
filtered_body, full_body = extract_email_message_content(
|
||||
message_obj, files, include_chained_msgs
|
||||
|
||||
@@ -3,13 +3,9 @@ class IgnoreTicketException(Exception):
|
||||
Raised when an email message is received from a sender who is marked to be ignored
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DeleteIgnoredTicketException(Exception):
|
||||
"""
|
||||
Raised when an email message is received from a sender who is marked to be ignored
|
||||
and the record is tagged to delete the email from the inbox
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -7,19 +7,23 @@ forms.py - Definitions of newforms-based forms for creating and maintaining
|
||||
tickets.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import ClassVar
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.lib import (
|
||||
convert_value,
|
||||
get_assignable_users,
|
||||
process_attachments,
|
||||
safe_template_context,
|
||||
get_assignable_users,
|
||||
)
|
||||
from helpdesk.models import (
|
||||
Checklist,
|
||||
@@ -41,10 +45,8 @@ from helpdesk.settings import (
|
||||
CUSTOMFIELD_TO_FIELD_DICT,
|
||||
HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST,
|
||||
)
|
||||
from helpdesk.validators import validate_file_extension
|
||||
from helpdesk.signals import new_ticket_done
|
||||
import logging
|
||||
|
||||
from helpdesk.validators import validate_file_extension
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.models import KBItem
|
||||
@@ -53,7 +55,7 @@ logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomFieldMixin(object):
|
||||
class CustomFieldMixin:
|
||||
"""
|
||||
Mixin that provides a method to turn CustomFields into an actual field
|
||||
"""
|
||||
@@ -105,9 +107,9 @@ class CustomFieldMixin(object):
|
||||
|
||||
except KeyError:
|
||||
# The data_type was not found anywhere
|
||||
raise NameError("Unrecognized data_type %s" % field.data_type)
|
||||
raise NameError(f"Unrecognized data_type {field.data_type}")
|
||||
|
||||
self.fields["custom_%s" % field.name] = fieldclass(**instanceargs)
|
||||
self.fields[f"custom_{field.name}"] = fieldclass(**instanceargs)
|
||||
|
||||
|
||||
class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
@@ -130,7 +132,7 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
"""
|
||||
Add any custom fields that are defined to the form
|
||||
"""
|
||||
super(EditTicketForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Since title is max 100 characters limit it in editing
|
||||
if "title" in self.fields:
|
||||
@@ -157,15 +159,15 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
if "datetime" == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(
|
||||
initial_value, CUSTOMFIELD_DATETIME_FORMAT
|
||||
)
|
||||
).replace(tzinfo=timezone.get_current_timezone())
|
||||
elif "date" == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(
|
||||
initial_value, CUSTOMFIELD_DATE_FORMAT
|
||||
)
|
||||
).replace(tzinfo=timezone.get_current_timezone())
|
||||
elif "time" == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(
|
||||
initial_value, CUSTOMFIELD_TIME_FORMAT
|
||||
)
|
||||
).replace(tzinfo=timezone.get_current_timezone())
|
||||
# If it is boolean field, transform the value to a real boolean
|
||||
# instead of a string
|
||||
elif "boolean" == current_value.field.data_type:
|
||||
@@ -200,7 +202,7 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
cfv.value = convert_value(value)
|
||||
cfv.save()
|
||||
|
||||
return super(EditTicketForm, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class EditTicketCustomFieldForm(EditTicketForm):
|
||||
@@ -212,7 +214,7 @@ class EditTicketCustomFieldForm(EditTicketForm):
|
||||
"""
|
||||
Add any custom fields that are defined to the form
|
||||
"""
|
||||
super(EditTicketCustomFieldForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST:
|
||||
fields = list(self.fields)
|
||||
@@ -231,15 +233,14 @@ class EditTicketCustomFieldForm(EditTicketForm):
|
||||
followup = kwargs.pop("followup", None)
|
||||
|
||||
for field, value in self.cleaned_data.items():
|
||||
if field.startswith("custom_"):
|
||||
if value != self.fields[field].initial:
|
||||
followup.ticketchange_set.create(
|
||||
field=field.replace("custom_", "", 1),
|
||||
old_value=self.fields[field].initial,
|
||||
new_value=value,
|
||||
)
|
||||
if field.startswith("custom_") and value != self.fields[field].initial:
|
||||
followup.ticketchange_set.create(
|
||||
field=field.replace("custom_", "", 1),
|
||||
old_value=self.fields[field].initial,
|
||||
new_value=value,
|
||||
)
|
||||
|
||||
super(EditTicketCustomFieldForm, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
@@ -259,7 +260,7 @@ class EditFollowUpForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Filter not opened tickets here."""
|
||||
super(EditFollowUpForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["ticket"].queryset = Ticket.objects.filter(
|
||||
status__in=Ticket.OPEN_STATUSES
|
||||
)
|
||||
@@ -333,19 +334,18 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
|
||||
def __init__(self, kbcategory=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
if kbcategory:
|
||||
self.fields["kbitem"] = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
required=False,
|
||||
label=_("Knowledge Base Item"),
|
||||
choices=[
|
||||
(kbi.pk, kbi.title)
|
||||
for kbi in KBItem.objects.filter(
|
||||
category=kbcategory.pk, enabled=True
|
||||
)
|
||||
],
|
||||
)
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED and kbcategory:
|
||||
self.fields["kbitem"] = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
required=False,
|
||||
label=_("Knowledge Base Item"),
|
||||
choices=[
|
||||
(kbi.pk, kbi.title)
|
||||
for kbi in KBItem.objects.filter(
|
||||
category=kbcategory.pk, enabled=True
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def _add_form_custom_fields(self, staff_only_filter=None):
|
||||
if staff_only_filter is None:
|
||||
@@ -540,10 +540,10 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
"""
|
||||
Add any (non-staff) custom fields that are defined to the form
|
||||
"""
|
||||
super(PublicTicketForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._add_form_custom_fields(False)
|
||||
|
||||
for field in self.fields.keys():
|
||||
for field in self.fields:
|
||||
if field in hidden_fields:
|
||||
self.fields[field].widget = forms.HiddenInput()
|
||||
if field in readonly_fields:
|
||||
@@ -620,13 +620,13 @@ class PublicTicketForm(AbstractTicketForm):
|
||||
class UserSettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserSettings
|
||||
exclude = ["user", "settings_pickled"]
|
||||
exclude: ClassVar[list] = ["user", "settings_pickled"]
|
||||
|
||||
|
||||
class EmailIgnoreForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = IgnoreEmail
|
||||
exclude = []
|
||||
exclude: ClassVar[list] = []
|
||||
|
||||
|
||||
class TicketCCForm(forms.ModelForm):
|
||||
@@ -637,7 +637,7 @@ class TicketCCForm(forms.ModelForm):
|
||||
exclude = ("ticket",)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["user"].queryset = get_assignable_users(
|
||||
helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC
|
||||
)
|
||||
@@ -647,7 +647,7 @@ class TicketCCUserForm(forms.ModelForm):
|
||||
"""Adds a helpdesk user as a CC on a Ticket"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCUserForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["user"].queryset = get_assignable_users(
|
||||
helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC
|
||||
)
|
||||
@@ -664,7 +664,7 @@ class TicketCCEmailForm(forms.ModelForm):
|
||||
"""Adds an email address as a CC on a Ticket"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TicketCCEmailForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
model = TicketCC
|
||||
@@ -682,7 +682,7 @@ class TicketDependencyForm(forms.ModelForm):
|
||||
fields = ("depends_on",)
|
||||
|
||||
def __init__(self, ticket, *args, **kwargs):
|
||||
super(TicketDependencyForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Only open tickets except myself, existing dependencies and parents
|
||||
self.fields["depends_on"].queryset = (
|
||||
@@ -701,7 +701,7 @@ class TicketResolvesForm(forms.ModelForm):
|
||||
fields = ("ticket",)
|
||||
|
||||
def __init__(self, ticket, *args, **kwargs):
|
||||
super(TicketResolvesForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Only open tickets except myself, existing dependencies and parents
|
||||
self.fields["ticket"].queryset = (
|
||||
@@ -748,7 +748,7 @@ class ChecklistTemplateForm(forms.ModelForm):
|
||||
|
||||
def clean_task_list(self):
|
||||
task_list = self.cleaned_data["task_list"]
|
||||
return list(map(lambda task: task.strip(), task_list))
|
||||
return [task.strip() for task in task_list]
|
||||
|
||||
|
||||
class ChecklistForm(forms.ModelForm):
|
||||
|
||||
@@ -9,13 +9,14 @@ lib.py - Common functions (eg multipart e-mail)
|
||||
import logging
|
||||
import mimetypes
|
||||
from datetime import date, datetime, time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.encoding import smart_str
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
logger = logging.getLogger("helpdesk")
|
||||
|
||||
@@ -48,7 +49,7 @@ def ticket_template_context(ticket):
|
||||
):
|
||||
attr = getattr(ticket, field, None)
|
||||
if callable(attr):
|
||||
context[field] = "%s" % attr()
|
||||
context[field] = f"{attr()}"
|
||||
else:
|
||||
context[field] = attr
|
||||
context["assigned_to"] = context["_get_assigned_to"]
|
||||
@@ -127,7 +128,7 @@ def text_is_spam(text, request):
|
||||
return False
|
||||
|
||||
ak = Akismet(
|
||||
blog_url="http://%s/" % site.domain,
|
||||
blog_url=f"http://{site.domain}/",
|
||||
key=apikey,
|
||||
)
|
||||
|
||||
@@ -193,10 +194,7 @@ def format_time_spent(time_spent):
|
||||
all graphical outputs
|
||||
"""
|
||||
if time_spent:
|
||||
time_spent = "{0:02d}h:{1:02d}m".format(
|
||||
int(time_spent.total_seconds()) // 3600,
|
||||
int(time_spent.total_seconds()) % 3600 // 60,
|
||||
)
|
||||
time_spent = f"{int(time_spent.total_seconds()) // 3600:02d}h:{int(time_spent.total_seconds()) % 3600 // 60:02d}m"
|
||||
else:
|
||||
time_spent = ""
|
||||
return time_spent
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
@@ -9,8 +8,11 @@ scripts/create_escalation_exclusion.py - Easy way to routinely add particular
|
||||
escalation should take place.
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk.models import EscalationExclusion, Queue
|
||||
|
||||
day_names = {
|
||||
@@ -71,7 +73,7 @@ class Command(BaseCommand):
|
||||
|
||||
for day_name in days:
|
||||
day = day_names[day_name]
|
||||
workdate = date.today()
|
||||
workdate = timezone.now().date()
|
||||
i = 0
|
||||
while i < occurrences:
|
||||
if day == workdate.weekday():
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
scripts/create_queue_permissions.py -
|
||||
Create automatically permissions for all Queues.
|
||||
@@ -18,6 +17,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from helpdesk.models import Queue
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
@@ -11,8 +10,8 @@ users who don't yet have them.
|
||||
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()
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
@@ -8,11 +7,13 @@ scripts/escalate_tickets.py - Easy way to escalate tickets based on their age,
|
||||
designed to be run from Cron or similar.
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from helpdesk.lib import safe_template_context
|
||||
from helpdesk.models import EscalationExclusion, Queue, Ticket
|
||||
|
||||
@@ -57,8 +58,8 @@ class Command(BaseCommand):
|
||||
self.stdout.write(f"Processing: {queues}")
|
||||
|
||||
for queue in queues:
|
||||
last = date.today() - timedelta(days=queue.escalate_days)
|
||||
today = date.today()
|
||||
last = timezone.now().date() - timedelta(days=queue.escalate_days)
|
||||
today = timezone.now().date()
|
||||
workdate = last
|
||||
|
||||
days = 0
|
||||
|
||||
@@ -12,6 +12,7 @@ scripts/get_email.py - Designed to be run from cron, this script checks the
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from helpdesk.email import process_email
|
||||
|
||||
|
||||
|
||||
@@ -7,10 +7,13 @@ models.py - Model (and hence database) definitions. This is the core of the
|
||||
helpdesk structure.
|
||||
"""
|
||||
|
||||
from .lib import format_time_spent, convert_value, daily_time_spent_calculation
|
||||
from .templated_email import send_templated_mail
|
||||
from .validators import validate_file_extension
|
||||
import datetime
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from io import StringIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
@@ -19,16 +22,17 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
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 django.utils.translation import gettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from markdown import markdown
|
||||
from markdown.extensions import Extension
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
from rest_framework import serializers
|
||||
import uuid
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
from .lib import convert_value, daily_time_spent_calculation, format_time_spent
|
||||
from .templated_email import send_templated_mail
|
||||
from .validators import validate_file_extension
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -370,7 +374,7 @@ class Queue(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.title
|
||||
return f"{self.title}"
|
||||
|
||||
class Meta:
|
||||
ordering = ("title",)
|
||||
@@ -390,13 +394,11 @@ class Queue(models.Model):
|
||||
)
|
||||
if default_email is not None:
|
||||
# already in the right format, so just include it here
|
||||
return "NO QUEUE EMAIL ADDRESS DEFINED %s" % settings.DEFAULT_FROM_EMAIL
|
||||
return f"NO QUEUE EMAIL ADDRESS DEFINED {settings.DEFAULT_FROM_EMAIL}"
|
||||
else:
|
||||
return (
|
||||
"NO QUEUE EMAIL ADDRESS DEFINED <%s>" % settings.DEFAULT_FROM_EMAIL
|
||||
)
|
||||
return f"NO QUEUE EMAIL ADDRESS DEFINED <{settings.DEFAULT_FROM_EMAIL}>"
|
||||
else:
|
||||
return "%s <%s>" % (self.title, self.email_address)
|
||||
return f"{self.title} <{self.email_address}>"
|
||||
|
||||
from_address = property(_from_address)
|
||||
|
||||
@@ -419,8 +421,8 @@ class Queue(models.Model):
|
||||
:return: The codename that can be used to create a new Permission object.
|
||||
"""
|
||||
# Prepare the permission associated to this Queue
|
||||
basename = "queue_access_%s" % self.slug
|
||||
self.permission_name = "helpdesk.%s" % basename
|
||||
basename = f"queue_access_{self.slug}"
|
||||
self.permission_name = f"helpdesk.{basename}"
|
||||
return basename
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -457,11 +459,11 @@ class Queue(models.Model):
|
||||
codename=basename,
|
||||
)
|
||||
|
||||
super(Queue, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
permission_name = self.permission_name
|
||||
super(Queue, self).delete(*args, **kwargs)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
# once the Queue is safely deleted, remove the permission (if exists)
|
||||
if permission_name:
|
||||
@@ -711,13 +713,13 @@ class Ticket(models.Model):
|
||||
"""A user-friendly ticket ID, which is a combination of ticket ID
|
||||
and queue slug. This is generally used in e-mail subjects."""
|
||||
|
||||
return "[%s]" % self.ticket_for_url
|
||||
return f"[{self.ticket_for_url}]"
|
||||
|
||||
ticket = property(_get_ticket)
|
||||
|
||||
def _get_ticket_for_url(self):
|
||||
"""A URL-friendly ticket ID, used in links."""
|
||||
return "%s-%s" % (self.queue.slug, self.id)
|
||||
return f"{self.queue.slug}-{self.id}"
|
||||
|
||||
ticket_for_url = property(_get_ticket_for_url)
|
||||
|
||||
@@ -746,7 +748,7 @@ class Ticket(models.Model):
|
||||
dep_msg = ""
|
||||
if not self.can_be_resolved:
|
||||
dep_msg = _(" - Open dependencies")
|
||||
return "%s%s%s" % (self.get_status_display(), held_msg, dep_msg)
|
||||
return f"{self.get_status_display()}{held_msg}{dep_msg}"
|
||||
|
||||
get_status = property(_get_status)
|
||||
|
||||
@@ -786,7 +788,7 @@ class Ticket(models.Model):
|
||||
protocol = "https"
|
||||
else:
|
||||
protocol = "http"
|
||||
return "%s://%s%s?ticket=%s&email=%s&key=%s" % (
|
||||
return "{}://{}{}?ticket={}&email={}&key={}".format(
|
||||
protocol,
|
||||
site.domain,
|
||||
reverse("helpdesk:public_view"),
|
||||
@@ -814,7 +816,7 @@ class Ticket(models.Model):
|
||||
protocol = "https"
|
||||
else:
|
||||
protocol = "http"
|
||||
return "%s://%s%s" % (
|
||||
return "{}://{}{}".format(
|
||||
protocol,
|
||||
site.domain,
|
||||
reverse("helpdesk:view", args=[self.id]),
|
||||
@@ -850,7 +852,7 @@ class Ticket(models.Model):
|
||||
verbose_name_plural = _("Tickets")
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self.id, self.title)
|
||||
return f"{self.id} {self.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
from django.urls import reverse
|
||||
@@ -870,7 +872,7 @@ class Ticket(models.Model):
|
||||
if len(self.title) > 200:
|
||||
self.title = self.title[:197] + "..."
|
||||
|
||||
super(Ticket, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def queue_and_id_from_query(query):
|
||||
@@ -935,7 +937,7 @@ class Ticket(models.Model):
|
||||
value = self.ticketcustomfieldvalue_set.get(field=field).value
|
||||
except TicketCustomFieldValue.DoesNotExist:
|
||||
value = None
|
||||
setattr(self, "custom_%s" % field.name, value)
|
||||
setattr(self, f"custom_{field.name}", value)
|
||||
|
||||
def save_custom_field_values(self, data):
|
||||
for field, value in data.items():
|
||||
@@ -1039,10 +1041,10 @@ class FollowUp(models.Model):
|
||||
verbose_name_plural = _("Follow-ups")
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.title
|
||||
return f"{self.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
||||
return f"{self.ticket.get_absolute_url()}#followup{self.id}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.ticket.modified = timezone.now()
|
||||
@@ -1051,7 +1053,7 @@ class FollowUp(models.Model):
|
||||
if helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO and not self.time_spent:
|
||||
self.time_spent = self.time_spent_calculation()
|
||||
|
||||
super(FollowUp, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_markdown(self):
|
||||
return get_markdown(self.comment)
|
||||
@@ -1178,7 +1180,7 @@ class TicketChange(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
out = "%s " % self.field
|
||||
out = f"{self.field} "
|
||||
if not self.new_value:
|
||||
out += gettext("removed")
|
||||
elif not self.old_value:
|
||||
@@ -1232,7 +1234,7 @@ class Attachment(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.filename
|
||||
return f"{self.filename}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.size:
|
||||
@@ -1247,7 +1249,7 @@ class Attachment(models.Model):
|
||||
or "application/octet-stream"
|
||||
)
|
||||
|
||||
return super(Attachment, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_filename(self):
|
||||
return str(self.file)
|
||||
@@ -1278,15 +1280,13 @@ class FollowUpAttachment(Attachment):
|
||||
)
|
||||
|
||||
def attachment_path(self, filename):
|
||||
path = "helpdesk/attachments/{ticket_for_url}-{secret_key}/{id_}".format(
|
||||
ticket_for_url=self.followup.ticket.ticket_for_url,
|
||||
secret_key=self.followup.ticket.secret_key,
|
||||
id_=self.followup.id,
|
||||
)
|
||||
path = f"helpdesk/attachments/{self.followup.ticket.ticket_for_url}-{self.followup.ticket.secret_key}/{self.followup.id}"
|
||||
att_path = os.path.join(settings.MEDIA_ROOT, path)
|
||||
if settings.STORAGES == "django.core.files.storage.FileSystemStorage":
|
||||
if not os.path.exists(att_path):
|
||||
os.makedirs(att_path, helpdesk_settings.HELPDESK_ATTACHMENT_DIR_PERMS)
|
||||
if (
|
||||
settings.STORAGES == "django.core.files.storage.FileSystemStorage"
|
||||
and not os.path.exists(att_path)
|
||||
):
|
||||
os.makedirs(att_path, helpdesk_settings.HELPDESK_ATTACHMENT_DIR_PERMS)
|
||||
return os.path.join(path, filename)
|
||||
|
||||
|
||||
@@ -1298,13 +1298,13 @@ class KBIAttachment(Attachment):
|
||||
)
|
||||
|
||||
def attachment_path(self, filename):
|
||||
path = "helpdesk/attachments/kb/{category}/{kbi}".format(
|
||||
category=self.kbitem.category, kbi=self.kbitem.id
|
||||
)
|
||||
path = f"helpdesk/attachments/kb/{self.kbitem.category}/{self.kbitem.id}"
|
||||
att_path = os.path.join(settings.MEDIA_ROOT, path)
|
||||
if settings.STORAGES == "django.core.files.storage.FileSystemStorage":
|
||||
if not os.path.exists(att_path):
|
||||
os.makedirs(att_path, helpdesk_settings.HELPDESK_ATTACHMENT_DIR_PERMS)
|
||||
if (
|
||||
settings.STORAGES == "django.core.files.storage.FileSystemStorage"
|
||||
and not os.path.exists(att_path)
|
||||
):
|
||||
os.makedirs(att_path, helpdesk_settings.HELPDESK_ATTACHMENT_DIR_PERMS)
|
||||
return os.path.join(path, filename)
|
||||
|
||||
|
||||
@@ -1352,7 +1352,7 @@ class PreSetReply(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.name
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class EscalationExclusion(models.Model):
|
||||
@@ -1386,7 +1386,7 @@ class EscalationExclusion(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.name
|
||||
return f"{self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Escalation exclusion")
|
||||
@@ -1450,7 +1450,7 @@ class EmailTemplate(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.template_name
|
||||
return f"{self.template_name}"
|
||||
|
||||
class Meta:
|
||||
ordering = ("template_name", "locale")
|
||||
@@ -1497,7 +1497,7 @@ class KBCategory(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.name
|
||||
return f"{self.name}"
|
||||
|
||||
class Meta:
|
||||
ordering = ("title",)
|
||||
@@ -1583,7 +1583,7 @@ class KBItem(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.last_updated:
|
||||
self.last_updated = timezone.now()
|
||||
return super(KBItem, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_team(self):
|
||||
return helpdesk_settings.HELPDESK_KBITEM_TEAM_GETTER(self)
|
||||
@@ -1598,7 +1598,7 @@ class KBItem(models.Model):
|
||||
score = property(_score)
|
||||
|
||||
def __str__(self):
|
||||
return "%s: %s" % (self.category.title, self.title)
|
||||
return f"{self.category.title}: {self.title}"
|
||||
|
||||
class Meta:
|
||||
ordering = (
|
||||
@@ -1672,9 +1672,9 @@ class SavedSearch(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
if self.shared:
|
||||
return "%s (*)" % self.title
|
||||
return f"{self.title} (*)"
|
||||
else:
|
||||
return "%s" % self.title
|
||||
return f"{self.title}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Saved search")
|
||||
@@ -1776,7 +1776,7 @@ class UserSettings(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Preferences for %s" % self.user
|
||||
return f"Preferences for {self.user}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User Setting")
|
||||
@@ -1851,12 +1851,12 @@ class IgnoreEmail(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.name
|
||||
return f"{self.name}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.date:
|
||||
self.date = timezone.now()
|
||||
return super(IgnoreEmail, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def queue_list(self):
|
||||
"""Return a list of the queues this IgnoreEmail applies to.
|
||||
@@ -1883,7 +1883,7 @@ class IgnoreEmail(models.Model):
|
||||
own_parts = self.email_address.split("@")
|
||||
email_parts = email.split("@")
|
||||
|
||||
if (
|
||||
return bool(
|
||||
self.email_address == email
|
||||
or own_parts[0] == "*"
|
||||
and own_parts[1] == email_parts[1]
|
||||
@@ -1891,10 +1891,7 @@ class IgnoreEmail(models.Model):
|
||||
and own_parts[0] == email_parts[0]
|
||||
or own_parts[0] == "*"
|
||||
and own_parts[1] == "*"
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
)
|
||||
|
||||
|
||||
class TicketCC(models.Model):
|
||||
@@ -1960,7 +1957,7 @@ class TicketCC(models.Model):
|
||||
display = property(_display)
|
||||
|
||||
def __str__(self):
|
||||
return "%s for %s" % (self.display, self.ticket.title)
|
||||
return f"{self.display} for {self.ticket.title}"
|
||||
|
||||
def clean(self):
|
||||
if self.user and not self.user.email:
|
||||
@@ -1969,7 +1966,7 @@ class TicketCC(models.Model):
|
||||
|
||||
class CustomFieldManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super(CustomFieldManager, self).get_queryset().order_by("ordering")
|
||||
return super().get_queryset().order_by("ordering")
|
||||
|
||||
|
||||
class CustomField(models.Model):
|
||||
@@ -2086,14 +2083,14 @@ class CustomField(models.Model):
|
||||
objects = CustomFieldManager()
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.name
|
||||
return f"{self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Custom field")
|
||||
verbose_name_plural = _("Custom fields")
|
||||
|
||||
def get_choices(self):
|
||||
if not self.data_type == "list":
|
||||
if self.data_type != "list":
|
||||
return None
|
||||
choices = self.choices_as_array
|
||||
if self.empty_selection_list:
|
||||
@@ -2136,7 +2133,7 @@ class CustomField(models.Model):
|
||||
try:
|
||||
return customfield_to_api_field_dict[self.data_type](**attributes)
|
||||
except KeyError:
|
||||
raise NameError("Unrecognized data_type %s" % self.data_type)
|
||||
raise NameError(f"Unrecognized data_type {self.data_type}")
|
||||
|
||||
|
||||
class TicketCustomFieldValue(models.Model):
|
||||
@@ -2155,7 +2152,7 @@ class TicketCustomFieldValue(models.Model):
|
||||
value = models.TextField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return "%s / %s" % (self.ticket, self.field)
|
||||
return f"{self.ticket} / {self.field}"
|
||||
|
||||
@property
|
||||
def default_value(self) -> str:
|
||||
@@ -2194,7 +2191,7 @@ class TicketDependency(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s / %s" % (self.ticket, self.depends_on)
|
||||
return f"{self.ticket} / {self.depends_on}"
|
||||
|
||||
|
||||
def is_a_list_without_empty_element(task_list):
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import json
|
||||
from base64 import b64decode, b64encode
|
||||
from django.db.models import Q, Max
|
||||
from django.db.models import F, Window, Subquery, OuterRef
|
||||
from .models import FollowUp
|
||||
|
||||
from django.db.models import F, Max, OuterRef, Q, Subquery, Window
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext as _
|
||||
from helpdesk.serializers import DatatablesTicketSerializer
|
||||
import json
|
||||
from model_utils import Choices
|
||||
|
||||
from helpdesk.serializers import DatatablesTicketSerializer
|
||||
|
||||
from .models import FollowUp
|
||||
|
||||
|
||||
def query_to_base64(query):
|
||||
"""
|
||||
@@ -112,31 +114,30 @@ class __Query__:
|
||||
q_args = []
|
||||
value_filters = self.params.get("filtering", {})
|
||||
null_filters = self.params.get("filtering_null", {})
|
||||
if null_filters:
|
||||
if value_filters:
|
||||
# Check if any of the value value_filters are for the same field as the
|
||||
# ISNULL filter so that an OR filter can be set up
|
||||
matched_null_keys = []
|
||||
for null_key in null_filters:
|
||||
field_path = null_key[:-8] # Chop off the "__isnull"
|
||||
matched_key = None
|
||||
for val_key in value_filters:
|
||||
if val_key.startswith(field_path):
|
||||
matched_key = val_key
|
||||
break
|
||||
if matched_key:
|
||||
# Remove the matching filters into a Q param
|
||||
matched_null_keys.append(null_key)
|
||||
# Create an OR query for the selected value(s) OR if the field is NULL
|
||||
v = {}
|
||||
v[val_key] = value_filters[val_key]
|
||||
n = {}
|
||||
n[null_key] = null_filters[null_key]
|
||||
q_args.append((Q(**v) | Q(**n)))
|
||||
del value_filters[matched_key]
|
||||
# Now remove the matched null keys
|
||||
for null_key in matched_null_keys:
|
||||
del null_filters[null_key]
|
||||
if null_filters and value_filters:
|
||||
# Check if any of the value value_filters are for the same field as the
|
||||
# ISNULL filter so that an OR filter can be set up
|
||||
matched_null_keys = []
|
||||
for null_key in null_filters:
|
||||
field_path = null_key[:-8] # Chop off the "__isnull"
|
||||
matched_key = None
|
||||
for val_key in value_filters:
|
||||
if val_key.startswith(field_path):
|
||||
matched_key = val_key
|
||||
break
|
||||
if matched_key:
|
||||
# Remove the matching filters into a Q param
|
||||
matched_null_keys.append(null_key)
|
||||
# Create an OR query for the selected value(s) OR if the field is NULL
|
||||
v = {}
|
||||
v[val_key] = value_filters[val_key]
|
||||
n = {}
|
||||
n[null_key] = null_filters[null_key]
|
||||
q_args.append(Q(**v) | Q(**n))
|
||||
del value_filters[matched_key]
|
||||
# Now remove the matched null keys
|
||||
for null_key in matched_null_keys:
|
||||
del null_filters[null_key]
|
||||
queryset = queryset.filter(
|
||||
*q_args,
|
||||
(Q(**value_filters) & Q(**null_filters)) & self.get_search_filter_args(),
|
||||
@@ -145,7 +146,7 @@ class __Query__:
|
||||
if sorting:
|
||||
sortreverse = self.params.get("sortreverse", None)
|
||||
if sortreverse:
|
||||
sorting = "-%s" % sorting
|
||||
sorting = f"-{sorting}"
|
||||
queryset = queryset.order_by(sorting)
|
||||
# https://stackoverflow.com/questions/30487056/django-queryset-contains-duplicate-entries
|
||||
return queryset.distinct()
|
||||
@@ -237,8 +238,7 @@ class __Query__:
|
||||
if followup.comment
|
||||
else _("No text")
|
||||
)
|
||||
+ '<br/> <a href="%s" class="btn" role="button">%s</a>'
|
||||
% (
|
||||
+ '<br/> <a href="{}" class="btn" role="button">{}</a>'.format(
|
||||
reverse(
|
||||
"helpdesk:view", kwargs={"ticket_id": ticket.pk}
|
||||
),
|
||||
|
||||
@@ -6,8 +6,8 @@ from rest_framework.exceptions import ValidationError
|
||||
from .forms import TicketForm
|
||||
from .lib import format_time_spent
|
||||
from .models import CustomField, FollowUp, FollowUpAttachment, Ticket
|
||||
from .user import HelpdeskUser
|
||||
from .update_ticket import update_ticket
|
||||
from .user import HelpdeskUser
|
||||
|
||||
|
||||
class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
@@ -145,7 +145,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
fields = ("first_name", "last_name", "username", "email", "password")
|
||||
|
||||
def create(self, validated_data):
|
||||
user = super(UserSerializer, self).create(validated_data)
|
||||
user = super().create(validated_data)
|
||||
user.is_active = True
|
||||
user.set_password(validated_data["password"])
|
||||
user.save()
|
||||
@@ -158,7 +158,7 @@ class BaseTicketSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Add custom fields
|
||||
for field in CustomField.objects.all():
|
||||
self.fields["custom_%s" % field.name] = field.build_api_field()
|
||||
self.fields[f"custom_{field.name}"] = field.build_api_field()
|
||||
|
||||
|
||||
class PublicTicketListingSerializer(BaseTicketSerializer):
|
||||
|
||||
@@ -3,15 +3,15 @@ Default settings for django-helpdesk.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
import sys
|
||||
|
||||
|
||||
DEFAULT_USER_SETTINGS = {
|
||||
"login_view_ticketlist": True,
|
||||
@@ -51,13 +51,13 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(
|
||||
HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(
|
||||
settings,
|
||||
"HELPDESK_PUBLIC_VIEW_PROTECTOR",
|
||||
lambda _: None, # noqa
|
||||
lambda _: None,
|
||||
)
|
||||
|
||||
HELPDESK_STAFF_VIEW_PROTECTOR = getattr(
|
||||
settings,
|
||||
"HELPDESK_STAFF_VIEW_PROTECTOR",
|
||||
lambda _: None, # noqa
|
||||
lambda _: None,
|
||||
)
|
||||
|
||||
# Enable ticket and Email attachments
|
||||
@@ -405,7 +405,10 @@ if HELPDESK_TEAMS_MODE_ENABLED:
|
||||
else:
|
||||
HELPDESK_TEAMS_MODEL = settings.AUTH_USER_MODEL
|
||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = []
|
||||
HELPDESK_KBITEM_TEAM_GETTER = lambda _: None # noqa
|
||||
|
||||
def HELPDESK_KBITEM_TEAM_GETTER(_kbitem):
|
||||
return None
|
||||
|
||||
|
||||
# show kanban board?
|
||||
HELPDESK_KANBAN_ENABLED = getattr(settings, "HELPDESK_KANBAN_ENABLED", True)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from .email import process_email
|
||||
from celery import shared_task
|
||||
|
||||
from .email import process_email
|
||||
|
||||
|
||||
@shared_task
|
||||
def helpdesk_process_email():
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import typing
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from helpdesk.lib import safe_template_context
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.lib import process_attachments
|
||||
from helpdesk.decorators import (
|
||||
is_helpdesk_staff,
|
||||
)
|
||||
from helpdesk.lib import process_attachments, safe_template_context
|
||||
from helpdesk.models import (
|
||||
FollowUp,
|
||||
Ticket,
|
||||
@@ -41,7 +38,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
|
||||
useremail = user.email.upper()
|
||||
except AttributeError:
|
||||
useremail = ""
|
||||
strings_to_check = list()
|
||||
strings_to_check = []
|
||||
strings_to_check.append(username)
|
||||
strings_to_check.append(useremail)
|
||||
|
||||
@@ -59,7 +56,7 @@ def return_ticketccstring_and_show_subscribe(user, ticket):
|
||||
|
||||
# check whether current user is a submitter or assigned to ticket
|
||||
assignedto_username = str(ticket.assigned_to).upper()
|
||||
strings_to_check = list()
|
||||
strings_to_check = []
|
||||
if ticket.submitter_email is not None:
|
||||
submitter_email = ticket.submitter_email.upper()
|
||||
strings_to_check.append(submitter_email)
|
||||
@@ -85,9 +82,8 @@ def subscribe_to_ticket_updates(
|
||||
if user_id is None and len(email) < 5:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"When you add somebody on Cc, you must provide either a User or a valid email. Email: %s"
|
||||
% email
|
||||
)
|
||||
"When you add somebody on Cc, you must provide either a User or a valid email. Email: {}"
|
||||
).format(email)
|
||||
)
|
||||
|
||||
return ticket.ticketcc_set.create(
|
||||
@@ -97,7 +93,7 @@ def subscribe_to_ticket_updates(
|
||||
|
||||
def get_and_set_ticket_status(
|
||||
new_status: int, ticket: Ticket, follow_up: FollowUp
|
||||
) -> typing.Tuple[str, int]:
|
||||
) -> tuple[str, int]:
|
||||
"""Performs comparision on previous status to new status,
|
||||
updating the title as required.
|
||||
|
||||
@@ -111,9 +107,9 @@ def get_and_set_ticket_status(
|
||||
ticket.save()
|
||||
follow_up.new_status = new_status
|
||||
if follow_up.title:
|
||||
follow_up.title += " and %s" % ticket.get_status_display()
|
||||
follow_up.title += f" and {ticket.get_status_display()}"
|
||||
else:
|
||||
follow_up.title = "%s" % ticket.get_status_display()
|
||||
follow_up.title = f"{ticket.get_status_display()}"
|
||||
|
||||
if not follow_up.title:
|
||||
if follow_up.comment:
|
||||
@@ -130,8 +126,8 @@ def process_email_notifications_for_ticket_update(
|
||||
ticket: Ticket,
|
||||
follow_up: FollowUp,
|
||||
context: dict,
|
||||
messages_sent_to: typing.Set[str],
|
||||
files: typing.List[typing.Tuple[str, str]],
|
||||
messages_sent_to: set[str],
|
||||
files: list[tuple[str, str]],
|
||||
reassigned: bool = False,
|
||||
):
|
||||
"""
|
||||
|
||||
@@ -11,6 +11,8 @@ from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import include, path, re_path
|
||||
from django.views.generic import TemplateView
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
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
|
||||
@@ -21,8 +23,6 @@ from helpdesk.views.api import (
|
||||
TicketViewSet,
|
||||
UserTicketViewSet,
|
||||
)
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.views import kb
|
||||
@@ -140,12 +140,12 @@ if helpdesk_settings.HELPDESK_UI_ENABLED:
|
||||
name="delete_checklist_template",
|
||||
),
|
||||
re_path(
|
||||
r"^datatables_ticket_list/(?P<query>{})$".format(base64_pattern),
|
||||
rf"^datatables_ticket_list/(?P<query>{base64_pattern})$",
|
||||
staff.datatables_ticket_list,
|
||||
name="datatables_ticket_list",
|
||||
),
|
||||
re_path(
|
||||
r"^timeline_ticket_list/(?P<query>{})$".format(base64_pattern),
|
||||
rf"^timeline_ticket_list/(?P<query>{base64_pattern})$",
|
||||
staff.timeline_ticket_list,
|
||||
name="timeline_ticket_list",
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -81,14 +80,12 @@ class HelpdeskUser:
|
||||
"""Check to see if the user has permission to access
|
||||
a ticket. If not then deny access."""
|
||||
user = self.user
|
||||
if self.can_access_queue(ticket.queue):
|
||||
return True
|
||||
elif self.has_full_access() or (
|
||||
ticket.assigned_to and user.id == ticket.assigned_to.id
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return bool(
|
||||
self.can_access_queue(ticket.queue)
|
||||
or self.has_full_access()
|
||||
or ticket.assigned_to
|
||||
and user.id == ticket.assigned_to.id
|
||||
)
|
||||
|
||||
def can_access_kbcategory(self, category):
|
||||
if category.public:
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# validators for file uploads, etc.
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
# TODO: can we use the builtin Django validator instead?
|
||||
# see:
|
||||
@@ -12,9 +12,10 @@ from helpdesk import settings as helpdesk_settings
|
||||
|
||||
|
||||
def validate_file_extension(value):
|
||||
from django.core.exceptions import ValidationError
|
||||
import os
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
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.
|
||||
@@ -22,8 +23,9 @@ def validate_file_extension(value):
|
||||
if not helpdesk_settings.HELPDESK_VALIDATE_ATTACHMENT_TYPES:
|
||||
return
|
||||
|
||||
if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS:
|
||||
# TODO: one more check in case it is a file with no extension; we
|
||||
# should always allow that?
|
||||
if not (ext.lower() == "" or ext.lower() == "."):
|
||||
raise ValidationError(_("Unsupported file extension: ") + ext.lower())
|
||||
# TODO: one more check in case it is a file with no extension; we
|
||||
# should always allow that?
|
||||
if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS and not (
|
||||
ext.lower() == "" or ext.lower() == "."
|
||||
):
|
||||
raise ValidationError(_("Unsupported file extension: ") + ext.lower())
|
||||
|
||||
@@ -21,7 +21,7 @@ class AbstractCreateTicketMixin:
|
||||
|
||||
query_param_fields = ["submitter_email", "title", "body", "queue", "kbitem"]
|
||||
custom_fields = [
|
||||
"custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)
|
||||
f"custom_{f.name}" for f in CustomField.objects.filter(staff_only=False)
|
||||
]
|
||||
query_param_fields += custom_fields
|
||||
for qpf in query_param_fields:
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import FollowUp, FollowUpAttachment, Ticket
|
||||
from helpdesk.serializers import (
|
||||
FollowUpAttachmentSerializer,
|
||||
FollowUpSerializer,
|
||||
PublicTicketListingSerializer,
|
||||
TicketSerializer,
|
||||
UserSerializer,
|
||||
PublicTicketListingSerializer,
|
||||
)
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
|
||||
class ConservativePagination(PageNumberPagination):
|
||||
@@ -30,7 +32,7 @@ class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
serializer_class = PublicTicketListingSerializer
|
||||
pagination_class = ConservativePagination
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes: ClassVar[list] = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
tickets = Ticket.objects.filter(
|
||||
@@ -53,7 +55,7 @@ class TicketViewSet(viewsets.ModelViewSet):
|
||||
queryset = Ticket.objects.all()
|
||||
serializer_class = TicketSerializer
|
||||
pagination_class = ConservativePagination
|
||||
permission_classes = [IsAdminUser]
|
||||
permission_classes: ClassVar[list] = [IsAdminUser]
|
||||
|
||||
def get_queryset(self):
|
||||
tickets = Ticket.objects.all()
|
||||
@@ -85,7 +87,7 @@ class FollowUpViewSet(viewsets.ModelViewSet):
|
||||
queryset = FollowUp.objects.all()
|
||||
serializer_class = FollowUpSerializer
|
||||
pagination_class = ConservativePagination
|
||||
permission_classes = [IsAdminUser]
|
||||
permission_classes: ClassVar[list] = [IsAdminUser]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
@@ -95,10 +97,10 @@ class FollowUpAttachmentViewSet(viewsets.ModelViewSet):
|
||||
queryset = FollowUpAttachment.objects.all()
|
||||
serializer_class = FollowUpAttachmentSerializer
|
||||
pagination_class = ConservativePagination
|
||||
permission_classes = [IsAdminUser]
|
||||
permission_classes: ClassVar[list] = [IsAdminUser]
|
||||
|
||||
|
||||
class CreateUserView(CreateModelMixin, GenericViewSet):
|
||||
queryset = get_user_model().objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAdminUser]
|
||||
permission_classes: ClassVar[list] = [IsAdminUser]
|
||||
|
||||
@@ -14,10 +14,10 @@ from django.db.models import Q
|
||||
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.user import HelpdeskUser
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -65,13 +65,13 @@ class OpenTicketsByUser(Feed):
|
||||
|
||||
def link(self, obj):
|
||||
if obj["queue"]:
|
||||
return "%s?assigned_to=%s&queue=%s" % (
|
||||
return "{}?assigned_to={}&queue={}".format(
|
||||
reverse("helpdesk:list"),
|
||||
obj["user"].id,
|
||||
obj["queue"].id,
|
||||
)
|
||||
else:
|
||||
return "%s?assigned_to=%s" % (
|
||||
return "{}?assigned_to={}".format(
|
||||
reverse("helpdesk:list"),
|
||||
obj["user"].id,
|
||||
)
|
||||
@@ -154,7 +154,7 @@ class OpenTicketsByQueue(Feed):
|
||||
}
|
||||
|
||||
def link(self, obj):
|
||||
return "%s?queue=%s" % (
|
||||
return "{}?queue={}".format(
|
||||
reverse("helpdesk:list"),
|
||||
obj.id,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,9 @@ views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
|
||||
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, user
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk import user
|
||||
from helpdesk.models import KBCategory, KBItem
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.shortcuts import resolve_url
|
||||
|
||||
|
||||
default_login_view = auth_views.LoginView.as_view(
|
||||
template_name="helpdesk/registration/login.html"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
|
||||
from helpdesk.decorators import is_helpdesk_staff
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ views/public.py - All public facing views, eg non-staff (no authentication
|
||||
required) views.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from importlib import import_module
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import (
|
||||
ImproperlyConfigured,
|
||||
@@ -21,17 +25,13 @@ 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 is_helpdesk_staff, protect_view
|
||||
from helpdesk.lib import text_is_spam
|
||||
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
|
||||
|
||||
from helpdesk.views import abstract_views, staff
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -123,8 +123,7 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
)
|
||||
try:
|
||||
return HttpResponseRedirect(
|
||||
"%s?ticket=%s&email=%s&key=%s"
|
||||
% (
|
||||
"{}?ticket={}&email={}&key={}".format(
|
||||
reverse("helpdesk:public_view"),
|
||||
ticket.ticket_for_url,
|
||||
quote(ticket.submitter_email),
|
||||
@@ -228,12 +227,14 @@ class ViewTicket(TemplateView):
|
||||
)
|
||||
|
||||
try:
|
||||
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
|
||||
if request.user.is_authenticated and request.user.email == email:
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||
elif (
|
||||
hasattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC")
|
||||
and settings.HELPDESK_VIEW_A_TICKET_PUBLIC
|
||||
_queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
|
||||
if (
|
||||
request.user.is_authenticated
|
||||
and request.user.email == email
|
||||
or (
|
||||
hasattr(settings, "HELPDESK_VIEW_A_TICKET_PUBLIC")
|
||||
and settings.HELPDESK_VIEW_A_TICKET_PUBLIC
|
||||
)
|
||||
):
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||
else:
|
||||
|
||||
@@ -7,11 +7,14 @@ 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 __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
@@ -20,16 +23,20 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||
from django.db.models import F, Q, Case, When
|
||||
from django.forms import HiddenInput, inlineformset_factory, TextInput
|
||||
from django.db.models import Case, F, Q, When
|
||||
from django.forms import HiddenInput, TextInput, inlineformset_factory
|
||||
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.timezone import now
|
||||
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 rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.decorators import (
|
||||
helpdesk_staff_member_required,
|
||||
@@ -38,13 +45,13 @@ from helpdesk.decorators import (
|
||||
superuser_required,
|
||||
)
|
||||
from helpdesk.forms import (
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
ChecklistForm,
|
||||
ChecklistTemplateForm,
|
||||
CreateChecklistForm,
|
||||
CUSTOMFIELD_DATE_FORMAT,
|
||||
EditFollowUpForm,
|
||||
EditTicketForm,
|
||||
EditTicketCustomFieldForm,
|
||||
EditTicketForm,
|
||||
EmailIgnoreForm,
|
||||
FormControlDeleteFormSet,
|
||||
MultipleTicketSelectForm,
|
||||
@@ -57,9 +64,9 @@ from helpdesk.forms import (
|
||||
UserSettingsForm,
|
||||
)
|
||||
from helpdesk.lib import (
|
||||
get_assignable_users,
|
||||
queue_template_context,
|
||||
safe_template_context,
|
||||
get_assignable_users,
|
||||
)
|
||||
from helpdesk.models import (
|
||||
Checklist,
|
||||
@@ -80,21 +87,17 @@ from helpdesk.models import (
|
||||
UserSettings,
|
||||
)
|
||||
from helpdesk.query import get_query_class, query_from_base64, query_to_base64
|
||||
from helpdesk.user import HelpdeskUser
|
||||
from helpdesk.update_ticket import (
|
||||
update_ticket,
|
||||
subscribe_to_ticket_updates,
|
||||
return_ticketccstring_and_show_subscribe,
|
||||
subscribe_to_ticket_updates,
|
||||
update_ticket,
|
||||
)
|
||||
import helpdesk.views.abstract_views as abstract_views
|
||||
from helpdesk.user import HelpdeskUser
|
||||
from helpdesk.views import abstract_views
|
||||
from helpdesk.views.permissions import MustBeStaffMixin
|
||||
import json
|
||||
import re
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
import typing
|
||||
from django.utils.timezone import now
|
||||
|
||||
from ..lib import format_time_spent
|
||||
from ..templated_email import send_templated_mail
|
||||
|
||||
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||
from helpdesk.models import KBItem
|
||||
@@ -487,9 +490,7 @@ def view_ticket(request, ticket_id):
|
||||
if submitter_userprofile is not None:
|
||||
content_type = ContentType.objects.get_for_model(submitter_userprofile)
|
||||
submitter_userprofile_url = reverse(
|
||||
"admin:{app}_{model}_change".format(
|
||||
app=content_type.app_label, model=content_type.model
|
||||
),
|
||||
f"admin:{content_type.app_label}_{content_type.model}_change",
|
||||
kwargs={"object_id": submitter_userprofile.id},
|
||||
)
|
||||
else:
|
||||
@@ -629,9 +630,7 @@ def get_ticket_from_request_with_authorisation(
|
||||
return get_object_or_404(Ticket, id=ticket_id)
|
||||
|
||||
|
||||
def get_due_date_from_form_or_ticket(
|
||||
form, ticket: Ticket
|
||||
) -> typing.Optional[datetime.date]:
|
||||
def get_due_date_from_form_or_ticket(form, ticket: Ticket) -> datetime.date | None:
|
||||
"""Tries to locate the due date for a ticket from the form
|
||||
'due_date' parameter or the `due_date_*` paramaters.
|
||||
"""
|
||||
@@ -654,7 +653,7 @@ def get_due_date_from_form_or_ticket(
|
||||
return due_date
|
||||
|
||||
|
||||
def get_time_spent_from_form(form: dict) -> typing.Optional[timedelta]:
|
||||
def get_time_spent_from_form(form: dict) -> timedelta | None:
|
||||
if form.data.get("time_spent"):
|
||||
(hours, minutes) = [int(f) for f in form.data.get("time_spent").split(":")]
|
||||
return timedelta(hours=hours, minutes=minutes)
|
||||
@@ -771,7 +770,7 @@ def mass_update(request):
|
||||
return redirect(
|
||||
reverse("helpdesk:merge_tickets")
|
||||
+ "?"
|
||||
+ "&".join(["tickets=%s" % ticket_id for ticket_id in tickets])
|
||||
+ "&".join([f"tickets={ticket_id}" for ticket_id in tickets])
|
||||
)
|
||||
|
||||
huser = HelpdeskUser(request.user)
|
||||
@@ -784,9 +783,8 @@ def mass_update(request):
|
||||
t.save()
|
||||
t.followup_set.create(
|
||||
date=timezone.now(),
|
||||
title=_(
|
||||
"Assigned to %(username)s in bulk update"
|
||||
% {"username": user.get_username()}
|
||||
title=_("Assigned to {username} in bulk update").format(
|
||||
username=user.get_username()
|
||||
),
|
||||
public=True,
|
||||
user=request.user,
|
||||
@@ -882,7 +880,7 @@ TICKET_ATTRIBUTES = (
|
||||
|
||||
|
||||
def merge_ticket_values(
|
||||
request: WSGIRequest, tickets: typing.List[Ticket], custom_fields
|
||||
request: WSGIRequest, tickets: list[Ticket], custom_fields
|
||||
) -> None:
|
||||
for ticket in tickets:
|
||||
ticket.values = {}
|
||||
@@ -1146,15 +1144,13 @@ def ticket_list(request):
|
||||
("priority", "priority__in"),
|
||||
("kbitem", "kbitem__in"),
|
||||
]
|
||||
filter_null_params = dict(
|
||||
[
|
||||
("queue", "queue__id__isnull"),
|
||||
("assigned_to", "assigned_to__id__isnull"),
|
||||
("status", "status__isnull"),
|
||||
("priority", "priority__isnull"),
|
||||
("kbitem", "kbitem__isnull"),
|
||||
]
|
||||
)
|
||||
filter_null_params = {
|
||||
"queue": "queue__id__isnull",
|
||||
"assigned_to": "assigned_to__id__isnull",
|
||||
"status": "status__isnull",
|
||||
"priority": "priority__isnull",
|
||||
"kbitem": "kbitem__isnull",
|
||||
}
|
||||
for param, filter_command in filter_in_params:
|
||||
if request.GET.get(param) is not None:
|
||||
patterns = request.GET.getlist(param)
|
||||
@@ -1569,7 +1565,7 @@ def get_report_table_and_totals(header1, summarytable, possible_options):
|
||||
for item in header1:
|
||||
data = []
|
||||
for hdr in possible_options:
|
||||
if hdr not in totals.keys():
|
||||
if hdr not in totals:
|
||||
totals[hdr] = summarytable[item, hdr]
|
||||
else:
|
||||
totals[hdr] += summarytable[item, hdr]
|
||||
@@ -1582,36 +1578,36 @@ def update_summary_tables(report_queryset, report, summarytable, summarytable2):
|
||||
metric3 = False
|
||||
for ticket in report_queryset:
|
||||
if report == "userpriority":
|
||||
metric1 = "%s" % ticket.get_assigned_to
|
||||
metric2 = "%s" % ticket.get_priority_display()
|
||||
metric1 = f"{ticket.get_assigned_to}"
|
||||
metric2 = f"{ticket.get_priority_display()}"
|
||||
|
||||
elif report == "userqueue":
|
||||
metric1 = "%s" % ticket.get_assigned_to
|
||||
metric2 = "%s" % ticket.queue.title
|
||||
metric1 = f"{ticket.get_assigned_to}"
|
||||
metric2 = f"{ticket.queue.title}"
|
||||
|
||||
elif report == "userstatus":
|
||||
metric1 = "%s" % ticket.get_assigned_to
|
||||
metric2 = "%s" % ticket.get_status_display()
|
||||
metric1 = f"{ticket.get_assigned_to}"
|
||||
metric2 = f"{ticket.get_status_display()}"
|
||||
|
||||
elif report == "usermonth":
|
||||
metric1 = "%s" % ticket.get_assigned_to
|
||||
metric2 = "%s-%s" % (ticket.created.year, ticket.created.month)
|
||||
metric1 = f"{ticket.get_assigned_to}"
|
||||
metric2 = f"{ticket.created.year}-{ticket.created.month}"
|
||||
|
||||
elif report == "queuepriority":
|
||||
metric1 = "%s" % ticket.queue.title
|
||||
metric2 = "%s" % ticket.get_priority_display()
|
||||
metric1 = f"{ticket.queue.title}"
|
||||
metric2 = f"{ticket.get_priority_display()}"
|
||||
|
||||
elif report == "queuestatus":
|
||||
metric1 = "%s" % ticket.queue.title
|
||||
metric2 = "%s" % ticket.get_status_display()
|
||||
metric1 = f"{ticket.queue.title}"
|
||||
metric2 = f"{ticket.get_status_display()}"
|
||||
|
||||
elif report == "queuemonth":
|
||||
metric1 = "%s" % ticket.queue.title
|
||||
metric2 = "%s-%s" % (ticket.created.year, ticket.created.month)
|
||||
metric1 = f"{ticket.queue.title}"
|
||||
metric2 = f"{ticket.created.year}-{ticket.created.month}"
|
||||
|
||||
elif report == "daysuntilticketclosedbymonth":
|
||||
metric1 = "%s" % ticket.queue.title
|
||||
metric2 = "%s-%s" % (ticket.created.year, ticket.created.month)
|
||||
metric1 = f"{ticket.queue.title}"
|
||||
metric2 = f"{ticket.created.year}-{ticket.created.month}"
|
||||
metric3 = ticket.modified - ticket.created
|
||||
metric3 = metric3.days
|
||||
|
||||
@@ -1619,9 +1615,8 @@ def update_summary_tables(report_queryset, report, summarytable, summarytable2):
|
||||
raise ValueError(f'report "{report}" is unrecognized.')
|
||||
|
||||
summarytable[metric1, metric2] += 1
|
||||
if metric3:
|
||||
if report == "daysuntilticketclosedbymonth":
|
||||
summarytable2[metric1, metric2] += metric3
|
||||
if metric3 and report == "daysuntilticketclosedbymonth":
|
||||
summarytable2[metric1, metric2] += metric3
|
||||
|
||||
|
||||
@helpdesk_staff_member_required
|
||||
@@ -1649,7 +1644,7 @@ def run_report(request, report):
|
||||
periods = []
|
||||
year, month = first_year, first_month
|
||||
working = True
|
||||
periods.append("%s-%s" % (year, month))
|
||||
periods.append(f"{year}-{month}")
|
||||
|
||||
while working:
|
||||
month += 1
|
||||
@@ -1658,7 +1653,7 @@ def run_report(request, report):
|
||||
month = 1
|
||||
if (year > last_year) or (month > last_month and year >= last_year):
|
||||
working = False
|
||||
periods.append("%s-%s" % (year, month))
|
||||
periods.append(f"{year}-{month}")
|
||||
|
||||
if report == "userpriority":
|
||||
title = _("User by Priority")
|
||||
@@ -1710,10 +1705,10 @@ def run_report(request, report):
|
||||
charttype = "date"
|
||||
update_summary_tables(report_queryset, report, summarytable, summarytable2)
|
||||
if report == "daysuntilticketclosedbymonth":
|
||||
for key in summarytable2.keys():
|
||||
for key in summarytable2:
|
||||
summarytable[key] = round(summarytable2[key] / summarytable[key], 2)
|
||||
|
||||
header1 = sorted(set(list(i for i, _ in summarytable.keys())))
|
||||
header1 = sorted({i for i, _ in summarytable})
|
||||
|
||||
column_headings = [col1heading] + possible_options
|
||||
|
||||
@@ -1724,12 +1719,10 @@ def run_report(request, report):
|
||||
|
||||
# Zip data and headers together in one list for Morris.js charts
|
||||
# will get a list like [(Header1, Data1), (Header2, Data2)...]
|
||||
seriesnum = 0
|
||||
morrisjs_data = []
|
||||
for label in column_headings[1:]:
|
||||
seriesnum += 1
|
||||
for seriesnum, label in enumerate(column_headings[1:], start=1):
|
||||
datadict = {"x": label}
|
||||
for n in range(0, len(table)):
|
||||
for n in range(len(table)):
|
||||
datadict[n] = table[n][seriesnum]
|
||||
morrisjs_data.append(datadict)
|
||||
|
||||
@@ -1799,7 +1792,7 @@ def save_query(request):
|
||||
query.save()
|
||||
|
||||
return HttpResponseRedirect(
|
||||
"%s?saved_query=%s" % (reverse("helpdesk:list"), query.id)
|
||||
"{}?saved_query={}".format(reverse("helpdesk:list"), query.id)
|
||||
)
|
||||
|
||||
|
||||
@@ -2067,7 +2060,7 @@ def attachment_del(request, ticket_id, attachment_id):
|
||||
def calc_average_nbr_days_until_ticket_resolved(Tickets):
|
||||
nbr_closed_tickets = len(Tickets)
|
||||
days_per_ticket = 0
|
||||
days_each_ticket = list()
|
||||
days_each_ticket = []
|
||||
|
||||
for ticket in Tickets:
|
||||
time_ticket_open = ticket.modified - ticket.created
|
||||
@@ -2086,7 +2079,7 @@ def calc_average_nbr_days_until_ticket_resolved(Tickets):
|
||||
def calc_basic_ticket_stats(Tickets):
|
||||
# all not closed tickets (open, reopened, resolved,) - independent of user
|
||||
all_open_tickets = Tickets.exclude(status=Ticket.CLOSED_STATUS)
|
||||
today = datetime.today()
|
||||
today = timezone.now()
|
||||
|
||||
date_30 = date_rel_to_today(today, 30)
|
||||
date_60 = date_rel_to_today(today, 60)
|
||||
@@ -2108,7 +2101,7 @@ def calc_basic_ticket_stats(Tickets):
|
||||
N_ota_ge_60 = len(ota_ge_60)
|
||||
|
||||
# (O)pen (T)icket (S)tats
|
||||
ots = list()
|
||||
ots = []
|
||||
# label, number entries, color, sort_string
|
||||
ots.append(
|
||||
[
|
||||
@@ -2176,13 +2169,7 @@ def date_rel_to_today(today, offset):
|
||||
|
||||
|
||||
def sort_string(begin, end):
|
||||
return "sort=created&date_from=%s&date_to=%s&status=%s&status=%s&status=%s" % (
|
||||
begin,
|
||||
end,
|
||||
Ticket.OPEN_STATUS,
|
||||
Ticket.REOPENED_STATUS,
|
||||
Ticket.RESOLVED_STATUS,
|
||||
)
|
||||
return f"sort=created&date_from={begin}&date_to={end}&status={Ticket.OPEN_STATUS}&status={Ticket.REOPENED_STATUS}&status={Ticket.RESOLVED_STATUS}"
|
||||
|
||||
|
||||
@helpdesk_staff_member_required
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
import logging
|
||||
from django.dispatch import receiver
|
||||
|
||||
from . import settings
|
||||
|
||||
@@ -1 +1 @@
|
||||
from .settings import * # noqa
|
||||
from .settings import *
|
||||
|
||||
@@ -9,8 +9,8 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -23,7 +23,9 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
try:
|
||||
SECRET_KEY = os.environ["DJANGO_HELPDESK_SECRET_KEY"]
|
||||
except KeyError:
|
||||
raise Exception("DJANGO_HELPDESK_SECRET_KEY environment variable is not set")
|
||||
raise ImproperlyConfigured(
|
||||
"DJANGO_HELPDESK_SECRET_KEY environment variable is not set"
|
||||
) from None
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from .local_urls import local_urlpatterns
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
from .local_urls import local_urlpatterns
|
||||
|
||||
urlpatterns = (
|
||||
[
|
||||
|
||||
@@ -7,9 +7,9 @@ For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "standalone.config.local_settings")
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from helpdesk.models import Queue, Ticket
|
||||
import sys
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -15,7 +14,7 @@ def get_user(
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.create_user(
|
||||
username=username, password=password, email="%s@example.com" % username
|
||||
username=username, password=password, email=f"{username}@example.com"
|
||||
)
|
||||
user.is_staff = is_staff
|
||||
user.is_superuser = is_superuser
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import base64
|
||||
import datetime
|
||||
from _datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.utils import timezone
|
||||
from freezegun import freeze_time
|
||||
from helpdesk.models import CustomField, Queue, Ticket
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework.exceptions import ErrorDetail
|
||||
from rest_framework.status import (
|
||||
@@ -14,11 +15,13 @@ from rest_framework.status import (
|
||||
HTTP_403_FORBIDDEN,
|
||||
)
|
||||
from rest_framework.test import APITestCase
|
||||
from _datetime import timedelta
|
||||
from helpdesk.lib import convert_value
|
||||
from django.utils import timezone
|
||||
|
||||
frozen_date_time_str = (datetime.datetime.now() - timedelta(days=100)).isoformat()
|
||||
from helpdesk.lib import convert_value
|
||||
from helpdesk.models import CustomField, Queue, Ticket
|
||||
|
||||
frozen_date_time_str = (
|
||||
(timezone.now() - timedelta(days=100)).replace(tzinfo=None).isoformat()
|
||||
)
|
||||
|
||||
|
||||
class TicketTest(APITestCase):
|
||||
@@ -163,7 +166,7 @@ class TicketTest(APITestCase):
|
||||
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.put(
|
||||
"/api/tickets/%d/" % test_ticket.id,
|
||||
f"/api/tickets/{test_ticket.id}/",
|
||||
{
|
||||
"queue": self.queue.id,
|
||||
"title": "Title",
|
||||
@@ -198,7 +201,7 @@ class TicketTest(APITestCase):
|
||||
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.patch(
|
||||
"/api/tickets/%d/" % test_ticket.id,
|
||||
f"/api/tickets/{test_ticket.id}/",
|
||||
{
|
||||
"description": "New description",
|
||||
},
|
||||
@@ -212,7 +215,7 @@ class TicketTest(APITestCase):
|
||||
staff_user = User.objects.create_user(username="admin", is_staff=True)
|
||||
test_ticket = Ticket.objects.create(queue=self.queue, title="Test ticket")
|
||||
self.client.force_authenticate(staff_user)
|
||||
response = self.client.delete("/api/tickets/%d/" % test_ticket.id)
|
||||
response = self.client.delete(f"/api/tickets/{test_ticket.id}/")
|
||||
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
|
||||
self.assertFalse(Ticket.objects.exists())
|
||||
|
||||
@@ -220,7 +223,7 @@ class TicketTest(APITestCase):
|
||||
def test_create_api_ticket_with_custom_fields(self):
|
||||
custom_date = "2022-04-11"
|
||||
custom_time = "23:59:59"
|
||||
custom_datetime = convert_value(datetime.datetime.now() - timedelta(days=30))
|
||||
custom_datetime = convert_value(timezone.now() - timedelta(days=30))
|
||||
|
||||
# Create custom fields
|
||||
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# vim: set fileencoding=utf-8 :
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
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 typing import ClassVar
|
||||
from unittest import mock
|
||||
from unittest.case import skip
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
from helpdesk import lib, models
|
||||
|
||||
MEDIA_DIR = os.path.join(gettempdir(), "helpdesk_test_media")
|
||||
|
||||
|
||||
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
||||
class AttachmentIntegrationTests(TestCase):
|
||||
fixtures = ["emailtemplate.json"]
|
||||
fixtures: ClassVar[list] = ["emailtemplate.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.queue_public = models.Queue.objects.create(
|
||||
@@ -69,7 +69,7 @@ class AttachmentIntegrationTests(TestCase):
|
||||
self.assertEqual(disk_content, "attached file content")
|
||||
|
||||
def test_create_pub_ticket_with_attachment_utf8(self):
|
||||
test_file = SimpleUploadedFile("ß°äöü.txt", "โจ".encode("utf-8"), "text/utf-8")
|
||||
test_file = SimpleUploadedFile("ß°äöü.txt", "โจ".encode(), "text/utf-8")
|
||||
post_data = self.ticket_data.copy()
|
||||
post_data.update(
|
||||
{
|
||||
@@ -184,7 +184,7 @@ class AttachmentUnitTests(TestCase):
|
||||
def setUp(self):
|
||||
self.file_attrs = {
|
||||
"filename": "°ßäöü.txt",
|
||||
"content": "โจ".encode("utf-8"),
|
||||
"content": "โจ".encode(),
|
||||
"content-type": "text/utf8",
|
||||
}
|
||||
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
|
||||
@@ -197,7 +197,9 @@ class AttachmentUnitTests(TestCase):
|
||||
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||
):
|
||||
"""check utf-8 data is parsed correctly"""
|
||||
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0]
|
||||
filename, _fileobj = lib.process_attachments(self.follow_up, [self.test_file])[
|
||||
0
|
||||
]
|
||||
mock_att_save.assert_called_with(
|
||||
file=self.test_file,
|
||||
filename=self.file_attrs["filename"],
|
||||
@@ -243,7 +245,9 @@ class AttachmentUnitTests(TestCase):
|
||||
self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save
|
||||
):
|
||||
"""don't mock saving to filesystem to test file renames caused by storage layer"""
|
||||
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0]
|
||||
_filename, _fileobj = lib.process_attachments(self.follow_up, [self.test_file])[
|
||||
0
|
||||
]
|
||||
# Attachment object was zeroth positional arg (i.e. self) of att.save
|
||||
# call
|
||||
attachment_obj = mock_att_save.return_value
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from _datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import Checklist, ChecklistTask, ChecklistTemplate, Queue, Ticket
|
||||
from _datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk.models import Checklist, ChecklistTask, ChecklistTemplate, Queue, Ticket
|
||||
|
||||
|
||||
class TicketChecklistTestCase(TestCase):
|
||||
@classmethod
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import mail
|
||||
from django.test import TestCase
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import Queue, Ticket
|
||||
from helpdesk.forms import TicketForm
|
||||
from helpdesk.views.staff import get_user_queues
|
||||
from helpdesk.models import Queue, Ticket
|
||||
from helpdesk.update_ticket import update_ticket
|
||||
from helpdesk.views.staff import get_user_queues
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -140,9 +142,7 @@ class TicketEmailNotificationTests(TestCase):
|
||||
Change various data points checking where submitter should get emailed.
|
||||
"""
|
||||
# Ensure the attribute is correct since it is not reset on each test
|
||||
setattr(
|
||||
helpdesk_settings, "HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES", False
|
||||
)
|
||||
helpdesk_settings.HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES = False
|
||||
update_ticket(
|
||||
self.update_user,
|
||||
self.existing_ticket,
|
||||
@@ -231,9 +231,7 @@ class TicketEmailNotificationTests(TestCase):
|
||||
Check submitter should always get emailed.
|
||||
"""
|
||||
# Ensure the attribute is correct since it is not reset on each test
|
||||
setattr(
|
||||
helpdesk_settings, "HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES", True
|
||||
)
|
||||
helpdesk_settings.HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES = True
|
||||
update_ticket(
|
||||
self.update_user,
|
||||
self.existing_ticket,
|
||||
@@ -331,9 +329,7 @@ class TicketEmailNotificationTests(TestCase):
|
||||
NOTE: Oddly the CC user for updates is also sent an email on create
|
||||
"""
|
||||
# Ensure the attribute is correct since it is not reset on each test
|
||||
setattr(
|
||||
helpdesk_settings, "HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES", False
|
||||
)
|
||||
helpdesk_settings.HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES = False
|
||||
queue_id = self.queue_public_2.id
|
||||
queue = Queue.objects.get(id=queue_id)
|
||||
queue.enable_notifications_on_email_events = True
|
||||
@@ -417,10 +413,8 @@ class TicketEmailNotificationTests(TestCase):
|
||||
original_notify_all = getattr(
|
||||
helpdesk_settings, "HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES", False
|
||||
)
|
||||
setattr(helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", False)
|
||||
setattr(
|
||||
helpdesk_settings, "HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES", False
|
||||
)
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = False
|
||||
helpdesk_settings.HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES = False
|
||||
|
||||
try:
|
||||
self.outbox.clear() # Clear any existing emails
|
||||
@@ -441,15 +435,11 @@ class TicketEmailNotificationTests(TestCase):
|
||||
)
|
||||
finally:
|
||||
# Restore original settings
|
||||
setattr(
|
||||
helpdesk_settings,
|
||||
"HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS",
|
||||
original_setting,
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = (
|
||||
original_setting
|
||||
)
|
||||
setattr(
|
||||
helpdesk_settings,
|
||||
"HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES",
|
||||
original_notify_all,
|
||||
helpdesk_settings.HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES = (
|
||||
original_notify_all
|
||||
)
|
||||
|
||||
def test_private_followup_no_emails_setting_enabled(self):
|
||||
@@ -458,7 +448,7 @@ class TicketEmailNotificationTests(TestCase):
|
||||
original_setting = getattr(
|
||||
helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", False
|
||||
)
|
||||
setattr(helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", True)
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = True
|
||||
|
||||
try:
|
||||
self.outbox.clear() # Clear any existing emails
|
||||
@@ -477,10 +467,8 @@ class TicketEmailNotificationTests(TestCase):
|
||||
)
|
||||
finally:
|
||||
# Restore original setting
|
||||
setattr(
|
||||
helpdesk_settings,
|
||||
"HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS",
|
||||
original_setting,
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = (
|
||||
original_setting
|
||||
)
|
||||
|
||||
def test_private_followup_setting_does_not_affect_public_followups(self):
|
||||
@@ -489,7 +477,7 @@ class TicketEmailNotificationTests(TestCase):
|
||||
original_setting = getattr(
|
||||
helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", False
|
||||
)
|
||||
setattr(helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", True)
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = True
|
||||
|
||||
try:
|
||||
self.outbox.clear() # Clear any existing emails
|
||||
@@ -516,10 +504,8 @@ class TicketEmailNotificationTests(TestCase):
|
||||
)
|
||||
finally:
|
||||
# Restore original setting
|
||||
setattr(
|
||||
helpdesk_settings,
|
||||
"HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS",
|
||||
original_setting,
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = (
|
||||
original_setting
|
||||
)
|
||||
|
||||
def test_private_followup_setting_blocks_assigned_user_emails(self):
|
||||
@@ -528,7 +514,7 @@ class TicketEmailNotificationTests(TestCase):
|
||||
original_setting = getattr(
|
||||
helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", False
|
||||
)
|
||||
setattr(helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", True)
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = True
|
||||
|
||||
try:
|
||||
# Assign ticket to a user first
|
||||
@@ -556,10 +542,8 @@ class TicketEmailNotificationTests(TestCase):
|
||||
)
|
||||
finally:
|
||||
# Restore original setting
|
||||
setattr(
|
||||
helpdesk_settings,
|
||||
"HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS",
|
||||
original_setting,
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = (
|
||||
original_setting
|
||||
)
|
||||
|
||||
def test_private_followup_setting_blocks_notify_all_submitter_emails(self):
|
||||
@@ -571,10 +555,8 @@ class TicketEmailNotificationTests(TestCase):
|
||||
original_notify_all = getattr(
|
||||
helpdesk_settings, "HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES", False
|
||||
)
|
||||
setattr(helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", True)
|
||||
setattr(
|
||||
helpdesk_settings, "HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES", True
|
||||
)
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = True
|
||||
helpdesk_settings.HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES = True
|
||||
|
||||
try:
|
||||
self.outbox.clear() # Clear any existing emails
|
||||
@@ -595,15 +577,11 @@ class TicketEmailNotificationTests(TestCase):
|
||||
)
|
||||
finally:
|
||||
# Restore original settings
|
||||
setattr(
|
||||
helpdesk_settings,
|
||||
"HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS",
|
||||
original_setting,
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = (
|
||||
original_setting
|
||||
)
|
||||
setattr(
|
||||
helpdesk_settings,
|
||||
"HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES",
|
||||
original_notify_all,
|
||||
helpdesk_settings.HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES = (
|
||||
original_notify_all
|
||||
)
|
||||
|
||||
def test_private_followup_setting_blocks_cc_user_emails(self):
|
||||
@@ -614,7 +592,7 @@ class TicketEmailNotificationTests(TestCase):
|
||||
original_setting = getattr(
|
||||
helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", False
|
||||
)
|
||||
setattr(helpdesk_settings, "HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS", True)
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = True
|
||||
|
||||
try:
|
||||
# Add a CC user to the ticket
|
||||
@@ -644,8 +622,6 @@ class TicketEmailNotificationTests(TestCase):
|
||||
)
|
||||
finally:
|
||||
# Restore original setting
|
||||
setattr(
|
||||
helpdesk_settings,
|
||||
"HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS",
|
||||
original_setting,
|
||||
helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS = (
|
||||
original_setting
|
||||
)
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from email.mime.message import MIMEMessage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import User
|
||||
@@ -6,10 +16,10 @@ from django.core import mail
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.management import call_command
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.test import override_settings, TestCase
|
||||
from email.mime.message import MIMEMessage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from django.test import TestCase, override_settings
|
||||
from mock.mock import patch
|
||||
from oauthlib.oauth2 import BackendApplicationClient
|
||||
|
||||
import helpdesk.email
|
||||
from helpdesk.email import extract_email_metadata, process_as_attachment
|
||||
from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException
|
||||
@@ -22,19 +32,8 @@ from helpdesk.models import (
|
||||
Ticket,
|
||||
TicketCC,
|
||||
)
|
||||
from . import utils
|
||||
import itertools
|
||||
import logging
|
||||
from mock.mock import patch
|
||||
from oauthlib.oauth2 import BackendApplicationClient
|
||||
import os
|
||||
from shutil import rmtree
|
||||
import sys
|
||||
from tempfile import mkdtemp
|
||||
import time
|
||||
import typing
|
||||
from unittest import mock
|
||||
|
||||
from . import utils
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# class A addresses can't have first octet of 0
|
||||
@@ -157,7 +156,7 @@ class GetEmailCommonTests(TestCase):
|
||||
self.assertIn(
|
||||
"prosazuje lepší",
|
||||
ticket.description,
|
||||
'Missing text "prosazuje lepší" in description: %s' % ticket.description,
|
||||
f'Missing text "prosazuje lepší" in description: {ticket.description}',
|
||||
)
|
||||
followups = FollowUp.objects.filter(ticket=ticket)
|
||||
followup = followups[0]
|
||||
@@ -239,7 +238,7 @@ class GetEmailCommonTests(TestCase):
|
||||
"""
|
||||
filename = "TeléfonoMañana.txt"
|
||||
part = utils.generate_file_mime_part(locale="es_ES", filename=filename)
|
||||
files: typing.List[SimpleUploadedFile] = []
|
||||
files: list[SimpleUploadedFile] = []
|
||||
process_as_attachment(part, counter=1, files=files, logger=self.logger)
|
||||
sent_file: SimpleUploadedFile = files[0]
|
||||
# The extractor prepends a part identifier so compare the ending
|
||||
@@ -341,16 +340,14 @@ class GetEmailCommonTests(TestCase):
|
||||
continue
|
||||
self.assertTrue(
|
||||
att_retrieved.filename.endswith(att_filename),
|
||||
"Filename of attached multipart not detected: %s"
|
||||
% (att_retrieved.filename),
|
||||
f"Filename of attached multipart not detected: {att_retrieved.filename}",
|
||||
)
|
||||
with att_retrieved.file.open("r") as f:
|
||||
retrieved_content = f.read()
|
||||
self.assertEqual(
|
||||
att_content,
|
||||
retrieved_content,
|
||||
"Retrieved attachment content different to original :\n\n%s\n\n%s"
|
||||
% (att_content, retrieved_content),
|
||||
f"Retrieved attachment content different to original :\n\n{att_content}\n\n{retrieved_content}",
|
||||
)
|
||||
|
||||
def test_email_with_inline_and_multipart_as_attachments(self):
|
||||
@@ -427,16 +424,14 @@ class GetEmailCommonTests(TestCase):
|
||||
else:
|
||||
self.assertTrue(
|
||||
False,
|
||||
"Unexpected file in ticket attachments: %s"
|
||||
% att_retrieved.filename,
|
||||
f"Unexpected file in ticket attachments: {att_retrieved.filename}",
|
||||
)
|
||||
self.assertTrue(
|
||||
email_attachment_found,
|
||||
"Email attachment file not found ticket attachments: %s"
|
||||
% (email_att_filename),
|
||||
f"Email attachment file not found ticket attachments: {email_att_filename}",
|
||||
)
|
||||
self.assertTrue(
|
||||
inline_found, "Inline file not found in email: %s" % (inline_att_filename)
|
||||
inline_found, f"Inline file not found in email: {inline_att_filename}"
|
||||
)
|
||||
|
||||
def test_email_with_txt_as_attachment_with_simple_alternative_message(self):
|
||||
@@ -483,13 +478,11 @@ class GetEmailCommonTests(TestCase):
|
||||
else:
|
||||
self.assertTrue(
|
||||
False,
|
||||
"Unexpected file in ticket attachments: %s"
|
||||
% att_retrieved.filename,
|
||||
f"Unexpected file in ticket attachments: {att_retrieved.filename}",
|
||||
)
|
||||
self.assertTrue(
|
||||
email_attachment_found,
|
||||
"Email attachment file not found ticket attachments: %s"
|
||||
% (email_att_filename),
|
||||
f"Email attachment file not found ticket attachments: {email_att_filename}",
|
||||
)
|
||||
|
||||
@override_settings(QUEUE_EMAIL_BOX_UPDATE_ONLY=False)
|
||||
@@ -590,7 +583,7 @@ class GetEmailCommonTests(TestCase):
|
||||
)
|
||||
ticket = extract_email_metadata(
|
||||
message.as_string(), self.queue_public, self.logger
|
||||
) # noqa
|
||||
)
|
||||
|
||||
# Assert get_ticket_id_from_subject_slug was called for current and then other1
|
||||
mock_get_ticket_id.assert_has_calls(
|
||||
@@ -602,7 +595,7 @@ class GetEmailCommonTests(TestCase):
|
||||
|
||||
# Assert that create_object_from_email_message was called with the ticket ID and the other queue
|
||||
mock_create_object.assert_called_once()
|
||||
args, kwargs = mock_create_object.call_args
|
||||
args, _kwargs = mock_create_object.call_args
|
||||
self.assertEqual(args[1], 123) # ticket_id
|
||||
self.assertEqual(args[2]["queue"], queue_other1) # payload
|
||||
|
||||
@@ -640,7 +633,7 @@ class GetEmailCommonTests(TestCase):
|
||||
|
||||
# Assert that create_object_from_email_message was called with None ticket_id and the original queue
|
||||
mock_create_object.assert_called_once()
|
||||
args, kwargs = mock_create_object.call_args
|
||||
args, _kwargs = mock_create_object.call_args
|
||||
self.assertIsNone(args[1])
|
||||
self.assertEqual(args[2]["queue"], self.queue_public)
|
||||
|
||||
@@ -700,7 +693,7 @@ class EmailTaskTests(TestCase):
|
||||
)
|
||||
|
||||
|
||||
class GetEmailParametricTemplate(object):
|
||||
class GetEmailParametricTemplate:
|
||||
"""TestCase that checks basic email functionality across methods and socks configs."""
|
||||
|
||||
def setUp(self):
|
||||
@@ -764,7 +757,7 @@ class GetEmailParametricTemplate(object):
|
||||
from socks import ProxyConnectionError
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port)
|
||||
ProxyConnectionError, f"{unrouted_socks_server}:{unused_port}"
|
||||
):
|
||||
call_command("get_email")
|
||||
|
||||
@@ -795,7 +788,7 @@ class GetEmailParametricTemplate(object):
|
||||
}
|
||||
pop3_mail_list = (
|
||||
"+OK 2 messages",
|
||||
("1 %d" % test_mail_len, "2 %d" % test_mail_len),
|
||||
(f"1 {test_mail_len}", f"2 {test_mail_len}"),
|
||||
)
|
||||
mocked_poplib_server = mock.Mock()
|
||||
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list)
|
||||
@@ -874,12 +867,12 @@ class GetEmailParametricTemplate(object):
|
||||
call_command("get_email")
|
||||
|
||||
ticket1 = get_object_or_404(Ticket, pk=1)
|
||||
self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id)
|
||||
self.assertEqual(ticket1.ticket_for_url, f"QQ-{ticket1.id}")
|
||||
self.assertEqual(ticket1.title, test_email_subject)
|
||||
self.assertEqual(ticket1.description, test_email_body)
|
||||
|
||||
ticket2 = get_object_or_404(Ticket, pk=2)
|
||||
self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id)
|
||||
self.assertEqual(ticket2.ticket_for_url, f"QQ-{ticket2.id}")
|
||||
self.assertEqual(ticket2.title, test_email_subject)
|
||||
self.assertEqual(ticket2.description, test_email_body)
|
||||
|
||||
@@ -907,7 +900,7 @@ class GetEmailParametricTemplate(object):
|
||||
from socks import ProxyConnectionError
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port)
|
||||
ProxyConnectionError, f"{unrouted_socks_server}:{unused_port}"
|
||||
):
|
||||
call_command("get_email")
|
||||
|
||||
@@ -938,7 +931,7 @@ class GetEmailParametricTemplate(object):
|
||||
}
|
||||
pop3_mail_list = (
|
||||
"+OK 2 messages",
|
||||
("1 %d" % test_mail_len, "2 %d" % test_mail_len),
|
||||
(f"1 {test_mail_len}", f"2 {test_mail_len}"),
|
||||
)
|
||||
mocked_poplib_server = mock.Mock()
|
||||
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list)
|
||||
@@ -1017,13 +1010,13 @@ class GetEmailParametricTemplate(object):
|
||||
call_command("get_email")
|
||||
|
||||
ticket1 = get_object_or_404(Ticket, pk=1)
|
||||
self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id)
|
||||
self.assertEqual(ticket1.ticket_for_url, f"QQ-{ticket1.id}")
|
||||
self.assertEqual(ticket1.submitter_email, test_email_from_meta[1])
|
||||
self.assertEqual(ticket1.title, test_email_subject)
|
||||
self.assertEqual(ticket1.description, test_email_body)
|
||||
|
||||
ticket2 = get_object_or_404(Ticket, pk=2)
|
||||
self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id)
|
||||
self.assertEqual(ticket2.ticket_for_url, f"QQ-{ticket2.id}")
|
||||
self.assertEqual(ticket2.submitter_email, test_email_from_meta[1])
|
||||
self.assertEqual(ticket2.title, test_email_subject)
|
||||
self.assertEqual(ticket2.description, test_email_body)
|
||||
@@ -1054,7 +1047,7 @@ class GetEmailParametricTemplate(object):
|
||||
from socks import ProxyConnectionError
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port)
|
||||
ProxyConnectionError, f"{unrouted_socks_server}:{unused_port}"
|
||||
):
|
||||
call_command("get_email")
|
||||
|
||||
@@ -1085,7 +1078,7 @@ class GetEmailParametricTemplate(object):
|
||||
}
|
||||
pop3_mail_list = (
|
||||
"+OK 2 messages",
|
||||
("1 %d" % test_mail_len, "2 %d" % test_mail_len),
|
||||
(f"1 {test_mail_len}", f"2 {test_mail_len}"),
|
||||
)
|
||||
mocked_poplib_server = mock.Mock()
|
||||
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list)
|
||||
@@ -1164,12 +1157,12 @@ class GetEmailParametricTemplate(object):
|
||||
call_command("get_email")
|
||||
|
||||
ticket1 = get_object_or_404(Ticket, pk=1)
|
||||
self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id)
|
||||
self.assertEqual(ticket1.ticket_for_url, f"QQ-{ticket1.id}")
|
||||
self.assertEqual(ticket1.title, test_email_subject)
|
||||
self.assertEqual(ticket1.description, test_email_body)
|
||||
|
||||
ticket2 = get_object_or_404(Ticket, pk=2)
|
||||
self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id)
|
||||
self.assertEqual(ticket2.ticket_for_url, f"QQ-{ticket2.id}")
|
||||
self.assertEqual(ticket2.title, test_email_subject)
|
||||
self.assertEqual(ticket2.description, test_email_body)
|
||||
|
||||
@@ -1224,7 +1217,7 @@ class GetEmailParametricTemplate(object):
|
||||
from socks import ProxyConnectionError
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port)
|
||||
ProxyConnectionError, f"{unrouted_socks_server}:{unused_port}"
|
||||
):
|
||||
call_command("get_email")
|
||||
|
||||
@@ -1257,7 +1250,7 @@ class GetEmailParametricTemplate(object):
|
||||
}
|
||||
pop3_mail_list = (
|
||||
"+OK 2 messages",
|
||||
("1 %d" % test_mail_len, "2 %d" % test_mail_len),
|
||||
(f"1 {test_mail_len}", f"2 {test_mail_len}"),
|
||||
)
|
||||
mocked_poplib_server = mock.Mock()
|
||||
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list)
|
||||
@@ -1336,7 +1329,7 @@ class GetEmailParametricTemplate(object):
|
||||
call_command("get_email")
|
||||
|
||||
ticket1 = get_object_or_404(Ticket, pk=1)
|
||||
self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id)
|
||||
self.assertEqual(ticket1.ticket_for_url, f"QQ-{ticket1.id}")
|
||||
self.assertEqual(ticket1.title, subject)
|
||||
# plain text should become description
|
||||
self.assertEqual(ticket1.description, text)
|
||||
@@ -1355,7 +1348,7 @@ class GetEmailParametricTemplate(object):
|
||||
self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 3)
|
||||
|
||||
ticket2 = get_object_or_404(Ticket, pk=2)
|
||||
self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id)
|
||||
self.assertEqual(ticket2.ticket_for_url, f"QQ-{ticket2.id}")
|
||||
self.assertEqual(ticket2.title, subject)
|
||||
# plain text should become description
|
||||
self.assertEqual(ticket2.description, text)
|
||||
@@ -1378,7 +1371,7 @@ class GetEmailParametricTemplate(object):
|
||||
from socks import ProxyConnectionError
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ProxyConnectionError, "%s:%s" % (unrouted_socks_server, unused_port)
|
||||
ProxyConnectionError, f"{unrouted_socks_server}:{unused_port}"
|
||||
):
|
||||
call_command("get_email")
|
||||
|
||||
@@ -1405,7 +1398,7 @@ class GetEmailParametricTemplate(object):
|
||||
pop3_emails = {
|
||||
"1": ("+OK", test_email.split("\n")),
|
||||
}
|
||||
pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len))
|
||||
pop3_mail_list = ("+OK 1 message", (f"1 {test_mail_len}"))
|
||||
mocked_poplib_server = mock.Mock()
|
||||
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list)
|
||||
mocked_poplib_server.retr = mock.Mock(
|
||||
@@ -1481,7 +1474,7 @@ class GetEmailParametricTemplate(object):
|
||||
call_command("get_email")
|
||||
|
||||
ticket1 = get_object_or_404(Ticket, pk=1)
|
||||
self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id)
|
||||
self.assertEqual(ticket1.ticket_for_url, f"QQ-{ticket1.id}")
|
||||
self.assertEqual(
|
||||
ticket1.title, "example email that crashes django-helpdesk get_email"
|
||||
)
|
||||
@@ -1691,7 +1684,7 @@ for method, socks in case_matrix:
|
||||
socks_str = "Nosocks"
|
||||
if socks:
|
||||
socks_str = socks.capitalize()
|
||||
test_name = str("TestGetEmail%s%s" % (method.capitalize(), socks_str))
|
||||
test_name = str(f"TestGetEmail{method.capitalize()}{socks_str}")
|
||||
|
||||
cl = type(
|
||||
test_name,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
from .helpers import get_staff_user
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from helpdesk.models import KBCategory, KBItem, Queue, Ticket
|
||||
|
||||
from .helpers import get_staff_user
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.test import override_settings, TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class TestLoginRedirect(TestCase):
|
||||
# the redirect url, so that the custom login can redirect the browser
|
||||
# back to helpdesk after the login.
|
||||
home_url = reverse("helpdesk:home")
|
||||
expected = "/custom/login/?next={}".format(home_url)
|
||||
expected = f"/custom/login/?next={home_url}"
|
||||
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
||||
|
||||
@override_settings(LOGIN_URL="/custom/login/")
|
||||
@@ -20,7 +20,7 @@ class TestLoginRedirect(TestCase):
|
||||
next_param = "/redirect/back"
|
||||
url = reverse("helpdesk:login") + "?next=" + next_param
|
||||
response = self.client.get(url)
|
||||
expected = "/custom/login/?next={}".format(next_param)
|
||||
expected = f"/custom/login/?next={next_param}"
|
||||
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
||||
|
||||
@override_settings(LOGIN_URL="helpdesk:login", SITE_ID=1)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from helpdesk.models import get_markdown
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
from importlib import reload
|
||||
|
||||
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 .helpers import create_ticket, get_staff_user, reload_urlconf, User
|
||||
from importlib import reload
|
||||
import sys
|
||||
|
||||
from .helpers import User, create_ticket, get_staff_user, reload_urlconf
|
||||
|
||||
|
||||
class KBDisabledTestCase(TestCase):
|
||||
@@ -42,7 +42,7 @@ class KBDisabledTestCase(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class StaffUserTestCaseMixin(object):
|
||||
class StaffUserTestCaseMixin:
|
||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = False
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk import settings
|
||||
from helpdesk.models import (
|
||||
FollowUp,
|
||||
@@ -45,15 +46,15 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
self.identifier_users = {}
|
||||
|
||||
for identifier in self.IDENTIFIERS:
|
||||
queue = self.__dict__["queue_%d" % identifier] = Queue.objects.create(
|
||||
title="Queue %d" % identifier,
|
||||
slug="q%d" % identifier,
|
||||
queue = self.__dict__[f"queue_{identifier}"] = Queue.objects.create(
|
||||
title=f"Queue {identifier}",
|
||||
slug=f"q{identifier}",
|
||||
)
|
||||
|
||||
user = self.__dict__["user_%d" % identifier] = User.objects.create(
|
||||
username="User_%d" % identifier,
|
||||
user = self.__dict__[f"user_{identifier}"] = User.objects.create(
|
||||
username=f"User_{identifier}",
|
||||
is_staff=True,
|
||||
email="foo%s@example.com" % identifier,
|
||||
email=f"foo{identifier}@example.com",
|
||||
)
|
||||
user.set_password(str(identifier))
|
||||
user.save()
|
||||
@@ -65,13 +66,11 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
for ticket_number in range(1, identifier + 1):
|
||||
Ticket.objects.create(
|
||||
title="Unassigned Ticket %d in Queue %d"
|
||||
% (ticket_number, identifier),
|
||||
title=f"Unassigned Ticket {ticket_number} in Queue {identifier}",
|
||||
queue=queue,
|
||||
)
|
||||
Ticket.objects.create(
|
||||
title="Ticket %d in Queue %d Assigned to User_%d"
|
||||
% (ticket_number, identifier, identifier),
|
||||
title=f"Ticket {ticket_number} in Queue {identifier} Assigned to User_{identifier}",
|
||||
queue=queue,
|
||||
assigned_to=user,
|
||||
)
|
||||
@@ -93,7 +92,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
# Regular users
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||
self.client.login(username=f"User_{identifier}", password=str(identifier))
|
||||
response = self.client.get(reverse("helpdesk:dashboard"))
|
||||
self.assertEqual(
|
||||
len(response.context["unassigned_tickets"]),
|
||||
@@ -130,7 +129,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
|
||||
# Regular users
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||
self.client.login(username=f"User_{identifier}", password=str(identifier))
|
||||
response = self.client.get(reverse("helpdesk:report_index"))
|
||||
self.assertEqual(
|
||||
len(response.context["dash_tickets"]),
|
||||
@@ -177,7 +176,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
"""
|
||||
# Regular users
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||
self.client.login(username=f"User_{identifier}", password=str(identifier))
|
||||
response = self.client.get(reverse("helpdesk:list"))
|
||||
tickets = __Query__(
|
||||
HelpdeskUser(self.identifier_users[identifier]),
|
||||
@@ -195,7 +194,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context["queue_choices"][0],
|
||||
Queue.objects.get(title="Queue %d" % identifier),
|
||||
Queue.objects.get(title=f"Queue {identifier}"),
|
||||
"Queue choices were not properly limited by queue membership",
|
||||
)
|
||||
|
||||
@@ -219,7 +218,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
"""
|
||||
# Regular users
|
||||
for identifier in self.IDENTIFIERS:
|
||||
self.client.login(username="User_%d" % identifier, password=str(identifier))
|
||||
self.client.login(username=f"User_{identifier}", password=str(identifier))
|
||||
response = self.client.get(
|
||||
reverse("helpdesk:run_report", kwargs={"report": "userqueue"})
|
||||
)
|
||||
@@ -249,7 +248,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
|
||||
# their ID
|
||||
self.assertEqual(
|
||||
response.context["headings"][1],
|
||||
"Queue %d" % identifier,
|
||||
f"Queue {identifier}",
|
||||
"Queue choices were not properly limited by queue membership",
|
||||
)
|
||||
|
||||
@@ -385,9 +384,9 @@ class PerQueuePermissionSecurityTestCase(TestCase):
|
||||
def test_merge_tickets_blocked_for_inaccessible_queue(self):
|
||||
"""user_1 cannot merge tickets when one belongs to queue_2."""
|
||||
self._login_user_1()
|
||||
url = reverse("helpdesk:merge_tickets") + "?tickets=%d&tickets=%d" % (
|
||||
self.ticket_2.id,
|
||||
self.ticket_2b.id,
|
||||
url = (
|
||||
reverse("helpdesk:merge_tickets")
|
||||
+ f"?tickets={self.ticket_2.id}&tickets={self.ticket_2b.id}"
|
||||
)
|
||||
response = self.client.post(url, {"chosen_ticket": self.ticket_2.id})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
|
||||
@@ -35,8 +36,7 @@ class PublicActionsTestCase(TestCase):
|
||||
def test_public_view_ticket(self):
|
||||
# Without key, we get 403
|
||||
response = self.client.get(
|
||||
"%s?ticket=%s&email=%s"
|
||||
% (
|
||||
"{}?ticket={}&email={}".format(
|
||||
reverse("helpdesk:public_view"),
|
||||
self.ticket.ticket_for_url,
|
||||
"test.submitter@example.com",
|
||||
@@ -46,8 +46,7 @@ class PublicActionsTestCase(TestCase):
|
||||
self.assertTemplateNotUsed(response, "helpdesk/public_view_form.html")
|
||||
# With a key it works
|
||||
response = self.client.get(
|
||||
"%s?ticket=%s&email=%s&key=%s"
|
||||
% (
|
||||
"{}?ticket={}&email={}&key={}".format(
|
||||
reverse("helpdesk:public_view"),
|
||||
self.ticket.ticket_for_url,
|
||||
"test.submitter@example.com",
|
||||
@@ -71,8 +70,7 @@ class PublicActionsTestCase(TestCase):
|
||||
current_followups = ticket.followup_set.all().count()
|
||||
|
||||
response = self.client.get(
|
||||
"%s?ticket=%s&email=%s&close&key=%s"
|
||||
% (
|
||||
"{}?ticket={}&email={}&close&key={}".format(
|
||||
reverse("helpdesk:public_view"),
|
||||
ticket.ticket_for_url,
|
||||
"test.submitter@example.com",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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 .helpers import get_staff_user
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from helpdesk.models import Queue
|
||||
|
||||
from .helpers import get_user
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
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
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
try: # python 3
|
||||
from urllib.parse import urlparse
|
||||
@@ -17,7 +19,7 @@ except ImportError: # python 2
|
||||
|
||||
|
||||
class TicketActionsTestCase(TestCase):
|
||||
fixtures = ["emailtemplate.json"]
|
||||
fixtures: ClassVar[list] = ["emailtemplate.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.queue_public = Queue.objects.create(
|
||||
@@ -216,18 +218,16 @@ class TicketActionsTestCase(TestCase):
|
||||
ticket_id = ticket.id
|
||||
|
||||
# generate the URL text
|
||||
result = num_to_link("this is ticket#%s" % ticket_id)
|
||||
result = num_to_link(f"this is ticket#{ticket_id}")
|
||||
self.assertEqual(
|
||||
result,
|
||||
"this is ticket <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a>"
|
||||
% (ticket_id, ticket_id),
|
||||
f"this is ticket <a href='/tickets/{ticket_id}/' class='ticket_link_status ticket_link_status_Open'>#{ticket_id}</a>",
|
||||
)
|
||||
|
||||
result2 = num_to_link("whoa another ticket is here #%s huh" % ticket_id)
|
||||
result2 = num_to_link(f"whoa another ticket is here #{ticket_id} huh")
|
||||
self.assertEqual(
|
||||
result2,
|
||||
"whoa another ticket is here <a href='/tickets/%s/' class='ticket_link_status ticket_link_status_Open'>#%s</a> huh"
|
||||
% (ticket_id, ticket_id),
|
||||
f"whoa another ticket is here <a href='/tickets/{ticket_id}/' class='ticket_link_status ticket_link_status_Open'>#{ticket_id}</a> huh",
|
||||
)
|
||||
|
||||
def test_create_ticket_getform(self):
|
||||
@@ -294,7 +294,7 @@ class TicketActionsTestCase(TestCase):
|
||||
data={"ticket_id": [str(ticket_1.id), str(ticket_2.id)], "action": "merge"},
|
||||
follow=True,
|
||||
)
|
||||
redirect_url = "%s?tickets=%s&tickets=%s" % (
|
||||
redirect_url = "{}?tickets={}&tickets={}".format(
|
||||
reverse("helpdesk:merge_tickets"),
|
||||
ticket_1.id,
|
||||
ticket_2.id,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
from helpdesk.models import Queue, Ticket
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import email
|
||||
import logging
|
||||
import uuid
|
||||
from typing import ClassVar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import mail
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
import email
|
||||
from helpdesk.email import extract_email_metadata, MULTIPLE_USERS_SAME_EMAIL_MSG
|
||||
|
||||
from helpdesk.email import MULTIPLE_USERS_SAME_EMAIL_MSG, extract_email_metadata
|
||||
from helpdesk.models import (
|
||||
CustomField,
|
||||
FollowUp,
|
||||
@@ -14,10 +20,8 @@ from helpdesk.models import (
|
||||
Ticket,
|
||||
TicketCC,
|
||||
)
|
||||
|
||||
from . import utils
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
import uuid
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -25,7 +29,7 @@ logger = logging.getLogger("helpdesk")
|
||||
|
||||
|
||||
class TicketBasicsTestCase(TestCase):
|
||||
fixtures = ["emailtemplate.json"]
|
||||
fixtures: ClassVar[list] = ["emailtemplate.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.queue_public = Queue.objects.create(
|
||||
@@ -62,7 +66,7 @@ class TicketBasicsTestCase(TestCase):
|
||||
email_count = len(mail.outbox)
|
||||
ticket_data = dict(queue=self.queue_public, **self.ticket_data)
|
||||
ticket = Ticket.objects.create(**ticket_data)
|
||||
self.assertEqual(ticket.ticket_for_url, "q1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"q1-{ticket.id}")
|
||||
self.assertEqual(email_count, len(mail.outbox))
|
||||
|
||||
def test_create_ticket_public(self):
|
||||
@@ -240,7 +244,7 @@ class TicketBasicsTestCase(TestCase):
|
||||
|
||||
|
||||
class EmailInteractionsTestCase(TestCase):
|
||||
fixtures = ["emailtemplate.json"]
|
||||
fixtures: ClassVar[list] = ["emailtemplate.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.queue_public = Queue.objects.create(
|
||||
@@ -294,7 +298,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify the sender (+1)
|
||||
# and the new and update queues (+2)
|
||||
@@ -329,7 +333,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
submitter_email=submitter_email,
|
||||
)
|
||||
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify the sender (+1)
|
||||
# and the new and update queues (+2)
|
||||
@@ -364,7 +368,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify:
|
||||
# the sender (+1),
|
||||
@@ -410,7 +414,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify:
|
||||
# the sender (+1),
|
||||
@@ -462,7 +466,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify:
|
||||
# the submitter (+1)
|
||||
@@ -575,7 +579,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# Ensure that <TicketCC> is created
|
||||
for cc_email in cc_list:
|
||||
@@ -675,7 +679,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As an update was made, we increase the expected_email_count with:
|
||||
# public_update_queue: +1
|
||||
@@ -783,7 +787,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# Ensure that <TicketCC> is created
|
||||
for cc_email in cc_list:
|
||||
@@ -825,7 +829,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify:
|
||||
# the sender (+1),
|
||||
@@ -877,7 +881,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq2-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq2-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify:
|
||||
# the sender (+1),
|
||||
@@ -922,7 +926,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify:
|
||||
# the sender (+1),
|
||||
@@ -959,7 +963,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# As an update was made, we increase the expected_email_count with:
|
||||
# submitter: +1
|
||||
@@ -1007,7 +1011,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq2-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq2-{ticket.id}")
|
||||
|
||||
# As we have created an Ticket from an email, we notify:
|
||||
# the sender (+1),
|
||||
@@ -1047,7 +1051,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq2-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq2-{ticket.id}")
|
||||
|
||||
# As an update was made, we increase the expected_email_count with:
|
||||
# public_update_queue: +1
|
||||
@@ -1100,7 +1104,7 @@ class EmailInteractionsTestCase(TestCase):
|
||||
|
||||
followup = FollowUp.objects.get(message_id=message_id)
|
||||
ticket = Ticket.objects.get(id=followup.ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id)
|
||||
self.assertEqual(ticket.ticket_for_url, f"mq1-{ticket.id}")
|
||||
|
||||
# Ensure that <TicketCC> is created
|
||||
for cc_email in cc_list:
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk.models import FollowUp, Queue, Ticket
|
||||
import uuid
|
||||
|
||||
|
||||
class TimeSpentTestCase(TestCase):
|
||||
@@ -42,7 +45,7 @@ class TimeSpentTestCase(TestCase):
|
||||
message_id = uuid.uuid4().hex
|
||||
followup = FollowUp.objects.create(
|
||||
ticket=self.ticket,
|
||||
date=datetime.datetime.now(),
|
||||
date=timezone.now(),
|
||||
title="Testing followup",
|
||||
comment="Testing followup time spent",
|
||||
public=True,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
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.auth import get_user_model
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from helpdesk.models import FollowUp, Queue, Ticket
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
import uuid
|
||||
from django.utils import timezone
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.models import FollowUp, Queue, Ticket
|
||||
|
||||
|
||||
@override_settings(USE_TZ=True)
|
||||
class TimeSpentAutoTestCase(TestCase):
|
||||
@@ -22,11 +24,11 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
dedicated_time=timedelta(minutes=60),
|
||||
)
|
||||
|
||||
self.ticket_data = dict(
|
||||
queue=self.queue_public,
|
||||
title="test ticket",
|
||||
description="test ticket description",
|
||||
)
|
||||
self.ticket_data = {
|
||||
"queue": self.queue_public,
|
||||
"title": "test ticket",
|
||||
"description": "test ticket description",
|
||||
}
|
||||
|
||||
self.user = User.objects.create(
|
||||
username="staff",
|
||||
@@ -401,7 +403,7 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
|
||||
# create queues
|
||||
queues_sequence = ("new", "stop1", "resume1", "stop2", "resume2", "end")
|
||||
queues = dict()
|
||||
queues = {}
|
||||
for slug in queues_sequence:
|
||||
queues[slug] = Queue.objects.create(
|
||||
title=slug,
|
||||
@@ -425,7 +427,7 @@ class TimeSpentAutoTestCase(TestCase):
|
||||
for i, queue in enumerate(queues_sequence):
|
||||
# create follow-up
|
||||
post_data = {
|
||||
"comment": "ticket in queue {}".format(queue),
|
||||
"comment": f"ticket in queue {queue}",
|
||||
"queue": queues[queue].id,
|
||||
"title": ticket.title,
|
||||
"priority": ticket.priority,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from helpdesk.models import Queue, Ticket, CustomField
|
||||
|
||||
from helpdesk.models import CustomField, Queue, Ticket
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TicketActionsTestCase(TestCase):
|
||||
fixtures = ["emailtemplate.json"]
|
||||
fixtures: ClassVar[list] = ["emailtemplate.json"]
|
||||
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
from django.contrib.auth.models import User
|
||||
from helpdesk.models import Queue, CustomField, TicketCustomFieldValue, Ticket
|
||||
from helpdesk.serializers import TicketSerializer
|
||||
from rest_framework.status import HTTP_201_CREATED
|
||||
from rest_framework.test import APITestCase
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import logging
|
||||
|
||||
# Set up a test weberver listeining on localhost:8123 for webhooks
|
||||
import http.server
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.status import HTTP_201_CREATED
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from helpdesk.models import CustomField, Queue, Ticket, TicketCustomFieldValue
|
||||
from helpdesk.serializers import TicketSerializer
|
||||
|
||||
|
||||
class WebhookRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
server: "WebhookServer"
|
||||
@@ -43,7 +44,7 @@ class WebhookRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
if not self.path == "/get-past-requests":
|
||||
if self.path != "/get-past-requests":
|
||||
self.send_response(HTTPStatus.NOT_FOUND)
|
||||
self.end_headers()
|
||||
return
|
||||
@@ -167,7 +168,7 @@ class WebhookTest(APITestCase):
|
||||
ticket.set_custom_field_values()
|
||||
serializer = TicketSerializer(ticket)
|
||||
self.assertEqual(
|
||||
list(sorted(serializer.fields.keys())),
|
||||
sorted(serializer.fields.keys()),
|
||||
[
|
||||
"assigned_to",
|
||||
"attachment",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", include("helpdesk.urls", namespace="helpdesk")),
|
||||
path("admin/", admin.site.urls),
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
"""UItility functions facilitate making unit testing easier and less brittle."""
|
||||
|
||||
from PIL import Image
|
||||
from __future__ import annotations
|
||||
|
||||
import email
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import unicodedata
|
||||
from email import encoders
|
||||
from email.message import Message
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import factory
|
||||
import faker
|
||||
from io import BytesIO
|
||||
from numpy.random import randint
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import typing
|
||||
from typing import Any, Optional, Tuple
|
||||
import unicodedata
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def strip_accents(text):
|
||||
@@ -125,7 +127,7 @@ def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any:
|
||||
"locale": locale,
|
||||
},
|
||||
)
|
||||
for _ in range(0, 4):
|
||||
for _ in range(4):
|
||||
html += (
|
||||
"<li>"
|
||||
+ factory.Faker("sentence").evaluate(
|
||||
@@ -137,7 +139,7 @@ def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any:
|
||||
)
|
||||
+ "</li>"
|
||||
)
|
||||
for _ in range(0, 4):
|
||||
for _ in range(4):
|
||||
html += "<p>" + factory.Faker("text").evaluate(
|
||||
{},
|
||||
None,
|
||||
@@ -151,9 +153,9 @@ def get_fake_html(locale: str = "en_US", wrap_in_body_tag=True) -> Any:
|
||||
def generate_email_address(
|
||||
locale: str = "en_US",
|
||||
use_short_email: bool = False,
|
||||
real_name_format: Optional[str] = "{last_name}, {first_name}",
|
||||
last_name_override: Optional[str] = None,
|
||||
) -> Tuple[str, str, str, str]:
|
||||
real_name_format: str | None = "{last_name}, {first_name}",
|
||||
last_name_override: str | None = None,
|
||||
) -> tuple[str, str, str, str]:
|
||||
"""
|
||||
Generate an RFC 2822 email address
|
||||
|
||||
@@ -187,7 +189,7 @@ def generate_email_address(
|
||||
|
||||
|
||||
def generate_file_mime_part(
|
||||
locale: str = "en_US", filename: str = None, content: str = None
|
||||
locale: str = "en_US", filename: str | None = None, content: str | None = None
|
||||
) -> Message:
|
||||
"""
|
||||
|
||||
@@ -202,12 +204,12 @@ def generate_file_mime_part(
|
||||
encoders.encode_base64(part)
|
||||
if not filename:
|
||||
filename = get_fake("word", locale=locale, min_length=8) + ".txt"
|
||||
part.add_header("Content-Disposition", "attachment; filename=%s" % filename)
|
||||
part.add_header("Content-Disposition", f"attachment; filename={filename}")
|
||||
return part
|
||||
|
||||
|
||||
def generate_executable_mime_part(
|
||||
locale: str = "en_US", filename: str = None, content: str = None
|
||||
locale: str = "en_US", filename: str | None = None, content: str | None = None
|
||||
) -> Message:
|
||||
"""
|
||||
|
||||
@@ -222,13 +224,13 @@ def generate_executable_mime_part(
|
||||
encoders.encode_base64(part)
|
||||
if not filename:
|
||||
filename = get_fake("word", locale=locale, min_length=8) + ".exe"
|
||||
part.add_header("Content-Disposition", "attachment; filename=%s" % filename)
|
||||
part.add_header("Content-Disposition", f"attachment; filename={filename}")
|
||||
return part
|
||||
|
||||
|
||||
def generate_image_mime_part(
|
||||
locale: str = "en_US",
|
||||
imagename: str = None,
|
||||
imagename: str | None = None,
|
||||
disposition_primary_type: str = "attachment",
|
||||
) -> Message:
|
||||
"""
|
||||
@@ -242,7 +244,7 @@ def generate_image_mime_part(
|
||||
if not imagename:
|
||||
imagename = get_fake("word", locale=locale, min_length=8) + ".jpg"
|
||||
part.add_header(
|
||||
"Content-Disposition", disposition_primary_type + "; filename= %s" % imagename
|
||||
"Content-Disposition", disposition_primary_type + f"; filename= {imagename}"
|
||||
)
|
||||
return part
|
||||
|
||||
@@ -260,14 +262,14 @@ def generate_email_list(
|
||||
"""
|
||||
email_address_list = [
|
||||
generate_email_address(locale, use_short_email=use_short_email)[0]
|
||||
for _ in range(0, address_cnt)
|
||||
for _ in range(address_cnt)
|
||||
]
|
||||
return ",".join(email_address_list)
|
||||
|
||||
|
||||
def add_simple_email_headers(
|
||||
message: Message, locale: str = "en_US", use_short_email: bool = False
|
||||
) -> typing.Tuple[typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
) -> tuple[tuple[str, str], tuple[str, str]]:
|
||||
"""
|
||||
Adds the key email headers to a Mime part
|
||||
|
||||
@@ -288,8 +290,8 @@ def add_simple_email_headers(
|
||||
def generate_mime_part(
|
||||
locale: str = "en_US",
|
||||
part_type: str = "plain",
|
||||
body: str = None,
|
||||
) -> typing.Optional[Message]:
|
||||
body: str | None = None,
|
||||
) -> Message | None:
|
||||
"""
|
||||
Generates a mime part of the specified type
|
||||
|
||||
@@ -311,17 +313,17 @@ def generate_mime_part(
|
||||
elif "image" == part_type:
|
||||
msg = generate_image_mime_part(locale=locale)
|
||||
else:
|
||||
raise Exception("Mime part not implemented: " + part_type)
|
||||
raise NotImplementedError("Mime part not implemented: " + part_type)
|
||||
return msg
|
||||
|
||||
|
||||
def generate_multipart_email(
|
||||
locale: str = "en_US",
|
||||
type_list: typing.List[str] = ["plain", "html", "image"],
|
||||
sub_type: str = None,
|
||||
type_list: list[str] | None = None,
|
||||
sub_type: str | None = None,
|
||||
use_short_email: bool = False,
|
||||
body: str = None,
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
body: str | None = None,
|
||||
) -> tuple[Message, tuple[str, str], tuple[str, str]]:
|
||||
"""
|
||||
Generates an email including headers with the defined multiparts
|
||||
|
||||
@@ -331,6 +333,8 @@ def generate_multipart_email(
|
||||
:param use_short_email: produces a "To" or "From" that is only the email address if True
|
||||
:param body: if provided then will be added to the plain mime part only
|
||||
"""
|
||||
if type_list is None:
|
||||
type_list = ["plain", "html", "image"]
|
||||
msg = MIMEMultipart(sub_type) if sub_type else MIMEMultipart()
|
||||
for part_type in type_list:
|
||||
msg.attach(generate_mime_part(locale=locale, part_type=part_type, body=body))
|
||||
@@ -343,8 +347,8 @@ def generate_multipart_email(
|
||||
def generate_text_email(
|
||||
locale: str = "en_US",
|
||||
use_short_email: bool = False,
|
||||
body: str = None,
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
body: str | None = None,
|
||||
) -> tuple[Message, tuple[str, str], tuple[str, str]]:
|
||||
"""
|
||||
Generates an email including headers
|
||||
"""
|
||||
@@ -359,7 +363,7 @@ def generate_text_email(
|
||||
|
||||
def generate_html_email(
|
||||
locale: str = "en_US", use_short_email: bool = False
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
) -> tuple[Message, tuple[str, str], tuple[str, str]]:
|
||||
"""
|
||||
Generates an email including headers
|
||||
"""
|
||||
@@ -375,9 +379,9 @@ def generate_email_with_subject(
|
||||
subject: str,
|
||||
locale: str = "en_US",
|
||||
use_short_email: bool = False,
|
||||
body: str = None,
|
||||
html_body: str = None,
|
||||
) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]:
|
||||
body: str | None = None,
|
||||
html_body: str | None = None,
|
||||
) -> tuple[Message, tuple[str, str], tuple[str, str]]:
|
||||
"""
|
||||
Generates an email with a specified subject, and optional plain and HTML bodies.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user