forked from extern/django-helpdesk
Create an API endpoint to list/create/update/delete tickets (including custom fields !) + Refactoring in forms.py
This commit is contained in:
parent
6b79e1499b
commit
1daa1d88aa
@ -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 = [
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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 #
|
||||||
|
@ -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
26
helpdesk/views/api.py
Normal 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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user