From 1daa1d88aafcef8186400baa9f3802cb48105a57 Mon Sep 17 00:00:00 2001 From: bbe Date: Thu, 31 Mar 2022 17:19:49 +0200 Subject: [PATCH 01/22] 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 + From e2cb881eb84da5b821bcdda5de37bfaebafd451a Mon Sep 17 00:00:00 2001 From: bbe Date: Thu, 31 Mar 2022 17:25:08 +0200 Subject: [PATCH 02/22] Create an API endpoint to list/create/update/delete tickets (ignoring custom fields) --- demo/demodesk/config/settings.py | 1 + demo/demodesk/config/urls.py | 1 + helpdesk/serializers.py | 22 ++++++++++++++-------- helpdesk/urls.py | 13 +++++++++++-- helpdesk/views/api.py | 14 ++++++++++++++ 5 files changed, 41 insertions(+), 10 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/serializers.py b/helpdesk/serializers.py index df0c6a5f..dac5e381 100644 --- a/helpdesk/serializers.py +++ b/helpdesk/serializers.py @@ -1,18 +1,15 @@ from rest_framework import serializers +from django.contrib.humanize.templatetags import humanize from .models import Ticket from .lib import format_time_spent -from django.contrib.humanize.templatetags import humanize - -""" -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. - -""" - 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 +65,12 @@ 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' + ) 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..1bf362fb --- /dev/null +++ b/helpdesk/views/api.py @@ -0,0 +1,14 @@ +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] From 88b234958a2fc7aeb2b915249a7a7bc89690ab47 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Thu, 7 Apr 2022 00:42:12 +0200 Subject: [PATCH 03/22] Fix a bug when assigned_to or merged_to were not passed in body --- helpdesk/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py index 3d7b61fd..7852f620 100644 --- a/helpdesk/serializers.py +++ b/helpdesk/serializers.py @@ -93,9 +93,9 @@ class TicketSerializer(serializers.ModelSerializer): data['body'] = data['description'] # TicketForm needs id for ForeignKey (not the instance themselves) data['queue'] = data['queue'].id - if data['assigned_to']: + if data.get('assigned_to'): data['assigned_to'] = data['assigned_to'].id - if data['merged_to']: + if data.get('merged_to'): data['merged_to'] = data['merged_to'].id ticket_form = TicketForm(data=data, queue_choices=queue_choices) From b8451eef164bcab0598de456b8fc4c5e70687653 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Thu, 7 Apr 2022 00:43:00 +0200 Subject: [PATCH 04/22] Add an API section in documentation + fix small things --- docs/api.rst | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 5 +++- docs/install.rst | 19 ++++++++-------- 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 docs/api.rst diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..c11df0c5 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,59 @@ +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. + +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/`` 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 optonal 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 +- **last_escalation**: date representation of when last escalation has been done +- **merged_to**: ID of the ticket to which it is merged + +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/`` 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/`` 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/`` with a **DELETE** request will let you delete the ticket you provided the ID. diff --git a/docs/index.rst b/docs/index.rst index 15888094..6ed44704 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,9 +16,9 @@ Contents settings spam custom_fields + api integration teams - contributing license @@ -40,6 +40,7 @@ django-helpdesk has been designed for small businesses who need to receive, mana * Tickets can be opened via email * Multiple queues / categories of tickets * Integrated FAQ / knowledgebase +* API to manage tickets 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 6. Assign tickets to themselves or other staff members 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. diff --git a/docs/install.rst b/docs/install.rst index e1107b07..6b34c3d4 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -27,10 +27,10 @@ Installing using PIP 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``. @@ -59,13 +59,14 @@ errors with trying to create User settings. 'django.contrib.humanize', # Required for elapsed time formatting 'bootstrap4form', # Required for nicer formatting of forms with the default templates 'account', # Required by pinax-teams - 'pinax.invitations', # required by pinax-teams - 'pinax.teams', # team support - 'reversion', # required by pinax-teams + 'pinax.invitations', # Required by pinax-teams + 'pinax.teams', # Team support + 'reversion', # Required by pinax-teams + 'rest_framework', # required for the API '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 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``:: - 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:: - 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. @@ -90,7 +91,7 @@ errors with trying to create User settings. Migrate using Django migrations:: - ./manage.py migrate helpdesk + python manage.py migrate helpdesk 4. Include your static files in your public web path:: From 25b1e1e5bdbb8a2480c671e3bab9d56f02b9bfbf Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Thu, 7 Apr 2022 00:48:50 +0200 Subject: [PATCH 05/22] Add a sentence about custom fields --- docs/api.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index c11df0c5..805000ca 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -32,6 +32,8 @@ You need to provide a JSON body with the following data : - **last_escalation**: date representation of when last escalation has been done - **merged_to**: ID of the ticket to which it is merged +Moreover, if you created custom fields, you can add them into the body with the key ``custom_``. + 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/' \ From dbcd0fe6b45de144d7d989fad38ca9cf1707a925 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Mon, 11 Apr 2022 00:15:11 +0200 Subject: [PATCH 06/22] Remove last_escalation field from serializer since it is a non-editable field --- helpdesk/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py index 7852f620..eb0cc447 100644 --- a/helpdesk/serializers.py +++ b/helpdesk/serializers.py @@ -75,7 +75,7 @@ class TicketSerializer(serializers.ModelSerializer): model = Ticket fields = ( '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): From 5701bf0229129ebbc794a97545c6afb8c02f70f1 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Mon, 11 Apr 2022 00:15:48 +0200 Subject: [PATCH 07/22] Fix a bug with get_choices function which doesn't return anything if empty_selection_list is False --- helpdesk/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/models.py b/helpdesk/models.py index 80aa3bae..cb3d4149 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -1891,7 +1891,7 @@ class CustomField(models.Model): choices = self.choices_as_array if self.empty_selection_list: choices.insert(0, ('', '---------')) - return choices + return choices def build_api_field(self): customfield_to_api_field_dict = { From 31acfb11ddccb66f9383e797acba750ca7f44ccd Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Mon, 11 Apr 2022 00:16:54 +0200 Subject: [PATCH 08/22] Add tests for the API --- helpdesk/tests/test_api.py | 262 +++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 helpdesk/tests/test_api.py diff --git a/helpdesk/tests/test_api.py b/helpdesk/tests/test_api.py new file mode 100644 index 00000000..3213509b --- /dev/null +++ b/helpdesk/tests/test_api.py @@ -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) + 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) + + 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' + }) + From 7e5ff05b02b3be8bc3dd37551a320c5435b734ad Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Mon, 11 Apr 2022 00:23:22 +0200 Subject: [PATCH 09/22] Update documentation --- docs/api.rst | 7 ++++--- helpdesk/tests/test_api.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 805000ca..42f4ec5d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -22,16 +22,17 @@ 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 optonal text for the resoltuion 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 -- **last_escalation**: date representation of when last escalation has been done - **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_``. Here is an example of a cURL request to create a ticket (using Basic authentication) :: @@ -39,7 +40,7 @@ Here is an example of a cURL request to create a ticket (using Basic authenticat 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}' + --data-raw '{"queue": 1, "title": "Test Ticket API", "description": "Test create ticket from API", "submitter_email": "test@mail.com", "priority": 4}' PUT --- diff --git a/helpdesk/tests/test_api.py b/helpdesk/tests/test_api.py index 3213509b..4b679479 100644 --- a/helpdesk/tests/test_api.py +++ b/helpdesk/tests/test_api.py @@ -109,10 +109,10 @@ class TicketTest(APITestCase): 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) + 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) + 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) From 9320df02683f084a4af01c2dc03c38bdd4d5a502 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Thu, 14 Apr 2022 23:30:42 +0200 Subject: [PATCH 10/22] Fix a bug I introduced in earlier commit + fix one test --- helpdesk/lib.py | 2 +- helpdesk/tests/test_attachments.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 05e80dfd..f9adb174 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -136,7 +136,7 @@ def process_attachments(followup, attached_files): if attached.size: filename = smart_text(attached.name) - att = followup.followupattachment_set( + att = followup.followupattachment_set.create( file=attached, filename=filename, mime_type=attached.content_type or diff --git a/helpdesk/tests/test_attachments.py b/helpdesk/tests/test_attachments.py index 942a2353..82983e68 100644 --- a/helpdesk/tests/test_attachments.py +++ b/helpdesk/tests/test_attachments.py @@ -147,7 +147,7 @@ class AttachmentUnitTests(TestCase): self.assertEqual(obj.size, len(self.file_attrs['content'])) 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) 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 """ From 3e331c08fb52b526dbc3bbbe92be463cf4283226 Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Thu, 14 Apr 2022 23:45:19 +0200 Subject: [PATCH 11/22] Try to fix other tests --- helpdesk/validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/validators.py b/helpdesk/validators.py index 01500a9a..1c0ccb24 100644 --- a/helpdesk/validators.py +++ b/helpdesk/validators.py @@ -16,10 +16,10 @@ def validate_file_extension(value): # check if VALID_EXTENSIONS is defined in settings.py # if not use defaults - if settings.VALID_EXTENSIONS: + if hasattr(settings, 'VALID_EXTENSIONS'): valid_extensions = settings.VALID_EXTENSIONS 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: raise ValidationError('Unsupported file extension.') From dc60d8a7f95ed789872817bf3a7e269f38b5fc3e Mon Sep 17 00:00:00 2001 From: Benbb96 Date: Fri, 15 Apr 2022 00:05:51 +0200 Subject: [PATCH 12/22] Add setting HELPDESK_ACTIVATE_API_ENDPOINT and document it --- demo/demodesk/config/settings.py | 3 +++ docs/api.rst | 4 ++++ docs/settings.rst | 4 ++++ helpdesk/settings.py | 3 +++ helpdesk/urls.py | 13 +++++++------ 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/demo/demodesk/config/settings.py b/demo/demodesk/config/settings.py index e2998c30..e4ac94f1 100644 --- a/demo/demodesk/config/settings.py +++ b/demo/demodesk/config/settings.py @@ -117,6 +117,9 @@ HELPDESK_KB_ENABLED = True # Allow users to change their passwords HELPDESK_SHOW_CHANGE_PASSWORD = True +# Activate the API +HELPDESK_ACTIVATE_API_ENDPOINT = True + # Instead of showing the public web portal first, # we can instead redirect users straight to the login page. HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False diff --git a/docs/api.rst b/docs/api.rst index 42f4ec5d..474bfe22 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,6 +3,10 @@ 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 diff --git a/docs/settings.rst b/docs/settings.rst index 405e8f8d..246ce4b6 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -168,6 +168,10 @@ Staff Ticket Creation Settings **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 ------------------------------ diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 56be1dbb..a46af953 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -172,6 +172,9 @@ HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTAC HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr( 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 # diff --git a/helpdesk/urls.py b/helpdesk/urls.py index 29a6654f..fc185cc9 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -219,12 +219,13 @@ urlpatterns += [ ] -# API -router = DefaultRouter() -router.register(r'tickets', TicketViewSet, basename='ticket') -urlpatterns += [ - url(r'^api/', include(router.urls)) -] +# API is added to url conf based on the setting (False by default) +if helpdesk_settings.HELPDESK_ACTIVATE_API_ENDPOINT: + router = DefaultRouter() + router.register(r'tickets', TicketViewSet, basename='ticket') + urlpatterns += [ + url(r'^api/', include(router.urls)) + ] urlpatterns += [ From 5792587e3eeacb7dd5a60dc56794df762182a77e Mon Sep 17 00:00:00 2001 From: Daniele Scasciafratte Date: Fri, 15 Apr 2022 18:48:21 +0200 Subject: [PATCH 13/22] feat(ticket): disable timeline --- demo/demodesk/config/settings.py | 2 ++ helpdesk/settings.py | 3 +++ helpdesk/templates/helpdesk/ticket_list.html | 2 ++ helpdesk/views/staff.py | 1 + 4 files changed, 8 insertions(+) diff --git a/demo/demodesk/config/settings.py b/demo/demodesk/config/settings.py index e4ac94f1..ec301a91 100644 --- a/demo/demodesk/config/settings.py +++ b/demo/demodesk/config/settings.py @@ -114,6 +114,8 @@ HELPDESK_SUBMIT_A_TICKET_PUBLIC = True # Should the Knowledgebase be enabled? HELPDESK_KB_ENABLED = True +HELPDESK_TICKETS_TIMELINE_ENABLED = True + # Allow users to change their passwords HELPDESK_SHOW_CHANGE_PASSWORD = True diff --git a/helpdesk/settings.py b/helpdesk/settings.py index a46af953..31a1f742 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -49,6 +49,9 @@ HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings, # show knowledgebase links? HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True) +# Disable Timeline on ticket list +HELPDESK_TICKETS_TIMELINE_ENABLED = getattr(settings, 'HELPDESK_TICKETS_TIMELINE_ENABLED', True) + # show extended navigation by default, to all users, irrespective of staff status? HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False) diff --git a/helpdesk/templates/helpdesk/ticket_list.html b/helpdesk/templates/helpdesk/ticket_list.html index 2894349b..56402486 100644 --- a/helpdesk/templates/helpdesk/ticket_list.html +++ b/helpdesk/templates/helpdesk/ticket_list.html @@ -33,6 +33,7 @@ {% block helpdesk_body %}
+ {% if helpdesk_settings.HELPDESK_TICKETS_TIMELINE_ENABLED %}
+ {% endif %}
{{ search_message|safe }}
diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index d8cd1f5d..82cddfa6 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -1167,6 +1167,7 @@ def ticket_list(request): from_saved_query=saved_query is not None, saved_query=saved_query, search_message=search_message, + helpdesk_settings=helpdesk_settings, )) From 26c6a778ec25654686f2281dc8e48683293c27ac Mon Sep 17 00:00:00 2001 From: Daniele Scasciafratte Date: Fri, 15 Apr 2022 18:51:02 +0200 Subject: [PATCH 14/22] feat(ticket): disable timeline --- helpdesk/templates/helpdesk/ticket_list.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helpdesk/templates/helpdesk/ticket_list.html b/helpdesk/templates/helpdesk/ticket_list.html index 56402486..fad94e25 100644 --- a/helpdesk/templates/helpdesk/ticket_list.html +++ b/helpdesk/templates/helpdesk/ticket_list.html @@ -127,9 +127,11 @@

