From 75632ddae43e18eca8bc24827b11ebca1d3e6ae7 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Wed, 12 Sep 2018 14:43:57 +0200 Subject: [PATCH 1/5] Login wall systems settings page Fixes #642 --- helpdesk/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/urls.py b/helpdesk/urls.py index 969d8dd1..02d1ff1d 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -222,6 +222,6 @@ urlpatterns += [ name='help_context'), url(r'^system_settings/$', - DirectTemplateView.as_view(template_name='helpdesk/system_settings.html'), + login_required(DirectTemplateView.as_view(template_name='helpdesk/system_settings.html')), name='system_settings'), ] From b4aafb0b54b6ac4556c7b71036dc00e903fec1f9 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Wed, 12 Sep 2018 15:28:54 +0200 Subject: [PATCH 2/5] Fix knowledge base categories display on public homepage --- helpdesk/views/public.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/helpdesk/views/public.py b/helpdesk/views/public.py index e198eec0..886d9cee 100644 --- a/helpdesk/views/public.py +++ b/helpdesk/views/public.py @@ -57,12 +57,10 @@ class CreateTicketView(FormView): return HttpResponseRedirect(reverse('helpdesk:dashboard')) return super().dispatch(*args, **kwargs) - def get_context(self): - knowledgebase_categories = KBCategory.objects.all() - return { - 'helpdesk_settings': helpdesk_settings, - 'kb_categories': knowledgebase_categories - } + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['kb_categories'] = KBCategory.objects.all() + return context def get_initial(self): request = self.request From caaa0e515844f61b2c47cb29fb320dfcee0d45a5 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Sat, 6 Oct 2018 21:23:44 +0200 Subject: [PATCH 3/5] Include ticket secret in path to new attachments in order to reduce URL guessability. --- helpdesk/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/models.py b/helpdesk/models.py index cb80219d..4c6e0879 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -781,7 +781,7 @@ def attachment_path(instance, filename): """ import os os.umask(0) - path = 'helpdesk/attachments/%s/%s' % (instance.followup.ticket.ticket_for_url, instance.followup.id) + path = 'helpdesk/attachments/%s-%s/%s' % (instance.followup.ticket.ticket_for_url, instance.followup.ticket.secret_key, instance.followup.id) att_path = os.path.join(settings.MEDIA_ROOT, path) if settings.DEFAULT_FILE_STORAGE == "django.core.files.storage.FileSystemStorage": if not os.path.exists(att_path): From fc028334d9a2fa1b1a4eedcf586c831ac72d264d Mon Sep 17 00:00:00 2001 From: Dilip Dwarak Date: Sun, 14 Oct 2018 20:23:28 -0400 Subject: [PATCH 4/5] Added optional serverside processing on datatables that lists all tickets - True by default --- helpdesk/lib.py | 57 ++++++++ helpdesk/serializers.py | 48 +++++++ helpdesk/settings.py | 4 + helpdesk/templates/helpdesk/ticket_list.html | 132 +++++++++++++----- .../templates/helpdesk/ticket_list_table.html | 17 +++ helpdesk/urls.py | 4 + helpdesk/views/staff.py | 38 +++++ requirements.txt | 2 + 8 files changed, 264 insertions(+), 38 deletions(-) create mode 100644 helpdesk/serializers.py create mode 100644 helpdesk/templates/helpdesk/ticket_list_table.html diff --git a/helpdesk/lib.py b/helpdesk/lib.py index df9a57d8..16c82758 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -21,6 +21,8 @@ from helpdesk.models import Attachment, EmailTemplate import six +from model_utils import Choices + if six.PY3: from base64 import encodebytes as b64encode from base64 import decodebytes as b64decode @@ -329,3 +331,58 @@ def process_attachments(followup, attached_files): attachments.append([filename, att.file]) return attachments + + +ORDER_COLUMN_CHOICES = Choices( + ('0', 'id'), + ('2', 'priority'), + ('3', 'title'), + ('4', 'queue'), + ('5', 'status'), + ('6', 'created'), + ('7', 'due_date'), + ('8', 'assigned_to') +) + +def query_tickets_by_args(objects, order_by, **kwargs): + """ + This function takes in a list of ticket objects from the views and throws it + to the datatables on ticket_list.html. If a search string was entered, this + function filters existing dataset on search string and returns a filtered + filtered list. The `draw`, `length` etc parameters are for datatables to + display meta data on the table contents. The returning queryset is passed + to a Serializer called TicketSerializer in serializers.py. + """ + draw = int(kwargs.get('draw', None)[0]) + length = int(kwargs.get('length', None)[0]) + start = int(kwargs.get('start', None)[0]) + search_value = kwargs.get('search[value]', None)[0] + order_column = kwargs.get('order[0][column]', None)[0] + order = kwargs.get('order[0][dir]', None)[0] + + order_column = ORDER_COLUMN_CHOICES[order_column] + # django orm '-' -> desc + if order == 'desc': + order_column = '-' + order_column + + queryset = objects.all().order_by(order_by) + total = queryset.count() + + if search_value: + queryset = queryset.filter(Q(id__icontains=search_value) | + Q(priority__icontains=search_value) | + Q(title__icontains=search_value) | + Q(queue__title__icontains=search_value) | + Q(status__icontains=search_value) | + Q(created__icontains=search_value) | + Q(due_date__icontains=search_value) | + Q(assigned_to__email__icontains=search_value)) + + count = queryset.count() + queryset = queryset.order_by(order_column)[start:start + length] + return { + 'items': queryset, + 'count': count, + 'total': total, + 'draw': draw +} diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py new file mode 100644 index 00000000..8422380b --- /dev/null +++ b/helpdesk/serializers.py @@ -0,0 +1,48 @@ +from rest_framework import serializers + +from .models import Ticket + +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 TicketSerializer(serializers.ModelSerializer): + ticket = serializers.SerializerMethodField() + assigned_to = serializers.SerializerMethodField() + created = serializers.SerializerMethodField() + due_date = serializers.SerializerMethodField() + status = serializers.SerializerMethodField() + row_class = serializers.SerializerMethodField() + + class Meta: + model = Ticket + # fields = '__all__' + fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status', 'created', 'due_date', 'assigned_to', 'row_class') + + def get_ticket(self, obj): + return (str(obj.id)+" "+obj.ticket) + + def get_status(self, obj): + return (obj.get_status) + + def get_created(self, obj): + return (humanize.naturaltime(obj.created)) + + def get_due_date(self, obj): + return (humanize.naturaltime(obj.due_date)) + + def get_assigned_to(self, obj): + if obj.assigned_to: + if obj.assigned_to.first_name: + return (obj.assigned_to.first_name) + else: + return (obj.assigned_to.email) + else: + return ("None") + + def get_row_class(self, obj): + return (obj.get_priority_css_class) diff --git a/helpdesk/settings.py b/helpdesk/settings.py index be2efbd3..2cafabbe 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -150,3 +150,7 @@ QUEUE_EMAIL_BOX_UPDATE_ONLY = getattr(settings, 'QUEUE_EMAIL_BOX_UPDATE_ONLY', F # only allow users to access queues that they are members of? HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION = getattr( settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION', False) + + +# Asynchronous Datatables - Optional +USE_SERVERSIDE_PROCESSING = True diff --git a/helpdesk/templates/helpdesk/ticket_list.html b/helpdesk/templates/helpdesk/ticket_list.html index 82797318..cf760198 100644 --- a/helpdesk/templates/helpdesk/ticket_list.html +++ b/helpdesk/templates/helpdesk/ticket_list.html @@ -6,32 +6,100 @@ + + + {% endblock %} {% block h1_title %}Tickets {% if from_saved_query %} [{{ saved_query.title }}]{% endif %}{% endblock %} @@ -227,21 +295,9 @@ $(document).ready(function() { {% trans "Owner" %} - - {% for ticket in tickets %} - - {{ ticket.ticket }} - - {{ ticket.priority }} - {{ ticket.title }} - {{ ticket.queue }} - {{ ticket.get_status }} - {{ ticket.created|naturaltime }} - {{ ticket.due_date|naturaltime }} - {{ ticket.get_assigned_to }} - - {% endfor %} - + {% if not server_side %} + {% include 'helpdesk/ticket_list_table.html' %} + {% endif %} {% csrf_token %} diff --git a/helpdesk/templates/helpdesk/ticket_list_table.html b/helpdesk/templates/helpdesk/ticket_list_table.html new file mode 100644 index 00000000..6b999cb7 --- /dev/null +++ b/helpdesk/templates/helpdesk/ticket_list_table.html @@ -0,0 +1,17 @@ +{% load i18n humanize %} + + + {% for ticket in tickets %} + + {{ ticket.ticket }} + + {{ ticket.priority }} + {{ ticket.title }} + {{ ticket.queue }} + {{ ticket.get_status }} + {{ ticket.created|naturaltime }} + {{ ticket.due_date|naturaltime }} + {{ ticket.get_assigned_to }} + + {% endfor %} + diff --git a/helpdesk/urls.py b/helpdesk/urls.py index 02d1ff1d..778ebf9e 100644 --- a/helpdesk/urls.py +++ b/helpdesk/urls.py @@ -141,6 +141,10 @@ urlpatterns = [ url(r'^ignore/delete/(?P[0-9]+)/$', staff.email_ignore_del, name='email_ignore_del'), + + url(r'^datatables_ticket_list/$', + staff.datatables_ticket_list, + name="datatables_ticket_list"), ] urlpatterns += [ diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 54961f07..1f58e629 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -30,6 +30,14 @@ from django.views.generic.edit import FormView from django.utils import six +# For datatables serverside +from django.core.cache import cache +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.decorators import api_view +from helpdesk.lib import query_tickets_by_args +from helpdesk.serializers import TicketSerializer + from helpdesk.decorators import ( helpdesk_staff_member_required, helpdesk_superuser_required, is_helpdesk_staff @@ -980,6 +988,14 @@ def ticket_list(request): user_saved_queries = SavedSearch.objects.filter(Q(user=request.user) | Q(shared__exact=True)) + # Serverside processing on datatables is optional. Set + # USE_SERVERSIDE_PROCESSING to False in settings.py to disable + if helpdesk_settings.USE_SERVERSIDE_PROCESSING: + cache.set('ticket_qs', ticket_qs) + context['server_side'] = True + else: + context['server_side'] = False + return render(request, 'helpdesk/ticket_list.html', dict( context, tickets=ticket_qs, @@ -999,6 +1015,28 @@ def ticket_list(request): ticket_list = staff_member_required(ticket_list) +@helpdesk_staff_member_required +@api_view(['GET', 'POST']) +def datatables_ticket_list(request): + """ + Datatable on ticket_list.html uses this view from to get objects to display + on the table. query_tickets_by_args is at lib.py, TicketSerializer is in + serializers.py. The serializers and this view use django-rest_framework methods + """ + try: + model_object = query_tickets_by_args(cache.get('ticket_qs'), '-date_created', **request.query_params) + serializer = TicketSerializer(model_object['items'], many=True) + result = dict() + result['data'] = serializer.data + result['draw'] = model_object['draw'] + result['recordsTotal'] = model_object['total'] + result['recordsFiltered'] = model_object['count'] + return (Response(result, status=status.HTTP_200_OK, template_name=None, content_type=None)) + + except TypeError as e: + return (Response(e, status=status.HTTP_404_NOT_FOUND, template_name=None, content_type=None)) + + @helpdesk_staff_member_required def edit_ticket(request, ticket_id): ticket = get_object_or_404(Ticket, id=ticket_id) diff --git a/requirements.txt b/requirements.txt index dbd87059..9d6284dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,5 @@ lxml simplejson pytz six +djangorestframework +django-model-utils \ No newline at end of file From 2b11a0e11a3105d744789c8058680f7cc74d7806 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Mon, 15 Oct 2018 13:56:54 -0400 Subject: [PATCH 5/5] PEP8 formatting fixes --- helpdesk/lib.py | 17 +++++++++-------- helpdesk/serializers.py | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 16c82758..0692213a 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -344,6 +344,7 @@ ORDER_COLUMN_CHOICES = Choices( ('8', 'assigned_to') ) + def query_tickets_by_args(objects, order_by, **kwargs): """ This function takes in a list of ticket objects from the views and throws it @@ -370,13 +371,13 @@ def query_tickets_by_args(objects, order_by, **kwargs): if search_value: queryset = queryset.filter(Q(id__icontains=search_value) | - Q(priority__icontains=search_value) | - Q(title__icontains=search_value) | - Q(queue__title__icontains=search_value) | - Q(status__icontains=search_value) | - Q(created__icontains=search_value) | - Q(due_date__icontains=search_value) | - Q(assigned_to__email__icontains=search_value)) + Q(priority__icontains=search_value) | + Q(title__icontains=search_value) | + Q(queue__title__icontains=search_value) | + Q(status__icontains=search_value) | + Q(created__icontains=search_value) | + Q(due_date__icontains=search_value) | + Q(assigned_to__email__icontains=search_value)) count = queryset.count() queryset = queryset.order_by(order_column)[start:start + length] @@ -385,4 +386,4 @@ def query_tickets_by_args(objects, order_by, **kwargs): 'count': count, 'total': total, 'draw': draw -} + } diff --git a/helpdesk/serializers.py b/helpdesk/serializers.py index 8422380b..e5c7dc7d 100644 --- a/helpdesk/serializers.py +++ b/helpdesk/serializers.py @@ -10,6 +10,7 @@ datatables for ticket_list.html. Called from staff.datatables_ticket_list. """ + class TicketSerializer(serializers.ModelSerializer): ticket = serializers.SerializerMethodField() assigned_to = serializers.SerializerMethodField() @@ -24,7 +25,7 @@ class TicketSerializer(serializers.ModelSerializer): fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status', 'created', 'due_date', 'assigned_to', 'row_class') def get_ticket(self, obj): - return (str(obj.id)+" "+obj.ticket) + return (str(obj.id) + " " + obj.ticket) def get_status(self, obj): return (obj.get_status)