Repository: fidals/shopelectro

Issue:
tests_views.py:79-80: Move method...
The puzzle `522-e4951f35` from #522 has to be resolved:

https://github.com/fidals/shopelectro/blob/7a2746e75fb55bffa94101e31350f0597b0be62e/shopelectro/tests/tests_views.py#L79-L80

The puzzle was created by duker33 on 22-Aug-18. 

Estimate: 15 minutes,     

If you have any technical questions, don't ask me, submit new tickets instead. The task will be "done" when the problem is fixed and the text of the puzzle is _removed_ from the source code. Here is more about [PDD](http://www.yegor256.com/2009/03/04/pdd.html) and [about me](http://www.yegor256.com/2017/04/05/pdd-in-action.html).


Discussion:
@artemiyds2 @duker33 please pay attention to this new issue.

Resolving patch:
diff --git a/shopelectro/management/commands/price.py b/shopelectro/management/commands/price.py
--- a/shopelectro/management/commands/price.py
+++ b/shopelectro/management/commands/price.py
@@ -3,24 +3,42 @@
 
 from django.conf import settings
 from django.core.management.base import BaseCommand
-from django.db.models import Q
+from django.db.models import Q, QuerySet
 from django.template.loader import render_to_string
 from django.urls import reverse
 
 from shopelectro.models import Product, Category
 
 
+class PriceFilter:
+    """Filter offers with individual price requirements."""
+
+    FILTERS = defaultdict(
+        lambda: (lambda qs: qs),
+        # Yandex Market feed requires picture for every offer
+        YM=lambda qs: (
+            qs
+            .filter(page__images__isnull=False)
+            .distinct()
+        ),
+        # Google Merchant feed should not contain offers cheaper then CONST
+        GM=lambda qs: (
+            qs
+            .filter(price__gt=settings.PRICE_GM_LOWER_BOUND)
+        )
+    )
+
+    def __init__(self, utm: str):
+        assert utm in settings.UTM_PRICE_MAP
+        self.utm = utm
+
+    def run(self, query_set: QuerySet) -> QuerySet:
+        return self.FILTERS[self.utm](query_set)
+
+
 class Command(BaseCommand):
     """Generate yml file for a given vendor (YM or price.ru)."""
 
-    # Online market services, that works with our prices.
-    # Dict keys - url targets for every service
-    TARGETS = {
-        'YM': 'yandex.yml',
-        'priceru': 'priceru.xml',
-        'GM': 'gm.yml',
-        'SE78': 'se78.yml',
-    }
     # price files will be stored at this dir
     BASE_DIR = settings.ASSETS_DIR
 
@@ -35,7 +53,7 @@ class Command(BaseCommand):
     })
 
     def create_prices(self):
-        for target in self.TARGETS.items():
+        for target in settings.UTM_PRICE_MAP.items():
             self.generate_yml(*target)
 
     def handle(self, *args, **options):
@@ -95,41 +113,15 @@ def filter_categories(utm):
 
             return result_categories
 
-        # @todo #533:30m Move `price.prepare_products` logic to `ProductQuerySet`
         def prepare_products(categories_, utm):
             """Filter product list and patch it for rendering."""
