Merge branch 'master' into pinax-remove

This commit is contained in:
Renato 2021-08-04 11:19:14 -03:00 committed by GitHub
commit 563b28ed14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 3383 additions and 2291 deletions

4
.flake8 Normal file
View 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

View File

@ -7,8 +7,9 @@ python:
- "3.8"
env:
- DJANGO=2.2.16
- DJANGO=3.1.2
- DJANGO=2.2.23
- DJANGO=3.1.11
- DJANGO=3.2.3
install:
- pip install -q Django==$DJANGO

View File

@ -155,7 +155,7 @@ collaborative translation. If you want to help translate django-helpdesk into
languages other than English, we encourage you to make use of our Transifex
project:
http://www.transifex.net/projects/p/django-helpdesk/resource/core/
http://www.transifex.com/projects/p/django-helpdesk/resource/core/
Once you have translated content via Transifex, please raise an issue on the
project Github page and tag it as "translations" to let us know it's ready to

View File

@ -108,8 +108,8 @@ HELPDESK_SHOW_CHANGE_PASSWORD = True
# Instead of showing the public web portal first,
# we can instead redirect users straight to the login page.
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/login/'
LOGIN_URL = 'helpdesk:login'
LOGIN_REDIRECT_URL = 'helpdesk:home'
# Database
# - by default, we use SQLite3 for the demo, but you can also

View File

@ -13,7 +13,7 @@ project_root = os.path.dirname(here)
NAME = 'django-helpdesk-demodesk'
DESCRIPTION = 'A demo Django project using django-helpdesk'
README = open(os.path.join(here, 'README.rst')).read()
VERSION = '0.3.0b2'
VERSION = '0.3.0b3'
#VERSION = open(os.path.join(project_root, 'VERSION')).read().strip()
AUTHOR = 'django-helpdesk team'
URL = 'https://github.com/django-helpdesk/django-helpdesk'

View File

@ -107,6 +107,10 @@ These options only change display of items on public-facing pages, not staff pag
**Default:** ``HELPDESK_SUBMIT_A_TICKET_PUBLIC = True``
- **HELPDESK_PUBLIC_TICKET_FORM_CLASS** Define custom form class to show on public pages for anon users. You can use it for adding custom fields and validation, captcha and so on.
**Default:** ``HELPDESK_PUBLIC_TICKET_FORM_CLASS = "helpdesk.forms.PublicTicketForm"``
Options for public ticket submission form
-----------------------------------------
@ -219,3 +223,7 @@ The following settings were defined in previous versions and are no longer suppo
- **HELPDESK_FOOTER_SHOW_CHANGE_LANGUAGE_LINK** Is never shown. Use your own template if required.
- **HELPDESK_ENABLE_PER_QUEUE_MEMBERSHIP** Discontinued in favor of HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION.
- **HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL** Do not ignore fowarded and replied text from the email messages which create a new ticket; useful for cases when customer forwards some email (error from service or something) and wants support to see that
- **HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE** Any incoming .eml message is saved and available, helps when customer spent some time doing fancy markup which has been corrupted during the email-to-ticket-comment translate process

3
helpdesk/.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
import-order-style = pep8

View File

