Merge pull request #1024 from Benbb96/followup-api

API for FollowUp and FollowUpAttachment
This commit is contained in:
Garret Wassermann 2022-07-02 06:17:02 -04:00 committed by GitHub
commit c996537cd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 171 additions and 16 deletions

View File

@ -1,20 +1,25 @@
API
===
A REST API (built with ``djangorestframework``) is available in order to list, create, update and delete tickets from other tools thanks to HTTP requests.
A REST API (built with ``djangorestframework``) is available in order to list, create, update and delete tickets from
other tools thanks to HTTP requests.
If you wish to use it, you have to add this line in your settings::
HELPDESK_ACTIVATE_API_ENDPOINT = True
You must be authenticated to access the API, the URL endpoint is ``/api/tickets/``. You can configure how you wish to authenticate to the API by customizing the ``DEFAULT_AUTHENTICATION_CLASSES`` key in the ``REST_FRAMEWORK`` setting (more information on this page : https://www.django-rest-framework.org/api-guide/authentication/)
You must be authenticated to access the API, the URL endpoint is ``/api/tickets/``.
You can configure how you wish to authenticate to the API by customizing the ``DEFAULT_AUTHENTICATION_CLASSES`` key
in the ``REST_FRAMEWORK`` setting (more information on this page : https://www.django-rest-framework.org/api-guide/authentication/)
GET
---
Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the complete list of tickets.
Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the complete list of tickets with their
followups and their attachment files.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **GET** request will return you the data of the ticket you provided the ID.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **GET** request will return you the data of the ticket you
provided the ID.
POST
----
@ -35,7 +40,8 @@ You need to provide a JSON body with the following data :
- **due_date**: date representation for when the ticket is due
- **merged_to**: ID of the ticket to which it is merged
Note that ``status`` will automatically be set to OPEN. Also, some fields are not configurable during creation: ``resolution``, ``on_hold`` and ``merged_to``.
Note that ``status`` will automatically be set to OPEN. Also, some fields are not configurable during creation:
``resolution``, ``on_hold`` and ``merged_to``.
Moreover, if you created custom fields, you can add them into the body with the key ``custom_<custom-field-slug>``.
@ -46,6 +52,34 @@ Here is an example of a cURL request to create a ticket (using Basic authenticat
--header 'Content-Type: application/json' \
--data-raw '{"queue": 1, "title": "Test Ticket API", "description": "Test create ticket from API", "submitter_email": "test@mail.com", "priority": 4}'
Note that you can attach one file as attachment but in this case, you cannot use JSON for the request content type.
Here is an example with form-data (curl default) ::
curl --location --request POST 'http://127.0.0.1:8000/api/tickets/' \
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
--form 'queue="1"' \
--form 'title="Test Ticket API with attachment"' \
--form 'description="Test create ticket from API avec attachment"' \
--form 'submitter_email="test@mail.com"' \
--form 'priority="2"' \
--form 'attachment=@"/C:/Users/benbb96/Documents/file.txt"'
----
Accessing the endpoint ``/api/followups/`` with a **POST** request will let you create a new followup on a ticket.
This time, you can attach multiple files thanks to the `attachments` field. Here is an example ::
curl --location --request POST 'http://127.0.0.1:8000/api/followups/' \
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
--form 'ticket="44"' \
--form 'title="Test ticket answer"' \
--form 'comment="This answer contains multiple files as attachment."' \
--form 'attachments=@"/C:/Users/benbb96/Documents/doc.pdf"' \
--form 'attachments=@"/C:/Users/benbb96/Documents/image.png"'
----
Accessing the endpoint ``/api/users/`` with a **POST** request will let you create a new user.
You need to provide a JSON body with the following data :
@ -59,18 +93,21 @@ You need to provide a JSON body with the following data :
PUT
---
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PUT** request will let you update the data of the ticket you provided the ID.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PUT** request will let you update the data of the ticket
you provided the ID.
You must include all fields in the JSON body.
PATCH
-----
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PATCH** request will let you do a partial update of the data of the ticket you provided the ID.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PATCH** request will let you do a partial update of the
data of the ticket you provided the ID.
You can include only the fields you need to update in the JSON body.
DELETE
------
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **DELETE** request will let you delete the ticket you provided the ID.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **DELETE** request will let you delete the ticket you
provided the ID.

View File

@ -4,8 +4,8 @@ from django.contrib.humanize.templatetags import humanize
from rest_framework.exceptions import ValidationError
from .forms import TicketForm
from .models import Ticket, CustomField
from .lib import format_time_spent
from .models import Ticket, CustomField, FollowUp, FollowUpAttachment
from .lib import format_time_spent, process_attachments
from .user import HelpdeskUser
@ -71,12 +71,44 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
return obj.kbitem.title if obj.kbitem else ""
class FollowUpAttachmentSerializer(serializers.ModelSerializer):
class Meta:
model = FollowUpAttachment
fields = ('id', 'followup', 'file', 'filename', 'mime_type', 'size')
class FollowUpSerializer(serializers.ModelSerializer):
followupattachment_set = FollowUpAttachmentSerializer(many=True, read_only=True)
attachments = serializers.ListField(
child=serializers.FileField(),
write_only=True,
required=False
)
class Meta:
model = FollowUp
fields = (
'id', 'ticket', 'date', 'title', 'comment', 'public', 'user', 'new_status', 'message_id',
'time_spent', 'followupattachment_set', 'attachments'
)
def create(self, validated_data):
attachments = validated_data.pop('attachments', None)
followup = super().create(validated_data)
if attachments:
process_attachments(followup, attachments)
return followup
class TicketSerializer(serializers.ModelSerializer):
followup_set = FollowUpSerializer(many=True, read_only=True)
attachment = serializers.FileField(write_only=True, required=False)
class Meta:
model = Ticket
fields = (
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold',
'priority', 'due_date', 'merged_to'
'priority', 'due_date', 'merged_to', 'attachment', 'followup_set'
)
def __init__(self, *args, **kwargs):
@ -99,7 +131,9 @@ class TicketSerializer(serializers.ModelSerializer):
if data.get('merged_to'):
data['merged_to'] = data['merged_to'].id
ticket_form = TicketForm(data=data, queue_choices=queue_choices)
files = {'attachment': data.pop('attachment', None)}
ticket_form = TicketForm(data=data, files=files, queue_choices=queue_choices)
if ticket_form.is_valid():
ticket = ticket_form.save(user=self.context['request'].user)
ticket.set_custom_field_values()

View File

@ -1,8 +1,11 @@
import base64
from collections import OrderedDict
from datetime import datetime
from django.core.files.uploadedfile import SimpleUploadedFile
from freezegun import freeze_time
from django.contrib.auth.models import User
from pytz import UTC
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework.exceptions import ErrorDetail
from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN
@ -72,6 +75,7 @@ class TicketTest(APITestCase):
self.assertEqual(created_ticket.description, 'Test description\nMulti lines')
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 4)
self.assertEqual(created_ticket.followup_set.count(), 1)
def test_create_api_ticket_with_basic_auth(self):
username = 'admin'
@ -178,6 +182,7 @@ class TicketTest(APITestCase):
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertFalse(Ticket.objects.exists())
@freeze_time('2022-06-30 23:09:44')
def test_create_api_ticket_with_custom_fields(self):
# Create custom fields
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
@ -247,6 +252,19 @@ class TicketTest(APITestCase):
'priority': 4,
'due_date': None,
'merged_to': None,
'followup_set': [OrderedDict([
('id', 1),
('ticket', 1),
('date', '2022-06-30T23:09:44'),
('title', 'Ticket Opened'),
('comment', 'Test description\nMulti lines'),
('public', True),
('user', 1),
('new_status', None),
('message_id', None),
('time_spent', None),
('followupattachment_set', [])
])],
'custom_varchar': 'test',
'custom_text': 'multi\nline',
'custom_integer': 1,
@ -262,3 +280,54 @@ class TicketTest(APITestCase):
'custom_slug': 'test-slug'
})
def test_create_api_ticket_with_attachment(self):
staff_user = User.objects.create_user(username='test', is_staff=True)
self.client.force_authenticate(staff_user)
test_file = SimpleUploadedFile('file.jpg', b'file_content', content_type='image/jpg')
response = self.client.post('/api/tickets/', {
'queue': self.queue.id,
'title': 'Test title',
'description': 'Test description\nMulti lines',
'submitter_email': 'test@mail.com',
'priority': 4,
'attachment': test_file
})
self.assertEqual(response.status_code, HTTP_201_CREATED)
created_ticket = Ticket.objects.get()
self.assertEqual(created_ticket.title, 'Test title')
self.assertEqual(created_ticket.description, 'Test description\nMulti lines')
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 4)
self.assertEqual(created_ticket.followup_set.count(), 1)
self.assertEqual(created_ticket.followup_set.get().followupattachment_set.count(), 1)
attachment = created_ticket.followup_set.get().followupattachment_set.get()
self.assertEqual(
attachment.file.name,
f'helpdesk/attachments/test-queue-1-{created_ticket.secret_key}/1/file.jpg'
)
def test_create_follow_up_with_attachments(self):
staff_user = User.objects.create_user(username='test', is_staff=True)
self.client.force_authenticate(staff_user)
ticket = Ticket.objects.create(queue=self.queue, title='Test')
test_file_1 = SimpleUploadedFile('file.jpg', b'file_content', content_type='image/jpg')
test_file_2 = SimpleUploadedFile('doc.pdf', b'Doc content', content_type='application/pdf')
response = self.client.post('/api/followups/', {
'ticket': ticket.id,
'title': 'Test',
'comment': 'Test answer\nMulti lines',
'attachments': [
test_file_1,
test_file_2
]
})
self.assertEqual(response.status_code, HTTP_201_CREATED)
created_followup = ticket.followup_set.last()
self.assertEqual(created_followup.title, 'Test')
self.assertEqual(created_followup.comment, 'Test answer\nMulti lines')
self.assertEqual(created_followup.followupattachment_set.count(), 2)
self.assertEqual(created_followup.followupattachment_set.first().filename, 'doc.pdf')
self.assertEqual(created_followup.followupattachment_set.first().mime_type, 'application/pdf')
self.assertEqual(created_followup.followupattachment_set.last().filename, 'file.jpg')
self.assertEqual(created_followup.followupattachment_set.last().mime_type, 'image/jpg')