+ {% if helpdesk_settings.HELPDESK_TICKETS_TIMELINE_ENABLED %}
+ {% endif %}
From 479451ccac99b3b2d413101128f0185cc3af8337 Mon Sep 17 00:00:00 2001 From: Daniele Scasciafratte Date: Wed, 20 Apr 2022 15:39:27 +0200 Subject: [PATCH 15/22] feat(docs): updated --- docs/settings.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/settings.rst b/docs/settings.rst index 246ce4b6..32fc1d00 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -94,6 +94,10 @@ These changes are visible throughout django-helpdesk **Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False`` +- **HELPDESK_TICKETS_TIMELINE_ENABLED** If True, redirects user to a 404 page when attempting to reach ticket pages while not logged in, rather than redirecting to a login screen. + + **Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = True`` + Options shown on public pages ----------------------------- From 041308d0748d1e3ca039ce7d5ed6017b9d46f1c2 Mon Sep 17 00:00:00 2001 From: Daniele Scasciafratte Date: Wed, 20 Apr 2022 15:47:36 +0200 Subject: [PATCH 16/22] feat(docs): updated --- docs/settings.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 32fc1d00..362241ad 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -94,9 +94,9 @@ These changes are visible throughout django-helpdesk **Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False`` -- **HELPDESK_TICKETS_TIMELINE_ENABLED** If True, redirects user to a 404 page when attempting to reach ticket pages while not logged in, rather than redirecting to a login screen. +- **HELPDESK_TICKETS_TIMELINE_ENABLED** If False, remove from the dashboard the Timeline view for tickets. - **Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = True`` + **Default:** ``HELPDESK_TICKETS_TIMELINE_ENABLED = True`` Options shown on public pages ----------------------------- From de9c5a709e02d03412cf010006c9f813a16f0d50 Mon Sep 17 00:00:00 2001 From: Daniele Scasciafratte Date: Wed, 20 Apr 2022 16:36:37 +0200 Subject: [PATCH 17/22] feat(fields): no time spent --- docs/settings.rst | 8 ++++++++ helpdesk/settings.py | 10 ++++++++++ helpdesk/templates/helpdesk/followup_edit.html | 2 ++ helpdesk/templates/helpdesk/report_index.html | 2 +- helpdesk/templates/helpdesk/ticket.html | 4 +++- helpdesk/templates/helpdesk/ticket_desc_table.html | 5 +++++ 6 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 246ce4b6..86d2b446 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -94,6 +94,14 @@ These changes are visible throughout django-helpdesk **Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False`` +- **HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET** If False, disable the fields on ticket. + + **Default:** ``HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = True`` + +- **HELPDESK_ENABLE_TIME_SPENT_ON_TICKET** If False, disable the fields on ticket. + + **Default:** ``HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = True`` + Options shown on public pages ----------------------------- diff --git a/helpdesk/settings.py b/helpdesk/settings.py index a46af953..66f8039a 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -41,6 +41,16 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings, 'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT', False) +# Enable the Dependencies field on ticket view +HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings, + 'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET', + True) + +# Enable the Time spent on field on ticket view +HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = getattr(settings, + 'HELPDESK_ENABLE_TIME_SPENT_ON_TICKET', + True) + # raises a 404 to anon users. It's like it was invisible HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings, 'HELPDESK_ANON_ACCESS_RAISES_404', diff --git a/helpdesk/templates/helpdesk/followup_edit.html b/helpdesk/templates/helpdesk/followup_edit.html index e74e3203..af61e6e1 100644 --- a/helpdesk/templates/helpdesk/followup_edit.html +++ b/helpdesk/templates/helpdesk/followup_edit.html @@ -46,8 +46,10 @@
{{ form.new_status }}

