Skip to content

Commit

Permalink
rf#168 Prepare context module for moving to refarm (#573)
Browse files Browse the repository at this point in the history
* rf#168  Inject products queryset to catalog context

* rf#168  Fix se#550's rebase. Inject tag qs tag context

* rf#168  Move tag_pairs stuff from catalog context to tags one

* rf#168  Broken code. Move to QS receive

* rf#168  Fix distinct-order_by bug

* rf#168  Implement new interface for `context.prepare_products`

* #550  Add minor pdd issue

* #550  Pdd issue to move context to refarm.catalog app

* rf#168  Grade refarm dep, minor fix in test_views

* rf#168  Apply linter rules
  • Loading branch information
duker33 authored Sep 16, 2018
1 parent 8452d76 commit b175023
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 207 deletions.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ ua-parser==0.8.0
user-agents==1.1.0
sorl-thumbnail==12.4a1
https://github.com/selwin/django-user_agents/archive/master.zip
https://github.com/fidals/refarm-site/archive/0.3.0.zip
https://github.com/fidals/refarm-site/archive/0.4.0.zip
169 changes: 126 additions & 43 deletions shopelectro/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,23 @@
>>> Category(url_kwargs, request=HttpRequest()) | TaggedCategory()
"""

# @todo #550:60m Move context module to refarm.catalog app

import typing
from abc import ABC, abstractmethod
from collections import defaultdict
from functools import lru_cache, partial

from django import http
from django.db.models import QuerySet
from django.conf import settings
from django.db.models import QuerySet
from django.core.paginator import Paginator, InvalidPage
from django_user_agents.utils import get_user_agent

from catalog.models import ProductQuerySet, Tag, TagQuerySet
from images.models import Image
from pages.models import ModelPage

from shopelectro import models


class SortingOption:
def __init__(self, index=0):
Expand Down Expand Up @@ -82,18 +84,22 @@ def number_url_map(self):

# @todo #550:30m Split to ProductImagesContext and ProductBrandContext
@lru_cache(maxsize=64)
def prepare_tile_products(products: QuerySet):
assert isinstance(products, QuerySet)
def prepare_tile_products(
products: ProductQuerySet, product_pages: QuerySet, tags: TagQuerySet=None
):
# @todo #550:60m Move prepare_tile_products func to context
# Now it's separated function with huge of inconsistent queryset deps.
assert isinstance(products, ProductQuerySet)

images = Image.objects.get_main_images_by_pages(
models.ProductPage.objects.filter(shopelectro_product__in=products)
product_pages.filter(shopelectro_product__in=products)
)

brands = (
models.Tag.objects
tags
.filter_by_products(products)
.get_brands(products)
)
) if tags else defaultdict(lambda: None)

return [
(product, images.get(product.page), brands.get(product))
Expand Down Expand Up @@ -177,18 +183,40 @@ class AbstractProductsListContext(AbstractPageContext, ABC):

super: 'AbstractProductsListContext' = None

def __init__( # Ignore PyDocStyleBear
self,
url_kwargs: typing.Dict[str, str]=None,
request: http.HttpRequest=None,
products: ProductQuerySet=None,
product_pages: QuerySet=None,
):
"""
:param url_kwargs: Came from `urls` module.
:param request: Came from `urls` module.
:param products: Every project provides products from DB.
"""
super().__init__(url_kwargs, request)
self.products_ = products
self.product_pages_ = product_pages

@property
def products(self) -> QuerySet:
def product_pages(self) -> QuerySet:
return self.product_pages_ or self.super.product_pages

@property
def products(self) -> ProductQuerySet:
if self.super:
return self.super.products
elif self.products_:
return self.products_
else:
raise NotImplementedError
raise NotImplementedError('Set products queryset')


class Category(AbstractProductsListContext):
@property
def products(self) -> QuerySet:
return models.Product.actives.get_category_descendants(
def products(self) -> ProductQuerySet:
return super().products.active().get_category_descendants(
self.page.model
)

Expand All @@ -198,32 +226,47 @@ def get_context_data(self):
# 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,
'products_data': prepare_tile_products(self.products, self.product_pages),
# can be `tile` or `list`. Defines products list layout.
'view_type': view_type,
}


class TaggedCategory(AbstractProductsListContext):

def __init__( # Ignore PyDocStyleBear
self,
url_kwargs: typing.Dict[str, str]=None,
request: http.HttpRequest=None,
products: ProductQuerySet=None,
tags: TagQuerySet=None
):
"""
:param url_kwargs: Came from `urls` module.
:param request: Came from `urls` module.
:param products: Every project provides products from DB.
:param tags: Every project provides tags from DB.
"""
super().__init__(url_kwargs, request, products)
# it's not good. Arg should not be default.
# That's how we'll prevent assertion.
# But we'll throw away inheritance in se#567.
assert tags, 'tags is required arg'
self.tags_ = tags

def get_sorting_index(self):
return int(self.url_kwargs.get('sorting', 0))

def get_tags(self) -> typing.Optional[models.TagQuerySet]:
# @todo #550:15m Move `TaggedCategory.get_tags` to property.
# As in `products` property case.
def get_tags(self) -> typing.Optional[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)
slugs = Tag.parse_url_tags(request_tags)
tags = self.tags_.filter(slug__in=slugs)
if not tags:
raise http.Http404('No such tag.')
return tags
Expand All @@ -242,15 +285,28 @@ def products(self):
# @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)
.order_by(sorting_option.field)
)
return products

def get_context_data(self):
context = self.super.get_context_data()
tags = self.get_tags()
group_tags_pairs = (
self.tags_
.filter_by_products(self.products)
.get_group_tags_pairs()
)
return {
**context,
'tags': tags,
'group_tags_pairs': group_tags_pairs,
'products_data': prepare_tile_products(
self.products,
self.product_pages,
# requires all tags, not only selected
self.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.
Expand Down Expand Up @@ -301,7 +357,7 @@ def get_sorting_index(self):
return int(self.url_kwargs.get('sorting', 0))

@property
def products(self) -> QuerySet:
def products(self) -> ProductQuerySet:
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)
Expand All @@ -310,7 +366,9 @@ def get_context_data(self):
context = self.super.get_context_data()
return {
**context,
'products_data': prepare_tile_products(self.products),
'products_data': prepare_tile_products(
self.products, self.product_pages
),
'sort': self.get_sorting_index(),
}

Expand All @@ -322,45 +380,70 @@ def get_products_count(self):
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):
def get_paginated_page_or_404(self, per_page, page_number) -> Paginator:
try:
return Paginator(self.products, per_page).page(page_number)
return Paginator(self.all_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(
@property
def products_on_page(self):
return int(self.request.GET.get(
'step', self.get_products_count(),
))
page_number = int(self.request.GET.get('page', 1))

@property
def page_number(self):
return int(self.request.GET.get('page', 1))

@property
def all_products(self) -> ProductQuerySet:
return self.super.products

@property
def products(self) -> ProductQuerySet:
"""Only products for current page."""
paginated_page = self.get_paginated_page_or_404(
self.products_on_page, self.page_number
)
# it's queryset, but it's sliced
products: ProductQuerySet = paginated_page.object_list
return products

@property
def products_count(self):
return (self.page_number - 1) * self.products_on_page + self.products.count()

def check_pagination_args(self):
if (
page_number < 1 or
products_on_page not in settings.CATEGORY_STEP_MULTIPLIERS
self.page_number < 1 or
self.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:
def get_context_data(self):
context = self.super.get_context_data()
self.check_pagination_args()

if not self.products:
raise http.Http404('Page without products does not exist.')

paginated = PaginatorLinks(
page_number,
self.page_number,
self.request.path,
Paginator(self.products, products_on_page)
Paginator(self.all_products, self.products_on_page)
)
paginated_page = paginated.page()

total_products = self.all_products.count()

return {
**context,
'products_data': prepare_tile_products(products),
'products_data': prepare_tile_products(
self.products, self.product_pages
),
'total_products': total_products,
'products_count': (page_number - 1) * products_on_page + products.count(),
'products_count': self.products_count,
'paginated': paginated,
'paginated_page': paginated_page,
'sorting_options': settings.CATEGORY_SORTING_OPTIONS.values(),
Expand Down
Loading

3 comments on commit b175023

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on b175023 Sep 16, 2018

Choose a reason for hiding this comment

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

Puzzle 550-6f1a456c discovered in shopelectro/context.py and submitted as #577. 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 b175023 Sep 16, 2018

Choose a reason for hiding this comment

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

Puzzle 550-f02726e9 discovered in shopelectro/context.py and submitted as #578. 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 b175023 Sep 16, 2018

Choose a reason for hiding this comment

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

Puzzle 550-79bb63d1 discovered in shopelectro/context.py and submitted as #579. 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.