Ruff 0.16.0 updates

This commit is contained in:
DavidVadnais
2026-07-23 12:27:41 -10:00
parent 1ee12aad71
commit 7ed7b43451
63 changed files with 692 additions and 731 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import logging
import requests
import requests.exceptions
import logging
from django.dispatch import receiver
from . import settings

View File

@@ -1 +1 @@
from .settings import * # noqa
from .settings import *

View File

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

View File

@@ -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 = (
[

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
from django.test import SimpleTestCase
from helpdesk.models import get_markdown

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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