Skip to content
This repository has been archived by the owner on Feb 23, 2020. It is now read-only.

Commit

Permalink
#669 Create sections (#700)
Browse files Browse the repository at this point in the history
* #558  Rm redundant pdd subtask

* #669  Draft for a  Section model code

* #669  Handle code duplication with PDD subtask

* #669  Create sections migrations

* #584  Rm closed manually pdd subtask

* #669  Create section page with test

* #669  Apply linter rules

* #669  Create subtasks for section feature

* #699  Apply linter rules

* #699  Review#1 fixes. Section: get_min_price, rm slug, tests subtask

* #699  Review#1 fixes. Rm search, fix tests setUp, subtask seo_texts exploring
  • Loading branch information
duker33 authored Jun 26, 2019
1 parent 8c92bc4 commit dde6256
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 49 deletions.
4 changes: 3 additions & 1 deletion stroyprombeton/management/commands/seo_texts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# @todo #669:30m Explore seo_texts command.
# Either document or remove it.

from functools import reduce
from operator import or_

Expand All @@ -7,7 +10,6 @@

from stroyprombeton.models import ProductPage


product_page = {
'content': '''
Производим, продаем и доставляем по России {} {}.
Expand Down
47 changes: 47 additions & 0 deletions stroyprombeton/migrations/0026_create_section.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-24 05:56
from __future__ import unicode_literals

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pages', '0018_page_template_increase_name_size'),
('stroyprombeton', '0025_series_slug'),
]

operations = [
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, max_length=1000, unique=True, verbose_name='name')),
('page', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stroyprombeton_section', to='pages.Page')),
],
options={
'verbose_name': 'Section',
'verbose_name_plural': 'Sections',
},
),
migrations.CreateModel(
name='SeriesPage',
fields=[
],
options={
'verbose_name': 'serie',
'verbose_name_plural': 'series',
'abstract': False,
'proxy': True,
'indexes': [],
},
bases=('pages.modelpage',),
),
migrations.AddField(
model_name='product',
name='section',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='stroyprombeton.Section', verbose_name='section'),
),
]
95 changes: 87 additions & 8 deletions stroyprombeton/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class SeriesQuerySet(models.QuerySet):

def bind_fields(self):
"""Prefetch or select typical related fields to reduce sql queries count."""
return self.select_related('page')
return self.select_related('page') # Ignore CPDBear

def active(self) -> 'SeriesQuerySet':
return self.filter(page__is_active=True)
Expand All @@ -125,9 +125,6 @@ def active(self):
return self.get_queryset().active()


# @todo #510:60m Create Series navigation.
# See the parent's task body for details about navigation.
# This task depends on Series page creation.
class Series(pages.models.PageMixin):
"""
Series is another way to organize products.
Expand All @@ -141,10 +138,8 @@ class Series(pages.models.PageMixin):
SLUG_MAX_LENGTH = 50

class Meta:
# @todo #510:15m Translate "Series" term.
# And "series" too. It's used in the relation with Product.
verbose_name = _('Series')
verbose_name_plural = _('Series')
verbose_name_plural = _('Series') # Ignore CPDBear

name = models.CharField(
max_length=1000, db_index=True, unique=True, verbose_name=_('name')
Expand All @@ -156,8 +151,10 @@ def url(self):

def get_absolute_url(self):
"""Url path to the related page."""
return reverse('series', args=(self.slug,))
return reverse('series', args=(self.slug,)) # Ignore CPDBear

# @todo #669:60m Remove `Series.slug` field.
# Series page already contains it.
slug = models.SlugField(
blank=False, unique=True, max_length=SLUG_MAX_LENGTH,
)
Expand Down Expand Up @@ -191,6 +188,80 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)


class SectionQuerySet(models.QuerySet):

def bind_fields(self):
"""Prefetch or select typical related fields to reduce sql queries count."""
return (
self.select_related('page')
.prefetch_related('products')
)

def active(self) -> 'SeriesQuerySet':
return self.filter(page__is_active=True)

def exclude_empty(self) -> 'SeriesQuerySet':
return (
self.active()
.filter(products__page__is_active=True)
.distinct()
)


class SectionManager(models.Manager.from_queryset(SeriesQuerySet)):
"""Get all products of given category by Category's id or instance."""

def active(self):
return self.get_queryset().active()


# @todo #669:30m Add Sections to the admin panel.
# @todo #669:30m Get rid of Category-Series-Section models code doubling.
class Section(pages.models.PageMixin):
"""
Section is one more way to organize products.
It's like Category, but has no hierarchy.
And it's like Series, but it's bound to products instead of options.
"""

# @todo #669:30m Doc section concept.
# What problem it solves, who required it.
# Why we solved problem in this way.

objects = SeriesManager()

SLUG_HASH_SIZE = 5
SLUG_MAX_LENGTH = 50

class Meta:
# @todo #510:15m Translate "Section" term.
# And "series" too. It's used in the relation with Product.
verbose_name = _('Section')
verbose_name_plural = _('Sections')

name = models.CharField(
max_length=1000, db_index=True, unique=True, verbose_name=_('name')
)

@property
def url(self):
return self.get_absolute_url()

def get_absolute_url(self):
"""Url path to the related page."""
return reverse('section', args=(self.page.slug,))

# @todo #669:60m Implement `Section.get_min_price` method.
# And look at the get_min_price arch in a whole.
# Maybe only ProductsQS and OptionsQS should have min_price,
# but Section and Series should not.
# Category, Series and Product page templates at the production DB
# contains series min price usage.
def get_min_price(self) -> float:
raise NotImplemented()


class OptionQuerySet(models.QuerySet):

def active(self):
Expand Down Expand Up @@ -328,6 +399,14 @@ class Product(catalog.models.AbstractProduct, pages.models.PageMixin):
verbose_name=_('category'),
)

section = models.ForeignKey(
Section,
on_delete=models.CASCADE,
related_name='products',
verbose_name=_('section'),
null=True,
)

def __str__(self):
return self.name

Expand Down
127 changes: 101 additions & 26 deletions stroyprombeton/tests/tests_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,30 +1016,6 @@ def get_series_soup(self, *args, **kwargs) -> BeautifulSoup:
'html.parser'
)

# @todo #570:60m Implement search on series.
# And resurrect the test below.
@unittest.skip
def test_fetch_positions_searching(self):
term = '#1'
options = self.series.options
searched = context.search(
term, options, context.SearchedOptions.LOOKUPS,
ordering=('product__name', )
)

response = self.client.post(
reverse('fetch_products'),
data={
'seriesId': self.series.id,
'filtered': 'true',
'term': term,
'offset': 0,
'limit': 10**6,
}
)
self.assertEqual(searched.count(), len(response.context['products']))
self.assertEqual(list(searched), list(response.context['products']))

def test_options_are_from_series(self):
response = self.client.get(self.get_series_url(self.series))
self.assertTrue(
Expand All @@ -1049,7 +1025,7 @@ def test_options_are_from_series(self):
def test_active_options(self):
"""Series page should contain only options with active related products."""
options_qs = (
self.series.options
self.series.options # Ignore CPDBear
.bind_fields()
.order_by(*settings.OPTIONS_ORDERING)
)
Expand Down Expand Up @@ -1079,14 +1055,113 @@ def test_product_images(self):
product = models.Product.objects.get(id=PRODUCT_WITH_IMAGE)
series = models.Series.objects.first()
product.options.update(series=series)
response = self.client.get(series.url)
response = self.client.get(series.url) # Ignore CPDBear
image = response.context['product_images'][110]
self.assertTrue(image)
self.assertTrue(image.image.url)

def test_product_image_button(self):
"""Series page should contain button to open existing product image."""
# product with image
product = models.Product.objects.get(id=PRODUCT_WITH_IMAGE) # Ignore CPDBear
response = self.client.get(product.category.url)
self.assertContains(response, 'table-photo-ico')


# will be removed during this Section class implementing.
@tag('fast', 'catalog')
class SectionFirst(BaseCatalogTestCase):
fixtures = ['dump.json']

def test_page_success(self):
section = models.Section.objects.create(
name='First section',
page=ModelPage.objects.create(
name='First section',
slug='first',
),
)
models.Product.objects.active().update(section=section)
response = self.client.get(section.url)
self.assertEqual(200, response.status_code)


# @todo #669:120m Create tests for Section.
# Use Section tests draft below. Rm current SectionFirst draft.
# Create fixtures with sections.
@unittest.skip
@tag('fast', 'catalog')
class Section(BaseCatalogTestCase):
fixtures = ['dump.json']

def setUp(self):
self.section = models.Section.objects.filter(products__isnull=False).first()

def get_section_url(
self,
section: models.Section = None,
):
section = section or self.section
return section.url

def get_section_page(self, *args, **kwargs):
return self.client.get(self.get_section_url(*args, **kwargs)) # Ignore CPDBear

def get_section_soup(self, *args, **kwargs) -> BeautifulSoup:
section_page = self.get_section_page(*args, **kwargs)
return BeautifulSoup(
section_page.content.decode('utf-8'),
'html.parser'
)

def test_options_are_from_section(self):
response = self.client.get(self.get_section_url(self.section))
self.assertTrue(
all(option.section == self.section for option in response.context['products'])
)

def test_active_options(self):
"""Section page should contain only options with active related products."""
options_qs = (
self.section.options
.bind_fields()
# @todo #699:60m Implement `OptionsQS.default_order` method. se2
.order_by(*settings.OPTIONS_ORDERING)
)
# make inactive the first option in a section page list
inactive = options_qs.first()
inactive.product.page.is_active = False
inactive.product.page.save()
active = options_qs.active().first()

response = self.client.get(self.get_section_url(self.section))
self.assertIn(active, response.context['products'])
self.assertNotIn(inactive, response.context['products'])

def test_emtpy_404(self):
"""Section with not active options should return response 404."""
section = (
models.Section.objects
.annotate(count=Count('options'))
.filter(count=0)
).first()
response = self.get_section_page(section)
self.assertEqual(404, response.status_code)

def test_product_images(self):
"""Section page should contain only options with active related products."""
# product with image
product = models.Product.objects.get(id=PRODUCT_WITH_IMAGE)
section = models.Section.objects.first()
product.options.update(section=section)
response = self.client.get(section.url)
image = response.context['product_images'][110]
self.assertTrue(image)
self.assertTrue(image.image.url)

def test_product_image_button(self):
"""Section page should contain button to open existing product image."""
# product with image
product = models.Product.objects.get(id=PRODUCT_WITH_IMAGE)
response = self.client.get(product.category.url)
self.assertContains(response, 'table-photo-ico')
Expand Down
2 changes: 2 additions & 0 deletions stroyprombeton/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
views.series, name='series'),
url(r'^series/(?P<series_slug>[\w_-]+)/category/(?P<category_id>[0-9]+)/$',
views.series_by_category, name='series_by_category'),
url(r'^section/(?P<section_slug>[\w_-]+)/$',
views.section, name='section'),
]

custom_pages = [
Expand Down
21 changes: 21 additions & 0 deletions stroyprombeton/views/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,27 @@ def series(request, series_slug: str):
)


# @todo #669:120m Create sections navigation.
# Top menu link, matrix page, links from and to categories.
def section(request, section_slug: str):
section = get_object_or_404(models.Section.objects, page__slug=section_slug)
products = section.products.active()
images = context.products.ProductImages(
products, Image.objects.all()
)
if not products:
raise http.Http404('<h1>В секции нет изделий</h1')
return render(
request,
'catalog/section.html',
{
**images.context(),
'products': products,
'page': section.page,
}
)


def series_by_category(request, series_slug: str, category_id: int):
series = get_object_or_404(models.Series.objects, slug=series_slug)
category = get_object_or_404(models.Category.objects, id=category_id)
Expand Down
Loading

13 comments on commit dde6256

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 510-d7a1e1df disappeared from stroyprombeton/models.py, that's why I closed #558. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 510-b8de40bc disappeared from stroyprombeton/models.py, that's why I closed #559. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 570-e576fe87 disappeared from stroyprombeton/tests/tests_views.py, that's why I closed #584. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 669-02688a32 discovered in stroyprombeton/views/catalog.py and submitted as #701. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 669-e87026f9 discovered in stroyprombeton/management/commands/seo_texts.py and submitted as #702. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 669-92706ca5 discovered in stroyprombeton/tests/tests_views.py and submitted as #703. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 699-0168785b discovered in stroyprombeton/tests/tests_views.py and submitted as #704. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 669-7e706acd discovered in stroyprombeton/models.py and submitted as #705. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 669-54df6c9d discovered in stroyprombeton/models.py and submitted as #706. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 669-d7479e24 discovered in stroyprombeton/models.py and submitted as #707. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 669-4e4d9868 discovered in stroyprombeton/models.py and submitted as #708. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 510-fa306f38 discovered in stroyprombeton/models.py and submitted as #709. 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 dde6256 Jun 26, 2019

Choose a reason for hiding this comment

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

Puzzle 669-b068266e discovered in stroyprombeton/models.py and submitted as #710. 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.