If the status was changed, what was it changed to?

+ {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
{{ form.time_spent }}
+ {% endif %}

{% csrf_token %} diff --git a/helpdesk/templates/helpdesk/report_index.html b/helpdesk/templates/helpdesk/report_index.html index 4d82f40c..055243a7 100644 --- a/helpdesk/templates/helpdesk/report_index.html +++ b/helpdesk/templates/helpdesk/report_index.html @@ -55,7 +55,7 @@ {% if queue.open %}{% endif %}{{ queue.open }}{% if queue.open %}{% endif %} {% if queue.resolved %}{% endif %}{{ queue.resolved }}{% if queue.resolved %}{% endif %} {% if queue.closed %}{% endif %}{{ queue.closed }}{% if queue.closed %}{% endif %} - {{ queue.time_spent }}{% if queue.dedicated_time %} / {{ queue.dedicated_time }}{% endif %} + {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}{{ queue.time_spent }}{% if queue.dedicated_time %} / {{ queue.dedicated_time }}{% endif %}{% endif %} {% empty %} {% trans "There are no unassigned tickets." %} diff --git a/helpdesk/templates/helpdesk/ticket.html b/helpdesk/templates/helpdesk/ticket.html index 5eade600..ff0a3fa8 100644 --- a/helpdesk/templates/helpdesk/ticket.html +++ b/helpdesk/templates/helpdesk/ticket.html @@ -46,7 +46,7 @@
{{ followup.title|num_to_link }}
-   +  

{% if followup.comment %} @@ -156,12 +156,14 @@

{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}
{% endif %} + {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %} {% if user.is_staff %}
{% trans "(Optional)" %}
{% endif %} + {% endif %}

diff --git a/helpdesk/templates/helpdesk/ticket_desc_table.html b/helpdesk/templates/helpdesk/ticket_desc_table.html index 36df46fb..07fadee4 100644 --- a/helpdesk/templates/helpdesk/ticket_desc_table.html +++ b/helpdesk/templates/helpdesk/ticket_desc_table.html @@ -82,8 +82,13 @@ {% trans "This ticket has no dependencies." %} {% endfor %} + {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %} {% trans "Total time spent" %} {{ ticket.time_spent_formated }} + {% else %} + + + {% endif %} {% if ticket.kbitem %} From 9bd4eb19bcfbe5d6ecb4521659766d74dd4c7ac2 Mon Sep 17 00:00:00 2001 From: Daniele Scasciafratte Date: Wed, 20 Apr 2022 16:44:48 +0200 Subject: [PATCH 18/22] feat(fields): no dependencies --- .../templates/helpdesk/ticket_desc_table.html | 7 +++++++ helpdesk/urls.py | 19 +++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/helpdesk/templates/helpdesk/ticket_desc_table.html b/helpdesk/templates/helpdesk/ticket_desc_table.html index 07fadee4..4a09a58b 100644 --- a/helpdesk/templates/helpdesk/ticket_desc_table.html +++ b/helpdesk/templates/helpdesk/ticket_desc_table.html @@ -70,7 +70,9 @@ {{ ticketcc_string }} {% if SHOW_SUBSCRIBE %} {% endif %} + {% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET != False and helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET != False %} + {% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET %} {% trans "Dependencies" %} @@ -82,6 +84,10 @@ {% trans "This ticket has no dependencies." %} {% endfor %} + {% else %} + + + {% endif %} {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %} {% trans "Total time spent" %} {{ ticket.time_spent_formated }} @@ -90,6 +96,7 @@ {% endif %} + {% endif %} {% if ticket.kbitem %} {% trans "Knowlegebase item" %} diff --git a/helpdesk/urls.py b/helpdesk/urls.py index fc185cc9..711ffa00 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -108,14 +108,6 @@ urlpatterns = [ staff.ticket_cc_del, name='ticket_cc_del'), - url(r'^tickets/(?P[0-9]+)/dependency/add/$', - staff.ticket_dependency_add, - name='ticket_dependency_add'), - - url(r'^tickets/(?P[0-9]+)/dependency/delete/(?P[0-9]+)/$', - staff.ticket_dependency_del, - name='ticket_dependency_del'), - url(r'^tickets/(?P[0-9]+)/attachment_delete/(?P[0-9]+)/$', staff.attachment_del, name='attachment_del'), @@ -170,6 +162,17 @@ urlpatterns = [ ] +if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET: + urlpatterns += [ + url(r'^tickets/(?P[0-9]+)/dependency/add/$', + staff.ticket_dependency_add, + name='ticket_dependency_add'), + + url(r'^tickets/(?P[0-9]+)/dependency/delete/(?P[0-9]+)/$', + staff.ticket_dependency_del, + name='ticket_dependency_del'), + ] + urlpatterns += [ url(r'^$', protect_view(public.Homepage.as_view()), From 6d5c81ea1492b4a559fd2d673e97e07bcad9a468 Mon Sep 17 00:00:00 2001 From: Daniele Scasciafratte Date: Wed, 20 Apr 2022 16:54:17 +0200 Subject: [PATCH 19/22] feat(fields): hide column for KB item, https://github.com/django-helpdesk/django-helpdesk/pull/1003 --- helpdesk/templates/helpdesk/report_index.html | 4 ++-- helpdesk/templates/helpdesk/ticket_list.html | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/helpdesk/templates/helpdesk/report_index.html b/helpdesk/templates/helpdesk/report_index.html index 055243a7..63194afb 100644 --- a/helpdesk/templates/helpdesk/report_index.html +++ b/helpdesk/templates/helpdesk/report_index.html @@ -45,7 +45,7 @@ {% trans "Open" %} {% trans "Resolved" %} {% trans "Closed" %} - {% trans "Time spent" %} + {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}{% trans "Time spent" %}{% endif %} @@ -55,7 +55,7 @@ {% if queue.open %}{% endif %}{{ queue.open }}{% if queue.open %}{% endif %} {% if queue.resolved %}{% endif %}{{ queue.resolved }}{% if queue.resolved %}{% endif %} {% if queue.closed %}{% endif %}{{ queue.closed }}{% if queue.closed %}{% endif %} - {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}{{ queue.time_spent }}{% if queue.dedicated_time %} / {{ queue.dedicated_time }}{% endif %}{% endif %} + {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}{{ queue.time_spent }}{% if queue.dedicated_time %} / {{ queue.dedicated_time }}{% endif %}{% endif %} {% empty %} {% trans "There are no unassigned tickets." %} diff --git a/helpdesk/templates/helpdesk/ticket_list.html b/helpdesk/templates/helpdesk/ticket_list.html index 2894349b..d7a7f490 100644 --- a/helpdesk/templates/helpdesk/ticket_list.html +++ b/helpdesk/templates/helpdesk/ticket_list.html @@ -75,7 +75,7 @@ {% trans "Owner" %} {% trans "Submitter" %} {% trans "Time Spent" %} - {% trans "KB item" %} + {% if helpdesk_settings.HELPDESK_KB_ENABLED %}{% trans "KB item" %}{% endif %} @@ -409,7 +409,9 @@ }, {data: "submitter"}, {data: "time_spent", "visible": false}, + {% if helpdesk_settings.HELPDESK_KB_ENABLED %} {data: "kbitem"}, + {% endif %} ] }); From 8d626cfd5f7425edc8e295ffa720b8d47b51ca06 Mon Sep 17 00:00:00 2001 From: Daniele Scasciafratte Date: Wed, 20 Apr 2022 17:30:55 +0200 Subject: [PATCH 20/22] feat(fields): hide column for time spent --- helpdesk/templates/helpdesk/ticket_list.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helpdesk/templates/helpdesk/ticket_list.html b/helpdesk/templates/helpdesk/ticket_list.html index d7a7f490..43c6ba93 100644 --- a/helpdesk/templates/helpdesk/ticket_list.html +++ b/helpdesk/templates/helpdesk/ticket_list.html @@ -74,7 +74,7 @@ {% trans "Due Date" %} {% trans "Owner" %} {% trans "Submitter" %} - {% trans "Time Spent" %} + {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}{% trans "Time Spent" %}{% endif %} {% if helpdesk_settings.HELPDESK_KB_ENABLED %}{% trans "KB item" %}{% endif %} @@ -408,7 +408,9 @@ } }, {data: "submitter"}, + {% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %} {data: "time_spent", "visible": false}, + {% endif %} {% if helpdesk_settings.HELPDESK_KB_ENABLED %} {data: "kbitem"}, {% endif %} From 5e8f5fed623f3719c73be3ef94495320b9036124 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Fri, 22 Apr 2022 14:52:51 -0400 Subject: [PATCH 21/22] Fixing some unit tests --- helpdesk/tests/test_api.py | 6 +++--- helpdesk/tests/test_get_email.py | 6 +++--- helpdesk/tests/test_ticket_submission.py | 8 ++++++-- helpdesk/validators.py | 6 ++++-- quicktest.py | 5 ++++- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/helpdesk/tests/test_api.py b/helpdesk/tests/test_api.py index 4b679479..8ca8a43c 100644 --- a/helpdesk/tests/test_api.py +++ b/helpdesk/tests/test_api.py @@ -5,7 +5,7 @@ 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.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN from rest_framework.test import APITestCase from helpdesk.models import Queue, Ticket, CustomField @@ -21,13 +21,13 @@ class TicketTest(APITestCase): def test_create_api_ticket_not_authenticated_user(self): response = self.client.post('/api/tickets/') - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 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) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) def test_create_api_ticket_no_data(self): staff_user = User.objects.create_user(username='test', is_staff=True) diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index c113b04f..07df3348 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -52,7 +52,7 @@ class GetEmailCommonTests(TestCase): """ with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml")) as fd: test_email = fd.read() - ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) + ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) # title got truncated because of max_lengh of the model.title field assert ticket.title == ( @@ -68,7 +68,7 @@ class GetEmailCommonTests(TestCase): """ with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml")) as fd: test_email = fd.read() - ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) + ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) self.assertEqual(ticket.title, "Český test") self.assertEqual(ticket.description, "Tohle je test českých písmen odeslaných z gmailu.") followups = FollowUp.objects.filter(ticket=ticket) @@ -86,7 +86,7 @@ class GetEmailCommonTests(TestCase): """ with open(os.path.join(THIS_DIR, "test_files/all-special-chars.eml")) as fd: test_email = fd.read() - ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) + ticket = helpdesk.email.object_from_message(test_email, self.queue_public, self.logger) self.assertEqual(ticket.title, "Testovácí email") self.assertEqual(ticket.description, "íářčšáíéřášč") diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index d9a6b22d..ce4813b2 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -603,13 +603,16 @@ class EmailInteractionsTestCase(TestCase): # As we have created a Ticket from an email, we notify the sender # and contacts on the cc_list (+1 as it's treated as a list), # the new and update queues (+2) + # then each cc gets its own email? (+2) + # TODO: check this is correct! # Ensure that the submitter is notified self.assertIn(submitter_email, mail.outbox[0].to) # As we have created an Ticket from an email, we notify the sender (+1) # and the new and update queues (+2) - expected_email_count = 1 + 2 + # then each cc gets its own email? (+2) + expected_email_count = 1 + 2 + 2 self.assertEqual(expected_email_count, len(mail.outbox)) # end of the Ticket and TicketCCs creation # @@ -637,7 +640,8 @@ class EmailInteractionsTestCase(TestCase): # As an update was made, we increase the expected_email_count with: # public_update_queue: +1 - expected_email_count += 1 + # since the submitter and the two ccs each get an email + expected_email_count += 1 + 3 self.assertEqual(expected_email_count, len(mail.outbox)) # As we have created a FollowUp from an email, we notify the sender diff --git a/helpdesk/validators.py b/helpdesk/validators.py index 1c0ccb24..f7e5b5f5 100644 --- a/helpdesk/validators.py +++ b/helpdesk/validators.py @@ -19,7 +19,9 @@ def validate_file_extension(value): if hasattr(settings, 'VALID_EXTENSIONS'): valid_extensions = settings.VALID_EXTENSIONS else: - valid_extensions = ['.txt', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] + valid_extensions = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] if not ext.lower() in valid_extensions: - raise ValidationError('Unsupported file extension.') + # TODO: one more check in case it is a file with no extension; we should always allow that? + if not (ext.lower() == '' or ext.lower() == '.'): + raise ValidationError('Unsupported file extension: %s.' % ext.lower()) diff --git a/quicktest.py b/quicktest.py index 0c488f9b..3837abd8 100644 --- a/quicktest.py +++ b/quicktest.py @@ -40,6 +40,7 @@ class QuickDjangoTest(object): #'account', #'pinax.invitations', #'pinax.teams', + 'rest_framework', 'helpdesk', #'reversion', ) @@ -104,7 +105,9 @@ class QuickDjangoTest(object): ## The following settings disable teams HELPDESK_TEAMS_MODEL = 'auth.User', HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [], - HELPDESK_KBITEM_TEAM_GETTER = lambda _: None + HELPDESK_KBITEM_TEAM_GETTER = lambda _: None, + ## test the API + HELPDESK_ACTIVATE_API_ENDPOINT=True ) from django.test.runner import DiscoverRunner From 3ed6c680467f0a769c5c18ed6ec14d2326f3494c Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Sat, 23 Apr 2022 02:06:35 -0400 Subject: [PATCH 22/22] Update to 0.3.4 bugfix release --- demo/setup.py | 2 +- helpdesk/tests/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/setup.py b/demo/setup.py index a10534e6..8a3604f9 100644 --- a/demo/setup.py +++ b/demo/setup.py @@ -13,7 +13,7 @@ project_root = os.path.dirname(here) NAME = 'django-helpdesk-demodesk' DESCRIPTION = 'A demo Django project using django-helpdesk' README = open(os.path.join(here, 'README.rst')).read() -VERSION = '0.3.3' +VERSION = '0.3.4' #VERSION = open(os.path.join(project_root, 'VERSION')).read().strip() AUTHOR = 'django-helpdesk team' URL = 'https://github.com/django-helpdesk/django-helpdesk' diff --git a/helpdesk/tests/__init__.py b/helpdesk/tests/__init__.py index 807df353..221459dd 100644 --- a/helpdesk/tests/__init__.py +++ b/helpdesk/tests/__init__.py @@ -1,5 +1,5 @@ # import all test_*.py files in directory. -# neccessary for automatic discovery in django <= 1.5 +# necessary for automatic discovery in django <= 1.5 # http://stackoverflow.com/a/15780326/1382740 import unittest diff --git a/setup.py b/setup.py index e160838f..27890c57 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.util import convert_path from fnmatch import fnmatchcase from setuptools import setup, find_packages -version = '0.3.3' +version = '0.3.4' # Provided as an attribute, so you can append to these instead # of replicating them: