Skip to content

Commit

Permalink
#525 Create Tag.get_brands and related tests (#531)
Browse files Browse the repository at this point in the history
Implement js-part for brand extraction

Self-review fixes

Fix test

Apply linter rules

The first review fixes

Create todo for Products.get_brands()
  • Loading branch information
ArtemijRodionov committed Aug 22, 2018
1 parent d5c1803 commit 64939d2
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 40 deletions.
1 change: 1 addition & 0 deletions front/js/components/category.es6
Expand Up @@ -168,6 +168,7 @@
name: $product.attr('productName'),
quantity: parseInt(quantity, 10),
category: DOM.$h1.data('name'),
brand: $product.attr('productBrand'),
};
};

Expand Down
6 changes: 4 additions & 2 deletions front/js/components/product.es6
Expand Up @@ -57,15 +57,17 @@
name: DOM.$addToCart.data('name'),
category: DOM.$addToCart.data('category'),
quantity: parseInt(DOM.$counter.val(), 10),
brand: DOM.$addToCart.data('brand'),
};
}

/**
* Publish onProductDetail event.
*/
function publishDetail() {
const { id, name, category } = getProductData();
if (id) mediator.publish('onProductDetail', [{ id, name, category }]);
const data = getProductData();
const { id } = data;
if (id) mediator.publish('onProductDetail', [data]);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion shopelectro/fixtures/dump.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion shopelectro/management/commands/price.py
Expand Up @@ -60,7 +60,7 @@ def put_utm(product):
product.prepared_params = list(
filter(
lambda x: x[0].name != 'Производитель',
product.params
product.get_params()
)
)

Expand Down
8 changes: 6 additions & 2 deletions shopelectro/management/commands/test_db.py
Expand Up @@ -39,11 +39,15 @@ class Command(BaseCommand):
def __init__(self):
super(BaseCommand, self).__init__()
self._product_id = 0
self.group_names = ['Напряжение', 'Сила тока', 'Мощность']
self.group_names = [
'Напряжение', 'Сила тока',
'Мощность', settings.BRAND_TAG_GROUP_NAME,
]
self.tag_names = [
['6 В', '24 В'],
['1.2 А', '10 А'],
['7.2 Вт', '240 Вт']
['7.2 Вт', '240 Вт'],
['Apple', 'Microsoft'],
]

def handle(self, *args, **options):
Expand Down
46 changes: 33 additions & 13 deletions shopelectro/models.py
Expand Up @@ -2,7 +2,7 @@
import string
from itertools import chain, groupby
from operator import attrgetter
from typing import List, Tuple
from typing import Dict, List, Optional, Tuple
from uuid import uuid4

from django.conf import settings
Expand Down Expand Up @@ -116,9 +116,8 @@ def feedback_count(self):
def feedback(self):
return self.product_feedbacks.all().order_by('-date')

@property
def params(self):
return Tag.objects.filter(products=self).get_group_tags_pairs()
def get_params(self):
return Tag.objects.filter_by_products([self]).get_group_tags_pairs()

# @todo #388:30m Move Product.get_siblings method to refarm-site
# And reuse it on STB.
Expand All @@ -130,6 +129,10 @@ def get_siblings(self, offset):
.select_related('page')[:offset]
)

def get_brand_name(self) -> str:
brand: Optional['Tag'] = Tag.objects.get_brands([self]).get(self)
return brand.name if brand else ''


