mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2025-06-03 00:15:46 +02:00
285 lines
8.3 KiB
Python
285 lines
8.3 KiB
Python
"""
|
|
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
|
|
|
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
|
|
|
lib.py - Common functions (eg multipart e-mail)
|
|
"""
|
|
|
|
from datetime import date, datetime, time
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
|
from django.utils.encoding import smart_str
|
|
from helpdesk.settings import (
|
|
CUSTOMFIELD_DATE_FORMAT,
|
|
CUSTOMFIELD_DATETIME_FORMAT,
|
|
CUSTOMFIELD_TIME_FORMAT,
|
|
)
|
|
import logging
|
|
import mimetypes
|
|
|
|
|
|
logger = logging.getLogger("helpdesk")
|
|
|
|
|
|
def ticket_template_context(ticket):
|
|
context = {}
|
|
|
|
for field in (
|
|
"title",
|
|
"created",
|
|
"modified",
|
|
"submitter_email",
|
|
"status",
|
|
"get_status_display",
|
|
"on_hold",
|
|
"description",
|
|
"resolution",
|
|
"priority",
|
|
"get_priority_display",
|
|
"last_escalation",
|
|
"ticket",
|
|
"ticket_for_url",
|
|
"merged_to",
|
|
"get_status",
|
|
"ticket_url",
|
|
"staff_url",
|
|
"_get_assigned_to",
|
|
):
|
|
attr = getattr(ticket, field, None)
|
|
if callable(attr):
|
|
context[field] = "%s" % attr()
|
|
else:
|
|
context[field] = attr
|
|
context["assigned_to"] = context["_get_assigned_to"]
|
|
|
|
return context
|
|
|
|
|
|
def queue_template_context(queue):
|
|
context = {}
|
|
|
|
for field in ("title", "slug", "email_address", "from_address", "locale"):
|
|
attr = getattr(queue, field, None)
|
|
if callable(attr):
|
|
context[field] = attr()
|
|
else:
|
|
context[field] = attr
|
|
|
|
return context
|
|
|
|
|
|
def safe_template_context(ticket):
|
|
"""
|
|
Return a dictionary that can be used as a template context to render
|
|
comments and other details with ticket or queue parameters. Note that
|
|
we don't just provide the Ticket & Queue objects to the template as
|
|
they could reveal confidential information. Just imagine these two options:
|
|
* {{ ticket.queue.email_box_password }}
|
|
* {{ ticket.assigned_to.password }}
|
|
|
|
Ouch!
|
|
|
|
The downside to this is that if we make changes to the model, we will also
|
|
have to update this code. Perhaps we can find a better way in the future.
|
|
"""
|
|
|
|
context = {
|
|
"queue": queue_template_context(ticket.queue),
|
|
"ticket": ticket_template_context(ticket),
|
|
}
|
|
context["ticket"]["queue"] = context["queue"]
|
|
|
|
return context
|
|
|
|
|
|
def text_is_spam(text, request):
|
|
# Based on a blog post by 'sciyoshi':
|
|
# http://sciyoshi.com/blog/2008/aug/27/using-akismet-djangos-new-comments-framework/
|
|
# This will return 'True' is the given text is deemed to be spam, or
|
|
# False if it is not spam. If it cannot be checked for some reason, we
|
|
# assume it isn't spam.
|
|
try:
|
|
from akismet import Akismet
|
|
except ImportError:
|
|
return False
|
|
from django.contrib.sites.models import Site
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
|
|
try:
|
|
site = Site.objects.get_current()
|
|
except ImproperlyConfigured:
|
|
site = Site(domain="configure-django-sites.com")
|
|
|
|
# see https://akismet.readthedocs.io/en/latest/overview.html#using-akismet
|
|
|
|
apikey = None
|
|
|
|
if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"):
|
|
apikey = settings.TYPEPAD_ANTISPAM_API_KEY
|
|
elif hasattr(settings, "PYTHON_AKISMET_API_KEY"):
|
|
# new env var expected by python-akismet package
|
|
apikey = settings.PYTHON_AKISMET_API_KEY
|
|
elif hasattr(settings, "AKISMET_API_KEY"):
|
|
# deprecated, but kept for backward compatibility
|
|
apikey = settings.AKISMET_API_KEY
|
|
else:
|
|
return False
|
|
|
|
ak = Akismet(
|
|
blog_url="http://%s/" % site.domain,
|
|
key=apikey,
|
|
)
|
|
|
|
if hasattr(settings, "TYPEPAD_ANTISPAM_API_KEY"):
|
|
ak.baseurl = "api.antispam.typepad.com/1.1/"
|
|
|
|
if ak.verify_key():
|
|
ak_data = {
|
|
"user_ip": request.META.get("REMOTE_ADDR", "127.0.0.1"),
|
|
"user_agent": request.headers.get("User-Agent", ""),
|
|
"referrer": request.headers.get("Referer", ""),
|
|
"comment_type": "comment",
|
|
"comment_author": "",
|
|
}
|
|
|
|
return ak.comment_check(smart_str(text), data=ak_data)
|
|
|
|
return False
|
|
|
|
|
|
def process_attachments(followup, attached_files):
|
|
max_email_attachment_size = getattr(
|
|
settings, "HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE", 512000
|
|
)
|
|
attachments = []
|
|
errors = set()
|
|
|
|
for attached in attached_files:
|
|
if attached.size:
|
|
from helpdesk.models import FollowUpAttachment
|
|
|
|
filename = smart_str(attached.name)
|
|
att = FollowUpAttachment(
|
|
followup=followup,
|
|
file=attached,
|
|
filename=filename,
|
|
mime_type=attached.content_type
|
|
or mimetypes.guess_type(filename, strict=False)[0]
|
|
or "application/octet-stream",
|
|
size=attached.size,
|
|
)
|
|
try:
|
|
att.full_clean()
|
|
except ValidationError as e:
|
|
errors.add(e)
|
|
else:
|
|
att.save()
|
|
|
|
if attached.size < max_email_attachment_size:
|
|
# Only files smaller than 512kb (or as defined in
|
|
# settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via
|
|
# email.
|
|
attachments.append([filename, att.file])
|
|
|
|
if errors:
|
|
raise ValidationError(list(errors))
|
|
|
|
return attachments
|
|
|
|
|
|
def format_time_spent(time_spent):
|
|
"""Format time_spent attribute to "[H]HHh:MMm" text string to be allign in
|
|
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,
|
|
)
|
|
else:
|
|
time_spent = ""
|
|
return time_spent
|
|
|
|
|
|
def convert_value(value):
|
|
"""Convert date/time data type to known fixed format string"""
|
|
if type(value) is datetime:
|
|
return value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
|
|
elif type(value) is date:
|
|
return value.strftime(CUSTOMFIELD_DATE_FORMAT)
|
|
elif type(value) is time:
|
|
return value.strftime(CUSTOMFIELD_TIME_FORMAT)
|
|
else:
|
|
return value
|
|
|
|
|
|
def daily_time_spent_calculation(earliest, latest, open_hours):
|
|
"""Returns the number of seconds for a single day time interval according to open hours."""
|
|
|
|
time_spent_seconds = 0
|
|
|
|
# avoid rendering day in different locale
|
|
weekday = (
|
|
"monday",
|
|
"tuesday",
|
|
"wednesday",
|
|
"thursday",
|
|
"friday",
|
|
"saturday",
|
|
"sunday",
|
|
)[earliest.weekday()]
|
|
|
|
# enforce correct settings
|
|
MIDNIGHT = 23.9999
|
|
start, end = open_hours.get(weekday, (0, MIDNIGHT))
|
|
if not 0 <= start <= end <= MIDNIGHT:
|
|
raise ImproperlyConfigured(
|
|
"HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
|
|
f" setting for {weekday} out of (0, 23.9999) boundary"
|
|
)
|
|
|
|
# transform decimals to minutes and seconds
|
|
start_hour, start_minute, start_second = (
|
|
int(start),
|
|
int(start % 1 * 60),
|
|
int(start * 60 % 1 * 60),
|
|
)
|
|
end_hour, end_minute, end_second = (
|
|
int(end),
|
|
int(end % 1 * 60),
|
|
int(end * 60 % 1 * 60),
|
|
)
|
|
|
|
# translate time for delta calculation
|
|
earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600
|
|
latest_f = (
|
|
latest.hour
|
|
+ latest.minute / 60
|
|
+ latest.second / (60 * 60)
|
|
+ latest.microsecond / (60 * 60 * 999999)
|
|
)
|
|
|
|
# if latest time is midnight and close hour is midnight, add a second to the time spent
|
|
if latest_f >= MIDNIGHT and end == MIDNIGHT:
|
|
time_spent_seconds += 1
|
|
|
|
if earliest_f < start:
|
|
earliest = earliest.replace(
|
|
hour=start_hour, minute=start_minute, second=start_second
|
|
)
|
|
elif earliest_f >= end:
|
|
earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
|
|
|
if latest_f < start:
|
|
latest = latest.replace(
|
|
hour=start_hour, minute=start_minute, second=start_second
|
|
)
|
|
elif latest_f >= end:
|
|
latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
|
|
|
day_delta = latest - earliest
|
|
time_spent_seconds += day_delta.seconds
|
|
|
|
return time_spent_seconds
|