forked from extern/django-helpdesk
Merge branch 'master' into pinax-remove
This commit is contained in:
commit
563b28ed14
4
.flake8
Normal file
4
.flake8
Normal file
@ -0,0 +1,4 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
exclude = .git,__pycache__,.tox,.eggs,*.egg,node_modules,.venv,migrations,docs,demo,tests,setup.py
|
||||
import-order-style = pep8
|
@ -7,8 +7,9 @@ python:
|
||||
- "3.8"
|
||||
|
||||
env:
|
||||
- DJANGO=2.2.16
|
||||
- DJANGO=3.1.2
|
||||
- DJANGO=2.2.23
|
||||
- DJANGO=3.1.11
|
||||
- DJANGO=3.2.3
|
||||
|
||||
install:
|
||||
- pip install -q Django==$DJANGO
|
||||
|
@ -155,7 +155,7 @@ collaborative translation. If you want to help translate django-helpdesk into
|
||||
languages other than English, we encourage you to make use of our Transifex
|
||||
project:
|
||||
|
||||
http://www.transifex.net/projects/p/django-helpdesk/resource/core/
|
||||
http://www.transifex.com/projects/p/django-helpdesk/resource/core/
|
||||
|
||||
Once you have translated content via Transifex, please raise an issue on the
|
||||
project Github page and tag it as "translations" to let us know it's ready to
|
||||
|
@ -108,8 +108,8 @@ HELPDESK_SHOW_CHANGE_PASSWORD = True
|
||||
# Instead of showing the public web portal first,
|
||||
# we can instead redirect users straight to the login page.
|
||||
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/login/'
|
||||
LOGIN_URL = 'helpdesk:login'
|
||||
LOGIN_REDIRECT_URL = 'helpdesk:home'
|
||||
|
||||
# Database
|
||||
# - by default, we use SQLite3 for the demo, but you can also
|
||||
|
@ -13,7 +13,7 @@ project_root = os.path.dirname(here)
|
||||
NAME = 'django-helpdesk-demodesk'
|
||||
DESCRIPTION = 'A demo Django project using django-helpdesk'
|
||||
README = open(os.path.join(here, 'README.rst')).read()
|
||||
VERSION = '0.3.0b2'
|
||||
VERSION = '0.3.0b3'
|
||||
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
|
||||
AUTHOR = 'django-helpdesk team'
|
||||
URL = 'https://github.com/django-helpdesk/django-helpdesk'
|
||||
|
@ -107,6 +107,10 @@ These options only change display of items on public-facing pages, not staff pag
|
||||
|
||||
**Default:** ``HELPDESK_SUBMIT_A_TICKET_PUBLIC = True``
|
||||
|
||||
- **HELPDESK_PUBLIC_TICKET_FORM_CLASS** Define custom form class to show on public pages for anon users. You can use it for adding custom fields and validation, captcha and so on.
|
||||
|
||||
**Default:** ``HELPDESK_PUBLIC_TICKET_FORM_CLASS = "helpdesk.forms.PublicTicketForm"``
|
||||
|
||||
|
||||
Options for public ticket submission form
|
||||
-----------------------------------------
|
||||
@ -219,3 +223,7 @@ The following settings were defined in previous versions and are no longer suppo
|
||||
- **HELPDESK_FOOTER_SHOW_CHANGE_LANGUAGE_LINK** Is never shown. Use your own template if required.
|
||||
|
||||
- **HELPDESK_ENABLE_PER_QUEUE_MEMBERSHIP** Discontinued in favor of HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION.
|
||||
|
||||
- **HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL** Do not ignore fowarded and replied text from the email messages which create a new ticket; useful for cases when customer forwards some email (error from service or something) and wants support to see that
|
||||
|
||||
- **HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE** Any incoming .eml message is saved and available, helps when customer spent some time doing fancy markup which has been corrupted during the email-to-ticket-comment translate process
|
||||
|
3
helpdesk/.flake8
Normal file
3
helpdesk/.flake8
Normal file
@ -0,0 +1,3 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
import-order-style = pep8
|
@ -4,42 +4,36 @@ Django Helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
(c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved.
|
||||
See LICENSE for details.
|
||||
"""
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils import encoding, timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from helpdesk import settings
|
||||
from helpdesk.lib import safe_template_context, process_attachments
|
||||
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
|
||||
|
||||
from datetime import timedelta
|
||||
import base64
|
||||
import binascii
|
||||
# import base64
|
||||
import email
|
||||
from email.header import decode_header
|
||||
from email.utils import getaddresses, parseaddr, collapse_rfc2231_value
|
||||
import imaplib
|
||||
import logging
|
||||
import mimetypes
|
||||
from os import listdir, unlink
|
||||
from os.path import isfile, join
|
||||
import os
|
||||
import poplib
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from email.utils import getaddresses
|
||||
from os.path import isfile, join
|
||||
from time import ctime
|
||||
from optparse import make_option
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from django.conf import settings as django_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.db.models import Q
|
||||
from django.utils import encoding, timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from email_reply_parser import EmailReplyParser
|
||||
|
||||
import logging
|
||||
from helpdesk import settings
|
||||
from helpdesk.lib import safe_template_context, process_attachments
|
||||
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
|
||||
|
||||
|
||||
# import User model, which may be a custom model
|
||||
User = get_user_model()
|
||||
@ -70,37 +64,48 @@ def process_email(quiet=False):
|
||||
if q.logging_type in logging_types:
|
||||
logger.setLevel(logging_types[q.logging_type])
|
||||
elif not q.logging_type or q.logging_type == 'none':
|
||||
logging.disable(logging.CRITICAL) # disable all messages
|
||||
# disable all handlers so messages go to nowhere
|
||||
logger.handlers = []
|
||||
logger.propagate = False
|
||||
if quiet:
|
||||
logger.propagate = False # do not propagate to root logger that would log to console
|
||||
logdir = q.logging_dir or '/var/log/helpdesk/'
|
||||
|
||||
# Log messages to specific file only if the queue has it configured
|
||||
if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set
|
||||
log_file_handler = logging.FileHandler(join(q.logging_dir, q.slug + '_get_email.log'))
|
||||
logger.addHandler(log_file_handler)
|
||||
else:
|
||||
log_file_handler = None
|
||||
|
||||
try:
|
||||
handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log'))
|
||||
logger.addHandler(handler)
|
||||
|
||||
if not q.email_box_last_check:
|
||||
q.email_box_last_check = timezone.now() - timedelta(minutes=30)
|
||||
|
||||
queue_time_delta = timedelta(minutes=q.email_box_interval or 0)
|
||||
|
||||
if (q.email_box_last_check + queue_time_delta) < timezone.now():
|
||||
process_queue(q, logger=logger)
|
||||
q.email_box_last_check = timezone.now()
|
||||
q.save()
|
||||
finally:
|
||||
# we must close the file handler correctly if it's created
|
||||
try:
|
||||
handler.close()
|
||||
if log_file_handler:
|
||||
log_file_handler.close()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
try:
|
||||
logger.removeHandler(handler)
|
||||
if log_file_handler:
|
||||
logger.removeHandler(log_file_handler)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
|
||||
def pop3_sync(q, logger, server):
|
||||
server.getwelcome()
|
||||
try:
|
||||
server.stls()
|
||||
except Exception:
|
||||
logger.warning("POP3 StartTLS failed or unsupported. Connection will be unencrypted.")
|
||||
server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
|
||||
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
|
||||
|
||||
@ -138,17 +143,27 @@ def pop3_sync(q, logger, server):
|
||||
|
||||
def imap_sync(q, logger, server):
|
||||
try:
|
||||
try:
|
||||
server.starttls()
|
||||
except Exception:
|
||||
logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
|
||||
server.login(q.email_box_user or
|
||||
settings.QUEUE_EMAIL_BOX_USER,
|
||||
q.email_box_pass or
|
||||
settings.QUEUE_EMAIL_BOX_PASSWORD)
|
||||
server.select(q.email_box_imap_folder)
|
||||
except imaplib.IMAP4.abort:
|
||||
logger.error("IMAP login failed. Check that the server is accessible and that the username and password are correct.")
|
||||
logger.error(
|
||||
"IMAP login failed. Check that the server is accessible and that "
|
||||
"the username and password are correct."
|
||||
)
|
||||
server.logout()
|
||||
sys.exit()
|
||||
except ssl.SSLError:
|
||||
logger.error("IMAP login failed due to SSL error. This is often due to a timeout. Please check your connection and try again.")
|
||||
logger.error(
|
||||
"IMAP login failed due to SSL error. This is often due to a timeout. "
|
||||
"Please check your connection and try again."
|
||||
)
|
||||
server.logout()
|
||||
sys.exit()
|
||||
|
||||
@ -171,7 +186,10 @@ def imap_sync(q, logger, server):
|
||||
else:
|
||||
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num)
|
||||
except imaplib.IMAP4.error:
|
||||
logger.error("IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?" % q.email_box_imap_folder)
|
||||
logger.error(
|
||||
"IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?",
|
||||
q.email_box_imap_folder
|
||||
)
|
||||
|
||||
server.expunge()
|
||||
server.close()
|
||||
@ -243,7 +261,7 @@ def process_queue(q, logger):
|
||||
|
||||
elif email_box_type == 'local':
|
||||
mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/'
|
||||
mail = [join(mail_dir, f) for f in listdir(mail_dir) if isfile(join(mail_dir, f))]
|
||||
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("Found %d messages in local mailbox directory" % len(mail))
|
||||
@ -253,15 +271,15 @@ def process_queue(q, logger):
|
||||
full_message = encoding.force_text(f.read(), errors='replace')
|
||||
ticket = object_from_message(message=full_message, queue=q, logger=logger)
|
||||
if ticket:
|
||||
logger.info("Successfully processed message %d, ticket/comment created." % i)
|
||||
logger.info("Successfully processed message %d, ticket/comment created.", i)
|
||||
try:
|
||||
unlink(m) # delete message file if ticket was successful
|
||||
except OSError:
|
||||
logger.error("Unable to delete message %d." % i)
|
||||
os.unlink(m) # delete message file if ticket was successful
|
||||
except OSError as e:
|
||||
logger.error("Unable to delete message %d (%s).", i, str(e))
|
||||
else:
|
||||
logger.info("Successfully deleted message %d." % i)
|
||||
logger.info("Successfully deleted message %d.", i)
|
||||
else:
|
||||
logger.warn("Message %d was not successfully processed, and will be left in local directory" % i)
|
||||
logger.warn("Message %d was not successfully processed, and will be left in local directory", i)
|
||||
|
||||
|
||||
def decodeUnknown(charset, string):
|
||||
@ -277,7 +295,26 @@ def decodeUnknown(charset, string):
|
||||
|
||||
def decode_mail_headers(string):
|
||||
decoded = email.header.decode_header(string)
|
||||
return u' '.join([str(msg, encoding=charset, errors='replace') if charset else str(msg) for msg, charset in decoded])
|
||||
return u' '.join([
|
||||
str(msg, encoding=charset, errors='replace') if charset else str(msg)
|
||||
for msg, charset
|
||||
in decoded
|
||||
])
|
||||
|
||||
|
||||
def is_autoreply(message):
|
||||
"""
|
||||
Accepting message as something with .get(header_name) method
|
||||
Returns True if it's likely to be auto-reply or False otherwise
|
||||
So we don't start mail loops
|
||||
"""
|
||||
any_if_this = [
|
||||
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("List-Id"),
|
||||
message.get("List-Unsubscribe"),
|
||||
]
|
||||
return any(any_if_this)
|
||||
|
||||
|
||||
def create_ticket_cc(ticket, cc_list):
|
||||
@ -305,7 +342,7 @@ def create_ticket_cc(ticket, cc_list):
|
||||
try:
|
||||
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email)
|
||||
new_ticket_ccs.append(ticket_cc)
|
||||
except ValidationError as err:
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
return new_ticket_ccs
|
||||
@ -362,7 +399,6 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id))
|
||||
|
||||
new = True
|
||||
update = ''
|
||||
|
||||
# Old issue being re-opened
|
||||
elif ticket.status == Ticket.CLOSED_STATUS:
|
||||
@ -374,7 +410,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
|
||||
date=now,
|
||||
public=True,
|
||||
comment=payload['body'],
|
||||
comment=payload.get('full_body', payload['body']) or "",
|
||||
message_id=message_id
|
||||
)
|
||||
|
||||
@ -389,7 +425,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
|
||||
attached = process_attachments(f, files)
|
||||
for att_file in attached:
|
||||
logger.info("Attachment '%s' (with size %s) successfully added to ticket from email." % (att_file[0], att_file[1].size))
|
||||
logger.info(
|
||||
"Attachment '%s' (with size %s) successfully added to ticket from email.",
|
||||
att_file[0], att_file[1].size
|
||||
)
|
||||
|
||||
context = safe_template_context(ticket)
|
||||
|
||||
@ -402,18 +441,29 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
|
||||
ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True)
|
||||
|
||||
for email in ticket_cc_list:
|
||||
notifications_to_be_sent.append(email)
|
||||
for email_address in ticket_cc_list:
|
||||
notifications_to_be_sent.append(email_address)
|
||||
|
||||
autoreply = is_autoreply(message)
|
||||
if autoreply:
|
||||
logger.info("Message seems to be auto-reply, not sending any emails back to the sender")
|
||||
else:
|
||||
# send mail to appropriate people now depending on what objects
|
||||
# were created and who was CC'd
|
||||
# Add auto-reply headers because it's an auto-reply and we must
|
||||
extra_headers = {
|
||||
'In-Reply-To': message_id,
|
||||
"Auto-Submitted": "auto-replied",
|
||||
"X-Auto-Response-Suppress": "All",
|
||||
"Precedence": "auto_reply",
|
||||
}
|
||||
if new:
|
||||
ticket.send(
|
||||
{'submitter': ('newticket_submitter', context),
|
||||
'new_ticket_cc': ('newticket_cc', context),
|
||||
'ticket_cc': ('newticket_cc', context)},
|
||||
fail_silently=True,
|
||||
extra_headers={'In-Reply-To': message_id},
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
else:
|
||||
context.update(comment=f.comment)
|
||||
@ -421,13 +471,13 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
{'submitter': ('newticket_submitter', context),
|
||||
'assigned_to': ('updated_owner', context)},
|
||||
fail_silently=True,
|
||||
extra_headers={'In-Reply-To': message_id},
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
if queue.enable_notifications_on_email_events:
|
||||
ticket.send(
|
||||
{'ticket_cc': ('updated_cc', context)},
|
||||
fail_silently=True,
|
||||
extra_headers={'In-Reply-To': message_id},
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
return ticket
|
||||
@ -445,6 +495,7 @@ def object_from_message(message, queue, logger):
|
||||
|
||||
sender = message.get('from', _('Unknown Sender'))
|
||||
sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender))
|
||||
|
||||
# to address bug #832, we wrap all the text in front of the email address in
|
||||
# double quotes by using replace() on the email string. Then,
|
||||
# take first item of list, second item of tuple is the actual email address.
|
||||
@ -453,8 +504,6 @@ def object_from_message(message, queue, logger):
|
||||
# correctly. Not ideal, but this seems to work for now.
|
||||
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1]
|
||||
|
||||
body_plain, body_html = '', ''
|
||||
|
||||
cc = message.get_all('cc', None)
|
||||
if cc:
|
||||
# first, fixup the encoding if necessary
|
||||
@ -484,6 +533,7 @@ def object_from_message(message, queue, logger):
|
||||
ticket = None
|
||||
|
||||
body = None
|
||||
full_body = None
|
||||
counter = 0
|
||||
files = []
|
||||
|
||||
@ -502,7 +552,19 @@ def object_from_message(message, queue, logger):
|
||||
if part['Content-Transfer-Encoding'] == '8bit' and part.get_content_charset() == 'utf-8':
|
||||
body = body.decode('unicode_escape')
|
||||
body = decodeUnknown(part.get_content_charset(), body)
|
||||
# have to use django_settings here so overwritting it works in tests
|
||||
# the default value is False anyway
|
||||
if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False):
|
||||
# first message in thread, we save full body to avoid losing forwards and things like that
|
||||
body_parts = []
|
||||
for f in EmailReplyParser.read(body).fragments:
|
||||
body_parts.append(f.content)
|
||||
full_body = '\n\n'.join(body_parts)
|
||||
body = EmailReplyParser.parse_reply(body)
|
||||
else:
|
||||
# second and other reply, save only first part of the message
|
||||
body = EmailReplyParser.parse_reply(body)
|
||||
full_body = body
|
||||
# workaround to get unicode text out rather than escaped text
|
||||
try:
|
||||
body = body.encode('ascii').decode('unicode_escape')
|
||||
@ -515,13 +577,23 @@ def object_from_message(message, queue, logger):
|
||||
except UnicodeDecodeError:
|
||||
email_body = encoding.smart_text(part.get_payload(decode=False))
|
||||
|
||||
payload = """
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
</head>
|
||||
%s
|
||||
</html>""" % email_body
|
||||
if not body and not full_body:
|
||||
# no text has been parsed so far - try such deep parsing for some messages
|
||||
altered_body = email_body.replace("</p>", "</p>\n").replace("<br", "\n<br")
|
||||
mail = BeautifulSoup(str(altered_body), "html.parser")
|
||||
full_body = mail.get_text()
|
||||
|
||||
if "<body" not in email_body:
|
||||
email_body = f"<body>{email_body}</body>"
|
||||
|
||||
payload = (
|
||||
'<html>'
|
||||
'<head>'
|
||||
'<meta charset="utf-8" />'
|
||||
'</head>'
|
||||
'%s'
|
||||
'</html>'
|
||||
) % email_body
|
||||
files.append(
|
||||
SimpleUploadedFile(_("email_html_body.html"), payload.encode("utf-8"), 'text/html')
|
||||
)
|
||||
@ -530,18 +602,25 @@ def object_from_message(message, queue, logger):
|
||||
if not name:
|
||||
ext = mimetypes.guess_extension(part.get_content_type())
|
||||
name = "part-%i%s" % (counter, ext)
|
||||
payload = part.get_payload()
|
||||
if isinstance(payload, list):
|
||||
payload = payload.pop().as_string()
|
||||
payloadToWrite = payload
|
||||
# check version of python to ensure use of only the correct error type
|
||||
non_b64_err = TypeError
|
||||
try:
|
||||
logger.debug("Try to base64 decode the attachment payload")
|
||||
payloadToWrite = base64.decodebytes(payload)
|
||||
except non_b64_err:
|
||||
logger.debug("Payload was not base64 encoded, using raw bytes")
|
||||
payloadToWrite = payload
|
||||
else:
|
||||
name = ("part-%i_" % counter) + name
|
||||
|
||||
# # FIXME: this code gets the paylods, then does something with it and then completely ignores it
|
||||
# # writing the part.get_payload(decode=True) instead; and then the payload variable is
|
||||
# # replaced by some dict later.
|
||||
# # the `payloadToWrite` has been also ignored so was commented
|
||||
# payload = part.get_payload()
|
||||
# if isinstance(payload, list):
|
||||
# payload = payload.pop().as_string()
|
||||
# # payloadToWrite = payload
|
||||
# # check version of python to ensure use of only the correct error type
|
||||
# non_b64_err = TypeError
|
||||
# try:
|
||||
# logger.debug("Try to base64 decode the attachment payload")
|
||||
# # payloadToWrite = base64.decodebytes(payload)
|
||||
# except non_b64_err:
|
||||
# logger.debug("Payload was not base64 encoded, using raw bytes")
|
||||
# # payloadToWrite = payload
|
||||
files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0]))
|
||||
logger.debug("Found MIME attachment %s" % name)
|
||||
|
||||
@ -553,11 +632,25 @@ def object_from_message(message, queue, logger):
|
||||
if beautiful_body:
|
||||
try:
|
||||
body = beautiful_body.text
|
||||
full_body = body
|
||||
except AttributeError:
|
||||
pass
|
||||
if not body:
|
||||
body = ""
|
||||
|
||||
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False):
|
||||
# save message as attachment in case of some complex markup renders wrong
|
||||
files.append(
|
||||
SimpleUploadedFile(
|
||||
_("original_message.eml").replace(
|
||||
".eml",
|
||||
timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml"
|
||||
),
|
||||
str(message).encode("utf-8"),
|
||||
'text/plain'
|
||||
)
|
||||
)
|
||||
|
||||
smtp_priority = message.get('priority', '')
|
||||
smtp_importance = message.get('importance', '')
|
||||
high_priority_types = {'high', 'important', '1', 'urgent'}
|
||||
@ -565,6 +658,7 @@ def object_from_message(message, queue, logger):
|
||||
|
||||
payload = {
|
||||
'body': body,
|
||||
'full_body': full_body or body,
|
||||
'subject': subject,
|
||||
'queue': queue,
|
||||
'sender_email': sender_email,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ forms.py - Definitions of newforms-based forms for creating and maintaining
|
||||
tickets.
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, date, time
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django import forms
|
||||
@ -35,6 +36,10 @@ CUSTOMFIELD_TO_FIELD_DICT = {
|
||||
'slug': forms.SlugField,
|
||||
}
|
||||
|
||||
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
|
||||
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
|
||||
CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT} {CUSTOMFIELD_TIME_FORMAT}"
|
||||
|
||||
|
||||
class CustomFieldMixin(object):
|
||||
"""
|
||||
@ -71,8 +76,14 @@ class CustomFieldMixin(object):
|
||||
# Try to use the immediate equivalences dictionary
|
||||
try:
|
||||
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
||||
# Change widget in case it is a boolean
|
||||
if fieldclass == forms.BooleanField:
|
||||
# Change widgets for the following classes
|
||||
if fieldclass == forms.DateField:
|
||||
instanceargs['widget'] = forms.DateInput(attrs={'class': 'form-control date-field'})
|
||||
elif fieldclass == forms.DateTimeField:
|
||||
instanceargs['widget'] = forms.DateTimeInput(attrs={'class': 'form-control datetime-field'})
|
||||
elif fieldclass == forms.TimeField:
|
||||
instanceargs['widget'] = forms.TimeInput(attrs={'class': 'form-control time-field'})
|
||||
elif fieldclass == forms.BooleanField:
|
||||
instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'})
|
||||
|
||||
except KeyError:
|
||||
@ -88,18 +99,38 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
model = Ticket
|
||||
exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to')
|
||||
|
||||
class Media:
|
||||
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Add any custom fields that are defined to the form
|
||||
"""
|
||||
super(EditTicketForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Disable and add help_text to the merged_to field on this form
|
||||
self.fields['merged_to'].disabled = True
|
||||
self.fields['merged_to'].help_text = _('This ticket is merged into the selected ticket.')
|
||||
|
||||
for field in CustomField.objects.all():
|
||||
initial_value = None
|
||||
try:
|
||||
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field)
|
||||
initial_value = current_value.value
|
||||
except TicketCustomFieldValue.DoesNotExist:
|
||||
initial_value = None
|
||||
# Attempt to convert from fixed format string to date/time data type
|
||||
if 'datetime' == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATETIME_FORMAT)
|
||||
elif 'date' == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATE_FORMAT)
|
||||
elif 'time' == current_value.field.data_type:
|
||||
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_TIME_FORMAT)
|
||||
# If it is boolean field, transform the value to a real boolean instead of a string
|
||||
elif 'boolean' == current_value.field.data_type:
|
||||
initial_value = 'True' == initial_value
|
||||
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
|
||||
# ValueError error if parsing fails, using initial_value = current_value.value
|
||||
# TypeError if parsing None type
|
||||
pass
|
||||
instanceargs = {
|
||||
'label': field.label,
|
||||
'help_text': field.help_text,
|
||||
@ -119,6 +150,15 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
||||
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
|
||||
except ObjectDoesNotExist:
|
||||
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
|
||||
|
||||
# Convert date/time data type to known fixed format string.
|
||||
if datetime is type(value):
|
||||
cfv.value = value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
|
||||
elif date is type(value):
|
||||
cfv.value = value.strftime(CUSTOMFIELD_DATE_FORMAT)
|
||||
elif time is type(value):
|
||||
cfv.value = value.strftime(CUSTOMFIELD_TIME_FORMAT)
|
||||
else:
|
||||
cfv.value = value
|
||||
cfv.save()
|
||||
|
||||
@ -175,7 +215,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
due_date = forms.DateTimeField(
|
||||
widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
||||
required=False,
|
||||
input_formats=['%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
|
||||
input_formats=[CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
|
||||
label=_('Due on'),
|
||||
)
|
||||
|
||||
@ -187,7 +227,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
)
|
||||
|
||||
class Media:
|
||||
js = ('helpdesk/js/init_due_date.js',)
|
||||
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js')
|
||||
|
||||
def __init__(self, kbcategory=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -8,13 +8,11 @@ lib.py - Common functions (eg multipart e-mail)
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import smart_text, smart_str
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
from helpdesk.models import FollowUpAttachment, EmailTemplate
|
||||
from helpdesk.models import FollowUpAttachment
|
||||
|
||||
|
||||
logger = logging.getLogger('helpdesk')
|
||||
|
@ -502,7 +502,7 @@ msgstr "Složka pro log soubory"
|
||||
#: third_party/django-helpdesk/helpdesk/models.py:306
|
||||
msgid ""
|
||||
"If logging is enabled, what directory should we use to store log files for "
|
||||
"this queue? If no directory is set, default to /var/log/helpdesk/"
|
||||
"this queue? The standard logging mechanims are used if no directory is set"
|
||||
msgstr ""
|
||||
|
||||
#: third_party/django-helpdesk/helpdesk/models.py:317
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -522,11 +522,10 @@ msgstr "Dossier de logs"
|
||||
#: .\models.py:308
|
||||
msgid ""
|
||||
"If logging is enabled, what directory should we use to store log files for "
|
||||
"this queue? If no directory is set, default to /var/log/helpdesk/"
|
||||
"this queue? The standard logging mechanims are used if no directory is set"
|
||||
msgstr ""
|
||||
"Si les logs sont activés, quel dossier doit être utilisé pour stocker les "
|
||||
"fichiers de logs pour cette file ? Si aucun dossier n'est défini, cela sera /"
|
||||
"var/log/helpdesk/ par défaut"
|
||||
"fichiers de logs pour cette file?"
|
||||
|
||||
#: .\models.py:319
|
||||
msgid "Default owner"
|
||||
|
@ -531,13 +531,15 @@ msgstr ""
|
||||
|
||||
#: models.py:247
|
||||
msgid "Logging Directory"
|
||||
msgstr ""
|
||||
msgstr "Директория логов"
|
||||
|
||||
#: models.py:251
|
||||
msgid ""
|
||||
"If logging is enabled, what directory should we use to store log files for "
|
||||
"this queue? If no directory is set, default to /var/log/helpdesk/"
|
||||
"this queue? The standard logging mechanims are used if no directory is set"
|
||||
msgstr ""
|
||||
"Директория в которую будут сохраняться файлы с логами; стандартная конфигурация "
|
||||
"используется если ничего не указано"
|
||||
|
||||
#: models.py:261
|
||||
msgid "Default owner"
|
||||
|
@ -470,7 +470,7 @@ msgstr ""
|
||||
#: models.py:308
|
||||
msgid ""
|
||||
"If logging is enabled, what directory should we use to store log files for "
|
||||
"this queue? If no directory is set, default to /var/log/helpdesk/"
|
||||
"this queue? The standard logging mechanims are used if no directory is set"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:319
|
||||
|
@ -13,7 +13,6 @@ from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from helpdesk.models import UserSettings
|
||||
from helpdesk.settings import DEFAULT_USER_SETTINGS
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
name='logging_dir',
|
||||
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? If no directory is set, default to /var/log/helpdesk/', max_length=200, null=True, verbose_name='Logging Directory'),
|
||||
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set', max_length=200, null=True, verbose_name='Logging Directory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='queue',
|
||||
|
@ -305,7 +305,7 @@ class Queue(models.Model):
|
||||
null=True,
|
||||
help_text=_('If logging is enabled, what directory should we use to '
|
||||
'store log files for this queue? '
|
||||
'If no directory is set, default to /var/log/helpdesk/'),
|
||||
'The standard logging mechanims are used if no directory is set'),
|
||||
)
|
||||
|
||||
default_owner = models.ForeignKey(
|
||||
@ -612,7 +612,7 @@ class Ticket(models.Model):
|
||||
'assigned_to': (template_name2, context),
|
||||
}
|
||||
|
||||
**kwargs are passed to send_templated_mail defined in templated_mail.py
|
||||
**kwargs are passed to send_templated_mail defined in templated_email.py
|
||||
|
||||
returns the set of email addresses the notification was delivered to.
|
||||
|
||||
@ -632,6 +632,7 @@ class Ticket(models.Model):
|
||||
template, context = roles[role]
|
||||
send_templated_mail(template, context, recipient, sender=self.queue.from_address, **kwargs)
|
||||
recipients.add(recipient)
|
||||
|
||||
send('submitter', self.submitter_email)
|
||||
send('ticket_cc', self.queue.updated_ticket_cc)
|
||||
send('new_ticket_cc', self.queue.new_ticket_cc)
|
||||
@ -1517,7 +1518,10 @@ class UserSettings(models.Model):
|
||||
|
||||
email_on_ticket_change = models.BooleanField(
|
||||
verbose_name=_('E-mail me on ticket change?'),
|
||||
help_text=_('If you\'re the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?'),
|
||||
help_text=_(
|
||||
'If you\'re the ticket owner and the ticket is changed via the web by somebody else,'
|
||||
'do you want to receive an e-mail?'
|
||||
),
|
||||
default=email_on_ticket_change_default,
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.db.models import Q
|
||||
from django.core.cache import cache
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from base64 import b64encode
|
||||
@ -135,7 +136,8 @@ class __Query__:
|
||||
if sortreverse:
|
||||
sorting = "-%s" % sorting
|
||||
queryset = queryset.order_by(sorting)
|
||||
return queryset.distinct() # https://stackoverflow.com/questions/30487056/django-queryset-contains-duplicate-entries
|
||||
# https://stackoverflow.com/questions/30487056/django-queryset-contains-duplicate-entries
|
||||
return queryset.distinct()
|
||||
|
||||
def get_cache_key(self):
|
||||
return str(self.huser.user.pk) + ":" + self.base64
|
||||
@ -200,8 +202,13 @@ class __Query__:
|
||||
'start_date': self.mk_timeline_date(followup.date),
|
||||
'text': {
|
||||
'headline': ticket.title + ' - ' + followup.title,
|
||||
'text': (followup.comment if followup.comment else _('No text')) + '<br/> <a href="%s" class="btn" role="button">%s</a>' %
|
||||
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")),
|
||||
'text': (
|
||||
(escape(followup.comment) if followup.comment else _('No text'))
|
||||
+
|
||||
'<br/> <a href="%s" class="btn" role="button">%s</a>'
|
||||
%
|
||||
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket"))
|
||||
),
|
||||
},
|
||||
'group': _('Messages'),
|
||||
}
|
||||
|
@ -79,6 +79,13 @@ HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC
|
||||
# show 'submit a ticket' section on public page?
|
||||
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
|
||||
|
||||
# change that to custom class to have extra fields or validation (like captcha)
|
||||
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
|
||||
settings,
|
||||
"HELPDESK_PUBLIC_TICKET_FORM_CLASS",
|
||||
"helpdesk.forms.PublicTicketForm"
|
||||
)
|
||||
|
||||
|
||||
###################################
|
||||
# options for update_ticket views #
|
||||
@ -89,7 +96,10 @@ HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PU
|
||||
# can be True/False or a callable accepting the active user and returning True if they must be considered helpdesk staff
|
||||
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
|
||||
if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)):
|
||||
warnings.warn("HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.", RuntimeWarning)
|
||||
warnings.warn(
|
||||
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
|
||||
RuntimeWarning
|
||||
)
|
||||
|
||||
# show edit buttons in ticket follow ups.
|
||||
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
|
||||
@ -159,3 +169,13 @@ HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(settings, 'HELPDESK_USE_HTTPS_IN_EMAI
|
||||
HELPDESK_TEAMS_MODEL = getattr(settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team')
|
||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [('pinax_teams', '0004_auto_20170511_0856')])
|
||||
HELPDESK_KBITEM_TEAM_GETTER = getattr(settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team)
|
||||
|
||||
# Include all signatures and forwards in the first ticket message if set
|
||||
# Useful if you get forwards dropped from them while they are useful part of request
|
||||
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
|
||||
|
||||
# If set then we always save incoming emails as .eml attachments
|
||||
# which is quite noisy but very helpful for complicated markup, forwards and so on
|
||||
# (which gets stripped/corrupted otherwise)
|
||||
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False)
|
||||
|
||||
|
10
helpdesk/static/helpdesk/js/init_datetime_classes.js
Normal file
10
helpdesk/static/helpdesk/js/init_datetime_classes.js
Normal file
@ -0,0 +1,10 @@
|
||||
$(() => {
|
||||
$(".date-field").datepicker({dateFormat: 'yy-mm-dd'});
|
||||
});
|
||||
$(() => {
|
||||
$(".datetime-field").datepicker({dateFormat: 'yy-mm-dd 00:00:00'});
|
||||
});
|
||||
$(() => {
|
||||
// TODO: This does not work as written, need to make functional
|
||||
$(".time-field").tooltip="Time format 24hr: 00:00:00";
|
||||
});
|
@ -1,3 +1,3 @@
|
||||
$(() => {
|
||||
$("#id_due_date").datepicker();
|
||||
$("#id_due_date").datepicker({dateFormat: 'yy-mm-dd 00:00:00'});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
from celery import task
|
||||
from celery.decorators import task
|
||||
|
||||
from .email import process_email
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import os
|
||||
import mimetypes
|
||||
import logging
|
||||
from smtplib import SMTPException
|
||||
|
||||
@ -16,7 +15,7 @@ def send_templated_mail(template_name,
|
||||
bcc=None,
|
||||
fail_silently=False,
|
||||
files=None,
|
||||
extra_headers={}):
|
||||
extra_headers=None):
|
||||
"""
|
||||
send_templated_mail() is a wrapper around Django's e-mail routines that
|
||||
allows us to easily send multipart (text/plain & text/html) e-mails using
|
||||
@ -54,6 +53,8 @@ def send_templated_mail(template_name,
|
||||
from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \
|
||||
HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||
|
||||
headers = extra_headers or {}
|
||||
|
||||
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||
|
||||
try:
|
||||
@ -96,7 +97,8 @@ def send_templated_mail(template_name,
|
||||
|
||||
msg = EmailMultiAlternatives(subject_part, text_part,
|
||||
sender or settings.DEFAULT_FROM_EMAIL,
|
||||
recipients, bcc=bcc)
|
||||
recipients, bcc=bcc,
|
||||
headers=headers)
|
||||
msg.attach_alternative(html_part, "text/html")
|
||||
|
||||
if files:
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
{% include 'helpdesk/base-head.html' %}
|
||||
{% block helpdesk_head %}{% endblock %}
|
||||
{% include 'helpdesk/base_js.html' %}
|
||||
|
||||
</head>
|
||||
|
||||
@ -44,8 +45,6 @@
|
||||
|
||||
{% include "helpdesk/debug.html" %}
|
||||
|
||||
|
||||
{% include 'helpdesk/base_js.html' %}
|
||||
{% block helpdesk_js %}{% endblock %}
|
||||
|
||||
</body>
|
||||
|
@ -34,7 +34,7 @@
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
<label for='id_{{ field.name }}'>
|
||||
{% trans field.label %}
|
||||
{{ field.label }}
|
||||
{% if not field.field.required %}
|
||||
({% trans "Optional" %})
|
||||
{% endif %}
|
||||
|
@ -1,58 +1,49 @@
|
||||
{% extends "helpdesk/base.html" %}{% load i18n bootstrap4form %}
|
||||
{% extends "helpdesk/base.html" %}
|
||||
|
||||
{% load i18n bootstrap4form %}
|
||||
|
||||
{% block helpdesk_title %}{% trans "Edit Ticket" %}{% endblock %}
|
||||
|
||||
{% block helpdesk_breadcrumb %}
|
||||
<li class="breadcrumb-item">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'helpdesk:list' %}">{% trans "Tickets" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'helpdesk:list' %}{{ ticket.id }}/">{{ ticket.queue.slug }}-{{ ticket.id }}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active">{% trans "Edit Ticket" %}</li>
|
||||
</li>
|
||||
<li class="breadcrumb-item active">{% trans "Edit Ticket" %}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block helpdesk_body %}
|
||||
<div class="col-xs-6">
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-body"><h2>{% trans "Edit a Ticket" %}</h2>
|
||||
|
||||
<p>{% trans "Unless otherwise stated, all fields are required." %} {% trans "Please provide as descriptive a title and description as possible." %}</p>
|
||||
|
||||
<p><strong>{% trans "Note" %}:</strong> {% blocktrans %}Editing a ticket does <em>not</em> send an e-mail to the ticket owner or submitter. No new details should be entered, this form should only be used to fix incorrect details or clean up the submission.{% endblocktrans %}</p>
|
||||
|
||||
<form method='post' action='./'>
|
||||
<fieldset>
|
||||
<div class="col-xs-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body"><h2>{% trans "Edit a Ticket" %}</h2>
|
||||
<p>
|
||||
{% trans "Unless otherwise stated, all fields are required." %}
|
||||
{% trans "Please provide as descriptive a title and description as possible." %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{% trans "Note" %}:</strong>
|
||||
{% blocktrans %}Editing a ticket does <em>not</em> send an e-mail to the ticket owner or submitter. No new details should be entered, this form should only be used to fix incorrect details or clean up the submission.{% endblocktrans %}
|
||||
</p>
|
||||
{% if errors %}<p class="text-danger">{% for error in errors %}{% trans "Error: " %}{{ error }}<br>{% endfor %}</p>{% endif %}
|
||||
<form method='post'>
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{{ form|bootstrap4form }}
|
||||
{% comment %}
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% else %}
|
||||
<dt><label for='id_{{ field.name }}'>{{ field.label }}</label>{% if not field.field.required %} <span class='form_optional'>{% trans "(Optional)" %}</span>{% endif %}</dt>
|
||||
<dd>{{ field }}</dd>
|
||||
{% if field.errors %}<dd class='error'>{{ field.errors }}</dd>{% endif %}
|
||||
{% if field.help_text %}<dd class='form_help_text'>{{ field.help_text }}</dd>{% endif %}</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dl>
|
||||
{% endcomment %}
|
||||
<div class='buttons form-group'>
|
||||
<input type='submit' class="btn btn-primary btn-sm" value='{% trans "Save Changes" %}' />
|
||||
<a href='{{ ticket.get_absolute_url }}'><button class="btn btn-danger">{% trans "Cancel Changes" %}</button></a>
|
||||
<input type='submit' class="btn btn-primary btn-sm" value='{% trans "Save Changes" %}'/>
|
||||
<a href='{{ ticket.get_absolute_url }}'>
|
||||
<button class="btn btn-danger">{% trans "Cancel Changes" %}</button>
|
||||
</a>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{% csrf_token %}</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$( function() {
|
||||
$( "#id_due_date" ).datepicker({dateFormat: 'yy-mm-dd'});
|
||||
} );
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block helpdesk_js %}
|
||||
{{ form.media.js }}
|
||||
{% endblock %}
|
||||
|
@ -25,7 +25,7 @@
|
||||
<td>{{ ticket.priority }}</td>
|
||||
<td>{{ ticket.queue }}</td>
|
||||
<td>{{ ticket.get_status }}</td>
|
||||
<td><span title='{{ ticket.modified|date:"r" }}'>{{ ticket.modified|naturaltime }}</span></td>
|
||||
<td><span title='{{ ticket.modified|date:"DATETIME_FORMAT" }}'>{{ ticket.modified|naturaltime }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>{% if ticket_list_empty_message %}<td colspan='6'>{{ ticket_list_empty_message }}</td>{% else %}<td colspan='6'>{% trans "You do not have any pending tickets." %}</td>{% endif %}</tr>
|
||||
|
@ -23,7 +23,7 @@
|
||||
<td class="tickettitle"><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}. {{ ticket.title }} </a></td>
|
||||
<td>{{ ticket.priority }}</td>
|
||||
<td>{{ ticket.queue }}</td>
|
||||
<td><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
|
||||
<td><span title='{{ ticket.created|date:"DATETIME_FORMAT" }}'>{{ ticket.created|naturaltime }}</span></td>
|
||||
<td class="text-center">
|
||||
<a href='{{ ticket.get_absolute_url }}?take'><button class='btn btn-primary btn-sm'><i class="fas fa-hand-paper"></i> {% trans "Take" %}</button></a>
|
||||
<a href='{% url 'helpdesk:delete' ticket.id %}?next=dashboard'><button class='btn btn-danger btn-sm'><i class="fas fa-trash"></i> {% trans "Delete" %}</button></a>
|
||||
@ -63,7 +63,7 @@
|
||||
<td class="tickettitle"><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}. {{ ticket.title }} </a></td>
|
||||
<td>{{ ticket.priority }}</td>
|
||||
<td>{{ ticket.queue }}</td>
|
||||
<td><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
|
||||
<td><span title='{{ ticket.created|date:"DATETIME_FORMAT" }}'>{{ ticket.created|naturaltime }}</span></td>
|
||||
<td class="text-center">
|
||||
<a href='{{ ticket.get_absolute_url }}?take'><button class='btn btn-primary btn-sm'><i class="fas fa-hand-paper"></i> {% trans "Take" %}</button></a>
|
||||
<a href='{% url 'helpdesk:delete' ticket.id %}?next=dashboard'><button class='btn btn-danger btn-sm'><i class="fas fa-trash"></i> {% trans "Delete" %}</button></a>
|
||||
|
@ -36,7 +36,7 @@
|
||||
|
||||
{% endif %}
|
||||
<a href='{% if iframe %}{% url 'helpdesk:submit_iframe' %}{% else %}{% url 'helpdesk:submit' %}{%endif%}?{% if category.queue %}queue={{category.queue.pk}};_readonly_fields_=queue;{%endif%}kbitem={{item.id}};{{query_param_string}}' class="col-sm">
|
||||
<div class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</div>
|
||||
<div class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-plus-circle fa-lg"></i> {% trans 'Create New Ticket' %}{% trans " Queue: " %}{{item}}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
@ -57,7 +57,7 @@
|
||||
{% if category.queue %}
|
||||
<a href='{% if iframe %}{% url 'helpdesk:submit_iframe' %}{% else %}{% url 'helpdesk:submit' %}{%endif%}?queue={{category.queue.pk}};_readonly_fields_=queue;{{query_param_string}}'>
|
||||
{% block submit_button %}
|
||||
<div class="btn btn-danger btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</div>
|
||||
<div class="btn btn-danger btn-circle btn-xl float-right"><i class="fa fa-plus-circle fa-lg"></i> {% trans 'Create New Ticket' %}{% if category.queue %}{% trans " Queue: " %}{{category.queue}}{% endif %}</div>
|
||||
{% endblock %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -12,13 +12,13 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans "Submitted On" %}</th>
|
||||
<td>{{ ticket.created|date:"r" }} ({{ ticket.created|naturaltime }})</td>
|
||||
<td>{{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})</td>
|
||||
</tr>
|
||||
|
||||
{% if ticket.due_date %}
|
||||
<tr>
|
||||
<th>{% trans "Due On" %}</th>
|
||||
<td>{{ ticket.due_date|date:"r" }} ({{ ticket.due_date|naturaltime }})</td>
|
||||
<td>{{ ticket.due_date|date:"DATETIME_FORMAT" }} ({{ ticket.due_date|naturaltime }})</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
{% load ticket_to_link %}
|
||||
{% for followup in ticket.followup_set.public_followups %}
|
||||
<div class='followup well'>
|
||||
<div class='title'>{{ followup.title }} <span class='byline text-info'>{% if followup.user %}by {{ followup.user }}{% endif %} <span title='{{ followup.date|date:"r" }}'>{{ followup.date|naturaltime }}</span></span></div>
|
||||
<div class='title'>{{ followup.title }} <span class='byline text-info'>{% if followup.user %}by {{ followup.user }}{% endif %} <span title='{{ followup.date|date:"DATETIME_FORMAT" }}'>{{ followup.date|naturaltime }}</span></span></div>
|
||||
{{ followup.comment|force_escape|urlizetrunc:50|num_to_link|linebreaksbr }}
|
||||
{% if followup.ticketchange_set.all %}<div class='changes'><ul>
|
||||
{% for change in followup.ticketchange_set.all %}
|
||||
|
@ -46,11 +46,11 @@
|
||||
<div class="list-group-item list-group-item-action">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ followup.title|num_to_link }}</h5>
|
||||
<small><i class="fas fa-clock"></i> <span class='byline text-info'>{% if followup.user %}by {{ followup.user }},{% endif %} <span title='{{ followup.date|date:"r" }}'>{{ followup.date|naturaltime }}</span>{% if followup.time_spent %}, <span>{% trans "time spent" %}: {{ followup.time_spent_formated }}</span>{% endif %} {% if not followup.public %} <span class='private'>({% trans "Private" %})</span>{% endif %}</span></small>
|
||||
<small><i class="fas fa-clock"></i> <span class='byline text-info'>{% if followup.user %}by {{ followup.user }},{% endif %} <span title='{{ followup.date|date:"DATETIME_FORMAT" }}'>{{ followup.date|naturaltime }}</span>{% if followup.time_spent %}, <span>{% trans "time spent" %}: {{ followup.time_spent_formated }}</span>{% endif %} {% if not followup.public %} <span class='private'>({% trans "Private" %})</span>{% endif %}</span></small>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
{% if followup.comment %}
|
||||
<p>{{ followup.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</p>
|
||||
<p>{{ followup.get_markdown|urlizetrunc:50|num_to_link }}</p>
|
||||
{% endif %}
|
||||
{% for change in followup.ticketchange_set.all %}
|
||||
{% if forloop.first %}<div class='changes'><ul>{% endif %}
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% load i18n humanize ticket_to_link %}
|
||||
{% load static %}
|
||||
{% load helpdesk_util %}
|
||||
|
||||
<div class="card mb-3">
|
||||
<!--div class="card-header">
|
||||
@ -21,27 +22,32 @@
|
||||
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
|
||||
<tr>
|
||||
<th class="table-secondary">{{ customfield.field.label }}</th>
|
||||
<td>{% ifequal customfield.field.data_type "url" %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>{% else %}{{ customfield.value|default:"" }}{% endifequal %}</td>
|
||||
<td>{% spaceless %}{% if "url" == customfield.field.data_type %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>
|
||||
{% elif "datetime" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
|
||||
{% elif "date" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
|
||||
{% elif "time" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
|
||||
{% else %}{{ customfield.value|default:"" }}
|
||||
{% endif %}{% endspaceless %}</td>
|
||||
</tr>{% endfor %}
|
||||
<tr>
|
||||
<th class="table-active">{% trans "Due Date" %}</th>
|
||||
<td>{{ ticket.due_date|date:"d. E Y" }} {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %}
|
||||
<td>{{ ticket.due_date|date:"DATETIME_FORMAT" }} {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %}
|
||||
</td>
|
||||
<th class="table-active">{% trans "Submitted On" %}</th>
|
||||
<td>{{ ticket.created|date:"d. E Y" }} ({{ ticket.created|naturaltime }})</td>
|
||||
<td>{{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="table-active">{% trans "Assigned To" %}</th>
|
||||
<td>{{ ticket.get_assigned_to }}{% ifequal ticket.get_assigned_to _('Unassigned') %} <strong>
|
||||
<a href='?take'><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-hand-paper"></i></button></a>
|
||||
</strong>{% endifequal %}
|
||||
<td>{{ ticket.get_assigned_to }}{% if _('Unassigned') == ticket.get_assigned_to %} <strong>
|
||||
<a data-toggle="tooltip" href='?take' title='{% trans "Assign this ticket to " %}{{ request.user.email }}'><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-hand-paper"></i></button></a>
|
||||
</strong>{% endif %}
|
||||
</td>
|
||||
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
||||
<td> {{ ticket.submitter_email }}
|
||||
{% if user.is_superuser %} {% if submitter_userprofile_url %}<strong><a href='{{submitter_userprofile_url}}'><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-address-book"></i></button></a></strong>{% endif %}
|
||||
<strong><a href ="{% url 'helpdesk:list'%}?q={{ticket.submitter_email}}">
|
||||
{% if user.is_superuser %} {% if submitter_userprofile_url %}<strong><a data-toggle="tooltip" href='{{submitter_userprofile_url}}' title='{% trans "Edit " %}{{ ticket.submitter_email }}{% trans " user profile" %}'><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-address-book"></i></button></a></strong>{% endif %}
|
||||
<strong><a data-toggle="tooltip" href ="{% url 'helpdesk:list'%}?q={{ticket.submitter_email}}" title='{% trans "Display tickets filtered for " %}{{ ticket.submitter_email }}{% trans " as a keyword" %}'>
|
||||
<button type="button" class="btn btn-primary btn-sm"><i class="fas fa-search"></i></button></a></strong>
|
||||
<strong><a href='{% url 'helpdesk:email_ignore_add' %}?email={{ ticket.submitter_email }}'>
|
||||
<strong><a data-toggle="tooltip" href='{% url 'helpdesk:email_ignore_add' %}?email={{ ticket.submitter_email }}' title='{% trans "Add email address for the ticket system to ignore." %}'>
|
||||
<button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-eye-slash"></i></button></a></strong>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -50,7 +56,7 @@
|
||||
<td class="{% if ticket.priority < 3 %}table-warning{% endif %}">{{ ticket.get_priority_display }}
|
||||
</td>
|
||||
<th class="table-active">{% trans "Copies To" %}</th>
|
||||
<td>{{ ticketcc_string }} <a data-toggle='tooltip' href='{% url 'helpdesk:ticket_cc' ticket.id %}' title='{% trans "Click here to add / remove people who should receive an e-mail whenever this ticket is updated." %}'><strong><button type="button" class="btn btn-warning btn-sm float-right"><i class="fa fa-share"></i></button></strong></a>{% if SHOW_SUBSCRIBE %}, <strong><a data-toggle='tooltip' href='?subscribe' title='{% trans "Click here to subscribe yourself to this ticket, if you want to receive an e-mail whenever this ticket is updated." %}'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-rss-square"></i></button></a></strong>{% endif %}</td>
|
||||
<td>{{ ticketcc_string }} <a data-toggle='tooltip' href='{% url 'helpdesk:ticket_cc' ticket.id %}' title='{% trans "Click here to add / remove people who should receive an e-mail whenever this ticket is updated." %}'><strong><button type="button" class="btn btn-warning btn-sm float-right"><i class="fa fa-share"></i></button></strong></a>{% if SHOW_SUBSCRIBE %} <strong><a data-toggle='tooltip' href='?subscribe' title='{% trans "Click here to subscribe yourself to this ticket, if you want to receive an e-mail whenever this ticket is updated." %}'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-rss-square"></i></button></a></strong>{% endif %}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@ -93,11 +99,11 @@
|
||||
<tr>
|
||||
<td id="ticket-description" colspan='4'>
|
||||
<h4>{% trans "Description" %}</h4>
|
||||
{{ ticket.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</td>
|
||||
{{ ticket.get_markdown|urlizetrunc:50|num_to_link }}</td>
|
||||
</tr>
|
||||
|
||||
{% if ticket.resolution %}<tr>
|
||||
<th colspan='2'>{% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} <a href='?close'><button type="button" class="btn btn-warning btn-sm">{% trans "Accept and Close" %}</button></a>{% endifequal %}</th>
|
||||
<th colspan='2'>{% trans "Resolution" %}{% if "Resolved" == ticket.get_status_display %} <a href='?close'><button type="button" class="btn btn-warning btn-sm">{% trans "Accept and Close" %}</button></a>{% endif %}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan='2'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td>
|
||||
|
@ -1,11 +1,11 @@
|
||||
"""
|
||||
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
||||
|
||||
templatetags/helpdesk_staff.py - The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff.
|
||||
The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff.
|
||||
templatetags/helpdesk_staff.py
|
||||
"""
|
||||
import logging
|
||||
from django.template import Library
|
||||
from django.db.models import Q
|
||||
|
||||
from helpdesk.decorators import is_helpdesk_staff
|
||||
|
||||
@ -18,5 +18,5 @@ register = Library()
|
||||
def helpdesk_staff(user):
|
||||
try:
|
||||
return is_helpdesk_staff(user)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed")
|
||||
|
@ -1,9 +1,35 @@
|
||||
from django import template
|
||||
from django.template import Library
|
||||
from django.template.defaultfilters import date as date_filter
|
||||
from django.conf import settings
|
||||
|
||||
register = template.Library()
|
||||
from datetime import datetime
|
||||
|
||||
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT, CUSTOMFIELD_DATETIME_FORMAT
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def get(value, arg, default=None):
|
||||
""" Call the dictionary get function """
|
||||
return value.get(arg, default)
|
||||
|
||||
|
||||
@register.filter(expects_localtime=True)
|
||||
def datetime_string_format(value):
|
||||
"""
|
||||
:param value: String - Expected to be a datetime, date, or time in specific format
|
||||
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
|
||||
"""
|
||||
try:
|
||||
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
|
||||
except (TypeError, ValueError):
|
||||
# If NoneType return empty string, else return original value
|
||||
new_value = "" if value is None else value
|
||||
return new_value
|
||||
|
27
helpdesk/tests/test_files/forwarded-message.eml
Normal file
27
helpdesk/tests/test_files/forwarded-message.eml
Normal file
@ -0,0 +1,27 @@
|
||||
Return-Path: sender@domain.tld
|
||||
To: recipient@domain.tld
|
||||
From: The Sender <sender@domain.tld>
|
||||
Subject: Test with original message from GitHub
|
||||
Message-ID: <0d3a17d5-7136-75c0-c8ba-a7698c57ac42@gmail.com>
|
||||
Date: Tue, 13 Apr 2021 12:56:39 +0200
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101
|
||||
Thunderbird/78.8.1
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Language: en-US
|
||||
|
||||
This is email body
|
||||
|
||||
-----Original Message-----
|
||||
From: GitHub <noreply@github.com>
|
||||
Sent: Tuesday, 6 April 2021 9:09 AM
|
||||
Subject: [GitHub API] Update notice for access token [SEC=UNOFFICIAL]
|
||||
|
||||
Hello there!
|
||||
|
||||
We noticed that, at April 5th, 2021 at 23:09 (UTC) your application accessed the GitHub API with a token that has an outdated format.
|
||||
|
||||
Thanks,
|
||||
The GitHub Team
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from django.core.management import call_command
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib.auth.models import User
|
||||
@ -84,9 +84,11 @@ class GetEmailCommonTests(TestCase):
|
||||
self.assertEqual(ticket.title, "Testovácí email")
|
||||
self.assertEqual(ticket.description, "íářčšáíéřášč")
|
||||
|
||||
@override_settings(HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL=True)
|
||||
def test_email_with_utf_8_non_decodable_sequences(self):
|
||||
"""
|
||||
Tests that emails with utf-8 non-decodable sequences are parsed correctly
|
||||
The message is fowarded as well
|
||||
"""
|
||||
with open(os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml")) as fd:
|
||||
test_email = fd.read()
|
||||
@ -99,6 +101,20 @@ class GetEmailCommonTests(TestCase):
|
||||
attachment = attachments[0]
|
||||
self.assertIn('prosazuje lepší', attachment.file.read().decode("utf-8"))
|
||||
|
||||
@override_settings(HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL=True)
|
||||
def test_email_with_forwarded_message(self):
|
||||
"""
|
||||
Forwarded message of that format must be still attached correctly
|
||||
"""
|
||||
with open(os.path.join(THIS_DIR, "test_files/forwarded-message.eml")) as fd:
|
||||
test_email = fd.read()
|
||||
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger)
|
||||
self.assertEqual(ticket.title, "Test with original message from GitHub")
|
||||
self.assertIn("This is email body", ticket.description)
|
||||
assert "Hello there!" not in ticket.description, ticket.description
|
||||
assert FollowUp.objects.filter(ticket=ticket).count() == 1
|
||||
assert "Hello there!" in FollowUp.objects.filter(ticket=ticket).first().comment
|
||||
|
||||
|
||||
class GetEmailParametricTemplate(object):
|
||||
"""TestCase that checks basic email functionality across methods and socks configs."""
|
||||
@ -153,9 +169,10 @@ class GetEmailParametricTemplate(object):
|
||||
else:
|
||||
# Test local email reading
|
||||
if self.method == 'local':
|
||||
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
|
||||
with mock.patch('os.listdir') as mocked_listdir, \
|
||||
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=test_email)):
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
|
||||
mock.patch('os.unlink'):
|
||||
mocked_isfile.return_value = True
|
||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||
|
||||
@ -224,9 +241,10 @@ class GetEmailParametricTemplate(object):
|
||||
else:
|
||||
# Test local email reading
|
||||
if self.method == 'local':
|
||||
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
|
||||
with mock.patch('os.listdir') as mocked_listdir, \
|
||||
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
|
||||
mock.patch('builtins.open' if six.PY3 else '__builtin__.open', mock.mock_open(read_data=test_email)):
|
||||
mock.patch('builtins.open' if six.PY3 else '__builtin__.open', mock.mock_open(read_data=test_email)), \
|
||||
mock.patch('os.unlink'):
|
||||
mocked_isfile.return_value = True
|
||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||
|
||||
@ -299,9 +317,10 @@ class GetEmailParametricTemplate(object):
|
||||
else:
|
||||
# Test local email reading
|
||||
if self.method == 'local':
|
||||
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
|
||||
with mock.patch('os.listdir') as mocked_listdir, \
|
||||
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=test_email)):
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
|
||||
mock.patch('os.unlink'):
|
||||
mocked_isfile.return_value = True
|
||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||
|
||||
@ -412,9 +431,10 @@ class GetEmailParametricTemplate(object):
|
||||
else:
|
||||
# Test local email reading
|
||||
if self.method == 'local':
|
||||
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
|
||||
with mock.patch('os.listdir') as mocked_listdir, \
|
||||
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())):
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())), \
|
||||
mock.patch('os.unlink'):
|
||||
mocked_isfile.return_value = True
|
||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||
|
||||
@ -502,9 +522,10 @@ class GetEmailParametricTemplate(object):
|
||||
else:
|
||||
# Test local email reading
|
||||
if self.method == 'local':
|
||||
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
|
||||
with mock.patch('os.listdir') as mocked_listdir, \
|
||||
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=test_email)):
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
|
||||
mock.patch('os.unlink'):
|
||||
mocked_isfile.return_value = True
|
||||
mocked_listdir.return_value = ['filename1']
|
||||
|
||||
@ -550,7 +571,7 @@ class GetEmailParametricTemplate(object):
|
||||
self.assertEqual(followup1.ticket.id, 1)
|
||||
attach1 = get_object_or_404(FollowUpAttachment, pk=1)
|
||||
self.assertEqual(attach1.followup.id, 1)
|
||||
self.assertEqual(attach1.filename, 'signature.asc')
|
||||
self.assertEqual(attach1.filename, 'part-1_signature.asc')
|
||||
self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIcBAEBCAAGBQJaA3dnAAoJELBLc7QPITnLN54P/3Zsu7+AIQWDFTvziJfCqswG
|
||||
@ -670,9 +691,10 @@ class GetEmailCCHandling(TestCase):
|
||||
test_email = "To: queue@example.com\nCc: " + test_email_cc_one + ", " + test_email_cc_one + ", " + test_email_cc_two + ", " + test_email_cc_three + "\nCC: " + test_email_cc_one + ", " + test_email_cc_three + ", " + test_email_cc_four + ", " + ticket_user_emails + "\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body
|
||||
test_mail_len = len(test_email)
|
||||
|
||||
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
|
||||
with mock.patch('os.listdir') as mocked_listdir, \
|
||||
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=test_email)):
|
||||
mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
|
||||
mock.patch('os.unlink'):
|
||||
mocked_isfile.return_value = True
|
||||
mocked_listdir.return_value = ['filename1']
|
||||
|
||||
|
@ -47,7 +47,7 @@ class KBTests(TestCase):
|
||||
self.assertContains(response, 'This is a test category')
|
||||
self.assertContains(response, 'KBItem 1')
|
||||
self.assertContains(response, 'KBItem 2')
|
||||
self.assertContains(response, 'Contact a human')
|
||||
self.assertContains(response, 'Create New Ticket Queue:')
|
||||
self.client.login(username=self.user.get_username(), password='password')
|
||||
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", )))
|
||||
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')
|
||||
|
@ -16,7 +16,8 @@ from helpdesk.decorators import helpdesk_staff_member_required, protect_view
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.views import feeds, staff, public, kb, login
|
||||
try:
|
||||
import helpdesk.tasks
|
||||
# TODO: why is it imported? due to some side-effect or by mistake?
|
||||
import helpdesk.tasks # NOQA
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
@ -67,7 +67,11 @@ class HelpdeskUser:
|
||||
if self.has_full_access():
|
||||
return True
|
||||
else:
|
||||
return helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION and self.user.has_perm(queue.permission_name)
|
||||
return (
|
||||
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
|
||||
and
|
||||
self.user.has_perm(queue.permission_name)
|
||||
)
|
||||
|
||||
def can_access_ticket(self, ticket):
|
||||
"""Check to see if the user has permission to access
|
||||
|
@ -1,5 +1,3 @@
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from helpdesk.models import CustomField, KBItem, Queue
|
||||
|
||||
|
||||
@ -11,8 +9,9 @@ class AbstractCreateTicketMixin():
|
||||
initial_data['queue'] = Queue.objects.get(slug=request.GET.get('queue', None)).id
|
||||
except Queue.DoesNotExist:
|
||||
pass
|
||||
if request.user.is_authenticated and request.user.usersettings_helpdesk.use_email_as_submitter and request.user.email:
|
||||
initial_data['submitter_email'] = request.user.email
|
||||
u = request.user
|
||||
if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email:
|
||||
initial_data['submitter_email'] = u.email
|
||||
|
||||
query_param_fields = ['submitter_email', 'title', 'body', 'queue', 'kbitem']
|
||||
custom_fields = ["custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]
|
||||
|
@ -11,7 +11,14 @@ default_login_view = auth_views.LoginView.as_view(
|
||||
def login(request):
|
||||
login_url = settings.LOGIN_URL
|
||||
# Prevent redirect loop by checking that LOGIN_URL is not this view's name
|
||||
if login_url and (login_url != resolve_url(request.resolver_match.view_name) and (login_url != request.resolver_match.view_name)):
|
||||
condition = (
|
||||
login_url
|
||||
and (
|
||||
login_url != resolve_url(request.resolver_match.view_name)
|
||||
and (login_url != request.resolver_match.view_name)
|
||||
)
|
||||
)
|
||||
if condition:
|
||||
if 'next' in request.GET:
|
||||
return_to = request.GET['next']
|
||||
else:
|
||||
|
@ -7,6 +7,7 @@ views/public.py - All public facing views, eg non-staff (no authentication
|
||||
required) views.
|
||||
"""
|
||||
import logging
|
||||
from importlib import import_module
|
||||
|
||||
from django.core.exceptions import (
|
||||
ObjectDoesNotExist, PermissionDenied, ImproperlyConfigured,
|
||||
@ -26,9 +27,8 @@ from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk.decorators import protect_view, is_helpdesk_staff
|
||||
import helpdesk.views.staff as staff
|
||||
import helpdesk.views.abstract_views as abstract_views
|
||||
from helpdesk.forms import PublicTicketForm
|
||||
from helpdesk.lib import text_is_spam
|
||||
from helpdesk.models import CustomField, Ticket, Queue, UserSettings, KBCategory, KBItem
|
||||
from helpdesk.models import Ticket, Queue, UserSettings
|
||||
from helpdesk.user import huser_from_request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -42,7 +42,17 @@ def create_ticket(request, *args, **kwargs):
|
||||
|
||||
|
||||
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
form_class = PublicTicketForm
|
||||
|
||||
def get_form_class(self):
|
||||
try:
|
||||
the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(".", 1)
|
||||
the_module = import_module(the_module)
|
||||
the_form_class = getattr(the_module, the_form_class)
|
||||
except Exception as e:
|
||||
raise ImproperlyConfigured(
|
||||
f"Invalid custom form class {helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS}"
|
||||
) from e
|
||||
return the_form_class
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
request = self.request
|
||||
@ -109,9 +119,6 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
# if someone enters a non-int string for the ticket
|
||||
return HttpResponseRedirect(reverse('helpdesk:home'))
|
||||
|
||||
def get_success_url(self):
|
||||
request = self.request
|
||||
|
||||
|
||||
class CreateTicketIframeView(BaseCreateTicketView):
|
||||
template_name = 'helpdesk/public_create_ticket_iframe.html'
|
||||
|
@ -24,6 +24,7 @@ from django.utils.html import escape
|
||||
from django.utils import timezone
|
||||
from django.views.generic.edit import FormView, UpdateView
|
||||
|
||||
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT
|
||||
from helpdesk.query import (
|
||||
get_query_class,
|
||||
query_to_base64,
|
||||
@ -75,9 +76,6 @@ else:
|
||||
lambda u: u.is_authenticated and u.is_active and u.is_staff)
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def _get_queue_choices(queues):
|
||||
"""Return list of `choices` array for html form for given queues
|
||||
|
||||
@ -153,16 +151,15 @@ def dashboard(request):
|
||||
# Open Resolved
|
||||
# Queue 1 10 4
|
||||
# Queue 2 4 12
|
||||
|
||||
queues = HelpdeskUser(request.user).get_queues().values_list('id', flat=True)
|
||||
|
||||
from_clause = """FROM helpdesk_ticket t,
|
||||
helpdesk_queue q"""
|
||||
if queues:
|
||||
where_clause = """WHERE q.id = t.queue_id AND
|
||||
q.id IN (%s)""" % (",".join(("%d" % pk for pk in queues)))
|
||||
else:
|
||||
where_clause = """WHERE q.id = t.queue_id"""
|
||||
# code never used (and prone to sql injections)
|
||||
# queues = HelpdeskUser(request.user).get_queues().values_list('id', flat=True)
|
||||
# from_clause = """FROM helpdesk_ticket t,
|
||||
# helpdesk_queue q"""
|
||||
# if queues:
|
||||
# where_clause = """WHERE q.id = t.queue_id AND
|
||||
# q.id IN (%s)""" % (",".join(("%d" % pk for pk in queues)))
|
||||
# else:
|
||||
# where_clause = """WHERE q.id = t.queue_id"""
|
||||
|
||||
# get user assigned tickets page
|
||||
paginator = Paginator(
|
||||
@ -382,6 +379,7 @@ def view_ticket(request, ticket_id):
|
||||
)
|
||||
else:
|
||||
submitter_userprofile_url = None
|
||||
|
||||
return render(request, 'helpdesk/ticket.html', {
|
||||
'ticket': ticket,
|
||||
'submitter_userprofile_url': submitter_userprofile_url,
|
||||
@ -555,7 +553,11 @@ def update_ticket(request, ticket_id, public=False):
|
||||
# broken into two stages to prevent changes from first replace being themselves
|
||||
# changed by the second replace due to conflicting syntax
|
||||
comment = comment.replace('{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM')
|
||||
comment = comment.replace('X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%').replace('X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}')
|
||||
comment = comment.replace(
|
||||
'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%'
|
||||
).replace(
|
||||
'X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}'
|
||||
)
|
||||
# render the neutralized template
|
||||
comment = template_func(comment).render(context)
|
||||
|
||||
@ -592,7 +594,6 @@ def update_ticket(request, ticket_id, public=False):
|
||||
ticket.status = new_status
|
||||
ticket.save()
|
||||
f.new_status = new_status
|
||||
ticket_status_changed = True
|
||||
if f.title:
|
||||
f.title += ' and %s' % ticket.get_status_display()
|
||||
else:
|
||||
@ -702,7 +703,10 @@ def update_ticket(request, ticket_id, public=False):
|
||||
else:
|
||||
template_staff = 'updated_owner'
|
||||
|
||||
if ticket.assigned_to and (ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assigned)):
|
||||
if ticket.assigned_to and (
|
||||
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
|
||||
or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assigned)
|
||||
):
|
||||
messages_sent_to.update(ticket.send(
|
||||
{'assigned_to': (template_staff, context)},
|
||||
dont_send_to=messages_sent_to,
|
||||
@ -1072,7 +1076,6 @@ def ticket_list(request):
|
||||
pass
|
||||
elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse', 'kbitem'}.intersection(request.GET):
|
||||
# Fall-back if no querying is being done
|
||||
all_queues = Queue.objects.all()
|
||||
query_params = deepcopy(default_query_params)
|
||||
else:
|
||||
filter_in_params = [
|
||||
@ -1213,15 +1216,12 @@ def edit_ticket(request, ticket_id):
|
||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||
ticket_perm_check(request, ticket)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = EditTicketForm(request.POST, instance=ticket)
|
||||
form = EditTicketForm(request.POST or None, instance=ticket)
|
||||
if form.is_valid():
|
||||
ticket = form.save()
|
||||
return HttpResponseRedirect(ticket.get_absolute_url())
|
||||
else:
|
||||
form = EditTicketForm(instance=ticket)
|
||||
return redirect(ticket)
|
||||
|
||||
return render(request, 'helpdesk/edit_ticket.html', {'form': form, 'ticket': ticket})
|
||||
return render(request, 'helpdesk/edit_ticket.html', {'form': form, 'ticket': ticket, 'errors': form.errors})
|
||||
|
||||
|
||||
edit_ticket = staff_member_required(edit_ticket)
|
||||
@ -1777,8 +1777,8 @@ def calc_basic_ticket_stats(Tickets):
|
||||
|
||||
date_30 = date_rel_to_today(today, 30)
|
||||
date_60 = date_rel_to_today(today, 60)
|
||||
date_30_str = date_30.strftime('%Y-%m-%d')
|
||||
date_60_str = date_60.strftime('%Y-%m-%d')
|
||||
date_30_str = date_30.strftime(CUSTOMFIELD_DATE_FORMAT)
|
||||
date_60_str = date_60.strftime(CUSTOMFIELD_DATE_FORMAT)
|
||||
|
||||
# > 0 & <= 30
|
||||
ota_le_30 = all_open_tickets.filter(created__gte=date_30_str)
|
||||
|
Loading…
Reference in New Issue
Block a user