Refactor query code into customizable class

This commit is contained in:
Timothy Hobbs 2019-10-30 19:34:58 +01:00
parent 6eee6d196c
commit b96d725239
5 changed files with 162 additions and 147 deletions

View File

@ -1,12 +1,16 @@
from django.db.models import Q from django.db.models import Q
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse
from model_utils import Choices from django.utils.translation import ugettext as _
from base64 import b64encode from base64 import b64encode
from base64 import b64decode from base64 import b64decode
import json import json
from model_utils import Choices
from helpdesk.serializers import DatatablesTicketSerializer
def query_to_base64(query): def query_to_base64(query):
""" """
@ -47,60 +51,26 @@ def query_to_dict(results, descriptions):
return output return output
def apply_query(queryset, params): def get_search_filter_args(search):
""" if search.startswith('queue:'):
Apply a dict-based set of filters & parameters to a queryset. return Q(queue__title__icontains=search[len('queue:'):])
if search.startswith('priority:'):
queryset is a Django queryset, eg MyModel.objects.all() or return Q(priority__icontains=search[len('priority:'):])
MyModel.objects.filter(user=request.user) return (
Q(id__icontains=search) |
params is a dictionary that contains the following: Q(title__icontains=search) |
filtering: A dict of Django ORM filters, eg: Q(description__icontains=search) |
{'user__id__in': [1, 3, 103], 'title__contains': 'foo'} Q(priority__icontains=search) |
Q(resolution__icontains=search) |
search_string: A freetext search string Q(submitter_email__icontains=search) |
Q(assigned_to__email__icontains=search) |
sorting: The name of the column to sort by Q(ticketcustomfieldvalue__value__icontains=search) |
""" Q(created__icontains=search) |
for key in params['filtering'].keys(): Q(due_date__icontains=search)
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_query(query, huser): DATATABLES_ORDER_COLUMN_CHOICES = Choices(
# 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(
('0', 'id'), ('0', 'id'),
('2', 'priority'), ('2', 'priority'),
('3', 'title'), ('3', 'title'),
@ -112,45 +82,132 @@ ORDER_COLUMN_CHOICES = Choices(
) )
def query_tickets_by_args(objects, order_by, **kwargs): def get_query_class():
""" from django.conf import settings
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]
order_column = ORDER_COLUMN_CHOICES[order_column] def _get_query_class():
# django orm '-' -> desc return __Query__
if order == 'desc': return getattr(settings,
order_column = '-' + order_column 'HELPDESK_QUERY_CLASS',
_get_query_class)()
queryset = objects.all().order_by(order_by)
total = queryset.count()
if search_value: class __Query__:
queryset = queryset.filter(Q(id__icontains=search_value) | def __init__(self, huser, base64query=None, query_params=None):
Q(priority__icontains=search_value) | self.huser = huser
Q(title__icontains=search_value) | self.params = query_params if query_params else query_from_base64(base64query)
Q(queue__title__icontains=search_value) | self.base64 = base64query if base64query else query_to_base64(query_params)
Q(status__icontains=search_value) | self.result = None
Q(created__icontains=search_value) |
Q(due_date__icontains=search_value) |
Q(assigned_to__email__icontains=search_value))
count = queryset.count() def get_search_filter_args(self):
queryset = queryset.order_by(order_column)[start:start + length] search = self.params.get('search_string', '')
return { return get_search_filter_args(search)
'items': queryset,
'count': count, def __run__(self, queryset):
'total': total, """
'draw': draw 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 + '<br/>' + followup.title,
'text': (followup.comment if followup.comment else _('No text')) + '<br/> <a href="%s" class="btn" role="button">%s</a>' %
(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,
}

View File