View File

@ -17,7 +17,7 @@ from rest_framework.routers import DefaultRouter
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
from helpdesk.views import feeds, staff, public, login
from helpdesk import settings as helpdesk_settings
from helpdesk.views.api import TicketViewSet, CreateUserView
from helpdesk.views.api import TicketViewSet, CreateUserView, FollowUpViewSet, FollowUpAttachmentViewSet
if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.views import kb
@ -176,6 +176,8 @@ urlpatterns += [
if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT:
router = DefaultRouter()
router.register(r"tickets", TicketViewSet, basename="ticket")
router.register(r"followups", FollowUpViewSet, basename="followups")
router.register(r"followups-attachments", FollowUpAttachmentViewSet, basename="followupattachments")
router.register(r"users", CreateUserView, basename="user")
urlpatterns += [re_path(r"^api/", include(router.urls))]

View File

@ -4,8 +4,8 @@ from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from django.contrib.auth import get_user_model
from helpdesk.models import Ticket
from helpdesk.serializers import TicketSerializer, UserSerializer
from helpdesk.models import Ticket, FollowUp, FollowUpAttachment
from helpdesk.serializers import TicketSerializer, UserSerializer, FollowUpSerializer, FollowUpAttachmentSerializer
class TicketViewSet(viewsets.ModelViewSet):
@ -28,6 +28,18 @@ class TicketViewSet(viewsets.ModelViewSet):
return ticket
class FollowUpViewSet(viewsets.ModelViewSet):
queryset = FollowUp.objects.all()
serializer_class = FollowUpSerializer
permission_classes = [IsAdminUser]
class FollowUpAttachmentViewSet(viewsets.ModelViewSet):
queryset = FollowUpAttachment.objects.all()
serializer_class = FollowUpAttachmentSerializer
permission_classes = [IsAdminUser]
class CreateUserView(CreateModelMixin, GenericViewSet):
queryset = get_user_model().objects.all()
serializer_class = UserSerializer

View File

@ -5,3 +5,4 @@ coverage
argparse
pbr
mock
freezegun