diff --git a/shopelectro/migrations/0036_matrixblock.py b/shopelectro/migrations/0036_matrixblock.py new file mode 100644 index 00000000..34a6000f --- /dev/null +++ b/shopelectro/migrations/0036_matrixblock.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-07-04 08:46 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('shopelectro', '0035_product_in_pack'), + ] + + operations = [ + migrations.CreateModel( + name='MatrixBlock', + fields=[ + ('category', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='matrix_block', serialize=False, to='shopelectro.Category', verbose_name='category')), + ('block_size', models.PositiveSmallIntegerField(null=True, verbose_name='block size')), + ], + options={ + 'verbose_name': 'Matrix block', + 'verbose_name_plural': 'Matrix blocks', + 'ordering': ['category__page__position', 'category__name'], + }, + ), + ] diff --git a/shopelectro/models.py b/shopelectro/models.py index 1667d670..ba5f0794 100644 --- a/shopelectro/models.py +++ b/shopelectro/models.py @@ -56,6 +56,63 @@ def get_absolute_url(self): return reverse('category', args=(self.page.slug,)) +class MatrixBlockManager(models.Manager): + + def blocks(self): + return ( + super() + .get_queryset() + .select_related('category', 'category__page') + .prefetch_related('category__children') + .filter(category__level=0, category__page__is_active=True) + ) + + +class MatrixBlock(models.Model): + """It is an UI element of catalog matrix.""" + + # @todo #880:30m Add MatrixBlock to the admin panel. + # Inline it on Category Edit page. + + # @todo #880:60m Use MatrixBlock in the matrix view. + # Get the block_size data from the matrix view and fill out the model. + + class Meta: + verbose_name = _('Matrix block') + verbose_name_plural = _('Matrix blocks') + ordering = ['category__page__position', 'category__name'] + + objects = MatrixBlockManager() + + category = models.OneToOneField( + Category, + on_delete=models.CASCADE, + primary_key=True, + verbose_name=_('category'), + related_name=_('matrix_block'), + limit_choices_to={'level': 0}, + ) + + block_size = models.PositiveSmallIntegerField( + null=True, + verbose_name=_('block size'), + ) + + @property + def name(self): + self.category.name + + @property + def url(self): + self.category.url + + def rows(self) -> SECategoryQuerySet: + rows = self.category.children.active() + if self.block_size: + return rows[:self.block_size] + return rows + + class Product( catalog_models.AbstractProduct, catalog_models.AbstractPosition, diff --git a/shopelectro/tests/tests_models.py b/shopelectro/tests/tests_models.py index 0be22810..e0d2a38f 100644 --- a/shopelectro/tests/tests_models.py +++ b/shopelectro/tests/tests_models.py @@ -2,10 +2,11 @@ from itertools import chain from django.conf import settings +from django.db.utils import IntegrityError from django.forms.models import model_to_dict from django.test import TestCase, TransactionTestCase, tag -from shopelectro.models import Product, Tag, TagGroup +from shopelectro.models import Category, MatrixBlock, Product, Tag, TagGroup @tag('fast') @@ -86,3 +87,75 @@ def test_get_brands_from_products(self): p: p.tags.filter(group__name=settings.BRAND_TAG_GROUP_NAME).first() for p in products } + + def test_get_matrix_blocks(self): + roots = Category.objects.filter(level=0) + roots_count = roots.count() + for category in roots: + MatrixBlock.objects.create(category=category) + + blocks = MatrixBlock.objects.blocks() + + # 2 queries: MatrixBlock + Category joined with CategoryPage + # select_related doesn't deffer the queries + with self.assertNumQueries(2): + self.assertEquals(roots_count, len(blocks)) + for block in blocks: + self.assertTrue(block.category) + self.assertTrue(block.category.page) + + with self.assertNumQueries(2): + for block in blocks: + self.assertTrue(block.rows()) + + +@tag('fast') +class MatrixBlockModel(TestCase): + + fixtures = ['dump.json'] + + def test_block_category_relation_uniqueness(self): + category = Category.objects.first() + + with self.assertRaises(IntegrityError): + MatrixBlock.objects.create(category=category) + MatrixBlock.objects.create(category=category) + + def test_unsized_rows_count(self): + block = MatrixBlock.objects.create(category=Category.objects.first()) + + self.assertEquals( + block.category.children.active().count(), + block.rows().count(), + ) + + def test_sized_rows_count(self): + sized_category, oversized_category = Category.objects.all()[:2] + + # block_size < category's children quantity + sized_block = MatrixBlock.objects.create( + category=sized_category, + block_size=sized_category.children.count() - 1, + ) + self.assertNotEquals( + sized_category.children.active().count(), + sized_block.rows().count(), + ) + self.assertEquals( + sized_block.block_size, + sized_block.rows().count(), + ) + + # block_size > category's children quantity + oversized_block = MatrixBlock.objects.create( + category=oversized_category, + block_size=oversized_category.children.count() + 1, + ) + self.assertEquals( + oversized_category.children.active().count(), + oversized_block.rows().count(), + ) + self.assertNotEquals( + oversized_block.block_size, + oversized_block.rows().count(), + ) diff --git a/shopelectro/views/catalog.py b/shopelectro/views/catalog.py index 9a854362..62faa2ca 100644 --- a/shopelectro/views/catalog.py +++ b/shopelectro/views/catalog.py @@ -42,9 +42,6 @@ def category_matrix(request, page: str): # How the matrix looks like: # https://github.com/fidals/shopelectro/issues/837#issuecomment-501161967 - # @todo #837:60m Improve categories matrix arch. - # Now it's untyped data structure with common comments. - # Turn it to the set of object with clear names. matrix[(root.name, root.url)] = ( children if i not in MATRIX_BLOCKS_TO_LIMIT