Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Garret Wassermann 2019-04-01 21:29:18 -04:00
commit 9ca7f9fb46
15 changed files with 132 additions and 84 deletions

View File

@ -471,9 +471,12 @@ def object_from_message(message, queue, logger):
if part.get_content_maintype() == 'text' and name is None:
if part.get_content_subtype() == 'plain':
body = EmailReplyParser.parse_reply(
decodeUnknown(part.get_content_charset(), part.get_payload(decode=True))
)
body = part.get_payload(decode=True)
# https://github.com/django-helpdesk/django-helpdesk/issues/732
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)
body = EmailReplyParser.parse_reply(body)
# workaround to get unicode text out rather than escaped text
try:
body = body.encode('ascii').decode('unicode_escape')
@ -481,9 +484,15 @@ def object_from_message(message, queue, logger):
body.encode('utf-8')
logger.debug("Discovered plain text MIME part")
else:
payload = encoding.smart_bytes(part.get_payload(decode=True))
payload = """
<html>
<head>
<meta charset="utf-8"/>
</head>
%s
</html>""" % encoding.smart_text(part.get_payload(decode=True))
files.append(
SimpleUploadedFile(_("email_html_body.html"), payload, 'text/html')
SimpleUploadedFile(_("email_html_body.html"), payload.encode("utf-8"), 'text/html')
)
logger.debug("Discovered HTML MIME part")
else:

View File

@ -748,6 +748,10 @@ class Ticket(models.Model):
def get_markdown(self):
return get_markdown(self.description)
@property
def get_resolution_markdown(self):
return get_markdown(self.resolution)
class FollowUpManager(models.Manager):

View File

@ -19,6 +19,7 @@ class TicketSerializer(serializers.ModelSerializer):
status = serializers.SerializerMethodField()
row_class = serializers.SerializerMethodField()
time_spent = serializers.SerializerMethodField()
queue = serializers.SerializerMethodField()
class Meta:
model = Ticket
@ -27,6 +28,9 @@ class TicketSerializer(serializers.ModelSerializer):
'created', 'due_date', 'assigned_to', 'row_class',
'time_spent')
def get_queue(self, obj):
return ({"title": obj.queue.title, "id": obj.queue.id})
def get_ticket(self, obj):
return (str(obj.id) + " " + obj.ticket)

View File

@ -87,3 +87,11 @@ pre {
padding: 1em;
border: 1pt solid white;
}
table .tickettitle {
max-width: 250px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -8,12 +8,11 @@
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<table class="table table-bordered table-sm table-striped" id="dataTable" width="100%" cellspacing="0">
<thead class="thead-light">
<tr>
<th>#</th>
<th>{% trans "Pr" %}</th>
<th>{% trans "Title" %}</th>
<th>{% trans "Ticket" %}</th>
<th>{% trans "Priority" %}</th>
<th>{% trans "Queue" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Last Update" %}</th>
@ -22,9 +21,8 @@
<tbody>
{% for ticket in ticket_list %}
<tr class="{{ ticket.get_priority_css_class }}">
<td><a href='{{ ticket.get_absolute_url }}'>{{ ticket.ticket }}</a></td>
<td class="tickettitle"><a href="{{ ticket.get_absolute_url }}">{{ ticket.id }}. {{ ticket.title }}</a></td>
<td>{{ ticket.priority }}</td>
<td><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></td>
<td>{{ ticket.queue }}</td>
<td>{{ ticket.get_status }}</td>
<td><span title='{{ ticket.modified|date:"r" }}'>{{ ticket.modified|naturaltime }}</span></td>

View File

@ -8,28 +8,26 @@
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<table class="table table-bordered table-sm table-striped" id="dataTable" width="100%" cellspacing="0">
<thead class="thead-light">
<tr>
<th>#</th>
<th>{% trans "Pr" %}</th>
<th>{% trans "Title" %}</th>
<th>{% trans "Ticket" %}</th>
<th>{% trans "Prority" %}</th>
<th>{% trans "Queue" %}</th>
<th>{% trans "Status" %}</th>
<th>&nbsp;</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for ticket in unassigned_tickets %}
<tr class="{{ ticket.get_priority_css_class }}">
<td><a href='{{ ticket.get_absolute_url }}'>{{ ticket.ticket }}</a></td>
<td class="tickettitle"><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}. {{ ticket.title }} </a></td>
<td>{{ ticket.priority }}</td>
<td><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></td>
<td>{{ ticket.queue }}</td>
<td><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
<td>
<a href='{{ ticket.get_absolute_url }}?take'><button class='btn btn-primary btn-xs'><i class="fas fa-hand-paper"></i>&nbsp;{% trans "Take" %}</button></a> |
<a href='{% url 'helpdesk:delete' ticket.id %}'><button class='btn btn-danger btn-xs'><i class="fas fa-trash"></i>&nbsp;{% trans "Delete" %}</button></a>
<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 %}'><button class='btn btn-danger btn-sm'><i class="fas fa-trash"></i>&nbsp;{% trans "Delete" %}</button></a>
</td>
</tr>
{% empty %}