@ -4,42 +4,36 @@ Django Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved.
See LICENSE for details.
"""
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.utils.translation import ugettext as _
from django.utils import encoding, timezone
from django.contrib.auth import get_user_model
from helpdesk import settings
from helpdesk.lib import safe_template_context, process_attachments
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
from datetime import timedelta
import base64
import binascii
# import base64
import email
from email.header import decode_header
from email.utils import getaddresses, parseaddr, collapse_rfc2231_value
import imaplib
import logging
import mimetypes
from os import listdir, unlink
from os.path import isfile, join
import os
import poplib
import re
import socket
import ssl
import sys
from datetime import timedelta
from email.utils import getaddresses
from os.path import isfile, join
from time import ctime
from optparse import make_option
from bs4 import BeautifulSoup
from django.conf import settings as django_settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q
from django.utils import encoding, timezone
from django.utils.translation import ugettext as _
from email_reply_parser import EmailReplyParser
import logging
from helpdesk import settings
from helpdesk.lib import safe_template_context, process_attachments
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail
# import User model, which may be a custom model
User = get_user_model()
@ -70,37 +64,48 @@ def process_email(quiet=False):
if q.logging_type in logging_types:
logger.setLevel(logging_types[q.logging_type])
elif not q.logging_type or q.logging_type == 'none':
logging.disable(logging.CRITICAL) # disable all messages
# disable all handlers so messages go to nowhere
logger.handlers = []
logger.propagate = False
if quiet:
logger.propagate = False # do not propagate to root logger that would log to console
logdir = q.logging_dir or '/var/log/helpdesk/'
# Log messages to specific file only if the queue has it configured
if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set
log_file_handler = logging.FileHandler(join(q.logging_dir, q.slug + '_get_email.log'))
logger.addHandler(log_file_handler)
else:
log_file_handler = None
try:
handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log'))
logger.addHandler(handler)
if not q.email_box_last_check:
q.email_box_last_check = timezone.now() - timedelta(minutes=30)
queue_time_delta = timedelta(minutes=q.email_box_interval or 0)
if (q.email_box_last_check + queue_time_delta) < timezone.now():
process_queue(q, logger=logger)
q.email_box_last_check = timezone.now()
q.save()
finally:
# we must close the file handler correctly if it's created
try:
handler.close()
if log_file_handler:
log_file_handler.close()
except Exception as e:
logging.exception(e)
try:
logger.removeHandler(handler)
if log_file_handler:
logger.removeHandler(log_file_handler)
except Exception as e:
logging.exception(e)
def pop3_sync(q, logger, server):
server.getwelcome()
try:
server.stls()
except Exception:
logger.warning("POP3 StartTLS failed or unsupported. Connection will be unencrypted.")
server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)
@ -138,17 +143,27 @@ def pop3_sync(q, logger, server):
def imap_sync(q, logger, server):
try:
try:
server.starttls()
except Exception:
logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
server.login(q.email_box_user or
settings.QUEUE_EMAIL_BOX_USER,
q.email_box_pass or
settings.QUEUE_EMAIL_BOX_PASSWORD)
server.select(q.email_box_imap_folder)
except imaplib.IMAP4.abort:
logger.error("IMAP login failed. Check that the server is accessible and that the username and password are correct.")
logger.error(
"IMAP login failed. Check that the server is accessible and that "
"the username and password are correct."
)
server.logout()
sys.exit()
except ssl.SSLError:
logger.error("IMAP login failed due to SSL error. This is often due to a timeout. Please check your connection and try again.")
logger.error(
"IMAP login failed due to SSL error. This is often due to a timeout. "
"Please check your connection and try again."
)
server.logout()
sys.exit()
@ -171,7 +186,10 @@ def imap_sync(q, logger, server):
else:
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num)
except imaplib.IMAP4.error:
logger.error("IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?" % q.email_box_imap_folder)
logger.error(
"IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?",
q.email_box_imap_folder
)
server.expunge()
server.close()
@ -243,7 +261,7 @@ def process_queue(q, logger):
elif email_box_type == 'local':
mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/'
mail = [join(mail_dir, f) for f in listdir(mail_dir) if isfile(join(mail_dir, f))]
mail = [join(mail_dir, f) for f in os.listdir(mail_dir) if isfile(join(mail_dir, f))]
logger.info("Found %d messages in local mailbox directory" % len(mail))
logger.info("Found %d messages in local mailbox directory" % len(mail))
@ -253,15 +271,15 @@ def process_queue(q, logger):
full_message = encoding.force_text(f.read(), errors='replace')
ticket = object_from_message(message=full_message, queue=q, logger=logger)
if ticket:
logger.info("Successfully processed message %d, ticket/comment created." % i)
logger.info("Successfully processed message %d, ticket/comment created.", i)
try:
unlink(m) # delete message file if ticket was successful
except OSError:
logger.error("Unable to delete message %d." % i)
os.unlink(m) # delete message file if ticket was successful
except OSError as e:
logger.error("Unable to delete message %d (%s).", i, str(e))
else:
logger.info("Successfully deleted message %d." % i)
logger.info("Successfully deleted message %d.", i)
else:
logger.warn("Message %d was not successfully processed, and will be left in local directory" % i)
logger.warn("Message %d was not successfully processed, and will be left in local directory", i)
def decodeUnknown(charset, string):
@ -277,7 +295,26 @@ def decodeUnknown(charset, string):
def decode_mail_headers(string):
decoded = email.header.decode_header(string)
return u' '.join([str(msg, encoding=charset, errors='replace') if charset else str(msg) for msg, charset in decoded])
return u' '.join([
str(msg, encoding=charset, errors='replace') if charset else str(msg)
for msg, charset
in decoded
])
def is_autoreply(message):
"""
Accepting message as something with .get(header_name) method
Returns True if it's likely to be auto-reply or False otherwise
So we don't start mail loops
"""
any_if_this = [
False if not message.get("Auto-Submitted") else message.get("Auto-Submitted").lower() != "no",
True if message.get("X-Auto-Response-Suppress") in ("DR", "AutoReply", "All") else False,
message.get("List-Id"),
message.get("List-Unsubscribe"),
]
return any(any_if_this)
def create_ticket_cc(ticket, cc_list):
@ -305,7 +342,7 @@ def create_ticket_cc(ticket, cc_list):
try:
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email)
new_ticket_ccs.append(ticket_cc)
except ValidationError as err:
except ValidationError:
pass
return new_ticket_ccs
@ -362,7 +399,6 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id))
new = True
update = ''
# Old issue being re-opened
elif ticket.status == Ticket.CLOSED_STATUS:
@ -374,7 +410,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}),
date=now,
public=True,
comment=payload['body'],
comment=payload.get('full_body', payload['body']) or "",
message_id=message_id
)
@ -389,7 +425,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
attached = process_attachments(f, files)
for att_file in attached:
logger.info("Attachment '%s' (with size %s) successfully added to ticket from email." % (att_file[0], att_file[1].size))
logger.info(
"Attachment '%s' (with size %s) successfully added to ticket from email.",
att_file[0], att_file[1].size
)
context = safe_template_context(ticket)
@ -402,18 +441,29 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True)
for email in ticket_cc_list:
notifications_to_be_sent.append(email)
for email_address in ticket_cc_list:
notifications_to_be_sent.append(email_address)
autoreply = is_autoreply(message)
if autoreply:
logger.info("Message seems to be auto-reply, not sending any emails back to the sender")
else:
# send mail to appropriate people now depending on what objects
# were created and who was CC'd
# Add auto-reply headers because it's an auto-reply and we must
extra_headers = {
'In-Reply-To': message_id,
"Auto-Submitted": "auto-replied",
"X-Auto-Response-Suppress": "All",
"Precedence": "auto_reply",
}
if new:
ticket.send(
{'submitter': ('newticket_submitter', context),
'new_ticket_cc': ('newticket_cc', context),
'ticket_cc': ('newticket_cc', context)},
fail_silently=True,
extra_headers={'In-Reply-To': message_id},
extra_headers=extra_headers,
)
else:
context.update(comment=f.comment)
@ -421,13 +471,13 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
{'submitter': ('newticket_submitter', context),
'assigned_to': ('updated_owner', context)},
fail_silently=True,
extra_headers={'In-Reply-To': message_id},
extra_headers=extra_headers,
)
if queue.enable_notifications_on_email_events:
ticket.send(
{'ticket_cc': ('updated_cc', context)},
fail_silently=True,
extra_headers={'In-Reply-To': message_id},
extra_headers=extra_headers,
)
return ticket
@ -445,6 +495,7 @@ def object_from_message(message, queue, logger):
sender = message.get('from', _('Unknown Sender'))
sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender))
# to address bug #832, we wrap all the text in front of the email address in
# double quotes by using replace() on the email string. Then,
# take first item of list, second item of tuple is the actual email address.
@ -453,8 +504,6 @@ def object_from_message(message, queue, logger):
# correctly. Not ideal, but this seems to work for now.
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1]
body_plain, body_html = '', ''
cc = message.get_all('cc', None)
if cc:
# first, fixup the encoding if necessary
@ -484,6 +533,7 @@ def object_from_message(message, queue, logger):
ticket = None
body = None
full_body = None
counter = 0
files = []
@ -502,7 +552,19 @@ def object_from_message(message, queue, logger):
if part['Content-Transfer-Encoding'] == '8bit' and part.get_content_charset() == 'utf-8':
body = body.decode('unicode_escape')
body = decodeUnknown(part.get_content_charset(), body)
# have to use django_settings here so overwritting it works in tests
# the default value is False anyway
if ticket is None and getattr(django_settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False):
# first message in thread, we save full body to avoid losing forwards and things like that
body_parts = []
for f in EmailReplyParser.read(body).fragments:
body_parts.append(f.content)
full_body = '\n\n'.join(body_parts)
body = EmailReplyParser.parse_reply(body)
else:
# second and other reply, save only first part of the message
body = EmailReplyParser.parse_reply(body)
full_body = body
# workaround to get unicode text out rather than escaped text
try:
body = body.encode('ascii').decode('unicode_escape')
@ -515,13 +577,23 @@ def object_from_message(message, queue, logger):
except UnicodeDecodeError:
email_body = encoding.smart_text(part.get_payload(decode=False))
payload = """
<html>
<head>
<meta charset="utf-8"/>
</head>
%s
</html>""" % email_body
if not body and not full_body:
# no text has been parsed so far - try such deep parsing for some messages
altered_body = email_body.replace("</p>", "</p>\n").replace("<br", "\n<br")
mail = BeautifulSoup(str(altered_body), "html.parser")
full_body = mail.get_text()
if "<body" not in email_body:
email_body = f"<body>{email_body}</body>"
payload = (
'<html>'
'<head>'
'<meta charset="utf-8" />'
'</head>'
'%s'
'</html>'
) % email_body
files.append(
SimpleUploadedFile(_("email_html_body.html"), payload.encode("utf-8"), 'text/html')
)
@ -530,18 +602,25 @@ def object_from_message(message, queue, logger):
if not name:
ext = mimetypes.guess_extension(part.get_content_type())
name = "part-%i%s" % (counter, ext)
payload = part.get_payload()
if isinstance(payload, list):
payload = payload.pop().as_string()
payloadToWrite = payload
# check version of python to ensure use of only the correct error type
non_b64_err = TypeError
try:
logger.debug("Try to base64 decode the attachment payload")
payloadToWrite = base64.decodebytes(payload)
except non_b64_err:
logger.debug("Payload was not base64 encoded, using raw bytes")
payloadToWrite = payload
else:
name = ("part-%i_" % counter) + name
# # FIXME: this code gets the paylods, then does something with it and then completely ignores it
# # writing the part.get_payload(decode=True) instead; and then the payload variable is
# # replaced by some dict later.
# # the `payloadToWrite` has been also ignored so was commented
# payload = part.get_payload()
# if isinstance(payload, list):
# payload = payload.pop().as_string()
# # payloadToWrite = payload
# # check version of python to ensure use of only the correct error type
# non_b64_err = TypeError
# try:
# logger.debug("Try to base64 decode the attachment payload")
# # payloadToWrite = base64.decodebytes(payload)
# except non_b64_err:
# logger.debug("Payload was not base64 encoded, using raw bytes")
# # payloadToWrite = payload
files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0]))
logger.debug("Found MIME attachment %s" % name)
@ -553,11 +632,25 @@ def object_from_message(message, queue, logger):
if beautiful_body:
try:
body = beautiful_body.text
full_body = body
except AttributeError:
pass
if not body:
body = ""
if getattr(django_settings, 'HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE', False):
# save message as attachment in case of some complex markup renders wrong
files.append(
SimpleUploadedFile(
_("original_message.eml").replace(
".eml",
timezone.localtime().strftime("_%d-%m-%Y_%H:%M") + ".eml"
),
str(message).encode("utf-8"),
'text/plain'
)
)
smtp_priority = message.get('priority', '')
smtp_importance = message.get('importance', '')
high_priority_types = {'high', 'important', '1', 'urgent'}
@ -565,6 +658,7 @@ def object_from_message(message, queue, logger):
payload = {
'body': body,
'full_body': full_body or body,
'subject': subject,
'queue': queue,
'sender_email': sender_email,

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ forms.py - Definitions of newforms-based forms for creating and maintaining
tickets.
"""
import logging
from datetime import datetime, date, time
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django import forms
@ -35,6 +36,10 @@ CUSTOMFIELD_TO_FIELD_DICT = {
'slug': forms.SlugField,
}
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT} {CUSTOMFIELD_TIME_FORMAT}"
class CustomFieldMixin(object):
"""
@ -71,8 +76,14 @@ class CustomFieldMixin(object):
# Try to use the immediate equivalences dictionary
try:
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
# Change widget in case it is a boolean
if fieldclass == forms.BooleanField:
# Change widgets for the following classes
if fieldclass == forms.DateField:
instanceargs['widget'] = forms.DateInput(attrs={'class': 'form-control date-field'})
elif fieldclass == forms.DateTimeField:
instanceargs['widget'] = forms.DateTimeInput(attrs={'class': 'form-control datetime-field'})
elif fieldclass == forms.TimeField:
instanceargs['widget'] = forms.TimeInput(attrs={'class': 'form-control time-field'})
elif fieldclass == forms.BooleanField:
instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'})
except KeyError:
@ -88,18 +99,38 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
model = Ticket
exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to')
class Media:
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js')
def __init__(self, *args, **kwargs):
"""
Add any custom fields that are defined to the form
"""
super(EditTicketForm, self).__init__(*args, **kwargs)
# Disable and add help_text to the merged_to field on this form
self.fields['merged_to'].disabled = True
self.fields['merged_to'].help_text = _('This ticket is merged into the selected ticket.')
for field in CustomField.objects.all():
initial_value = None
try:
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field)
initial_value = current_value.value
except TicketCustomFieldValue.DoesNotExist:
initial_value = None
# Attempt to convert from fixed format string to date/time data type
if 'datetime' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATETIME_FORMAT)
elif 'date' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATE_FORMAT)
elif 'time' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_TIME_FORMAT)
# If it is boolean field, transform the value to a real boolean instead of a string
elif 'boolean' == current_value.field.data_type:
initial_value = 'True' == initial_value
except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
# ValueError error if parsing fails, using initial_value = current_value.value
# TypeError if parsing None type
pass
instanceargs = {
'label': field.label,
'help_text': field.help_text,
@ -119,6 +150,15 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
except ObjectDoesNotExist:
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
# Convert date/time data type to known fixed format string.
if datetime is type(value):
cfv.value = value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
elif date is type(value):
cfv.value = value.strftime(CUSTOMFIELD_DATE_FORMAT)
elif time is type(value):
cfv.value = value.strftime(CUSTOMFIELD_TIME_FORMAT)
else:
cfv.value = value
cfv.save()
@ -175,7 +215,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
due_date = forms.DateTimeField(
widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}),
required=False,
input_formats=['%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
input_formats=[CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
label=_('Due on'),
)
@ -187,7 +227,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
)
class Media:
js = ('helpdesk/js/init_due_date.js',)
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js')
def __init__(self, kbcategory=None, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -8,13 +8,11 @@ lib.py - Common functions (eg multipart e-mail)
import logging
import mimetypes
import os
from django.conf import settings
from django.utils.encoding import smart_text, smart_str
from django.utils.safestring import mark_safe
from django.utils.encoding import smart_text
from helpdesk.models import FollowUpAttachment, EmailTemplate
from helpdesk.models import FollowUpAttachment
logger = logging.getLogger('helpdesk')

View File

@ -502,7 +502,7 @@ msgstr "Složka pro log soubory"
#: third_party/django-helpdesk/helpdesk/models.py:306
msgid ""
"If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/"
"this queue? The standard logging mechanims are used if no directory is set"
msgstr ""
#: third_party/django-helpdesk/helpdesk/models.py:317

File diff suppressed because it is too large Load Diff

View File

@ -522,11 +522,10 @@ msgstr "Dossier de logs"
#: .\models.py:308
msgid ""
"If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/"
"this queue? The standard logging mechanims are used if no directory is set"
msgstr ""
"Si les logs sont activés, quel dossier doit être utilisé pour stocker les "
"fichiers de logs pour cette file ? Si aucun dossier n'est défini, cela sera /"
"var/log/helpdesk/ par défaut"
"fichiers de logs pour cette file?"
#: .\models.py:319
msgid "Default owner"

View File

@ -531,13 +531,15 @@ msgstr ""
#: models.py:247
msgid "Logging Directory"
msgstr ""
msgstr "Директория логов"
#: models.py:251
msgid ""
"If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/"
"this queue? The standard logging mechanims are used if no directory is set"
msgstr ""
"Директория в которую будут сохраняться файлы с логами; стандартная конфигурация "
"используется если ничего не указано"
#: models.py:261
msgid "Default owner"

View File

@ -470,7 +470,7 @@ msgstr ""
#: models.py:308
msgid ""
"If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/"
"this queue? The standard logging mechanims are used if no directory is set"
msgstr ""
#: models.py:319

View File

@ -13,7 +13,6 @@ from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from helpdesk.models import UserSettings
from helpdesk.settings import DEFAULT_USER_SETTINGS
User = get_user_model()

View File

@ -18,7 +18,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='queue',
name='logging_dir',
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? If no directory is set, default to /var/log/helpdesk/', max_length=200, null=True, verbose_name='Logging Directory'),
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set', max_length=200, null=True, verbose_name='Logging Directory'),
),
migrations.AddField(
model_name='queue',

View File

@ -305,7 +305,7 @@ class Queue(models.Model):
null=True,
help_text=_('If logging is enabled, what directory should we use to '
'store log files for this queue? '
'If no directory is set, default to /var/log/helpdesk/'),
'The standard logging mechanims are used if no directory is set'),
)
default_owner = models.ForeignKey(
@ -612,7 +612,7 @@ class Ticket(models.Model):
'assigned_to': (template_name2, context),
}
**kwargs are passed to send_templated_mail defined in templated_mail.py
**kwargs are passed to send_templated_mail defined in templated_email.py
returns the set of email addresses the notification was delivered to.
@ -632,6 +632,7 @@ class Ticket(models.Model):
template, context = roles[role]
send_templated_mail(template, context, recipient, sender=self.queue.from_address, **kwargs)
recipients.add(recipient)
send('submitter', self.submitter_email)
send('ticket_cc', self.queue.updated_ticket_cc)
send('new_ticket_cc', self.queue.new_ticket_cc)
@ -1517,7 +1518,10 @@ class UserSettings(models.Model):
email_on_ticket_change = models.BooleanField(
verbose_name=_('E-mail me on ticket change?'),
help_text=_('If you\'re the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?'),
help_text=_(
'If you\'re the ticket owner and the ticket is changed via the web by somebody else,'
'do you want to receive an e-mail?'
),
default=email_on_ticket_change_default,
)

View File

@ -1,6 +1,7 @@
from django.db.models import Q
from django.core.cache import cache
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import ugettext as _
from base64 import b64encode
@ -135,7 +136,8 @@ class __Query__:
if sortreverse:
sorting = "-%s" % sorting
queryset = queryset.order_by(sorting)
return queryset.distinct() # https://stackoverflow.com/questions/30487056/django-queryset-contains-duplicate-entries
# https://stackoverflow.com/questions/30487056/django-queryset-contains-duplicate-entries
return queryset.distinct()
def get_cache_key(self):
return str(self.huser.user.pk) + ":" + self.base64
@ -200,8 +202,13 @@ class __Query__:
'start_date': self.mk_timeline_date(followup.date),
'text': {
'headline': ticket.title + ' - ' + followup.title,
'text': (followup.comment if followup.comment else _('No text')) + '<br/> <a href="%s" class="btn" role="button">%s</a>' %
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")),
'text': (
(escape(followup.comment) if followup.comment else _('No text'))
+
'<br/> <a href="%s" class="btn" role="button">%s</a>'
%
(reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket"))
),
},
'group': _('Messages'),
}

View File

@ -79,6 +79,13 @@ HELPDESK_VIEW_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC
# show 'submit a ticket' section on public page?
HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PUBLIC', True)
# change that to custom class to have extra fields or validation (like captcha)
HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
settings,
"HELPDESK_PUBLIC_TICKET_FORM_CLASS",
"helpdesk.forms.PublicTicketForm"
)
###################################
# options for update_ticket views #
@ -89,7 +96,10 @@ HELPDESK_SUBMIT_A_TICKET_PUBLIC = getattr(settings, 'HELPDESK_SUBMIT_A_TICKET_PU
# can be True/False or a callable accepting the active user and returning True if they must be considered helpdesk staff
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)
if not (HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE in (True, False) or callable(HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE)):
warnings.warn("HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.", RuntimeWarning)
warnings.warn(
"HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE should be set to either True/False or a callable.",
RuntimeWarning
)
# show edit buttons in ticket follow ups.
HELPDESK_SHOW_EDIT_BUTTON_FOLLOW_UP = getattr(settings,
@ -159,3 +169,13 @@ HELPDESK_USE_HTTPS_IN_EMAIL_LINK = getattr(settings, 'HELPDESK_USE_HTTPS_IN_EMAI
HELPDESK_TEAMS_MODEL = getattr(settings, 'HELPDESK_TEAMS_MODEL', 'pinax_teams.Team')
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = getattr(settings, 'HELPDESK_TEAMS_MIGRATION_DEPENDENCIES', [('pinax_teams', '0004_auto_20170511_0856')])
HELPDESK_KBITEM_TEAM_GETTER = getattr(settings, 'HELPDESK_KBITEM_TEAM_GETTER', lambda kbitem: kbitem.team)
# Include all signatures and forwards in the first ticket message if set
# Useful if you get forwards dropped from them while they are useful part of request
HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr(settings, 'HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL', False)
# If set then we always save incoming emails as .eml attachments
# which is quite noisy but very helpful for complicated markup, forwards and so on
# (which gets stripped/corrupted otherwise)
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr(settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False)

View 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";
});

View File

@ -1,3 +1,3 @@
$(() => {
$("#id_due_date").datepicker();
$("#id_due_date").datepicker({dateFormat: 'yy-mm-dd 00:00:00'});
});

View File

@ -1,4 +1,4 @@
from celery import task
from celery.decorators import task
from .email import process_email

View File

@ -1,5 +1,4 @@
import os
import mimetypes
import logging
from smtplib import SMTPException
@ -16,7 +15,7 @@ def send_templated_mail(template_name,
bcc=None,
fail_silently=False,
files=None,
extra_headers={}):
extra_headers=None):
"""
send_templated_mail() is a wrapper around Django's e-mail routines that
allows us to easily send multipart (text/plain & text/html) e-mails using
@ -54,6 +53,8 @@ def send_templated_mail(template_name,
from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \
HELPDESK_EMAIL_FALLBACK_LOCALE
headers = extra_headers or {}
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
try:
@ -96,7 +97,8 @@ def send_templated_mail(template_name,
msg = EmailMultiAlternatives(subject_part, text_part,
sender or settings.DEFAULT_FROM_EMAIL,
recipients, bcc=bcc)
recipients, bcc=bcc,
headers=headers)
msg.attach_alternative(html_part, "text/html")
if files:

View File

@ -11,6 +11,7 @@
{% include 'helpdesk/base-head.html' %}
{% block helpdesk_head %}{% endblock %}
{% include 'helpdesk/base_js.html' %}
</head>
@ -44,8 +45,6 @@
{% include "helpdesk/debug.html" %}
{% include 'helpdesk/base_js.html' %}
{% block helpdesk_js %}{% endblock %}
</body>

View File

@ -34,7 +34,7 @@
{% else %}
<div class="form-group">
<label for='id_{{ field.name }}'>
{% trans field.label %}
{{ field.label }}
{% if not field.field.required %}
({% trans "Optional" %})
{% endif %}

View File

@ -1,58 +1,49 @@
{% extends "helpdesk/base.html" %}{% load i18n bootstrap4form %}
{% extends "helpdesk/base.html" %}
{% load i18n bootstrap4form %}
{% block helpdesk_title %}{% trans "Edit Ticket" %}{% endblock %}
{% block helpdesk_breadcrumb %}
<li class="breadcrumb-item">
<li class="breadcrumb-item">
<a href="{% url 'helpdesk:list' %}">{% trans "Tickets" %}</a>
</li>
<li class="breadcrumb-item">
</li>
<li class="breadcrumb-item">
<a href="{% url 'helpdesk:list' %}{{ ticket.id }}/">{{ ticket.queue.slug }}-{{ ticket.id }}</a>
</li>
<li class="breadcrumb-item active">{% trans "Edit Ticket" %}</li>
</li>
<li class="breadcrumb-item active">{% trans "Edit Ticket" %}</li>
{% endblock %}
{% block helpdesk_body %}
<div class="col-xs-6">
<div class="panel panel-default">
<div class="panel-body"><h2>{% trans "Edit a Ticket" %}</h2>
<p>{% trans "Unless otherwise stated, all fields are required." %} {% trans "Please provide as descriptive a title and description as possible." %}</p>
<p><strong>{% trans "Note" %}:</strong> {% blocktrans %}Editing a ticket does <em>not</em> send an e-mail to the ticket owner or submitter. No new details should be entered, this form should only be used to fix incorrect details or clean up the submission.{% endblocktrans %}</p>
<form method='post' action='./'>
<fieldset>
<div class="col-xs-6">
<div class="panel panel-default">
<div class="panel-body"><h2>{% trans "Edit a Ticket" %}</h2>
<p>
{% trans "Unless otherwise stated, all fields are required." %}
{% trans "Please provide as descriptive a title and description as possible." %}
</p>
<p>
<strong>{% trans "Note" %}:</strong>
{% blocktrans %}Editing a ticket does <em>not</em> send an e-mail to the ticket owner or submitter. No new details should be entered, this form should only be used to fix incorrect details or clean up the submission.{% endblocktrans %}
</p>
{% if errors %}<p class="text-danger">{% for error in errors %}{% trans "Error: " %}{{ error }}<br>{% endfor %}</p>{% endif %}
<form method='post'>
{% csrf_token %}
<fieldset>
{{ form|bootstrap4form }}
{% comment %}
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<dt><label for='id_{{ field.name }}'>{{ field.label }}</label>{% if not field.field.required %} <span class='form_optional'>{% trans "(Optional)" %}</span>{% endif %}</dt>
<dd>{{ field }}</dd>
{% if field.errors %}<dd class='error'>{{ field.errors }}</dd>{% endif %}
{% if field.help_text %}<dd class='form_help_text'>{{ field.help_text }}</dd>{% endif %}</label>
{% endif %}
{% endfor %}
</dl>
{% endcomment %}
<div class='buttons form-group'>
<input type='submit' class="btn btn-primary btn-sm" value='{% trans "Save Changes" %}' />
<a href='{{ ticket.get_absolute_url }}'><button class="btn btn-danger">{% trans "Cancel Changes" %}</button></a>
<input type='submit' class="btn btn-primary btn-sm" value='{% trans "Save Changes" %}'/>
<a href='{{ ticket.get_absolute_url }}'>
<button class="btn btn-danger">{% trans "Cancel Changes" %}</button>
</a>
</div>
</fieldset>
</form>
</div>
</fieldset>
{% csrf_token %}</form>
</div>
</div>
</div>
<script>
$( function() {
$( "#id_due_date" ).datepicker({dateFormat: 'yy-mm-dd'});
} );
</script>
{% endblock %}
{% block helpdesk_js %}
{{ form.media.js }}
{% endblock %}

View File

@ -25,7 +25,7 @@
<td>{{ ticket.priority }}</td>
<td>{{ ticket.queue }}</td>
<td>{{ ticket.get_status }}</td>
<td><span title='{{ ticket.modified|date:"r" }}'>{{ ticket.modified|naturaltime }}</span></td>
<td><span title='{{ ticket.modified|date:"DATETIME_FORMAT" }}'>{{ ticket.modified|naturaltime }}</span></td>
</tr>
{% empty %}
<tr>{% if ticket_list_empty_message %}<td colspan='6'>{{ ticket_list_empty_message }}</td>{% else %}<td colspan='6'>{% trans "You do not have any pending tickets." %}</td>{% endif %}</tr>

View File

@ -23,7 +23,7 @@
<td class="tickettitle"><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}. {{ ticket.title }} </a></td>
<td>{{ ticket.priority }}</td>
<td>{{ ticket.queue }}</td>
<td><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
<td><span title='{{ ticket.created|date:"DATETIME_FORMAT" }}'>{{ ticket.created|naturaltime }}</span></td>
<td class="text-center">
<a href='{{ ticket.get_absolute_url }}?take'><button class='btn btn-primary btn-sm'><i class="fas fa-hand-paper"></i>&nbsp;{% 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>&nbsp;{% trans "Delete" %}</button></a>
@ -63,7 +63,7 @@
<td class="tickettitle"><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}. {{ ticket.title }} </a></td>
<td>{{ ticket.priority }}</td>
<td>{{ ticket.queue }}</td>
<td><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
<td><span title='{{ ticket.created|date:"DATETIME_FORMAT" }}'>{{ ticket.created|naturaltime }}</span></td>
<td class="text-center">
<a href='{{ ticket.get_absolute_url }}?take'><button class='btn btn-primary btn-sm'><i class="fas fa-hand-paper"></i>&nbsp;{% 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>&nbsp;{% trans "Delete" %}</button></a>

View File

@ -36,7 +36,7 @@
{% endif %}
<a href='{% if iframe %}{% url 'helpdesk:submit_iframe' %}{% else %}{% url 'helpdesk:submit' %}{%endif%}?{% if category.queue %}queue={{category.queue.pk}};_readonly_fields_=queue;{%endif%}kbitem={{item.id}};{{query_param_string}}' class="col-sm">
<div class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</div>
<div class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-plus-circle fa-lg"></i> {% trans 'Create New Ticket' %}{% trans " Queue: " %}{{item}}</div>
</a>
</div>
<div>
@ -57,7 +57,7 @@
{% if category.queue %}
<a href='{% if iframe %}{% url 'helpdesk:submit_iframe' %}{% else %}{% url 'helpdesk:submit' %}{%endif%}?queue={{category.queue.pk}};_readonly_fields_=queue;{{query_param_string}}'>
{% block submit_button %}
<div class="btn btn-danger btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</div>
<div class="btn btn-danger btn-circle btn-xl float-right"><i class="fa fa-plus-circle fa-lg"></i> {% trans 'Create New Ticket' %}{% if category.queue %}{% trans " Queue: " %}{{category.queue}}{% endif %}</div>
{% endblock %}
</a>
{% endif %}

View File

@ -12,13 +12,13 @@
<tbody>
<tr>
<th>{% trans "Submitted On" %}</th>
<td>{{ ticket.created|date:"r" }} ({{ ticket.created|naturaltime }})</td>
<td>{{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})</td>
</tr>
{% if ticket.due_date %}
<tr>
<th>{% trans "Due On" %}</th>
<td>{{ ticket.due_date|date:"r" }} ({{ ticket.due_date|naturaltime }})</td>
<td>{{ ticket.due_date|date:"DATETIME_FORMAT" }} ({{ ticket.due_date|naturaltime }})</td>
</tr>
{% endif %}
@ -67,7 +67,7 @@
{% load ticket_to_link %}
{% for followup in ticket.followup_set.public_followups %}
<div class='followup well'>
<div class='title'>{{ followup.title }} <span class='byline text-info'>{% if followup.user %}by {{ followup.user }}{% endif %} <span title='{{ followup.date|date:"r" }}'>{{ followup.date|naturaltime }}</span></span></div>
<div class='title'>{{ followup.title }} <span class='byline text-info'>{% if followup.user %}by {{ followup.user }}{% endif %} <span title='{{ followup.date|date:"DATETIME_FORMAT" }}'>{{ followup.date|naturaltime }}</span></span></div>
{{ followup.comment|force_escape|urlizetrunc:50|num_to_link|linebreaksbr }}
{% if followup.ticketchange_set.all %}<div class='changes'><ul>
{% for change in followup.ticketchange_set.all %}

View File

@ -46,11 +46,11 @@
<div class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ followup.title|num_to_link }}</h5>
<small><i class="fas fa-clock"></i>&nbsp;<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>&nbsp;<span class='byline text-info'>{% if followup.user %}by {{ followup.user }},{% endif %} <span title='{{ followup.date|date:"DATETIME_FORMAT" }}'>{{ followup.date|naturaltime }}</span>{% if followup.time_spent %}, <span>{% trans "time spent" %}: {{ followup.time_spent_formated }}</span>{% endif %} {% if not followup.public %} <span class='private'>({% trans "Private" %})</span>{% endif %}</span></small>
</div>
<p class="mb-1">
{% if followup.comment %}
<p>{{ followup.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</p>
<p>{{ followup.get_markdown|urlizetrunc:50|num_to_link }}</p>
{% endif %}
{% for change in followup.ticketchange_set.all %}
{% if forloop.first %}<div class='changes'><ul>{% endif %}

View File

@ -1,5 +1,6 @@
{% load i18n humanize ticket_to_link %}
{% load static %}
{% load helpdesk_util %}
<div class="card mb-3">
<!--div class="card-header">
@ -21,27 +22,32 @@
{% for customfield in ticket.ticketcustomfieldvalue_set.all %}
<tr>
<th class="table-secondary">{{ customfield.field.label }}</th>
<td>{% ifequal customfield.field.data_type "url" %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>{% else %}{{ customfield.value|default:"" }}{% endifequal %}</td>
<td>{% spaceless %}{% if "url" == customfield.field.data_type %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>
{% elif "datetime" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
{% elif "date" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
{% elif "time" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
{% else %}{{ customfield.value|default:"" }}
{% endif %}{% endspaceless %}</td>
</tr>{% endfor %}
<tr>
<th class="table-active">{% trans "Due Date" %}</th>
<td>{{ ticket.due_date|date:"d. E Y" }} {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %}
<td>{{ ticket.due_date|date:"DATETIME_FORMAT" }} {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %}
</td>
<th class="table-active">{% trans "Submitted On" %}</th>
<td>{{ ticket.created|date:"d. E Y" }} ({{ ticket.created|naturaltime }})</td>
<td>{{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})</td>
</tr>
<tr>
<th class="table-active">{% trans "Assigned To" %}</th>
<td>{{ ticket.get_assigned_to }}{% ifequal ticket.get_assigned_to _('Unassigned') %} <strong>
<a href='?take'><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-hand-paper"></i></button></a>
</strong>{% endifequal %}
<td>{{ ticket.get_assigned_to }}{% if _('Unassigned') == ticket.get_assigned_to %} <strong>
<a data-toggle="tooltip" href='?take' title='{% trans "Assign this ticket to " %}{{ request.user.email }}'><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-hand-paper"></i></button></a>
</strong>{% endif %}
</td>
<th class="table-active">{% trans "Submitter E-Mail" %}</th>
<td> {{ ticket.submitter_email }}
{% if user.is_superuser %} {% if submitter_userprofile_url %}<strong><a href='{{submitter_userprofile_url}}'><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-address-book"></i></button></a></strong>{% endif %}
<strong><a href ="{% url 'helpdesk:list'%}?q={{ticket.submitter_email}}">
{% if user.is_superuser %} {% if submitter_userprofile_url %}<strong><a data-toggle="tooltip" href='{{submitter_userprofile_url}}' title='{% trans "Edit " %}{{ ticket.submitter_email }}{% trans " user profile" %}'><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-address-book"></i></button></a></strong>{% endif %}
<strong><a data-toggle="tooltip" href ="{% url 'helpdesk:list'%}?q={{ticket.submitter_email}}" title='{% trans "Display tickets filtered for " %}{{ ticket.submitter_email }}{% trans " as a keyword" %}'>
<button type="button" class="btn btn-primary btn-sm"><i class="fas fa-search"></i></button></a></strong>
<strong><a href='{% url 'helpdesk:email_ignore_add' %}?email={{ ticket.submitter_email }}'>
<strong><a data-toggle="tooltip" href='{% url 'helpdesk:email_ignore_add' %}?email={{ ticket.submitter_email }}' title='{% trans "Add email address for the ticket system to ignore." %}'>
<button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-eye-slash"></i></button></a></strong>{% endif %}
</td>
</tr>
@ -50,7 +56,7 @@
<td class="{% if ticket.priority < 3 %}table-warning{% endif %}">{{ ticket.get_priority_display }}
</td>
<th class="table-active">{% trans "Copies To" %}</th>
<td>{{ ticketcc_string }} <a data-toggle='tooltip' href='{% url 'helpdesk:ticket_cc' ticket.id %}' title='{% trans "Click here to add / remove people who should receive an e-mail whenever this ticket is updated." %}'><strong><button type="button" class="btn btn-warning btn-sm float-right"><i class="fa fa-share"></i></button></strong></a>{% if SHOW_SUBSCRIBE %}, <strong><a data-toggle='tooltip' href='?subscribe' title='{% trans "Click here to subscribe yourself to this ticket, if you want to receive an e-mail whenever this ticket is updated." %}'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-rss-square"></i></button></a></strong>{% endif %}</td>
<td>{{ ticketcc_string }} <a data-toggle='tooltip' href='{% url 'helpdesk:ticket_cc' ticket.id %}' title='{% trans "Click here to add / remove people who should receive an e-mail whenever this ticket is updated." %}'><strong><button type="button" class="btn btn-warning btn-sm float-right"><i class="fa fa-share"></i></button></strong></a>{% if SHOW_SUBSCRIBE %} <strong><a data-toggle='tooltip' href='?subscribe' title='{% trans "Click here to subscribe yourself to this ticket, if you want to receive an e-mail whenever this ticket is updated." %}'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-rss-square"></i></button></a></strong>{% endif %}</td>
</tr>
<tr>
@ -93,11 +99,11 @@
<tr>
<td id="ticket-description" colspan='4'>
<h4>{% trans "Description" %}</h4>
{{ ticket.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</td>
{{ ticket.get_markdown|urlizetrunc:50|num_to_link }}</td>
</tr>
{% if ticket.resolution %}<tr>
<th colspan='2'>{% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} <a href='?close'><button type="button" class="btn btn-warning btn-sm">{% trans "Accept and Close" %}</button></a>{% endifequal %}</th>
<th colspan='2'>{% trans "Resolution" %}{% if "Resolved" == ticket.get_status_display %} <a href='?close'><button type="button" class="btn btn-warning btn-sm">{% trans "Accept and Close" %}</button></a>{% endif %}</th>
</tr>
<tr>
<td colspan='2'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td>

View File

@ -1,11 +1,11 @@
"""
django-helpdesk - A Django powered ticket tracker for small enterprise.
templatetags/helpdesk_staff.py - The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff.
The is_helpdesk_staff template filter returns True if the user qualifies as Helpdesk staff.
templatetags/helpdesk_staff.py
"""
import logging
from django.template import Library
from django.db.models import Q
from helpdesk.decorators import is_helpdesk_staff
@ -18,5 +18,5 @@ register = Library()
def helpdesk_staff(user):
try:
return is_helpdesk_staff(user)
except Exception as e:
except Exception:
logger.exception("'helpdesk_staff' template tag (django-helpdesk) crashed")

View File

@ -1,9 +1,35 @@
from django import template
from django.template import Library
from django.template.defaultfilters import date as date_filter
from django.conf import settings
register = template.Library()
from datetime import datetime
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT, CUSTOMFIELD_DATETIME_FORMAT
register = Library()
@register.filter
def get(value, arg, default=None):
""" Call the dictionary get function """
return value.get(arg, default)
@register.filter(expects_localtime=True)
def datetime_string_format(value):
"""
:param value: String - Expected to be a datetime, date, or time in specific format
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
"""
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
except (TypeError, ValueError):
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
except (TypeError, ValueError):
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
except (TypeError, ValueError):
# If NoneType return empty string, else return original value
new_value = "" if value is None else value
return new_value

View 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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from django.test import TestCase
from django.test import TestCase, override_settings
from django.core.management import call_command
from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User
@ -84,9 +84,11 @@ class GetEmailCommonTests(TestCase):
self.assertEqual(ticket.title, "Testovácí email")
self.assertEqual(ticket.description, "íářčšáíéřášč")
@override_settings(HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL=True)
def test_email_with_utf_8_non_decodable_sequences(self):
"""
Tests that emails with utf-8 non-decodable sequences are parsed correctly
The message is fowarded as well
"""
with open(os.path.join(THIS_DIR, "test_files/utf-nondecodable.eml")) as fd:
test_email = fd.read()
@ -99,6 +101,20 @@ class GetEmailCommonTests(TestCase):
attachment = attachments[0]
self.assertIn('prosazuje lepší', attachment.file.read().decode("utf-8"))
@override_settings(HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL=True)
def test_email_with_forwarded_message(self):
"""
Forwarded message of that format must be still attached correctly
"""
with open(os.path.join(THIS_DIR, "test_files/forwarded-message.eml")) as fd:
test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger)
self.assertEqual(ticket.title, "Test with original message from GitHub")
self.assertIn("This is email body", ticket.description)
assert "Hello there!" not in ticket.description, ticket.description
assert FollowUp.objects.filter(ticket=ticket).count() == 1
assert "Hello there!" in FollowUp.objects.filter(ticket=ticket).first().comment
class GetEmailParametricTemplate(object):
"""TestCase that checks basic email functionality across methods and socks configs."""
@ -153,9 +169,10 @@ class GetEmailParametricTemplate(object):
else:
# Test local email reading
if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=test_email)):
mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1', 'filename2']
@ -224,9 +241,10 @@ class GetEmailParametricTemplate(object):
else:
# Test local email reading
if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open' if six.PY3 else '__builtin__.open', mock.mock_open(read_data=test_email)):
mock.patch('builtins.open' if six.PY3 else '__builtin__.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1', 'filename2']
@ -299,9 +317,10 @@ class GetEmailParametricTemplate(object):
else:
# Test local email reading
if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=test_email)):
mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1', 'filename2']
@ -412,9 +431,10 @@ class GetEmailParametricTemplate(object):
else:
# Test local email reading
if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())):
mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1', 'filename2']
@ -502,9 +522,10 @@ class GetEmailParametricTemplate(object):
else:
# Test local email reading
if self.method == 'local':
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=test_email)):
mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1']
@ -550,7 +571,7 @@ class GetEmailParametricTemplate(object):
self.assertEqual(followup1.ticket.id, 1)
attach1 = get_object_or_404(FollowUpAttachment, pk=1)
self.assertEqual(attach1.followup.id, 1)
self.assertEqual(attach1.filename, 'signature.asc')
self.assertEqual(attach1.filename, 'part-1_signature.asc')
self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE-----
iQIcBAEBCAAGBQJaA3dnAAoJELBLc7QPITnLN54P/3Zsu7+AIQWDFTvziJfCqswG
@ -670,9 +691,10 @@ class GetEmailCCHandling(TestCase):
test_email = "To: queue@example.com\nCc: " + test_email_cc_one + ", " + test_email_cc_one + ", " + test_email_cc_two + ", " + test_email_cc_three + "\nCC: " + test_email_cc_one + ", " + test_email_cc_three + ", " + test_email_cc_four + ", " + ticket_user_emails + "\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body
test_mail_len = len(test_email)
with mock.patch('helpdesk.email.listdir') as mocked_listdir, \
with mock.patch('os.listdir') as mocked_listdir, \
mock.patch('helpdesk.email.isfile') as mocked_isfile, \
mock.patch('builtins.open', mock.mock_open(read_data=test_email)):
mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \
mock.patch('os.unlink'):
mocked_isfile.return_value = True
mocked_listdir.return_value = ['filename1']

