mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-12-25 16:18:51 +01:00
Merge in bugfixes and features from 0.3.4 release including new api
This commit is contained in:
commit
d1187b02fa
@ -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 = [
|
||||||
@ -113,9 +114,14 @@ HELPDESK_SUBMIT_A_TICKET_PUBLIC = True
|
|||||||
# Should the Knowledgebase be enabled?
|
# Should the Knowledgebase be enabled?
|
||||||
HELPDESK_KB_ENABLED = True
|
HELPDESK_KB_ENABLED = True
|
||||||
|
|
||||||
|
HELPDESK_TICKETS_TIMELINE_ENABLED = True
|
||||||
|
|
||||||
# Allow users to change their passwords
|
# Allow users to change their passwords
|
||||||
HELPDESK_SHOW_CHANGE_PASSWORD = True
|
HELPDESK_SHOW_CHANGE_PASSWORD = True
|
||||||
|
|
||||||
|
# Activate the API
|
||||||
|
HELPDESK_ACTIVATE_API_ENDPOINT = True
|
||||||
|
|
||||||
# Instead of showing the public web portal first,
|
# Instead of showing the public web portal first,
|
||||||
# we can instead redirect users straight to the login page.
|
# we can instead redirect users straight to the login page.
|
||||||
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False
|
HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False
|
||||||
|
@ -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)
|
||||||
|
66
docs/api.rst
Normal file
66
docs/api.rst
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
API
|
||||||
|
===
|
||||||
|
|
||||||
|
A REST API (built with ``djangorestframework``) is available in order to list, create, update and delete tickets from other tools thanks to HTTP requests.
|
||||||
|
|
||||||
|
If you wish to use it, you have to add this line in your settings::
|
||||||
|
|
||||||
|
HELPDESK_ACTIVATE_API_ENDPOINT = True
|
||||||
|
|
||||||
|
You must be authenticated to access the API, the URL endpoint is ``/api/tickets/``. You can configure how you wish to authenticate to the API by customizing the ``DEFAULT_AUTHENTICATION_CLASSES`` key in the ``REST_FRAMEWORK`` setting (more information on this page : https://www.django-rest-framework.org/api-guide/authentication/)
|
||||||
|
|
||||||
|
GET
|
||||||
|
---
|
||||||
|
|
||||||
|
Accessing the endpoint ``/api/tickets/`` with a **GET** request will return you the complete list of tickets.
|
||||||
|
|
||||||
|
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **GET** request will return you the data of the ticket you provided the ID.
|
||||||
|
|
||||||
|
POST
|
||||||
|
----
|
||||||
|
|
||||||
|
Accessing the endpoint ``/api/tickets/`` with a **POST** request will let you create a new tickets.
|
||||||
|
|
||||||
|
You need to provide a JSON body with the following data :
|
||||||
|
|
||||||
|
- **queue**: ID of the queue
|
||||||
|
- **title**: the title (subject) of the ticket
|
||||||
|
- **description**: the description of the ticket
|
||||||
|
- **resolution**: an optional text for the resoltuion of the ticket
|
||||||
|
- **submitter_email**: the email of the ticket submitter
|
||||||
|
- **assigned_to**: ID of the ticket's assigned user
|
||||||
|
- **status**: integer corresponding to the status (OPEN=1, REOPENED=2, RESOLVED=3, CLOSED=4, DUPLICATE=5). It is OPEN by default.
|
||||||
|
- **on_hold**: boolean to indicates if the ticket is on hold
|
||||||
|
- **priority**: integer corresponding to different degrees of priority 1 to 5 (1 is Critical and 5 is Very Low)
|
||||||
|
- **due_date**: date representation for when the ticket is due
|
||||||
|
- **merged_to**: ID of the ticket to which it is merged
|
||||||
|
|
||||||
|
Note that ``status`` will automatically be set to OPEN. Also, some fields are not configurable during creation: ``resolution``, ``on_hold`` and ``merged_to``.
|
||||||
|
|
||||||
|
Moreover, if you created custom fields, you can add them into the body with the key ``custom_<custom-field-slug>``.
|
||||||
|
|
||||||
|
Here is an example of a cURL request to create a ticket (using Basic authentication) ::
|
||||||
|
|
||||||
|
curl --location --request POST 'http://127.0.0.1:8000/api/tickets/' \
|
||||||
|
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{"queue": 1, "title": "Test Ticket API", "description": "Test create ticket from API", "submitter_email": "test@mail.com", "priority": 4}'
|
||||||
|
|
||||||
|
PUT
|
||||||
|
---
|
||||||
|
|
||||||
|
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PUT** request will let you update the data of the ticket you provided the ID.
|
||||||
|
|
||||||
|
You must include all fields in the JSON body.
|
||||||
|
|
||||||
|
PATCH
|
||||||
|
-----
|
||||||
|
|
||||||
|
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **PATCH** request will let you do a partial update of the data of the ticket you provided the ID.
|
||||||
|
|
||||||
|
You can include only the fields you need to update in the JSON body.
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
------
|
||||||
|
|
||||||
|
Accessing the endpoint ``/api/tickets/<ticket-id>`` with a **DELETE** request will let you delete the ticket you provided the ID.
|
@ -16,9 +16,9 @@ Contents
|
|||||||
settings
|
settings
|
||||||
spam
|
spam
|
||||||
custom_fields
|
custom_fields
|
||||||
|
api
|
||||||
integration
|
integration
|
||||||
teams
|
teams
|
||||||
contributing
|
|
||||||
license
|
license
|
||||||
|
|
||||||
|
|
||||||
@ -40,6 +40,7 @@ django-helpdesk has been designed for small businesses who need to receive, mana
|
|||||||
* Tickets can be opened via email
|
* Tickets can be opened via email
|
||||||
* Multiple queues / categories of tickets
|
* Multiple queues / categories of tickets
|
||||||
* Integrated FAQ / knowledgebase
|
* Integrated FAQ / knowledgebase
|
||||||
|
* API to manage tickets
|
||||||
|
|
||||||
Customer-facing Capabilities
|
Customer-facing Capabilities
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@ -62,6 +63,8 @@ If a user is a staff member, they get general helpdesk access, including:
|
|||||||
5. Follow up or respond to tickets
|
5. Follow up or respond to tickets
|
||||||
6. Assign tickets to themselves or other staff members
|
6. Assign tickets to themselves or other staff members
|
||||||
7. Resolve tickets
|
7. Resolve tickets
|
||||||
|
8. Merge multiple tickets into one
|
||||||
|
9. Create, Read, Update and Delete tickets through a REST API
|
||||||
|
|
||||||
Optionally, their access to view tickets, both on the dashboard and through searches and reports, may be restricted by a list of queues to which they have been granted membership. Create and update permissions for individual tickets are not limited by this optional restriction.
|
Optionally, their access to view tickets, both on the dashboard and through searches and reports, may be restricted by a list of queues to which they have been granted membership. Create and update permissions for individual tickets are not limited by this optional restriction.
|
||||||
|
|
||||||
|
@ -27,10 +27,10 @@ Installing using PIP
|
|||||||
|
|
||||||
Try using ``pip install django-helpdesk``. Go and have a beer to celebrate Python packaging.
|
Try using ``pip install django-helpdesk``. Go and have a beer to celebrate Python packaging.
|
||||||
|
|
||||||
Checkout ``master`` from git (Cutting Edge)
|
Checkout ``stable`` from git (Cutting Edge)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
If you're planning on editing the code or just want to get whatever is the latest and greatest, you can clone the official Git repository with ``git clone git://github.com/django-helpdesk/django-helpdesk.git``. We use the ``master`` branch as our development branch for the next major release of ``django-helpdesk``.
|
If you're planning on editing the code or just want to get whatever is the latest and greatest, you can clone the official Git repository with ``git clone git://github.com/django-helpdesk/django-helpdesk.git``. We use the ``stable`` branch as our development branch for the next major release of ``django-helpdesk``.
|
||||||
|
|
||||||
Copy the ``helpdesk`` folder into your ``PYTHONPATH``.
|
Copy the ``helpdesk`` folder into your ``PYTHONPATH``.
|
||||||
|
|
||||||
@ -59,13 +59,14 @@ errors with trying to create User settings.
|
|||||||
'django.contrib.humanize', # Required for elapsed time formatting
|
'django.contrib.humanize', # Required for elapsed time formatting
|
||||||
'bootstrap4form', # Required for nicer formatting of forms with the default templates
|
'bootstrap4form', # Required for nicer formatting of forms with the default templates
|
||||||
'account', # Required by pinax-teams
|
'account', # Required by pinax-teams
|
||||||
'pinax.invitations', # required by pinax-teams
|
'pinax.invitations', # Required by pinax-teams
|
||||||
'pinax.teams', # team support
|
'pinax.teams', # Team support
|
||||||
'reversion', # required by pinax-teams
|
'reversion', # Required by pinax-teams
|
||||||
|
'rest_framework', # required for the API
|
||||||
'helpdesk', # This is us!
|
'helpdesk', # This is us!
|
||||||
)
|
)
|
||||||
|
|
||||||
Note: you do not need to use pinax-teams. To dissable teams see the :doc:`teams` section.
|
Note: you do not need to use pinax-teams. To disable teams see the :doc:`teams` section.
|
||||||
|
|
||||||
Your ``settings.py`` file should also define a ``SITE_ID`` that allows multiple projects to share
|
Your ``settings.py`` file should also define a ``SITE_ID`` that allows multiple projects to share
|
||||||
a single database, and is required by ``django.contrib.sites`` in Django 1.9+.
|
a single database, and is required by ``django.contrib.sites`` in Django 1.9+.
|
||||||
@ -75,11 +76,11 @@ errors with trying to create User settings.
|
|||||||
|
|
||||||
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following line to ``urls.py``::
|
2. Make sure django-helpdesk is accessible via ``urls.py``. Add the following line to ``urls.py``::
|
||||||
|
|
||||||
url(r'helpdesk/', include('helpdesk.urls')),
|
path('helpdesk/', include('helpdesk.urls')),
|
||||||
|
|
||||||
Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the line will be as follows::
|
Note that you can change 'helpdesk/' to anything you like, such as 'support/' or 'help/'. If you want django-helpdesk to be available at the root of your site (for example at http://support.mysite.tld/) then the line will be as follows::
|
||||||
|
|
||||||
url(r'', include('helpdesk.urls', namespace='helpdesk')),
|
path('', include('helpdesk.urls', namespace='helpdesk')),
|
||||||
|
|
||||||
This line will have to come *after* any other lines in your urls.py such as those used by the Django admin.
|
This line will have to come *after* any other lines in your urls.py such as those used by the Django admin.
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ errors with trying to create User settings.
|
|||||||
|
|
||||||
Migrate using Django migrations::
|
Migrate using Django migrations::
|
||||||
|
|
||||||
./manage.py migrate helpdesk
|
python manage.py migrate helpdesk
|
||||||
|
|
||||||
4. Include your static files in your public web path::
|
4. Include your static files in your public web path::
|
||||||
|
|
||||||
|
@ -94,6 +94,18 @@ These changes are visible throughout django-helpdesk
|
|||||||
|
|
||||||
**Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False``
|
**Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False``
|
||||||
|
|
||||||
|
- **HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET** If False, disable the dependencies fields on ticket.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = True``
|
||||||
|
|
||||||
|
- **HELPDESK_ENABLE_TIME_SPENT_ON_TICKET** If False, disable the time spent fields on ticket.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_ENABLE_TIME_SPENT_ON_TICKET = True``
|
||||||
|
|
||||||
|
- **HELPDESK_TICKETS_TIMELINE_ENABLED** If False, remove from the dashboard the Timeline view for tickets.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_TICKETS_TIMELINE_ENABLED = True``
|
||||||
|
|
||||||
Options shown on public pages
|
Options shown on public pages
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
@ -168,6 +180,10 @@ Staff Ticket Creation Settings
|
|||||||
|
|
||||||
**Default:** ``HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = False``
|
**Default:** ``HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = False``
|
||||||
|
|
||||||
|
- **HELPDESK_ACTIVATE_API_ENDPOINT** Activate the API endpoint to manage tickets thanks to Django REST Framework. See the API section in documentation for more information.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_ACTIVATE_API_ENDPOINT = False``
|
||||||
|
|
||||||
|
|
||||||
Staff Ticket View Settings
|
Staff Ticket View Settings
|
||||||
------------------------------
|
------------------------------
|
||||||
|
@ -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.create(
|
||||||
followup=followup,
|
|
||||||
file=attached,
|
file=attached,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
mime_type=attached.content_type or
|
mime_type=attached.content_type or
|
||||||
@ -169,3 +168,15 @@ def format_time_spent(time_spent):
|
|||||||
else:
|
else:
|
||||||
time_spent = ""
|
time_spent = ""
|
||||||
return time_spent
|
return time_spent
|
||||||
|
|
||||||
|
|
||||||
|
def convert_value(value):
|
||||||
|
""" Convert date/time data type to known fixed format string """
|
||||||
|
if type(value) == datetime:
|
||||||
|
return value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
|
||||||
|
elif type(value) == date:
|
||||||
|
return value.strftime(CUSTOMFIELD_DATE_FORMAT)
|
||||||
|
elif type(value) == time:
|
||||||
|
return value.strftime(CUSTOMFIELD_TIME_FORMAT)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
@ -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', 'merged_to'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Add custom fields
|
||||||
|
for field in CustomField.objects.all():
|
||||||
|
self.fields['custom_%s' % field.name] = field.build_api_field()
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
""" Use TicketForm to validate and create ticket """
|
||||||
|
queues = HelpdeskUser(self.context['request'].user).get_queues()
|
||||||
|
queue_choices = [(q.id, q.title) for q in queues]
|
||||||
|
data = validated_data.copy()
|
||||||
|
data['body'] = data['description']
|
||||||
|
# TicketForm needs id for ForeignKey (not the instance themselves)
|
||||||
|
data['queue'] = data['queue'].id
|
||||||
|
if data.get('assigned_to'):
|
||||||
|
data['assigned_to'] = data['assigned_to'].id
|
||||||
|
if data.get('merged_to'):
|
||||||
|
data['merged_to'] = data['merged_to'].id
|
||||||
|
|
||||||
|
ticket_form = TicketForm(data=data, queue_choices=queue_choices)
|
||||||
|
if ticket_form.is_valid():
|
||||||
|
ticket = ticket_form.save(user=self.context['request'].user)
|
||||||
|
ticket.set_custom_field_values()
|
||||||
|
return ticket
|
||||||
|
|
||||||
|
raise ValidationError(ticket_form.errors)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
instance = super().update(instance, validated_data)
|
||||||
|
instance.save_custom_field_values(validated_data)
|
||||||
|
return instance
|
||||||
|
@ -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,
|
||||||
@ -39,6 +41,16 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
|
|||||||
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
|
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
|
||||||
False)
|
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
|
# raises a 404 to anon users. It's like it was invisible
|
||||||
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
|
HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
|
||||||
'HELPDESK_ANON_ACCESS_RAISES_404',
|
'HELPDESK_ANON_ACCESS_RAISES_404',
|
||||||
@ -47,6 +59,9 @@ HELPDESK_ANON_ACCESS_RAISES_404 = getattr(settings,
|
|||||||
# show knowledgebase links?
|
# show knowledgebase links?
|
||||||
HELPDESK_KB_ENABLED = getattr(settings, 'HELPDESK_KB_ENABLED', True)
|
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?
|
# show extended navigation by default, to all users, irrespective of staff status?
|
||||||
HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False)
|
HELPDESK_NAVIGATION_ENABLED = getattr(settings, 'HELPDESK_NAVIGATION_ENABLED', False)
|
||||||
|
|
||||||
@ -97,6 +112,21 @@ HELPDESK_PUBLIC_TICKET_FORM_CLASS = getattr(
|
|||||||
"helpdesk.forms.PublicTicketForm"
|
"helpdesk.forms.PublicTicketForm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Custom fields constants
|
||||||
|
CUSTOMFIELD_TO_FIELD_DICT = {
|
||||||
|
'boolean': forms.BooleanField,
|
||||||
|
'date': forms.DateField,
|
||||||
|
'time': forms.TimeField,
|
||||||
|
'datetime': forms.DateTimeField,
|
||||||
|
'email': forms.EmailField,
|
||||||
|
'url': forms.URLField,
|
||||||
|
'ipaddress': forms.GenericIPAddressField,
|
||||||
|
'slug': forms.SlugField,
|
||||||
|
}
|
||||||
|
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
|
||||||
|
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
|
||||||
|
CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT}T%H:%M"
|
||||||
|
|
||||||
|
|
||||||
###################################
|
###################################
|
||||||
# options for update_ticket views #
|
# options for update_ticket views #
|
||||||
@ -155,6 +185,9 @@ HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = getattr(settings, 'HELPDESK_MAX_EMAIL_ATTAC
|
|||||||
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = getattr(
|
||||||
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
|
settings, 'HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO', False)
|
||||||
|
|
||||||
|
# Activate the API endpoint to manage tickets thanks to Django REST Framework
|
||||||
|
HELPDESK_ACTIVATE_API_ENDPOINT = getattr(settings, 'HELPDESK_ACTIVATE_API_ENDPOINT', False)
|
||||||
|
|
||||||
|
|
||||||
#################
|
#################
|
||||||
# email options #
|
# email options #
|
||||||
|
@ -46,8 +46,10 @@
|
|||||||
<dt><label for="id_new_status">New Status:</label></dt>
|
<dt><label for="id_new_status">New Status:</label></dt>
|
||||||
<dd>{{ form.new_status }}</dd>
|
<dd>{{ form.new_status }}</dd>
|
||||||
<p>If the status was changed, what was it changed to?</p>
|
<p>If the status was changed, what was it changed to?</p>
|
||||||
|
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
|
||||||
<dt><label for="id_time_spent">Time spent:</label></dt>
|
<dt><label for="id_time_spent">Time spent:</label></dt>
|
||||||
<dd>{{ form.time_spent }}</dd>
|
<dd>{{ form.time_spent }}</dd>
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<p><input class="btn btn-primary btn-sm" type="submit" value="Submit"></p>{% csrf_token %}
|
<p><input class="btn btn-primary btn-sm" type="submit" value="Submit"></p>{% csrf_token %}
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
<th>{% trans "Open" %}</th>
|
<th>{% trans "Open" %}</th>
|
||||||
<th>{% trans "Resolved" %}</th>
|
<th>{% trans "Resolved" %}</th>
|
||||||
<th>{% trans "Closed" %}</th>
|
<th>{% trans "Closed" %}</th>
|
||||||
<th>{% trans "Time spent" %}</th>
|
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}<th>{% trans "Time spent" %}</th>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -55,7 +55,7 @@
|
|||||||
<td>{% if queue.open %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=1&status=2'>{% endif %}{{ queue.open }}{% if queue.open %}</a>{% endif %}</td>
|
<td>{% if queue.open %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=1&status=2'>{% endif %}{{ queue.open }}{% if queue.open %}</a>{% endif %}</td>
|
||||||
<td>{% if queue.resolved %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=3'>{% endif %}{{ queue.resolved }}{% if queue.resolved %}</a>{% endif %}</td>
|
<td>{% if queue.resolved %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=3'>{% endif %}{{ queue.resolved }}{% if queue.resolved %}</a>{% endif %}</td>
|
||||||
<td>{% if queue.closed %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=4'>{% endif %}{{ queue.closed }}{% if queue.closed %}</a>{% endif %}</td>
|
<td>{% if queue.closed %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=4'>{% endif %}{{ queue.closed }}{% if queue.closed %}</a>{% endif %}</td>
|
||||||
<td>{{ queue.time_spent }}{% if queue.dedicated_time %} / {{ queue.dedicated_time }}{% endif %}</td>
|
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}<td>{{ queue.time_spent }}{% if queue.dedicated_time %} / {{ queue.dedicated_time }}{% endif %}</td>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr>
|
<tr><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr>
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
<div class="list-group-item list-group-item-action">
|
<div class="list-group-item list-group-item-action">
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h5 class="mb-1">{{ followup.title|num_to_link }}</h5>
|
<h5 class="mb-1">{{ followup.title|num_to_link }}</h5>
|
||||||
<small><i class="fas fa-clock"></i> <span class='byline text-info'>{% if followup.user %}by {{ followup.user }},{% endif %} <span title='{{ followup.date|date:"DATETIME_FORMAT" }}'>{{ followup.date|naturaltime }}</span>{% if followup.time_spent %}, <span>{% trans "time spent" %}: {{ followup.time_spent_formated }}</span>{% endif %} {% if not followup.public %} <span class='private'>({% trans "Private" %})</span>{% endif %}</span></small>
|
<small><i class="fas fa-clock"></i> <span class='byline text-info'>{% if followup.user %}by {{ followup.user }},{% endif %} <span title='{{ followup.date|date:"DATETIME_FORMAT" }}'>{{ followup.date|naturaltime }}</span>{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}{% if followup.time_spent %}{% endif %}, <span>{% trans "time spent" %}: {{ followup.time_spent_formated }}</span>{% endif %} {% if not followup.public %} <span class='private'>({% trans "Private" %})</span>{% endif %}</span></small>
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
{% if followup.comment %}
|
{% if followup.comment %}
|
||||||
@ -156,12 +156,14 @@
|
|||||||
<dd class='form_help_text'>{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}</dd>
|
<dd class='form_help_text'>{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<dt>
|
<dt>
|
||||||
<label for='id_time_spent'>{% trans "Time spent" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span>
|
<label for='id_time_spent'>{% trans "Time spent" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd><input name='time_spent' type="time" /></dd>
|
<dd><input name='time_spent' type="time" /></dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details »" %}</button></p>
|
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details »" %}</button></p>
|
||||||
|
@ -70,7 +70,9 @@
|
|||||||
<td>{{ ticketcc_string }} <a data-toggle='tooltip' href='{% url 'helpdesk:ticket_cc' ticket.id %}' title='{% trans "Click here to add / remove people who should receive an e-mail whenever this ticket is updated." %}'><strong><button type="button" class="btn btn-warning btn-sm float-right"><i class="fa fa-share"></i></button></strong></a>{% if SHOW_SUBSCRIBE %} <strong><a data-toggle='tooltip' href='?subscribe' title='{% trans "Click here to subscribe yourself to this ticket, if you want to receive an e-mail whenever this ticket is updated." %}'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-rss-square"></i></button></a></strong>{% endif %}</td>
|
<td>{{ ticketcc_string }} <a data-toggle='tooltip' href='{% url 'helpdesk:ticket_cc' ticket.id %}' title='{% trans "Click here to add / remove people who should receive an e-mail whenever this ticket is updated." %}'><strong><button type="button" class="btn btn-warning btn-sm float-right"><i class="fa fa-share"></i></button></strong></a>{% if SHOW_SUBSCRIBE %} <strong><a data-toggle='tooltip' href='?subscribe' title='{% trans "Click here to subscribe yourself to this ticket, if you want to receive an e-mail whenever this ticket is updated." %}'><button type="button" class="btn btn-warning btn-sm float-right"><i class="fas fa-rss-square"></i></button></a></strong>{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET != False and helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET != False %}
|
||||||
<tr>
|
<tr>
|
||||||
|
{% if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET %}
|
||||||
<th class="table-active">{% trans "Dependencies" %}</th>
|
<th class="table-active">{% trans "Dependencies" %}</th>
|
||||||
<td>
|
<td>
|
||||||
<a data-toggle='tooltip' href='{% url 'helpdesk:ticket_dependency_add' ticket.id %}' title="{% trans "Click on 'Add Dependency', if you want to make this ticket dependent on another ticket. A ticket may not be closed until all tickets it depends on are closed." %}"><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-link"></i></button></a>
|
<a data-toggle='tooltip' href='{% url 'helpdesk:ticket_dependency_add' ticket.id %}' title="{% trans "Click on 'Add Dependency', if you want to make this ticket dependent on another ticket. A ticket may not be closed until all tickets it depends on are closed." %}"><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-link"></i></button></a>
|
||||||
@ -82,9 +84,19 @@
|
|||||||
{% trans "This ticket has no dependencies." %}
|
{% trans "This ticket has no dependencies." %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<th class="table-active"></th>
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
|
||||||
<th class="table-active">{% trans "Total time spent" %}</th>
|
<th class="table-active">{% trans "Total time spent" %}</th>
|
||||||
<td>{{ ticket.time_spent_formated }}</td>
|
<td>{{ ticket.time_spent_formated }}</td>
|
||||||
|
{% else %}
|
||||||
|
<th class="table-active"></th>
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% if ticket.kbitem %}
|
{% if ticket.kbitem %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="table-active">{% trans "Knowlegebase item" %}</th>
|
<th class="table-active">{% trans "Knowlegebase item" %}</th>
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
{% block helpdesk_body %}
|
{% block helpdesk_body %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
{% if helpdesk_settings.HELPDESK_TICKETS_TIMELINE_ENABLED %}
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li class="nav-item" style="width: 200px;">
|
<li class="nav-item" style="width: 200px;">
|
||||||
@ -54,6 +55,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ search_message|safe }}
|
{{ search_message|safe }}
|
||||||
<div class="tab-content" id="myTabContent">
|
<div class="tab-content" id="myTabContent">
|
||||||
@ -74,8 +76,8 @@
|
|||||||
<th>{% trans "Due Date" %}</th>
|
<th>{% trans "Due Date" %}</th>
|
||||||
<th>{% trans "Owner" %}</th>
|
<th>{% trans "Owner" %}</th>
|
||||||
<th>{% trans "Submitter" %}</th>
|
<th>{% trans "Submitter" %}</th>
|
||||||
<th>{% trans "Time Spent" %}</th>
|
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}<th>{% trans "Time Spent" %}</th>{% endif %}
|
||||||
<th>{% trans "KB item" %}</th>
|
{% if helpdesk_settings.HELPDESK_KB_ENABLED %}<th>{% trans "KB item" %}</th>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
@ -125,9 +127,11 @@
|
|||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% if helpdesk_settings.HELPDESK_TICKETS_TIMELINE_ENABLED %}
|
||||||
<div class="tab-pane fade" id="timelinetabcontents" role="tabpanel" aria-labelledby="timelinetabcontents-tab">
|
<div class="tab-pane fade" id="timelinetabcontents" role="tabpanel" aria-labelledby="timelinetabcontents-tab">
|
||||||
<div id='timeline-embed' style="width: 100%; height: 80vh"></div>
|
<div id='timeline-embed' style="width: 100%; height: 80vh"></div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /.panel-body -->
|
<!-- /.panel-body -->
|
||||||
@ -408,8 +412,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{data: "submitter"},
|
{data: "submitter"},
|
||||||
|
{% if helpdesk_settings.HELPDESK_ENABLE_TIME_SPENT_ON_TICKET %}
|
||||||
{data: "time_spent", "visible": false},
|
{data: "time_spent", "visible": false},
|
||||||
|
{% endif %}
|
||||||
|
{% if helpdesk_settings.HELPDESK_KB_ENABLED %}
|
||||||
{data: "kbitem"},
|
{data: "kbitem"},
|
||||||
|
{% endif %}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# import all test_*.py files in directory.
|
# 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
|
# http://stackoverflow.com/a/15780326/1382740
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
262
helpdesk/tests/test_api.py
Normal file
262
helpdesk/tests/test_api.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import base64
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from pytz import UTC
|
||||||
|
from rest_framework import HTTP_HEADER_ENCODING
|
||||||
|
from rest_framework.exceptions import ErrorDetail
|
||||||
|
from rest_framework.status import HTTP_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
|
||||||
|
|
||||||
|
|
||||||
|
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, 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, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_create_api_ticket_no_data(self):
|
||||||
|
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||||
|
self.client.force_authenticate(staff_user)
|
||||||
|
response = self.client.post('/api/tickets/')
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(response.data, {
|
||||||
|
'queue': [ErrorDetail(string='This field is required.', code='required')],
|
||||||
|
'title': [ErrorDetail(string='This field is required.', code='required')]
|
||||||
|
})
|
||||||
|
self.assertFalse(Ticket.objects.exists())
|
||||||
|
|
||||||
|
def test_create_api_ticket_wrong_date_format(self):
|
||||||
|
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||||
|
self.client.force_authenticate(staff_user)
|
||||||
|
response = self.client.post('/api/tickets/', {
|
||||||
|
'queue': self.queue.id,
|
||||||
|
'title': 'Test title',
|
||||||
|
'due_date': 'monday, 1st of may 2022'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(response.data, {
|
||||||
|
'due_date': [ErrorDetail(string='Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].', code='invalid')]
|
||||||
|
})
|
||||||
|
self.assertFalse(Ticket.objects.exists())
|
||||||
|
|
||||||
|
def test_create_api_ticket_authenticated_staff_user(self):
|
||||||
|
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||||
|
self.client.force_authenticate(staff_user)
|
||||||
|
response = self.client.post('/api/tickets/', {
|
||||||
|
'queue': self.queue.id,
|
||||||
|
'title': 'Test title',
|
||||||
|
'description': 'Test description\nMulti lines',
|
||||||
|
'submitter_email': 'test@mail.com',
|
||||||
|
'priority': 4
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
|
created_ticket = Ticket.objects.get()
|
||||||
|
self.assertEqual(created_ticket.title, 'Test title')
|
||||||
|
self.assertEqual(created_ticket.description, 'Test description\nMulti lines')
|
||||||
|
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
||||||
|
self.assertEqual(created_ticket.priority, 4)
|
||||||
|
|
||||||
|
def test_create_api_ticket_with_basic_auth(self):
|
||||||
|
username = 'admin'
|
||||||
|
password = 'admin'
|
||||||
|
User.objects.create_user(username=username, password=password, is_staff=True)
|
||||||
|
|
||||||
|
test_user = User.objects.create_user(username='test')
|
||||||
|
merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket')
|
||||||
|
|
||||||
|
# Generate base64 credentials string
|
||||||
|
credentials = f"{username}:{password}"
|
||||||
|
base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
|
||||||
|
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}")
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/tickets/',
|
||||||
|
{
|
||||||
|
'queue': self.queue.id,
|
||||||
|
'title': 'Title',
|
||||||
|
'description': 'Description',
|
||||||
|
'resolution': 'Resolution',
|
||||||
|
'assigned_to': test_user.id,
|
||||||
|
'submitter_email': 'test@mail.com',
|
||||||
|
'status': Ticket.RESOLVED_STATUS,
|
||||||
|
'priority': 1,
|
||||||
|
'on_hold': True,
|
||||||
|
'due_date': datetime(2022, 4, 10, 15, 6),
|
||||||
|
'merged_to': merge_ticket.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
|
created_ticket = Ticket.objects.last()
|
||||||
|
self.assertEqual(created_ticket.title, 'Title')
|
||||||
|
self.assertEqual(created_ticket.description, 'Description')
|
||||||
|
self.assertIsNone(created_ticket.resolution) # resolution can not be set on creation
|
||||||
|
self.assertEqual(created_ticket.assigned_to, test_user)
|
||||||
|
self.assertEqual(created_ticket.submitter_email, 'test@mail.com')
|
||||||
|
self.assertEqual(created_ticket.priority, 1)
|
||||||
|
self.assertFalse(created_ticket.on_hold) # on_hold is False on creation
|
||||||
|
self.assertEqual(created_ticket.status, Ticket.OPEN_STATUS) # status is always open on creation
|
||||||
|
self.assertEqual(created_ticket.due_date, datetime(2022, 4, 10, 15, 6, tzinfo=UTC))
|
||||||
|
self.assertIsNone(created_ticket.merged_to) # merged_to can not be set on creation
|
||||||
|
|
||||||
|
def test_edit_api_ticket(self):
|
||||||
|
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
||||||
|
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket')
|
||||||
|
|
||||||
|
test_user = User.objects.create_user(username='test')
|
||||||
|
merge_ticket = Ticket.objects.create(queue=self.queue, title='merge ticket')
|
||||||
|
|
||||||
|
self.client.force_authenticate(staff_user)
|
||||||
|
response = self.client.put(
|
||||||
|
'/api/tickets/%d/' % test_ticket.id,
|
||||||
|
{
|
||||||
|
'queue': self.queue.id,
|
||||||
|
'title': 'Title',
|
||||||
|
'description': 'Description',
|
||||||
|
'resolution': 'Resolution',
|
||||||
|
'assigned_to': test_user.id,
|
||||||
|
'submitter_email': 'test@mail.com',
|
||||||
|
'status': Ticket.RESOLVED_STATUS,
|
||||||
|
'priority': 1,
|
||||||
|
'on_hold': True,
|
||||||
|
'due_date': datetime(2022, 4, 10, 15, 6),
|
||||||
|
'merged_to': merge_ticket.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
test_ticket.refresh_from_db()
|
||||||
|
self.assertEqual(test_ticket.title, 'Title')
|
||||||
|
self.assertEqual(test_ticket.description, 'Description')
|
||||||
|
self.assertEqual(test_ticket.resolution, 'Resolution')
|
||||||
|
self.assertEqual(test_ticket.assigned_to, test_user)
|
||||||
|
self.assertEqual(test_ticket.submitter_email, 'test@mail.com')
|
||||||
|
self.assertEqual(test_ticket.priority, 1)
|
||||||
|
self.assertTrue(test_ticket.on_hold)
|
||||||
|
self.assertEqual(test_ticket.status, Ticket.RESOLVED_STATUS)
|
||||||
|
self.assertEqual(test_ticket.due_date, datetime(2022, 4, 10, 15, 6, tzinfo=UTC))
|
||||||
|
self.assertEqual(test_ticket.merged_to, merge_ticket)
|
||||||
|
|
||||||
|
def test_partial_edit_api_ticket(self):
|
||||||
|
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
||||||
|
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket')
|
||||||
|
|
||||||
|
self.client.force_authenticate(staff_user)
|
||||||
|
response = self.client.patch(
|
||||||
|
'/api/tickets/%d/' % test_ticket.id,
|
||||||
|
{
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
test_ticket.refresh_from_db()
|
||||||
|
self.assertEqual(test_ticket.description, 'New description')
|
||||||
|
|
||||||
|
def test_delete_api_ticket(self):
|
||||||
|
staff_user = User.objects.create_user(username='admin', is_staff=True)
|
||||||
|
test_ticket = Ticket.objects.create(queue=self.queue, title='Test ticket')
|
||||||
|
self.client.force_authenticate(staff_user)
|
||||||
|
response = self.client.delete('/api/tickets/%d/' % test_ticket.id)
|
||||||
|
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
|
||||||
|
self.assertFalse(Ticket.objects.exists())
|
||||||
|
|
||||||
|
def test_create_api_ticket_with_custom_fields(self):
|
||||||
|
# Create custom fields
|
||||||
|
for field_type, field_display in CustomField.DATA_TYPE_CHOICES:
|
||||||
|
extra_data = {}
|
||||||
|
if field_type in ('varchar', 'text'):
|
||||||
|
extra_data['max_length'] = 10
|
||||||
|
if field_type == 'integer':
|
||||||
|
# Set one field as required to test error if not provided
|
||||||
|
extra_data['required'] = True
|
||||||
|
if field_type == 'decimal':
|
||||||
|
extra_data['max_length'] = 7
|
||||||
|
extra_data['decimal_places'] = 3
|
||||||
|
if field_type == 'list':
|
||||||
|
extra_data['list_values'] = '''Green
|
||||||
|
Blue
|
||||||
|
Red
|
||||||
|
Yellow'''
|
||||||
|
CustomField.objects.create(name=field_type, label=field_display, data_type=field_type, **extra_data)
|
||||||
|
|
||||||
|
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||||
|
self.client.force_authenticate(staff_user)
|
||||||
|
|
||||||
|
# Test creation without providing required field
|
||||||
|
response = self.client.post('/api/tickets/', {
|
||||||
|
'queue': self.queue.id,
|
||||||
|
'title': 'Test title',
|
||||||
|
'description': 'Test description\nMulti lines',
|
||||||
|
'submitter_email': 'test@mail.com',
|
||||||
|
'priority': 4
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(response.data, {'custom_integer': [ErrorDetail(string='This field is required.', code='required')]})
|
||||||
|
|
||||||
|
# Test creation with custom field values
|
||||||
|
response = self.client.post('/api/tickets/', {
|
||||||
|
'queue': self.queue.id,
|
||||||
|
'title': 'Test title',
|
||||||
|
'description': 'Test description\nMulti lines',
|
||||||
|
'submitter_email': 'test@mail.com',
|
||||||
|
'priority': 4,
|
||||||
|
'custom_varchar': 'test',
|
||||||
|
'custom_text': 'multi\nline',
|
||||||
|
'custom_integer': '1',
|
||||||
|
'custom_decimal': '42.987',
|
||||||
|
'custom_list': 'Red',
|
||||||
|
'custom_boolean': True,
|
||||||
|
'custom_date': '2022-4-11',
|
||||||
|
'custom_time': '23:59:59',
|
||||||
|
'custom_datetime': '2022-4-10 18:27',
|
||||||
|
'custom_email': 'email@test.com',
|
||||||
|
'custom_url': 'http://django-helpdesk.readthedocs.org/',
|
||||||
|
'custom_ipaddress': '127.0.0.1',
|
||||||
|
'custom_slug': 'test-slug',
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
|
# Check all fields with data returned from the response
|
||||||
|
self.assertEqual(response.data, {
|
||||||
|
'id': 1,
|
||||||
|
'queue': 1,
|
||||||
|
'title': 'Test title',
|
||||||
|
'description': 'Test description\nMulti lines',
|
||||||
|
'resolution': None,
|
||||||
|
'submitter_email': 'test@mail.com',
|
||||||
|
'assigned_to': None,
|
||||||
|
'status': 1,
|
||||||
|
'on_hold': False,
|
||||||
|
'priority': 4,
|
||||||
|
'due_date': None,
|
||||||
|
'merged_to': None,
|
||||||
|
'custom_varchar': 'test',
|
||||||
|
'custom_text': 'multi\nline',
|
||||||
|
'custom_integer': 1,
|
||||||
|
'custom_decimal': '42.987',
|
||||||
|
'custom_list': 'Red',
|
||||||
|
'custom_boolean': True,
|
||||||
|
'custom_date': '2022-04-11',
|
||||||
|
'custom_time': '23:59:59',
|
||||||
|
'custom_datetime': '2022-04-10T18:27',
|
||||||
|
'custom_email': 'email@test.com',
|
||||||
|
'custom_url': 'http://django-helpdesk.readthedocs.org/',
|
||||||
|
'custom_ipaddress': '127.0.0.1',
|
||||||
|
'custom_slug': 'test-slug'
|
||||||
|
})
|
||||||
|
|
@ -147,7 +147,7 @@ class AttachmentUnitTests(TestCase):
|
|||||||
self.assertEqual(obj.size, len(self.file_attrs['content']))
|
self.assertEqual(obj.size, len(self.file_attrs['content']))
|
||||||
self.assertEqual(obj.mime_type, "text/plain")
|
self.assertEqual(obj.mime_type, "text/plain")
|
||||||
|
|
||||||
@mock.patch.object(lib.FollowUpAttachment, 'save', autospec=True)
|
@mock.patch.object('helpdesk.lib.FollowUpAttachment', 'save', autospec=True)
|
||||||
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
@override_settings(MEDIA_ROOT=MEDIA_DIR)
|
||||||
def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
def test_unicode_filename_to_filesystem(self, mock_att_save, mock_queue_save, mock_ticket_save, mock_follow_up_save):
|
||||||
""" don't mock saving to filesystem to test file renames caused by storage layer """
|
""" don't mock saving to filesystem to test file renames caused by storage layer """
|
||||||
|
@ -52,7 +52,7 @@ class GetEmailCommonTests(TestCase):
|
|||||||
"""
|
"""
|
||||||
with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml")) as fd:
|
with open(os.path.join(THIS_DIR, "test_files/blank-body-with-attachment.eml")) as fd:
|
||||||
test_email = fd.read()
|
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
|
# title got truncated because of max_lengh of the model.title field
|
||||||
assert ticket.title == (
|
assert ticket.title == (
|
||||||
@ -68,7 +68,7 @@ class GetEmailCommonTests(TestCase):
|
|||||||
"""
|
"""
|
||||||
with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml")) as fd:
|
with open(os.path.join(THIS_DIR, "test_files/quoted_printable.eml")) as fd:
|
||||||
test_email = fd.read()
|
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.title, "Český test")
|
||||||
self.assertEqual(ticket.description, "Tohle je test českých písmen odeslaných z gmailu.")
|
self.assertEqual(ticket.description, "Tohle je test českých písmen odeslaných z gmailu.")
|
||||||
followups = FollowUp.objects.filter(ticket=ticket)
|
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:
|
with open(os.path.join(THIS_DIR, "test_files/all-special-chars.eml")) as fd:
|
||||||
test_email = fd.read()
|
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.title, "Testovácí email")
|
||||||
self.assertEqual(ticket.description, "íářčšáíéřášč")
|
self.assertEqual(ticket.description, "íářčšáíéřášč")
|
||||||
|
|
||||||
|
@ -603,13 +603,16 @@ class EmailInteractionsTestCase(TestCase):
|
|||||||
# As we have created a Ticket from an email, we notify the sender
|
# 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),
|
# and contacts on the cc_list (+1 as it's treated as a list),
|
||||||
# the new and update queues (+2)
|
# 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
|
# Ensure that the submitter is notified
|
||||||
self.assertIn(submitter_email, mail.outbox[0].to)
|
self.assertIn(submitter_email, mail.outbox[0].to)
|
||||||
|
|
||||||
# As we have created an Ticket from an email, we notify the sender (+1)
|
# As we have created an Ticket from an email, we notify the sender (+1)
|
||||||
# and the new and update queues (+2)
|
# 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))
|
self.assertEqual(expected_email_count, len(mail.outbox))
|
||||||
# end of the Ticket and TicketCCs creation #
|
# 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:
|
# As an update was made, we increase the expected_email_count with:
|
||||||
# public_update_queue: +1
|
# 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))
|
self.assertEqual(expected_email_count, len(mail.outbox))
|
||||||
|
|
||||||
# As we have created a FollowUp from an email, we notify the sender
|
# As we have created a FollowUp from an email, we notify the sender
|
||||||
|
@ -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
|
||||||
@ -107,14 +108,6 @@ urlpatterns = [
|
|||||||
staff.ticket_cc_del,
|
staff.ticket_cc_del,
|
||||||
name='ticket_cc_del'),
|
name='ticket_cc_del'),
|
||||||
|
|
||||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/dependency/add/$',
|
|
||||||
staff.ticket_dependency_add,
|
|
||||||
name='ticket_dependency_add'),
|
|
||||||
|
|
||||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/dependency/delete/(?P<dependency_id>[0-9]+)/$',
|
|
||||||
staff.ticket_dependency_del,
|
|
||||||
name='ticket_dependency_del'),
|
|
||||||
|
|
||||||
url(r'^tickets/(?P<ticket_id>[0-9]+)/attachment_delete/(?P<attachment_id>[0-9]+)/$',
|
url(r'^tickets/(?P<ticket_id>[0-9]+)/attachment_delete/(?P<attachment_id>[0-9]+)/$',
|
||||||
staff.attachment_del,
|
staff.attachment_del,
|
||||||
name='attachment_del'),
|
name='attachment_del'),
|
||||||
@ -169,6 +162,17 @@ urlpatterns = [
|
|||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if helpdesk_settings.HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET:
|
||||||
|
urlpatterns += [
|
||||||
|
url(r'^tickets/(?P<ticket_id>[0-9]+)/dependency/add/$',
|
||||||
|
staff.ticket_dependency_add,
|
||||||
|
name='ticket_dependency_add'),
|
||||||
|
|
||||||
|
url(r'^tickets/(?P<ticket_id>[0-9]+)/dependency/delete/(?P<dependency_id>[0-9]+)/$',
|
||||||
|
staff.ticket_dependency_del,
|
||||||
|
name='ticket_dependency_del'),
|
||||||
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
url(r'^$',
|
url(r'^$',
|
||||||
protect_view(public.Homepage.as_view()),
|
protect_view(public.Homepage.as_view()),
|
||||||
@ -218,6 +222,15 @@ urlpatterns += [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# 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 += [
|
urlpatterns += [
|
||||||
url(r'^login/$',
|
url(r'^login/$',
|
||||||
login.login,
|
login.login,
|
||||||
|
@ -16,10 +16,12 @@ def validate_file_extension(value):
|
|||||||
# check if VALID_EXTENSIONS is defined in settings.py
|
# check if VALID_EXTENSIONS is defined in settings.py
|
||||||
# if not use defaults
|
# if not use defaults
|
||||||
|
|
||||||
if settings.VALID_EXTENSIONS:
|
if hasattr(settings, 'VALID_EXTENSIONS'):
|
||||||
valid_extensions = settings.VALID_EXTENSIONS
|
valid_extensions = settings.VALID_EXTENSIONS
|
||||||
else:
|
else:
|
||||||
valid_extensions = ['.txt', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png']
|
valid_extensions = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']
|
||||||
|
|
||||||
if not ext.lower() in valid_extensions:
|
if not ext.lower() in valid_extensions:
|
||||||
raise ValidationError('Unsupported file extension.')
|
# 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())
|
||||||
|
25
helpdesk/views/api.py
Normal file
25
helpdesk/views/api.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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
|
@ -1167,6 +1167,7 @@ def ticket_list(request):
|
|||||||
from_saved_query=saved_query is not None,
|
from_saved_query=saved_query is not None,
|
||||||
saved_query=saved_query,
|
saved_query=saved_query,
|
||||||
search_message=search_message,
|
search_message=search_message,
|
||||||
|
helpdesk_settings=helpdesk_settings,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ class QuickDjangoTest(object):
|
|||||||
#'account',
|
#'account',
|
||||||
#'pinax.invitations',
|
#'pinax.invitations',
|
||||||
#'pinax.teams',
|
#'pinax.teams',
|
||||||
|
'rest_framework',
|
||||||
'helpdesk',
|
'helpdesk',
|
||||||
#'reversion',
|
#'reversion',
|
||||||
)
|
)
|
||||||
@ -104,7 +105,9 @@ class QuickDjangoTest(object):
|
|||||||
## The following settings disable teams
|
## The following settings disable teams
|
||||||
HELPDESK_TEAMS_MODEL = 'auth.User',
|
HELPDESK_TEAMS_MODEL = 'auth.User',
|
||||||
HELPDESK_TEAMS_MIGRATION_DEPENDENCIES = [],
|
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 django.test.runner import DiscoverRunner
|
||||||
|
Loading…
Reference in New Issue
Block a user