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.staticfiles',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.humanize', 'django.contrib.humanize',
'markdown_deux',
'bootstrap4form', 'bootstrap4form',
'helpdesk' '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.sites', # Required for determining domain url for use in emails
'django.contrib.admin', # Required for helpdesk admin/maintenance 'django.contrib.admin', # Required for helpdesk admin/maintenance
'django.contrib.humanize', # Required for elapsed time formatting '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 'bootstrap4form', # Required for nicer formatting of forms with the default templates
'helpdesk', # This is us! '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. 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``:: 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``::
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``::
LOGIN_URL = '/helpdesk/login/' LOGIN_URL = '/helpdesk/login/'
Alter the URL to suit your installation path. 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 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. 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 django.utils.translation import ugettext_lazy as _
from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory
from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem 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 from helpdesk.models import CustomField
@admin.register(Queue) @admin.register(Queue)
class QueueAdmin(admin.ModelAdmin): class QueueAdmin(admin.ModelAdmin):
list_display = ('title', 'slug', 'email_address', 'locale') list_display = ('title', 'slug', 'email_address', 'locale', 'time_spent')
prepopulated_fields = {"slug": ("title",)} 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) @admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin): 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' date_hierarchy = 'created'
list_filter = ('queue', 'assigned_to', 'status') list_filter = ('queue', 'assigned_to', 'status')
@ -28,19 +37,30 @@ class TicketAdmin(admin.ModelAdmin):
return ticket.submitter_email return ticket.submitter_email
hidden_submitter_email.short_description = _('Submitter E-Mail') hidden_submitter_email.short_description = _('Submitter E-Mail')
def time_spent(self, ticket):
return ticket.time_spent
class TicketChangeInline(admin.StackedInline): class TicketChangeInline(admin.StackedInline):
model = TicketChange model = TicketChange
extra = 0
class AttachmentInline(admin.StackedInline): class FollowUpAttachmentInline(admin.StackedInline):
model = Attachment model = FollowUpAttachment
extra = 0
class KBIAttachmentInline(admin.StackedInline):
model = KBIAttachment
extra = 0
@admin.register(FollowUp) @admin.register(FollowUp)
class FollowUpAdmin(admin.ModelAdmin): class FollowUpAdmin(admin.ModelAdmin):
inlines = [TicketChangeInline, AttachmentInline] inlines = [TicketChangeInline, FollowUpAttachmentInline]
list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket', 'user', 'new_status') list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket',
'user', 'new_status', 'time_spent')
list_filter = ('user', 'date', 'new_status') list_filter = ('user', 'date', 'new_status')
def ticket_get_ticket_for_url(self, obj): def ticket_get_ticket_for_url(self, obj):
@ -51,6 +71,7 @@ class FollowUpAdmin(admin.ModelAdmin):
@admin.register(KBItem) @admin.register(KBItem)
class KBItemAdmin(admin.ModelAdmin): class KBItemAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'last_updated',) list_display = ('category', 'title', 'last_updated',)
inlines = [KBIAttachmentInline]
readonly_fields = ('voted_by',) readonly_fields = ('voted_by',)
list_display_links = ('title',) list_display_links = ('title',)

View File