View File

@ -24,12 +24,13 @@
{% cycle 'one' 'two' 'three' as itemnumperrow silent %}
{% ifequal itemnumperrow 'one' %}<div class="card-deck">{% endifequal %}
<div class="card">
<div class="card-body">
<div class="card-header">
<h5 class="card-title">{{ item.title }}</h5>
</div>
<div class="card-body">
<p class="card-text">{{ item.question }}</p>
<p class="card-text">
<p>
{% blocktrans with item.get_absolute_url as url %}View <a href='{{ url }}'>Answer <i class="fa fa-arrow-right"></i></a>{% endblocktrans %}
{% blocktrans with item.get_absolute_url as url %}<a href='{{ url }}' class="btn btn-primary"> Go to answer <i class="fa fa-share"></i></a>{% endblocktrans %}
</p>
<div class="well well-sm">
<p>{% trans 'Rating' %}: {{ item.score }}</p>

View File

@ -18,10 +18,12 @@
{% cycle 'one' 'two' 'three' as catnumperrow silent %}
{% if catnumperrow == 'one' %}<div class="card-deck">{% endif %}
<div class="card">
<div class="card-header">
<h5>{{ category.title }}</h5>
</div>
<div class="card-body">
<h5 class="card-title">{{ category.title }}</h5>
<p class="card-text">{{ category.description }}</p>
<p class="card-text"><small class="text-muted"><a href='{{ category.get_absolute_url }}'>{% trans 'View articles' %}<i class="fa fa-arrow-right"></i></a></small></p>
<p class="card-text"><small class="text-muted"><a class="btn btn-primary" href='{{ category.get_absolute_url }}'>{% trans 'View articles' %} <i class="fa fa-share"></i></a></small></p>
</div>
</div>
{% if catnumperrow == 'three' %}</div>{% endif %}

View File

@ -11,7 +11,7 @@
{% endblock %}
{% block helpdesk_body %}
<h2>{% trans 'Knowledgebase' %}:{% blocktrans with item.title as item %}{{ item }}{% endblocktrans %}</h2>
<h2>{% trans 'Knowledgebase' %}: {% blocktrans with item.title as item %}{{ item }}{% endblocktrans %}</h2>
<div class="card mb-3">
<div class="card-header">

View File

@ -33,7 +33,7 @@
<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>
</tr>
<tr>
<td colspan='2'>{{ ticket.resolution|force_escape|urlizetrunc:50|linebreaksbr }}</td>
<td colspan='2'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td>
</tr>{% endif %}
<tr>
<th>{% trans "Due Date" %}</th>

View File

