Sync with upstream merges

This commit is contained in:
Garret Wassermann 2019-03-09 19:01:51 -05:00
commit ff74a8c21b
29 changed files with 496 additions and 63 deletions

View File

@ -37,7 +37,6 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.humanize',
'markdown_deux',
'bootstrap4form',
'helpdesk'
]

View File

@ -57,7 +57,6 @@ errors with trying to create User settings.
'django.contrib.sites', # Required for determining domain url for use in emails
'django.contrib.admin', # Required for helpdesk admin/maintenance
'django.contrib.humanize', # Required for elapsed time formatting
'markdown_deux', # Required for Knowledgebase item formatting
'bootstrap4form', # Required for nicer formatting of forms with the default templates
'helpdesk', # This is us!
)
@ -114,21 +113,17 @@ errors with trying to create User settings.
Ideally, accessing http://MEDIA_URL/helpdesk/attachments/ will give you a 403 access denied error.
7. If it's not already installed, install ``markdown_deux`` and ensure it's in your ``INSTALLED_APPS``::
pip install django-markdown-deux
8. If you already have a view handling your logins, then great! If not, add the following to ``settings.py`` to get your Django installation to use the login view included in ``django-helpdesk``::
7. If you already have a view handling your logins, then great! If not, add the following to ``settings.py`` to get your Django installation to use the login view included in ``django-helpdesk``::
LOGIN_URL = '/helpdesk/login/'
Alter the URL to suit your installation path.
9. Load initial e-mail templates, otherwise you will not be able to send e-mail::
8. Load initial e-mail templates, otherwise you will not be able to send e-mail::
python manage.py loaddata emailtemplate.json
10. If you intend on using local mail directories for processing email into tickets, be sure to create the mail directory before adding it to the queue in the Django administrator interface. The default mail directory is ``/var/lib/mail/helpdesk/``. Ensure that the directory has appropriate permissions so that your Django/web server instance may read and write files from this directory.
9. If you intend on using local mail directories for processing email into tickets, be sure to create the mail directory before adding it to the queue in the Django administrator interface. The default mail directory is ``/var/lib/mail/helpdesk/``. Ensure that the directory has appropriate permissions so that your Django/web server instance may read and write files from this directory.
Note that by default, any mail files placed in your local directory will be permanently deleted after being successfully processed. It is strongly recommended that you take further steps to save emails if you wish to retain backups.

View File

@ -2,19 +2,28 @@ from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory
from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem
from helpdesk.models import TicketChange, Attachment, IgnoreEmail
from helpdesk.models import TicketChange, KBIAttachment, FollowUpAttachment, IgnoreEmail
from helpdesk.models import CustomField
@admin.register(Queue)
class QueueAdmin(admin.ModelAdmin):
list_display = ('title', 'slug', 'email_address', 'locale')
list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent')
prepopulated_fields = {"slug": ("title",)}
def time_spent(self, q):
if q.dedicated_time:
return "{} / {}".format(q.time_spent, q.dedicated_time)
elif q.time_spent:
return q.time_spent
else:
return "-"
@admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin):
list_display = ('title', 'status', 'assigned_to', 'queue', 'hidden_submitter_email',)
list_display = ('title', 'status', 'assigned_to', 'queue',
'hidden_submitter_email', 'time_spent')
date_hierarchy = 'created'
list_filter = ('queue', 'assigned_to', 'status')
@ -28,19 +37,30 @@ class TicketAdmin(admin.ModelAdmin):
return ticket.submitter_email
hidden_submitter_email.short_description = _('Submitter E-Mail')
def time_spent(self, ticket):
return ticket.time_spent
class TicketChangeInline(admin.StackedInline):
model = TicketChange
extra = 0
class AttachmentInline(admin.StackedInline):
model = Attachment
class FollowUpAttachmentInline(admin.StackedInline):
model = FollowUpAttachment
extra = 0
class KBIAttachmentInline(admin.StackedInline):
model = KBIAttachment
extra = 0
@admin.register(FollowUp)
class FollowUpAdmin(admin.ModelAdmin):
inlines = [TicketChangeInline, AttachmentInline]
list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket', 'user', 'new_status')
inlines = [TicketChangeInline, FollowUpAttachmentInline]
list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket',
'user', 'new_status', 'time_spent')
list_filter = ('user', 'date', 'new_status')
def ticket_get_ticket_for_url(self, obj):
@ -51,6 +71,7 @@ class FollowUpAdmin(admin.ModelAdmin):
@admin.register(KBItem)
class KBItemAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'last_updated',)
inlines = [KBIAttachmentInline]
readonly_fields = ('voted_by',)
list_display_links = ('title',)

