Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#207 Context classes #212

Merged
merged 4 commits into from
Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
3 changes: 2 additions & 1 deletion catalog/newcontext/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# @todo #183:120m Implement Page, PaginatedProducts, ProductBrands, ProductsImages context classes.
# @todo #183:120m Implement Page, PaginatedProducts context classes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

#183 -> #207

from . import context, 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
62 changes: 62 additions & 0 deletions tests/catalog/test_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import unittest
Copy link
Collaborator

Choose a reason for hiding this comment

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

in this tests we don't document=test contexts functionality, but it's inner (and temporary because of it) realization.
I think, that tests should be based on in/out interfaces, but not on inner calls.

But maybe it's about taste and my school

Copy link
Contributor Author

@ArtemijRodionov ArtemijRodionov Nov 13, 2018

Choose a reason for hiding this comment

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

@duker33 I agree with the point, but unfortunately we still haven't Tag fixtures, so i have to either use mocked objects or create tags at a place of use. I don't like both of these approaches, but right now mocked objects look better.

But maybe it's about taste and my school

It is rather about a difference between the black box testing and the white box testing


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):

@override_settings(CATEGORY_SORTING_OPTIONS={
1: {'label': 'price', 'field': 'price', 'direction': ''}
})
def test_ordered_products(self):
products_ctx = mocked_ctx()
context.products.OrderedProducts(products_ctx, {'sorting': 1}).qs()
self.assertTrue(products_ctx.qs().order_by.called)
self.assertEqual(products_ctx.qs().order_by.call_args[0][0], 'price')

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()
raw_tags = 'test'
context.tags.ParsedTags(tags_ctx, {'tags': raw_tags}).qs()
self.assertTrue(tags_ctx.qs().parsed.called)

def test_unparsed_tags(self):
tags_ctx = mocked_ctx()
context.tags.ParsedTags(tags_ctx, {}).qs()
self.assertFalse(tags_ctx.qs().parsed.called)

def test_404_check_tags(self):
with self.assertRaises(Http404):
context.tags.Checked404Tags(mocked_ctx(qs_attrs={'exists.return_value': False})).qs()