Skip to content

Commit

Permalink
#183 object compositions (#194)
Browse files Browse the repository at this point in the history
* Create catalog.context package

* Create context package

* Specify context ABS classes

* Implement base product contexts classes

* Implement base tag contexts classes

* Resolve todo and create another one for  ordering arg

* Self-review fixes

* Review fixes

* Self-review fixes
  • Loading branch information
ArtemijRodionov authored Nov 4, 2018
1 parent a799c38 commit fb2fa2a
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 5 deletions.
6 changes: 2 additions & 4 deletions catalog/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,8 @@ def products(self):
products = (
products
.filter(tags__in=tags)
# Use distinct because filtering by QuerySet tags,
# that related with products by many-to-many relation.
# @todo #550:60m Try to rm sorting staff from context.TaggedCategory.
# Or explain again why it's impossible. Now it's not clear from comment.
# See the ProductQuerySet.tagged
# for detail about `distinct` and `order_by` above
.distinct(*self.get_undirected_sorting_options())
.order_by(*self.get_undirected_sorting_options())
)
Expand Down
1 change: 1 addition & 0 deletions catalog/context/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @todo #183:120m Implement Page, PaginatedProducts, ProductBrands, ProductsImages context classes.
55 changes: 55 additions & 0 deletions catalog/context/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import abc
import collections
import typing

from django.db.models import QuerySet


class Context(abc.ABC):

@abc.abstractmethod
def context(self) -> typing.Dict[str, typing.Any]:
...


class ModelContext(abc.ABC):

def __init__(self, qs: QuerySet):
self._qs = qs

def qs(self):
return self._qs

@abc.abstractmethod
def context(self) -> typing.Dict[str, typing.Any]:
...


Context.register(ModelContext)


class Contexts(Context):

def __init__(self, *contexts: typing.List[Context]):
self.contexts = contexts

def context(self):
return collections.ChainMap(
*[ctx.context() for ctx in self.contexts]
)


class Tags(ModelContext):

def context(self):
return {
'tags': self.qs(),
}


class Products(ModelContext):

def context(self):
return {
'products': self.qs(),
}
57 changes: 57 additions & 0 deletions catalog/context/products.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import typing

from django.db.models import QuerySet

from catalog.context.context import Products, Tags
from catalog.models import AbstractCategory


class ActiveProducts(Products):

def __init__(self, products: Products):
self._products = products

def qs(self):
return self._products.qs().active()


class OrderedProducts(Products):

def __init__(self, products: Products, req_kwargs):
self._products = products
self._sorting_index = self._req_kwargs.get('sorting', 0)

def qs(self):
return self._products.qs().order_by(
SortingOption(index=self._sorting_index).directed_field,
)

def context(self):
return {
**super().context(),
'sorting_index': self._sorting_index,
}


class ProductsByCategory(Products):

def __init__(self, products: Products, category: AbstractCategory):
self._products = products
self._category = category

def qs(self):
return self._products.qs().get_category_descendants(self._category)


class ProductsByTags(Products):

def __init__(self, products: Products, tags_context: Tags):
self._products = products
self._tags = tags

def qs(self):
tags = self._tags.qs()
if tags.exists():
return self._products.qs().tagged(tags)
else:
return self._products.qs()
39 changes: 39 additions & 0 deletions catalog/context/tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from catalog.context.context import Context, Tags, Products

from django.db.models import QuerySet


class GroupedTags(Context):

def __init__(self, tags: Tags):
self._tags = tags

def context(self):
return {
'group_tags_pairs': self._tags.qs().get_group_tags_pairs(),
}


class ParsedTags(Tags):

def __init__(self, tags: Tags, req_kwargs):
self._tags = tags
self._raw_tags = req_kwargs.get('tags')

def qs(self):
tags = self._tags.qs()
if not self._raw_tags:
tags.none()
return tags.parsed(self._raw_tags)


class Checked404Tags(Tags):

def __init__(self, tags: Tags):
self._tags = tags

def qs(self):
tags = self._tags.qs()
if not tags.exists():
raise http.Http404('No such tag.')
return tags
23 changes: 22 additions & 1 deletion catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class ProductQuerySet(models.QuerySet):
def get_offset(self, start, step):
return self[start:start + step]

# @todo #183:30m Try to remove `ordering` arg from ProductQuerySet.get_by_category
def get_by_category(self, category: models.Model, ordering: [str]=None) -> models.QuerySet:
ordering = ordering or ['name']
categories = category.get_descendants(True)
Expand All @@ -124,6 +125,17 @@ def filter_by_categories(self, categories: Iterable[AbstractCategory]):
def active(self):
return self.filter(page__is_active=True)

def tagged(self, tags):
# Distinct because a relation of tags and products is M2M.
# We do not specify the args for `distinct` to avoid dependencies
# between `order_by` and `distinct` methods.

# Postgres has `SELECT DISTINCT ON`, that depends on `ORDER BY`.
# 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()


class ProductManager(models.Manager.from_queryset(ProductQuerySet)):
"""Get all products of given category by Category's id or instance."""
Expand All @@ -142,6 +154,9 @@ def get_category_descendants(
def active(self):
return self.get_queryset().active()

def tagged(self, tags):
return self.get_queryset().tagged(tags)


class AbstractProduct(models.Model, AdminTreeDisplayMixin):
"""
Expand Down Expand Up @@ -241,7 +256,7 @@ def get_group_tags_pairs(self) -> List[Tuple[TagGroup, List['Tag']]]:
for group, tags_ in grouped_tags
]

def get_brands(self, products: List[AbstractProduct]) -> Dict[AbstractProduct, 'Tag']:
def get_brands(self, products: Iterable[AbstractProduct]) -> Dict[AbstractProduct, 'Tag']:
brand_tags = (
self.filter(group__name=settings.BRAND_TAG_GROUP_NAME)
.prefetch_related('products')
Expand Down Expand Up @@ -324,6 +339,9 @@ def serialize_tags_to_title(self) -> str:
group_delimiter=settings.TAG_GROUPS_TITLE_DELIMITER
)

def parsed(self, raw: str):
return self.filter(slug__in=Tag.parse_url_tags(raw))


class TagManager(models.Manager.from_queryset(TagQuerySet)):

Expand All @@ -343,6 +361,9 @@ def get_brands(self, products):
"""Get a batch of products' brands."""
return self.get_queryset().get_brands(products)

def parsed(self, raw):
return self.get_queryset().parsed(raw)


class Tag(models.Model):

Expand Down

3 comments on commit fb2fa2a

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on fb2fa2a Nov 4, 2018

Choose a reason for hiding this comment

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

Puzzle 550-035b850e disappeared from catalog/context.py, that's why I closed #186. 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 fb2fa2a Nov 4, 2018

Choose a reason for hiding this comment

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

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

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on fb2fa2a Nov 4, 2018

Choose a reason for hiding this comment

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

Puzzle 183-a9ba47b7 discovered in catalog/models.py and submitted as #208. 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.