mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-11-21 23:43:11 +01:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
2e9208116b
2
LICENSE
2
LICENSE
@ -1,5 +1,5 @@
|
||||
Copyright (c) 2008 Ross Poulton (Trading as Jutda),
|
||||
Copyright (c) 2008-2019 django-helpdesk contributors.
|
||||
Copyright (c) 2008-2020 django-helpdesk contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
|
5
Makefile
5
Makefile
@ -78,8 +78,9 @@ readme:
|
||||
#: demo - Setup demo project using Python3.
|
||||
.PHONY: demo
|
||||
demo:
|
||||
$(PIP) install -e . --user
|
||||
$(PIP) install -e demo --user
|
||||
# running it with and without --user flag because it started to be problematic for some setups
|
||||
$(PIP) install -e . --user || $(PIP) install -e .
|
||||
$(PIP) install -e demo --user || $(PIP) install -e demo
|
||||
demodesk migrate --noinput
|
||||
# Create superuser; user will be prompted to manually set a password
|
||||
# When you get a prompt, enter a password of your choosing.
|
||||
|
@ -38,7 +38,12 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sites',
|
||||
'django.contrib.humanize',
|
||||
'bootstrap4form',
|
||||
'helpdesk'
|
||||
|
||||
'account', # Required by pinax-teams
|
||||
'pinax.invitations', # required by pinax-teams
|
||||
'pinax.teams', # team support
|
||||
'helpdesk', # This is us!
|
||||
'reversion', # required by pinax-teams
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -19,9 +19,9 @@ AUTHOR = 'django-helpdesk team'
|
||||
URL = 'https://github.com/django-helpdesk/django-helpdesk'
|
||||
CLASSIFIERS = ['Development Status :: 4 - Beta',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Framework :: Django :: 2.0',
|
||||
'Framework :: Django :: 2.1',
|
||||
'Framework :: Django :: 2.2']
|
||||
|
@ -21,6 +21,8 @@ Before django-helpdesk will be much use, you need to do some basic configuration
|
||||
|
||||
If you wish to use `celery` instead of cron, you must add 'django_celery_beat' to `INSTALLED_APPS` and add a periodic celery task through the Django admin.
|
||||
|
||||
You will need to create a support queue, and associated login/host values, in the Django admin interface, in order for mail to be picked-up from the mail server and placed in the tickets table of your database. The values in the settings file alone, will not create the necessary values to trigger the get_email function.
|
||||
|
||||
4. If you wish to automatically escalate tickets based on their age, set up a cronjob to run the escalation command on a regular basis::
|
||||
|
||||
0 * * * * /path/to/helpdesksite/manage.py escalate_tickets
|
||||
|
@ -101,11 +101,11 @@ errors with trying to create User settings.
|
||||
|
||||
(substitute www-data for the user / group that your web server runs as, eg 'apache' or 'httpd')
|
||||
|
||||
If all else fails ensure all users can write to it::
|
||||
If all else fails, you could ensure all users can write to it::
|
||||
|
||||
chmod 777 attachments/
|
||||
|
||||
This is NOT recommended, especially if you're on a shared server.
|
||||
But this is NOT recommended, especially if you're on a shared server.
|
||||
|
||||
6. Ensure that your ``attachments`` folder has directory listings turned off, to ensure users don't download files that they are not specifically linked to from their tickets.
|
||||
|
||||
|
@ -82,6 +82,10 @@ These changes are visible throughout django-helpdesk
|
||||
|
||||
**Default:** ``HELPDESK_EMAIL_FALLBACK_LOCALE = "en"``
|
||||
|
||||
- **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``
|
||||
|
||||
- **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``
|
||||
|
@ -74,18 +74,29 @@ def process_email(quiet=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/'
|
||||
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)
|
||||
try:
|
||||
handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log'))
|
||||
logger.addHandler(handler)
|
||||
|
||||
queue_time_delta = timedelta(minutes=q.email_box_interval or 0)
|
||||
if not q.email_box_last_check:
|
||||
q.email_box_last_check = timezone.now() - timedelta(minutes=30)
|
||||
|
||||
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()
|
||||
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:
|
||||
try:
|
||||
handler.close()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
try:
|
||||
logger.removeHandler(handler)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
|
||||
def pop3_sync(q, logger, server):
|
||||
@ -428,7 +439,13 @@ def object_from_message(message, queue, logger):
|
||||
|
||||
sender = message.get('from', _('Unknown Sender'))
|
||||
sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender))
|
||||
sender_email = email.utils.parseaddr(sender)[1]
|
||||
# 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.
|
||||
# Note that the replace won't work on just an email with no real name,
|
||||
# but the getaddresses() function seems to be able to handle just unclosed quotes
|
||||
# correctly. Not ideal, but this seems to work for now.
|
||||
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1]
|
||||
|
||||
body_plain, body_html = '', ''
|
||||
|
||||
|
@ -129,7 +129,7 @@ def text_is_spam(text, request):
|
||||
|
||||
|
||||
def process_attachments(followup, attached_files):
|
||||
max_email_attachment_size = getattr(settings, 'MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
||||
max_email_attachment_size = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
||||
attachments = []
|
||||
|
||||
for attached in attached_files:
|
||||
@ -149,7 +149,7 @@ def process_attachments(followup, attached_files):
|
||||
|
||||
if attached.size < max_email_attachment_size:
|
||||
# Only files smaller than 512kb (or as defined in
|
||||
# settings.MAX_EMAIL_ATTACHMENT_SIZE) are sent via email.
|
||||
# settings.HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE) are sent via email.
|
||||
attachments.append([filename, att.file])
|
||||
|
||||
return attachments
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1334,8 +1334,9 @@ class KBItem(models.Model):
|
||||
return super(KBItem, self).save(*args, **kwargs)
|
||||
|
||||
def _score(self):
|
||||
""" Return a score out of 10 or Unrated if no votes """
|
||||
if self.votes > 0:
|
||||
return int(self.recommendations / self.votes)
|
||||
return (self.recommendations / self.votes) * 10
|
||||
else:
|
||||
return _('Unrated')
|
||||
score = property(_score)
|
||||
|
@ -121,6 +121,10 @@ if HELPDESK_EMAIL_SUBJECT_TEMPLATE.find("ticket.ticket") < 0:
|
||||
# default fallback locale when queue locale not found
|
||||
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en')
|
||||
|
||||
# default maximum email attachment size, in bytes
|
||||
# only attachments smaller than this size will be sent via email
|
||||
HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
||||
|
||||
|
||||
########################################
|
||||
# options for staff.create_ticket view #
|
||||
|
@ -24,18 +24,21 @@
|
||||
|
||||
{% if all_tickets_reported_by_current_user %}
|
||||
{% trans "All Tickets submitted by you" as ticket_list_caption %}
|
||||
{% include 'helpdesk/include/tickets.html' with ticket_list=all_tickets_reported_by_current_user ticket_list_empty_message="" %}
|
||||
{% trans "atrbcu_page" as page_var %}
|
||||
{% include 'helpdesk/include/tickets.html' with ticket_list=all_tickets_reported_by_current_user ticket_list_empty_message="" page_var=page_var %}
|
||||
{% endif %}
|
||||
|
||||
{% trans "Open Tickets assigned to you (you are working on this ticket)" as ticket_list_caption %}
|
||||
{% trans "You have no tickets assigned to you." as no_assigned_tickets %}
|
||||
{% include 'helpdesk/include/tickets.html' with ticket_list=user_tickets ticket_list_empty_message=no_assigned_tickets %}
|
||||
{% trans "ut_page" as page_var %}
|
||||
{% include 'helpdesk/include/tickets.html' with ticket_list=user_tickets ticket_list_empty_message=no_assigned_tickets page_var=page_var %}
|
||||
|
||||
{% include 'helpdesk/include/unassigned.html' %}
|
||||
|
||||
{% if user_tickets_closed_resolved %}
|
||||
{% trans "Closed & resolved Tickets you used to work on" as ticket_list_caption %}
|
||||
{% include 'helpdesk/include/tickets.html' with ticket_list=user_tickets_closed_resolved ticket_list_empty_message="" %}
|
||||
{% trans "utcr_page" as page_var %}
|
||||
{% include 'helpdesk/include/tickets.html' with ticket_list=user_tickets_closed_resolved ticket_list_empty_message="" page_var=page_var %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -40,6 +40,7 @@
|
||||
{% 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>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% load i18n humanize %}
|
||||
|
||||
<<<<<<< HEAD
|
||||
<!-- DataTables Example -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
@ -33,6 +34,37 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- /.table-responsive -->
|
||||
{% if ticket_list.has_other_pages %}
|
||||
<ul class="pagination">
|
||||
<!-- if we aren't on page one, go back to start and go back one controls -->
|
||||
{% if ticket_list.has_previous %}
|
||||
<li><a href="?{{ page_var }}=1">««</a></li>
|
||||
<li><a href="?{{ page_var }}={{ ticket_list.previous_page_number }}">«</a></li>
|
||||
{% else %}
|
||||
<li class="disabled"><span>««</span></li>
|
||||
<li class="disabled"><span>«</span></li>
|
||||
{% endif %}
|
||||
<!-- other pages, set thresh to the number to show before and after active -->
|
||||
{% with 5 as thresh %}
|
||||
{% for i in ticket_list.paginator.page_range %}
|
||||
{% if ticket_list.number == i %}
|
||||
<li class="active"><span>{{ i }} <span class="sr-only">(current)</span></span></li>
|
||||
{% elif i <= ticket_list.number|add:5 and i >= ticket_list.number|add:-5 %}
|
||||
<li><a href="?{{ page_var }}={{ i }}">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<!-- if we aren't on the last page, go forward one and go to end controls -->
|
||||
{% if ticket_list.has_next %}
|
||||
<li><a href="?{{ page_var }}={{ ticket_list.next_page_number }}">»</a></li>
|
||||
<li><a href="?{{ page_var }}={{ ticket_list.paginator.num_pages }}">»»</a></li>
|
||||
{% else %}
|
||||
<li class="disabled"><span>»</span></li>
|
||||
<li class="disabled"><span>»»</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer small text-muted">Listing {{ ticket_list|length }} ticket(s).</div>
|
||||
</div>
|
||||
|
@ -5,7 +5,6 @@
|
||||
{% block helpdesk_title %}{% trans "Tickets" %}{% endblock %}
|
||||
|
||||
{% block helpdesk_head %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -205,6 +205,79 @@ class GetEmailParametricTemplate(object):
|
||||
self.assertEqual(ticket2.title, test_email_subject)
|
||||
self.assertEqual(ticket2.description, test_email_body)
|
||||
|
||||
def test_commas_in_mail_headers(self):
|
||||
"""Tests correctly decoding mail headers when a comma is encoded into
|
||||
UTF-8. See bug report #832."""
|
||||
|
||||
# example email text from Django docs: https://docs.djangoproject.com/en/1.10/ref/unicode/
|
||||
test_email_from = "Bernard-Bouissières, Benjamin <bbb@example.com>"
|
||||
test_email_subject = "Commas in From lines"
|
||||
test_email_body = "Testing commas in from email UTF-8."
|
||||
test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + "\nSubject: " + test_email_subject + "\n\n" + test_email_body
|
||||
test_mail_len = len(test_email)
|
||||
|
||||
if self.socks:
|
||||
from socks import ProxyConnectionError
|
||||
with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)):
|
||||
call_command('get_email')
|
||||
|
||||
else:
|
||||
# Test local email reading
|
||||
if self.method == 'local':
|
||||
with mock.patch('helpdesk.management.commands.get_email.listdir') as mocked_listdir, \
|
||||
mock.patch('helpdesk.management.commands.get_email.isfile') as mocked_isfile, \
|
||||
mock.patch('builtins.open' if six.PY3 else '__builtin__.open', mock.mock_open(read_data=test_email)):
|
||||
mocked_isfile.return_value = True
|
||||
mocked_listdir.return_value = ['filename1', 'filename2']
|
||||
|
||||
call_command('get_email')
|
||||
|
||||
mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1')
|
||||
mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename2')
|
||||
|
||||
elif self.method == 'pop3':
|
||||
# mock poplib.POP3's list and retr methods to provide responses as per RFC 1939
|
||||
pop3_emails = {
|
||||
'1': ("+OK", test_email.split('\n')),
|
||||
'2': ("+OK", test_email.split('\n')),
|
||||
}
|
||||
pop3_mail_list = ("+OK 2 messages", ("1 %d" % test_mail_len, "2 %d" % test_mail_len))
|
||||
mocked_poplib_server = mock.Mock()
|
||||
mocked_poplib_server.list = mock.Mock(return_value=pop3_mail_list)
|
||||
mocked_poplib_server.retr = mock.Mock(side_effect=lambda x: pop3_emails[x])
|
||||
with mock.patch('helpdesk.management.commands.get_email.poplib', autospec=True) as mocked_poplib:
|
||||
mocked_poplib.POP3 = mock.Mock(return_value=mocked_poplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
elif self.method == 'imap':
|
||||
# mock imaplib.IMAP4's search and fetch methods with responses from RFC 3501
|
||||
imap_emails = {
|
||||
"1": ("OK", (("1", test_email),)),
|
||||
"2": ("OK", (("2", test_email),)),
|
||||
}
|
||||
imap_mail_list = ("OK", ("1 2",))
|
||||
mocked_imaplib_server = mock.Mock()
|
||||
mocked_imaplib_server.search = mock.Mock(return_value=imap_mail_list)
|
||||
|
||||
# we ignore the second arg as the data item/mime-part is constant (RFC822)
|
||||
mocked_imaplib_server.fetch = mock.Mock(side_effect=lambda x, _: imap_emails[x])
|
||||
with mock.patch('helpdesk.management.commands.get_email.imaplib', autospec=True) as mocked_imaplib:
|
||||
mocked_imaplib.IMAP4 = mock.Mock(return_value=mocked_imaplib_server)
|
||||
call_command('get_email')
|
||||
|
||||
ticket1 = get_object_or_404(Ticket, pk=1)
|
||||
self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id)
|
||||
self.assertEqual(ticket1.submitter_email, 'bbb@example.com')
|
||||
self.assertEqual(ticket1.title, test_email_subject)
|
||||
self.assertEqual(ticket1.description, test_email_body)
|
||||
|
||||
ticket2 = get_object_or_404(Ticket, pk=2)
|
||||
self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id)
|
||||
self.assertEqual(ticket2.submitter_email, 'bbb@example.com')
|
||||
self.assertEqual(ticket2.title, test_email_subject)
|
||||
self.assertEqual(ticket2.description, test_email_body)
|
||||
|
||||
def test_read_email_with_template_tag(self):
|
||||
"""Tests reading plain text emails from a queue and creating tickets,
|
||||
except this time the email body contains a Django template tag.
|
||||
|
@ -97,7 +97,7 @@ class BaseCreateTicketView(abstract_views.AbstractCreateTicketMixin, FormView):
|
||||
# This submission is spam. Let's not save it.
|
||||
return render(request, template_name='helpdesk/public_spam.html')
|
||||
else:
|
||||
ticket = form.save()
|
||||
ticket = form.save(user=self.request.user if self.request.user.is_authenticated else None)
|
||||
try:
|
||||
return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % (
|
||||
reverse('helpdesk:public_view'),
|
||||
|
@ -16,6 +16,7 @@ from django.contrib.auth.decorators import user_passes_test
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.core.exceptions import ValidationError, PermissionDenied
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect, Http404, HttpResponse, JsonResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
@ -31,6 +32,7 @@ from helpdesk.query import (
|
||||
query_to_dict,
|
||||
query_to_base64,
|
||||
query_from_base64,
|
||||
apply_query,
|
||||
)
|
||||
|
||||
from helpdesk.user import HelpdeskUser
|
||||
@ -157,6 +159,41 @@ def dashboard(request):
|
||||
else:
|
||||
where_clause = """WHERE q.id = t.queue_id"""
|
||||
|
||||
# get user assigned tickets page
|
||||
paginator = Paginator(
|
||||
tickets, tickets_per_page)
|
||||
try:
|
||||
tickets = paginator.page(user_tickets_page)
|
||||
except PageNotAnInteger:
|
||||
tickets = paginator.page(1)
|
||||
except EmptyPage:
|
||||
tickets = paginator.page(
|
||||
paginator.num_pages)
|
||||
|
||||
# get user completed tickets page
|
||||
paginator = Paginator(
|
||||
tickets_closed_resolved, tickets_per_page)
|
||||
try:
|
||||
tickets_closed_resolved = paginator.page(
|
||||
user_tickets_closed_resolved_page)
|
||||
except PageNotAnInteger:
|
||||
tickets_closed_resolved = paginator.page(1)
|
||||
except EmptyPage:
|
||||
tickets_closed_resolved = paginator.page(
|
||||
paginator.num_pages)
|
||||
|
||||
# get user submitted tickets page
|
||||
paginator = Paginator(
|
||||
all_tickets_reported_by_current_user, tickets_per_page)
|
||||
try:
|
||||
all_tickets_reported_by_current_user = paginator.page(
|
||||
all_tickets_reported_by_current_user_page)
|
||||
except PageNotAnInteger:
|
||||
all_tickets_reported_by_current_user = paginator.page(1)
|
||||
except EmptyPage:
|
||||
all_tickets_reported_by_current_user = paginator.page(
|
||||
paginator.num_pages)
|
||||
|
||||
return render(request, 'helpdesk/dashboard.html', {
|
||||
'user_tickets': tickets,
|
||||
'user_tickets_closed_resolved': tickets_closed_resolved,
|
||||
|
@ -1,4 +1,4 @@
|
||||
Django>=2.2.9,<3
|
||||
Django>=2.2.13,<3
|
||||
django-bootstrap4-form
|
||||
celery
|
||||
django-celery-beat
|
||||
@ -12,4 +12,6 @@ pytz
|
||||
six
|
||||
djangorestframework
|
||||
django-model-utils
|
||||
pinax-teams @ git+https://github.com/auto-mat/pinax-teams.git@slugify#egg=pinax-teams
|
||||
|
||||
# specific commit because the current release has no required library upgrade
|
||||
pinax-teams @ git+https://github.com/pinax/pinax-teams.git@dd75e1c#egg=pinax-teams
|
||||
|
2
setup.py
2
setup.py
@ -126,9 +126,9 @@ setup(
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Framework :: Django",
|
||||
"Framework :: Django :: 2.0",
|
||||
"Framework :: Django :: 2.1",
|
||||
|
Loading…
Reference in New Issue
Block a user