From 1daa1d88aafcef8186400baa9f3802cb48105a57 Mon Sep 17 00:00:00 2001 From: bbe Date: Thu, 31 Mar 2022 17:19:49 +0200 Subject: [PATCH] Create an API endpoint to list/create/update/delete tickets (including custom fields !) + Refactoring in forms.py --- demo/demodesk/config/settings.py | 1 + demo/demodesk/config/urls.py | 1 + helpdesk/forms.py | 45 +++----------------- helpdesk/lib.py | 19 +++++++-- helpdesk/models.py | 70 ++++++++++++++++++++++++++++++++ helpdesk/serializers.py | 60 +++++++++++++++++++++++---- helpdesk/settings.py | 19 ++++++++- helpdesk/urls.py | 13 +++++- helpdesk/views/api.py | 26 ++++++++++++ 9 files changed, 199 insertions(+), 55 deletions(-) create mode 100644 helpdesk/views/api.py diff --git a/demo/demodesk/config/settings.py b/demo/demodesk/config/settings.py index 7914d648..e2998c30 100644 --- a/demo/demodesk/config/settings.py +++ b/demo/demodesk/config/settings.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ 'pinax.teams', # team support 'reversion', # required by pinax-teams 'helpdesk', # This is us! + 'rest_framework', # required for the API ] MIDDLEWARE = [ diff --git a/demo/demodesk/config/urls.py b/demo/demodesk/config/urls.py index 0c37aac6..52f6f407 100644 --- a/demo/demodesk/config/urls.py +++ b/demo/demodesk/config/urls.py @@ -28,4 +28,5 @@ from django.conf.urls.static import static urlpatterns = [ url(r'^admin/', admin.site.urls), 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) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 399b7e18..6b776bfc 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -16,11 +16,12 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import get_user_model 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, CustomField, TicketCustomFieldValue, TicketDependency, UserSettings) 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: from helpdesk.models import (KBItem) @@ -28,22 +29,6 @@ if helpdesk_settings.HELPDESK_KB_ENABLED: logger = logging.getLogger(__name__) 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): """ @@ -71,10 +56,7 @@ class CustomFieldMixin(object): instanceargs['widget'] = forms.NumberInput(attrs={'class': 'form-control'}) elif field.data_type == 'list': fieldclass = forms.ChoiceField - choices = field.choices_as_array - if field.empty_selection_list: - choices.insert(0, ('', '---------')) - instanceargs['choices'] = choices + instanceargs['choices'] = field.get_choices() instanceargs['widget'] = forms.Select(attrs={'class': 'form-control'}) else: # Try to use the immediate equivalences dictionary @@ -155,15 +137,7 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm): except ObjectDoesNotExist: cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield) - # Convert date/time data type to known fixed format string. - 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.value = convert_value(value) cfv.save() return super(EditTicketForm, self).save(*args, **kwargs) @@ -290,14 +264,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form): return ticket, queue def _create_custom_fields(self, ticket): - for field, value in self.cleaned_data.items(): - 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() + ticket.save_custom_field_values(self.cleaned_data) def _create_follow_up(self, ticket, title, user=None): followup = FollowUp(ticket=ticket, diff --git a/helpdesk/lib.py b/helpdesk/lib.py index da853990..05e80dfd 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -8,12 +8,12 @@ lib.py - Common functions (eg multipart e-mail) import logging import mimetypes +from datetime import datetime, date, time from django.conf import settings 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') @@ -136,8 +136,7 @@ def process_attachments(followup, attached_files): if attached.size: filename = smart_text(attached.name) - att = FollowUpAttachment( - followup=followup, + att = followup.followupattachment_set( file=attached, filename=filename, mime_type=attached.content_type or @@ -169,3 +168,15 @@ def format_time_spent(time_spent): else: 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 diff --git a/helpdesk/models.py b/helpdesk/models.py index f8ffb8b1..80aa3bae 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -28,7 +28,10 @@ from markdown.extensions import Extension import uuid +from rest_framework import serializers + from helpdesk import settings as helpdesk_settings +from .lib import convert_value from .validators import validate_file_extension @@ -861,6 +864,27 @@ class Ticket(models.Model): ticketcc = self.ticketcc_set.create(email=email) 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): @@ -1861,6 +1885,52 @@ class CustomField(models.Model): verbose_name = _('Custom field') 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): ticket = models.ForeignKey( diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py index df0c6a5f..3d7b61fd 100644 --- a/helpdesk/serializers.py +++ b/helpdesk/serializers.py @@ -1,18 +1,18 @@ from rest_framework import serializers - -from .models import Ticket -from .lib import format_time_spent - from django.contrib.humanize.templatetags import humanize +from rest_framework.exceptions import ValidationError -""" -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. - -""" +from .forms import TicketForm +from .models import Ticket, CustomField +from .lib import format_time_spent +from .user import HelpdeskUser 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() assigned_to = serializers.SerializerMethodField() submitter = serializers.SerializerMethodField() @@ -68,3 +68,45 @@ class DatatablesTicketSerializer(serializers.ModelSerializer): def get_kbitem(self, obj): 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 diff --git a/helpdesk/settings.py b/helpdesk/settings.py index c68cc4b9..56be1dbb 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -2,10 +2,12 @@ Default settings for django-helpdesk. """ +import os import warnings + +from django import forms from django.conf import settings from django.core.exceptions import ImproperlyConfigured -import os DEFAULT_USER_SETTINGS = { 'login_view_ticketlist': True, @@ -97,6 +99,21 @@ HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr( "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 # diff --git a/helpdesk/urls.py b/helpdesk/urls.py index e9df6bb2..29a6654f 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -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.contrib.auth.decorators import login_required from django.contrib.auth import views as auth_views +from django.urls import include from django.views.generic import TemplateView +from rest_framework.routers import DefaultRouter 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 import settings as helpdesk_settings - +from helpdesk.views.api import TicketViewSet if helpdesk_settings.HELPDESK_KB_ENABLED: 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 += [ url(r'^login/$', login.login, diff --git a/helpdesk/views/api.py b/helpdesk/views/api.py new file mode 100644 index 00000000..272b386f --- /dev/null +++ b/helpdesk/views/api.py @@ -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 +