View File

@ -481,8 +481,9 @@ 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))
files.append(
SimpleUploadedFile(_("email_html_body.html"), encoding.smart_bytes(part.get_payload()), 'text/html')
SimpleUploadedFile(_("email_html_body.html"), payload, 'text/html')
)
logger.debug("Discovered HTML MIME part")
else:

View File

@ -17,7 +17,7 @@ from django.contrib.auth import get_user_model
from django.utils import timezone
from helpdesk.lib import safe_template_context, process_attachments
from helpdesk.models import (Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC,
from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC,
CustomField, TicketCustomFieldValue, TicketDependency, UserSettings)
from helpdesk import settings as helpdesk_settings

View File

@ -15,7 +15,7 @@ from django.db.models import Q
from django.utils.encoding import smart_text, smart_str
from django.utils.safestring import mark_safe
from helpdesk.models import Attachment, EmailTemplate
from helpdesk.models import FollowUpAttachment, EmailTemplate
from model_utils import Choices
@ -218,7 +218,7 @@ def process_attachments(followup, attached_files):
if attached.size:
filename = smart_text(attached.name)
att = Attachment(
att = FollowUpAttachment(
followup=followup,
file=attached,
filename=filename,

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.5 on 2019-02-06 13:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0023_add_enable_notifications_on_email_events_to_ticket'),
]
operations = [
migrations.AddField(
model_name='followup',
name='time_spent',
field=models.DurationField(blank=True, help_text='Time spent on this follow up', null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.5 on 2019-02-19 21:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0024_time_spent'),
]
operations = [
migrations.AddField(
model_name='queue',
name='dedicated_time',
field=models.DurationField(blank=True, help_text='Time to be spent on this Queue in total', null=True),
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 2.0.5 on 2019-03-07 20:30
from django.db import migrations, models
import django.db.models.deletion
import helpdesk.models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0025_queue_dedicated_time'),
]
operations = [
migrations.CreateModel(
name='KBIAttachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(max_length=1000, upload_to=helpdesk.models.attachment_path, verbose_name='File')),
('filename', models.CharField(max_length=1000, verbose_name='Filename')),
('mime_type', models.CharField(max_length=255, verbose_name='MIME Type')),
('size', models.IntegerField(help_text='Size of this file in bytes', verbose_name='Size')),
('kbitem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='helpdesk.KBItem', verbose_name='Knowledge base item')),
],
options={
'verbose_name': 'Attachment',
'verbose_name_plural': 'Attachments',
'ordering': ('filename',),
'abstract': False,
},
),
migrations.RenameModel(
old_name='Attachment',
new_name='FollowUpAttachment',
),
]

View File

@ -17,12 +17,41 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext
from io import StringIO
import re
import os
import mimetypes
import datetime
from django.utils.safestring import mark_safe
from markdown import markdown
from markdown.extensions import Extension
import uuid
from .templated_email import send_templated_mail
class EscapeHtml(Extension):
def extendMarkdown(self, md, md_globals):
del md.preprocessors['html_block']
del md.inlinePatterns['html']
def get_markdown(text):
if not text:
return ""
return mark_safe(
markdown(
text,
extensions=[
EscapeHtml(), 'markdown.extensions.nl2br',
'markdown.extensions.fenced_code'
]
)
)
class Queue(models.Model):
"""
A queue is a collection of tickets into what would generally be business
@ -275,6 +304,11 @@ class Queue(models.Model):
verbose_name=_('Default owner'),
)
dedicated_time = models.DurationField(
help_text=_("Time to be spent on this Queue in total"),
blank=True, null=True
)
def __str__(self):
return "%s" % self.title
@ -301,6 +335,17 @@ class Queue(models.Model):
return u'%s <%s>' % (self.title, self.email_address)
from_address = property(_from_address)
@property
def time_spent(self):
"""Return back total time spent on the ticket. This is calculated value
based on total sum from all FollowUps
"""
total = datetime.timedelta(0)
for val in self.ticket_set.all():
if val.time_spent:
total = total + val.time_spent
return total
def prepare_permission_name(self):
"""Prepare internally the codename for the permission and store it in permission_name.
:return: The codename that can be used to create a new Permission object.
@ -497,6 +542,17 @@ class Ticket(models.Model):
default=mk_secret,
)
@property
def time_spent(self):
"""Return back total time spent on the ticket. This is calculated value
based on total sum from all FollowUps
"""
total = datetime.timedelta(0)
for val in self.followup_set.all():
if val.time_spent:
total = total + val.time_spent
return total
def send(self, roles, dont_send_to=None, **kwargs):
"""
Send notifications to everyone interested in this ticket.
@ -689,6 +745,9 @@ class Ticket(models.Model):
queue = '-'.join(parts[0:-1])
return queue, parts[-1]
def get_markdown(self):
return get_markdown(self.description)
class FollowUpManager(models.Manager):
@ -740,8 +799,10 @@ class FollowUp(models.Model):
_('Public'),
blank=True,
default=False,
help_text=_('Public tickets are viewable by the submitter and all '
'staff, but non-public tickets can only be seen by staff.'),
help_text=_(
'Public tickets are viewable by the submitter and all '
'staff, but non-public tickets can only be seen by staff.'
),
)
user = models.ForeignKey(
@ -771,6 +832,11 @@ class FollowUp(models.Model):
objects = FollowUpManager()
time_spent = models.DurationField(
help_text=_("Time spent on this follow up"),
blank=True, null=True
)
class Meta:
ordering = ('date',)
verbose_name = _('Follow-up')
@ -788,6 +854,9 @@ class FollowUp(models.Model):
t.save()
super(FollowUp, self).save(*args, **kwargs)
def get_markdown(self):
return get_markdown(self.comment)
class TicketChange(models.Model):
"""
@ -837,18 +906,8 @@ class TicketChange(models.Model):
def attachment_path(instance, filename):
"""
Provide a file path that will help prevent files being overwritten, by
putting attachments in a folder off attachments for ticket/followup_id/.
"""
import os
os.umask(0)
path = 'helpdesk/attachments/%s-%s/%s' % (instance.followup.ticket.ticket_for_url, instance.followup.ticket.secret_key, instance.followup.id)
att_path = os.path.join(settings.MEDIA_ROOT, path)
if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage":
if not os.path.exists(att_path):
os.makedirs(att_path, 0o777)
return os.path.join(path, filename)
"""Just bridge"""
return instance.attachment_path(filename)
class Attachment(models.Model):
@ -857,12 +916,6 @@ class Attachment(models.Model):
attachment, or it could be uploaded via the web interface.
"""
followup = models.ForeignKey(
FollowUp,
on_delete=models.CASCADE,
verbose_name=_('Follow-up'),
)
file = models.FileField(
_('File'),
upload_to=attachment_path,
@ -871,26 +924,102 @@ class Attachment(models.Model):
filename = models.CharField(
_('Filename'),
blank=True,
max_length=1000,
)
mime_type = models.CharField(
_('MIME Type'),
blank=True,
max_length=255,
)
size = models.IntegerField(
_('Size'),
blank=True,
help_text=_('Size of this file in bytes'),
)
def __str__(self):
return '%s' % self.filename
def save(self, *args, **kwargs):
if not self.size:
self.size = self.get_size()
if not self.filename:
self.filename = self.get_filename()
if not self.mime_type:
self.mime_type = \
mimetypes.guess_type(self.filename, strict=False)[0] or \
'application/octet-stream',
return super(Attachment, self).save(*args, **kwargs)
def get_filename(self):
return str(self.file)
def get_size(self):
return self.file.file.size
def attachment_path(self, filename):
"""Provide a file path that will help prevent files being overwritten, by
putting attachments in a folder off attachments for ticket/followup_id/.
"""
assert NotImplementedError(
"This method is to be implemented by Attachment classes"
)
class Meta:
ordering = ('filename',)
verbose_name = _('Attachment')
verbose_name_plural = _('Attachments')
abstract = True
class FollowUpAttachment(Attachment):
followup = models.ForeignKey(
FollowUp,
on_delete=models.CASCADE,
verbose_name=_('Follow-up'),
)
def attachment_path(self, filename):
os.umask(0)
path = 'helpdesk/attachments/{ticket_for_url}-{secret_key}/{id_}'.format(
ticket_for_url=self.followup.ticket.ticket_for_url,
secret_key=self.followup.ticket.secret_key,
id_=self.followup.id)
att_path = os.path.join(settings.MEDIA_ROOT, path)
if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage":
if not os.path.exists(att_path):
os.makedirs(att_path, 0o777)
return os.path.join(path, filename)
class KBIAttachment(Attachment):
kbitem = models.ForeignKey(
"KBItem",
on_delete=models.CASCADE,
verbose_name=_('Knowledge base item'),
)
def attachment_path(self, filename):
os.umask(0)
path = 'helpdesk/attachments/kb/{category}/{kbi}'.format(
category=self.kbitem.category,
kbi=self.kbitem.id)
att_path = os.path.join(settings.MEDIA_ROOT, path)
if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage":
if not os.path.exists(att_path):
os.makedirs(att_path, 0o777)
return os.path.join(path, filename)
class PreSetReply(models.Model):
@ -1128,6 +1257,9 @@ class KBItem(models.Model):
from django.urls import reverse
return reverse('helpdesk:kb_item', args=(self.id,))
def get_markdown(self):
return get_markdown(self.answer)
class SavedSearch(models.Model):
"""

View File

@ -18,11 +18,14 @@ class TicketSerializer(serializers.ModelSerializer):
due_date = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
row_class = serializers.SerializerMethodField()
time_spent = serializers.SerializerMethodField()
class Meta:
model = Ticket
# fields = '__all__'
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status', 'created', 'due_date', 'assigned_to', 'row_class')
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
'created', 'due_date', 'assigned_to', 'row_class',
'time_spent')
def get_ticket(self, obj):
return (str(obj.id) + " " + obj.ticket)
@ -45,5 +48,8 @@ class TicketSerializer(serializers.ModelSerializer):
else:
return ("None")
def get_time_spent(self, obj):
return str(obj.time_spent)
def get_row_class(self, obj):
return (obj.get_priority_css_class)

View File

@ -81,3 +81,9 @@ img.brand {padding-right: 30px;}
div.card-body img {
max-width: 900px;
}
pre {
background: #eee;
padding: 1em;
border: 1pt solid white;
}

View File

@ -46,6 +46,8 @@
<dt><label for="id_new_status">New Status:</label></dt>
<dd>{{ form.new_status }}</dd>
<p>If the status was changed, what was it changed to?</p>
<dt><label for="id_time_spent">Time spent:</label></dt>
<dd>{{ form.time_spent }}</dd>
</dl>
</fieldset>
<p><input class="btn btn-primary" type="submit" value="Submit"></p>{% csrf_token %}

View File

@ -1,4 +1,4 @@
{% extends "helpdesk/public_base.html" %}{% load i18n %}{% load markdown_deux_tags %}
{% extends "helpdesk/public_base.html" %}{% load i18n %}
{% block helpdesk_breadcrumb %}
<li class="breadcrumb-item">
@ -19,7 +19,7 @@
{{ item.question }}
</div>
<div class="card-body">
<p>{{ item.answer|markdown }}</p>
<p>{{ item.get_markdown }}</p>
</div>
<div class="card-footer">
<div class="row">

View File

@ -66,7 +66,7 @@
<li>{% blocktrans with change.field as field and change.old_value as old_value and change.new_value as new_value %}Changed {{ field }} from {{ old_value }} to {{ new_value }}.{% endblocktrans %}</li>
{% endfor %}
</ul></div>{% endif %}
{% for attachment in followup.attachment_set.all %}{% if forloop.first %}<div class='attachments'><ul>{% endif %}
{% for attachment in followup.followupattachment_set.all %}{% if forloop.first %}<div class='attachments'><ul>{% endif %}
<li><a href='{{ attachment.file.url }}'>{{ attachment.filename }}</a> ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})</li>
{% if forloop.last %}</ul></div>{% endif %}
{% endfor %}

