diff --git a/.coafile b/.coafile index 2aa3a41f..47c09a69 100644 --- a/.coafile +++ b/.coafile @@ -13,7 +13,7 @@ minimum_tokens = 35 max_line_length = 100 # https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes # ignore W503 according to pep8 notes: https://goo.gl/rEjtY6 -pycodestyle_ignore = E305,W503 +pycodestyle_ignore = W503 pydocstyle_ignore = D100,D101,D102,D103,D104,D105,D106,D203,D212 language = Python 3 diff --git a/shopelectro/context.py b/shopelectro/context.py new file mode 100644 index 00000000..b9d35092 --- /dev/null +++ b/shopelectro/context.py @@ -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, + } diff --git a/shopelectro/management/commands/_update_catalog/update_tags.py b/shopelectro/management/commands/_update_catalog/update_tags.py index f3827d93..3e39459c 100644 --- a/shopelectro/management/commands/_update_catalog/update_tags.py +++ b/shopelectro/management/commands/_update_catalog/update_tags.py @@ -47,6 +47,7 @@ def get_uuid_name_pair( 'tags_data': tags_data, } + tag_file = XmlFile( fetch_callback=fetch_tags, xml_path_pattern='**/webdata/**/properties/**/import*.xml', diff --git a/shopelectro/models.py b/shopelectro/models.py index 81febc29..4c43ee8c 100644 --- a/shopelectro/models.py +++ b/shopelectro/models.py @@ -1,11 +1,17 @@ -from typing import Optional +import random +import string +from itertools import chain, groupby +from operator import attrgetter +import typing from uuid import uuid4 from django.conf import settings from django.db import models from django.urls import reverse +from django.utils.text import slugify from django.utils.translation import ugettext_lazy as _ from mptt.querysets import TreeQuerySet +from unidecode import unidecode from catalog.models import ( AbstractCategory, @@ -13,13 +19,19 @@ CategoryManager, ProductActiveManager, ProductManager, - Tag as caTag, TagGroup as caTagGroup, ) from ecommerce.models import Order as ecOrder from pages.models import CustomPage, ModelPage, Page, SyncPageMixin, PageManager +def randomize_slug(slug: str) -> str: + slug_hash = ''.join( + random.choices(string.ascii_lowercase, k=settings.SLUG_HASH_SIZE) + ) + return f'{slug}_{slug_hash}' + + class SECategoryQuerySet(TreeQuerySet): def get_categories_tree_with_pictures(self) -> 'SECategoryQuerySet': categories_with_pictures = ( @@ -109,7 +121,7 @@ def get_params(self): return Tag.objects.filter_by_products([self]).get_group_tags_pairs() def get_brand_name(self) -> str: - brand: Optional['Tag'] = Tag.objects.get_brands([self]).get(self) + brand: typing.Optional['Tag'] = Tag.objects.get_brands([self]).get(self) return brand.name if brand else '' @@ -190,14 +202,142 @@ class Meta(ModelPage.Meta): # Ignore PycodestyleBear (E303) class TagGroup(caTagGroup): - pass + + def __str__(self): + return self.name + + +class TagQuerySet(models.QuerySet): + + def filter_by_products(self, products: typing.List[Product]): + ordering = settings.TAGS_ORDER + distinct = [order.lstrip('-') for order in ordering] + + return ( + self + .filter(products__in=products) + .order_by(*ordering) + .distinct(*distinct, 'id') + ) + + def get_group_tags_pairs(self) -> typing.List[typing.Tuple[TagGroup, typing.List['Tag']]]: + grouped_tags = groupby(self.prefetch_related('group'), key=attrgetter('group')) + return [ + (group, list(tags_)) + for group, tags_ in grouped_tags + ] + + def get_brands(self, products: typing.List[Product]) -> typing.Dict[Product, 'Tag']: + brand_tags = ( + self.filter(group__name=settings.BRAND_TAG_GROUP_NAME) + .prefetch_related('products') + .select_related('group') + ) + + return { + product: brand + for brand in brand_tags for product in products + if product in brand.products.all() + } + + def as_string( # Ignore PyDocStyleBear + self, + field_name: str, + type_delimiter: str, + group_delimiter: str, + ) -> str: + """ + :param field_name: Only field's value is used to represent tag as string. + :param type_delimiter: + :param group_delimiter: + :return: + """ + if not self: + return '' + + group_tags_map = self.get_group_tags_pairs() + + _, tags_by_group = zip(*group_tags_map) + + return group_delimiter.join( + type_delimiter.join(getattr(tag, field_name) for tag in tags_list) + for tags_list in tags_by_group + ) + + def as_title(self) -> str: + return self.as_string( + field_name='name', + type_delimiter=settings.TAGS_TITLE_DELIMITER, + group_delimiter=settings.TAG_GROUPS_TITLE_DELIMITER + ) + + def as_url(self) -> str: + return self.as_string( + field_name='slug', + type_delimiter=settings.TAGS_URL_DELIMITER, + group_delimiter=settings.TAG_GROUPS_URL_DELIMITER + ) -class Tag(caTag): +class TagManager(models.Manager.from_queryset(TagQuerySet)): + + def get_queryset(self): + return ( + super().get_queryset() + .order_by(*settings.TAGS_ORDER) + ) + + def get_group_tags_pairs(self): + return self.get_queryset().get_group_tags_pairs() + + def filter_by_products(self, products): + return self.get_queryset().filter_by_products(products) + + def get_brands(self, products): + """Get a batch of products' brands.""" + return self.get_queryset().get_brands(products) + + +class Tag(models.Model): + + # Uncomment it after moving to refarm with rf#162 + # class Meta: + # unique_together = ('name', 'group') + + objects = TagManager() + + uuid = models.UUIDField(default=uuid4, editable=False) + name = models.CharField( + max_length=100, db_index=True, verbose_name=_('name')) + position = models.PositiveSmallIntegerField( + default=0, blank=True, db_index=True, verbose_name=_('position'), + ) + + # Set it as unique with rf#162 + slug = models.SlugField(default='') + group = models.ForeignKey( TagGroup, on_delete=models.CASCADE, null=True, related_name='tags', ) + def save(self, *args, **kwargs): + if not self.slug: + # same slugify code used in PageMixin object + self.slug = slugify( + unidecode(self.name.replace('.', '-').replace('+', '-')) + ) + doubled_tag_qs = self.__class__.objects.filter(slug=self.slug) + if doubled_tag_qs: + self.slug = randomize_slug(self.slug) + super(Tag, self).save(*args, **kwargs) + + @staticmethod + def parse_url_tags(tags: str) -> list: + groups = tags.split(settings.TAGS_URL_DELIMITER) + return set(chain.from_iterable( + group.split(settings.TAG_GROUPS_URL_DELIMITER) for group in groups + )) + class ExcludedModelTPageManager(PageManager): diff --git a/shopelectro/settings/base.py b/shopelectro/settings/base.py index dd2e96eb..9e50b5a8 100644 --- a/shopelectro/settings/base.py +++ b/shopelectro/settings/base.py @@ -500,4 +500,8 @@ def get_robots_content(): # Number of pagination neighbors shown for page. # If PAGINATION_NEIGHBORS = 4 and number of a page = 5, # then will be shown neighbors by number: 3, 4, 6, 7 -PAGINATION_NEIGHBORS = 4 +PAGINATION_NEIGHBORS = 10 + +PRODUCTS_ON_PAGE_PC = 48 +PRODUCTS_ON_PAGE_MOB = 12 + diff --git a/shopelectro/tests/tests_views.py b/shopelectro/tests/tests_views.py index 4eb01da3..ca12b5df 100644 --- a/shopelectro/tests/tests_views.py +++ b/shopelectro/tests/tests_views.py @@ -18,9 +18,9 @@ from django.test import TestCase from django.urls import reverse from django.utils.translation import ugettext as _ -from catalog.models import TagQuerySet, serialize_tags_to_url -from shopelectro.models import Category, Product, Tag, TagGroup +from shopelectro import context +from shopelectro.models import Category, Product, Tag, TagGroup, TagQuerySet from shopelectro.views.service import generate_md5_for_ya_kassa, YANDEX_REQUEST_PARAM from shopelectro.tests.helpers import create_doubled_tag @@ -39,7 +39,7 @@ def reverse_catalog_url( if tags: # PyCharm's option: # noinspection PyTypeChecker - tags_slug = serialize_tags_to_url(tags) + tags_slug = tags.as_url() route_kwargs['tags'] = tags_slug if sorting is not None: route_kwargs['sorting'] = sorting @@ -76,6 +76,19 @@ def get_category_page( )) +class CatalogPage(BaseCatalogTestCase): + + def test_merge_product_cache(self): + """Context merging should cached.""" + products = Product.objects.all()[:2] + with self.assertNumQueries(7): + # N db queries without before cached + context.prepare_tile_products(products) + with self.assertNumQueries(0): + # no db queries after cached + context.prepare_tile_products(products) + + class CatalogTags(BaseCatalogTestCase): def test_category_page_contains_all_tags(self): diff --git a/shopelectro/views/catalog.py b/shopelectro/views/catalog.py index b5efcae3..fe3bef58 100644 --- a/shopelectro/views/catalog.py +++ b/shopelectro/views/catalog.py @@ -1,19 +1,16 @@ import typing -from functools import partial from django import http from django.conf import settings -from django.core.paginator import Paginator, InvalidPage +from django.core.paginator import Paginator from django.shortcuts import render, get_object_or_404 from django.views.decorators.http import require_POST from django_user_agents.utils import get_user_agent -from catalog import models as ca_models from catalog.views import catalog -from images.models import Image from pages import views as pages_views -from shopelectro import models +from shopelectro import context, models from shopelectro.views.helpers import set_csrf_cookie PRODUCTS_ON_PAGE_PC = 48 @@ -26,74 +23,11 @@ def get_products_count(request): return PRODUCTS_ON_PAGE_MOB if mobile_view else PRODUCTS_ON_PAGE_PC -# @todo #539:60m Move PaginatorLinks to refarm-site. -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} - - -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 - - # CATALOG VIEWS class CategoryTree(catalog.CategoryTree): category_model = models.Category -def prepare_tile_products(products): - images = Image.objects.get_main_images_by_pages( - models.ProductPage.objects.filter( - shopelectro_product__in=products - ) - ) - categories = models.Category.objects.get_root_categories_by_products( - products - ) - return [ - (product, images.get(product.page), categories.get(product)) - for product in products - ] - - @set_csrf_cookie class ProductPage(catalog.ProductPage): pk_url_kwarg = None @@ -121,18 +55,18 @@ def get(self, request, *args, **kwargs): return self.render_to_response(context) def get_context_data(self, **kwargs): - context = super(ProductPage, self).get_context_data(**kwargs) + context_ = super(ProductPage, self).get_context_data(**kwargs) product = self.object if not product.page.is_active: # this context required to render 404 page # with it's own logic - return context + return context_ return { - **context, + **context_, 'price_bounds': settings.PRICE_BOUNDS, 'group_tags_pairs': product.get_params(), - 'tile_products': prepare_tile_products( + 'tile_products': context.prepare_tile_products( product.get_siblings(offset=settings.PRODUCT_SIBLINGS_COUNT) ), } @@ -148,9 +82,9 @@ def render_siblings_on_404( ).first() if inactive_product: self.object = inactive_product - context = self.get_context_data( + context_ = self.get_context_data( object=inactive_product, - tile_products=prepare_tile_products( + tile_products=context.prepare_tile_products( inactive_product.get_siblings( offset=settings.PRODUCT_SIBLINGS_COUNT ) @@ -158,7 +92,7 @@ def render_siblings_on_404( tile_title='Возможно вас заинтересуют похожие товары:', **url_kwargs, ) - return render(request, 'catalog/product_404.html', context, status=404) + return render(request, 'catalog/product_404.html', context_, status=404) # SHOPELECTRO-SPECIFIC VIEWS @@ -167,7 +101,7 @@ class IndexPage(pages_views.CustomPageView): def get_context_data(self, **kwargs): """Extended method. Add product's images to context.""" - context = super(IndexPage, self).get_context_data(**kwargs) + context_ = super(IndexPage, self).get_context_data(**kwargs) mobile_view = get_user_agent(self.request).is_mobile tile_products = [] @@ -178,119 +112,31 @@ def get_context_data(self, **kwargs): .prefetch_related('category') .select_related('page') ) - tile_products = prepare_tile_products(top_products) + tile_products = context.prepare_tile_products(top_products) return { - **context, + **context_, 'tile_title': 'ТОП 10 ТОВАРОВ', 'category_tile': settings.MAIN_PAGE_TILE, 'tile_products': tile_products, } -def merge_products_context(products): - 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 - ] - - @set_csrf_cookie class CategoryPage(catalog.CategoryPage): def get_context_data(self, **kwargs): """Add sorting options and view_types in context.""" - context = super().get_context_data(**kwargs) - products_on_page = int(self.request.GET.get( - 'step', get_products_count(self.request), - )) - page_number = int(self.request.GET.get('page', 1)) - view_type = self.request.session.get('view_type', 'tile') - sorting_index = int(self.kwargs.get('sorting', 0)) - sorting_option = SortingOption(index=sorting_index) - category = context['category'] - if ( - page_number < 1 or - products_on_page not in settings.CATEGORY_STEP_MULTIPLIERS - ): - raise http.Http404('Page does not exist.') # Ignore CPDBear - - all_products = models.Product.actives.get_category_descendants( - category, ordering=(sorting_option.directed_field, ) + context_ = ( + context.Category(self.kwargs, self.request) + | context.TaggedCategory() + | context.SortingCategory() + | context.PaginationCategory() # requires SortingCategory + | context.DBTemplate() # requires TaggedCategory ) - - group_tags_pairs = ( - models.Tag.objects - .filter_by_products(all_products) - .get_group_tags_pairs() - ) - - tags = self.kwargs.get('tags') - - tag_titles = '' - if tags: - slugs = models.Tag.parse_url_tags(tags) - tags = models.Tag.objects.filter(slug__in=slugs) - - if not tags: - raise http.Http404('No such tag.') - - all_products = ( - all_products - .filter(tags__in=tags) - # Use distinct because filtering by QuerySet tags, - # that related with products by many-to-many relation. - .distinct(sorting_option.field) - ) - - tag_titles = ca_models.serialize_tags_to_title(tags) - - def template_context(page, tag_titles, tags): - return { - 'page': page, - 'tag_titles': tag_titles, - 'tags': tags, - } - - page = context['page'] - page.get_template_render_context = partial( - template_context, page, tag_titles, tags) - - paginated = PaginatorLinks( - page_number, - self.request.path, - Paginator(all_products, products_on_page) - ) - paginated_page = paginated.page() - - total_products = all_products.count() - products_on_page = paginated_page.object_list - if not products_on_page: - raise http.Http404('Page without products does not exist.') - return { - **context, - 'products_data': merge_products_context(products_on_page), - 'group_tags_pairs': group_tags_pairs, - 'total_products': total_products, - 'paginated': paginated, - 'paginated_page': paginated_page, - 'sorting_options': settings.CATEGORY_SORTING_OPTIONS.values(), - 'limits': settings.CATEGORY_STEP_MULTIPLIERS, - 'sort': sorting_index, - 'tags': tags, - 'view_type': view_type, - 'skip_canonical': bool(tags), + **super().get_context_data(**kwargs), + **context_.get_context_data(), } @@ -320,7 +166,7 @@ def load_more(request, category_slug, offset=0, limit=0, sorting=0, tags=None): # 12 // 12 = 1, 23 // 12 = 1, but it should be the second page page_number = (offset // products_on_page) + 1 category = get_object_or_404(models.CategoryPage, slug=category_slug).model - sorting_option = SortingOption(index=int(sorting)) + sorting_option = context.SortingOption(index=int(sorting)) all_products = models.Product.actives.get_category_descendants( category, ordering=(sorting_option.directed_field,) @@ -339,7 +185,7 @@ def load_more(request, category_slug, offset=0, limit=0, sorting=0, tags=None): .distinct(sorting_option.field) ) - paginated = PaginatorLinks( + paginated = context.PaginatorLinks( page_number, request.path, Paginator(all_products, products_on_page) @@ -349,7 +195,7 @@ def load_more(request, category_slug, offset=0, limit=0, sorting=0, tags=None): view = request.session.get('view_type', 'tile') return render(request, 'catalog/category_products.html', { - 'products_data': merge_products_context(products), + 'products_data': context.prepare_tile_products(products), 'paginated': paginated, 'paginated_page': paginated_page, 'view_type': view,