@ -481,8 +481,9 @@ def object_from_message(message, queue, logger):
body.encode('utf-8') body.encode('utf-8')
logger.debug("Discovered plain text MIME part") logger.debug("Discovered plain text MIME part")
else: else:
payload = encoding.smart_bytes(part.get_payload(decode=True))
files.append( 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") logger.debug("Discovered HTML MIME part")
else: else:

View File

@ -17,7 +17,7 @@ from django.contrib.auth import get_user_model
from django.utils import timezone from django.utils import timezone
from helpdesk.lib import safe_template_context, process_attachments 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) CustomField, TicketCustomFieldValue, TicketDependency, UserSettings)
from helpdesk import settings as helpdesk_settings 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.encoding import smart_text, smart_str
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from helpdesk.models import Attachment, EmailTemplate from helpdesk.models import FollowUpAttachment, EmailTemplate
from model_utils import Choices from model_utils import Choices
@ -218,7 +218,7 @@ def process_attachments(followup, attached_files):
if attached.size: if attached.size:
filename = smart_text(attached.name) filename = smart_text(attached.name)
att = Attachment( att = FollowUpAttachment(
followup=followup, followup=followup,
file=attached, file=attached,
filename=filename, 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 django.utils.translation import ugettext_lazy as _, ugettext
from io import StringIO from io import StringIO
import re 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 import uuid
from .templated_email import send_templated_mail 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): class Queue(models.Model):
""" """
A queue is a collection of tickets into what would generally be business 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'), 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): def __str__(self):
return "%s" % self.title return "%s" % self.title
@ -301,6 +335,17 @@ class Queue(models.Model):
return u'%s <%s>' % (self.title, self.email_address) return u'%s <%s>' % (self.title, self.email_address)
from_address = property(_from_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): def prepare_permission_name(self):
"""Prepare internally the codename for the permission and store it in permission_name. """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. :return: The codename that can be used to create a new Permission object.
@ -497,6 +542,17 @@ class Ticket(models.Model):
default=mk_secret, 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): def send(self, roles, dont_send_to=None, **kwargs):
""" """
Send notifications to everyone interested in this ticket. Send notifications to everyone interested in this ticket.
@ -689,6 +745,9 @@ class Ticket(models.Model):
queue = '-'.join(parts[0:-1]) queue = '-'.join(parts[0:-1])
return queue, parts[-1] return queue, parts[-1]
def get_markdown(self):
return get_markdown(self.description)
class FollowUpManager(models.Manager): class FollowUpManager(models.Manager):
@ -740,8 +799,10 @@ class FollowUp(models.Model):
_('Public'), _('Public'),
blank=True, blank=True,
default=False, default=False,
help_text=_('Public tickets are viewable by the submitter and all ' help_text=_(
'staff, but non-public tickets can only be seen by staff.'), 'Public tickets are viewable by the submitter and all '
'staff, but non-public tickets can only be seen by staff.'
),
) )
user = models.ForeignKey( user = models.ForeignKey(
@ -771,6 +832,11 @@ class FollowUp(models.Model):
objects = FollowUpManager() objects = FollowUpManager()
time_spent = models.DurationField(
help_text=_("Time spent on this follow up"),
blank=True, null=True
)
class Meta: class Meta:
ordering = ('date',) ordering = ('date',)
verbose_name = _('Follow-up') verbose_name = _('Follow-up')
@ -788,6 +854,9 @@ class FollowUp(models.Model):
t.save() t.save()
super(FollowUp, self).save(*args, **kwargs) super(FollowUp, self).save(*args, **kwargs)
def get_markdown(self):
return get_markdown(self.comment)
class TicketChange(models.Model): class TicketChange(models.Model):
""" """
@ -837,18 +906,8 @@ class TicketChange(models.Model):
def attachment_path(instance, filename): def attachment_path(instance, filename):
""" """Just bridge"""
Provide a file path that will help prevent files being overwritten, by return instance.attachment_path(filename)
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)
class Attachment(models.Model): class Attachment(models.Model):
@ -857,12 +916,6 @@ class Attachment(models.Model):
attachment, or it could be uploaded via the web interface. 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 = models.FileField(
_('File'), _('File'),
upload_to=attachment_path, upload_to=attachment_path,
@ -871,26 +924,102 @@ class Attachment(models.Model):
filename = models.CharField( filename = models.CharField(
_('Filename'), _('Filename'),
blank=True,
max_length=1000, max_length=1000,
) )
mime_type = models.CharField( mime_type = models.CharField(
_('MIME Type'), _('MIME Type'),
blank=True,
max_length=255, max_length=255,
) )
size = models.IntegerField( size = models.IntegerField(
_('Size'), _('Size'),
blank=True,
help_text=_('Size of this file in bytes'), help_text=_('Size of this file in bytes'),
) )
def __str__(self): def __str__(self):
return '%s' % self.filename 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: class Meta:
ordering = ('filename',) ordering = ('filename',)
verbose_name = _('Attachment') verbose_name = _('Attachment')
verbose_name_plural = _('Attachments') 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): class PreSetReply(models.Model):
@ -1128,6 +1257,9 @@ class KBItem(models.Model):
from django.urls import reverse from django.urls import reverse
return reverse('helpdesk:kb_item', args=(self.id,)) return reverse('helpdesk:kb_item', args=(self.id,))
def get_markdown(self):
return get_markdown(self.answer)
class SavedSearch(models.Model): class SavedSearch(models.Model):
""" """

View File

@ -18,11 +18,14 @@ class TicketSerializer(serializers.ModelSerializer):
due_date = serializers.SerializerMethodField() due_date = serializers.SerializerMethodField()
status = serializers.SerializerMethodField() status = serializers.SerializerMethodField()
row_class = serializers.SerializerMethodField() row_class = serializers.SerializerMethodField()
time_spent = serializers.SerializerMethodField()
class Meta: class Meta:
model = Ticket model = Ticket
# fields = '__all__' # 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): def get_ticket(self, obj):
return (str(obj.id) + " " + obj.ticket) return (str(obj.id) + " " + obj.ticket)
@ -45,5 +48,8 @@ class TicketSerializer(serializers.ModelSerializer):
else: else:
return ("None") return ("None")
def get_time_spent(self, obj):
return str(obj.time_spent)
def get_row_class(self, obj): def get_row_class(self, obj):
return (obj.get_priority_css_class) return (obj.get_priority_css_class)

View File

@ -81,3 +81,9 @@ img.brand {padding-right: 30px;}
div.card-body img { div.card-body img {
max-width: 900px; 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> <dt><label for="id_new_status">New Status:</label></dt>
<dd>{{ form.new_status }}</dd> <dd>{{ form.new_status }}</dd>
<p>If the status was changed, what was it changed to?</p> <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> </dl>
</fieldset> </fieldset>
<p><input class="btn btn-primary" type="submit" value="Submit"></p>{% csrf_token %} <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 %} {% block helpdesk_breadcrumb %}
<li class="breadcrumb-item"> <li class="breadcrumb-item">
@ -19,7 +19,7 @@
{{ item.question }} {{ item.question }}
</div> </div>
<div class="card-body"> <div class="card-body">
<p>{{ item.answer|markdown }}</p> <p>{{ item.get_markdown }}</p>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<div class="row"> <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> <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 %} {% endfor %}
</ul></div>{% endif %} </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> <li><a href='{{ attachment.file.url }}'>{{ attachment.filename }}</a> ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})</li>
{% if forloop.last %}</ul></div>{% endif %} {% if forloop.last %}</ul></div>{% endif %}
{% endfor %} {% endfor %}

View File

@ -45,6 +45,7 @@
<th>{% trans "Open" %}</th> <th>{% trans "Open" %}</th>
<th>{% trans "Resolved" %}</th> <th>{% trans "Resolved" %}</th>
<th>{% trans "Closed" %}</th> <th>{% trans "Closed" %}</th>
<th>{% trans "Time spent" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <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.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.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>{% 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> </tr>
{% empty %} {% empty %}
<tr><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr> <tr><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr>

View File

@ -2,6 +2,8 @@
{% load i18n bootstrap4form humanize %} {% load i18n bootstrap4form humanize %}
{% load static from staticfiles %} {% load static from staticfiles %}
{% block helpdesk_title %}{{ ticket.queue.slug }}-{{ ticket.id }} : {% trans "View Ticket Details" %}{% endblock %} {% block helpdesk_title %}{{ ticket.queue.slug }}-{{ ticket.id }} : {% trans "View Ticket Details" %}{% endblock %}
{% block helpdesk_head %} {% block helpdesk_head %}
@ -44,7 +46,10 @@
</div> </div>
<p class="mb-1"> <p class="mb-1">
{% if followup.comment %} {% 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 %} {% endif %}
{% for change in followup.ticketchange_set.all %} {% for change in followup.ticketchange_set.all %}
{% if forloop.first %}<div class='changes'><ul>{% endif %} {% 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><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> <dd class='form_help_text'>{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}</dd>
{% endif %} {% 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> </dl>
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details &raquo;" %}</button></p> <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> <th colspan='2'>{% trans "Description" %}</th>
</tr> </tr>
<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> </tr>
{% if ticket.resolution %}<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> <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> </td>
</tr> </tr>
<tr>
<th>{% trans "Total time spent" %}</th>
<td>{{ ticket.time_spent }}</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -222,6 +222,7 @@
<th>{% trans "Created" %}</th> <th>{% trans "Created" %}</th>
<th>{% trans "Due Date" %}</th> <th>{% trans "Due Date" %}</th>
<th>{% trans "Owner" %}</th> <th>{% trans "Owner" %}</th>
<th>{% trans "Time Spent" %}</th>
</tr> </tr>
</thead> </thead>
{% if not server_side %} {% if not server_side %}
@ -339,6 +340,7 @@
{"data": "created"}, {"data": "created"},
{"data": "due_date"}, {"data": "due_date"},
{"data": "assigned_to"}, {"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.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 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.get_assigned_to }}</td>
<td>{{ ticket.time_spent }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -58,7 +58,7 @@ class AttachmentIntegrationTests(TestCase):
self.assertContains(response, test_file.name) self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content # 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: with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = file_on_disk.read() disk_content = file_on_disk.read()
self.assertEqual(disk_content, 'attached file content') self.assertEqual(disk_content, 'attached file content')
@ -76,7 +76,7 @@ class AttachmentIntegrationTests(TestCase):
self.assertContains(response, test_file.name) self.assertContains(response, test_file.name)
# Ensure attachment is available with correct content # 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: with open(os.path.join(MEDIA_DIR, att.file.name)) as file_on_disk:
disk_content = smart_text(file_on_disk.read(), 'utf-8') disk_content = smart_text(file_on_disk.read(), 'utf-8')
self.assertEqual(disk_content, 'โจ') self.assertEqual(disk_content, 'โจ')
@ -96,7 +96,7 @@ class AttachmentUnitTests(TestCase):
self.test_file = SimpleUploadedFile.from_dict(self.file_attrs) self.test_file = SimpleUploadedFile.from_dict(self.file_attrs)
self.follow_up = models.FollowUp(ticket=models.Ticket(queue=models.Queue())) 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): 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 """ """ check utf-8 data is parsed correcltly """
filename, fileobj = lib.process_attachments(self.follow_up, [self.test_file])[0] 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']) 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) @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): 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 """ """ 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] attachment_obj = mock_att_save.call_args[0][0]
mock_att_save.assert_called_once_with(attachment_obj) 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']) 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.models import User
from django.contrib.auth.hashers import make_password 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 helpdesk.email
import itertools 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: with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml")) as fd:
test_email = fd.read() test_email = fd.read()
ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) 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, "") 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): class GetEmailParametricTemplate(object):
"""TestCase that checks basic email functionality across methods and socks configs.""" """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 # HTML MIME part should be attached to follow up
followup1 = get_object_or_404(FollowUp, pk=1) followup1 = get_object_or_404(FollowUp, pk=1)
self.assertEqual(followup1.ticket.id, 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.followup.id, 1)
self.assertEqual(attach1.filename, 'email_html_body.html') self.assertEqual(attach1.filename, 'email_html_body.html')
cc0 = get_object_or_404(TicketCC, pk=1) cc0 = get_object_or_404(TicketCC, pk=1)
@ -365,7 +393,7 @@ class GetEmailParametricTemplate(object):
# HTML MIME part should be attached to follow up # HTML MIME part should be attached to follow up
followup2 = get_object_or_404(FollowUp, pk=2) followup2 = get_object_or_404(FollowUp, pk=2)
self.assertEqual(followup2.ticket.id, 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.followup.id, 2)
self.assertEqual(attach2.filename, 'email_html_body.html') self.assertEqual(attach2.filename, 'email_html_body.html')
@ -432,7 +460,7 @@ class GetEmailParametricTemplate(object):
# MIME part should be attached to follow up # MIME part should be attached to follow up
followup1 = get_object_or_404(FollowUp, pk=1) followup1 = get_object_or_404(FollowUp, pk=1)
self.assertEqual(followup1.ticket.id, 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.followup.id, 1)
self.assertEqual(attach1.filename, 'signature.asc') self.assertEqual(attach1.filename, 'signature.asc')
self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE----- self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE-----

View File

@ -55,6 +55,18 @@ class TicketActionsTestCase(TestCase):
self.user.save() self.user.save()
self.client.login(username='User_1', password='pass') 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): def test_delete_ticket_staff(self):
# make staff user # make staff user
self.loginUser() 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, process_attachments, queue_template_context,
) )
from helpdesk.models import ( from helpdesk.models import (
Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch, Ticket, Queue, FollowUp, TicketChange, PreSetReply, FollowUpAttachment, SavedSearch,
IgnoreEmail, TicketCC, TicketDependency, UserSettings, IgnoreEmail, TicketCC, TicketDependency, UserSettings,
) )
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
@ -237,6 +237,7 @@ def followup_edit(request, ticket_id, followup_id):
'comment': escape(followup.comment), 'comment': escape(followup.comment),
'public': followup.public, 'public': followup.public,
'new_status': followup.new_status, 'new_status': followup.new_status,
'time_spent': followup.time_spent,
}) })
ticketcc_string, show_subscribe = \ ticketcc_string, show_subscribe = \
@ -256,15 +257,19 @@ def followup_edit(request, ticket_id, followup_id):
comment = form.cleaned_data['comment'] comment = form.cleaned_data['comment']
public = form.cleaned_data['public'] public = form.cleaned_data['public']
new_status = form.cleaned_data['new_status'] new_status = form.cleaned_data['new_status']
time_spent = form.cleaned_data['time_spent']
# will save previous date # will save previous date
old_date = followup.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. # keep old user if one did exist before.
if followup.user: if followup.user:
new_followup.user = followup.user new_followup.user = followup.user
new_followup.save() new_followup.save()
# get list of old attachments & link them to new_followup # 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: for attachment in attachments:
attachment.followup = new_followup attachment.followup = new_followup
attachment.save() 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_year = int(request.POST.get('due_date_year', 0))
due_date_month = int(request.POST.get('due_date_month', 0)) due_date_month = int(request.POST.get('due_date_month', 0))
due_date_day = int(request.POST.get('due_date_day', 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 # NOTE: jQuery's default for dates is mm/dd/yy
# very US-centric but for now that's the only format supported # very US-centric but for now that's the only format supported
# until we clean up code to internationalize a little more # 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: if owner is -1 and ticket.assigned_to:
owner = ticket.assigned_to.id 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): if is_helpdesk_staff(request.user):
f.user = request.user f.user = request.user
@ -1161,6 +1172,8 @@ def report_index(request):
'open': queue.ticket_set.filter(status__in=[1, 2]).count(), 'open': queue.ticket_set.filter(status__in=[1, 2]).count(),
'resolved': queue.ticket_set.filter(status=3).count(), 'resolved': queue.ticket_set.filter(status=3).count(),
'closed': queue.ticket_set.filter(status=4).count(), 'closed': queue.ticket_set.filter(status=4).count(),
'time_spent': queue.time_spent,
'dedicated_time': queue.dedicated_time
} }
dash_tickets.append(dash_ticket) 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): if not _is_my_ticket(request.user, ticket):
raise PermissionDenied() raise PermissionDenied()
attachment = get_object_or_404(Attachment, id=attachment_id) attachment = get_object_or_404(FolllowUpAttachment, id=attachment_id)
if request.method == 'POST': if request.method == 'POST':
attachment.delete() attachment.delete()
return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket_id])) return HttpResponseRedirect(reverse('helpdesk:view', args=[ticket_id]))

View File

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

View File

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