View File

@ -45,6 +45,7 @@
<th>{% trans "Open" %}</th>
<th>{% trans "Resolved" %}</th>
<th>{% trans "Closed" %}</th>
<th>{% trans "Time spent" %}</th>
</tr>
</thead>
<tbody>
@ -54,6 +55,7 @@
<td>{% if queue.open %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=1&status=2'>{% endif %}{{ queue.open }}{% if queue.open %}</a>{% endif %}</td>
<td>{% if queue.resolved %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=3'>{% endif %}{{ queue.resolved }}{% if queue.resolved %}</a>{% endif %}</td>
<td>{% if queue.closed %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=4'>{% endif %}{{ queue.closed }}{% if queue.closed %}</a>{% endif %}</td>
<td>{{ queue.time_spent }}{% if queue.dedicated_time %} / {{ queue.dedicated_time }}{% endif %}</td>
</tr>
{% empty %}
<tr><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr>

View File

@ -2,6 +2,8 @@
{% load i18n bootstrap4form humanize %}
{% load static from staticfiles %}
{% block helpdesk_title %}{{ ticket.queue.slug }}-{{ ticket.id }} : {% trans "View Ticket Details" %}{% endblock %}
{% block helpdesk_head %}
@ -44,7 +46,10 @@
</div>
<p class="mb-1">
{% if followup.comment %}
<p>{{ followup.comment|force_escape|urlizetrunc:50|num_to_link|linebreaksbr }}</p>
<p>{{ followup.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</p>
{% endif %}
{% if followup.time_spent %}
<small>{% trans "Time spent" %}: {{ followup.time_spent }}</small></p>
{% endif %}
{% for change in followup.ticketchange_set.all %}
{% if forloop.first %}<div class='changes'><ul>{% endif %}
@ -152,6 +157,11 @@
<dd><input type='checkbox' name='public' value='1' checked='checked' />&nbsp; {% trans 'Yes, make this update public.' %}</dd>
<dd class='form_help_text'>{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}</dd>
{% endif %}
<dt>
<label for='id_time_spent'>{% trans "Time spent" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span>
</dt>
<dd><input name='time_spent' type="time" /></dd>
</dl>
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details &raquo;" %}</button></p>

View File

@ -26,7 +26,7 @@
<th colspan='2'>{% trans "Description" %}</th>
</tr>
<tr>
<td id="ticket-description" colspan='2'>{{ ticket.description|force_escape|urlizetrunc:50|num_to_link|linebreaksbr }}</td>
<td id="ticket-description" colspan='2'>{{ ticket.get_markdown|urlizetrunc:50|num_to_link|linebreaksbr }}</td>
</tr>
{% if ticket.resolution %}<tr>
@ -89,6 +89,10 @@
<p><a data-toggle='tooltip' href='{% url 'helpdesk:ticket_dependency_add' ticket.id %}' title="{% trans "Click on 'Add Dependency', if you want to make this ticket dependent on another ticket. A ticket may not be closed until all tickets it depends on are closed." %}"><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-child"></i>&nbsp;{% trans "Add Dependency" %}</button></a></p>
</td>
</tr>
<tr>
<th>{% trans "Total time spent" %}</th>
<td>{{ ticket.time_spent }}</td>
</tr>
</tbody>
</table>
</div>

View File

@ -222,6 +222,7 @@
<th>{% trans "Created" %}</th>
<th>{% trans "Due Date" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Time Spent" %}</th>
</tr>
</thead>
{% if not server_side %}
@ -339,6 +340,7 @@
{"data": "created"},
{"data": "due_date"},
{"data": "assigned_to"},
{"data": "time_spent"},
]
});
})

View File

@ -12,6 +12,7 @@
<td data-order='{{ ticket.created|date:"U" }}'><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
<td data-order='{{ ticket.due_date|date:"U" }}'><span title='{{ ticket.due_date|date:"r" }}'>{{ ticket.due_date|naturaltime }}</span></td>
<td>{{ ticket.get_assigned_to }}</td>
<td>{{ ticket.time_spent }}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -58,7 +58,7 @@ class AttachmentIntegrationTests(TestCase):
self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content
att = models.Attachment.objects.get(followup__ticket=response.context['ticket'])
att = models.FollowUpAttachment.objects.get(followup__ticket=response.context['ticket'])
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = file_on_disk.read()
self.assertEqual(disk_content, 'attached file content')
@ -76,7 +76,7 @@ class AttachmentIntegrationTests(TestCase):
self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content
att = models.Attachment.objects.get(followup__ticket=response.context['ticket'])
att = models.FollowUpAttachment.objects.get(followup__ticket=response.context['ticket'])
with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = smart_text(file_on_disk.read(), 'utf-8')
self.assertEqual(disk_content, 'โจ')
@ -96,7 +96,7 @@ class AttachmentUnitTests(TestCase):
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
self.follow_up = models.FollowUp(ticket=models.Ticket(queue=models.Queue()))
@mock.patch('helpdesk.lib.Attachment', autospec=True)
@mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
def test_unicode_attachment_filename(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" check utf-8 data is parsed correcltly """
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0]
@ -109,7 +109,22 @@ class AttachmentUnitTests(TestCase):
)
self.assertEqual(filename, self.file_attrs['filename'])
@mock.patch.object(lib.Attachment, 'save', autospec=True)
@mock.patch('helpdesk.lib.FollowUpAttachment', autospec=True)
def xest_autofill_(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" check utf-8 data is parsed correcltly """
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0]
obj = mock_att_save.assert_called_with(
file=self.test_file,
filename=None,
mime_type=None,
size=None,
followup=self.follow_up
)
self.assertEqual(obj.filename, self.file_attrs['filename'])
self.assertEqual(obj.size, len(self.file_attrs['content']))
self.assertEqual(obj.mime_type, self.file_attrs['content-type'])
@mock.patch.object(lib.FollowUpAttachment, 'save', autospec=True)
@override_settings(MEDIA_ROOT=MEDIA_DIR)
def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" don't mock saving to filesystem to test file renames caused by storage layer """
@ -118,7 +133,7 @@ class AttachmentUnitTests(TestCase):
attachment_obj = mock_att_save.call_args[0][0]
mock_att_save.assert_called_once_with(attachment_obj)
self.assertIsInstance(attachment_obj, models.Attachment)
self.assertIsInstance(attachment_obj, models.FollowUpAttachment)
self.assertEqual(attachment_obj.filename, self.file_attrs['filename'])

View File

@ -0,0 +1,15 @@
To: helpdesk@auto-mat.cz
From: Timothy Hobbs <timothy.hobbs@auto-mat.cz>
Subject: =?UTF-8?B?VGVzdG92w6Fjw60gZW1haWw=?=
Openpgp: preference=signencrypt
Message-ID: <0fd7067e-2842-5b6c-3548-3cf7e6a1c9ea@auto-mat.cz>
Date: Fri, 8 Mar 2019 23:40:04 +0100
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101
Thunderbird/60.4.0
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Language: en-US
íářčšáíéřášč

View File

@ -0,0 +1,22 @@
MIME-Version: 1.0
Date: Tue, 5 Mar 2019 18:03:06 +0100
Message-ID: <CACf_UpPA7=DRF4NLFf4WpOVwHsRMtfNEwy1DHLjfh09DjuKZFA@mail.gmail.com>
Subject: =?UTF-8?B?xIxlc2vDvSB0ZXN0?=
From: Tim hobbs <tim.thelion@gmail.com>
To: helpdesk@auto-mat.cz
Content-Type: multipart/alternative; boundary="00000000000002a7c505835bd998"
--00000000000002a7c505835bd998
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
Tohle je test =C4=8Desk=C3=BDch p=C3=ADsmen odeslan=C3=BDch z gmailu.
--00000000000002a7c505835bd998
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<div dir=3D"ltr">Tohle je test =C4=8Desk=C3=BDch p=C3=ADsmen odeslan=C3=BDc=
h z gmailu.</div>
--00000000000002a7c505835bd998--

View File

@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, Attachment
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, FollowUpAttachment
import helpdesk.email
import itertools
@ -53,9 +53,37 @@ 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, "Attachment without body")
self.assertEqual(ticket.title, "FollowUpAttachment without body")
self.assertEqual(ticket.description, "")
def test_email_with_quoted_printable_body(self):
"""
Tests that emails with quoted-printable bodies work.
"""
with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml")) as fd:
test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger)
self.assertEqual(ticket.title, "Český test")
self.assertEqual(ticket.description, "Tohle je test českých písmen odeslaných z gmailu.")
followups = FollowUp.objects.filter(ticket=ticket)
self.assertEqual(len(followups), 1)
followup = followups[0]
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')
def test_email_with_8bit_encoding_and_utf_8(self):
"""
Tests that emails with 8bit transfer encoding and utf-8 charset
https://github.com/django-helpdesk/django-helpdesk/issues/732
"""
with open(os.path.join(THIS_DIR, "test_files/all-special-chars.eml")) as fd:
test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger)
self.assertEqual(ticket.title, "Testovácí email")
self.assertEqual(ticket.description, "íářčšáíéřášč")
class GetEmailParametricTemplate(object):
"""TestCase that checks basic email functionality across methods and socks configs."""
@ -346,7 +374,7 @@ class GetEmailParametricTemplate(object):
# HTML MIME part should be attached to follow up
followup1 = get_object_or_404(FollowUp, pk=1)
self.assertEqual(followup1.ticket.id, 1)
attach1 = get_object_or_404(Attachment, pk=1)
attach1 = get_object_or_404(FollowUpAttachment, pk=1)
self.assertEqual(attach1.followup.id, 1)
self.assertEqual(attach1.filename, 'email_html_body.html')
cc0 = get_object_or_404(TicketCC, pk=1)
@ -365,7 +393,7 @@ class GetEmailParametricTemplate(object):
# HTML MIME part should be attached to follow up
followup2 = get_object_or_404(FollowUp, pk=2)
self.assertEqual(followup2.ticket.id, 2)
attach2 = get_object_or_404(Attachment, pk=2)
attach2 = get_object_or_404(FollowUpAttachment, pk=2)
self.assertEqual(attach2.followup.id, 2)
self.assertEqual(attach2.filename, 'email_html_body.html')
@ -432,7 +460,7 @@ class GetEmailParametricTemplate(object):
# MIME part should be attached to follow up
followup1 = get_object_or_404(FollowUp, pk=1)
self.assertEqual(followup1.ticket.id, 1)
attach1 = get_object_or_404(Attachment, pk=1)
attach1 = get_object_or_404(FollowUpAttachment, pk=1)
self.assertEqual(attach1.followup.id, 1)
self.assertEqual(attach1.filename, 'signature.asc')
self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE-----

