Skip to content

Commit

Permalink
#666 Init OOP arch for prices command (#705)
Browse files Browse the repository at this point in the history
* #255  Increase selenium timeout value

* #666  Create initial OOP structure for `price` command

* #666  Review fixes

* #666  Fix error in tests

* #666  Fix arch for File(s) classes
  • Loading branch information
duker33 committed Jan 29, 2019
1 parent eedafb4 commit 234023f
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 47 deletions.
4 changes: 2 additions & 2 deletions docker/drone_env/app
Expand Up @@ -17,5 +17,5 @@ REDIS_PORT=6379
RABBITMQ_URL=rabbitmq
RABBITMQ_PORT=5672

SELENIUM_WAIT_SECONDS=60
SELENIUM_TIMEOUT_SECONDS=30
SELENIUM_WAIT_SECONDS=120
SELENIUM_TIMEOUT_SECONDS=60
119 changes: 76 additions & 43 deletions shopelectro/management/commands/price.py
@@ -1,15 +1,47 @@
from collections import defaultdict
import os
import typing
from collections import defaultdict

from django.conf import settings
from django.core.management.base import BaseCommand
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, 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."""

Expand All @@ -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 = [
'Измерительные приборы', 'Новогодние вращающиеся светодиодные лампы',
Expand All @@ -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)),
]
Expand Down Expand Up @@ -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':
"""
Expand All @@ -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()
1 change: 1 addition & 0 deletions shopelectro/settings/base.py
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions shopelectro/tests/tests_commands.py
Expand Up @@ -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):
Expand All @@ -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, {
Expand Down

3 comments on commit 234023f

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on 234023f Jan 29, 2019

Choose a reason for hiding this comment

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

Puzzle 661-ec781b69 disappeared from shopelectro/management/commands/price.py, that's why I closed #666. 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 234023f Jan 29, 2019

Choose a reason for hiding this comment

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

Puzzle 666-c099ca68 discovered in shopelectro/settings/base.py and submitted as #714. 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 234023f Jan 29, 2019

Choose a reason for hiding this comment

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

Puzzle 666-23eb8556 discovered in shopelectro/management/commands/price.py and submitted as #715. 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.