-            products_result_qs = (
-                Product.actives
-                .select_related('page')
-                .prefetch_related('category')
-                .prefetch_related('page__images')
-                .filter(category__in=categories_, price__gt=0)
-            )
-
-            if utm == 'YM':
-                """
-                Yandex Market feed requires items in some categories to have pictures
-                To simplify filtering we are excluding all products without pictures
-                """
-                products_result_qs = (
-                    products_result_qs
-                    .filter(page__images__isnull=False)
-                    .distinct()
-                )
-
-            if utm == 'GM':
-                products_result_qs = (
-                    products_result_qs
-                    .filter(price__gt=settings.PRICE_GM_LOWER_BOUND)
-                )
-
-            result_products = [
+            return [
                 put_crumbs(put_utm(product))
-                for product in products_result_qs
+                for product in PriceFilter(utm).run(
+                    Product.actives.filter_by_categories(categories_)
+                )
             ]
 
-            return result_products
-
         categories = (
             filter_categories(utm) if utm != 'SE78'
             else Category.objects.all()
diff --git a/shopelectro/settings/base.py b/shopelectro/settings/base.py
--- a/shopelectro/settings/base.py
+++ b/shopelectro/settings/base.py
@@ -487,3 +487,12 @@ def get_robots_content():
 
 # products with price lower this value should not be presented at gm.yml price
 PRICE_GM_LOWER_BOUND = 200.0
+
+# Online market services, that works with our prices.
+# Dict keys - url targets for every service.
+UTM_PRICE_MAP = {
+    'YM': 'yandex.yml',
+    'priceru': 'priceru.xml',
+    'GM': 'gm.yml',
+    'SE78': 'se78.yml',
+}


Test patch:
diff --git a/shopelectro/tests/helpers.py b/shopelectro/tests/helpers.py
--- a/shopelectro/tests/helpers.py
+++ b/shopelectro/tests/helpers.py
@@ -14,6 +14,8 @@
 from selenium.webdriver.support.ui import WebDriverWait
 from seleniumrequests import Remote  # We use this instead of standard selenium
 
+from shopelectro.models import Tag, TagGroup
+
 disable_celery = override_settings(USE_CELERY=False)
 enable_russian_language = override_settings(
     LANGUAGE_CODE='ru-ru', LANGUAGES=(('ru', 'Russian'),)
@@ -43,6 +45,17 @@ def context_click(browser, element):
     ActionChains(browser).context_click(element).perform()
 
 
+def create_doubled_tag(tag_from: Tag=None):
+    tag_from = tag_from or Tag.objects.first()
+    group_to = TagGroup.objects.exclude(id=tag_from.group.id).first()
+    tag_to = Tag.objects.create(
+        group=group_to, name=tag_from.name, position=tag_from.position
+    )
+    tag_to.products.set(tag_from.products.all())
+    tag_to.save()
+    return tag_to
+
+
 class SeleniumTestCase(LiveServerTestCase):
     """Common superclass for running selenium-based tests."""
 
diff --git a/shopelectro/tests/tests_commands.py b/shopelectro/tests/tests_commands.py
--- a/shopelectro/tests/tests_commands.py
+++ b/shopelectro/tests/tests_commands.py
@@ -16,7 +16,6 @@
 from django.core.management import call_command
 from django.test import TestCase
 
-from shopelectro.management.commands import price
 from shopelectro.management.commands._update_catalog import (
     update_products, update_tags
 )
@@ -152,7 +151,7 @@ def setUpTestData(cls):
 
     @classmethod
     def tearDownClass(cls):
-        for file_name in price.Command.TARGETS.values():
+        for file_name in settings.UTM_PRICE_MAP.values():
             os.remove(cls.get_price_file_path(file_name))
         super(GeneratePrices, cls).tearDownClass()
 
@@ -192,7 +191,7 @@ def test_prices_exists(self):
         """Price command should generate various price-list files."""
         price_file_min_size = 10 ** 3  # ~1kb
 
-        for name in price.Command.TARGETS.values():
+        for name in settings.UTM_PRICE_MAP.values():
             file_name = self.get_price_file_path(name)
             self.assertIn(name, os.listdir(settings.ASSETS_DIR))
             size = os.stat(file_name).st_size
diff --git a/shopelectro/tests/tests_models.py b/shopelectro/tests/tests_models.py
--- a/shopelectro/tests/tests_models.py
+++ b/shopelectro/tests/tests_models.py
@@ -6,6 +6,7 @@
 from django.test import TestCase, TransactionTestCase
 
 from shopelectro.models import Product, Tag, TagGroup
+from shopelectro.tests.helpers import create_doubled_tag
 
 
 class ProductModel(TestCase):
@@ -52,18 +53,8 @@ def test_filter_by_products(self):
 
     def test_double_named_tag_saving(self):
         """Two tags with the same name should have unique slugs."""
-        def save_doubled_tag(tag_from_):
-            group_to = TagGroup.objects.exclude(id=tag_from_.group.id).first()
-            tag_to = Tag(
-                group=group_to, name=tag_from_.name, position=tag_from_.position
-            )
-            # required to create `tag.products` field
-            tag_to.save()
-            tag_to.products.set(tag_from.products.all())
-            tag_to.save()
-            return tag_to
         tag_from = Tag.objects.first()
-        tag_to = save_doubled_tag(tag_from)
+        tag_to = create_doubled_tag(tag_from)
         self.assertNotEqual(tag_from.slug, tag_to.slug)
 
 
diff --git a/shopelectro/tests/tests_views.py b/shopelectro/tests/tests_views.py
--- a/shopelectro/tests/tests_views.py
+++ b/shopelectro/tests/tests_views.py
@@ -21,6 +21,7 @@
 
 from shopelectro.models import Category, Product, Tag, TagGroup, TagQuerySet, serialize_tags_to_url
 from shopelectro.views.service import generate_md5_for_ya_kassa, YANDEX_REQUEST_PARAM
+from shopelectro.tests.helpers import create_doubled_tag
 
 
 CANONICAL_HTML_TAG = '<link rel="canonical" href="{path}">'
@@ -49,7 +50,7 @@ def get_page_number(response):
     return response.context['paginated_page'].number
 
 
-def json_to_dict(response: HttpResponse) -> dict():
+def json_to_dict(response: HttpResponse) -> dict:
     return json.loads(response.content)
 
 
@@ -76,19 +77,6 @@ def get_category_page(
 
 class CatalogTags(BaseCatalogTestCase):
 
-    # @todo #522:15m Move method `CatalogTags.create_doubled_tag` to test helpers.
-    #  And rm code doubling at
-    # `shopelectro.tests.tests_models.TagTest#test_double_named_tag_creation`.
-    def create_doubled_tag(self):
-        tag_from = Tag.objects.first()
-        group_to = TagGroup.objects.exclude(id=tag_from.group.id).first()
-        tag_to = Tag.objects.create(
-            group=group_to, name=tag_from.name, position=tag_from.position
-        )
-        tag_to.products.set(tag_from.products.all())
-        tag_to.save()
-        return tag_to
-
     def test_category_page_contains_all_tags(self):
         """Category contains all Product's tags."""
         response = self.get_category_page()
@@ -198,7 +186,7 @@ class instance.
 
     def test_doubled_tag(self):
         """Category tags page filtered by the same tag from different tag groups."""
-        tag_ = self.create_doubled_tag()
+        tag_ = create_doubled_tag()
         response = self.get_category_page(
             tags=Tag.objects.filter(id=tag_.id)
         )