From 7ca180f8e61cf4698867b9cf04b9f2a60a22bd2d Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 23 Feb 2021 11:57:25 +0000 Subject: [PATCH] added filter by shipping cost to product search --- products/tests.py | 59 ++++++++++++++++++++++++++++++++++++++++++----- products/utils.py | 31 +++++++++++++++++++++++++ products/views.py | 33 +++++++++++++++++--------- 3 files changed, 106 insertions(+), 17 deletions(-) diff --git a/products/tests.py b/products/tests.py index 66e639c..21d12c1 100644 --- a/products/tests.py +++ b/products/tests.py @@ -460,6 +460,9 @@ class ProductSearchTest(TestCase): self.user.set_password(self.password) self.user.save() + def tearDown(self): + self.model.objects.all().delete() + def test_anon_user_can_search(self): expected_instances = [ self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes"), @@ -478,7 +481,6 @@ class ProductSearchTest(TestCase): url = f"{self.endpoint}?query_string={query_string}" # send in request response = self.client.get(url) - # import ipdb; ipdb.set_trace() # check response self.assertEqual(response.status_code, 200) @@ -523,7 +525,56 @@ class ProductSearchTest(TestCase): # load response data payload = response.json() self.assertEquals(len(payload['products']), limit) - self.assertEquals(payload['total_results'], len(expected_instances)) + self.assertEquals(payload['count'], len(expected_instances)) + + def test_anon_user_can_filter_shipping_cost_true(self): + expected_instances = [ + self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes", shipping_cost=None), + self.factory(tags="colores/rojos, tono/brillante", shipping_cost=100.00), + self.factory(tags="lunares/azules", description="zapatos rojos", shipping_cost=12.00), + self.factory(tags="lunares/rojos", description="zapatos", shipping_cost=0.00), + self.factory(attributes='"zapatos de campo", tono/oscuro', shipping_cost=9.00), + ] + unexpected_instances = [ + self.factory(description="chanclas"), + self.factory(tags="azules"), + ] + + query_string = quote("zapatos rojos") + # shipping_cost=true + url = f"{self.endpoint}?query_string={query_string}&shipping_cost=true" + # send in request + response = self.client.get(url) + # check response + self.assertEqual(response.status_code, 200) + # load response data + payload = response.json() + self.assertEquals(len(payload['products']), 3) + + def test_anon_user_can_filter_shipping_cost_false(self): + expected_instances = [ + self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes", shipping_cost=None), + self.factory(tags="colores/rojos, tono/brillante", shipping_cost=100.00), + self.factory(tags="lunares/azules", description="zapatos rojos", shipping_cost=12.00), + self.factory(tags="lunares/rojos", description="zapatos", shipping_cost=0.00), + self.factory(attributes='"zapatos de campo", tono/oscuro', shipping_cost=9.00), + ] + unexpected_instances = [ + self.factory(description="chanclas"), + self.factory(tags="azules"), + ] + + query_string = quote("zapatos rojos") + + # shipping_cost=false + url = f"{self.endpoint}?query_string={query_string}&shipping_cost=false" + # send in request + response = self.client.get(url) + # check response + self.assertEqual(response.status_code, 200) + # load response data + payload = response.json() + self.assertEquals(len(payload['products']), 2) class MyProductsViewTest(APITestCase): @@ -607,7 +658,3 @@ class FindRelatedProductsTest(APITestCase): # assert result self.assertTrue(len(results) == len(expected_instances)) - - - - diff --git a/products/utils.py b/products/utils.py index 2b6f8e0..09758f9 100644 --- a/products/utils.py +++ b/products/utils.py @@ -125,6 +125,37 @@ def find_related_products_v3(keyword): return set(products_qs) +def find_related_products_v6(keyword, shipping_cost=None): + """ + Ranked product search + + SearchVectors for the fields + SearchQuery for the value + SearchRank for relevancy scoring and ranking + + allow filtering by: + - shipping cost + """ + vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__name') + query = SearchQuery(keyword) + + products_qs = Product.objects.annotate( + rank=SearchRank(vector, query) + ).filter(rank__gt=0.05) # removed order_by because its lost in casting + + if shipping_cost is True: + # only instances with shipping costs + products_qs = products_qs.filter( + Q(shipping_cost__isnull=False)& + Q(shipping_cost__gte=1) + ) + elif shipping_cost is False: + # only intances without shpping costs + products_qs = products_qs.filter(Q(shipping_cost=None)|Q(shipping_cost=0.00)) + + return set(products_qs) + + def find_related_products_v4(keyword): """ Similarity-ranked search using trigrams diff --git a/products/views.py b/products/views.py index 7a36b1f..8e98fd7 100644 --- a/products/views.py +++ b/products/views.py @@ -24,7 +24,7 @@ from companies.models import Company from history.models import HistorySync from back_latienda.permissions import IsCreator -from .utils import extract_search_filters, find_related_products_v3 +from .utils import extract_search_filters, find_related_products_v3, find_related_products_v6 from utils.tag_serializers import TaggitSerializer from utils.tag_filters import ProductTagFilter @@ -150,35 +150,46 @@ def product_search(request): - query_string: used for search [MANDATORY] - limit: max number of returned instances [OPTIONAL] - offset: where to start counting results [OPTIONAL] + - shipping_cost: true/false """ query_string = request.GET.get('query_string', None) - + limit = request.GET.get('limit', None) + offset = request.GET.get('offset', None) + shipping_cost = request.GET.get('shipping_cost', None) + if shipping_cost is not None: + if shipping_cost == 'true': + shipping_cost = True + elif shipping_cost == 'false': + shipping_cost = False + else: + shipping_cost = None if query_string is None: return Response({"errors": {"details": "No query string to parse"}}) + elif query_string is '': + # return everything + pass try: - # save results + # we collect our results here result_set = set() + # split query string into single words chunks = query_string.split(' ') - for chunk in chunks: - product_set = find_related_products_v3(chunk) + product_set = find_related_products_v6(chunk, shipping_cost) # add to result set result_set.update(product_set) - # TODO: add search for entire phrase + # TODO: add search for entire phrase ??? # extract filters from result_set filters = extract_search_filters(result_set) - # order results and respond + # order results by RANK result_list = list(result_set) ranked_products = sorted(result_list, key= lambda rank:rank.rank, reverse=True) serializer = SearchResultSerializer(ranked_products, many=True) product_results = [dict(i) for i in serializer.data] total_results = len(product_results) - # check for pagination - limit = request.GET.get('limit', None) - offset = request.GET.get('offset', None) + # RESULTS PAGINATION if limit is not None and offset is not None: limit = int(limit) offset = int(offset) @@ -187,6 +198,6 @@ def product_search(request): limit = int(limit) product_results = product_results[:limit] - return Response(data={"filters": filters, "total_results": total_results, "products": product_results}) + return Response(data={"filters": filters, "count": total_results, "products": product_results}) except Exception as e: return Response({"errors": {"details": str(e)}}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)