From 4b333aa9550936b9c4f5f7642c3bbf21bd2e7106 Mon Sep 17 00:00:00 2001 From: Artemiy Date: Wed, 14 Nov 2018 00:05:59 +0300 Subject: [PATCH] #207 Context classes (#212) * Implement ProductBrands class * Implement ProductImages class * Create tests: ProductsContext and TagsContext. Fix a lot of code. * Apply the review's fixes --- catalog/models.py | 3 +- catalog/newcontext/__init__.py | 4 +- catalog/newcontext/products.py | 63 ++++++++++++++++++++++++++++-- catalog/newcontext/tags.py | 5 ++- images/models.py | 2 +- tests/catalog/test_context.py | 70 ++++++++++++++++++++++++++++++++++ 6 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 tests/catalog/test_context.py diff --git a/catalog/models.py b/catalog/models.py index f34eda3..e2f400d 100644 --- a/catalog/models.py +++ b/catalog/models.py @@ -134,7 +134,7 @@ def tagged(self, tags): # See docs for details: # https://www.postgresql.org/docs/10/static/sql-select.html#SQL-DISTINCT # https://docs.djangoproject.com/en/2.1/ref/models/querysets/#django.db.models.query.QuerySet.distinct - return self.filter(tags__in=self._tags).distinct() + return self.filter(tags__in=tags).distinct() class ProductManager(models.Manager.from_queryset(ProductQuerySet)): @@ -259,6 +259,7 @@ def get_group_tags_pairs(self) -> List[Tuple[TagGroup, List['Tag']]]: def get_brands(self, products: Iterable[AbstractProduct]) -> Dict[AbstractProduct, 'Tag']: brand_tags = ( self.filter(group__name=settings.BRAND_TAG_GROUP_NAME) + .filter_by_products(products) .prefetch_related('products') .select_related('group') ) diff --git a/catalog/newcontext/__init__.py b/catalog/newcontext/__init__.py index 40fc422..2f79330 100644 --- a/catalog/newcontext/__init__.py +++ b/catalog/newcontext/__init__.py @@ -1 +1,3 @@ -# @todo #183:120m Implement Page, PaginatedProducts, ProductBrands, ProductsImages context classes. +# @todo #207:120m Implement Page, PaginatedProducts context classes. +from . import products, tags +from .context import Context, Contexts, ModelContext, Products, Tags diff --git a/catalog/newcontext/products.py b/catalog/newcontext/products.py index 5d2989c..05b0246 100644 --- a/catalog/newcontext/products.py +++ b/catalog/newcontext/products.py @@ -1,11 +1,24 @@ import typing +from django.conf import settings from django.db.models import QuerySet -from catalog.newcontext.context import Products, Tags +from catalog.newcontext.context import Context, Products, Tags from catalog.models import AbstractCategory +class SortingOption: + def __init__(self, index=0): + options = settings.CATEGORY_SORTING_OPTIONS[index] + self.label = options['label'] + self.field = options['field'] + self.direction = options['direction'] + + @property + def directed_field(self): + return self.direction + self.field + + class ActiveProducts(Products): def __init__(self, products: Products): @@ -19,7 +32,7 @@ class OrderedProducts(Products): def __init__(self, products: Products, req_kwargs): self._products = products - self._sorting_index = self._req_kwargs.get('sorting', 0) + self._sorting_index = req_kwargs.get('sorting', 0) def qs(self): return self._products.qs().order_by( @@ -43,9 +56,9 @@ def qs(self): return self._products.qs().get_category_descendants(self._category) -class ProductsByTags(Products): +class TaggedProducts(Products): - def __init__(self, products: Products, tags_context: Tags): + def __init__(self, products: Products, tags: Tags): self._products = products self._tags = tags @@ -55,3 +68,45 @@ def qs(self): return self._products.qs().tagged(tags) else: return self._products.qs() + + +class ProductBrands(Context): + + def __init__(self, products: Products, tags: Tags): + self._products = products + self._tags = tags + + def context(self): + products_qs = self.products.qs() + brands = self.tags.qs().get_brands(products_qs) + + product_brands = { + product.id: brands.get(product) + for product in products_qs + } + + return { + 'product_brands': product_brands, + } + + +class ProductImages(Context): + + def __init__(self, products: Products, images: QuerySet): + self._products = products + self._images = images + + def context(self): + page_product_map = { + product.page: product + for product in self._products.qs() + } + images = self._images.get_main_images_by_pages(page_product_map.keys()) + product_images = { + product: images.get(page) + for page, product in page_product_map.items() + } + + return { + 'product_images': product_images, + } diff --git a/catalog/newcontext/tags.py b/catalog/newcontext/tags.py index 0da326f..0148ad3 100644 --- a/catalog/newcontext/tags.py +++ b/catalog/newcontext/tags.py @@ -1,6 +1,7 @@ from catalog.newcontext.context import Context, Tags, Products from django.db.models import QuerySet +from django.http import Http404 class GroupedTags(Context): @@ -23,7 +24,7 @@ def __init__(self, tags: Tags, req_kwargs): def qs(self): tags = self._tags.qs() if not self._raw_tags: - tags.none() + return tags.none() return tags.parsed(self._raw_tags) @@ -35,5 +36,5 @@ def __init__(self, tags: Tags): def qs(self): tags = self._tags.qs() if not tags.exists(): - raise http.Http404('No such tag.') + raise Http404('No such tag.') return tags diff --git a/images/models.py b/images/models.py index 8d5e4e0..5d946b1 100644 --- a/images/models.py +++ b/images/models.py @@ -25,7 +25,7 @@ def get_main_images_by_pages(self, pages) -> dict: images_query = ( self.get_queryset() - .filter(object_id__in=[page.id for page in pages], is_main=True) + .filter(object_id__in=[page.id for page in pages], is_main=True) ) if not images_query.exists(): diff --git a/tests/catalog/test_context.py b/tests/catalog/test_context.py new file mode 100644 index 0000000..bd9a352 --- /dev/null +++ b/tests/catalog/test_context.py @@ -0,0 +1,70 @@ +import unittest + +from django.test import TestCase, override_settings +from django.http import Http404 + +from catalog import newcontext as context +from tests.catalog import models as catalog_models + + +def mocked_ctx(qs_attrs=None, context_attrs=None): + ctx = unittest.mock.Mock() + ctx.qs.return_value = unittest.mock.Mock(**(qs_attrs or {})) + ctx.context.return_value = unittest.mock.Mock(**(context_attrs or {})) + + return ctx + + +class ProductsContext(TestCase): + + fixtures = ['catalog.json'] + + def products_ctx(self, qs=None) -> context.context.Products: + return context.context.Products(qs or catalog_models.MockProduct.objects.all()) + + def test_ordered_products(self): + order_by = 'price' + with override_settings(CATEGORY_SORTING_OPTIONS={ + 1: {'label': order_by, 'field': order_by, 'direction': ''} + }): + products_ctx = self.products_ctx() + self.assertEqual( + list(products_ctx.qs().order_by(order_by)), + list(context.products.OrderedProducts(products_ctx, {'sorting': 1}).qs()), + ) + + def test_tagged_products(self): + products_ctx = mocked_ctx() + context.products.TaggedProducts( + products_ctx, mocked_ctx(qs_attrs={'exists.return_value': True}), + ).qs() + + self.assertTrue(products_ctx.qs().tagged.called) + + def test_non_tagged_products(self): + """If there are no tags, then products don't changed.""" + products_ctx = mocked_ctx() + context.products.TaggedProducts( + products_ctx, mocked_ctx(qs_attrs={'exists.return_value': False}), + ).qs() + + self.assertFalse(products_ctx.qs().tagged.called) + + +class TagsContext(TestCase): + + def test_parsed_tags(self): + tags_ctx = mocked_ctx() + context.tags.ParsedTags(tags_ctx, {'tags': 'test'}).qs() + self.assertTrue(tags_ctx.qs().parsed.called) + + def test_unparsed_tags(self): + self.assertFalse( + context.tags.ParsedTags( + mocked_ctx(qs_attrs={'none.return_value': []}), {}, + ).qs() + ) + + def test_404_check_tags(self): + with self.assertRaises(Http404): + context.tags.Checked404Tags(mocked_ctx(qs_attrs={'exists.return_value': False})).qs()