mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2025-02-26 07:01:42 +01:00
Sync with upstream merges
This commit is contained in:
commit
ff74a8c21b
@ -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'
|
||||||
]
|
]
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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',)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
18
helpdesk/migrations/0024_time_spent.py
Normal file
18
helpdesk/migrations/0024_time_spent.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
18
helpdesk/migrations/0025_queue_dedicated_time.py
Normal file
18
helpdesk/migrations/0025_queue_dedicated_time.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
36
helpdesk/migrations/0026_kbitem_attachments.py
Normal file
36
helpdesk/migrations/0026_kbitem_attachments.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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">
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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' /> {% trans 'Yes, make this update public.' %}</dd>
|
<dd><input type='checkbox' name='public' value='1' checked='checked' /> {% 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 »" %}</button></p>
|
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details »" %}</button></p>
|
||||||
|
@ -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> {% 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> {% 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>
|
||||||
|
@ -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"},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
|
@ -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'])
|
||||||
|
|
||||||
|
|
||||||
|
15
helpdesk/tests/test_files/all-special-chars.eml
Normal file
15
helpdesk/tests/test_files/all-special-chars.eml
Normal 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
|
||||||
|
|
||||||
|
íářčšáíéřášč
|
||||||
|
|
22
helpdesk/tests/test_files/quoted_printable.eml
Normal file
22
helpdesk/tests/test_files/quoted_printable.eml
Normal 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--
|
@ -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-----
|
||||||
|
@ -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()
|
||||||
|
76
helpdesk/tests/test_time_spent.py
Normal file
76
helpdesk/tests/test_time_spent.py
Normal 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
|
||||||
|
)
|
@ -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]))
|
||||||
|
@ -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 = [
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user