From d98ee02f320f77397153aa29997e250677e92f09 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 24 Feb 2021 13:28:22 +0000 Subject: [PATCH 1/5] working on ordering products by created date --- history/models.py | 4 ++-- products/models.py | 4 ++-- products/tests.py | 52 ++++++++++++++++++++++++++++++++++++++++++++ products/views.py | 3 +++ stats/models.py | 6 ++--- utils/tag_filters.py | 2 +- 6 files changed, 63 insertions(+), 8 deletions(-) diff --git a/history/models.py b/history/models.py index e102cb0..5296478 100644 --- a/history/models.py +++ b/history/models.py @@ -9,9 +9,9 @@ class HistorySync(models.Model): company = models.ForeignKey('companies.Company', on_delete=models.DO_NOTHING, null=True) rss_url = models.URLField('URL del feed', null=True, blank=True) - sync_date = models.DateTimeField('Fecha de lanzamiento', null=True) + sync_date = models.DateTimeField('Fecha de lanzamiento', null=True, blank=True) result = models.TextField('Resultado', null=True, blank=True) - quantity = models.PositiveIntegerField('Productos importados', null=True) + quantity = models.PositiveIntegerField('Productos importados', null=True, blank=True) # internal created = models.DateTimeField('date of creation', auto_now_add=True) diff --git a/products/models.py b/products/models.py index 84c7060..30631bb 100644 --- a/products/models.py +++ b/products/models.py @@ -33,9 +33,9 @@ class Product(models.Model): sourcing_date = models.DateTimeField('Fecha de importación original de producto', null=True, blank=True) update_date = models.DateTimeField('Fecha de actualización de producto', null=True, blank=True) discount = models.DecimalField('Descuento', max_digits=5, decimal_places=2, null=True, blank=True) - stock = models.PositiveIntegerField('Stock', null=True) + stock = models.PositiveIntegerField('Stock', null=True, blank=True) tags = TagField(to=TreeTag) - category = SingleTagField(blank=True, null=True) # main tag category + category = SingleTagField(null=True, blank=True) # main tag category attributes = TagField(to=TreeTag, related_name='product_attributes') identifiers = models.TextField('Identificador único de producto', null=True, blank=True) diff --git a/products/tests.py b/products/tests.py index d32e1f6..b3a33ce 100644 --- a/products/tests.py +++ b/products/tests.py @@ -163,6 +163,58 @@ class ProductViewSetTest(APITestCase): # Assert number of instnaces in response self.assertEquals(len(expected_instance), len(payload)) + def test_anon_user_can_filter_company(self): + # create instances + company = CompanyFactory() + expected_instance = [ + self.factory(category='ropa', tags="zapatos, rojos", company=company), + self.factory(category='ropa', tags="rojos", company=company), + self.factory(category='ropa', tags="colores/rojos", company=company) + ] + unexpected_instance = [ + self.factory(category='roperos', tags="zapatos, azules"), + self.factory(category='enropados', tags="xxl") + ] + + # prepare url + url = f"{self.endpoint}?company={company.id}" + + # Request list + response = self.client.get(url) + payload = response.json() + + # Assert access is granted + self.assertEqual(response.status_code, status.HTTP_200_OK) + # Assert number of instnaces in response + self.assertEquals(len(expected_instance), len(payload)) + + def test_anon_user_can_order_products(self): + # create instances + company = CompanyFactory() + unexpected_instance = [ + self.factory(category='roperos', tags="zapatos, azules"), + self.factory(category='enropados', tags="xxl") + ] + expected_instance = [ + self.factory(category='ropa', tags="zapatos, rojos", company=company), + self.factory(category='ropa', tags="rojos", company=company), + self.factory(category='ropa', tags="colores/rojos", company=company) + ] + # prepare url + url = f"{self.endpoint}?ordering=created" + + # Request list + response = self.client.get(url) + payload = response.json() + + # Assert access is granted + self.assertEqual(response.status_code, status.HTTP_200_OK) + # TODO: assert correct order + previous_date = datetime.datetime.now() + for instance in payload: + self.assertTrue(datetime.datetime.fromisoformat(instance['created'][:-1]) < previous_date) + previous_date = datetime.datetime.fromisoformat(instance['created'][:-1]) + # authenticated user def test_auth_user_can_list_instances(self): """Regular logged-in user can list instance diff --git a/products/views.py b/products/views.py index fd69c1b..69d7069 100644 --- a/products/views.py +++ b/products/views.py @@ -15,6 +15,7 @@ from rest_framework import viewsets from rest_framework.response import Response from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAdminUser, IsAuthenticated from rest_framework.decorators import api_view, permission_classes, action +from rest_framework.filters import OrderingFilter import requests @@ -41,6 +42,8 @@ class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer permission_classes = [IsAuthenticatedOrReadOnly, IsCreator] + # filter_backends = [ProductTagFilter, OrderingFilter] + # ordering_fields = ['created'] filterset_class = ProductTagFilter filterset_fields = ['name', 'tags', 'category', 'attributes', 'company', 'created'] diff --git a/stats/models.py b/stats/models.py index 1de6942..0b86ec8 100644 --- a/stats/models.py +++ b/stats/models.py @@ -17,11 +17,11 @@ class StatsLog(models.Model): action_object_object_id = models.CharField(max_length=255, blank=True, null=True) action_object = GenericForeignKey('action_object_content_type', 'action_object_object_id') user = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True) - anonymous = models.BooleanField('Usuario no registrado', null=True) + anonymous = models.BooleanField('Usuario no registrado', null=True, blank=True) ip_address = models.GenericIPAddressField('IP usuario', null=True, blank=True) geo = models.PointField('Ubicación aproximada', null=True, blank=True ) - contact = models.BooleanField('Empresa contactada', null=True) - shop = models.BooleanField('Redirigido por botón "Comprar"', null=True) + contact = models.BooleanField('Empresa contactada', null=True, blank=True) + shop = models.BooleanField('Redirigido por botón "Comprar"', null=True, blank=True) # internal created = models.DateTimeField('date of creation', auto_now_add=True) diff --git a/utils/tag_filters.py b/utils/tag_filters.py index 3cc8502..e80d9a6 100644 --- a/utils/tag_filters.py +++ b/utils/tag_filters.py @@ -26,7 +26,7 @@ class ProductTagFilter(django_filters.FilterSet): class Meta: model = Product - fields = ['name', 'tags', 'category', 'attributes'] + fields = ['name', 'tags', 'category', 'attributes', 'company', 'created'] def tag_filter(self, queryset, name, value): return queryset.filter(**{ From 1532040c2c8d6c8f62be5cb7f92ebc3829fb64aa Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 24 Feb 2021 13:59:42 +0000 Subject: [PATCH 2/5] still trying to order products --- products/tests.py | 10 +++++----- products/views.py | 4 ++-- utils/tag_filters.py | 13 ++++++++++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/products/tests.py b/products/tests.py index b3a33ce..8783e44 100644 --- a/products/tests.py +++ b/products/tests.py @@ -209,11 +209,11 @@ class ProductViewSetTest(APITestCase): # Assert access is granted self.assertEqual(response.status_code, status.HTTP_200_OK) - # TODO: assert correct order - previous_date = datetime.datetime.now() - for instance in payload: - self.assertTrue(datetime.datetime.fromisoformat(instance['created'][:-1]) < previous_date) - previous_date = datetime.datetime.fromisoformat(instance['created'][:-1]) + # assert correct order + dates = [d['created'][:-1] for d in payload] + for i in range(len(dates)-1): + # first instance should be most recent + self.assertTrue(datetime.datetime.fromisoformat(dates[i]) > datetime.datetime.fromisoformat(dates[i+1])) # authenticated user def test_auth_user_can_list_instances(self): diff --git a/products/views.py b/products/views.py index 69d7069..557340e 100644 --- a/products/views.py +++ b/products/views.py @@ -27,7 +27,7 @@ from history.models import HistorySync from back_latienda.permissions import IsCreator 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 +from utils.tag_filters import ProductTagFilter, ProductOrderFilter logging.basicConfig( @@ -42,7 +42,7 @@ class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer permission_classes = [IsAuthenticatedOrReadOnly, IsCreator] - # filter_backends = [ProductTagFilter, OrderingFilter] + # filter_backends = [ProductTagFilter, ProductOrderFilter] # ordering_fields = ['created'] filterset_class = ProductTagFilter filterset_fields = ['name', 'tags', 'category', 'attributes', 'company', 'created'] diff --git a/utils/tag_filters.py b/utils/tag_filters.py index e80d9a6..eec4d5a 100644 --- a/utils/tag_filters.py +++ b/utils/tag_filters.py @@ -1,5 +1,7 @@ import django_filters +from rest_framework.filters import BaseFilterBackend + from companies.models import Company from products.models import Product @@ -26,10 +28,19 @@ class ProductTagFilter(django_filters.FilterSet): class Meta: model = Product - fields = ['name', 'tags', 'category', 'attributes', 'company', 'created'] + fields = ['name', 'tags', 'category', 'attributes', 'company', 'created',] def tag_filter(self, queryset, name, value): return queryset.filter(**{ name: value, }) + +class ProductOrderFilter(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + order_field = request.GET.get('order', None) + if order_field is not None: + return queryset.order_by(order_field) + else: + return queryset + From 61a84611edaf0f5a92acf89d0fb12c2c51320204 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 25 Feb 2021 10:02:13 +0000 Subject: [PATCH 3/5] changed product search param from query_string to q --- README.md | 2 +- products/tests.py | 43 +++++++++++++++++++++---------------------- products/views.py | 10 +++++----- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 7c2ba1d..a44efe4 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ Endpoint: `/api/v1/product_search/` Query parameters: - - `query_string`: text from the search input box + - `q`: text from the search input box Response format: diff --git a/products/tests.py b/products/tests.py index 8783e44..4b57c8d 100644 --- a/products/tests.py +++ b/products/tests.py @@ -528,9 +528,9 @@ class ProductSearchTest(TestCase): self.factory(tags="azules"), ] - query_string = quote("zapatos rojos") + q = quote("zapatos rojos") - url = f"{self.endpoint}?query_string={query_string}" + url = f"{self.endpoint}?q={q}" # send in request response = self.client.get(url) @@ -562,10 +562,10 @@ class ProductSearchTest(TestCase): self.factory(tags="azules"), ] - query_string = quote("zapatos rojos") + q = quote("zapatos rojos") limit = 2 - url = f"{self.endpoint}?query_string={query_string}&limit=2" + url = f"{self.endpoint}?q={q}&limit=2" # send in request response = self.client.get(url) @@ -589,9 +589,9 @@ class ProductSearchTest(TestCase): self.factory(tags="lunares/rojos", description="zapatos", shipping_cost=0.00), ] - query_string = quote("zapatos rojos") + q = quote("zapatos rojos") # shipping_cost=true - url = f"{self.endpoint}?query_string={query_string}&shipping_cost=true" + url = f"{self.endpoint}?q={q}&shipping_cost=true" # send in request response = self.client.get(url) # check response @@ -610,10 +610,10 @@ class ProductSearchTest(TestCase): self.factory(tags="azules", shipping_cost=10.00), ] - query_string = quote("zapatos rojos") + q = quote("zapatos rojos") # shipping_cost=false - url = f"{self.endpoint}?query_string={query_string}&shipping_cost=false" + url = f"{self.endpoint}?q={q}&shipping_cost=false" # send in request response = self.client.get(url) # check response @@ -633,9 +633,9 @@ class ProductSearchTest(TestCase): self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes", discount=None), ] - query_string = quote("zapatos rojos") + q = quote("zapatos rojos") # discount=true - url = f"{self.endpoint}?query_string={query_string}&discount=true" + url = f"{self.endpoint}?q={q}&discount=true" # send in request response = self.client.get(url) # check response @@ -655,9 +655,9 @@ class ProductSearchTest(TestCase): self.factory(tags="azules", discount=9.00), ] - query_string = quote("zapatos rojos") + q = quote("zapatos rojos") # discount=true - url = f"{self.endpoint}?query_string={query_string}&discount=false" + url = f"{self.endpoint}?q={q}&discount=false" # send in request response = self.client.get(url) # check response @@ -677,9 +677,8 @@ class ProductSearchTest(TestCase): self.factory(tags="azules"), ] - query_string = quote("zapatos rojos") # discount=true - url = f"{self.endpoint}?query_string=" + url = f"{self.endpoint}?q=" # send in request response = self.client.get(url) # check response @@ -699,9 +698,9 @@ class ProductSearchTest(TestCase): self.factory(tags="zapatos/azules", category="deporte", description='rojos', discount=12.00), ] - query_string = quote("zapatos rojos") + q = quote("zapatos rojos") # discount=true - url = f"{self.endpoint}?query_string={query_string}&category=ropa" + url = f"{self.endpoint}?q={q}&category=ropa" # send in request response = self.client.get(url) # check response @@ -721,9 +720,9 @@ class ProductSearchTest(TestCase): self.factory(tags="zapatos/azules", category="deporte", description='rojos', discount=12.00), ] - query_string = quote("zapatos rojos") + q = quote("zapatos rojos") # discount=true - url = f"{self.endpoint}?query_string={query_string}&tags=deporte" + url = f"{self.endpoint}?q={q}&tags=deporte" # send in request response = self.client.get(url) # check response @@ -743,8 +742,8 @@ class ProductSearchTest(TestCase): self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes", price=None), ] price_min = 5.00 - query_string = quote("zapatos rojos") - url = f"{self.endpoint}?query_string={query_string}&price_min={price_min}" + q = quote("zapatos rojos") + url = f"{self.endpoint}?q={q}&price_min={price_min}" # send in request response = self.client.get(url) @@ -767,8 +766,8 @@ class ProductSearchTest(TestCase): self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes", price=100.00), ] price_max = 50.00 - query_string = quote("zapatos rojos") - url = f"{self.endpoint}?query_string={query_string}&price_max={price_max}" + q = quote("zapatos rojos") + url = f"{self.endpoint}?q={q}&price_max={price_max}" # send in request response = self.client.get(url) diff --git a/products/views.py b/products/views.py index 557340e..6a3fe0c 100644 --- a/products/views.py +++ b/products/views.py @@ -150,7 +150,7 @@ def product_search(request): Takes a string of data, return relevant products Params: - - query_string: used for search [MANDATORY] + - q: used for search [MANDATORY] - limit: max number of returned instances [OPTIONAL] - offset: where to start counting results [OPTIONAL] - shipping_cost: true/false @@ -159,7 +159,7 @@ def product_search(request): - tags: string """ # capture query params - query_string = request.GET.get('query_string', None) + q = request.GET.get('q', None) limit = request.GET.get('limit', None) offset = request.GET.get('offset', None) shipping_cost = request.GET.get('shipping_cost', None) @@ -183,9 +183,9 @@ def product_search(request): price_min = request.GET.get('price_min', None) price_max = request.GET.get('price_max', None) - if query_string is None: + if q is None: return Response({"errors": {"details": "No query string to parse"}}) - elif query_string is '': + elif q is '': # return everything serializer = ProductSerializer(Product.objects.all(), many=True) products = serializer.data @@ -196,7 +196,7 @@ def product_search(request): result_set = set() # split query string into single words - chunks = query_string.split(' ') + chunks = q.split(' ') for chunk in chunks: product_set = find_related_products_v6(chunk, shipping_cost, discount, category, tags, price_min, price_max) # add to result set From 8ea0f74b8c2ea593ee0875ef1d4be920d57beec4 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 25 Feb 2021 10:48:11 +0000 Subject: [PATCH 4/5] cleanup --- products/views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/products/views.py b/products/views.py index 6a3fe0c..68545a9 100644 --- a/products/views.py +++ b/products/views.py @@ -39,13 +39,10 @@ logging.basicConfig( class ProductViewSet(viewsets.ModelViewSet): - queryset = Product.objects.all() + queryset = Product.objects.all().order_by('-created') serializer_class = ProductSerializer permission_classes = [IsAuthenticatedOrReadOnly, IsCreator] - # filter_backends = [ProductTagFilter, ProductOrderFilter] - # ordering_fields = ['created'] filterset_class = ProductTagFilter - filterset_fields = ['name', 'tags', 'category', 'attributes', 'company', 'created'] def perform_create(self, serializer): serializer.save(creator=self.request.user) From db55de0cf55de498d388fe6db04aec8e1202c4ac Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 25 Feb 2021 11:17:35 +0000 Subject: [PATCH 5/5] individual models for each tagged field in Product --- core/management/commands/addtaxonomy.py | 8 ++--- core/management/commands/extractparenttags.py | 8 ++--- core/models.py | 7 ----- products/models.py | 29 +++++++++++++++++-- products/tests.py | 2 +- utils/woocommerce.py | 11 +++---- 6 files changed, 41 insertions(+), 24 deletions(-) diff --git a/core/management/commands/addtaxonomy.py b/core/management/commands/addtaxonomy.py index 569c4ac..040fe32 100644 --- a/core/management/commands/addtaxonomy.py +++ b/core/management/commands/addtaxonomy.py @@ -3,7 +3,7 @@ import logging from django.core.management.base import BaseCommand from django.conf import settings -from core.models import TreeTag +from products.models import CategoryTag from products.models import Product @@ -15,7 +15,7 @@ class Command(BaseCommand): print(self.help) print("Deleting existing instances") - TreeTag.objects.all().delete() + CategoryTag.objects.all().delete() file_path = settings.BASE_DIR + '/../datasets/' + settings.TAXONOMY_FILE counter = 0 @@ -23,11 +23,11 @@ class Command(BaseCommand): print(f"Reading from {settings.TAXONOMY_FILE}") for line in data_file.readlines(): try: - tag = Product.tags.tag_model.objects.create(name=line) + tag = Product.category.tag_model.objects.create(name=line) counter += 1 print('.', end='') logging.debug(f"{tag} created from {line}") except Exception as e: logging.error(f"{type(e)} while creating tags from {settings.TAXONOMY_FILE}") - print(f"\nAdded {counter} Tag objects to Product.tags") + print(f"\nAdded {counter} Tag objects to Product.category") print('Shutting down\n') diff --git a/core/management/commands/extractparenttags.py b/core/management/commands/extractparenttags.py index 7e14657..2ffec50 100644 --- a/core/management/commands/extractparenttags.py +++ b/core/management/commands/extractparenttags.py @@ -3,19 +3,19 @@ import logging from django.core.management.base import BaseCommand from django.conf import settings -from core.models import TreeTag +from products.models import CategoryTag class Command(BaseCommand): - help = 'Extract top level tags' + help = 'Extract top level catefory tags' def handle(self, *args, **kwargs): # get all instances - tags = TreeTag.objects.all() + tags = CategoryTag.objects.all() top_tags = [] - print("Extracting top-level tags from TreeTag instances") + print("Extracting top-level tags from CategoryTag instances") # extract tags with no ancestor for tag in tags: if not tag.get_ancestors(): diff --git a/core/models.py b/core/models.py index 5e42300..3c49a74 100644 --- a/core/models.py +++ b/core/models.py @@ -73,10 +73,3 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): verbose_name = 'Usuario' verbose_name_plural = 'Usuarios' - -class TreeTag(TagTreeModel): - class TagMeta: - initial = "" - force_lowercase = True - max_count=20 - # autocomplete_view = 'myapp.views.hobbies_autocomplete' diff --git a/products/models.py b/products/models.py index 30631bb..81f5146 100644 --- a/products/models.py +++ b/products/models.py @@ -2,11 +2,34 @@ from django.contrib.gis.db import models from tagulous.models import SingleTagField, TagField, TagTreeModel -from core.models import TreeTag from companies.models import Company # Create your models here. +class TreeTag(TagTreeModel): + class TagMeta: + initial = "" + force_lowercase = True + max_count=20 + # autocomplete_view = 'myapp.views.hobbies_autocomplete' + + +class CategoryTag(TagTreeModel): + class TagMeta: + initial = "" + force_lowercase = True + max_count=20 + # autocomplete_view = 'myapp.views.hobbies_autocomplete' + + +class AttributeTag(TagTreeModel): + class TagMeta: + initial = "" + force_lowercase = True + max_count=20 + # autocomplete_view = 'myapp.views.hobbies_autocomplete' + + class Product(models.Model): @@ -35,8 +58,8 @@ class Product(models.Model): discount = models.DecimalField('Descuento', max_digits=5, decimal_places=2, null=True, blank=True) stock = models.PositiveIntegerField('Stock', null=True, blank=True) tags = TagField(to=TreeTag) - category = SingleTagField(null=True, blank=True) # main tag category - attributes = TagField(to=TreeTag, related_name='product_attributes') + category = SingleTagField(to=CategoryTag, null=True, blank=True) # main tag category + attributes = TagField(to=AttributeTag, related_name='product_attributes') identifiers = models.TextField('Identificador único de producto', null=True, blank=True) # internal diff --git a/products/tests.py b/products/tests.py index 4b57c8d..4a6540b 100644 --- a/products/tests.py +++ b/products/tests.py @@ -318,7 +318,7 @@ class ProductViewSetTest(APITestCase): 'discount': '0.05', 'stock': 22, 'tags': ['tag1x, tag2x'], - 'category': 'MayorTagCategory2', + 'category': 'mayortagcategory2', 'attributes': ['color/blue', 'size/m'], 'identifiers': '34rf34f43c43', } diff --git a/utils/woocommerce.py b/utils/woocommerce.py index 36f1cd1..3d5cc7c 100644 --- a/utils/woocommerce.py +++ b/utils/woocommerce.py @@ -89,7 +89,7 @@ def create_imported_product(info, company, history, user): return new else: logging.error(f"{serializer.errors}") - return [] + return None def migrate_shop_products(url, key, secret, user=None, version="wc/v3"): @@ -145,10 +145,11 @@ def migrate_shop_products(url, key, secret, user=None, version="wc/v3"): counter = 0 for product in products: new = create_imported_product(product, company, history, user) - new_products.append(new) - counter += 1 - logging.info(f"Product '{new.name}' created") - print(f"Product '{new.name}' created") + if new is not None: + new_products.append(new) + counter += 1 + logging.info(f"Product '{new.name}' created") + print(f"Product '{new.name}' created") # update history.quantity history.quantity = counter