View File

@ -47,7 +47,7 @@ class KBTests(TestCase):
self.assertContains(response, 'This is a test category')
self.assertContains(response, 'KBItem 1')
self.assertContains(response, 'KBItem 2')
self.assertContains(response, 'Contact a human')
self.assertContains(response, 'Create New Ticket Queue:')
self.client.login(username=self.user.get_username(), password='password')
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", )))
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')

View File

@ -16,7 +16,8 @@ from helpdesk.decorators import helpdesk_staff_member_required, protect_view
from helpdesk import settings as helpdesk_settings
from helpdesk.views import feeds, staff, public, kb, login
try:
import helpdesk.tasks
# TODO: why is it imported? due to some side-effect or by mistake?
import helpdesk.tasks # NOQA
except ImportError:
pass

View File

@ -67,7 +67,11 @@ class HelpdeskUser:
if self.has_full_access():
return True
else:
return helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION and self.user.has_perm(queue.permission_name)
return (
helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION
and
self.user.has_perm(queue.permission_name)
)
def can_access_ticket(self, ticket):
"""Check to see if the user has permission to access

View File

@ -1,5 +1,3 @@
from django.views.generic.edit import FormView
from helpdesk.models import CustomField, KBItem, Queue
@ -11,8 +9,9 @@ class AbstractCreateTicketMixin():
initial_data['queue'] = Queue.objects.get(slug=request.GET.get('queue', None)).id
except Queue.DoesNotExist:
pass
if request.user.is_authenticated and request.user.usersettings_helpdesk.use_email_as_submitter and request.user.email:
initial_data['submitter_email'] = request.user.email
u = request.user
if u.is_authenticated and u.usersettings_helpdesk.use_email_as_submitter and u.email:
initial_data['submitter_email'] = u.email
query_param_fields = ['submitter_email', 'title', 'body', 'queue', 'kbitem']
custom_fields = ["custom_%s" % f.name for f in CustomField.objects.filter(staff_only=False)]

View File

@ -11,7 +11,14 @@ default_login_view = auth_views.LoginView.as_view(
def login(request):
login_url = settings.LOGIN_URL
# Prevent redirect loop by checking that LOGIN_URL is not this view's name
if login_url and (login_url != resolve_url(request.resolver_match.view_name) and (login_url != request.resolver_match.view_name)):
condition = (
login_url
and (
login_url != resolve_url(request.resolver_match.view_name)
and (login_url != request.resolver_match.view_name)
)
)
if condition:
if 'next' in request.GET:
return_to = request.GET['next']
else:

View File

@ -7,6 +7,7 @@ views/public.py - All public facing views, eg non-staff (no authentication
required) views.
"""
import logging
from importlib import import_module
from django.core.exceptions import (
ObjectDoesNotExist, PermissionDenied, ImproperlyConfigured,
@ -26,9 +27,8 @@ from helpdesk import settings as helpdesk_settings
from helpdesk.decorators import protect_view, is_helpdesk_staff
import helpdesk.views.staff as staff
import helpdesk.views.abstract_views as abstract_views
from helpdesk.forms import PublicTicketForm
from helpdesk.lib import text_is_spam
from helpdesk.models import CustomField, Ticket, Queue, UserSettings, KBCategory, KBItem
from helpdesk.models import Ticket, Queue, UserSettings
from helpdesk.user import huser_from_request
logger = logging.getLogger(__name__)
@ -42,7 +42,17 @@ def create_ticket(request, *args, **kwargs):
class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
form_class = PublicTicketForm
def get_form_class(self):
try:
the_module, the_form_class = helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS.rsplit(".", 1)
the_module = import_module(the_module)
the_form_class = getattr(the_module, the_form_class)
except Exception as e:
raise ImproperlyConfigured(
f"Invalid custom form class {helpdesk_settings.HELPDESK_PUBLIC_TICKET_FORM_CLASS}"
) from e
return the_form_class
def dispatch(self, *args, **kwargs):
request = self.request
@ -109,9 +119,6 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
# if someone enters a non-int string for the ticket
return HttpResponseRedirect(reverse('helpdesk:home'))
def get_success_url(self):
request = self.request
class CreateTicketIframeView(BaseCreateTicketView):
template_name = 'helpdesk/public_create_ticket_iframe.html'

View File

@ -24,6 +24,7 @@ from django.utils.html import escape
from django.utils import timezone
from django.views.generic.edit import FormView, UpdateView
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT
from helpdesk.query import (
get_query_class,
query_to_base64,
@ -75,9 +76,6 @@ else:
lambda u: u.is_authenticated and u.is_active and u.is_staff)
User = get_user_model()
def _get_queue_choices(queues):
"""Return list of `choices` array for html form for given queues
@ -153,16 +151,15 @@ def dashboard(request):
# Open Resolved
# Queue 1 10 4
# Queue 2 4 12
queues = HelpdeskUser(request.user).get_queues().values_list('id', flat=True)
from_clause = """FROM helpdesk_ticket t,
helpdesk_queue q"""
if queues:
where_clause = """WHERE q.id = t.queue_id AND
q.id IN (%s)""" % (",".join(("%d" % pk for pk in queues)))
else:
where_clause = """WHERE q.id = t.queue_id"""
# code never used (and prone to sql injections)
# queues = HelpdeskUser(request.user).get_queues().values_list('id', flat=True)
# from_clause = """FROM helpdesk_ticket t,
# helpdesk_queue q"""
# if queues:
# where_clause = """WHERE q.id = t.queue_id AND
# q.id IN (%s)""" % (",".join(("%d" % pk for pk in queues)))
# else:
# where_clause = """WHERE q.id = t.queue_id"""
# get user assigned tickets page
paginator = Paginator(
@ -382,6 +379,7 @@ def view_ticket(request, ticket_id):
)
else:
submitter_userprofile_url = None
return render(request, 'helpdesk/ticket.html', {
'ticket': ticket,
'submitter_userprofile_url': submitter_userprofile_url,
@ -555,7 +553,11 @@ def update_ticket(request, ticket_id, public=False):
# broken into two stages to prevent changes from first replace being themselves
# changed by the second replace due to conflicting syntax
comment = comment.replace('{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM')
comment = comment.replace('X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%').replace('X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}')
comment = comment.replace(
'X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%'
).replace(
'X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}'
)
# render the neutralized template
comment = template_func(comment).render(context)
@ -592,7 +594,6 @@ def update_ticket(request, ticket_id, public=False):
ticket.status = new_status
ticket.save()
f.new_status = new_status
ticket_status_changed = True
if f.title:
f.title += ' and %s' % ticket.get_status_display()
else:
@ -702,7 +703,10 @@ def update_ticket(request, ticket_id, public=False):
else:
template_staff = 'updated_owner'
if ticket.assigned_to and (ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assigned)):
if ticket.assigned_to and (
ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
or (reassigned and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assigned)
):
messages_sent_to.update(ticket.send(
{'assigned_to': (template_staff, context)},
dont_send_to=messages_sent_to,
@ -1072,7 +1076,6 @@ def ticket_list(request):
pass
elif not {'queue', 'assigned_to', 'status', 'q', 'sort', 'sortreverse', 'kbitem'}.intersection(request.GET):
# Fall-back if no querying is being done
all_queues = Queue.objects.all()
query_params = deepcopy(default_query_params)
else:
filter_in_params = [
@ -1213,15 +1216,12 @@ def edit_ticket(request, ticket_id):
ticket = get_object_or_404(Ticket, id=ticket_id)
ticket_perm_check(request, ticket)
if request.method == 'POST':
form = EditTicketForm(request.POST, instance=ticket)
form = EditTicketForm(request.POST or None, instance=ticket)
if form.is_valid():
ticket = form.save()
return HttpResponseRedirect(ticket.get_absolute_url())
else:
form = EditTicketForm(instance=ticket)
return redirect(ticket)
return render(request, 'helpdesk/edit_ticket.html', {'form': form, 'ticket': ticket})
return render(request, 'helpdesk/edit_ticket.html', {'form': form, 'ticket': ticket, 'errors': form.errors})
edit_ticket = staff_member_required(edit_ticket)
@ -1777,8 +1777,8 @@ def calc_basic_ticket_stats(Tickets):
date_30 = date_rel_to_today(today, 30)
date_60 = date_rel_to_today(today, 60)
date_30_str = date_30.strftime('%Y-%m-%d')
date_60_str = date_60.strftime('%Y-%m-%d')
date_30_str = date_30.strftime(CUSTOMFIELD_DATE_FORMAT)
date_60_str = date_60.strftime(CUSTOMFIELD_DATE_FORMAT)
# > 0 & <= 30
ota_le_30 = all_open_tickets.filter(created__gte=date_30_str)

View File

@ -6,7 +6,7 @@ from distutils.util import convert_path
from fnmatch import fnmatchcase
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
# of replicating them: