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"
|
- "3.8"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO=2.2.16
|
- DJANGO=2.2.23
|
||||||
- DJANGO=3.1.2
|
- DJANGO=3.1.11
|
||||||
|
- DJANGO=3.2.3
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install -q Django==$DJANGO
|
- 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
|
languages other than English, we encourage you to make use of our Transifex
|
||||||
project:
|
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
|
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
|
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,
|
# Instead of showing the public web portal first,
|
||||||
# we can instead redirect users straight to the login page.
|
# we can instead redirect users straight to the login page.
|
||||||
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False
|
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False
|
||||||
LOGIN_URL = '/login/'
|
LOGIN_URL = 'helpdesk:login'
|
||||||
LOGIN_REDIRECT_URL = '/login/'
|
LOGIN_REDIRECT_URL = 'helpdesk:home'
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# - by default, we use SQLite3 for the demo, but you can also
|
# - 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'
|
NAME = 'django-helpdesk-demodesk'
|
||||||
DESCRIPTION = 'A demo Django project using django-helpdesk'
|
DESCRIPTION = 'A demo Django project using django-helpdesk'
|
||||||
README = open(os.path.join(here, 'README.rst')).read()
|
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()
|
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
|
||||||
AUTHOR = 'django-helpdesk team'
|
AUTHOR = 'django-helpdesk team'
|
||||||
URL = 'https://github.com/django-helpdesk/django-helpdesk'
|
URL = 'https://github.com/django-helpdesk/django-helpdesk'
|
||||||
|
@ -81,19 +81,19 @@ These changes are visible throughout django-helpdesk
|
|||||||
- **HELPDESK_EMAIL_FALLBACK_LOCALE** Fallback locale for templated emails when queue locale not found
|
- **HELPDESK_EMAIL_FALLBACK_LOCALE** Fallback locale for templated emails when queue locale not found
|
||||||
|
|
||||||
**Default:** ``HELPDESK_EMAIL_FALLBACK_LOCALE = "en"``
|
**Default:** ``HELPDESK_EMAIL_FALLBACK_LOCALE = "en"``
|
||||||
|
|
||||||
- **HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE** Maximum size, in bytes, of file attachments that will be sent via email
|
- **HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE** Maximum size, in bytes, of file attachments that will be sent via email
|
||||||
|
|
||||||
**Default:** ``HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = 512000``
|
**Default:** ``HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = 512000``
|
||||||
|
|
||||||
- **QUEUE_EMAIL_BOX_UPDATE_ONLY** Only process mail with a valid tracking ID; all other mail will be ignored instead of creating a new ticket.
|
- **QUEUE_EMAIL_BOX_UPDATE_ONLY** Only process mail with a valid tracking ID; all other mail will be ignored instead of creating a new ticket.
|
||||||
|
|
||||||
**Default:** ``QUEUE_EMAIL_BOX_UPDATE_ONLY = False``
|
**Default:** ``QUEUE_EMAIL_BOX_UPDATE_ONLY = False``
|
||||||
|
|
||||||
- **HELPDESK_ANON_ACCESS_RAISES_404** If True, redirects user to a 404 page when attempting to reach ticket pages while not logged in, rather than redirecting to a login screen.
|
- **HELPDESK_ANON_ACCESS_RAISES_404** If True, redirects user to a 404 page when attempting to reach ticket pages while not logged in, rather than redirecting to a login screen.
|
||||||
|
|
||||||
**Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False``
|
**Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False``
|
||||||
|
|
||||||
Options shown on public pages
|
Options shown on public pages
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
@ -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``
|
**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
|
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_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_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.
|
(c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved.
|
||||||
See LICENSE for details.
|
See LICENSE for details.
|
||||||
"""
|
"""
|
||||||
from django.core.exceptions import ValidationError
|
# import base64
|
||||||
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 email
|
import email
|
||||||
from email.header import decode_header
|
|
||||||
from email.utils import getaddresses, parseaddr, collapse_rfc2231_value
|
|
||||||
import imaplib
|
import imaplib
|
||||||
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from os import listdir, unlink
|
import os
|
||||||
from os.path import isfile, join
|
|
||||||
import poplib
|
import poplib
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import timedelta
|
||||||
|
from email.utils import getaddresses
|
||||||
|
from os.path import isfile, join
|
||||||
from time import ctime
|
from time import ctime
|
||||||
from optparse import make_option
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
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
|
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
|
# import User model, which may be a custom model
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -70,37 +64,48 @@ def process_email(quiet=False):
|
|||||||
if q.logging_type in logging_types:
|
if q.logging_type in logging_types:
|
||||||
logger.setLevel(logging_types[q.logging_type])
|
logger.setLevel(logging_types[q.logging_type])
|
||||||
elif not q.logging_type or q.logging_type == 'none':
|
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:
|
if quiet:
|
||||||
logger.propagate = False # do not propagate to root logger that would log to console
|
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:
|
try:
|
||||||
handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log'))
|
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
if not q.email_box_last_check:
|
if not q.email_box_last_check:
|
||||||
q.email_box_last_check = timezone.now() - timedelta(minutes=30)
|
q.email_box_last_check = timezone.now() - timedelta(minutes=30)
|
||||||
|
|
||||||
queue_time_delta = timedelta(minutes=q.email_box_interval or 0)
|
queue_time_delta = timedelta(minutes=q.email_box_interval or 0)
|
||||||
|
|
||||||
if (q.email_box_last_check + queue_time_delta) < timezone.now():
|
if (q.email_box_last_check + queue_time_delta) < timezone.now():
|
||||||
process_queue(q, logger=logger)
|
process_queue(q, logger=logger)
|
||||||
q.email_box_last_check = timezone.now()
|
q.email_box_last_check = timezone.now()
|
||||||
q.save()
|
q.save()
|
||||||
finally:
|
finally:
|
||||||
|
# we must close the file handler correctly if it's created
|
||||||
try:
|
try:
|
||||||
handler.close()
|
if log_file_handler:
|
||||||
|
log_file_handler.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
try:
|
try:
|
||||||
logger.removeHandler(handler)
|
if log_file_handler:
|
||||||
|
logger.removeHandler(log_file_handler)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
|
|
||||||
|
|
||||||
def pop3_sync(q, logger, server):
|
def pop3_sync(q, logger, server):
|
||||||
server.getwelcome()
|
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.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
|
||||||
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
|
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):
|
def imap_sync(q, logger, server):
|
||||||
try:
|
try:
|
||||||
|
try:
|
||||||
|
server.starttls()
|
||||||
|
except Exception:
|
||||||
|
logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
|
||||||
server.login(q.email_box_user or
|
server.login(q.email_box_user or
|
||||||
settings.QUEUE_EMAIL_BOX_USER,
|
settings.QUEUE_EMAIL_BOX_USER,
|
||||||
q.email_box_pass or
|
q.email_box_pass or
|
||||||
settings.QUEUE_EMAIL_BOX_PASSWORD)
|
settings.QUEUE_EMAIL_BOX_PASSWORD)
|
||||||
server.select(q.email_box_imap_folder)
|
server.select(q.email_box_imap_folder)
|
||||||
except imaplib.IMAP4.abort:
|
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()
|
server.logout()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
except ssl.SSLError:
|
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()
|
server.logout()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
@ -171,7 +186,10 @@ def imap_sync(q, logger, server):
|
|||||||
else:
|
else:
|
||||||
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num)
|
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num)
|
||||||
except imaplib.IMAP4.error:
|
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.expunge()
|
||||||
server.close()
|
server.close()
|
||||||
@ -243,7 +261,7 @@ def process_queue(q, logger):
|
|||||||
|
|
||||||
elif email_box_type == 'local':
|
elif email_box_type == 'local':
|
||||||
mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/'
|
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))
|
||||||
|
|
||||||
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')
|
full_message = encoding.force_text(f.read(), errors='replace')
|
||||||
ticket = object_from_message(message=full_message, queue=q, logger=logger)
|
ticket = object_from_message(message=full_message, queue=q, logger=logger)
|
||||||
if ticket:
|
if ticket:
|
||||||
logger.info("Successfully processed message %d, ticket/comment created." % i)
|
logger.info("Successfully processed message %d, ticket/comment created.", i)
|
||||||
try:
|
try:
|
||||||
unlink(m) # delete message file if ticket was successful
|
os.unlink(m) # delete message file if ticket was successful
|
||||||
except OSError:
|
except OSError as e:
|
||||||
logger.error("Unable to delete message %d." % i)
|
logger.error("Unable to delete message %d (%s).", i, str(e))
|
||||||
else:
|
else:
|
||||||
logger.info("Successfully deleted message %d." % i)
|
logger.info("Successfully deleted message %d.", i)
|
||||||
else:
|
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):
|
def decodeUnknown(charset, string):
|
||||||
@ -277,7 +295,26 @@ def decodeUnknown(charset, string):
|
|||||||
|
|
||||||
def decode_mail_headers(string):
|
def decode_mail_headers(string):
|
||||||
decoded = email.header.decode_header(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):
|
def create_ticket_cc(ticket, cc_list):
|
||||||
@ -305,7 +342,7 @@ def create_ticket_cc(ticket, cc_list):
|
|||||||
try:
|
try:
|
||||||
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email)
|
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email)
|
||||||
new_ticket_ccs.append(ticket_cc)
|
new_ticket_ccs.append(ticket_cc)
|
||||||
except ValidationError as err:
|
except ValidationError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return new_ticket_ccs
|
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))
|
logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id))
|
||||||
|
|
||||||
new = True
|
new = True
|
||||||
update = ''
|
|
||||||
|
|
||||||
# Old issue being re-opened
|
# Old issue being re-opened
|
||||||
elif ticket.status == Ticket.CLOSED_STATUS:
|
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}),
|
title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
|
||||||
date=now,
|
date=now,
|
||||||
public=True,
|
public=True,
|
||||||
comment=payload['body'],
|
comment=payload.get('full_body', payload['body']) or "",
|
||||||
message_id=message_id
|
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)
|
attached = process_attachments(f, files)
|
||||||
for att_file in attached:
|
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)
|
context = safe_template_context(ticket)
|
||||||
|
|
||||||
@ -402,33 +441,44 @@ 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)
|
ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True)
|
||||||
|
|
||||||
for email in ticket_cc_list:
|
for email_address in ticket_cc_list:
|
||||||
notifications_to_be_sent.append(email)
|
notifications_to_be_sent.append(email_address)
|
||||||
|
|
||||||
# send mail to appropriate people now depending on what objects
|
autoreply = is_autoreply(message)
|
||||||
# were created and who was CC'd
|
if autoreply:
|
||||||
if new:
|
logger.info("Message seems to be auto-reply, not sending any emails back to the sender")
|
||||||
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},
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
context.update(comment=f.comment)
|
# send mail to appropriate people now depending on what objects
|
||||||
ticket.send(
|
# were created and who was CC'd
|
||||||
{'submitter': ('newticket_submitter', context),
|
# Add auto-reply headers because it's an auto-reply and we must
|
||||||
'assigned_to': ('updated_owner', context)},
|
extra_headers = {
|
||||||
fail_silently=True,
|
'In-Reply-To': message_id,
|
||||||
extra_headers={'In-Reply-To': message_id},
|
"Auto-Submitted": "auto-replied",
|
||||||
)
|
"X-Auto-Response-Suppress": "All",
|
||||||
if queue.enable_notifications_on_email_events:
|
"Precedence": "auto_reply",
|
||||||
|
}
|
||||||
|
if new:
|
||||||
ticket.send(
|
ticket.send(
|
||||||
{'ticket_cc': ('updated_cc', context)},
|
{'submitter': ('newticket_submitter', context),
|
||||||
|
'new_ticket_cc': ('newticket_cc', context),
|
||||||
|
'ticket_cc': ('newticket_cc', context)},
|
||||||
fail_silently=True,
|
fail_silently=True,
|
||||||
extra_headers={'In-Reply-To': message_id},
|
extra_headers=extra_headers,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
context.update(comment=f.comment)
|
||||||
|
ticket.send(
|
||||||
|
{'submitter': ('newticket_submitter', context),
|
||||||
|
'assigned_to': ('updated_owner', context)},
|
||||||
|
fail_silently=True,
|
||||||
|
extra_headers=extra_headers,
|
||||||
|
)
|
||||||
|
if queue.enable_notifications_on_email_events:
|
||||||
|
ticket.send(
|
||||||
|
{'ticket_cc': ('updated_cc', context)},
|
||||||
|
fail_silently=True,
|
||||||
|
extra_headers=extra_headers,
|
||||||
|
)
|
||||||
|
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
@ -445,6 +495,7 @@ def object_from_message(message, queue, logger):
|
|||||||
|
|
||||||
sender = message.get('from', _('Unknown Sender'))
|
sender = message.get('from', _('Unknown Sender'))
|
||||||
sender = decode_mail_headers(decodeUnknown(message.get_charset(), 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
|
# 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,
|
# double quotes by using replace() on the email string. Then,
|
||||||
# take first item of list, second item of tuple is the actual email address.
|
# 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.
|
# correctly. Not ideal, but this seems to work for now.
|
||||||
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1]
|
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1]
|
||||||
|
|
||||||
body_plain, body_html = '', ''
|
|
||||||
|
|
||||||
cc = message.get_all('cc', None)
|
cc = message.get_all('cc', None)
|
||||||
if cc:
|
if cc:
|
||||||
# first, fixup the encoding if necessary
|
# first, fixup the encoding if necessary
|
||||||
@ -484,6 +533,7 @@ def object_from_message(message, queue, logger):
|
|||||||
ticket = None
|
ticket = None
|
||||||
|
|
||||||
body = None
|
body = None
|
||||||
|
full_body = None
|
||||||
counter = 0
|
counter = 0
|
||||||
files = []
|
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':
|
if part['Content-Transfer-Encoding'] == '8bit' and part.get_content_charset() == 'utf-8':
|
||||||
body = body.decode('unicode_escape')
|
body = body.decode('unicode_escape')
|
||||||
body = decodeUnknown(part.get_content_charset(), body)
|
body = decodeUnknown(part.get_content_charset(), body)
|
||||||
body = EmailReplyParser.parse_reply(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
|
# workaround to get unicode text out rather than escaped text
|
||||||
try:
|
try:
|
||||||
body = body.encode('ascii').decode('unicode_escape')
|
body = body.encode('ascii').decode('unicode_escape')
|
||||||
@ -515,13 +577,23 @@ def object_from_message(message, queue, logger):
|
|||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
email_body = encoding.smart_text(part.get_payload(decode=False))
|
email_body = encoding.smart_text(part.get_payload(decode=False))
|
||||||
|
|
||||||
payload = """
|
if not body and not full_body:
|
||||||
<html>
|
# no text has been parsed so far - try such deep parsing for some messages
|
||||||
<head>
|
altered_body = email_body.replace("</p>", "</p>\n").replace("<br", "\n<br")
|
||||||
<meta charset="utf-8"/>
|
mail = BeautifulSoup(str(altered_body), "html.parser")
|
||||||
</head>
|
full_body = mail.get_text()
|
||||||
%s
|
|
||||||
</html>""" % email_body
|
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(
|
files.append(
|
||||||
SimpleUploadedFile(_("email_html_body.html"), payload.encode("utf-8"), 'text/html')
|
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:
|
if not name:
|
||||||
ext = mimetypes.guess_extension(part.get_content_type())
|
ext = mimetypes.guess_extension(part.get_content_type())
|
||||||
name = "part-%i%s" % (counter, ext)
|
name = "part-%i%s" % (counter, ext)
|
||||||
payload = part.get_payload()
|
else:
|
||||||
if isinstance(payload, list):
|
name = ("part-%i_" % counter) + name
|
||||||
payload = payload.pop().as_string()
|
|
||||||
payloadToWrite = payload
|
# # FIXME: this code gets the paylods, then does something with it and then completely ignores it
|
||||||
# check version of python to ensure use of only the correct error type
|
# # writing the part.get_payload(decode=True) instead; and then the payload variable is
|
||||||
non_b64_err = TypeError
|
# # replaced by some dict later.
|
||||||
try:
|
# # the `payloadToWrite` has been also ignored so was commented
|
||||||
logger.debug("Try to base64 decode the attachment payload")
|
# payload = part.get_payload()
|
||||||
payloadToWrite = base64.decodebytes(payload)
|
# if isinstance(payload, list):
|
||||||
except non_b64_err:
|
# payload = payload.pop().as_string()
|
||||||
logger.debug("Payload was not base64 encoded, using raw bytes")
|
# # payloadToWrite = payload
|
||||||
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]))
|
files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0]))
|
||||||
logger.debug("Found MIME attachment %s" % name)
|
logger.debug("Found MIME attachment %s" % name)
|
||||||
|
|
||||||
@ -553,11 +632,25 @@ def object_from_message(message, queue, logger):
|
|||||||
if beautiful_body:
|
if beautiful_body:
|
||||||
try:
|
try:
|
||||||
body = beautiful_body.text
|
body = beautiful_body.text
|
||||||
|
full_body = body
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
if not body:
|
if not body:
|
||||||
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_priority = message.get('priority', '')
|
||||||
smtp_importance = message.get('importance', '')
|
smtp_importance = message.get('importance', '')
|
||||||
high_priority_types = {'high', 'important', '1', 'urgent'}
|
high_priority_types = {'high', 'important', '1', 'urgent'}
|
||||||
@ -565,6 +658,7 @@ def object_from_message(message, queue, logger):
|
|||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'body': body,
|
'body': body,
|
||||||
|
'full_body': full_body or body,
|
||||||
'subject': subject,
|
'subject': subject,
|
||||||
'queue': queue,
|
'queue': queue,
|
||||||
'sender_email': sender_email,
|
'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.
|
tickets.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime, date, time
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -35,6 +36,10 @@ CUSTOMFIELD_TO_FIELD_DICT = {
|
|||||||
'slug': forms.SlugField,
|
'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):
|
class CustomFieldMixin(object):
|
||||||
"""
|
"""
|
||||||
@ -71,8 +76,14 @@ class CustomFieldMixin(object):
|
|||||||
# Try to use the immediate equivalences dictionary
|
# Try to use the immediate equivalences dictionary
|
||||||
try:
|
try:
|
||||||
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
|
||||||
# Change widget in case it is a boolean
|
# Change widgets for the following classes
|
||||||
if fieldclass == forms.BooleanField:
|
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'})
|
instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'})
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -88,18 +99,38 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
|||||||
model = Ticket
|
model = Ticket
|
||||||
exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to')
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Add any custom fields that are defined to the form
|
Add any custom fields that are defined to the form
|
||||||
"""
|
"""
|
||||||
super(EditTicketForm, self).__init__(*args, **kwargs)
|
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():
|
for field in CustomField.objects.all():
|
||||||
|
initial_value = None
|
||||||
try:
|
try:
|
||||||
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field)
|
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field)
|
||||||
initial_value = current_value.value
|
initial_value = current_value.value
|
||||||
except TicketCustomFieldValue.DoesNotExist:
|
# Attempt to convert from fixed format string to date/time data type
|
||||||
initial_value = None
|
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 = {
|
instanceargs = {
|
||||||
'label': field.label,
|
'label': field.label,
|
||||||
'help_text': field.help_text,
|
'help_text': field.help_text,
|
||||||
@ -119,7 +150,16 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
|
|||||||
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
|
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
|
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
|
||||||
cfv.value = value
|
|
||||||
|
# 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()
|
cfv.save()
|
||||||
|
|
||||||
return super(EditTicketForm, self).save(*args, **kwargs)
|
return super(EditTicketForm, self).save(*args, **kwargs)
|
||||||
@ -175,7 +215,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|||||||
due_date = forms.DateTimeField(
|
due_date = forms.DateTimeField(
|
||||||
widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}),
|
||||||
required=False,
|
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'),
|
label=_('Due on'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -187,7 +227,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Media:
|
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):
|
def __init__(self, kbcategory=None, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -8,13 +8,11 @@ lib.py - Common functions (eg multipart e-mail)
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.encoding import smart_text, smart_str
|
from django.utils.encoding import smart_text
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
from helpdesk.models import FollowUpAttachment, EmailTemplate
|
from helpdesk.models import FollowUpAttachment
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('helpdesk')
|
logger = logging.getLogger('helpdesk')
|
||||||
|
@ -502,7 +502,7 @@ msgstr "Složka pro log soubory"
|
|||||||
#: third_party/django-helpdesk/helpdesk/models.py:306
|
#: third_party/django-helpdesk/helpdesk/models.py:306
|
||||||
msgid ""
|
msgid ""
|
||||||
"If logging is enabled, what directory should we use to store log files for "
|
"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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: third_party/django-helpdesk/helpdesk/models.py:317
|
#: 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
|
#: .\models.py:308
|
||||||
msgid ""
|
msgid ""
|
||||||
"If logging is enabled, what directory should we use to store log files for "
|
"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 ""
|
msgstr ""
|
||||||
"Si les logs sont activés, quel dossier doit être utilisé pour stocker les "
|
"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 /"
|
"fichiers de logs pour cette file?"
|
||||||
"var/log/helpdesk/ par défaut"
|
|
||||||
|
|
||||||
#: .\models.py:319
|
#: .\models.py:319
|
||||||
msgid "Default owner"
|
msgid "Default owner"
|
||||||
|
@ -531,13 +531,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: models.py:247
|
#: models.py:247
|
||||||
msgid "Logging Directory"
|
msgid "Logging Directory"
|
||||||
msgstr ""
|
msgstr "Директория логов"
|
||||||
|
|
||||||
#: models.py:251
|
#: models.py:251
|
||||||
msgid ""
|
msgid ""
|
||||||
"If logging is enabled, what directory should we use to store log files for "
|
"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 ""
|
msgstr ""
|
||||||
|
"Директория в которую будут сохраняться файлы с логами; стандартная конфигурация "
|
||||||
|
"используется если ничего не указано"
|
||||||
|
|
||||||
#: models.py:261
|
#: models.py:261
|
||||||
msgid "Default owner"
|
msgid "Default owner"
|
||||||
|
@ -470,7 +470,7 @@ msgstr ""
|
|||||||
#: models.py:308
|
#: models.py:308
|
||||||
msgid ""
|
msgid ""
|
||||||
"If logging is enabled, what directory should we use to store log files for "
|
"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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: models.py:319
|
#: models.py:319
|
||||||
|
@ -13,7 +13,6 @@ from django.core.management.base import BaseCommand
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from helpdesk.models import UserSettings
|
from helpdesk.models import UserSettings
|
||||||
from helpdesk.settings import DEFAULT_USER_SETTINGS
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name='queue',
|
||||||
name='logging_dir',
|
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(
|
migrations.AddField(
|
||||||
model_name='queue',
|
model_name='queue',
|
||||||
|
@ -305,7 +305,7 @@ class Queue(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text=_('If logging is enabled, what directory should we use to '
|
help_text=_('If logging is enabled, what directory should we use to '
|
||||||
'store log files for this queue? '
|
'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(
|
default_owner = models.ForeignKey(
|
||||||
@ -612,7 +612,7 @@ class Ticket(models.Model):
|
|||||||
'assigned_to': (template_name2, context),
|
'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.
|
returns the set of email addresses the notification was delivered to.
|
||||||
|
|
||||||
@ -632,6 +632,7 @@ class Ticket(models.Model):
|
|||||||
template, context = roles[role]
|
template, context = roles[role]
|
||||||
send_templated_mail(template, context, recipient, sender=self.queue.from_address, **kwargs)
|
send_templated_mail(template, context, recipient, sender=self.queue.from_address, **kwargs)
|
||||||
recipients.add(recipient)
|
recipients.add(recipient)
|
||||||
|
|
||||||
send('submitter', self.submitter_email)
|
send('submitter', self.submitter_email)
|
||||||
send('ticket_cc', self.queue.updated_ticket_cc)
|
send('ticket_cc', self.queue.updated_ticket_cc)
|
||||||
send('new_ticket_cc', self.queue.new_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(
|
email_on_ticket_change = models.BooleanField(
|
||||||
verbose_name=_('E-mail me on ticket change?'),
|
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,
|
default=email_on_ticket_change_default,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
@ -135,7 +136,8 @@ class __Query__:
|
|||||||
if sortreverse:
|
if sortreverse:
|
||||||
sorting = "-%s" % sorting
|
sorting = "-%s" % sorting
|
||||||
queryset = queryset.order_by(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):
|
def get_cache_key(self):
|
||||||
return str(self.huser.user.pk) + ":" + self.base64
|
return str(self.huser.user.pk) + ":" + self.base64
|
||||||
@ -200,8 +202,13 @@ class __Query__:
|
|||||||
'start_date': self.mk_timeline_date(followup.date),
|
'start_date': self.mk_timeline_date(followup.date),
|
||||||
'text': {
|
'text': {
|
||||||
'headline': ticket.title + ' - ' + followup.title,
|
'headline': ticket.title + ' - ' + followup.title,
|
||||||
'text': (followup.comment if followup.comment else _('No text')) + '<br/> <a href="%s" class="btn" role="button">%s</a>' %
|
'text': (
|
||||||
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")),
|
(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'),
|
'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?
|
# show 'submit a ticket' section on public page?
|
||||||
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
|
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 #
|
# 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
|
# 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)
|
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)):
|
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.
|
# show edit buttons in ticket follow ups.
|
||||||
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
|
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_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_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)
|
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
|
from .email import process_email
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
import mimetypes
|
|
||||||
import logging
|
import logging
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
|
||||||
@ -16,7 +15,7 @@ def send_templated_mail(template_name,
|
|||||||
bcc=None,
|
bcc=None,
|
||||||
fail_silently=False,
|
fail_silently=False,
|
||||||
files=None,
|
files=None,
|
||||||
extra_headers={}):
|
extra_headers=None):
|
||||||
"""
|
"""
|
||||||
send_templated_mail() is a wrapper around Django's e-mail routines that
|
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
|
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, \
|
from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \
|
||||||
HELPDESK_EMAIL_FALLBACK_LOCALE
|
HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||||
|
|
||||||
|
headers = extra_headers or {}
|
||||||
|
|
||||||
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
|
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -96,7 +97,8 @@ def send_templated_mail(template_name,
|
|||||||
|
|
||||||
msg = EmailMultiAlternatives(subject_part, text_part,
|
msg = EmailMultiAlternatives(subject_part, text_part,
|
||||||
sender or settings.DEFAULT_FROM_EMAIL,
|
sender or settings.DEFAULT_FROM_EMAIL,
|
||||||
recipients, bcc=bcc)
|
recipients, bcc=bcc,
|
||||||
|
headers=headers)
|
||||||
msg.attach_alternative(html_part, "text/html")
|
msg.attach_alternative(html_part, "text/html")
|
||||||
|
|
||||||
if files:
|
if files:
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
{% include 'helpdesk/base-head.html' %}
|
{% include 'helpdesk/base-head.html' %}
|
||||||
{% block helpdesk_head %}{% endblock %}
|
{% block helpdesk_head %}{% endblock %}
|
||||||
|
{% include 'helpdesk/base_js.html' %}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -44,8 +45,6 @@
|
|||||||
|
|
||||||
{% include "helpdesk/debug.html" %}
|
{% include "helpdesk/debug.html" %}
|
||||||
|
|
||||||
|
|
||||||
{% include 'helpdesk/base_js.html' %}
|
|
||||||
{% block helpdesk_js %}{% endblock %}
|
{% block helpdesk_js %}{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for='id_{{ field.name }}'>
|
<label for='id_{{ field.name }}'>
|
||||||
{% trans field.label %}
|
{{ field.label }}
|
||||||
{% if not field.field.required %}
|
{% if not field.field.required %}
|
||||||
({% trans "Optional" %})
|
({% trans "Optional" %})
|
||||||
{% endif %}
|
{% 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_title %}{% trans "Edit Ticket" %}{% endblock %}
|
||||||
|
|
||||||
{% block helpdesk_breadcrumb %}
|
{% block helpdesk_breadcrumb %}
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
<a href="{% url 'helpdesk:list' %}">{% trans "Tickets" %}</a>
|
<a href="{% url 'helpdesk:list' %}">{% trans "Tickets" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
<a href="{% url 'helpdesk:list' %}{{ ticket.id }}/">{{ ticket.queue.slug }}-{{ ticket.id }}</a>
|
<a href="{% url 'helpdesk:list' %}{{ ticket.id }}/">{{ ticket.queue.slug }}-{{ ticket.id }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item active">{% trans "Edit Ticket" %}</li>
|
<li class="breadcrumb-item active">{% trans "Edit Ticket" %}</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block helpdesk_body %}
|
{% block helpdesk_body %}
|
||||||
<div class="col-xs-6">
|
<div class="col-xs-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body"><h2>{% trans "Edit a Ticket" %}</h2>
|
||||||
<div class="panel-body"><h2>{% trans "Edit a Ticket" %}</h2>
|
<p>
|
||||||
|
{% trans "Unless otherwise stated, all fields are required." %}
|
||||||
<p>{% trans "Unless otherwise stated, all fields are required." %} {% trans "Please provide as descriptive a title and description as possible." %}</p>
|
{% 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>
|
<p>
|
||||||
|
<strong>{% trans "Note" %}:</strong>
|
||||||
<form method='post' action='./'>
|
{% 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 %}
|
||||||
<fieldset>
|
</p>
|
||||||
{{ form|bootstrap4form }}
|
{% if errors %}<p class="text-danger">{% for error in errors %}{% trans "Error: " %}{{ error }}<br>{% endfor %}</p>{% endif %}
|
||||||
{% comment %}
|
<form method='post'>
|
||||||
{% for field in form %}
|
{% csrf_token %}
|
||||||
{% if field.is_hidden %}
|
<fieldset>
|
||||||
{{ field }}
|
{{ form|bootstrap4form }}
|
||||||
{% else %}
|
<div class='buttons form-group'>
|
||||||
<dt><label for='id_{{ field.name }}'>{{ field.label }}</label>{% if not field.field.required %} <span class='form_optional'>{% trans "(Optional)" %}</span>{% endif %}</dt>
|
<input type='submit' class="btn btn-primary btn-sm" value='{% trans "Save Changes" %}'/>
|
||||||
<dd>{{ field }}</dd>
|
<a href='{{ ticket.get_absolute_url }}'>
|
||||||
{% if field.errors %}<dd class='error'>{{ field.errors }}</dd>{% endif %}
|
<button class="btn btn-danger">{% trans "Cancel Changes" %}</button>
|
||||||
{% if field.help_text %}<dd class='form_help_text'>{{ field.help_text }}</dd>{% endif %}</label>
|
</a>
|
||||||
{% endif %}
|
</div>
|
||||||
{% endfor %}
|
</fieldset>
|
||||||
</dl>
|
</form>
|
||||||
{% endcomment %}
|
</div>
|
||||||
<div class='buttons form-group'>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
</fieldset>
|
{% endblock %}
|
||||||
|
|
||||||
{% csrf_token %}</form>
|
{% block helpdesk_js %}
|
||||||
</div>
|
{{ form.media.js }}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
$( function() {
|
|
||||||
$( "#id_due_date" ).datepicker({dateFormat: 'yy-mm-dd'});
|
|
||||||
} );
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<td>{{ ticket.priority }}</td>
|
<td>{{ ticket.priority }}</td>
|
||||||
<td>{{ ticket.queue }}</td>
|
<td>{{ ticket.queue }}</td>
|
||||||
<td>{{ ticket.get_status }}</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>
|
</tr>
|
||||||
{% empty %}
|
{% 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>
|
<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 class="tickettitle"><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}. {{ ticket.title }} </a></td>
|
||||||
<td>{{ ticket.priority }}</td>
|
<td>{{ ticket.priority }}</td>
|
||||||
<td>{{ ticket.queue }}</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">
|
<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='{{ 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>
|
<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 class="tickettitle"><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}. {{ ticket.title }} </a></td>
|
||||||
<td>{{ ticket.priority }}</td>
|
<td>{{ ticket.priority }}</td>
|
||||||
<td>{{ ticket.queue }}</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">
|
<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='{{ 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>
|
<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 %}
|
{% 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">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -57,7 +57,7 @@
|
|||||||
{% if category.queue %}
|
{% 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}}'>
|
<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 %}
|
{% 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 %}
|
{% endblock %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -12,13 +12,13 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Submitted On" %}</th>
|
<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>
|
</tr>
|
||||||
|
|
||||||
{% if ticket.due_date %}
|
{% if ticket.due_date %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Due On" %}</th>
|
<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>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -67,7 +67,7 @@
|
|||||||
{% load ticket_to_link %}
|
{% load ticket_to_link %}
|
||||||
{% for followup in ticket.followup_set.public_followups %}
|
{% for followup in ticket.followup_set.public_followups %}
|
||||||
<div class='followup well'>
|
<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 }}
|
{{ followup.comment|force_escape|urlizetrunc:50|num_to_link|linebreaksbr }}
|
||||||
{% if followup.ticketchange_set.all %}<div class='changes'><ul>
|
{% if followup.ticketchange_set.all %}<div class='changes'><ul>
|
||||||
{% for change in followup.ticketchange_set.all %}
|
{% for change in followup.ticketchange_set.all %}
|
||||||
|
@ -46,11 +46,11 @@
|
|||||||
<div class="list-group-item list-group-item-action">
|
<div class="list-group-item list-group-item-action">
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h5 class="mb-1">{{ followup.title|num_to_link }}</h5>
|
<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>
|
</div>
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
{% if followup.comment %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% for change in followup.ticketchange_set.all %}
|
{% for change in followup.ticketchange_set.all %}
|
||||||
{% if forloop.first %}<div class='changes'><ul>{% endif %}
|
{% if forloop.first %}<div class='changes'><ul>{% endif %}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% load i18n humanize ticket_to_link %}
|
{% load i18n humanize ticket_to_link %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load helpdesk_util %}
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<!--div class="card-header">
|
<!--div class="card-header">
|
||||||
@ -9,7 +10,7 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-border">
|
<table class="table table-sm table-border">
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
<tr class=''><th colspan='4'><h3>{{ ticket.queue.slug }}-{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</h3>
|
<tr class=''><th colspan='4'><h3>{{ ticket.queue.slug }}-{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</h3>
|
||||||
{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %}
|
{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %}
|
||||||
<span class='ticket_toolbar float-right'>
|
<span class='ticket_toolbar float-right'>
|
||||||
<a href="{% url 'helpdesk:edit' ticket.id %}" class="ticket-edit"><button class="btn btn-warning btn-sm"><i class="fas fa-pencil-alt"></i> {% trans "Edit" %}</button></a>
|
<a href="{% url 'helpdesk:edit' ticket.id %}" class="ticket-edit"><button class="btn btn-warning btn-sm"><i class="fas fa-pencil-alt"></i> {% trans "Edit" %}</button></a>
|
||||||
@ -21,27 +22,32 @@
|
|||||||
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
|
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-secondary">{{ customfield.field.label }}</th>
|
<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>{% endfor %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-active">{% trans "Due Date" %}</th>
|
<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>
|
</td>
|
||||||
<th class="table-active">{% trans "Submitted On" %}</th>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-active">{% trans "Assigned To" %}</th>
|
<th class="table-active">{% trans "Assigned To" %}</th>
|
||||||
<td>{{ ticket.get_assigned_to }}{% ifequal ticket.get_assigned_to _('Unassigned') %} <strong>
|
<td>{{ ticket.get_assigned_to }}{% if _('Unassigned') == ticket.get_assigned_to %} <strong>
|
||||||
<a href='?take'><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-hand-paper"></i></button></a>
|
<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>{% endifequal %}
|
</strong>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
|
||||||
<td> {{ ticket.submitter_email }}
|
<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 %}
|
{% 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 href ="{% url 'helpdesk:list'%}?q={{ticket.submitter_email}}">
|
<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>
|
<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 %}
|
<button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-eye-slash"></i></button></a></strong>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -50,7 +56,7 @@
|
|||||||
<td class="{% if ticket.priority < 3 %}table-warning{% endif %}">{{ ticket.get_priority_display }}
|
<td class="{% if ticket.priority < 3 %}table-warning{% endif %}">{{ ticket.get_priority_display }}
|
||||||
</td>
|
</td>
|
||||||
<th class="table-active">{% trans "Copies To" %}</th>
|
<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>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
@ -93,11 +99,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td id="ticket-description" colspan='4'>
|
<td id="ticket-description" colspan='4'>
|
||||||
<h4>{% trans "Description" %}</h4>
|
<h4>{% trans "Description" %}</h4>
|
||||||
{{ ticket.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</td>
|
{{ ticket.get_markdown|urlizetrunc:50|num_to_link }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% if ticket.resolution %}<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan='2'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td>
|
<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.
|
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
|
import logging
|
||||||
from django.template import Library
|
from django.template import Library
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from helpdesk.decorators import is_helpdesk_staff
|
from helpdesk.decorators import is_helpdesk_staff
|
||||||
|
|
||||||
@ -18,5 +18,5 @@ register = Library()
|
|||||||
def helpdesk_staff(user):
|
def helpdesk_staff(user):
|
||||||
try:
|
try:
|
||||||
return is_helpdesk_staff(user)
|
return is_helpdesk_staff(user)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed")
|
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
|
@register.filter
|
||||||
def get(value, arg, default=None):
|
def get(value, arg, default=None):
|
||||||
""" Call the dictionary get function """
|
""" Call the dictionary get function """
|
||||||
return value.get(arg, default)
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.test import TestCase
|
from django.test import TestCase, override_settings
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -84,9 +84,11 @@ class GetEmailCommonTests(TestCase):
|
|||||||
self.assertEqual(ticket.title, "Testovácí email")
|
self.assertEqual(ticket.title, "Testovácí email")
|
||||||
self.assertEqual(ticket.description, "íářčšáíéřášč")
|
self.assertEqual(ticket.description, "íářčšáíéřášč")
|
||||||
|
|
||||||
|
@override_settings(HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL=True)
|
||||||
def test_email_with_utf_8_non_decodable_sequences(self):
|
def test_email_with_utf_8_non_decodable_sequences(self):
|
||||||
"""
|
"""
|
||||||
Tests that emails with utf-8 non-decodable sequences are parsed correctly
|
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:
|
with open(os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml")) as fd:
|
||||||
test_email = fd.read()
|
test_email = fd.read()
|
||||||
@ -99,6 +101,20 @@ class GetEmailCommonTests(TestCase):
|
|||||||
attachment = attachments[0]
|
attachment = attachments[0]
|
||||||
self.assertIn('prosazuje lepší', attachment.file.read().decode("utf-8"))
|
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):
|
class GetEmailParametricTemplate(object):
|
||||||
"""TestCase that checks basic email functionality across methods and socks configs."""
|
"""TestCase that checks basic email functionality across methods and socks configs."""
|
||||||
@ -153,9 +169,10 @@ class GetEmailParametricTemplate(object):
|
|||||||
else:
|
else:
|
||||||
# Test local email reading
|
# Test local email reading
|
||||||
if self.method == 'local':
|
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('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_isfile.return_value = True
|
||||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||||
|
|
||||||
@ -224,9 +241,10 @@ class GetEmailParametricTemplate(object):
|
|||||||
else:
|
else:
|
||||||
# Test local email reading
|
# Test local email reading
|
||||||
if self.method == 'local':
|
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('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_isfile.return_value = True
|
||||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||||
|
|
||||||
@ -299,9 +317,10 @@ class GetEmailParametricTemplate(object):
|
|||||||
else:
|
else:
|
||||||
# Test local email reading
|
# Test local email reading
|
||||||
if self.method == 'local':
|
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('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_isfile.return_value = True
|
||||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||||
|
|
||||||
@ -412,9 +431,10 @@ class GetEmailParametricTemplate(object):
|
|||||||
else:
|
else:
|
||||||
# Test local email reading
|
# Test local email reading
|
||||||
if self.method == 'local':
|
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('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_isfile.return_value = True
|
||||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||||
|
|
||||||
@ -502,9 +522,10 @@ class GetEmailParametricTemplate(object):
|
|||||||
else:
|
else:
|
||||||
# Test local email reading
|
# Test local email reading
|
||||||
if self.method == 'local':
|
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('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_isfile.return_value = True
|
||||||
mocked_listdir.return_value = ['filename1']
|
mocked_listdir.return_value = ['filename1']
|
||||||
|
|
||||||
@ -550,7 +571,7 @@ class GetEmailParametricTemplate(object):
|
|||||||
self.assertEqual(followup1.ticket.id, 1)
|
self.assertEqual(followup1.ticket.id, 1)
|
||||||
attach1 = get_object_or_404(FollowUpAttachment, pk=1)
|
attach1 = get_object_or_404(FollowUpAttachment, pk=1)
|
||||||
self.assertEqual(attach1.followup.id, 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-----
|
self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
iQIcBAEBCAAGBQJaA3dnAAoJELBLc7QPITnLN54P/3Zsu7+AIQWDFTvziJfCqswG
|
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_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)
|
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('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_isfile.return_value = True
|
||||||
mocked_listdir.return_value = ['filename1']
|
mocked_listdir.return_value = ['filename1']
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ class KBTests(TestCase):
|
|||||||
self.assertContains(response, 'This is a test category')
|
self.assertContains(response, 'This is a test category')
|
||||||
self.assertContains(response, 'KBItem 1')
|
self.assertContains(response, 'KBItem 1')
|
||||||
self.assertContains(response, 'KBItem 2')
|
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')
|
self.client.login(username=self.user.get_username(), password='password')
|
||||||
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", )))
|
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", )))
|
||||||
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')
|
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 import settings as helpdesk_settings
|
||||||
from helpdesk.views import feeds, staff, public, kb, login
|
from helpdesk.views import feeds, staff, public, kb, login
|
||||||
try:
|
try:
|
||||||
import helpdesk.tasks
|
# TODO: why is it imported? due to some side-effect or by mistake?
|
||||||
|
import helpdesk.tasks # NOQA
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -67,7 +67,11 @@ class HelpdeskUser:
|
|||||||
if self.has_full_access():
|
if self.has_full_access():
|
||||||
return True
|
return True
|
||||||
else:
|
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):
|
def can_access_ticket(self, ticket):
|
||||||
"""Check to see if the user has permission to access
|
"""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
|
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
|
initial_data['queue'] = Queue.objects.get(slug=request.GET.get('queue', None)).id
|
||||||
except Queue.DoesNotExist:
|
except Queue.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
if request.user.is_authenticated and request.user.usersettings_helpdesk.use_email_as_submitter and request.user.email:
|
u = request.user
|
||||||
initial_data['submitter_email'] = request.user.email
|
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']
|
query_param_fields = ['submitter_email', 'title', 'body', 'queue', 'kbitem']
|
||||||
custom_fields = ["custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]
|
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):
|
def login(request):
|
||||||
login_url = settings.LOGIN_URL
|
login_url = settings.LOGIN_URL
|
||||||
# Prevent redirect loop by checking that LOGIN_URL is not this view's name
|
# 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:
|
if 'next' in request.GET:
|
||||||
return_to = request.GET['next']
|
return_to = request.GET['next']
|
||||||
else:
|
else:
|
||||||
|
@ -7,6 +7,7 @@ views/public.py - All public facing views, eg non-staff (no authentication
|
|||||||
required) views.
|
required) views.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from django.core.exceptions import (
|
from django.core.exceptions import (
|
||||||
ObjectDoesNotExist, PermissionDenied, ImproperlyConfigured,
|
ObjectDoesNotExist, PermissionDenied, ImproperlyConfigured,
|
||||||
@ -26,9 +27,8 @@ from helpdesk import settings as helpdesk_settings
|
|||||||
from helpdesk.decorators import protect_view, is_helpdesk_staff
|
from helpdesk.decorators import protect_view, is_helpdesk_staff
|
||||||
import helpdesk.views.staff as staff
|
import helpdesk.views.staff as staff
|
||||||
import helpdesk.views.abstract_views as abstract_views
|
import helpdesk.views.abstract_views as abstract_views
|
||||||
from helpdesk.forms import PublicTicketForm
|
|
||||||
from helpdesk.lib import text_is_spam
|
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
|
from helpdesk.user import huser_from_request
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -42,7 +42,17 @@ def create_ticket(request, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
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):
|
def dispatch(self, *args, **kwargs):
|
||||||
request = self.request
|
request = self.request
|
||||||
@ -109,9 +119,6 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
|||||||
# if someone enters a non-int string for the ticket
|
# if someone enters a non-int string for the ticket
|
||||||
return HttpResponseRedirect(reverse('helpdesk:home'))
|
return HttpResponseRedirect(reverse('helpdesk:home'))
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
request = self.request
|
|
||||||
|
|
||||||
|
|
||||||
class CreateTicketIframeView(BaseCreateTicketView):
|
class CreateTicketIframeView(BaseCreateTicketView):
|
||||||
template_name = 'helpdesk/public_create_ticket_iframe.html'
|
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.utils import timezone
|
||||||
from django.views.generic.edit import FormView, UpdateView
|
from django.views.generic.edit import FormView, UpdateView
|
||||||
|
|
||||||
|
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT
|
||||||
from helpdesk.query import (
|
from helpdesk.query import (
|
||||||
get_query_class,
|
get_query_class,
|
||||||
query_to_base64,
|
query_to_base64,
|
||||||
@ -75,9 +76,6 @@ else:
|
|||||||
lambda u: u.is_authenticated and u.is_active and u.is_staff)
|
lambda u: u.is_authenticated and u.is_active and u.is_staff)
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_queue_choices(queues):
|
def _get_queue_choices(queues):
|
||||||
"""Return list of `choices` array for html form for given queues
|
"""Return list of `choices` array for html form for given queues
|
||||||
|
|
||||||
@ -153,16 +151,15 @@ def dashboard(request):
|
|||||||
# Open Resolved
|
# Open Resolved
|
||||||
# Queue 1 10 4
|
# Queue 1 10 4
|
||||||
# Queue 2 4 12
|
# Queue 2 4 12
|
||||||
|
# code never used (and prone to sql injections)
|
||||||
queues = HelpdeskUser(request.user).get_queues().values_list('id', flat=True)
|
# queues = HelpdeskUser(request.user).get_queues().values_list('id', flat=True)
|
||||||
|
# from_clause = """FROM helpdesk_ticket t,
|
||||||
from_clause = """FROM helpdesk_ticket t,
|
# helpdesk_queue q"""
|
||||||
helpdesk_queue q"""
|
# if queues:
|
||||||
if queues:
|
# where_clause = """WHERE q.id = t.queue_id AND
|
||||||
where_clause = """WHERE q.id = t.queue_id AND
|
# q.id IN (%s)""" % (",".join(("%d" % pk for pk in queues)))
|
||||||
q.id IN (%s)""" % (",".join(("%d" % pk for pk in queues)))
|
# else:
|
||||||
else:
|
# where_clause = """WHERE q.id = t.queue_id"""
|
||||||
where_clause = """WHERE q.id = t.queue_id"""
|
|
||||||
|
|
||||||
# get user assigned tickets page
|
# get user assigned tickets page
|
||||||
paginator = Paginator(
|
paginator = Paginator(
|
||||||
@ -382,6 +379,7 @@ def view_ticket(request, ticket_id):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
submitter_userprofile_url = None
|
submitter_userprofile_url = None
|
||||||
|
|
||||||
return render(request, 'helpdesk/ticket.html', {
|
return render(request, 'helpdesk/ticket.html', {
|
||||||
'ticket': ticket,
|
'ticket': ticket,
|
||||||
'submitter_userprofile_url': submitter_userprofile_url,
|
'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
|
# broken into two stages to prevent changes from first replace being themselves
|
||||||
# changed by the second replace due to conflicting syntax
|
# 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').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
|
# render the neutralized template
|
||||||
comment = template_func(comment).render(context)
|
comment = template_func(comment).render(context)
|
||||||
|
|
||||||
@ -592,7 +594,6 @@ def update_ticket(request, ticket_id, public=False):
|
|||||||
ticket.status = new_status
|
ticket.status = new_status
|
||||||
ticket.save()
|
ticket.save()
|
||||||
f.new_status = new_status
|
f.new_status = new_status
|
||||||
ticket_status_changed = True
|
|
||||||
if f.title:
|
if f.title:
|
||||||
f.title += ' and %s' % ticket.get_status_display()
|
f.title += ' and %s' % ticket.get_status_display()
|
||||||
else:
|
else:
|
||||||
@ -702,7 +703,10 @@ def update_ticket(request, ticket_id, public=False):
|
|||||||
else:
|
else:
|
||||||
template_staff = 'updated_owner'
|
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(
|
messages_sent_to.update(ticket.send(
|
||||||
{'assigned_to': (template_staff, context)},
|
{'assigned_to': (template_staff, context)},
|
||||||
dont_send_to=messages_sent_to,
|
dont_send_to=messages_sent_to,
|
||||||
@ -1072,7 +1076,6 @@ def ticket_list(request):
|
|||||||
pass
|
pass
|
||||||
elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse', 'kbitem'}.intersection(request.GET):
|
elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse', 'kbitem'}.intersection(request.GET):
|
||||||
# Fall-back if no querying is being done
|
# Fall-back if no querying is being done
|
||||||
all_queues = Queue.objects.all()
|
|
||||||
query_params = deepcopy(default_query_params)
|
query_params = deepcopy(default_query_params)
|
||||||
else:
|
else:
|
||||||
filter_in_params = [
|
filter_in_params = [
|
||||||
@ -1213,15 +1216,12 @@ def edit_ticket(request, ticket_id):
|
|||||||
ticket = get_object_or_404(Ticket, id=ticket_id)
|
ticket = get_object_or_404(Ticket, id=ticket_id)
|
||||||
ticket_perm_check(request, ticket)
|
ticket_perm_check(request, ticket)
|
||||||
|
|
||||||
if request.method == 'POST':
|
form = EditTicketForm(request.POST or None, instance=ticket)
|
||||||
form = EditTicketForm(request.POST, instance=ticket)
|
if form.is_valid():
|
||||||
if form.is_valid():
|
ticket = form.save()
|
||||||
ticket = form.save()
|
return redirect(ticket)
|
||||||
return HttpResponseRedirect(ticket.get_absolute_url())
|
|
||||||
else:
|
|
||||||
form = EditTicketForm(instance=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)
|
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_30 = date_rel_to_today(today, 30)
|
||||||
date_60 = date_rel_to_today(today, 60)
|
date_60 = date_rel_to_today(today, 60)
|
||||||
date_30_str = date_30.strftime('%Y-%m-%d')
|
date_30_str = date_30.strftime(CUSTOMFIELD_DATE_FORMAT)
|
||||||
date_60_str = date_60.strftime('%Y-%m-%d')
|
date_60_str = date_60.strftime(CUSTOMFIELD_DATE_FORMAT)
|
||||||
|
|
||||||
# > 0 & <= 30
|
# > 0 & <= 30
|
||||||
ota_le_30 = all_open_tickets.filter(created__gte=date_30_str)
|
ota_le_30 = all_open_tickets.filter(created__gte=date_30_str)
|
||||||
|
2
setup.py
2
setup.py
@ -6,7 +6,7 @@ from distutils.util import convert_path
|
|||||||
from fnmatch import fnmatchcase
|
from fnmatch import fnmatchcase
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
version = '0.3.0b2'
|
version = '0.3.0b3'
|
||||||
|
|
||||||
# Provided as an attribute, so you can append to these instead
|
# Provided as an attribute, so you can append to these instead
|
||||||
# of replicating them:
|
# of replicating them:
|
||||||
|
Loading…
Reference in New Issue
Block a user