@ -41,7 +41,7 @@
<div class="card">
<div class="card-header" id="headingOne">
<h5 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<button class="btn btn-link btn-sm" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<i class="fas fa-question-circle"></i>
<strong>{% trans "Change Query" %}</strong>
</button>
@ -51,7 +51,9 @@
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#queryAccordion">
<div class="card-body">
<form>
<select name='select' id='filterBuilderSelect'>
<div class="form-group">
<label for="filterBuilderSelect">{% trans "Select filter" %}:</label>
<select aria-describedby="select-description" name='select' id='filterBuilderSelect'>
<option value='Sort'>{% trans "Sorting" %}</option>
<option value='Owner'>{% trans "Owner" %}</option>
<option value='Queue'>{% trans "Queue" %}</option>
@ -59,10 +61,13 @@
<option value='Keywords'>{% trans "Keywords" %}</option>
<option value='Dates'>{% trans "Date Range" %}</option>
</select>
<button class='btn btn-success btn-sm' id='filterBuilderButton'><i class="fas fa-plus-circle"></i></button>
<small id="select-description" class="form-text text-muted">{% trans "Add another filter to list of filers for finer selection of displayed tickets" %}</small>
<button class='btn btn-success btn-sm' id='filterBuilderButton'>{% trans "Add selected filter" %} <i class="fas fa-plus-circle"></i></button>
{% csrf_token %}</form>
</div>
<form method='get' action='./'>
<div class="d-flex align-items-stretch">
<div class='thumbnail filterBox{% if query_params.sorting %} filterBoxShow{% endif %}' id='filterBoxSort'>
<label for='id_sort'>{% trans "Sorting" %}</label>
<select id='id_sort' name='sort'>
@ -128,15 +133,17 @@
<p class='filterHelp'>{% trans "Keywords are case-insensitive, and will be looked for in the title, body and submitter fields." %}</p>
<button class='filterBuilderRemove btn btn-danger btn-sm'><i class="fas fa-trash-alt"></i></button>
</div>
</div> <!-- end div d-flex class -->
<hr style='clear: both;' />
<input class="btn btn-primary" type='submit' value='{% trans "Apply Filter" %}' />
<input class="btn btn-primary btn-sm" type='submit' value='{% trans "Apply Filters" %}' />
{% if from_saved_query and saved_query.user == user %}
<p>{% blocktrans with saved_query.title as query_name %}You are currently viewing saved query <strong>"{{ query_name }}"</strong>.{% endblocktrans %} <a href='{% url 'helpdesk:delete_query' saved_query.id %}'>{% trans "Delete Saved Query" %}</a></p>
{% endif %}
{% if from_saved_query %}
<p>{% blocktrans with saved_query.id as query_id %}<a href='../reports/?saved_query={{ query_id }}'>Run a report</a> on this query to see stats and charts for the data listed below.{% endblocktrans %}</p>
{% endif %}
{% csrf_token %}</form>
{% csrf_token %}
</form>
</div>
</div>
</div> <!-- end card -->
@ -144,7 +151,7 @@
<div class="card">
<div class="card-header" id="headingTwo">
<h5 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<button class="btn btn-link collapsed btn-sm" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<i class="fas fa-save"></i>
<strong>{% trans "Save Query" %}</strong>
</button>
@ -175,7 +182,7 @@
<div class="card">
<div class="card-header" id="headingThree">
<h5 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<button class="btn btn-link collapsed btn-sm" type="button" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<i class="fas fa-clipboard-check"></i>
<strong>{% trans "Use Saved Query" %}</strong>
</button>
@ -210,13 +217,12 @@
<div class="card-body">
{{ search_message|safe }}
<form method='post' action='{% url 'helpdesk:mass_update' %}' id="ticket_mass_update">
<table width="100%" class="table table-striped table-bordered table-hover" id="ticketTable" data-page-length='{{ default_tickets_per_page }}'>
<thead>
<table width="100%" class="table table-sm table-striped table-bordered table-hover" id="ticketTable" data-page-length='{{ default_tickets_per_page }}'>
<thead class="thead-light">
<tr>
<th>#</th>
<th>&nbsp;</th>
<th>{% trans "Pr" %}</th>
<th>{% trans "Title" %}</th>
<th>{% trans "Ticket" %}</th>
<th>{% trans "Prority" %}</th>
<th>{% trans "Queue" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Created" %}</th>
@ -301,18 +307,6 @@
},
"columns": [
{"data": "ticket",
"render": function (data, type, row, meta)
{
var id = data.split(" ")[0];
var name = data.split(" ")[1];
if (type === 'display')
{
data = '<b><a href="' + get_url(row) + '" >' + name + '</a></b>';
}
return data
}
},
{"data": "id",
"orderable": false,
"render": function(data, type, row, meta)
@ -324,22 +318,49 @@
return data
}
},
{"data": "priority"},
{"data": "title",
{"data": "ticket",
"render": function (data, type, row, meta)
{
if (type === 'display')
{
data = '<b><a href="' + get_url(row) + '" >' + data + '</a></b>';
}
return data
var id = data.split(" ")[0];
var name = data.split(" ")[1];
if (type === 'display')
{
data = '<div class="tickettitle"><a href="' + get_url(row) + '" >' +
row.id + '. ' +
row.title + '</a></div>';
}
return data
}
},
{"data": "queue"},
{"data": "priority",
"render": function (data, type, row, meta) {
var priority = "success";
if (data == 4 ) {
priority = "warning";
} else if (data == 5) {
priority = "danger";
}
return '<p class="text-'+priority+'">'+data+'</p>';
}
},
{"data": "queue",
"render": function(data, type, row, meta) {
return data.title;
}
},
{"data": "status"},
{"data": "created"},
{"data": "due_date"},
{"data": "assigned_to"},
{"data": "assigned_to",
"render": function(data, type, row, meta) {
if (data != "None") {
return data;
}
else {
return "";
}
}
},
{"data": "time_spent"},
]
});

View File

@ -5,7 +5,7 @@
<tr class="{{ ticket.get_priority_css_class }}">
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.ticket }}</a></th>
<td><input type='checkbox' name='ticket_id' value='{{ ticket.id }}' class='ticket_multi_select' /></td>
<td>{{ ticket.priority }}</td>
<td>{{ ticket.priority }}|||||</td>
<th><a href='{{ ticket.get_absolute_url }}'>{{ ticket.title }}</a></th>
<td>{{ ticket.queue }}</td>
<td>{{ ticket.get_status }}</td>

