Repository: fidals/shopelectro

Issue:
Move price ignored entities to settings
Add possibility to ignore products too.

PO needs product ignoring right now.

RM this string by the way for YA price by the way:
```
При заказе от 5000 руб. доставка по СПб бесплатно
она в теге <sales_notes>
```


Discussion:
i'll repeat this logic on STB with the same feature

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
@@ -67,23 +67,11 @@ def context(self) -> dict:
 class CategoriesFilter:
     """Categories list for particular market place."""
 
-    # dict keys are url targets for every service
-    IGNORED_CATEGORIES_MAP = defaultdict(list, {
-        'GM': ['Усилители звука для слабослышащих'],
-        'YM': ['Пиротехника'],
-        # will be ignored by every category
-        'default': [
-            'Измерительные приборы', 'Новогодние вращающиеся светодиодные лампы',
-            'Новогодние лазерные проекторы', 'MP3- колонки', 'Беспроводные звонки',
-            'Радиоприёмники', 'Фонари', 'Отвертки', 'Весы электронные портативные',
-        ]
-    })
-
     @property
     def ignored(self) -> typing.List[str]:
         return (
-            self.IGNORED_CATEGORIES_MAP['default']
-            + self.IGNORED_CATEGORIES_MAP[self.target]
+            settings.PRICE_IGNORED_CATEGORIES_MAP['default']
+            + settings.PRICE_IGNORED_CATEGORIES_MAP[self.target]
         )
 
     def __init__(self, target: str):
@@ -121,6 +109,10 @@ def qs(self) -> models.SECategoryQuerySet:
 class ProductsFilter:
     """Filter offers with individual price requirements."""
 
+    @property
+    def ignored(self) -> typing.List[str]:
+        return settings.PRICE_IGNORED_PRODUCTS_MAP[self.target]
+
     FILTERS = defaultdict(
         lambda: (lambda qs: qs),
         # Yandex Market feed requires picture for every offer
@@ -145,6 +137,7 @@ def qs(self) -> QuerySet:
         return self.FILTERS[self.target](
             models.Product.objects.active()
             .filter(category__in=self.categories, price__gt=0)
+            .exclude(id__in=self.ignored)
         )
 
 
diff --git a/shopelectro/settings/base.py b/shopelectro/settings/base.py
--- a/shopelectro/settings/base.py
+++ b/shopelectro/settings/base.py
@@ -11,6 +11,7 @@
 """
 import os
 import socket
+from collections import defaultdict
 from datetime import datetime
 
 import sentry_sdk
@@ -499,6 +500,23 @@ def get_robots_content():
     'SE78': 'se78.yml',
 }
 
+PRICE_IGNORED_CATEGORIES_MAP = defaultdict(list, {
+    'GM': ['Усилители звука для слабослышащих'],
+    'YM': ['Пиротехника'],
+    # will be ignored by every category
+    'default': [
+        'Измерительные приборы', 'Новогодние вращающиеся светодиодные лампы',
+        'Новогодние лазерные проекторы', 'MP3- колонки', 'Беспроводные звонки',
+        'Радиоприёмники', 'Фонари', 'Отвертки', 'Весы электронные портативные',
+    ]
+})
+
+
+# contains some values for example. Local.py will contain the real values
+PRICE_IGNORED_PRODUCTS_MAP = defaultdict(list, {
+    'YM': [1, 2, 3],
+})
+
 # Number of pagination neighbors shown for page.
 # If PAGINATION_NEIGHBORS = 4 and number of a page = 5,
 # then will be shown neighbors by number: 3, 4, 6, 7


Test patch:
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
@@ -11,12 +11,11 @@
 import urllib.parse
 import uuid
 from collections import defaultdict
-from unittest import mock
 from xml.etree import ElementTree
 
 from django.conf import settings
 from django.core.management import call_command
-from django.test import TestCase, tag
+from django.test import TestCase, override_settings, tag
 
 from shopelectro.management.commands._update_catalog import (
     update_products, update_tags
@@ -213,10 +212,21 @@ class GeneratePrices(TestCase):
 
     fixtures = ['dump.json']
     CATEGORY_TO_EXCLUDE = 'Category #1 of #Category #0 of #Category #1'
+    PRICE_IGNORED_CATEGORIES_MAP = defaultdict(
+        list, {'GM': [CATEGORY_TO_EXCLUDE]}
+    )
+    ignore_categories = override_settings(
+        PRICE_IGNORED_CATEGORIES_MAP=PRICE_IGNORED_CATEGORIES_MAP
+    )
+    PRODUCTS_TO_EXCLUDE = [1, 2, 3]
+    ignore_products = override_settings(
+        PRICE_IGNORED_PRODUCTS_MAP=defaultdict(list, {'YM': PRODUCTS_TO_EXCLUDE})
+    )
 
     @classmethod
     def setUpTestData(cls):
-        cls.call_command_patched('price')
+        with cls.ignore_categories, cls.ignore_products:
+            call_command('price')
         super(GeneratePrices, cls).setUpTestData()
         cls.prices = Prices(settings.UTM_PRICE_MAP.keys())
 
@@ -225,18 +235,6 @@ def tearDownClass(cls):
         cls.prices.remove()
         super(GeneratePrices, cls).tearDownClass()
 
-    @classmethod
-    def call_command_patched(cls, name):
-        """Patch with test constants and call."""
-        with mock.patch(
-            'shopelectro.management.commands.price.CategoriesFilter.IGNORED_CATEGORIES_MAP',
-            new_callable=mock.PropertyMock
-        ) as target:
-            target.return_value = defaultdict(list, {
-                'GM': [cls.CATEGORY_TO_EXCLUDE]
-            })
-            call_command(name)
-
     def test_prices_exists(self):
         """Price command should generate various price-list files."""
         price_file_min_size = 10 ** 3  # ~1kb
@@ -257,6 +255,7 @@ def test_categories_in_yandex_price(self):
             Category.objects.get_categories_tree_with_pictures().count()
         )
 
+    @ignore_categories
     def test_categories_excluded_by_utm(self):
         """Price file should not contain it's excluded category."""
         def find_category(categories, name):
@@ -282,6 +281,15 @@ def find_category(categories, name):
             )
         )
 
+    @ignore_products
+    def test_products_excluded_by_id(self):
+        to_ignore = set(settings.PRICE_IGNORED_PRODUCTS_MAP['YM'])
+        ignored = set(
+            offer.attrib['id']
+            for offer in self.prices['YM'].offers_node.findall('offer')
+        )
+        self.assertFalse(to_ignore.intersection(ignored))
+
     def test_products_in_price(self):
         products = self.prices['priceru'].offers_node
         self.assertEqual(len(products), Product.objects.count())
@@ -296,10 +304,16 @@ def test_products_in_gm_price_bounds(self):
         self.assertTrue(prices_are_in_bounds)
 
     def test_products_in_yandex_price(self):
-        products = self.prices['YM'].offers_node
+        origin = (
+            Product.objects
+            .exclude(id__in=self.PRODUCTS_TO_EXCLUDE)
+            .filter(page__images__isnull=False)
+            .distinct()
+        )
+        result = self.prices['YM'].offers_node
         self.assertEqual(
-            len(products),
-            Product.objects.filter(page__images__isnull=False).distinct().count()
+            len(result),
+            origin.count()
         )
 
     def test_brands(self):