show mail attachments in frontend #47
11 changed files with 252 additions and 23 deletions
|
@ -11,6 +11,7 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from core.settings import MEDIA_ROOT
|
from core.settings import MEDIA_ROOT
|
||||||
from files.models import File
|
from files.models import File
|
||||||
|
from mail.models import EmailAttachment
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(method='GET', auto_schema=None)
|
@swagger_auto_schema(method='GET', auto_schema=None)
|
||||||
|
@ -21,7 +22,11 @@ def media_urls(request, hash):
|
||||||
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash:
|
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash:
|
||||||
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
|
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
|
||||||
|
|
||||||
file = File.objects.get(hash=hash)
|
file = File.objects.filter(hash=hash).first()
|
||||||
|
attachment = EmailAttachment.objects.filter(hash=hash).first()
|
||||||
|
file = file if file else attachment
|
||||||
|
if not file:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
hash_path = file.file
|
hash_path = file.file
|
||||||
return HttpResponse(status=status.HTTP_200_OK,
|
return HttpResponse(status=status.HTTP_200_OK,
|
||||||
content_type=file.mime_type,
|
content_type=file.mime_type,
|
||||||
|
@ -33,9 +38,10 @@ def media_urls(request, hash):
|
||||||
'Age': 0,
|
'Age': 0,
|
||||||
'ETag': file.hash,
|
'ETag': file.hash,
|
||||||
})
|
})
|
||||||
|
|
||||||
except File.DoesNotExist:
|
except File.DoesNotExist:
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
except EmailAttachment.DoesNotExist:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(method='GET', auto_schema=None)
|
@swagger_auto_schema(method='GET', auto_schema=None)
|
||||||
|
@ -47,7 +53,11 @@ def thumbnail_urls(request, size, hash):
|
||||||
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash + "_" + str(size):
|
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash + "_" + str(size):
|
||||||
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
|
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
|
||||||
try:
|
try:
|
||||||
file = File.objects.get(hash=hash)
|
file = File.objects.filter(hash=hash).first()
|
||||||
|
attachment = EmailAttachment.objects.filter(hash=hash).first()
|
||||||
|
file = file if file else attachment
|
||||||
|
if not file:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
hash_path = file.file
|
hash_path = file.file
|
||||||
if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'):
|
if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'):
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
@ -72,6 +82,8 @@ def thumbnail_urls(request, size, hash):
|
||||||
|
|
||||||
except File.DoesNotExist:
|
except File.DoesNotExist:
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
except EmailAttachment.DoesNotExist:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -77,6 +77,13 @@ class AbstractFile(models.Model):
|
||||||
|
|
||||||
objects = FileManager()
|
objects = FileManager()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
from django.utils import timezone
|
||||||
|
if not self.created_at:
|
||||||
|
self.created_at = timezone.now()
|
||||||
|
self.updated_at = timezone.now()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
from rest_framework import routers, viewsets, serializers
|
from rest_framework import routers, viewsets, serializers
|
||||||
|
|
||||||
from mail.models import Email
|
from mail.models import Email, EmailAttachment
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = EmailAttachment
|
||||||
|
fields = ['hash', 'mime_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class EmailSerializer(serializers.ModelSerializer):
|
class EmailSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
|
||||||
def generate_email_attachments(apps, schema_editor):
|
def generate_email_attachments(apps, schema_editor):
|
||||||
for email in Email.objects.all():
|
for email in Email.objects.all():
|
||||||
raw = email.raw
|
raw = email.raw
|
||||||
if raw is None:
|
if raw is None or raw == '':
|
||||||
continue
|
continue
|
||||||
parsed, body, attachments = parse_email_body(raw.encode('utf-8'), NullLogger())
|
parsed, body, attachments = parse_email_body(raw.encode('utf-8'), NullLogger())
|
||||||
email.attachments.clear()
|
email.attachments.clear()
|
||||||
|
|
|
@ -92,8 +92,8 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
|
||||||
session = mock.Mock()
|
session = mock.Mock()
|
||||||
envelope = Envelope()
|
envelope = Envelope()
|
||||||
envelope.mail_from = 'test1@test'
|
envelope.mail_from = 'test1@test'
|
||||||
envelope.rcpt_tos = ['test2@test']
|
envelope.rcpt_tos = ['test2@localhost']
|
||||||
envelope.content = b'Subject: test\nFrom: test3@test\nTo: test4@test\nMessage-ID: <1@test>\n\ntest'
|
envelope.content = b'Subject: test\nFrom: test3@test\nTo: test4@localhost\nMessage-ID: <1@test>\n\ntest'
|
||||||
result = async_to_sync(handler.handle_DATA)(server, session, envelope)
|
result = async_to_sync(handler.handle_DATA)(server, session, envelope)
|
||||||
self.assertEqual(result, '250 Message accepted for delivery')
|
self.assertEqual(result, '250 Message accepted for delivery')
|
||||||
self.assertEqual(len(Email.objects.all()), 2)
|
self.assertEqual(len(Email.objects.all()), 2)
|
||||||
|
@ -101,14 +101,14 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
|
||||||
aiosmtplib.send.assert_called_once()
|
aiosmtplib.send.assert_called_once()
|
||||||
self.assertEqual('test', Email.objects.all()[0].subject)
|
self.assertEqual('test', Email.objects.all()[0].subject)
|
||||||
self.assertEqual('test1@test', Email.objects.all()[0].sender)
|
self.assertEqual('test1@test', Email.objects.all()[0].sender)
|
||||||
self.assertEqual('test2@test', Email.objects.all()[0].recipient)
|
self.assertEqual('test2@localhost', Email.objects.all()[0].recipient)
|
||||||
self.assertEqual('test', Email.objects.all()[0].body)
|
self.assertEqual('test', Email.objects.all()[0].body)
|
||||||
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread)
|
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread)
|
||||||
self.assertEqual('<1@test>', Email.objects.all()[0].reference)
|
self.assertEqual('<1@test>', Email.objects.all()[0].reference)
|
||||||
self.assertEqual(None, Email.objects.all()[0].in_reply_to)
|
self.assertEqual(None, Email.objects.all()[0].in_reply_to)
|
||||||
self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()),
|
self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()),
|
||||||
Email.objects.all()[1].subject)
|
Email.objects.all()[1].subject)
|
||||||
self.assertEqual('test2@test', Email.objects.all()[1].sender)
|
self.assertEqual('test2@localhost', Email.objects.all()[1].sender)
|
||||||
self.assertEqual('test1@test', Email.objects.all()[1].recipient)
|
self.assertEqual('test1@test', Email.objects.all()[1].recipient)
|
||||||
self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()),
|
self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()),
|
||||||
Email.objects.all()[1].body)
|
Email.objects.all()[1].body)
|
||||||
|
@ -284,19 +284,19 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
|
||||||
|
|
||||||
def test_mail_reply(self):
|
def test_mail_reply(self):
|
||||||
issue_thread = IssueThread.objects.create(
|
issue_thread = IssueThread.objects.create(
|
||||||
name="test",
|
name="test subject",
|
||||||
)
|
)
|
||||||
mail1 = Email.objects.create(
|
mail1 = Email.objects.create(
|
||||||
subject='test subject',
|
subject='test subject',
|
||||||
body='test',
|
body='test',
|
||||||
sender='test1@test',
|
sender='test1@test',
|
||||||
recipient='test2@' + MAIL_DOMAIN,
|
recipient='test2@localhost',
|
||||||
issue_thread=issue_thread,
|
issue_thread=issue_thread,
|
||||||
)
|
)
|
||||||
mail1_reply = Email.objects.create(
|
mail1_reply = Email.objects.create(
|
||||||
subject='Re: test subject',
|
subject='Message received',
|
||||||
body='Thank you for your message.',
|
body='Thank you for your message.',
|
||||||
sender='test2@' + MAIL_DOMAIN,
|
sender='test2@localhost',
|
||||||
recipient='test1@test',
|
recipient='test1@test',
|
||||||
in_reply_to=mail1.reference,
|
in_reply_to=mail1.reference,
|
||||||
issue_thread=issue_thread,
|
issue_thread=issue_thread,
|
||||||
|
@ -310,8 +310,22 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
|
||||||
self.assertEqual(len(Email.objects.all()), 3)
|
self.assertEqual(len(Email.objects.all()), 3)
|
||||||
self.assertEqual(len(IssueThread.objects.all()), 1)
|
self.assertEqual(len(IssueThread.objects.all()), 1)
|
||||||
aiosmtplib.send.assert_called_once()
|
aiosmtplib.send.assert_called_once()
|
||||||
self.assertEqual(Email.objects.all()[2].subject, 'Re: test subject')
|
self.assertEqual(Email.objects.all()[0].subject, 'test subject')
|
||||||
self.assertEqual(Email.objects.all()[2].sender, 'test2@' + MAIL_DOMAIN)
|
self.assertEqual(Email.objects.all()[0].sender, 'test1@test')
|
||||||
|
self.assertEqual(Email.objects.all()[0].recipient, 'test2@localhost')
|
||||||
|
self.assertEqual(Email.objects.all()[0].body, 'test')
|
||||||
|
self.assertEqual(Email.objects.all()[0].issue_thread, issue_thread)
|
||||||
|
self.assertEqual(Email.objects.all()[0].reference, mail1.reference)
|
||||||
|
self.assertEqual(Email.objects.all()[1].subject, 'Message received')
|
||||||
|
self.assertEqual(Email.objects.all()[1].sender, 'test2@localhost')
|
||||||
|
self.assertEqual(Email.objects.all()[1].recipient, 'test1@test')
|
||||||
|
self.assertEqual(Email.objects.all()[1].body, 'Thank you for your message.')
|
||||||
|
self.assertEqual(Email.objects.all()[1].issue_thread, issue_thread)
|
||||||
|
self.assertTrue(Email.objects.all()[1].reference.startswith("<"))
|
||||||
|
self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>"))
|
||||||
|
self.assertEqual(Email.objects.all()[1].in_reply_to, mail1.reference)
|
||||||
|
self.assertEqual(Email.objects.all()[2].subject, 'Re: test subject [#{0}]'.format(issue_thread.short_uuid()))
|
||||||
|
self.assertEqual(Email.objects.all()[2].sender, 'test2@localhost')
|
||||||
self.assertEqual(Email.objects.all()[2].recipient, 'test1@test')
|
self.assertEqual(Email.objects.all()[2].recipient, 'test1@test')
|
||||||
self.assertEqual(Email.objects.all()[2].body, 'test')
|
self.assertEqual(Email.objects.all()[2].body, 'test')
|
||||||
self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread)
|
self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread)
|
||||||
|
@ -633,25 +647,26 @@ dGVzdGltYWdl
|
||||||
|
|
||||||
def test_mail_plus_issue_thread(self):
|
def test_mail_plus_issue_thread(self):
|
||||||
issue_thread = IssueThread.objects.create(
|
issue_thread = IssueThread.objects.create(
|
||||||
name="test",
|
name="test subject",
|
||||||
)
|
)
|
||||||
mail1 = Email.objects.create(
|
mail1 = Email.objects.create(
|
||||||
subject='test subject',
|
subject='test subject',
|
||||||
body='test',
|
body='test',
|
||||||
sender='test1@test',
|
sender='test1@test',
|
||||||
recipient='test2@test',
|
recipient='test2@localhost',
|
||||||
issue_thread=issue_thread,
|
issue_thread=issue_thread,
|
||||||
)
|
)
|
||||||
mail1_reply = Email.objects.create(
|
mail1_reply = Email.objects.create(
|
||||||
subject='Message received',
|
subject='Message received',
|
||||||
body='Thank you for your message.',
|
body='Thank you for your message.',
|
||||||
sender='test2@test',
|
sender='test2@localhost',
|
||||||
recipient='test1@test',
|
recipient='test1@test',
|
||||||
in_reply_to=mail1.reference,
|
in_reply_to=mail1.reference,
|
||||||
issue_thread=issue_thread,
|
issue_thread=issue_thread,
|
||||||
)
|
)
|
||||||
from aiosmtpd.smtp import Envelope
|
from aiosmtpd.smtp import Envelope
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
|
from email.message import EmailMessage
|
||||||
import aiosmtplib
|
import aiosmtplib
|
||||||
import logging
|
import logging
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
@ -677,4 +692,23 @@ dGVzdGltYWdl
|
||||||
self.assertEqual(Email.objects.all()[2].body, 'bar')
|
self.assertEqual(Email.objects.all()[2].body, 'bar')
|
||||||
self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread)
|
self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread)
|
||||||
self.assertEqual(Email.objects.all()[2].reference, '<3@test>')
|
self.assertEqual(Email.objects.all()[2].reference, '<3@test>')
|
||||||
self.assertEqual('test', IssueThread.objects.all()[0].name)
|
self.assertEqual('test subject', IssueThread.objects.all()[0].name)
|
||||||
|
response = self.client.post(f'/api/2/tickets/{issue_thread.id}/reply/', {
|
||||||
|
'message': 'test'
|
||||||
|
})
|
||||||
|
aiosmtplib.send.assert_called_once();
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(4, len(Email.objects.all()))
|
||||||
|
self.assertEqual(4, len(Email.objects.filter(issue_thread=issue_thread)))
|
||||||
|
self.assertEqual(1, len(IssueThread.objects.all()))
|
||||||
|
self.assertEqual(Email.objects.all()[3].subject, 'Re: test subject [#{0}]'.format(issue_thread.short_uuid()))
|
||||||
|
self.assertEqual(Email.objects.all()[3].sender, 'test2@localhost')
|
||||||
|
self.assertEqual(Email.objects.all()[3].recipient, 'test1@test')
|
||||||
|
self.assertEqual(Email.objects.all()[3].body, 'test')
|
||||||
|
self.assertEqual(Email.objects.all()[3].issue_thread, issue_thread)
|
||||||
|
self.assertTrue(Email.objects.all()[3].reference.startswith("<"))
|
||||||
|
self.assertTrue(Email.objects.all()[3].reference.endswith("@localhost>"))
|
||||||
|
self.assertEqual(Email.objects.all()[3].in_reply_to, mail1.reference)
|
||||||
|
self.assertEqual('test subject', IssueThread.objects.all()[0].name)
|
||||||
|
self.assertEqual('pending_new', IssueThread.objects.all()[0].state)
|
||||||
|
self.assertEqual(None, IssueThread.objects.all()[0].assigned_to)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from asgiref.sync import async_to_sync
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
|
|
||||||
from core.settings import MAIL_DOMAIN
|
from core.settings import MAIL_DOMAIN
|
||||||
|
from mail.api_v2 import AttachmentSerializer
|
||||||
from mail.models import Email
|
from mail.models import Email
|
||||||
from mail.protocol import send_smtp, make_reply, collect_references
|
from mail.protocol import send_smtp, make_reply, collect_references
|
||||||
from notify_sessions.models import SystemEvent
|
from notify_sessions.models import SystemEvent
|
||||||
|
@ -75,6 +76,7 @@ class IssueSerializer(serializers.ModelSerializer):
|
||||||
'recipient': email.recipient,
|
'recipient': email.recipient,
|
||||||
'subject': email.subject,
|
'subject': email.subject,
|
||||||
'body': email.body,
|
'body': email.body,
|
||||||
|
'attachments': AttachmentSerializer(email.attachments.all(), many=True).data,
|
||||||
})
|
})
|
||||||
return sorted(timeline, key=lambda x: x['timestamp'])
|
return sorted(timeline, key=lambda x: x['timestamp'])
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from datetime import datetime, timedelta
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
from authentication.models import ExtendedUser
|
||||||
from mail.models import Email
|
from mail.models import Email, EmailAttachment
|
||||||
from tickets.models import IssueThread, StateChange, Comment
|
from tickets.models import IssueThread, StateChange, Comment
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from knox.models import AuthToken
|
from knox.models import AuthToken
|
||||||
|
@ -147,6 +147,88 @@ class IssueApiTest(TestCase):
|
||||||
self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
response.json()[2]['timeline'][1]['timestamp'])
|
response.json()[2]['timeline'][1]['timestamp'])
|
||||||
|
|
||||||
|
def test_issues_with_files(self):
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from hashlib import sha256
|
||||||
|
now = datetime.now()
|
||||||
|
issue = IssueThread.objects.create(
|
||||||
|
name="test issue",
|
||||||
|
)
|
||||||
|
mail1 = Email.objects.create(
|
||||||
|
subject='test',
|
||||||
|
body='test',
|
||||||
|
sender='test',
|
||||||
|
recipient='test',
|
||||||
|
issue_thread=issue,
|
||||||
|
timestamp=now,
|
||||||
|
)
|
||||||
|
mail2 = Email.objects.create(
|
||||||
|
subject='test',
|
||||||
|
body='test',
|
||||||
|
sender='test',
|
||||||
|
recipient='test',
|
||||||
|
issue_thread=issue,
|
||||||
|
in_reply_to=mail1.reference,
|
||||||
|
timestamp=now + timedelta(seconds=2),
|
||||||
|
)
|
||||||
|
comment = Comment.objects.create(
|
||||||
|
issue_thread=issue,
|
||||||
|
comment="test",
|
||||||
|
timestamp=now + timedelta(seconds=3),
|
||||||
|
)
|
||||||
|
file1 = EmailAttachment.objects.create(
|
||||||
|
name='file1', mime_type='text/plain', file=ContentFile(b"foo1", "f1"),
|
||||||
|
hash=sha256(b"foo1").hexdigest(), email=mail1
|
||||||
|
)
|
||||||
|
file2 = EmailAttachment.objects.create(
|
||||||
|
name='file2', mime_type='text/plain', file=ContentFile(b"foo2", "f2"),
|
||||||
|
hash=sha256(b"foo2").hexdigest(), email=mail1
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/api/2/tickets/')
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
self.assertEqual(1, len(response.json()))
|
||||||
|
self.assertEqual(issue.id, response.json()[0]['id'])
|
||||||
|
self.assertEqual('pending_new', response.json()[0]['state'])
|
||||||
|
self.assertEqual('test issue', response.json()[0]['name'])
|
||||||
|
self.assertEqual(None, response.json()[0]['assigned_to'])
|
||||||
|
self.assertEqual(36, len(response.json()[0]['uuid']))
|
||||||
|
self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
|
response.json()[0]['last_activity'])
|
||||||
|
self.assertEqual(4, len(response.json()[0]['timeline']))
|
||||||
|
self.assertEqual('state', response.json()[0]['timeline'][0]['type'])
|
||||||
|
self.assertEqual('mail', response.json()[0]['timeline'][1]['type'])
|
||||||
|
self.assertEqual('mail', response.json()[0]['timeline'][2]['type'])
|
||||||
|
self.assertEqual('comment', response.json()[0]['timeline'][3]['type'])
|
||||||
|
self.assertEqual(mail1.id, response.json()[0]['timeline'][1]['id'])
|
||||||
|
self.assertEqual(mail2.id, response.json()[0]['timeline'][2]['id'])
|
||||||
|
self.assertEqual(comment.id, response.json()[0]['timeline'][3]['id'])
|
||||||
|
self.assertEqual('pending_new', response.json()[0]['timeline'][0]['state'])
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][1]['sender'])
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][1]['recipient'])
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][1]['subject'])
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][1]['body'])
|
||||||
|
self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
|
response.json()[0]['timeline'][1]['timestamp'])
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][2]['sender'])
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][2]['recipient'])
|
||||||
|
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][2]['subject'])
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][2]['body'])
|
||||||
|
self.assertEqual(mail2.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
|
response.json()[0]['timeline'][2]['timestamp'])
|
||||||
|
self.assertEqual('test', response.json()[0]['timeline'][3]['comment'])
|
||||||
|
self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
|
response.json()[0]['timeline'][3]['timestamp'])
|
||||||
|
self.assertEqual(2, len(response.json()[0]['timeline'][1]['attachments']))
|
||||||
|
self.assertEqual(0, len(response.json()[0]['timeline'][2]['attachments']))
|
||||||
|
self.assertEqual('file1', response.json()[0]['timeline'][1]['attachments'][0]['name'])
|
||||||
|
self.assertEqual('file2', response.json()[0]['timeline'][1]['attachments'][1]['name'])
|
||||||
|
self.assertEqual('text/plain', response.json()[0]['timeline'][1]['attachments'][0]['mime_type'])
|
||||||
|
self.assertEqual('text/plain', response.json()[0]['timeline'][1]['attachments'][1]['mime_type'])
|
||||||
|
self.assertEqual(file1.hash, response.json()[0]['timeline'][1]['attachments'][0]['hash'])
|
||||||
|
self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash'])
|
||||||
|
|
||||||
def test_manual_creation(self):
|
def test_manual_creation(self):
|
||||||
response = self.client.post('/api/2/tickets/manual/',
|
response = self.client.post('/api/2/tickets/manual/',
|
||||||
{'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},
|
{'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"vue-router": "^3.1.3",
|
"vue-router": "^3.1.3",
|
||||||
"vuex": "^3.1.2",
|
"vuex": "^3.1.2",
|
||||||
"vuex-router-sync": "^5.0.0",
|
"vuex-router-sync": "^5.0.0",
|
||||||
|
"vuex-shared-mutations": "^1.0.2",
|
||||||
"yarn": "^1.22.21"
|
"yarn": "^1.22.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
48
web/src/components/AuthenticatedDataLink.vue
Normal file
48
web/src/components/AuthenticatedDataLink.vue
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<a :href="image_data">{{ download }}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {mapActions} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AuthenticatedDataLink",
|
||||||
|
props: {
|
||||||
|
href: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "unnamed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
image_data: "",
|
||||||
|
servers: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['fetchImage']),
|
||||||
|
loadImage() {
|
||||||
|
this.fetchImage(this.href).then((response) => {
|
||||||
|
const mime_type = response.headers.get("content-type");
|
||||||
|
response.arrayBuffer().then((buf) => {
|
||||||
|
const base64 = btoa(new Uint8Array(buf)
|
||||||
|
.reduce((data, byte) => data + String.fromCharCode(byte), ""));
|
||||||
|
this.image_data = "data:" + mime_type + ";base64," + base64;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -18,6 +18,14 @@
|
||||||
<font-awesome-icon icon="user"/>
|
<font-awesome-icon icon="user"/>
|
||||||
</button-->
|
</button-->
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-footer" v-if="item.attachments.length">
|
||||||
|
<ul>
|
||||||
|
<li v-for="attachment in item.attachments">
|
||||||
|
<AuthenticatedImage :src="`/media/2/256/${attachment.hash}/`" :alt="attachment.name" v-if="attachment.mime_type.startsWith('image/')"/>
|
||||||
|
<AuthenticatedDataLink :href="`/media/2/256/${attachment.hash}/`" :download="attachment.name" v-else/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--button class="show-replies">
|
<!--button class="show-replies">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-forward"
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-forward"
|
||||||
|
@ -44,8 +52,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||||
|
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TimelineMail',
|
name: 'TimelineMail',
|
||||||
|
components: {AuthenticatedImage, AuthenticatedDataLink},
|
||||||
props: {
|
props: {
|
||||||
'item': {
|
'item': {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import router from '../router';
|
||||||
import * as base64 from 'base-64';
|
import * as base64 from 'base-64';
|
||||||
import * as utf8 from 'utf8';
|
import * as utf8 from 'utf8';
|
||||||
import {ticketStateColorLookup, ticketStateIconLookup} from "@/utils";
|
import {ticketStateColorLookup, ticketStateIconLookup} from "@/utils";
|
||||||
|
import createMutationsSharer from "vuex-shared-mutations";
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
const axios = AxiosBootstrap.create({
|
const axios = AxiosBootstrap.create({
|
||||||
|
@ -103,7 +104,6 @@ const store = new Vuex.Store({
|
||||||
slug: slug,
|
slug: slug,
|
||||||
text: 'Unknown'
|
text: 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isLoggedIn(state) {
|
isLoggedIn(state) {
|
||||||
|
@ -273,10 +273,11 @@ const store = new Vuex.Store({
|
||||||
//async verifyToken({commit, state}) {
|
//async verifyToken({commit, state}) {
|
||||||
async afterLogin({dispatch}) {
|
async afterLogin({dispatch}) {
|
||||||
const boxes = dispatch('loadBoxes');
|
const boxes = dispatch('loadBoxes');
|
||||||
|
const states = dispatch('fetchTicketStates');
|
||||||
const items = dispatch('loadEventItems');
|
const items = dispatch('loadEventItems');
|
||||||
const tickets = dispatch('loadTickets');
|
const tickets = dispatch('loadTickets');
|
||||||
const user = dispatch('loadUserInfo');
|
const user = dispatch('loadUserInfo');
|
||||||
await Promise.all([boxes, items, tickets, user]);
|
await Promise.all([boxes, items, tickets, user, states]);
|
||||||
},
|
},
|
||||||
async fetchImage({state}, url) {
|
async fetchImage({state}, url) {
|
||||||
return await fetch(url, {headers: {'Authorization': `Token ${state.token}`}});
|
return await fetch(url, {headers: {'Authorization': `Token ${state.token}`}});
|
||||||
|
@ -394,7 +395,31 @@ const store = new Vuex.Store({
|
||||||
const {data} = await axios.patch(`/2/tickets/${id}/`, ticket);
|
const {data} = await axios.patch(`/2/tickets/${id}/`, ticket);
|
||||||
commit('updateTicket', data);
|
commit('updateTicket', data);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
plugins: [createMutationsSharer({
|
||||||
|
predicate: [
|
||||||
|
'replaceLoadedItems',
|
||||||
|
'setItemCache',
|
||||||
|
'setLayout',
|
||||||
|
'replaceBoxes',
|
||||||
|
'updateItem',
|
||||||
|
'removeItem',
|
||||||
|
'appendItem',
|
||||||
|
'replaceTickets',
|
||||||
|
'replaceUsers',
|
||||||
|
'replaceGroups',
|
||||||
|
'updateTicket',
|
||||||
|
'openAddBoxModal',
|
||||||
|
'closeAddBoxModal',
|
||||||
|
'createToast',
|
||||||
|
'removeToast',
|
||||||
|
'setRemember',
|
||||||
|
'setUser',
|
||||||
|
'setPermissions',
|
||||||
|
'setToken',
|
||||||
|
'logout',
|
||||||
|
]
|
||||||
|
})],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|
Loading…
Reference in a new issue