implement simple backend search for items and tickets
This commit is contained in:
parent
f2647f0dbd
commit
fdf5ab8ad1
6 changed files with 160 additions and 9 deletions
|
@ -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([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(' ')
|
if not request.user.has_event_perm(event, 'view_item'):
|
||||||
q = Item.objects.filter(event=event)
|
return Response(status=403)
|
||||||
for token in query_tokens:
|
items = filter_items(Item.objects.filter(event=event), b64decode(query).decode('utf-8'))
|
||||||
if token:
|
return Response(SearchResultSerializer(items, many=True).data)
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
@ -83,3 +83,14 @@ class ItemSerializer(BasicItemSerializer):
|
||||||
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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
|
@ -199,3 +201,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]['id'], item1.id)
|
self.assertEqual(response.json()[0]['id'], item1.id)
|
||||||
|
|
||||||
|
|
||||||
|
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.id, response.json()[0]['id'])
|
||||||
|
self.assertEqual('abc def', response.json()[0]['description'])
|
||||||
|
self.assertEqual('BOX', response.json()[0]['box'])
|
||||||
|
self.assertEqual(self.box.id, 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.id, response.json()[0]['id'])
|
||||||
|
self.assertEqual('abc def', response.json()[0]['description'])
|
||||||
|
self.assertEqual('BOX', response.json()[0]['box'])
|
||||||
|
self.assertEqual(self.box.id, response.json()[0]['cid'])
|
||||||
|
self.assertEqual(1, response.json()[0]['search_score'])
|
||||||
|
self.assertEqual(self.box.id, response.json()[1]['cid'])
|
||||||
|
self.assertEqual('def ghi', response.json()[1]['description'])
|
||||||
|
self.assertEqual('BOX', response.json()[1]['box'])
|
||||||
|
self.assertEqual(self.box.id, 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.id, response.json()[0]['id'])
|
||||||
|
self.assertEqual('jkl mno pqr', response.json()[0]['description'])
|
||||||
|
self.assertEqual('BOX', response.json()[0]['box'])
|
||||||
|
self.assertEqual(self.box.id, 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.id, response.json()[0]['id'])
|
||||||
|
self.assertEqual('abc def', response.json()[0]['description'])
|
||||||
|
self.assertEqual('BOX', response.json()[0]['box'])
|
||||||
|
self.assertEqual(self.box.id, response.json()[0]['cid'])
|
||||||
|
self.assertEqual(2, response.json()[0]['search_score'])
|
||||||
|
self.assertEqual(self.item2.id, response.json()[1]['id'])
|
||||||
|
self.assertEqual('def ghi', response.json()[1]['description'])
|
||||||
|
self.assertEqual('BOX', response.json()[1]['box'])
|
||||||
|
self.assertEqual(self.box.id, response.json()[1]['cid'])
|
||||||
|
self.assertEqual(1, response.json()[1]['search_score'])
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,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, ItemRelation
|
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher, ItemRelation
|
||||||
from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer
|
from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer, SearchResultSerializer
|
||||||
from tickets.shared_serializers import RelationSerializer
|
from tickets.shared_serializers import RelationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,6 +136,30 @@ 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'matches', RelationViewSet, basename='matches')
|
router.register(r'matches', RelationViewSet, basename='matches')
|
||||||
|
@ -146,4 +170,6 @@ urlpatterns = ([
|
||||||
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'^(?P<event_slug>[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'),
|
re_path(r'^(?P<event_slug>[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'),
|
||||||
|
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)
|
||||||
|
|
|
@ -130,3 +130,14 @@ class IssueSerializer(BasicIssueSerializer):
|
||||||
})
|
})
|
||||||
return sorted(timeline, key=lambda x: x['timestamp'])
|
return sorted(timeline, key=lambda x: x['timestamp'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -9,6 +9,8 @@ from tickets.models import IssueThread, StateChange, Comment, ItemRelation
|
||||||
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):
|
||||||
|
|
||||||
|
@ -365,3 +367,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())
|
||||||
|
|
Loading…
Reference in a new issue