class ProductFeedback(models.Model):
product = models.ForeignKey(
Expand Down Expand Up @@ -222,26 +225,36 @@ def __str__(self):

class TagQuerySet(models.QuerySet):

SLUG_HASH_SIZE = 5

def get_group_tags_pairs(self) -> List[Tuple[TagGroup, List['Tag']]]:
def filter_by_products(self, products: List[Product]):
ordering = settings.TAGS_ORDER
distinct = [order.lstrip('-') for order in ordering]

tags = (
return (
self
.all()
.prefetch_related('group')
.filter(products__in=products)
.order_by(*ordering)
.distinct(*distinct, 'id')
)

group_tags_pair = [
def get_group_tags_pairs(self) -> List[Tuple[TagGroup, List['Tag']]]:
grouped_tags = groupby(self.prefetch_related('group'), key=attrgetter('group'))
return [
(group, list(tags_))
for group, tags_ in groupby(tags, key=attrgetter('group'))
for group, tags_ in grouped_tags
]

return group_tags_pair
def get_brands(self, products: List[Product]) -> 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()
}


class TagManager(models.Manager.from_queryset(TagQuerySet)):
Expand All @@ -255,6 +268,13 @@ def get_queryset(self):
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):

Expand Down
2 changes: 2 additions & 0 deletions shopelectro/settings/base.py
Expand Up @@ -482,3 +482,5 @@ def get_robots_content():

# random string to append to doubled slugs
SLUG_HASH_SIZE = 5

BRAND_TAG_GROUP_NAME = 'Производитель'
2 changes: 1 addition & 1 deletion shopelectro/sitemaps.py
Expand Up @@ -50,7 +50,7 @@ def get_categories_with_tags() -> Generator[
"""
for category in Category.objects.filter(page__is_active=True):
products = Product.objects.get_by_category(category)
tags = Tag.objects.filter(products__in=products).distinct()
tags = Tag.objects.filter_by_products(products)
for group_name, group_tags in tags.get_group_tags_pairs():
for group_tag in group_tags:
yield category, group_tag
Expand Down
59 changes: 57 additions & 2 deletions shopelectro/tests/tests_models.py
@@ -1,5 +1,9 @@
from itertools import chain
from functools import partial

from django.conf import settings
from django.forms.models import model_to_dict
from django.test import TestCase
from django.test import TestCase, TransactionTestCase

from shopelectro.models import Product, Tag, TagGroup

Expand All @@ -23,10 +27,29 @@ def test_creation_deactivated_product(self):
self.fail(f'Creation of existing product failed: {{ error }}')


class TagTest(TestCase):
class TagModel(TestCase):

fixtures = ['dump.json']

def get_products(self):
return Product.objects.all()[:3]

def test_get_brands_content(self):
brand_group = TagGroup.objects.get(name=settings.BRAND_TAG_GROUP_NAME)
for product, tag in Tag.objects.get_brands(self.get_products()).items():
self.assertEquals(tag.group, brand_group)
self.assertIn(product, tag.products.all())

def test_filter_by_products(self):
sort_by_id = partial(sorted, key=lambda x: x.id)
products = self.get_products()
self.assertEquals(
sort_by_id(Tag.objects.filter_by_products(products)),
sort_by_id(list(set(
chain.from_iterable(product.tags.all() for product in products)
))),
)

def test_double_named_tag_saving(self):
"""Two tags with the same name should have unique slugs."""
def save_doubled_tag(tag_from_):
Expand All @@ -42,3 +65,35 @@ def save_doubled_tag(tag_from_):
tag_from = Tag.objects.first()
tag_to = save_doubled_tag(tag_from)
self.assertNotEqual(tag_from.slug, tag_to.slug)


class QueryQuantities(TransactionTestCase):
"""Test quantity of db-queries for different methods."""

fixtures = ['dump.json']

def test_get_brands_from_tags(self):
"""Perform two queries to fetch tags, its products."""
product = list(Product.objects.all())
with self.assertNumQueries(2):
Tag.objects.get_brands(product)

def test_get_brands_from_products(self):
"""
Perform a lot of queries, so we should fetch brands from the Tag model.
@todo #525:30m Try to implement Product.objects.get_brands()
Currently we use Tag.objects.get_brands(), be it seems is not so convenient
as it may be.
Details:
https://github.com/fidals/shopelectro/pull/531#discussion_r211858857
https://github.com/fidals/shopelectro/pull/531#discussion_r211962280
"""
products = Product.objects.all().prefetch_related('tags', 'tags__group')
products_quantity = products.count() + 3

with self.assertNumQueries(products_quantity):
{
p: p.tags.filter(group__name=settings.BRAND_TAG_GROUP_NAME).first()
for p in products
}
2 changes: 1 addition & 1 deletion shopelectro/tests/tests_views.py
Expand Up @@ -242,7 +242,7 @@ def test_pagination_step(self):
"""Category page contains `pagination_step` count of products in list."""
pagination_step = 25
response = self.get_category_page(query_string={'step': pagination_step})
self.assertEqual(len(response.context['product_image_pairs']), pagination_step)
self.assertEqual(len(response.context['products_data']), pagination_step)

def test_pagination_404(self):
"""Category page returns 404 for a nonexistent page number."""
Expand Down
24 changes: 12 additions & 12 deletions shopelectro/views/catalog.py
Expand Up @@ -98,16 +98,10 @@ def get_context_data(self, **kwargs):
# with it's own logic
return context

group_tags_pairs = (
models.Tag.objects
.filter(products=product)
.get_group_tags_pairs()
)

return {
**context,
'price_bounds': settings.PRICE_BOUNDS,
'group_tags_pairs': group_tags_pairs,
'group_tags_pairs': product.get_params(),
'tile_products': prepare_tile_products(
product.get_siblings(offset=settings.PRODUCT_SIBLINGS_COUNT)
),
Expand Down Expand Up @@ -164,13 +158,19 @@ def get_context_data(self, **kwargs):
}


def merge_products_and_images(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))
(product, images.get(product.page), brands.get(product))
for product in products
]

Expand Down Expand Up @@ -201,7 +201,7 @@ def get_context_data(self, **kwargs):

group_tags_pairs = (
models.Tag.objects
.filter(products__in=all_products)
.filter_by_products(all_products)
.get_group_tags_pairs()
)

Expand Down Expand Up @@ -241,7 +241,7 @@ def template_context(page, tag_titles, tags):

return {
**context,
'product_image_pairs': merge_products_and_images(products),
'products_data': merge_products_context(products),
'group_tags_pairs': group_tags_pairs,
'total_products': total_products,
'products_count': (page_number - 1) * products_on_page + products.count(),
Expand Down Expand Up @@ -305,7 +305,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', {
'product_image_pairs': merge_products_and_images(products),
'products_data': merge_products_context(products),
'paginated_page': paginated_page,
'view_type': view,
'prods': products_on_page,
Expand Down
7 changes: 4 additions & 3 deletions templates/catalog/category_products.html
Expand Up @@ -2,7 +2,7 @@
{% load se_extras %}
{% load user_agents %}

{% for product, image in product_image_pairs %}
{% for product, image, brand in products_data %}
<div class="product-card col-xs-6 col-md-4" productId="{{ product.id }}"
itemscope itemtype="https://schema.org/Product">
<meta property="name" itemprop="name" content="{{ product.name }}">
Expand Down Expand Up @@ -43,11 +43,12 @@
{% endcomment %}
<button class="btn btn-blue btn-category-buy js-product-to-cart"
productId="{{ product.id }}" productPrice="{{ product.price }}"
productName="{{ product.name }}">
productName="{{ product.name }}"
productBrand="{% if brand %}{{ brand.name }}{% endif %}">
Купить
</button>
</div>
</div>
</div>
{% endfor %}
<div class="hidden js-products-loaded">{{ product_image_pairs|length }}</div>
<div class="hidden js-products-loaded">{{ products_data|length }}</div>
3 changes: 2 additions & 1 deletion templates/catalog/product.html
Expand Up @@ -63,7 +63,8 @@ <h1 class="product-h1" itemprop="name">{{ page.display_h1 }}</h1>

<button class="btn btn-blue btn-to-basket js-to-cart-on-product-page" id="btn-to-basket"
data-id="{{ product.id }}" data-name="{{ product.name }}"
data-category="{{ product.category.name }}">В корзину</button>
data-category="{{ product.category.name }}"
data-brand="{{ product.get_brand_name }}">В корзину</button>

<p class="product-one-click">Купить в один клик</p>
<input class="form-control product-one-click-phone js-masked-phone" id="input-one-click-phone"
Expand Down
2 changes: 1 addition & 1 deletion templates/prices/price.yml
Expand Up @@ -46,7 +46,7 @@
{% endif %}
{# product_type tag in google merchant doc : https://goo.gl/b0UJQp #}
{% if utm == 'GM' %}<product_type>{{ product.crumbs }}</product_type>{% endif %}
{% for name, values in product.params %}
{% for name, values in product.get_params %}
<param name="{{ name }}">{{ values|first }}</param>
{% endfor %}
</offer>
Expand Down

1 comment on commit 64939d2

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on 64939d2 Aug 22, 2018

Choose a reason for hiding this comment

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

Puzzle 525-5d17d280 discovered in shopelectro/tests/tests_models.py and submitted as #537. 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.