Repository: fidals/shopelectro

Issue:
price.py:39-40: Refactor price command. Create Price...
The puzzle `661-ec781b69` from #661 has to be resolved: 

https://github.com/fidals/shopelectro/blob/2f195484e2a7430b5e2a69fe4be27eb3dfee5be2/shopelectro/management/commands/price.py#L39-L40

The puzzle was created by duker33 on 11-Dec-18. 

  

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). 

price.py:39-40: Refactor price command. Create Price...
The puzzle `661-ec781b69` from #661 has to be resolved: 

https://github.com/fidals/shopelectro/blob/2f195484e2a7430b5e2a69fe4be27eb3dfee5be2/shopelectro/management/commands/price.py#L39-L40

The puzzle was created by duker33 on 11-Dec-18. 

  

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.
@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
@@ -1,5 +1,6 @@
-from collections import defaultdict
 import os
+import typing
+from collections import defaultdict
 
 from django.conf import settings
 from django.core.management.base import BaseCommand
@@ -7,9 +8,40 @@
 from django.template.loader import render_to_string
 from django.urls import reverse
 
-from shopelectro.models import Product, Category, Tag
+from catalog import newcontext
+from shopelectro import models
+
+
+# --- files processing ---
+class File:
+    def __init__(self, path: str, context: dict):
+        self.path = path
+        self.context = context
+
+    def create(self):
+        with open(self.path, 'w', encoding='utf-8') as file:
+            file.write(render_to_string('prices/price.yml', self.context).strip())
+        return f'{self.path} generated...'
+
+
+class Files:
+    def __init__(self, files: typing.List[File]):
+        self.files = files
+
+    def create(self):
+        for file in self.files:
+            file.create()
+
 
+class Price:
+    pass
 
+
+class Prices:
+    pass
+
+
+# --- data ---
 class PriceFilter:
     """Filter offers with individual price requirements."""
 
@@ -36,13 +68,13 @@ def run(self, query_set: QuerySet) -> QuerySet:
         return self.FILTERS[self.utm](query_set)
 
 
-# @todo #661:120m Refactor price command.
-#  Create Price class with it's own price processing.
-class Command(BaseCommand):
-    """Generate yml file for a given vendor (YM or price.ru)."""
+class Context(newcontext.Context):
+    """DB data, extracted for price file."""
 
-    # price files will be stored at this dir
-    BASE_DIR = settings.ASSETS_DIR
+    UTM_MEDIUM_DATA = defaultdict(
+        lambda: 'cpc',
+        {'YM': 'cpc-market'}
+    )
 
     IGNORED_CATEGORIES = [
         'Измерительные приборы', 'Новогодние вращающиеся светодиодные лампы',
@@ -55,26 +87,22 @@ class Command(BaseCommand):
         'GM': ['Усилители звука для слабослышащих'],
     })
 
-    UTM_MEDIUM_DATA = defaultdict(
-        lambda: 'cpc',
-        {'YM': 'cpc-market'}
-    )
-
-    def create_prices(self):
-        for target in settings.UTM_PRICE_MAP.items():
-            self.generate_yml(*target)
-
-    def handle(self, *args, **options):
-        self.create_prices()
-
-    @classmethod
-    def get_context_for_yml(cls, utm):
-        """Create context dictionary for rendering files."""
+    def __init__(self, target: str):
+        self.target = target
+
+    # @todo #666:120m  Split price.Context.context to smaller classes
+    #  Don't forget it:
+    #  - Move class `PriceFilter` to `Product`
+    #  - Merge to `IGNORED` constants
+    #  - Rm constants from Tree class
+    #  - Inject logs object here instead of returning operation status
+    #  - Maybe split this file to blocks
+    def context(self) -> dict:
         def put_utm(product):
             """Put UTM attribute to product."""
             utm_marks = [
-                ('utm_source', utm),
-                ('utm_medium', cls.UTM_MEDIUM_DATA[utm]),
+                ('utm_source', self.target),
+                ('utm_medium', self.UTM_MEDIUM_DATA[self.target]),
                 ('utm_content', product.get_root_category().page.slug),
                 ('utm_term', str(product.vendor_code)),
             ]