View File

@ -53,7 +53,7 @@ class GetEmailCommonTests(TestCase):
with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml")) as fd:
test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger)
self.assertEqual(ticket.title, "FollowUpAttachment without body")
self.assertEqual(ticket.title, "Attachment without body")
self.assertEqual(ticket.description, "")
def test_email_with_quoted_printable_body(self):
@ -71,7 +71,7 @@ class GetEmailCommonTests(TestCase):
attachments = FollowUpAttachment.objects.filter(followup=followup)
self.assertEqual(len(attachments), 1)
attachment = attachments[0]
self.assertEqual(attachment.file.read().decode("utf-8"), '<div dir="ltr">Tohle je test českých písmen odeslaných z gmailu.</div>\n')
self.assertIn('<div dir="ltr">Tohle je test českých písmen odeslaných z gmailu.</div>\n', attachment.file.read().decode("utf-8"))
def test_email_with_8bit_encoding_and_utf_8(self):
"""

View File

@ -138,13 +138,15 @@ def dashboard(request):
showing ticket counts by queue/status, and a list of unassigned tickets
with options for them to 'Take' ownership of said tickets.
"""
# open & reopened tickets, assigned to current user
tickets = Ticket.objects.select_related('queue').filter(
assigned_to=request.user,
).exclude(
active_tickets = Ticket.objects.select_related('queue').exclude(
status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS],
)
# open & reopened tickets, assigned to current user
tickets = active_tickets.filter(
assigned_to=request.user,
)
# closed & resolved tickets, assigned to current user
tickets_closed_resolved = Ticket.objects.select_related('queue').filter(
assigned_to=request.user,
@ -152,11 +154,9 @@ def dashboard(request):
user_queues = _get_user_queues(request.user)
unassigned_tickets = Ticket.objects.select_related('queue').filter(
unassigned_tickets = active_tickets.filter(
assigned_to__isnull=True,
queue__in=user_queues
).exclude(
status=Ticket.CLOSED_STATUS,
)
# all tickets, reported by current user
@ -269,7 +269,7 @@ def followup_edit(request, ticket_id, followup_id):
new_followup.user = followup.user
new_followup.save()
# get list of old attachments & link them to new_followup
attachments = FolllowUpAttachment.objects.filter(followup=followup)
attachments = FollowUpAttachment.objects.filter(followup=followup)
for attachment in attachments:
attachment.followup = new_followup
attachment.save()
@ -1581,7 +1581,7 @@ def attachment_del(request, ticket_id, attachment_id):
if not _is_my_ticket(request.user, ticket):
raise PermissionDenied()
attachment = get_object_or_404(FolllowUpAttachment, id=attachment_id)
attachment = get_object_or_404(FollowUpAttachment, id=attachment_id)
if request.method == 'POST':
attachment.delete()
return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket_id]))

View File

@ -27,7 +27,8 @@ class QuickDjangoTest(object):
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.staticfiles',
'bootstrap4form'
'bootstrap4form',
'helpdesk',
)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
@ -62,7 +63,7 @@ class QuickDjangoTest(object):
def __init__(self, *args, **kwargs):
self.apps = args
self.tests = args
self._tests()
def _tests(self):
@ -79,7 +80,7 @@ class QuickDjangoTest(object):
'PORT': '',
}
},
INSTALLED_APPS=self.INSTALLED_APPS + self.apps,
INSTALLED_APPS=self.INSTALLED_APPS,
MIDDLEWARE=self.MIDDLEWARE,
ROOT_URLCONF='helpdesk.tests.urls',
STATIC_URL='/static/',
@ -92,7 +93,7 @@ class QuickDjangoTest(object):
test_runner = DiscoverRunner(verbosity=1)
django.setup()
failures = test_runner.run_tests(self.apps)
failures = test_runner.run_tests(self.tests)
if failures:
sys.exit(failures)
@ -102,13 +103,15 @@ if __name__ == '__main__':
Example usage:
$ python quicktest.py app1 app2
$ python quicktest.py test1 test2
"""
parser = argparse.ArgumentParser(
usage="[args]",
description="Run Django tests on the provided applications."
description="Run Django tests."
)
parser.add_argument('apps', nargs='+', type=str)
parser.add_argument('tests', nargs="*", type=str)
args = parser.parse_args()
QuickDjangoTest(*args.apps)
if not args.tests:
args.tests = ['helpdesk']
QuickDjangoTest(*args.tests)