implement simple backend search for items and tickets

This commit is contained in:
j3d1 2024-11-09 05:05:05 +01:00
parent 5a6349c5d3
commit 3e6fe6b002
6 changed files with 176 additions and 19 deletions

View file

@ -1,4 +1,4 @@
from django.urls import path from django.urls import re_path
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from rest_framework import routers, viewsets from rest_framework import routers, viewsets
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
@ -6,7 +6,9 @@ from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item
from inventory.serializers import EventSerializer, ContainerSerializer, ItemSerializer from inventory.serializers import EventSerializer, ContainerSerializer, ItemSerializer, SearchResultSerializer
from base64 import b64decode
class EventViewSet(viewsets.ModelViewSet): class EventViewSet(viewsets.ModelViewSet):
@ -20,18 +22,26 @@ class ContainerViewSet(viewsets.ModelViewSet):
queryset = Container.objects.all() queryset = Container.objects.all()
def filter_items(items, query):
query_tokens = query.split(' ')
for item in items:
value = 0
for token in query_tokens:
if token in item.description:
value += 1
if value > 0:
yield {'search_score': value, 'item': item}
@api_view(['GET']) @api_view(['GET'])
@permission_classes([IsAuthenticated]) @permission_classes([])
@permission_required('view_item', raise_exception=True) # @permission_classes([IsAuthenticated])
# @permission_required('view_item', raise_exception=True)
def search_items(request, event_slug, query): def search_items(request, event_slug, query):
try: try:
event = Event.objects.get(slug=event_slug) event = Event.objects.get(slug=event_slug)
query_tokens = query.split(' ') items = filter_items(Item.objects.filter(event=event), b64decode(query).decode('utf-8'))
q = Item.objects.filter(event=event) return Response(SearchResultSerializer(items, many=True).data)
for token in query_tokens:
if token:
q = q.filter(description__icontains=token)
return Response(ItemSerializer(q, many=True).data)
except Event.DoesNotExist: except Event.DoesNotExist:
return Response(status=404) return Response(status=404)
@ -99,8 +109,12 @@ router.register(r'boxes', ContainerViewSet, basename='boxes')
router.register(r'box', ContainerViewSet, basename='boxes') router.register(r'box', ContainerViewSet, basename='boxes')
urlpatterns = router.urls + [ urlpatterns = router.urls + [
path('<event_slug>/items/', item), # path('<event_slug>/items/', item),
path('<event_slug>/items/<query>/', search_items), # path('<event_slug>/items/<query>/', search_items),
path('<event_slug>/item/', item), # path('<event_slug>/item/', item),
path('<event_slug>/item/<id>/', item_by_id), # path('<event_slug>/item/<id>/', item_by_id),
re_path(r'^(?P<event_slug>[\w-]+)/items/$', item, name='item'),
re_path(r'^(?P<event_slug>[\w-]+)/items/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'),
re_path(r'^(?P<event_slug>[\w-]+)/item/$', item, name='item'),
re_path(r'^(?P<event_slug>[\w-]+)/item/(?P<id>\d+)/$', item_by_id, name='item_by_id'),
] ]

View file

@ -95,3 +95,14 @@ class ItemSerializer(serializers.ModelSerializer):
validated_data.pop('dataImage') validated_data.pop('dataImage')
instance.files.add(file) instance.files.add(file)
return super().update(instance, validated_data) return super().update(instance, validated_data)
class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField()
item = ItemSerializer()
def to_representation(self, instance):
return {**ItemSerializer(instance['item']).data, 'search_score': instance['search_score']}
class Meta:
model = Item

View file

@ -7,6 +7,8 @@ from authentication.models import ExtendedUser
from files.models import File from files.models import File
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item
from base64 import b64encode
class ItemTestCase(TestCase): class ItemTestCase(TestCase):
@ -169,3 +171,74 @@ class ItemTestCase(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['uid'], item1.uid) self.assertEqual(response.json()[0]['uid'], item1.uid)
class ItemSearchTestCase(TestCase):
def setUp(self):
super().setUp()
self.event = Event.objects.create(slug='EVENT', name='Event')
self.box = Container.objects.create(name='BOX')
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
self.item1 = Item.objects.create(container=self.box, event=self.event, description='abc def')
self.item2 = Item.objects.create(container=self.box, event=self.event, description='def ghi')
self.item3 = Item.objects.create(container=self.box, event=self.event, description='jkl mno pqr')
self.item4 = Item.objects.create(container=self.box, event=self.event, description='stu vwx')
def test_search(self):
search_query = b64encode(b'abc').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(self.item1.uid, response.json()[0]['uid'])
self.assertEqual('abc def', response.json()[0]['description'])
self.assertEqual('BOX', response.json()[0]['box'])
self.assertEqual(self.box.cid, response.json()[0]['cid'])
self.assertEqual(1, response.json()[0]['search_score'])
def test_search2(self):
search_query = b64encode(b'def').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(2, len(response.json()))
self.assertEqual(self.item1.uid, response.json()[0]['uid'])
self.assertEqual('abc def', response.json()[0]['description'])
self.assertEqual('BOX', response.json()[0]['box'])
self.assertEqual(self.box.cid, response.json()[0]['cid'])
self.assertEqual(1, response.json()[0]['search_score'])
self.assertEqual(self.item2.uid, response.json()[1]['uid'])
self.assertEqual('def ghi', response.json()[1]['description'])
self.assertEqual('BOX', response.json()[1]['box'])
self.assertEqual(self.box.cid, response.json()[1]['cid'])
self.assertEqual(1, response.json()[0]['search_score'])
def test_search3(self):
search_query = b64encode(b'jkl').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(self.item3.uid, response.json()[0]['uid'])
self.assertEqual('jkl mno pqr', response.json()[0]['description'])
self.assertEqual('BOX', response.json()[0]['box'])
self.assertEqual(self.box.cid, response.json()[0]['cid'])
self.assertEqual(1, response.json()[0]['search_score'])
def test_search4(self):
search_query = b64encode(b'abc def').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(2, len(response.json()))
self.assertEqual(self.item1.uid, response.json()[0]['uid'])
self.assertEqual('abc def', response.json()[0]['description'])
self.assertEqual('BOX', response.json()[0]['box'])
self.assertEqual(self.box.cid, response.json()[0]['cid'])
self.assertEqual(2, response.json()[0]['search_score'])
self.assertEqual(self.item2.uid, response.json()[1]['uid'])
self.assertEqual('def ghi', response.json()[1]['description'])
self.assertEqual('BOX', response.json()[1]['box'])
self.assertEqual(self.box.cid, response.json()[1]['cid'])
self.assertEqual(1, response.json()[1]['search_score'])

