Create an API endpoint to list/create/update/delete tickets (including custom fields !) + Refactoring in forms.py

This commit is contained in:
bbe 2022-03-31 17:19:49 +02:00
parent 6b79e1499b
commit 1daa1d88aa
9 changed files with 199 additions and 55 deletions

View File

@ -55,6 +55,7 @@ INSTALLED_APPS = [
'pinax.teams', # team support 'pinax.teams', # team support
'reversion', # required by pinax-teams 'reversion', # required by pinax-teams
'helpdesk', # This is us! 'helpdesk', # This is us!
'rest_framework', # required for the API
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -28,4 +28,5 @@ from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^', include('helpdesk.urls', namespace='helpdesk')), url(r'^', include('helpdesk.urls', namespace='helpdesk')),
url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

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(
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,18 +1,18 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Ticket
from .lib import format_time_spent
from django.contrib.humanize.templatetags import humanize from django.contrib.humanize.templatetags import humanize
from rest_framework.exceptions import ValidationError
""" from .forms import TicketForm
A serializer for the Ticket model, returns data in the format as required by from .models import Ticket, CustomField
datatables for ticket_list.html. Called from staff.datatables_ticket_list. from .lib import format_time_spent
from .user import HelpdeskUser
"""
class DatatablesTicketSerializer(serializers.ModelSerializer): class DatatablesTicketSerializer(serializers.ModelSerializer):
"""
A serializer for the Ticket model, returns data in the format as required by
datatables for ticket_list.html. Called from staff.datatables_ticket_list.
"""
ticket = serializers.SerializerMethodField() ticket = serializers.SerializerMethodField()
assigned_to = serializers.SerializerMethodField() assigned_to = serializers.SerializerMethodField()
submitter = serializers.SerializerMethodField() submitter = serializers.SerializerMethodField()
@ -68,3 +68,45 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
def get_kbitem(self, obj): def get_kbitem(self, obj):
return obj.kbitem.title if obj.kbitem else "" return obj.kbitem.title if obj.kbitem else ""
class TicketSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
fields = (
'id', 'queue', 'title', 'description', 'resolution', 'submitter_email', 'assigned_to', 'status', 'on_hold',
'priority', 'due_date', 'last_escalation', '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['assigned_to']:
data['assigned_to'] = data['assigned_to'].id
if data['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 #

View File

@ -10,13 +10,14 @@ urls.py - Mapping of URL's to our various views. Note we always used NAMED
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import include
from django.views.generic import TemplateView from django.views.generic import TemplateView
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 import settings as helpdesk_settings
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
if helpdesk_settings.HELPDESK_KB_ENABLED: if helpdesk_settings.HELPDESK_KB_ENABLED:
from helpdesk.views import kb from helpdesk.views import kb
@ -218,6 +219,14 @@ urlpatterns += [
] ]
# API
router = DefaultRouter()
router.register(r'tickets', TicketViewSet, basename='ticket')
urlpatterns += [
url(r'^api/', include(router.urls))
]
urlpatterns += [ urlpatterns += [
url(r'^login/$', url(r'^login/$',
login.login, login.login,

26
helpdesk/views/api.py Normal file
View File

@ -0,0 +1,26 @@
from rest_framework import viewsets
from rest_framework.permissions import IsAdminUser
from helpdesk.models import Ticket
from helpdesk.serializers import TicketSerializer
class TicketViewSet(viewsets.ModelViewSet):
"""
A viewset that provides the standard actions to handle Ticket
"""
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
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