From db0f286989cb5cd25f9c21215f8a9cedb9375232 Mon Sep 17 00:00:00 2001 From: Jachym Cepicky Date: Thu, 7 Mar 2019 21:58:04 +0100 Subject: [PATCH 1/2] adding support for images as knowledgebase attachment --- helpdesk/admin.py | 16 ++- helpdesk/forms.py | 2 +- helpdesk/lib.py | 4 +- .../migrations/0026_kbitem_attachments.py | 36 +++++++ helpdesk/models.py | 98 +++++++++++++++---- .../helpdesk/public_view_ticket.html | 2 +- helpdesk/tests/test_attachments.py | 45 ++++++++- helpdesk/tests/test_get_email.py | 12 +-- helpdesk/views/staff.py | 6 +- 9 files changed, 181 insertions(+), 40 deletions(-) create mode 100644 helpdesk/migrations/0026_kbitem_attachments.py diff --git a/helpdesk/admin.py b/helpdesk/admin.py index 32bcfd9b..52318ec6 100644 --- a/helpdesk/admin.py +++ b/helpdesk/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.utils.translation import ugettext_lazy as _ from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem -from helpdesk.models import TicketChange, Attachment, IgnoreEmail +from helpdesk.models import TicketChange, KBIAttachment, FollowUpAttachment, IgnoreEmail from helpdesk.models import CustomField @@ -43,15 +43,22 @@ class TicketAdmin(admin.ModelAdmin): class TicketChangeInline(admin.StackedInline): model = TicketChange + extra = 0 -class AttachmentInline(admin.StackedInline): - model = Attachment +class FollowUpAttachmentInline(admin.StackedInline): + model = FollowUpAttachment + extra = 0 + + +class KBIAttachmentInline(admin.StackedInline): + model = KBIAttachment + extra = 0 @admin.register(FollowUp) class FollowUpAdmin(admin.ModelAdmin): - inlines = [TicketChangeInline, AttachmentInline] + inlines = [TicketChangeInline, FollowUpAttachmentInline] list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket', 'user', 'new_status', 'time_spent') list_filter = ('user', 'date', 'new_status') @@ -64,6 +71,7 @@ class FollowUpAdmin(admin.ModelAdmin): @admin.register(KBItem) class KBItemAdmin(admin.ModelAdmin): list_display = ('category', 'title', 'last_updated',) + inlines = [KBIAttachmentInline] readonly_fields = ('voted_by',) list_display_links = ('title',) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 95eb7e2d..2fc4479f 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -17,7 +17,7 @@ from django.contrib.auth import get_user_model from django.utils import timezone from helpdesk.lib import safe_template_context, process_attachments -from helpdesk.models import (Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC, +from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC, CustomField, TicketCustomFieldValue, TicketDependency, UserSettings) from helpdesk import settings as helpdesk_settings diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 2c32e299..b44e4109 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -15,7 +15,7 @@ from django.db.models import Q from django.utils.encoding import smart_text, smart_str from django.utils.safestring import mark_safe -from helpdesk.models import Attachment, EmailTemplate +from helpdesk.models import FollowUpAttachment, EmailTemplate from model_utils import Choices @@ -218,7 +218,7 @@ def process_attachments(followup, attached_files): if attached.size: filename = smart_text(attached.name) - att = Attachment( + att = FollowUpAttachment( followup=followup, file=attached, filename=filename, diff --git a/helpdesk/migrations/0026_kbitem_attachments.py b/helpdesk/migrations/0026_kbitem_attachments.py new file mode 100644 index 00000000..810672c5 --- /dev/null +++ b/helpdesk/migrations/0026_kbitem_attachments.py @@ -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', + ), + ] diff --git a/helpdesk/models.py b/helpdesk/models.py index 78a02e61..73b29f06 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -17,6 +17,8 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _, ugettext from io import StringIO import re +import os +import mimetypes import datetime from django.utils.safestring import mark_safe @@ -904,18 +906,8 @@ class TicketChange(models.Model): def attachment_path(instance, filename): - """ - Provide a file path that will help prevent files being overwritten, by - putting attachments in a folder off attachments for ticket/followup_id/. - """ - import os - os.umask(0) - path = 'helpdesk/attachments/%s-%s/%s' % (instance.followup.ticket.ticket_for_url, instance.followup.ticket.secret_key, instance.followup.id) - att_path = os.path.join(settings.MEDIA_ROOT, path) - if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage": - if not os.path.exists(att_path): - os.makedirs(att_path, 0o777) - return os.path.join(path, filename) + """Just bridge""" + return instance.attachment_path(filename) class Attachment(models.Model): @@ -924,12 +916,6 @@ class Attachment(models.Model): attachment, or it could be uploaded via the web interface. """ - followup = models.ForeignKey( - FollowUp, - on_delete=models.CASCADE, - verbose_name=_('Follow-up'), - ) - file = models.FileField( _('File'), upload_to=attachment_path, @@ -938,26 +924,102 @@ class Attachment(models.Model): filename = models.CharField( _('Filename'), + blank=True, max_length=1000, ) mime_type = models.CharField( _('MIME Type'), + blank=True, max_length=255, ) size = models.IntegerField( _('Size'), + blank=True, help_text=_('Size of this file in bytes'), ) def __str__(self): return '%s' % self.filename + def save(self, *args, **kwargs): + + if not self.size: + self.size = self.get_size() + + if not self.filename: + self.filename = self.get_filename() + + if not self.mime_type: + self.mime_type = \ + mimetypes.guess_type(self.filename, strict=False)[0] or \ + 'application/octet-stream' + + return super(Attachment, self).save(*args, **kwargs) + + def get_filename(self): + return str(self.file) + + def get_size(self): + return self.file.file.size + + def attachment_path(self, filename): + """Provide a file path that will help prevent files being overwritten, by + putting attachments in a folder off attachments for ticket/followup_id/. + """ + assert NotImplementedError( + "This method is to be implemented by Attachment classes" + ) + class Meta: ordering = ('filename',) verbose_name = _('Attachment') verbose_name_plural = _('Attachments') + abstract = True + + +class FollowUpAttachment(Attachment): + + followup = models.ForeignKey( + FollowUp, + on_delete=models.CASCADE, + verbose_name=_('Follow-up'), + ) + + def attachment_path(self, filename): + + os.umask(0) + path = 'helpdesk/attachments/{ticket_for_url}-{secret_key}/{id_}'.format( + ticket_for_url=self.followup.ticket.ticket_for_url, + secret_key=self.followup.ticket.secret_key, + id_=self.followup.id) + att_path = os.path.join(settings.MEDIA_ROOT, path) + if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage": + if not os.path.exists(att_path): + os.makedirs(att_path, 0o777) + return os.path.join(path, filename) + + +class KBIAttachment(Attachment): + + kbitem = models.ForeignKey( + "KBItem", + on_delete=models.CASCADE, + verbose_name=_('Knowledge base item'), + ) + + def attachment_path(self, filename): + + os.umask(0) + path = 'helpdesk/attachments/kb/{category}/{kbi}'.format( + category=self.kbitem.category, + kbi=self.kbitem.id) + att_path = os.path.join(settings.MEDIA_ROOT, path) + if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage": + if not os.path.exists(att_path): + os.makedirs(att_path, 0o777) + return os.path.join(path, filename) class PreSetReply(models.Model): diff --git a/helpdesk/templates/helpdesk/public_view_ticket.html b/helpdesk/templates/helpdesk/public_view_ticket.html index ccf9ae25..c3ebcce5 100644 --- a/helpdesk/templates/helpdesk/public_view_ticket.html +++ b/helpdesk/templates/helpdesk/public_view_ticket.html @@ -66,7 +66,7 @@
  • {% 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 %}
  • {% endfor %} {% endif %} -{% for attachment in followup.attachment_set.all %}{% if forloop.first %}