Merge pull request #1006 from Benbb96/api-solution-1

Create an API endpoint to manage tickets (including custom fields)
This commit is contained in:
Garret Wassermann 2022-04-14 19:24:12 -04:00 committed by GitHub
commit 935b95ecc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 520 additions and 65 deletions

View File

@ -117,6 +117,9 @@ HELPDESK_KB_ENABLED = True
# Allow users to change their passwords # Allow users to change their passwords
HELPDESK_SHOW_CHANGE_PASSWORD = True HELPDESK_SHOW_CHANGE_PASSWORD = True
# Activate the API
HELPDESK_ACTIVATE_API_ENDPOINT = True
# Instead of showing the public web portal first, # Instead of showing the public web portal first,
# we can instead redirect users straight to the login page. # we can instead redirect users straight to the login page.
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False

66
docs/api.rst Normal file
View File

@ -0,0 +1,66 @@
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.
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/)
GET
---
Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the complete list of tickets.
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **GET** request will return you the data of the ticket you provided the ID.
POST
----
Accessing the endpoint ``/api/tickets/`` with a **POST** request will let you create a new tickets.
You need to provide a JSON body with the following data :
- **queue**: ID of the queue
- **title**: the title (subject) of the ticket
- **description**: the description of the ticket
- **resolution**: an optional text for the resoltuion of the ticket
- **submitter_email**: the email of the ticket submitter
- **assigned_to**: ID of the ticket's assigned user
- **status**: integer corresponding to the status (OPEN=1, REOPENED=2, RESOLVED=3, CLOSED=4, DUPLICATE=5). It is OPEN by default.
- **on_hold**: boolean to indicates if the ticket is on hold
- **priority**: integer corresponding to different degrees of priority 1 to 5 (1 is Critical and 5 is Very Low)
- **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``.
Moreover, if you created custom fields, you can add them into the body with the key ``custom_<custom-field-slug>``.
Here is an example of a cURL request to create a ticket (using Basic authentication) ::
curl --location --request POST 'http://127.0.0.1:8000/api/tickets/' \
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
--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}'
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.
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.
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.

View File

@ -16,9 +16,9 @@ Contents
settings settings
spam spam
custom_fields custom_fields
api
integration integration
teams teams
contributing
license license
@ -40,6 +40,7 @@ django-helpdesk has been designed for small businesses who need to receive, mana
* Tickets can be opened via email * Tickets can be opened via email
* Multiple queues / categories of tickets * Multiple queues / categories of tickets
* Integrated FAQ / knowledgebase * Integrated FAQ / knowledgebase
* API to manage tickets
Customer-facing Capabilities Customer-facing Capabilities
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -62,6 +63,8 @@ If a user is a staff member, they get general helpdesk access, including:
5. Follow up or respond to tickets 5. Follow up or respond to tickets
6. Assign tickets to themselves or other staff members 6. Assign tickets to themselves or other staff members
7. Resolve tickets 7. Resolve tickets
8. Merge multiple tickets into one
9. Create, Read, Update and Delete tickets through a REST API
Optionally, their access to view tickets, both on the dashboard and through searches and reports, may be restricted by a list of queues to which they have been granted membership. Create and update permissions for individual tickets are not limited by this optional restriction. Optionally, their access to view tickets, both on the dashboard and through searches and reports, may be restricted by a list of queues to which they have been granted membership. Create and update permissions for individual tickets are not limited by this optional restriction.

View File