View File

@ -55,6 +55,18 @@ class TicketActionsTestCase(TestCase):
self.user.save()
self.client.login(username='User_1', password='pass')
def test_ticket_markdown(self):
ticket_data = {
'queue': self.queue_public,
'title': 'Test Ticket',
'description': '*bold*',
}
ticket = Ticket.objects.create(**ticket_data)
self.assertEqual(ticket.get_markdown(),
"<p><em>bold</em></p>")
def test_delete_ticket_staff(self):
# make staff user
self.loginUser()

View File

@ -0,0 +1,76 @@
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core import mail
from django.urls import reverse
from django.test import TestCase
from django.test.client import Client
from helpdesk.models import Queue, Ticket, FollowUp
from helpdesk import settings as helpdesk_settings
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
import uuid
import datetime
try: # python 3
from urllib.parse import urlparse
except ImportError: # python 2
from urlparse import urlparse
from helpdesk.templatetags.ticket_to_link import num_to_link
from helpdesk.views.staff import _is_my_ticket
class TimeSpentTestCase(TestCase):
def setUp(self):
self.queue_public = Queue.objects.create(
title='Queue 1',
slug='q1',
allow_public_submission=True,
dedicated_time=datetime.timedelta(minutes=60)
)
self.ticket_data = {
'title': 'Test Ticket',
'description': 'Some Test Ticket',
}
ticket_data = dict(queue=self.queue_public, **self.ticket_data)
self.ticket = Ticket.objects.create(**ticket_data)
self.client = Client()
user1_kwargs = {
'username': 'staff',
'email': 'staff@example.com',
'password': make_password('Test1234'),
'is_staff': True,
'is_superuser': False,
'is_active': True
}
self.user = User.objects.create(**user1_kwargs)
def test_add_followup(self):
"""Tests whether staff can delete tickets"""
message_id = uuid.uuid4().hex
followup = FollowUp.objects.create(
ticket=self.ticket,
date=datetime.datetime.now(),
title="Testing followup",
comment="Testing followup time spent",
public=True,
user=self.user,
new_status=1,
message_id=message_id,
time_spent=datetime.timedelta(minutes=30)
)
followup.save()
self.assertEqual(followup.time_spent.seconds, 1800)
self.assertEqual(self.ticket.time_spent.seconds, 1800)
self.assertEqual(self.queue_public.time_spent.seconds, 1800)
self.assertTrue(
self.queue_public.dedicated_time.seconds > self.queue_public.time_spent.seconds
)

