tickets module: api_v2, admin views and tests

This commit is contained in:
j3d1 2024-01-07 21:18:42 +01:00
parent 434dfe807e
commit cbc27b143f
8 changed files with 365 additions and 16 deletions

20
core/tickets/admin.py Normal file
View file

@ -0,0 +1,20 @@
from django.contrib import admin
from tickets.models import IssueThread, Comment, StateChange
class IssueThreadAdmin(admin.ModelAdmin):
pass
class CommentAdmin(admin.ModelAdmin):
pass
class StateChangeAdmin(admin.ModelAdmin):
pass
admin.site.register(IssueThread, IssueThreadAdmin)
admin.site.register(Comment, CommentAdmin)
admin.site.register(StateChange, StateChangeAdmin)

125
core/tickets/api_v2.py Normal file
View file

@ -0,0 +1,125 @@
import logging
from django.urls import re_path
from django.contrib.auth.decorators import permission_required
from rest_framework import routers, viewsets, serializers, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from core.settings import MAIL_DOMAIN
from mail.models import Email
from mail.protocol import send_smtp, make_reply, collect_references
from notify_sessions.models import SystemEvent
from tickets.models import IssueThread
class IssueSerializer(serializers.ModelSerializer):
timeline = serializers.SerializerMethodField()
class Meta:
model = IssueThread
fields = ('id', 'timeline', 'name', 'state', 'assigned_to', 'last_activity')
read_only_fields = ('id', 'timeline', 'last_activity')
@staticmethod
def get_timeline(obj):
timeline = []
for comment in obj.comments.all():
timeline.append({
'type': 'comment',
'id': comment.id,
'timestamp': comment.timestamp,
'comment': comment.comment,
})
for state_change in obj.state_changes.all():
timeline.append({
'type': 'state',
'id': state_change.id,
'timestamp': state_change.timestamp,
'state': state_change.state,
})
for email in obj.emails.all():
timeline.append({
'type': 'mail',
'id': email.id,
'timestamp': email.timestamp,
'sender': email.sender,
'recipient': email.recipient,
'subject': email.subject,
'body': email.body,
})
return sorted(timeline, key=lambda x: x['timestamp'])
class IssueViewSet(viewsets.ModelViewSet):
serializer_class = IssueSerializer
queryset = IssueThread.objects.all()
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_required('tickets.add_issuethread', raise_exception=True)
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)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_required('tickets.add_issuethread_manual', raise_exception=True)
def manual_ticket(request):
if 'name' not in request.data:
return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST)
if 'sender' not in request.data:
return Response({'status': 'error', 'message': 'missing sender'}, status=status.HTTP_400_BAD_REQUEST)
if 'recipient' not in request.data:
return Response({'status': 'error', 'message': 'missing recipient'}, status=status.HTTP_400_BAD_REQUEST)
if 'body' not in request.data:
return Response({'status': 'error', 'message': 'missing body'}, status=status.HTTP_400_BAD_REQUEST)
issue = IssueThread.objects.create(
name=request.data['name'],
manually_created=True,
)
email = Email.objects.create(
issue_thread=issue,
sender=request.data['sender'],
recipient=request.data['recipient'],
subject=request.data['name'],
body=request.data['body'],
)
systemevent = SystemEvent.objects.create(type='email received', reference=email.id)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'general', {"type": "generic.event", "name": "send_message_to_frontend", "event_id": systemevent.id,
"message": "email received"}
)
return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED)
router = routers.SimpleRouter()
router.register(r'tickets', IssueViewSet, basename='issues')
urlpatterns = ([
re_path(r'^tickets/(?P<pk>\d+)/reply/$', reply, name='reply'),
re_path(r'^tickets/manual/$', manual_ticket, name='manual_ticket'),
] + router.urls)

View file

View file

View file

@ -0,0 +1,110 @@
from datetime import datetime, timedelta
from django.test import TestCase, Client
from authentication.models import ExtendedUser
from mail.models import Email
from tickets.models import IssueThread, StateChange, Comment
from django.contrib.auth.models import Permission
from knox.models import AuthToken
class IssueApiTest(TestCase):
def setUp(self):
super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_issues_empty(self):
response = self.client.get('/api/2/tickets/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_issues(self):
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,
)
state = StateChange.objects.create(
issue_thread=issue,
state="new",
timestamp=now + timedelta(seconds=1),
)
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),
)
response = self.client.get('/api/2/tickets/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['id'], issue.id)
self.assertEqual(response.json()[0]['name'], "test issue")
self.assertEqual(response.json()[0]['state'], "new")
self.assertEqual(response.json()[0]['assigned_to'], None)
self.assertEqual(response.json()[0]['last_activity'], issue.last_activity.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(len(response.json()[0]['timeline']), 4)
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'mail')
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'state')
self.assertEqual(response.json()[0]['timeline'][2]['type'], 'mail')
self.assertEqual(response.json()[0]['timeline'][3]['type'], 'comment')
self.assertEqual(response.json()[0]['timeline'][0]['id'], mail1.id)
self.assertEqual(response.json()[0]['timeline'][1]['id'], state.id)
self.assertEqual(response.json()[0]['timeline'][2]['id'], mail2.id)
self.assertEqual(response.json()[0]['timeline'][3]['id'], comment.id)
self.assertEqual(response.json()[0]['timeline'][0]['sender'], 'test')
self.assertEqual(response.json()[0]['timeline'][0]['recipient'], 'test')
self.assertEqual(response.json()[0]['timeline'][0]['subject'], 'test')
self.assertEqual(response.json()[0]['timeline'][0]['body'], 'test')
self.assertEqual(response.json()[0]['timeline'][0]['timestamp'],
mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][1]['state'], 'new')
self.assertEqual(response.json()[0]['timeline'][1]['timestamp'],
state.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][2]['sender'], 'test')
self.assertEqual(response.json()[0]['timeline'][2]['recipient'], 'test')
self.assertEqual(response.json()[0]['timeline'][2]['subject'], 'test')
self.assertEqual(response.json()[0]['timeline'][2]['body'], 'test')
self.assertEqual(response.json()[0]['timeline'][2]['timestamp'],
mail2.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][3]['comment'], 'test')
self.assertEqual(response.json()[0]['timeline'][3]['timestamp'],
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
def test_manual_creation(self):
response = self.client.post('/api/2/tickets/manual/', {'name': 'test issue', 'sender': 'test',
'recipient': 'test', 'body': 'test'})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['state'], 'new')
self.assertEqual(response.json()['name'], 'test issue')
self.assertEqual(response.json()['assigned_to'], None)
timeline = response.json()['timeline']
self.assertEqual(len(timeline), 1)
self.assertEqual(timeline[0]['type'], 'mail')
self.assertEqual(timeline[0]['sender'], 'test')
self.assertEqual(timeline[0]['recipient'], 'test')
self.assertEqual(timeline[0]['subject'], 'test issue')
self.assertEqual(timeline[0]['body'], 'test')