@ -27,10 +27,10 @@ Installing using PIP
Try using ``pip install django-helpdesk``. Go and have a beer to celebrate Python packaging. Try using ``pip install django-helpdesk``. Go and have a beer to celebrate Python packaging.
Checkout ``master`` from git (Cutting Edge) Checkout ``stable`` from git (Cutting Edge)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you're planning on editing the code or just want to get whatever is the latest and greatest, you can clone the official Git repository with ``git clone git://github.com/django-helpdesk/django-helpdesk.git``. We use the ``master`` branch as our development branch for the next major release of ``django-helpdesk``. If you're planning on editing the code or just want to get whatever is the latest and greatest, you can clone the official Git repository with ``git clone git://github.com/django-helpdesk/django-helpdesk.git``. We use the ``stable`` branch as our development branch for the next major release of ``django-helpdesk``.
Copy the ``helpdesk`` folder into your ``PYTHONPATH``. Copy the ``helpdesk`` folder into your ``PYTHONPATH``.
@ -59,13 +59,14 @@ errors with trying to create User settings.
'django.contrib.humanize', # Required for elapsed time formatting 'django.contrib.humanize', # Required for elapsed time formatting
'bootstrap4form', # Required for nicer formatting of forms with the default templates 'bootstrap4form', # Required for nicer formatting of forms with the default templates
'account', # Required by pinax-teams 'account', # Required by pinax-teams
'pinax.invitations', # required by pinax-teams 'pinax.invitations', # Required by pinax-teams
'pinax.teams', # team support 'pinax.teams', # Team support
'reversion', # required by pinax-teams 'reversion', # Required by pinax-teams
'rest_framework', # required for the API
'helpdesk', # This is us! 'helpdesk', # This is us!
) )
Note: you do not need to use pinax-teams. To dissable teams see the :doc:`teams` section. Note: you do not need to use pinax-teams. To disable teams see the :doc:`teams` section.
Your ``settings.py`` file should also define a ``SITE_ID`` that allows multiple projects to share Your ``settings.py`` file should also define a ``SITE_ID`` that allows multiple projects to share
a single database, and is required by ``django.contrib.sites`` in Django 1.9+. a single database, and is required by ``django.contrib.sites`` in Django 1.9+.
@ -75,11 +76,11 @@ errors with trying to create User settings.
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following line to ``urls.py``:: 2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following line to ``urls.py``::
url(r'helpdesk/', include('helpdesk.urls')), path('helpdesk/', include('helpdesk.urls')),
Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the line will be as follows:: Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the line will be as follows::
url(r'', include('helpdesk.urls', namespace='helpdesk')), path('', include('helpdesk.urls', namespace='helpdesk')),
This line will have to come *after* any other lines in your urls.py such as those used by the Django admin. This line will have to come *after* any other lines in your urls.py such as those used by the Django admin.
@ -90,7 +91,7 @@ errors with trying to create User settings.
Migrate using Django migrations:: Migrate using Django migrations::
./manage.py migrate helpdesk python manage.py migrate helpdesk
4. Include your static files in your public web path:: 4. Include your static files in your public web path::

View File

@ -168,6 +168,10 @@ Staff Ticket Creation Settings
**Default:** ``HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = False`` **Default:** ``HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = False``
- **HELPDESK_ACTIVATE_API_ENDPOINT** Activate the API endpoint to manage tickets thanks to Django REST Framework. See the API section in documentation for more information.
**Default:** ``HELPDESK_ACTIVATE_API_ENDPOINT = False``
Staff Ticket View Settings Staff Ticket View Settings
------------------------------ ------------------------------

View File