View File

@ -42,7 +42,7 @@ from helpdesk.lib import (
process_attachments, queue_template_context,
)
from helpdesk.models import (
Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch,
Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
IgnoreEmail, TicketCC, TicketDependency, UserSettings,
)
from helpdesk import settings as helpdesk_settings
@ -237,6 +237,7 @@ def followup_edit(request, ticket_id, followup_id):
'comment': escape(followup.comment),
'public': followup.public,
'new_status': followup.new_status,
'time_spent': followup.time_spent,
})
ticketcc_string, show_subscribe = \
@ -256,15 +257,19 @@ def followup_edit(request, ticket_id, followup_id):
comment = form.cleaned_data['comment']
public = form.cleaned_data['public']
new_status = form.cleaned_data['new_status']
time_spent = form.cleaned_data['time_spent']
# will save previous date
old_date = followup.date
new_followup = FollowUp(title=title, date=old_date, ticket=_ticket, comment=comment, public=public, new_status=new_status, )
new_followup = FollowUp(title=title, date=old_date, ticket=_ticket,
comment=comment, public=public,
new_status=new_status,
time_spent=time_spent)
# keep old user if one did exist before.
if followup.user:
new_followup.user = followup.user
new_followup.save()
# get list of old attachments & link them to new_followup
attachments = Attachment.objects.filter(followup=followup)
attachments = FolllowUpAttachment.objects.filter(followup=followup)
for attachment in attachments:
attachment.followup = new_followup
attachment.save()
@ -469,6 +474,11 @@ def update_ticket(request, ticket_id, public=False):
due_date_year = int(request.POST.get('due_date_year', 0))
due_date_month = int(request.POST.get('due_date_month', 0))
due_date_day = int(request.POST.get('due_date_day', 0))
if request.POST.get("time_spent"):
(hours, minutes) = [int(f) for f in request.POST.get("time_spent").split(":")]
time_spent = timedelta(hours=hours, minutes=minutes)
else:
time_spent = None
# NOTE: jQuery's default for dates is mm/dd/yy
# very US-centric but for now that's the only format supported
# until we clean up code to internationalize a little more
@ -523,7 +533,8 @@ def update_ticket(request, ticket_id, public=False):
if owner is -1 and ticket.assigned_to:
owner = ticket.assigned_to.id
f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment)
f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment,
time_spent=time_spent)
if is_helpdesk_staff(request.user):
f.user = request.user
@ -1161,6 +1172,8 @@ def report_index(request):
'open': queue.ticket_set.filter(status__in=[1, 2]).count(),
'resolved': queue.ticket_set.filter(status=3).count(),
'closed': queue.ticket_set.filter(status=4).count(),
'time_spent': queue.time_spent,
'dedicated_time': queue.dedicated_time
}
dash_tickets.append(dash_ticket)
@ -1568,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(Attachment, id=attachment_id)
attachment = get_object_or_404(FolllowUpAttachment, id=attachment_id)
if request.method == 'POST':
attachment.delete()
return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket_id]))

View File

@ -27,7 +27,6 @@ class QuickDjangoTest(object):
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.staticfiles',
'markdown_deux',
'bootstrap4form'
)
MIDDLEWARE = [

View File

@ -4,7 +4,7 @@ celery
django-celery-beat
email-reply-parser
akismet
django-markdown-deux
markdown
beautifulsoup4
lxml
simplejson