@ -9,5 +9,5 @@
<div class="col col-sm-6"> <div class="col col-sm-6">
<button class='filterBuilderRemove btn btn-danger btn-sm float-right'><i class="fas fa-trash-alt"></i></button> <button class='filterBuilderRemove btn btn-danger btn-sm float-right'><i class="fas fa-trash-alt"></i></button>
</div> </div>
<p class='filterHelp'>{% trans "Keywords are case-insensitive, and will be looked for in the title, body and submitter fields." %}</p> <p class='filterHelp'>{% 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." %}</p>
</div> </div>

View File

@ -125,8 +125,8 @@
if(!timeline_loaded){ if(!timeline_loaded){
timeline = new TL.Timeline( timeline = new TL.Timeline(
'timeline-embed', 'timeline-embed',
'{% url 'helpdesk:timeline_ticket_list' urlsafe_query %}', '{% url 'helpdesk:timeline_ticket_list' urlsafe_query %}'
) );
timeline_loaded = true; timeline_loaded = true;
} }
}); });

View File

@ -6,7 +6,7 @@ from django.test.client import Client
from helpdesk.models import Queue, Ticket from helpdesk.models import Queue, Ticket
from helpdesk import settings from helpdesk import settings
from helpdesk.query import get_query from helpdesk.query import __Query__
from helpdesk.user import HelpdeskUser from helpdesk.user import HelpdeskUser
@ -166,7 +166,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
for identifier in self.IDENTIFIERS: for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % identifier, password=str(identifier)) self.client.login(username='User_%d' % identifier, password=str(identifier))
response = self.client.get(reverse('helpdesk:list')) 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( self.assertEqual(
len(tickets), len(tickets),
identifier * 2, identifier * 2,
@ -186,7 +186,7 @@ class PerQueueStaffMembershipTestCase(TestCase):
# Superuser # Superuser
self.client.login(username='superuser', password='superuser') self.client.login(username='superuser', password='superuser')
response = self.client.get(reverse('helpdesk:list')) 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( self.assertEqual(
len(tickets), len(tickets),
6, 6,

View File

@ -27,18 +27,14 @@ from django.utils import timezone
from django.views.generic.edit import FormView, UpdateView from django.views.generic.edit import FormView, UpdateView
from helpdesk.query import ( from helpdesk.query import (
get_query_class,
query_to_dict, query_to_dict,
get_query,
apply_query,
query_tickets_by_args,
query_to_base64, query_to_base64,
query_from_base64, query_from_base64,
) )
from helpdesk.user import HelpdeskUser from helpdesk.user import HelpdeskUser
from helpdesk.serializers import DatatablesTicketSerializer
from helpdesk.decorators import ( from helpdesk.decorators import (
helpdesk_staff_member_required, helpdesk_superuser_required, helpdesk_staff_member_required, helpdesk_superuser_required,
is_helpdesk_staff is_helpdesk_staff
@ -71,6 +67,7 @@ import re
User = get_user_model() User = get_user_model()
Query = get_query_class()
if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE: if helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE:
# treat 'normal' users like 'staff' # treat 'normal' users like 'staff'
@ -896,7 +893,7 @@ def ticket_list(request):
urlsafe_query = query_to_base64(query_params) 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)) 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 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 serializers.py. The serializers and this view use django-rest_framework methods
""" """
objects = get_query(query, HelpdeskUser(request.user)) query = Query(HelpdeskUser(request.user), base64query=query)
model_object = query_tickets_by_args(objects, '-date_created', **request.query_params) result = query.get_datatables_context(**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']
return (JsonResponse(result, status=status.HTTP_200_OK)) return (JsonResponse(result, status=status.HTTP_200_OK))
@helpdesk_staff_member_required @helpdesk_staff_member_required
@api_view(['GET']) @api_view(['GET'])
def timeline_ticket_list(request, query): def timeline_ticket_list(request, query):
""" query = Query(HelpdeskUser(request.user), base64query=query)
Datatable on ticket_list.html uses this view from to get objects to display return (JsonResponse(query.get_timeline_context(), status=status.HTTP_200_OK))
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 + '<br/>' + followup.title,
'text': (followup.comment if followup.comment else _('No text')) + '<br/> <a href="%s" class="btn" role="button">%s</a>' %
(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))
@helpdesk_staff_member_required @helpdesk_staff_member_required