@ -16,11 +16,12 @@ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils import timezone from django.utils import timezone
from helpdesk.lib import safe_template_context, process_attachments from helpdesk.lib import safe_template_context, process_attachments, convert_value
from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC, from helpdesk.models import (Ticket, Queue, FollowUp, IgnoreEmail, TicketCC,
CustomField, TicketCustomFieldValue, TicketDependency, UserSettings) CustomField, TicketCustomFieldValue, TicketDependency, UserSettings)
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from helpdesk.settings import CUSTOMFIELD_TO_FIELD_DICT, CUSTOMFIELD_DATETIME_FORMAT, \
CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.models import (KBItem) from helpdesk.models import (KBItem)
@ -28,22 +29,6 @@ if helpdesk_settings.HELPDESK_KB_ENABLED:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
CUSTOMFIELD_TO_FIELD_DICT = {
# Store the immediate equivalences here
'boolean': forms.BooleanField,
'date': forms.DateField,
'time': forms.TimeField,
'datetime': forms.DateTimeField,
'email': forms.EmailField,
'url': forms.URLField,
'ipaddress': forms.GenericIPAddressField,
'slug': forms.SlugField,
}
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT} {CUSTOMFIELD_TIME_FORMAT}"
class CustomFieldMixin(object): class CustomFieldMixin(object):
""" """
@ -71,10 +56,7 @@ class CustomFieldMixin(object):
instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'})
elif field.data_type == 'list': elif field.data_type == 'list':
fieldclass = forms.ChoiceField fieldclass = forms.ChoiceField
choices = field.choices_as_array instanceargs['choices'] = field.get_choices()
if field.empty_selection_list:
choices.insert(0, ('', '---------'))
instanceargs['choices'] = choices
instanceargs['widget'] = forms.Select(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.Select(attrs={'class': 'form-control'})
else: else:
# Try to use the immediate equivalences dictionary # Try to use the immediate equivalences dictionary
@ -155,15 +137,7 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
except ObjectDoesNotExist: except ObjectDoesNotExist:
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
# Convert date/time data type to known fixed format string. cfv.value = convert_value(value)
if datetime is type(value):
cfv.value = value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
elif date is type(value):
cfv.value = value.strftime(CUSTOMFIELD_DATE_FORMAT)
elif time is type(value):
cfv.value = value.strftime(CUSTOMFIELD_TIME_FORMAT)
else:
cfv.value = value
cfv.save() cfv.save()
return super(EditTicketForm, self).save(*args, **kwargs) return super(EditTicketForm, self).save(*args, **kwargs)
@ -290,14 +264,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
return ticket, queue return ticket, queue
def _create_custom_fields(self, ticket): def _create_custom_fields(self, ticket):
for field, value in self.cleaned_data.items(): ticket.save_custom_field_values(self.cleaned_data)
if field.startswith('custom_'):
field_name = field.replace('custom_', '', 1)
custom_field = CustomField.objects.get(name=field_name)
cfv = TicketCustomFieldValue(ticket=ticket,
field=custom_field,
value=value)
cfv.save()
def _create_follow_up(self, ticket, title, user=None): def _create_follow_up(self, ticket, title, user=None):
followup = FollowUp(ticket=ticket, followup = FollowUp(ticket=ticket,

View File

@ -8,12 +8,12 @@ lib.py - Common functions (eg multipart e-mail)
import logging import logging
import mimetypes import mimetypes
from datetime import datetime, date, time
from django.conf import settings from django.conf import settings
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from helpdesk.models import FollowUpAttachment from helpdesk.settings import CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT
logger = logging.getLogger('helpdesk') logger = logging.getLogger('helpdesk')
@ -136,8 +136,7 @@ def process_attachments(followup, attached_files):
if attached.size: if attached.size:
filename = smart_text(attached.name) filename = smart_text(attached.name)
att = FollowUpAttachment( att = followup.followupattachment_set.create(
followup=followup,
file=attached, file=attached,
filename=filename, filename=filename,
mime_type=attached.content_type or mime_type=attached.content_type or
@ -169,3 +168,15 @@ def format_time_spent(time_spent):
else: else:
time_spent = "" time_spent = ""
return time_spent return time_spent
def convert_value(value):
""" Convert date/time data type to known fixed format string """
if type(value) == datetime:
return value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
elif type(value) == date:
return value.strftime(CUSTOMFIELD_DATE_FORMAT)
elif type(value) == time:
return value.strftime(CUSTOMFIELD_TIME_FORMAT)
else:
return value

View File

@ -28,7 +28,10 @@ from markdown.extensions import Extension
import uuid import uuid
from rest_framework import serializers
from helpdesk import settings as helpdesk_settings from helpdesk import settings as helpdesk_settings
from .lib import convert_value
from .validators import validate_file_extension from .validators import validate_file_extension
@ -861,6 +864,27 @@ class Ticket(models.Model):
ticketcc = self.ticketcc_set.create(email=email) ticketcc = self.ticketcc_set.create(email=email)
return ticketcc return ticketcc
def set_custom_field_values(self):
for field in CustomField.objects.all():
try:
value = self.ticketcustomfieldvalue_set.get(field=field).value
except TicketCustomFieldValue.DoesNotExist:
value = None
setattr(self, 'custom_%s' % field.name, value)
def save_custom_field_values(self, data):
for field, value in data.items():
if field.startswith('custom_'):
field_name = field.replace('custom_', '', 1)
customfield = CustomField.objects.get(name=field_name)
cfv, created = self.ticketcustomfieldvalue_set.get_or_create(
field=customfield,
defaults={'value': convert_value(value)}
)
if not created:
cfv.value = convert_value(value)
cfv.save()
class FollowUpManager(models.Manager): class FollowUpManager(models.Manager):
@ -1861,6 +1885,52 @@ class CustomField(models.Model):
verbose_name = _('Custom field') verbose_name = _('Custom field')
verbose_name_plural = _('Custom fields') verbose_name_plural = _('Custom fields')
def get_choices(self):
if not self.data_type == 'list':
return None
choices = self.choices_as_array
if self.empty_selection_list:
choices.insert(0, ('', '---------'))
return choices
def build_api_field(self):
customfield_to_api_field_dict = {
'varchar': serializers.CharField,
'text': serializers.CharField,
'integer': serializers.IntegerField,
'decimal': serializers.DecimalField,
'list': serializers.ChoiceField,
'boolean': serializers.BooleanField,
'date': serializers.DateField,
'time': serializers.TimeField,
'datetime': serializers.DateTimeField,
'email': serializers.EmailField,
'url': serializers.URLField,
'ipaddress': serializers.IPAddressField,
'slug': serializers.SlugField,
}
# Prepare attributes for each types
attributes = {
'label': self.label,
'help_text': self.help_text,
'required': self.required,
}
if self.data_type in ('varchar', 'text'):
attributes['max_length'] = self.max_length
if self.data_type == 'text':
attributes['style'] = {'base_template': 'textarea.html'}
elif self.data_type == 'decimal':
attributes['decimal_places'] = self.decimal_places
attributes['max_digits'] = self.max_length
elif self.data_type == 'list':
attributes['choices'] = self.get_choices()
try:
return customfield_to_api_field_dict[self.data_type](**attributes)
except KeyError:
raise NameError("Unrecognized data_type %s" % self.data_type)
class TicketCustomFieldValue(models.Model): class TicketCustomFieldValue(models.Model):
ticket = models.ForeignKey( ticket = models.ForeignKey(

View File

@ -1,8 +1,11 @@
from rest_framework import serializers from rest_framework import serializers
from django.contrib.humanize.templatetags import humanize from django.contrib.humanize.templatetags import humanize
from rest_framework.exceptions import ValidationError
from .models import Ticket from .forms import TicketForm
from .models import Ticket, CustomField
from .lib import format_time_spent from .lib import format_time_spent
from .user import HelpdeskUser
class DatatablesTicketSerializer(serializers.ModelSerializer): class DatatablesTicketSerializer(serializers.ModelSerializer):
@ -72,5 +75,38 @@ class TicketSerializer(serializers.ModelSerializer):
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', 'last_escalation', 'merged_to' 'priority', 'due_date', 'merged_to'
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add custom fields
for field in CustomField.objects.all():
self.fields['custom_%s' % field.name] = field.build_api_field()
def create(self, validated_data):
""" Use TicketForm to validate and create ticket """
queues = HelpdeskUser(self.context['request'].user).get_queues()
queue_choices = [(q.id, q.title) for q in queues]
data = validated_data.copy()
data['body'] = data['description']
# TicketForm needs id for ForeignKey (not the instance themselves)
data['queue'] = data['queue'].id
if data.get('assigned_to'):
data['assigned_to'] = data['assigned_to'].id
if data.get('merged_to'):
data['merged_to'] = data['merged_to'].id
ticket_form = TicketForm(data=data, queue_choices=queue_choices)
if ticket_form.is_valid():
ticket = ticket_form.save(user=self.context['request'].user)
ticket.set_custom_field_values()
return ticket
raise ValidationError(ticket_form.errors)
def update(self, instance, validated_data):
instance = super().update(instance, validated_data)
instance.save_custom_field_values(validated_data)
return instance

View File

@ -2,10 +2,12 @@
Default settings for django-helpdesk. Default settings for django-helpdesk.
""" """
import os
import warnings import warnings
from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
import os
DEFAULT_USER_SETTINGS = { DEFAULT_USER_SETTINGS = {
'login_view_ticketlist': True, 'login_view_ticketlist': True,
@ -97,6 +99,21 @@ HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
"helpdesk.forms.PublicTicketForm" "helpdesk.forms.PublicTicketForm"
) )
# Custom fields constants
CUSTOMFIELD_TO_FIELD_DICT = {
'boolean': forms.BooleanField,
'date': forms.DateField,
'time': forms.TimeField,
'datetime': forms.DateTimeField,
'email': forms.EmailField,
'url': forms.URLField,
'ipaddress': forms.GenericIPAddressField,
'slug': forms.SlugField,
}
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M"
################################### ###################################
# options for update_ticket views # # options for update_ticket views #
@ -155,6 +172,9 @@ HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTAC
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr( HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False) settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
# Activate the API endpoint to manage tickets thanks to Django REST Framework
HELPDESK_ACTIVATE_API_ENDPOINT = getattr(settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False)
################# #################
# email options # # email options #

262
helpdesk/tests/test_api.py Normal file
View File

@ -0,0 +1,262 @@
import base64
from datetime import datetime
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_201_CREATED, HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST
from rest_framework.test import APITestCase
from helpdesk.models import Queue, Ticket, CustomField
class TicketTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.queue = Queue.objects.create(
title='Test Queue',
slug='test-queue',
)
def test_create_api_ticket_not_authenticated_user(self):
response = self.client.post('/api/tickets/')
self.assertEqual(response.status_code, 403)
def test_create_api_ticket_authenticated_non_staff_user(self):
non_staff_user = User.objects.create_user(username='test')
self.client.force_authenticate(non_staff_user)
response = self.client.post('/api/tickets/')
self.assertEqual(response.status_code, 403)
def test_create_api_ticket_no_data(self):
staff_user = User.objects.create_user(username='test', is_staff=True)
self.client.force_authenticate(staff_user)
response = self.client.post('/api/tickets/')
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {
'queue': [ErrorDetail(string='This field is required.', code='required')],
'title': [ErrorDetail(string='This field is required.', code='required')]
})
self.assertFalse(Ticket.objects.exists())
def test_create_api_ticket_wrong_date_format(self):
staff_user = User.objects.create_user(username='test', is_staff=True)
self.client.force_authenticate(staff_user)
response = self.client.post('/api/tickets/', {
'queue': self.queue.id,
'title': 'Test title',
'due_date': 'monday, 1st of may 2022'
})
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {
'due_date': [ErrorDetail(string='Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].', code='invalid')]
})
self.assertFalse(Ticket.objects.exists())
def test_create_api_ticket_authenticated_staff_user(self):
staff_user = User.objects.create_user(username='test', is_staff=True)
self.client.force_authenticate(staff_user)
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
})
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)
def test_create_api_ticket_with_basic_auth(self):
username = 'admin'
password = 'admin'
User.objects.create_user(username=username, password=password, is_staff=True)
test_user = User.objects.create_user(username='test')
merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket')
# Generate base64 credentials string
credentials = f"{username}:{password}"
base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
response = self.client.post(
'/api/tickets/',
{
'queue': self.queue.id,
'title': 'Title',
'description': 'Description',
'resolution': 'Resolution',
'assigned_to': test_user.id,
'submitter_email': 'test@mail.com',
'status': Ticket.RESOLVED_STATUS,
'priority': 1,
'on_hold': True,
'due_date': datetime(2022, 4, 10, 15, 6),
'merged_to': merge_ticket.id
}
)
self.assertEqual(response.status_code, HTTP_201_CREATED)
created_ticket = Ticket.objects.last()
self.assertEqual(created_ticket.title, 'Title')
self.assertEqual(created_ticket.description, 'Description')
self.assertIsNone(created_ticket.resolution) # resolution can not be set on creation
self.assertEqual(created_ticket.assigned_to, test_user)
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
self.assertEqual(created_ticket.priority, 1)
self.assertFalse(created_ticket.on_hold) # on_hold is False on creation
self.assertEqual(created_ticket.status, Ticket.OPEN_STATUS) # status is always open on creation
self.assertEqual(created_ticket.due_date, datetime(2022, 4, 10, 15, 6, tzinfo=UTC))
self.assertIsNone(created_ticket.merged_to) # merged_to can not be set on creation
def test_edit_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket')
test_user = User.objects.create_user(username='test')
merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket')
self.client.force_authenticate(staff_user)
response = self.client.put(
'/api/tickets/%d/' % test_ticket.id,
{
'queue': self.queue.id,
'title': 'Title',
'description': 'Description',
'resolution': 'Resolution',
'assigned_to': test_user.id,
'submitter_email': 'test@mail.com',
'status': Ticket.RESOLVED_STATUS,
'priority': 1,
'on_hold': True,
'due_date': datetime(2022, 4, 10, 15, 6),
'merged_to': merge_ticket.id
}
)
self.assertEqual(response.status_code, HTTP_200_OK)
test_ticket.refresh_from_db()
self.assertEqual(test_ticket.title, 'Title')
self.assertEqual(test_ticket.description, 'Description')
self.assertEqual(test_ticket.resolution, 'Resolution')
self.assertEqual(test_ticket.assigned_to, test_user)
self.assertEqual(test_ticket.submitter_email, 'test@mail.com')
self.assertEqual(test_ticket.priority, 1)
self.assertTrue(test_ticket.on_hold)
self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS)
self.assertEqual(test_ticket.due_date, datetime(2022, 4, 10, 15, 6, tzinfo=UTC))
self.assertEqual(test_ticket.merged_to, merge_ticket)
def test_partial_edit_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket')
self.client.force_authenticate(staff_user)
response = self.client.patch(
'/api/tickets/%d/' % test_ticket.id,
{
'description': 'New description',
}
)
self.assertEqual(response.status_code, HTTP_200_OK)
test_ticket.refresh_from_db()
self.assertEqual(test_ticket.description, 'New description')
def test_delete_api_ticket(self):
staff_user = User.objects.create_user(username='admin', is_staff=True)
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket')
self.client.force_authenticate(staff_user)
response = self.client.delete('/api/tickets/%d/' % test_ticket.id)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertFalse(Ticket.objects.exists())
def test_create_api_ticket_with_custom_fields(self):
# Create custom fields
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
extra_data = {}
if field_type in ('varchar', 'text'):
extra_data['max_length'] = 10
if field_type == 'integer':
# Set one field as required to test error if not provided
extra_data['required'] = True
if field_type == 'decimal':
extra_data['max_length'] = 7
extra_data['decimal_places'] = 3
if field_type == 'list':
extra_data['list_values'] = '''Green
Blue
Red
Yellow'''
CustomField.objects.create(name=field_type, label=field_display, data_type=field_type, **extra_data)
staff_user = User.objects.create_user(username='test', is_staff=True)
self.client.force_authenticate(staff_user)
# Test creation without providing required field
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
})
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'custom_integer': [ErrorDetail(string='This field is required.', code='required')]})
# Test creation with custom field values
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,
'custom_varchar': 'test',
'custom_text': 'multi\nline',
'custom_integer': '1',
'custom_decimal': '42.987',
'custom_list': 'Red',
'custom_boolean': True,
'custom_date': '2022-4-11',
'custom_time': '23:59:59',
'custom_datetime': '2022-4-10 18:27',
'custom_email': 'email@test.com',
'custom_url': 'http://django-helpdesk.readthedocs.org/',
'custom_ipaddress': '127.0.0.1',
'custom_slug': 'test-slug',
})
self.assertEqual(response.status_code, HTTP_201_CREATED)
# Check all fields with data returned from the response
self.assertEqual(response.data, {
'id': 1,
'queue': 1,
'title': 'Test title',
'description': 'Test description\nMulti lines',
'resolution': None,
'submitter_email': 'test@mail.com',
'assigned_to': None,
'status': 1,
'on_hold': False,
'priority': 4,
'due_date': None,
'merged_to': None,
'custom_varchar': 'test',
'custom_text': 'multi\nline',
'custom_integer': 1,
'custom_decimal': '42.987',
'custom_list': 'Red',
'custom_boolean': True,
'custom_date': '2022-04-11',
'custom_time': '23:59:59',
'custom_datetime': '2022-04-10T18:27',
'custom_email': 'email@test.com',
'custom_url': 'http://django-helpdesk.readthedocs.org/',
'custom_ipaddress': '127.0.0.1',
'custom_slug': 'test-slug'
})

