diff --git a/core/inventory/api_v2.py b/core/inventory/api_v2.py index 0a34140..e027110 100644 --- a/core/inventory/api_v2.py +++ b/core/inventory/api_v2.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import re_path from django.contrib.auth.decorators import permission_required from rest_framework import routers, viewsets 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 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): @@ -20,18 +22,26 @@ class ContainerViewSet(viewsets.ModelViewSet): 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']) -@permission_classes([IsAuthenticated]) -@permission_required('view_item', raise_exception=True) +@permission_classes([]) +# @permission_classes([IsAuthenticated]) +# @permission_required('view_item', raise_exception=True) def search_items(request, event_slug, query): try: event = Event.objects.get(slug=event_slug) - query_tokens = query.split(' ') - q = Item.objects.filter(event=event) - for token in query_tokens: - if token: - q = q.filter(description__icontains=token) - return Response(ItemSerializer(q, many=True).data) + items = filter_items(Item.objects.filter(event=event), b64decode(query).decode('utf-8')) + return Response(SearchResultSerializer(items, many=True).data) except Event.DoesNotExist: return Response(status=404) @@ -99,8 +109,12 @@ router.register(r'boxes', ContainerViewSet, basename='boxes') router.register(r'box', ContainerViewSet, basename='boxes') urlpatterns = router.urls + [ - path('/items/', item), - path('/items//', search_items), - path('/item/', item), - path('/item//', item_by_id), + # path('/items/', item), + # path('/items//', search_items), + # path('/item/', item), + # path('/item//', item_by_id), + re_path(r'^(?P[\w-]+)/items/$', item, name='item'), + re_path(r'^(?P[\w-]+)/items/(?P[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'), + re_path(r'^(?P[\w-]+)/item/$', item, name='item'), + re_path(r'^(?P[\w-]+)/item/(?P\d+)/$', item_by_id, name='item_by_id'), ] diff --git a/core/inventory/serializers.py b/core/inventory/serializers.py index 5a26623..4aef119 100644 --- a/core/inventory/serializers.py +++ b/core/inventory/serializers.py @@ -95,3 +95,14 @@ class ItemSerializer(serializers.ModelSerializer): validated_data.pop('dataImage') instance.files.add(file) 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 diff --git a/core/inventory/tests/v2/test_items.py b/core/inventory/tests/v2/test_items.py index 056b38c..616fb81 100644 --- a/core/inventory/tests/v2/test_items.py +++ b/core/inventory/tests/v2/test_items.py @@ -7,6 +7,8 @@ from authentication.models import ExtendedUser from files.models import File from inventory.models import Event, Container, Item +from base64 import b64encode + class ItemTestCase(TestCase): @@ -169,3 +171,74 @@ class ItemTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) 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']) + diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index 596bd9b..3166bc2 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -1,4 +1,5 @@ import logging +from base64 import b64decode from django.urls import re_path 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 notify_sessions.models import SystemEvent 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): @@ -56,7 +57,7 @@ def reply(request, pk): @api_view(['POST']) @permission_classes([IsAuthenticated]) @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: return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST) 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) +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.register(r'tickets', IssueViewSet, basename='issues') router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') urlpatterns = ([ + re_path(r'tickets/states/$', get_available_states, name='get_available_states'), re_path(r'^tickets/(?P\d+)/reply/$', reply, name='reply'), re_path(r'^tickets/(?P\d+)/comment/$', add_comment, name='add_comment'), - re_path(r'^tickets/manual/$', manual_ticket, name='manual_ticket'), - re_path(r'^tickets/states/$', get_available_states, name='get_available_states'), + re_path(r'^(?P[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'), + re_path(r'^(?P[\w-]+)/tickets/(?P[-A-Za-z0-9+/]*={0,3})/$', search_issues, + name='search_issues'), ] + router.urls) diff --git a/core/tickets/serializers.py b/core/tickets/serializers.py index a980b97..0168a6a 100644 --- a/core/tickets/serializers.py +++ b/core/tickets/serializers.py @@ -134,3 +134,14 @@ class IssueSerializer(serializers.ModelSerializer): def get_queryset(self): 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 diff --git a/core/tickets/tests/v2/test_tickets.py b/core/tickets/tests/v2/test_tickets.py index 8223506..88c169c 100644 --- a/core/tickets/tests/v2/test_tickets.py +++ b/core/tickets/tests/v2/test_tickets.py @@ -3,11 +3,14 @@ from datetime import datetime, timedelta from django.test import TestCase, Client from authentication.models import ExtendedUser +from inventory.models import Event from mail.models import Email, EmailAttachment from tickets.models import IssueThread, StateChange, Comment from django.contrib.auth.models import Permission from knox.models import AuthToken +from base64 import b64encode + class IssueApiTest(TestCase): @@ -230,7 +233,7 @@ class IssueApiTest(TestCase): self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash']) 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'}, content_type='application/json') self.assertEqual(response.status_code, 201) @@ -305,3 +308,21 @@ class IssueApiTest(TestCase): self.assertEqual('pending_new', timeline[0]['state']) self.assertEqual('assignment', timeline[1]['type']) 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())