View file

@ -1,4 +1,5 @@
import logging import logging
from base64 import b64decode
from django.urls import re_path from django.urls import re_path
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
@ -15,7 +16,7 @@ from mail.models import Email
from mail.protocol import send_smtp, make_reply, collect_references from mail.protocol import send_smtp, make_reply, collect_references
from notify_sessions.models import SystemEvent from notify_sessions.models import SystemEvent
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher
from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer, SearchResultSerializer
class IssueViewSet(viewsets.ModelViewSet): class IssueViewSet(viewsets.ModelViewSet):
@ -56,7 +57,7 @@ def reply(request, pk):
@api_view(['POST']) @api_view(['POST'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
@permission_required('tickets.add_issuethread_manual', raise_exception=True) @permission_required('tickets.add_issuethread_manual', raise_exception=True)
def manual_ticket(request): def manual_ticket(request, event_slug):
if 'name' not in request.data: if 'name' not in request.data:
return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST) return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST)
if 'sender' not in request.data: if 'sender' not in request.data:
@ -117,13 +118,39 @@ def add_comment(request, pk):
return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED) return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED)
def filter_issues(issues, query):
query_tokens = query.split(' ')
for issue in issues:
value = 0
for token in query_tokens:
if token in issue.description:
value += 1
if value > 0:
yield {'search_score': value, 'issue': issue}
@api_view(['GET'])
@permission_classes([])
# @permission_classes([IsAuthenticated])
# @permission_required('view_item', raise_exception=True)
def search_issues(request, event_slug, query):
try:
event = Event.objects.get(slug=event_slug)
items = filter_issues(IssueThread.objects.filter(event=event), b64decode(query).decode('utf-8'))
return Response(SearchResultSerializer(items, many=True).data)
except Event.DoesNotExist:
return Response(status=404)
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register(r'tickets', IssueViewSet, basename='issues') router.register(r'tickets', IssueViewSet, basename='issues')
router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers')
urlpatterns = ([ urlpatterns = ([
re_path(r'tickets/states/$', get_available_states, name='get_available_states'),
re_path(r'^tickets/(?P<pk>\d+)/reply/$', reply, name='reply'), re_path(r'^tickets/(?P<pk>\d+)/reply/$', reply, name='reply'),
re_path(r'^tickets/(?P<pk>\d+)/comment/$', add_comment, name='add_comment'), re_path(r'^tickets/(?P<pk>\d+)/comment/$', add_comment, name='add_comment'),
re_path(r'^tickets/manual/$', manual_ticket, name='manual_ticket'), re_path(r'^(?P<event_slug>[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'),
re_path(r'^tickets/states/$', get_available_states, name='get_available_states'), re_path(r'^(?P<event_slug>[\w-]+)/tickets/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_issues,
name='search_issues'),
] + router.urls) ] + router.urls)

View file

@ -134,3 +134,14 @@ class IssueSerializer(serializers.ModelSerializer):
def get_queryset(self): def get_queryset(self):
return IssueThread.objects.all().order_by('-last_activity') return IssueThread.objects.all().order_by('-last_activity')
class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField()
item = IssueSerializer()
def to_representation(self, instance):
return {**IssueSerializer(instance['item']).data, 'search_score': instance['search_score']}
class Meta:
model = IssueThread

View file

@ -3,11 +3,14 @@ from datetime import datetime, timedelta
from django.test import TestCase, Client from django.test import TestCase, Client
from authentication.models import ExtendedUser from authentication.models import ExtendedUser
from inventory.models import Event
from mail.models import Email, EmailAttachment from mail.models import Email, EmailAttachment
from tickets.models import IssueThread, StateChange, Comment from tickets.models import IssueThread, StateChange, Comment
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from knox.models import AuthToken from knox.models import AuthToken
from base64 import b64encode
class IssueApiTest(TestCase): class IssueApiTest(TestCase):
@ -230,7 +233,7 @@ class IssueApiTest(TestCase):
self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash']) self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash'])
def test_manual_creation(self): def test_manual_creation(self):
response = self.client.post('/api/2/tickets/manual/', response = self.client.post('/api/2/evt/tickets/manual/',
{'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'}, {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},
content_type='application/json') content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
@ -305,3 +308,21 @@ class IssueApiTest(TestCase):
self.assertEqual('pending_new', timeline[0]['state']) self.assertEqual('pending_new', timeline[0]['state'])
self.assertEqual('assignment', timeline[1]['type']) self.assertEqual('assignment', timeline[1]['type'])
self.assertEqual(self.user.username, timeline[1]['assigned_to']) self.assertEqual(self.user.username, timeline[1]['assigned_to'])
class IssueSearchTest(TestCase):
def setUp(self):
super().setUp()
self.event = Event.objects.create(slug='EVENT', name='Event')
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_search(self):
search_query = b64encode(b'abc').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual([], response.json())