View File

@ -147,7 +147,7 @@ class AttachmentUnitTests(TestCase):
self.assertEqual(obj.size, len(self.file_attrs['content'])) self.assertEqual(obj.size, len(self.file_attrs['content']))
self.assertEqual(obj.mime_type, "text/plain") self.assertEqual(obj.mime_type, "text/plain")
@mock.patch.object(lib.FollowUpAttachment, 'save', autospec=True) @mock.patch.object('helpdesk.lib.FollowUpAttachment', 'save', autospec=True)
@override_settings(MEDIA_ROOT=MEDIA_DIR) @override_settings(MEDIA_ROOT=MEDIA_DIR)
def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save): def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
""" don't mock saving to filesystem to test file renames caused by storage layer """ """ don't mock saving to filesystem to test file renames caused by storage layer """

View File

@ -219,12 +219,13 @@ urlpatterns += [
] ]
# API # API is added to url conf based on the setting (False by default)
router = DefaultRouter() if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT:
router.register(r'tickets', TicketViewSet, basename='ticket') router = DefaultRouter()
urlpatterns += [ router.register(r'tickets', TicketViewSet, basename='ticket')
url(r'^api/', include(router.urls)) urlpatterns += [
] url(r'^api/', include(router.urls))
]
urlpatterns += [ urlpatterns += [

View File

@ -16,10 +16,10 @@ def validate_file_extension(value):
# check if VALID_EXTENSIONS is defined in settings.py # check if VALID_EXTENSIONS is defined in settings.py
# if not use defaults # if not use defaults
if settings.VALID_EXTENSIONS: if hasattr(settings, 'VALID_EXTENSIONS'):
valid_extensions = settings.VALID_EXTENSIONS valid_extensions = settings.VALID_EXTENSIONS
else: else:
valid_extensions = ['.txt', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png'] valid_extensions = ['.txt', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']
if not ext.lower() in valid_extensions: if not ext.lower() in valid_extensions:
raise ValidationError('Unsupported file extension.') raise ValidationError('Unsupported file extension.')

View File

@ -12,3 +12,14 @@ class TicketViewSet(viewsets.ModelViewSet):
queryset = Ticket.objects.all() queryset = Ticket.objects.all()
serializer_class = TicketSerializer serializer_class = TicketSerializer
permission_classes = [IsAdminUser] permission_classes = [IsAdminUser]
def get_queryset(self):
tickets = Ticket.objects.all()
for ticket in tickets:
ticket.set_custom_field_values()
return tickets
def get_object(self):
ticket = super().get_object()
ticket.set_custom_field_values()
return ticket