forked from extern/django-helpdesk
Merge pull request #1024 from Benbb96/followup-api
API for FollowUp and FollowUpAttachment
This commit is contained in:
commit
c996537cd5
53
docs/api.rst
53
docs/api.rst
@ -1,20 +1,25 @@
|
|||||||
API
|
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::
|
If you wish to use it, you have to add this line in your settings::
|
||||||
|
|
||||||
HELPDESK_ACTIVATE_API_ENDPOINT = True
|
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
|
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
|
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
|
- **due_date**: date representation for when the ticket is due
|
||||||
- **merged_to**: ID of the ticket to which it is merged
|
- **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>``.
|
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' \
|
--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}'
|
--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.
|
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 :
|
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
|
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.
|
You must include all fields in the JSON body.
|
||||||
|
|
||||||
PATCH
|
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.
|
You can include only the fields you need to update in the JSON body.
|
||||||
|
|
||||||
DELETE
|
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.
|
||||||
|
@ -4,8 +4,8 @@ from django.contrib.humanize.templatetags import humanize
|
|||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from .forms import TicketForm
|
from .forms import TicketForm
|
||||||
from .models import Ticket, CustomField
|
from .models import Ticket, CustomField, FollowUp, FollowUpAttachment
|
||||||
from .lib import format_time_spent
|
from .lib import format_time_spent, process_attachments
|
||||||
from .user import HelpdeskUser
|
from .user import HelpdeskUser
|
||||||
|
|
||||||
|
|
||||||
@ -71,12 +71,44 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
|||||||
return obj.kbitem.title if obj.kbitem else ""
|
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):
|
class TicketSerializer(serializers.ModelSerializer):
|
||||||
|
followup_set = FollowUpSerializer(many=True, read_only=True)
|
||||||
|
attachment = serializers.FileField(write_only=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold',
|
'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):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -99,7 +131,9 @@ class TicketSerializer(serializers.ModelSerializer):
|
|||||||
if data.get('merged_to'):
|
if data.get('merged_to'):
|
||||||
data['merged_to'] = data['merged_to'].id
|
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():
|
if ticket_form.is_valid():
|
||||||
ticket = ticket_form.save(user=self.context['request'].user)
|
ticket = ticket_form.save(user=self.context['request'].user)
|
||||||
ticket.set_custom_field_values()
|
ticket.set_custom_field_values()
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import base64
|
import base64
|
||||||
|
from collections import OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from pytz import UTC
|
|
||||||
from rest_framework import HTTP_HEADER_ENCODING
|
from rest_framework import HTTP_HEADER_ENCODING
|
||||||
from rest_framework.exceptions import ErrorDetail
|
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
|
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.description, 'Test description\nMulti lines')
|
||||||
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
||||||
self.assertEqual(created_ticket.priority, 4)
|
self.assertEqual(created_ticket.priority, 4)
|
||||||
|
self.assertEqual(created_ticket.followup_set.count(), 1)
|
||||||
|
|
||||||
def test_create_api_ticket_with_basic_auth(self):
|
def test_create_api_ticket_with_basic_auth(self):
|
||||||
username = 'admin'
|
username = 'admin'
|
||||||
@ -178,6 +182,7 @@ class TicketTest(APITestCase):
|
|||||||
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
|
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
|
||||||
self.assertFalse(Ticket.objects.exists())
|
self.assertFalse(Ticket.objects.exists())
|
||||||
|
|
||||||
|
@freeze_time('2022-06-30 23:09:44')
|
||||||
def test_create_api_ticket_with_custom_fields(self):
|
def test_create_api_ticket_with_custom_fields(self):
|
||||||
# Create custom fields
|
# Create custom fields
|
||||||
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
|
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
|
||||||
@ -247,6 +252,19 @@ class TicketTest(APITestCase):
|
|||||||
'priority': 4,
|
'priority': 4,
|
||||||
'due_date': None,
|
'due_date': None,
|
||||||
'merged_to': 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_varchar': 'test',
|
||||||
'custom_text': 'multi\nline',
|
'custom_text': 'multi\nline',
|
||||||
'custom_integer': 1,
|
'custom_integer': 1,
|
||||||
@ -262,3 +280,54 @@ class TicketTest(APITestCase):
|
|||||||
'custom_slug': 'test-slug'
|
'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')
|
||||||
|
@ -17,7 +17,7 @@ from rest_framework.routers import DefaultRouter
|
|||||||
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
|
from helpdesk.decorators import helpdesk_staff_member_required, protect_view
|
||||||
from helpdesk.views import feeds, staff, public, login
|
from helpdesk.views import feeds, staff, public, login
|
||||||
from helpdesk import settings as helpdesk_settings
|
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:
|
if helpdesk_settings.HELPDESK_KB_ENABLED:
|
||||||
from helpdesk.views import kb
|
from helpdesk.views import kb
|
||||||
@ -176,6 +176,8 @@ urlpatterns += [
|
|||||||
if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT:
|
if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT:
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r"tickets", TicketViewSet, basename="ticket")
|
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")
|
router.register(r"users", CreateUserView, basename="user")
|
||||||
urlpatterns += [re_path(r"^api/", include(router.urls))]
|
urlpatterns += [re_path(r"^api/", include(router.urls))]
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@ from rest_framework.viewsets import GenericViewSet
|
|||||||
from rest_framework.mixins import CreateModelMixin
|
from rest_framework.mixins import CreateModelMixin
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from helpdesk.models import Ticket
|
from helpdesk.models import Ticket, FollowUp, FollowUpAttachment
|
||||||
from helpdesk.serializers import TicketSerializer, UserSerializer
|
from helpdesk.serializers import TicketSerializer, UserSerializer, FollowUpSerializer, FollowUpAttachmentSerializer
|
||||||
|
|
||||||
|
|
||||||
class TicketViewSet(viewsets.ModelViewSet):
|
class TicketViewSet(viewsets.ModelViewSet):
|
||||||
@ -28,6 +28,18 @@ class TicketViewSet(viewsets.ModelViewSet):
|
|||||||
return ticket
|
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):
|
class CreateUserView(CreateModelMixin, GenericViewSet):
|
||||||
queryset = get_user_model().objects.all()
|
queryset = get_user_model().objects.all()
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
@ -5,3 +5,4 @@ coverage
|
|||||||
argparse
|
argparse
|
||||||
pbr
|
pbr
|
||||||
mock
|
mock
|
||||||
|
freezegun
|
||||||
|
Loading…
Reference in New Issue
Block a user