Skip to content

Commit

Permalink
#207 Context classes (#212)
Browse files Browse the repository at this point in the history
* Implement ProductBrands class

* Implement ProductImages class

* Create tests: ProductsContext and TagsContext. Fix a lot of code.

* Apply the review's fixes
  • Loading branch information
ArtemijRodionov authored Nov 13, 2018
1 parent 3a11d7d commit 4b333aa
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 9 deletions.
3 changes: 2 additions & 1 deletion catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down Expand Up @@ -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')
)
Expand Down
4 changes: 3 additions & 1 deletion catalog/newcontext/__init__.py
Original file line number Diff line number Diff line change
@@ -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
63 changes: 59 additions & 4 deletions catalog/newcontext/products.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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(
Expand All @@ -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

Expand All @@ -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,
}
5 changes: 3 additions & 2 deletions catalog/newcontext/tags.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)


Expand All @@ -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
2 changes: 1 addition & 1 deletion images/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
70 changes: 70 additions & 0 deletions tests/catalog/test_context.py
Original file line number Diff line number Diff line change
@@ -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()

2 comments on commit 4b333aa

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on 4b333aa Nov 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Puzzle 183-d6341b3e disappeared from catalog/context/__init__.py, that's why I closed #207. Please, remember that the puzzle was not necessarily removed in this particular commit. Maybe it happened earlier, but we discovered this fact only now.

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on 4b333aa Nov 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Puzzle 207-854acb10 discovered in catalog/newcontext/__init__.py and submitted as #213. Please, remember that the puzzle was not necessarily added in this particular commit. Maybe it was added earlier, but we discovered it only now.

Please sign in to comment.