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 Ross Poulton (Trading as Jutda),
Copyright (c) 2008-2019 django-helpdesk contributors. Copyright (c) 2008-2020 django-helpdesk contributors.
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, 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. #: demo - Setup demo project using Python3.
.PHONY: demo .PHONY: demo
demo: demo:
$(PIP) install -e . --user # running it with and without --user flag because it started to be problematic for some setups
$(PIP) install -e demo --user $(PIP) install -e . --user || $(PIP) install -e .
$(PIP) install -e demo --user || $(PIP) install -e demo
demodesk migrate --noinput demodesk migrate --noinput
# Create superuser; user will be prompted to manually set a password # Create superuser; user will be prompted to manually set a password
# When you get a prompt, enter a password of your choosing. # When you get a prompt, enter a password of your choosing.

View File

@ -38,7 +38,12 @@ INSTALLED_APPS = [
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.humanize', 'django.contrib.humanize',
'bootstrap4form', '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 = [ MIDDLEWARE = [

View File

@ -19,9 +19,9 @@ AUTHOR = 'django-helpdesk team'
URL = 'https://github.com/django-helpdesk/django-helpdesk' URL = 'https://github.com/django-helpdesk/django-helpdesk'
CLASSIFIERS = ['Development Status :: 4 - Beta', CLASSIFIERS = ['Development Status :: 4 - Beta',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Framework :: Django :: 2.0', 'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1', 'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2'] '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. 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:: 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 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') (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/ 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. 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"`` **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. - **QUEUE_EMAIL_BOX_UPDATE_ONLY** Only process mail with a valid tracking ID; all other mail will be ignored instead of creating a new ticket.
**Default:** ``QUEUE_EMAIL_BOX_UPDATE_ONLY = False`` **Default:** ``QUEUE_EMAIL_BOX_UPDATE_ONLY = False``

View File

@ -74,6 +74,8 @@ def process_email(quiet=False):
if quiet: if quiet:
logger.propagate = False # do not propagate to root logger that would log to console logger.propagate = False # do not propagate to root logger that would log to console
logdir = q.logging_dir or '/var/log/helpdesk/' logdir = q.logging_dir or '/var/log/helpdesk/'
try:
handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log')) handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log'))
logger.addHandler(handler) logger.addHandler(handler)
@ -86,6 +88,15 @@ def process_email(quiet=False):
process_queue(q, logger=logger) process_queue(q, logger=logger)
q.email_box_last_check = timezone.now() q.email_box_last_check = timezone.now()
q.save() q.save()
finally:
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): def pop3_sync(q, logger, server):
@ -428,7 +439,13 @@ def object_from_message(message, queue, logger):
sender = message.get('from', _('Unknown Sender')) sender = message.get('from', _('Unknown Sender'))
sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender)) sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender))
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 = '', '' body_plain, body_html = '', ''

View File

@ -129,7 +129,7 @@ def text_is_spam(text, request):
def process_attachments(followup, attached_files): 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 = [] attachments = []
for attached in attached_files: for attached in attached_files:
@ -149,7 +149,7 @@ def process_attachments(followup, attached_files):
if attached.size < max_email_attachment_size: if attached.size < max_email_attachment_size:
# Only files smaller than 512kb (or as defined in # 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]) attachments.append([filename, att.file])
return attachments 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) return super(KBItem, self).save(*args, **kwargs)
def _score(self): def _score(self):
""" Return a score out of 10 or Unrated if no votes """
if self.votes > 0: if self.votes > 0:
return int(self.recommendations / self.votes) return (self.recommendations / self.votes) * 10
else: else:
return _('Unrated') return _('Unrated')
score = property(_score) 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 # default fallback locale when queue locale not found
HELPDESK_EMAIL_FALLBACK_LOCALE = getattr(settings, 'HELPDESK_EMAIL_FALLBACK_LOCALE', 'en') 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 # # options for staff.create_ticket view #

