-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* #550 Review#3 fixes. Rm get_tags caching #550 Resolve branch inner conflicts #550 Apply linter rules #550 Review#2 fixes. Rm redundant pdd issue, fix imports #550 Rm redundant pdd issue #550 Pdd issue about context module improving #550 Merge fixes after hell #550 Minor self-review fixes #550 Add code example for creating context #550 Make context names shorten #550 Fork AbstractPageContext #550 Cleanup code #550 Fork PaginationCategoryContext class #550 Fork SortingCategoryContext class #550 Fork DBTemplateContext class. Improve pipe mech #550 Fork TaggedCategoryContext class #550 Implement CatalogContext #550 Fork SortingCategoryPage class #550 Fork PaginatedCatalogPage class #550 Fork DBContextCatalogPage class #548 Apply E305 linter rule #550 Review#1 fixes. Cache in memory db queries #548 Rm E305 pycodestyle rule #550 Add pdd issue for continue splitting #550 Create TaggedCategoryPage class * #550 Merge fixes * #550 Fix shadowed var * #550 Apply linter rules after rebase
- Loading branch information
Showing
7 changed files
with
559 additions
and
187 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,368 @@ | ||
""" | ||
Contains view context classes. | ||
Context is our own concept. | ||
It's part of the View abstract layer in MTV paradigm. | ||
Data flow looks like this: | ||
Urls -> Context -> View. | ||
So, by concept, View receives context directly | ||
without accessing url args. | ||
It stands between urls and view functions. | ||
Every context class is used via objects composition. | ||
Code example to create tagged category: | ||
>>> from django.http import HttpRequest | ||
>>> url_kwargs = {'slug': 'my-page'} | ||
>>> Category(url_kwargs, request=HttpRequest()) | TaggedCategory() | ||
""" | ||
|
||
import typing | ||
from abc import ABC, abstractmethod | ||
from functools import lru_cache, partial | ||
|
||
from django import http | ||
from django.db.models import QuerySet | ||
from django.conf import settings | ||
from django.core.paginator import Paginator, InvalidPage | ||
from django_user_agents.utils import get_user_agent | ||
|
||
from images.models import Image | ||
from pages.models import ModelPage | ||
|
||
from shopelectro import models | ||
|
||
|
||
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 PaginatorLinks: | ||
|
||
def __init__(self, number, path, paginated: Paginator): | ||
self.paginated = paginated | ||
self.number = number | ||
self.path = path | ||
|
||
self.index = number - 1 | ||
self.neighbor_bounds = settings.PAGINATION_NEIGHBORS // 2 | ||
self.neighbor_range = list(self.paginated.page_range) | ||
|
||
def page(self): | ||
try: | ||
return self.paginated.page(self.number) | ||
except InvalidPage: | ||
raise http.Http404('Page does not exist') | ||
|
||
def showed_number(self): | ||
return self.index * self.paginated.per_page + self.page().object_list.count() | ||
|
||
def _url(self, number): | ||
self.paginated.validate_number(number) | ||
return self.path if number == 1 else f'{self.path}?page={number}' | ||
|
||
def prev_numbers(self): | ||
return self.neighbor_range[:self.index][-self.neighbor_bounds:] | ||
|
||
def next_numbers(self): | ||
return self.neighbor_range[self.index + 1:][:self.neighbor_bounds] | ||
|
||
def number_url_map(self): | ||
numbers = self.prev_numbers() + self.next_numbers() | ||
return {number: self._url(number) for number in numbers} | ||
|
||
|
||
# @todo #550:30m Split to ProductImagesContext and ProductBrandContext | ||
@lru_cache(maxsize=64) | ||
def prepare_tile_products(products: QuerySet): | ||
assert isinstance(products, QuerySet) | ||
|
||
images = Image.objects.get_main_images_by_pages( | ||
models.ProductPage.objects.filter(shopelectro_product__in=products) | ||
) | ||
|
||
brands = ( | ||
models.Tag.objects | ||
.filter_by_products(products) | ||
.get_brands(products) | ||
) | ||
|
||
return [ | ||
(product, images.get(product.page), brands.get(product)) | ||
for product in products | ||
] | ||
|
||
|
||
class ObjectsComposition: | ||
|
||
super: 'ObjectsComposition' = None | ||
|
||
def __or__(self, other: 'ObjectsComposition'): | ||
other.super = self | ||
return other | ||
|
||
|
||
# @todo #550:120m Move context realization to pure to objects composition. | ||
# Discussed some thoughts with Artemiy via call. | ||
# Artemiy will do it. | ||
# For example SortedCategory should | ||
# consist of separated SortedList and Category classes/objects. | ||
class AbstractContext(ObjectsComposition, ABC): | ||
|
||
super: 'AbstractContext' = None | ||
|
||
def __init__( # Ignore PyDocStyleBear | ||
self, | ||
url_kwargs: typing.Dict[str, str]=None, | ||
request: http.HttpRequest=None | ||
): | ||
""" | ||
:param url_kwargs: Came from `urls` module. | ||
:param request: Came from `urls` module | ||
""" | ||
|
||
self.url_kwargs_ = url_kwargs or {} | ||
self.request_ = request | ||
|
||
@property | ||
def url_kwargs(self) -> typing.Dict[str, str]: | ||
return self.url_kwargs_ or self.super.url_kwargs | ||
|
||
@property | ||
def request(self) -> http.HttpRequest: | ||
return self.request_ or self.super.request | ||
|
||
@abstractmethod | ||
def get_context_data(self) -> typing.Dict[str, typing.Any]: | ||
... | ||
|
||
|
||
class AbstractPageContext(AbstractContext, ABC): | ||
|
||
super: 'AbstractPageContext' = None | ||
|
||
def __init__( # Ignore PyDocStyleBear | ||
self, | ||
url_kwargs: typing.Dict[str, str]=None, | ||
request: http.HttpRequest=None | ||
): | ||
""" | ||
:param url_kwargs: Came from `urls` module. | ||
:param request: Came from `urls` module | ||
""" | ||
|
||
if url_kwargs: | ||
assert 'slug' in url_kwargs | ||
super().__init__(url_kwargs, request) | ||
|
||
@property | ||
def slug(self) -> str: | ||
return self.url_kwargs['slug'] | ||
|
||
@property | ||
@lru_cache(maxsize=1) | ||
def page(self): | ||
return ModelPage.objects.get(slug=self.slug) | ||
|
||
|
||
class AbstractProductsListContext(AbstractPageContext, ABC): | ||
|
||
super: 'AbstractProductsListContext' = None | ||
|
||
@property | ||
def products(self) -> QuerySet: | ||
if self.super: | ||
return self.super.products | ||
else: | ||
raise NotImplementedError | ||
|
||
|
||
class Category(AbstractProductsListContext): | ||
@property | ||
def products(self) -> QuerySet: | ||
return models.Product.actives.get_category_descendants( | ||
self.page.model | ||
) | ||
|
||
def get_context_data(self): | ||
"""Add sorting options and view_types in context.""" | ||
# @todo #550:15m Take `view_type` value from dataclass. | ||
# Depends on updating to python3.7 | ||
view_type = self.request.session.get('view_type', 'tile') | ||
|
||
group_tags_pairs = ( | ||
models.Tag.objects | ||
.filter_by_products(self.products) | ||
.get_group_tags_pairs() | ||
) | ||
|
||
return { | ||
'products_data': prepare_tile_products(self.products), | ||
'group_tags_pairs': group_tags_pairs, | ||
# can be `tile` or `list`. Defines products list layout. | ||
'view_type': view_type, | ||
} | ||
|
||
|
||
class TaggedCategory(AbstractProductsListContext): | ||
|
||
def get_sorting_index(self): | ||
return int(self.url_kwargs.get('sorting', 0)) | ||
|
||
def get_tags(self) -> typing.Optional[models.TagQuerySet]: | ||
request_tags = self.url_kwargs.get('tags') | ||
if not request_tags: | ||
return None | ||
|
||
slugs = models.Tag.parse_url_tags(request_tags) | ||
tags = models.Tag.objects.filter(slug__in=slugs) | ||
if not tags: | ||
raise http.Http404('No such tag.') | ||
return tags | ||
|
||
@property | ||
def products(self): | ||
products = self.super.products | ||
sorting_option = SortingOption(index=self.get_sorting_index()) | ||
tags = self.get_tags() | ||
if tags: | ||
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. | ||
.distinct(sorting_option.field) | ||
) | ||
return products | ||
|
||
def get_context_data(self): | ||
context = self.super.get_context_data() | ||
tags = self.get_tags() | ||
return { | ||
**context, | ||
'tags': tags, | ||
# Category's canonical link is `category.page.get_absolute_url`. | ||
# So, this link always contains no tags. | ||
# That's why we skip canonical link on tagged category page. | ||
'skip_canonical': bool(tags), | ||
} | ||
|
||
|
||
class DBTemplate(AbstractPageContext): | ||
"""Processes some page data fields as templates with their own context.""" | ||
|
||
@property | ||
@lru_cache(maxsize=1) | ||
def page(self): | ||
page = self.super.page | ||
context = self.get_super_context_data_cached() | ||
|
||
def template_context(page, tag_titles, tags): | ||
return { | ||
'page': page, | ||
'tag_titles': tag_titles, | ||
'tags': tags, | ||
} | ||
|
||
tags = context['tags'] | ||
if tags: | ||
tag_titles = tags.as_title() | ||
page.get_template_render_context = partial( | ||
template_context, page, tag_titles, tags | ||
) | ||
|
||
return page | ||
|
||
@lru_cache(maxsize=1) | ||
def get_super_context_data_cached(self): | ||
return self.super.get_context_data() | ||
|
||
@lru_cache(maxsize=1) | ||
def get_context_data(self): | ||
return { | ||
**self.get_super_context_data_cached(), | ||
'page': self.page, | ||
} | ||
|
||
|
||
class SortingCategory(AbstractProductsListContext): | ||
|
||
def get_sorting_index(self): | ||
return int(self.url_kwargs.get('sorting', 0)) | ||
|
||
@property | ||
def products(self) -> QuerySet: | ||
sorting_index = int(self.url_kwargs.get('sorting', 0)) | ||
sorting_option = SortingOption(index=sorting_index) | ||
return self.super.products.order_by(sorting_option.directed_field) | ||
|
||
def get_context_data(self): | ||
context = self.super.get_context_data() | ||
return { | ||
**context, | ||
'products_data': prepare_tile_products(self.products), | ||
'sort': self.get_sorting_index(), | ||
} | ||
|
||
|
||
class PaginationCategory(AbstractProductsListContext): | ||
|
||
def get_products_count(self): | ||
"""Calculate max products list size from request. List size depends on device type.""" | ||
mobile_view = get_user_agent(self.request).is_mobile | ||
return settings.PRODUCTS_ON_PAGE_MOB if mobile_view else settings.PRODUCTS_ON_PAGE_PC | ||
|
||
def get_paginated_page_or_404(self, per_page, page_number): | ||
try: | ||
return Paginator(self.products, per_page).page(page_number) | ||
except InvalidPage: | ||
raise http.Http404('Page does not exist') | ||
|
||
def get_context_data(self): | ||
context = self.super.get_context_data() | ||
products_on_page = int(self.request.GET.get( | ||
'step', self.get_products_count(), | ||
)) | ||
page_number = int(self.request.GET.get('page', 1)) | ||
|
||
if ( | ||
page_number < 1 or | ||
products_on_page not in settings.CATEGORY_STEP_MULTIPLIERS | ||
): | ||
raise http.Http404('Page does not exist.') # Ignore CPDBear | ||
|
||
paginated_page = self.get_paginated_page_or_404( | ||
products_on_page, page_number | ||
) | ||
total_products = self.products.count() | ||
products = paginated_page.object_list | ||
if not products: | ||
raise http.Http404('Page without products does not exist.') | ||
|
||
paginated = PaginatorLinks( | ||
page_number, | ||
self.request.path, | ||
Paginator(self.products, products_on_page) | ||
) | ||
paginated_page = paginated.page() | ||
|
||
return { | ||
**context, | ||
'products_data': prepare_tile_products(products), | ||
'total_products': total_products, | ||
'products_count': (page_number - 1) * products_on_page + products.count(), | ||
'paginated': paginated, | ||
'paginated_page': paginated_page, | ||
'sorting_options': settings.CATEGORY_SORTING_OPTIONS.values(), | ||
'limits': settings.CATEGORY_STEP_MULTIPLIERS, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
9399dd6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Puzzle
539-e6f6dde5
disappeared fromshopelectro/views/catalog.py
, that's why I closed #556. Please, remember that the puzzle was not necessarily removed in this particular commit. Maybe it happened earlier, but we discovered this fact only now.9399dd6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Puzzle
550-87015712
discovered inshopelectro/context.py
and submitted as #566. Please, remember that the puzzle was not necessarily added in this particular commit. Maybe it was added earlier, but we discovered it only now.9399dd6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Puzzle
550-c08f412e
discovered inshopelectro/context.py
and submitted as #567. Please, remember that the puzzle was not necessarily added in this particular commit. Maybe it was added earlier, but we discovered it only now.9399dd6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Puzzle
550-6e482196
discovered inshopelectro/context.py
and submitted as #568. Please, remember that the puzzle was not necessarily added in this particular commit. Maybe it was added earlier, but we discovered it only now.9399dd6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Puzzle
550-035b850e
discovered inshopelectro/context.py
and submitted as #569. Please, remember that the puzzle was not necessarily added in this particular commit. Maybe it was added earlier, but we discovered it only now.