Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Benbb96 2020-10-02 19:11:33 +02:00
commit 2e9208116b
24 changed files with 4396 additions and 2844 deletions

View File

@ -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,

View File

@ -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.

View File

@ -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 = [

View File

@ -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']

View File

@ -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

View File

@ -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.

View File

@ -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``

View File

@ -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 = '', ''

View File

@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -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 #

View File

@ -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 %}

View File

@ -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>

View File

@ -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">&laquo;&laquo;</a></li>
<li><a href="?{{ page_var }}={{ ticket_list.previous_page_number }}">&laquo;</a></li>
{% else %}
<li class="disabled"><span>&laquo;&laquo;</span></li>
<li class="disabled"><span>&laquo;</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 }}">&raquo;</a></li>
<li><a href="?{{ page_var }}={{ ticket_list.paginator.num_pages }}">&raquo;&raquo;</a></li>
{% else %}
<li class="disabled"><span>&raquo;</span></li>
<li class="disabled"><span>&raquo;&raquo;</span></li>
{% endif %}
</ul>
{% endif %}
</div>
<div class="card-footer small text-muted">Listing {{ ticket_list|length }} ticket(s).</div>
</div>

View File

@ -5,7 +5,6 @@
{% block helpdesk_title %}{% trans "Tickets" %}{% endblock %}
{% block helpdesk_head %}
{% endblock %}

View File

@ -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.

View File

@ -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'),

View File

@ -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,

View File

@ -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

View File

@ -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",