View File

@ -24,18 +24,21 @@
{% if all_tickets_reported_by_current_user %} {% if all_tickets_reported_by_current_user %}
{% trans "All Tickets submitted by you" as ticket_list_caption %} {% 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 %} {% endif %}
{% trans "Open Tickets assigned to you (you are working on this ticket)" as ticket_list_caption %} {% 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 %} {% 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' %} {% include 'helpdesk/include/unassigned.html' %}
{% if user_tickets_closed_resolved %} {% if user_tickets_closed_resolved %}
{% trans "Closed & resolved Tickets you used to work on" as ticket_list_caption %} {% 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 %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -40,6 +40,7 @@
{% endcomment %} {% endcomment %}
<div class='buttons form-group'> <div class='buttons form-group'>
<input type='submit' class="btn btn-primary btn-sm" value='{% trans "Save Changes" %}' /> <input type='submit' class="btn btn-primary btn-sm" value='{% trans "Save Changes" %}' />
<a href='{{ ticket.get_absolute_url }}'><button class="btn btn-danger">{% trans "Cancel Changes" %}</button></a>
</div> </div>
</fieldset> </fieldset>

View File

@ -1,5 +1,6 @@
{% load i18n humanize %} {% load i18n humanize %}
<<<<<<< HEAD
<!-- DataTables Example --> <!-- DataTables Example -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
@ -33,6 +34,37 @@
</tbody> </tbody>
</table> </table>
</div> </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>
<div class="card-footer small text-muted">Listing {{ ticket_list|length }} ticket(s).</div> <div class="card-footer small text-muted">Listing {{ ticket_list|length }} ticket(s).</div>
</div> </div>

View File

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

View File

@ -205,6 +205,79 @@ class GetEmailParametricTemplate(object):
self.assertEqual(ticket2.title, test_email_subject) self.assertEqual(ticket2.title, test_email_subject)
self.assertEqual(ticket2.description, test_email_body) 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): def test_read_email_with_template_tag(self):
"""Tests reading plain text emails from a queue and creating tickets, """Tests reading plain text emails from a queue and creating tickets,
except this time the email body contains a Django template tag. 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. # This submission is spam. Let's not save it.
return render(request, template_name='helpdesk/public_spam.html') return render(request, template_name='helpdesk/public_spam.html')
else: else:
ticket = form.save() ticket = form.save(user=self.request.user if self.request.user.is_authenticated else None)
try: try:
return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % ( return HttpResponseRedirect('%s?ticket=%s&email=%s&key=%s' % (
reverse('helpdesk:public_view'), 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.contrib.contenttypes.models import ContentType
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.core.exceptions import ValidationError, PermissionDenied from django.core.exceptions import ValidationError, PermissionDenied
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect, Http404, HttpResponse, JsonResponse from django.http import HttpResponseRedirect, Http404, HttpResponse, JsonResponse
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
@ -31,6 +32,7 @@ from helpdesk.query import (
query_to_dict, query_to_dict,
query_to_base64, query_to_base64,
query_from_base64, query_from_base64,
apply_query,
) )
from helpdesk.user import HelpdeskUser from helpdesk.user import HelpdeskUser
@ -157,6 +159,41 @@ def dashboard(request):
else: else:
where_clause = """WHERE q.id = t.queue_id""" 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', { return render(request, 'helpdesk/dashboard.html', {
'user_tickets': tickets, 'user_tickets': tickets,
'user_tickets_closed_resolved': tickets_closed_resolved, '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 django-bootstrap4-form
celery celery
django-celery-beat django-celery-beat
@ -12,4 +12,6 @@ pytz
six six
djangorestframework djangorestframework
django-model-utils 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", "Development Status :: 4 - Beta",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Framework :: Django", "Framework :: Django",
"Framework :: Django :: 2.0", "Framework :: Django :: 2.0",
"Framework :: Django :: 2.1", "Framework :: Django :: 2.1",