diff --git a/helpdesk/query.py b/helpdesk/query.py index a2263326..6a56baea 100644 --- a/helpdesk/query.py +++ b/helpdesk/query.py @@ -1,12 +1,16 @@ from django.db.models import Q from django.core.cache import cache - -from model_utils import Choices +from django.urls import reverse +from django.utils.translation import ugettext as _ from base64 import b64encode from base64 import b64decode import json +from model_utils import Choices + +from helpdesk.serializers import DatatablesTicketSerializer + def query_to_base64(query): """ @@ -47,60 +51,26 @@ def query_to_dict(results, descriptions): return output -def apply_query(queryset, params): - """ - Apply a dict-based set of filters & parameters to a queryset. - - queryset is a Django queryset, eg MyModel.objects.all() or - MyModel.objects.filter(user=request.user) - - params is a dictionary that contains the following: - filtering: A dict of Django ORM filters, eg: - {'user__id__in': [1, 3, 103], 'title__contains': 'foo'} - - search_string: A freetext search string - - sorting: The name of the column to sort by - """ - for key in params['filtering'].keys(): - filter = {key: params['filtering'][key]} - queryset = queryset.filter(**filter) - - search = params.get('search_string', '') - if search: - qset = ( - Q(title__icontains=search) | - Q(description__icontains=search) | - Q(resolution__icontains=search) | - Q(submitter_email__icontains=search) | - Q(ticketcustomfieldvalue__value__icontains=search) - ) - - queryset = queryset.filter(qset) - - sorting = params.get('sorting', None) - if sorting: - sortreverse = params.get('sortreverse', None) - if sortreverse: - sorting = "-%s" % sorting - queryset = queryset.order_by(sorting) - - return queryset +def get_search_filter_args(search): + if search.startswith('queue:'): + return Q(queue__title__icontains=search[len('queue:'):]) + if search.startswith('priority:'): + return Q(priority__icontains=search[len('priority:'):]) + return ( + Q(id__icontains=search) | + Q(title__icontains=search) | + Q(description__icontains=search) | + Q(priority__icontains=search) | + Q(resolution__icontains=search) | + Q(submitter_email__icontains=search) | + Q(assigned_to__email__icontains=search) | + Q(ticketcustomfieldvalue__value__icontains=search) | + Q(created__icontains=search) | + Q(due_date__icontains=search) + ) -def get_query(query, huser): - # Prefilter the allowed tickets - objects = cache.get(huser.user.email + query) - if objects is not None: - return objects - tickets = huser.get_tickets_in_queues().select_related() - query_params = query_from_base64(query) - ticket_qs = apply_query(tickets, query_params) - cache.set(huser.user.email + query, ticket_qs, timeout=3600) - return ticket_qs - - -ORDER_COLUMN_CHOICES = Choices( +DATATABLES_ORDER_COLUMN_CHOICES = Choices( ('0', 'id'), ('2', 'priority'), ('3', 'title'), @@ -112,45 +82,132 @@ ORDER_COLUMN_CHOICES = Choices( ) -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 DatatablesTicketSerializer 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] +def get_query_class(): + from django.conf import settings - order_column = ORDER_COLUMN_CHOICES[order_column] - # django orm '-' -> desc - if order == 'desc': - order_column = '-' + order_column + def _get_query_class(): + return __Query__ + return getattr(settings, + 'HELPDESK_QUERY_CLASS', + _get_query_class)() - 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)) +class __Query__: + def __init__(self, huser, base64query=None, query_params=None): + self.huser = huser + self.params = query_params if query_params else query_from_base64(base64query) + self.base64 = base64query if base64query else query_to_base64(query_params) + self.result = None - count = queryset.count() - queryset = queryset.order_by(order_column)[start:start + length] - return { - 'items': queryset, - 'count': count, - 'total': total, - 'draw': draw - } + def get_search_filter_args(self): + search = self.params.get('search_string', '') + return get_search_filter_args(search) + + def __run__(self, queryset): + """ + Apply a dict-based set of filters & parameters to a queryset. + + queryset is a Django queryset, eg MyModel.objects.all() or + MyModel.objects.filter(user=request.user) + + params is a dictionary that contains the following: + filtering: A dict of Django ORM filters, eg: + {'user__id__in': [1, 3, 103], 'title__contains': 'foo'} + + search_string: A freetext search string + + sorting: The name of the column to sort by + """ + for key in self.params['filtering'].keys(): + filter = {key: self.params['filtering'][key]} + queryset = queryset.filter(**filter) + queryset = queryset.filter(self.get_search_filter_args()) + sorting = self.params.get('sorting', None) + if sorting: + sortreverse = self.params.get('sortreverse', None) + if sortreverse: + sorting = "-%s" % sorting + queryset = queryset.order_by(sorting) + return queryset + + def refresh_query(self): + tickets = self.huser.get_tickets_in_queues().select_related() + ticket_qs = self.__run__(tickets) + cache.set(self.huser.user.email + self.base64, ticket_qs, timeout=3600) + return ticket_qs + + def get(self): + # Prefilter the allowed tickets + objects = cache.get(self.huser.user.email + self.base64) + if objects is not None: + return objects + return self.refresh_query() + + def get_datatables_context(self, **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 DatatablesTicketSerializer in serializers.py. + """ + objects = self.get() + order_by = '-date_created' + 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 = DATATABLES_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(get_search_filter_args(search_value)) + + count = queryset.count() + queryset = queryset.order_by(order_column)[start:start + length] + return { + 'data': DatatablesTicketSerializer(queryset, many=True).data, + 'recordsFiltered': count, + 'recordsTotal': total, + 'draw': draw + } + + def get_timeline_context(self): + events = [] + + for ticket in self.get(): + for followup in ticket.followup_set.all(): + event = { + 'start_date': self.mk_timeline_date(followup.date), + 'text': { + 'headline': ticket.title + '
' + followup.title, + 'text': (followup.comment if followup.comment else _('No text')) + '
%s' % + (reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")), + }, + 'group': _('Messages'), + } + events.append(event) + + return { + 'events': events, + } + + def mk_timeline_date(self, date): + return { + 'year': date.year, + 'month': date.month, + 'day': date.day, + 'hour': date.hour, + 'minute': date.minute, + 'second': date.second, + 'second': date.second, + } diff --git a/helpdesk/templates/helpdesk/filters/keywords.html b/helpdesk/templates/helpdesk/filters/keywords.html index 60af3fd6..adf37a7e 100644 --- a/helpdesk/templates/helpdesk/filters/keywords.html +++ b/helpdesk/templates/helpdesk/filters/keywords.html @@ -9,5 +9,5 @@
-

{% trans "Keywords are case-insensitive, and will be looked for in the title, body and submitter fields." %}

+

{% trans "Keywords are case-insensitive, and will be looked for pretty much everywhere possible. Prepend with 'queue:' or 'priority:' to search by queue or priority." %}

diff --git a/helpdesk/templates/helpdesk/ticket_list.html b/helpdesk/templates/helpdesk/ticket_list.html index f9818f99..3e56eb7c 100644 --- a/helpdesk/templates/helpdesk/ticket_list.html +++ b/helpdesk/templates/helpdesk/ticket_list.html @@ -125,8 +125,8 @@ if(!timeline_loaded){ timeline = new TL.Timeline( 'timeline-embed', - '{% url 'helpdesk:timeline_ticket_list' urlsafe_query %}', - ) + '{% url 'helpdesk:timeline_ticket_list' urlsafe_query %}' + ); timeline_loaded = true; } }); diff --git a/helpdesk/tests/test_per_queue_staff_permission.py b/helpdesk/tests/test_per_queue_staff_permission.py index b70d7816..5c036322 100644 --- a/helpdesk/tests/test_per_queue_staff_permission.py +++ b/helpdesk/tests/test_per_queue_staff_permission.py @@ -6,7 +6,7 @@ from django.test.client import Client from helpdesk.models import Queue, Ticket from helpdesk import settings -from helpdesk.query import get_query +from helpdesk.query import __Query__ from helpdesk.user import HelpdeskUser @@ -166,7 +166,7 @@ class PerQueueStaffMembershipTestCase(TestCase): for identifier in self.IDENTIFIERS: self.client.login(username='User_%d' % identifier, password=str(identifier)) response = self.client.get(reverse('helpdesk:list')) - tickets = get_query(response.context['urlsafe_query'], HelpdeskUser(self.identifier_users[identifier])) + tickets = __Query__(HelpdeskUser(self.identifier_users[identifier]), base64query = response.context['urlsafe_query']).get() self.assertEqual( len(tickets), identifier * 2, @@ -186,7 +186,7 @@ class PerQueueStaffMembershipTestCase(TestCase): # Superuser self.client.login(username='superuser', password='superuser') response = self.client.get(reverse('helpdesk:list')) - tickets = get_query(response.context['urlsafe_query'], HelpdeskUser(self.superuser)) + tickets = __Query__(HelpdeskUser(self.superuser), base64query = response.context['urlsafe_query']).get() self.assertEqual( len(tickets), 6, diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 87e1b68a..c374b51b 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -27,18 +27,14 @@ from django.utils import timezone from django.views.generic.edit import FormView, UpdateView from helpdesk.query import ( + get_query_class, query_to_dict, - get_query, - apply_query, - query_tickets_by_args, query_to_base64, query_from_base64, ) from helpdesk.user import HelpdeskUser -from helpdesk.serializers import DatatablesTicketSerializer - from helpdesk.decorators import ( helpdesk_staff_member_required, helpdesk_superuser_required, is_helpdesk_staff @@ -71,6 +67,7 @@ import re User = get_user_model() +Query = get_query_class() if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE: # treat 'normal' users like 'staff' @@ -896,7 +893,7 @@ def ticket_list(request): urlsafe_query = query_to_base64(query_params) - get_query(urlsafe_query, huser) + Query(huser, base64query=urlsafe_query).refresh_query() user_saved_queries = SavedSearch.objects.filter(Q(user=request.user) | Q(shared__exact=True)) @@ -964,55 +961,16 @@ def datatables_ticket_list(request, query): on the table. query_tickets_by_args is at lib.py, DatatablesTicketSerializer is in serializers.py. The serializers and this view use django-rest_framework methods """ - objects = get_query(query, HelpdeskUser(request.user)) - model_object = query_tickets_by_args(objects, '-date_created', **request.query_params) - serializer = DatatablesTicketSerializer(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'] + query = Query(HelpdeskUser(request.user), base64query=query) + result = query.get_datatables_context(**request.query_params) return (JsonResponse(result, status=status.HTTP_200_OK)) @helpdesk_staff_member_required @api_view(['GET']) def timeline_ticket_list(request, query): - """ - 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, DatatablesTicketSerializer is in - serializers.py. The serializers and this view use django-rest_framework methods - """ - tickets = get_query(query, HelpdeskUser(request.user)) - events = [] - - def mk_timeline_date(date): - return { - 'year': date.year, - 'month': date.month, - 'day': date.day, - 'hour': date.hour, - 'minute': date.minute, - 'second': date.second, - 'second': date.second, - } - for ticket in tickets: - for followup in ticket.followup_set.all(): - event = { - 'start_date': mk_timeline_date(followup.date), - 'text': { - 'headline': ticket.title + '
' + followup.title, - 'text': (followup.comment if followup.comment else _('No text')) + '
%s' % - (reverse('helpdesk:view', kwargs={'ticket_id': ticket.pk}), _("View ticket")), - }, - 'group': _('Messages'), - } - events.append(event) - - result = { - 'events': events, - } - return (JsonResponse(result, status=status.HTTP_200_OK)) + query = Query(HelpdeskUser(request.user), base64query=query) + return (JsonResponse(query.get_timeline_context(), status=status.HTTP_200_OK)) @helpdesk_staff_member_required