@@ -106,15 +134,15 @@ def put_brand(product, brands):
 
         def filter_categories(utm):
             categories_to_exclude = (
-                Category.objects
+                models.Category.objects
                 .filter(
-                    Q(name__in=cls.IGNORED_CATEGORIES)
-                    | Q(name__in=cls.IGNORED_CATEGORIES_MAP[utm])
+                    Q(name__in=self.IGNORED_CATEGORIES)
+                    | Q(name__in=self.IGNORED_CATEGORIES_MAP[utm])
                 )
                 .get_descendants(include_self=True)
             )
 
-            result_categories = Category.objects.exclude(id__in=categories_to_exclude)
+            result_categories = models.Category.objects.exclude(id__in=categories_to_exclude)
 
             if utm == 'YM':
                 """
@@ -129,36 +157,41 @@ def filter_categories(utm):
         def prepare_products(categories_, utm):
             """Filter product list and patch it for rendering."""
             products = PriceFilter(utm).run(
-                Product.objects.active().filter_by_categories(categories_)
+                models.Product.objects.active().filter_by_categories(categories_)
             )
-            brands = Tag.objects.get_brands(products)
+            brands = models.Tag.objects.get_brands(products)
             return [
                 put_brand(put_crumbs(put_utm(product)), brands)
                 for product in products
             ]
 
         categories = (
-            filter_categories(utm) if utm != 'SE78'
-            else Category.objects.all()
+            filter_categories(self.target) if self.target != 'SE78'
+            else models.Category.objects.all()
         )
 
-        products = prepare_products(categories, utm)
+        products = prepare_products(categories, self.target)
 
         return {
             'base_url': settings.BASE_URL,
             'categories': categories,
             'products': products,
             'shop': settings.SHOP,
-            'utm': utm,
+            'utm': self.target,
         }
 
-    @classmethod
-    def generate_yml(cls, utm, file_name):
-        """Generate yml file."""
-        file_to_write = os.path.join(cls.BASE_DIR, file_name)
-        context = cls.get_context_for_yml(utm)
 
-        with open(file_to_write, 'w', encoding='utf-8') as file:
-            file.write(render_to_string('prices/price.yml', context).strip())
+# --- command block ---
+class Command(BaseCommand):
+    """Generate yml file for a given vendor (YM or price.ru)."""
+
+    # price files will be stored at this dir
+    BASE_DIR = settings.ASSETS_DIR
 
-        return '{} generated...'.format(file_name)
+    def handle(self, *args, **options):
+        Files(
+            [File(
+                path=os.path.join(self.BASE_DIR, filename),
+                context=Context(target).context()
+            ) for target, filename in settings.UTM_PRICE_MAP.items()]
+        ).create()
diff --git a/shopelectro/settings/base.py b/shopelectro/settings/base.py
--- a/shopelectro/settings/base.py
+++ b/shopelectro/settings/base.py
@@ -487,6 +487,7 @@ def get_robots_content():
 
 # Online market services, that works with our prices.
 # Dict keys - url targets for every service.
+# @todo #666:30m  Move UTM_PRICE_MAP to an iterable Enum
 UTM_PRICE_MAP = {
     'YM': 'yandex.yml',
     'priceru': 'priceru.xml',


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
@@ -218,7 +218,7 @@ class GeneratePrices(TestCase):
     def setUpTestData(cls):
         cls.call_command_patched('price')
         super(GeneratePrices, cls).setUpTestData()
-        cls.prices = Prices(['GM', 'YM', 'priceru', 'SE78'])
+        cls.prices = Prices(settings.UTM_PRICE_MAP.keys())
 
     @classmethod
     def tearDownClass(cls):
@@ -229,7 +229,7 @@ def tearDownClass(cls):
     def call_command_patched(cls, name):
         """Patch with test constants and call."""
         with mock.patch(
-            'shopelectro.management.commands.price.Command.IGNORED_CATEGORIES_MAP',
+            'shopelectro.management.commands.price.Context.IGNORED_CATEGORIES_MAP',
             new_callable=mock.PropertyMock
         ) as target:
             target.return_value = defaultdict(list, {