stash
This commit is contained in:
parent
29e7c4d283
commit
55577adde8
5 changed files with 104 additions and 7 deletions
|
@ -3,6 +3,7 @@ from unittest import mock
|
||||||
|
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
|
from core.settings import MAIL_DOMAIN
|
||||||
from inventory.models import Event
|
from inventory.models import Event
|
||||||
from mail.models import Email
|
from mail.models import Email
|
||||||
from mail.protocol import LMTPHandler
|
from mail.protocol import LMTPHandler
|
||||||
|
@ -133,3 +134,41 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
|
||||||
self.assertEqual(IssueThread.objects.all()[0].name, 'test')
|
self.assertEqual(IssueThread.objects.all()[0].name, 'test')
|
||||||
self.assertEqual(IssueThread.objects.all()[0].state, 'new')
|
self.assertEqual(IssueThread.objects.all()[0].state, 'new')
|
||||||
self.assertEqual(IssueThread.objects.all()[0].assigned_to, None)
|
self.assertEqual(IssueThread.objects.all()[0].assigned_to, None)
|
||||||
|
|
||||||
|
def test_mail_reply(self):
|
||||||
|
issue_thread = IssueThread.objects.create(
|
||||||
|
name="test",
|
||||||
|
)
|
||||||
|
mail1 = Email.objects.create(
|
||||||
|
subject='test subject',
|
||||||
|
body='test',
|
||||||
|
sender='test1@test',
|
||||||
|
recipient='test2@' + MAIL_DOMAIN,
|
||||||
|
issue_thread=issue_thread,
|
||||||
|
)
|
||||||
|
mail1_reply = Email.objects.create(
|
||||||
|
subject='Re: test subject',
|
||||||
|
body='Thank you for your message.',
|
||||||
|
sender='test2@' + MAIL_DOMAIN,
|
||||||
|
recipient='test1@test',
|
||||||
|
in_reply_to=mail1.reference,
|
||||||
|
issue_thread=issue_thread,
|
||||||
|
)
|
||||||
|
import aiosmtplib
|
||||||
|
aiosmtplib.send = make_mocked_coro()
|
||||||
|
client = Client()
|
||||||
|
response = client.post(f'/api/2/tickets/{issue_thread.id}/reply/', {
|
||||||
|
'message': 'test'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(len(Email.objects.all()), 3)
|
||||||
|
self.assertEqual(len(IssueThread.objects.all()), 1)
|
||||||
|
aiosmtplib.send.assert_called_once()
|
||||||
|
self.assertEqual(Email.objects.all()[2].subject, 'Re: test subject')
|
||||||
|
self.assertEqual(Email.objects.all()[2].sender, 'test2@' + MAIL_DOMAIN)
|
||||||
|
self.assertEqual(Email.objects.all()[2].recipient, 'test1@test')
|
||||||
|
self.assertEqual(Email.objects.all()[2].body, 'test')
|
||||||
|
self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread)
|
||||||
|
self.assertTrue(Email.objects.all()[2].reference.startswith("<"))
|
||||||
|
self.assertTrue(Email.objects.all()[2].reference.endswith("@localhost>"))
|
||||||
|
self.assertEqual(Email.objects.all()[2].in_reply_to, mail1.reference)
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
from rest_framework import routers, viewsets, serializers
|
import logging
|
||||||
|
|
||||||
from tickets.models import IssueThread, Comment, StateChange
|
from django.urls import path
|
||||||
|
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
||||||
|
from rest_framework import routers, viewsets, serializers, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
|
||||||
|
from core.settings import MAIL_DOMAIN
|
||||||
|
from mail.models import Email
|
||||||
|
from mail.protocol import send_smtp, make_reply, collect_references
|
||||||
|
from tickets.models import IssueThread
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(serializers.ModelSerializer):
|
class IssueSerializer(serializers.ModelSerializer):
|
||||||
|
@ -48,7 +57,32 @@ class IssueViewSet(viewsets.ModelViewSet):
|
||||||
authentication_classes = []
|
authentication_classes = []
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['POST'])
|
||||||
|
@permission_classes([])
|
||||||
|
@authentication_classes([])
|
||||||
|
def reply(request, pk):
|
||||||
|
issue = IssueThread.objects.get(pk=pk)
|
||||||
|
# email = issue.reply(request.data['body']) # TODO evaluate if this is a useful abstraction
|
||||||
|
references = collect_references(issue)
|
||||||
|
most_recent = Email.objects.filter(issue_thread=issue, recipient__endswith='@' + MAIL_DOMAIN).order_by(
|
||||||
|
'-timestamp').first()
|
||||||
|
mail = Email.objects.create(
|
||||||
|
issue_thread=issue,
|
||||||
|
sender=most_recent.recipient,
|
||||||
|
recipient=most_recent.sender,
|
||||||
|
subject=f'Re: {most_recent.subject}',
|
||||||
|
body=request.data['message'],
|
||||||
|
in_reply_to=most_recent.reference,
|
||||||
|
)
|
||||||
|
log = logging.getLogger('mail.log')
|
||||||
|
async_to_sync(send_smtp)(make_reply(mail, references), log)
|
||||||
|
|
||||||
|
return Response({'status': 'ok'}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
router = routers.SimpleRouter()
|
router = routers.SimpleRouter()
|
||||||
router.register(r'tickets', IssueViewSet, basename='issues')
|
router.register(r'tickets', IssueViewSet, basename='issues')
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls + [
|
||||||
|
path('tickets/<int:pk>/reply/', reply, name='reply'),
|
||||||
|
]
|
||||||
|
|
|
@ -24,7 +24,12 @@
|
||||||
<font-awesome-icon icon="comment"/>
|
<font-awesome-icon icon="comment"/>
|
||||||
</span>
|
</span>
|
||||||
<div class="new-comment">
|
<div class="new-comment">
|
||||||
<input type="text" placeholder="Add a comment..."/>
|
<div class="input-group">
|
||||||
|
<input type="text" placeholder="Add a comment..." v-model="newMail">
|
||||||
|
<button class="btn" @click="sendMail">
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -45,6 +50,16 @@ export default {
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['sendMail'],
|
||||||
|
data: () => ({
|
||||||
|
newMail: ""
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
sendMail() {
|
||||||
|
this.$emit('sendMail', this.newMail);
|
||||||
|
this.newMail = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -105,7 +120,6 @@ img {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--gray-dark);
|
color: var(--gray-dark);
|
||||||
|
|
|
@ -157,6 +157,10 @@ const store = new Vuex.Store({
|
||||||
const {data} = await axios.get('/2/tickets/');
|
const {data} = await axios.get('/2/tickets/');
|
||||||
commit('replaceTickets', data);
|
commit('replaceTickets', data);
|
||||||
},
|
},
|
||||||
|
async sendMail({commit, dispatch}, {id, message}) {
|
||||||
|
const {data} = await axios.post(`/2/tickets/${id}/reply/`, {message});
|
||||||
|
await dispatch('loadTickets');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Ticket #{{ ticket.id }} - {{ ticket.name }}</h3>
|
<h3>Ticket #{{ ticket.id }} - {{ ticket.name }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<Timeline :timeline="ticket.timeline"/>
|
<Timeline :timeline="ticket.timeline" @sendMail="handleMail"/>
|
||||||
<div class="card-footer d-flex justify-content-between">
|
<div class="card-footer d-flex justify-content-between">
|
||||||
<router-link :to="{name: 'tickets'}" class="btn btn-secondary mr-2">Back</router-link>
|
<router-link :to="{name: 'tickets'}" class="btn btn-secondary mr-2">Back</router-link>
|
||||||
<button class="btn btn-danger" @click="deleteItem({type: 'tickets', id: ticket.id})">
|
<button class="btn btn-danger" @click="deleteItem({type: 'tickets', id: ticket.id})">
|
||||||
|
@ -39,7 +39,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['deleteItem', 'markItemReturned', 'loadTickets']),
|
...mapActions(['deleteItem', 'markItemReturned', 'loadTickets', 'sendMail']),
|
||||||
|
handleMail(mail) {
|
||||||
|
this.sendMail({
|
||||||
|
id: this.ticket.id,
|
||||||
|
message: mail
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadTickets()
|
this.loadTickets()
|
||||||
|
|
Loading…
Reference in a new issue