added guide type field, added tasks, refactored GuideElement model, added endpoint,

This commit is contained in:
Anatoly 2019-12-25 22:14:35 +03:00
parent 65746b1cd0
commit d9df886b98
10 changed files with 458 additions and 15 deletions

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.7 on 2019-12-25 18:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('collection', '0028_merge_20191223_1415'),
('collection', '0028_guide_initial_objects_counter'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-25 18:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0029_merge_20191225_1819'),
]
operations = [
migrations.AddField(
model_name='guidefilter',
name='guide_type',
field=models.PositiveSmallIntegerField(choices=[(0, 'Restaurant'), (1, 'Artisan'), (2, 'Wine')], default=0, verbose_name='guide type'),
),
]

View File

@ -5,17 +5,19 @@ from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel
from mptt.models import MPTTModel, TreeForeignKey
from slugify import slugify
from location.models import Country, Region, WineRegion, WineSubRegion
from location.models import Country, Region, WineRegion, WineSubRegion, City
from review.models import Review
from product.models import Product
from translation.models import Language
from utils.models import IntermediateGalleryModelMixin
from utils.models import (
ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
URLImageMixin, IntermediateGalleryModelMixin
)
from utils.methods import slug_into_section_name
from utils.querysets import RelatedObjectsCountMixin
@ -381,6 +383,20 @@ class GuideFilterQuerySet(models.QuerySet):
class GuideFilter(ProjectBaseMixin):
"""Guide filter model."""
RESTAURANT = 0
ARTISAN = 1
WINE = 2
GUIDE_TYPES = (
(RESTAURANT, _('Restaurant')),
(ARTISAN, _('Artisan')),
(WINE, _('Wine')),
)
guide_type = models.PositiveSmallIntegerField(choices=GUIDE_TYPES,
default=RESTAURANT,
verbose_name=_('guide type'))
establishment_type_json = JSONField(blank=True, null=True,
verbose_name='establishment types')
country_json = JSONField(blank=True, null=True,
@ -568,13 +584,73 @@ class GuideFilter(ProjectBaseMixin):
'public_mark__isnull': False,
})
if self.locale_json:
if self.locales:
filters.update({
'reviews__text__has_any_keys': self.locales,
})
return filters
@property
def product_filter_set(self):
filters = {
# establishment.Establishment
'establishment__public_mark__in': [self.min_mark, self.max_mark],
# review.Reviews
'reviews__vintage__in': self.review_vintages,
}
if self.establishment_type_ids:
filters.update({
# establishment.EstablishmentType
'establishment__establishment_type_id__in': self.establishment_type_ids,
})
if self.country_ids:
filters.update({
# location.Country
'establishment__address__city__country_id__in': self.country_ids,
})
if self.region_ids:
filters.update({
# location.Region
'establishment__address__city__region__parent_id__in': self.region_ids,
})
if self.sub_region_ids:
filters.update({
# location.Region
'establishment__address__city__region__parent_id__in': self.region_ids,
'establishment__address__city__region_id__in': self.sub_region_ids,
})
if self.wine_region_ids:
filters.update({
# location.WineRegion
'wine_origins__wine_region_id__in': self.wine_region_ids,
})
if self.with_mark:
filters.update({
# establishment.Establishment
'establishment__public_mark__isnull': False,
})
if self.locales:
filters.update({
'reviews__text__has_any_keys': self.locales,
})
return filters
@property
def filter_set(self):
if self.guide_type in [self.RESTAURANT, self.ARTISAN]:
return self.establishment_filter_set
elif self.guide_type == self.WINE:
return self.product_filter_set
class GuideElementType(models.Model):
"""Model for type of guide elements."""
@ -657,6 +733,138 @@ class GuideElementManager(models.Manager):
if qs.exists():
return qs.first()
def get_or_create_root_node(self, guide_id: int):
"""Get or Create RootNode."""
guide_element_type_qs = GuideElementType.objects.filter(name='Root')
guide_qs = Guide.objects.filter(id=guide_id)
if guide_element_type_qs.exists() and guide_qs.exists():
guide = guide_qs.first()
return self.get_or_create(guide_id=guide_qs.first(),
guide_element_type=guide_element_type_qs.first(),
defaults={
'guide_id': guide.id,
'guide_element_type': guide_element_type_qs.first()})
return None, False
def get_or_create_city_node(self, root_node_id: int, city_id: int):
"""Get or Create CityNode."""
parent_node_qs = GuideElement.objects.filter(id=root_node_id)
guide_element_type_qs = GuideElementType.objects.filter(name='CityNode')
city_qs = City.objects.filter(id=city_id)
if parent_node_qs.exists() and city_qs.exists() and guide_element_type_qs.exists():
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=parent_node_qs.first(),
city=city_qs.first())
return None, False
def get_or_create_establishment_section_node(self, city_node: int, establishment_node_name: str):
"""Get or Create (Restaurant|Shop...)SectionNode."""
parent_node_qs = GuideElement.objects.filter(id=city_node)
guide_element_type_qs = GuideElementType.objects.filter(name__iexact=establishment_node_name)
if parent_node_qs.exists() and guide_element_type_qs.exists():
parent_node = parent_node_qs.first()
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=parent_node,
guide=parent_node.get_root().guide)
return None, False
def get_or_create_establishment_node(self, restaurant_section_node_id: int,
establishment_id: int, review_id: int = None):
"""Get or Create EstablishmentNode."""
from establishment.models import Establishment
data = {}
guide_element_type_qs = GuideElementType.objects.filter(name='EstablishmentNode')
parent_node_qs = GuideElement.objects.filter(id=restaurant_section_node_id)
establishment_qs = Establishment.objects.filter(id=establishment_id)
if parent_node_qs.exists() and establishment_qs.exists() and guide_element_type_qs.exists():
establishment = establishment_qs.first()
parent_node = parent_node_qs.first()
data.update({
'guide_element_type': guide_element_type_qs.first(),
'parent': parent_node,
'guide': parent_node.get_root().guide,
'establishment': establishment
})
if review_id:
review_qs = Review.objects.filter(id=review_id)
if review_qs.exists(): data.update({'review_id': review_qs.first().id})
return self.get_or_create(**data)
return None, False
def get_or_create_wine_region_node(self, root_node_id: int, wine_region_id: int):
"""Get or Create WineRegionNode."""
guide_element_type_qs = GuideElementType.objects.filter(name='RegionNode')
parent_node_qs = GuideElement.objects.filter(id=root_node_id)
wine_region_qs = WineRegion.objects.filter(id=wine_region_id)
if parent_node_qs.exists() and parent_node_qs.first().guide and wine_region_qs.exists() and guide_element_type_qs.exists():
root_node = parent_node_qs.first()
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=root_node,
guide=root_node.guide,
wine_region=wine_region_qs.first())
return None, False
def get_or_create_yard_node(self, product_id: int, wine_region_node_id: int):
"""Make YardNode."""
from establishment.models import Establishment
guide_element_type_qs = GuideElementType.objects.filter(name='YardNode')
wine_region_node_qs = GuideElement.objects.filter(id=wine_region_node_id)
product_qs = Product.objects.filter(id=product_id)
if product_qs.exists() and wine_region_node_qs.exists():
wine_region_node = wine_region_node_qs.first()
root_node = wine_region_node.get_root()
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=wine_region_node,
guide=root_node.guide,
product=product_qs.first())
return None, False
def get_or_create_color_wine_section_node(self, wine_color_name: str, yard_node_id: int):
"""Get or Create WineSectionNode."""
guide_element_type_qs = GuideElementType.objects.filter(name='ColorWineSectionNode')
parent_node_qs = GuideElement.objects.filter(id=yard_node_id)
if not wine_color_name.endswith('SectionNode'):
wine_color_name = slug_into_section_name(wine_color_name)
wine_color_section, _ = GuideWineColorSection.objects.get_or_create(
name=wine_color_name,
defaults={
'name': wine_color_name
})
if parent_node_qs.exists() and guide_element_type_qs.exists():
root_node = parent_node_qs.first().get_root()
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=root_node,
wine_color_section=wine_color_section,
guide=root_node.guide)
return None, False
def get_or_create_wine_node(self, color_wine_section_node_id: int, wine_id: int, review_id: int):
"""Get or Create WineNode."""
guide_element_type_qs = GuideElementType.objects.filter(name='WineNode')
parent_node_qs = GuideElement.objects.filter(id=color_wine_section_node_id)
wine_qs = Product.objects.wines().filter(id=wine_id)
review_qs = Review.objects.filter(id=review_id)
if parent_node_qs.exists() and wine_qs.exists() and review_qs.exists() and guide_element_type_qs.exists():
root_node = parent_node_qs.first().get_root()
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=root_node,
product=wine_qs.first(),
guide=root_node.guide,
review=review_qs.first())
return None, False
class GuideElementQuerySet(models.QuerySet):
"""QuerySet for model Guide elements."""

View File

@ -8,6 +8,7 @@ from rest_framework_recursive.fields import RecursiveField
from establishment.serializers import EstablishmentGuideElementSerializer
from product.serializers import ProductGuideElementSerializer
from django.shortcuts import get_object_or_404
from utils import exceptions
class CollectionBaseSerializer(serializers.ModelSerializer):
@ -203,3 +204,40 @@ class GuideElementBaseSerializer(serializers.ModelSerializer):
'parent',
'label_photo',
]
class AdvertorialBaseSerializer(serializers.ModelSerializer):
"""Serializer for model Advertorial."""
class Meta:
"""Meta class."""
model = models.Advertorial
fields = [
'number_of_pages',
'right_pages',
'guide_element',
]
extra_kwargs = {
'guide_element': {'required': False}
}
@property
def request_kwargs(self):
return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs):
# check existence in guide
guide = get_object_or_404(models.Guide.objects.all(),
pk=self.request_kwargs.get('pk'))
root_node = models.GuideElement.objects.get_root_node(guide)
guide_element_qs = root_node.get_children().filter(pk=self.request_kwargs.get('element_pk'))
guide_element = guide_element_qs.first()
if not guide_element_qs.exists():
raise exceptions.GuideElementError()
if models.Advertorial.objects.filter(guide_element=guide_element).exists():
raise exceptions.AdvertorialError()
attrs['guide_element'] = guide_element
return attrs

View File

@ -1,21 +1,133 @@
"""Collectoin app celery tasks."""
"""Collection app celery tasks."""
import logging
from celery import shared_task
from django.utils.translation import gettext_lazy as _
from collection import models as collection_models
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
# todo: finish this
def get_additional_establishment_data(section_node, establishment):
data = [
section_node.id,
establishment.id,
]
if establishment.last_published_review:
data.append(establishment.last_published_review.id)
return data
def get_additional_product_data(section_node, product):
data = [
section_node.id,
product.id,
]
if product.last_published_review:
data.append(product.last_published_review.id)
return data
@shared_task
def generate_guide_elements(guide_id: int):
"""Send verification email to user."""
def generate_establishment_guide_elements(guide_id: int, queryset_values: dict, section_node_name: str):
"""Generate guide elements."""
from collection.models import GuideElement, Guide
from establishment.models import Establishment
guide = Guide.objects.get(id=guide_id)
try:
obj = collection_models.Guide.objects.get(id=guide_id)
for instance in queryset_values:
establishment_id = instance.get('id')
establishment_qs = Establishment.objects.filter(id=establishment_id)
if establishment_qs.exists():
establishment = establishment_qs.first()
root_node, _ = GuideElement.objects.get_or_create_root_node(guide_id)
if root_node:
city_node, _ = GuideElement.objects.get_or_create_city_node(root_node.id,
establishment.address.city_id)
if city_node:
section_node, _ = GuideElement.objects.get_or_create_establishment_section_node(
city_node.id,
section_node_name,
)
if section_node:
GuideElement.objects.get_or_create_establishment_node(
*get_additional_establishment_data(section_node=section_node,
establishment=establishment))
else:
logger.error(
f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - SectionNode is not exists.')
else:
logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - CityNode is not exists.')
else:
logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - RootNode is not exists.')
else:
logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - Establishment {establishment_id} id is not exists.')
except Exception as e:
logger.error(f'METHOD_NAME: {generate_guide_elements.__name__}\n'
f'DETAIL: guide {guide_id}, - {e}')
logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - {e}')
else:
guide.update_count_related_objects()
# Update tree indexes
GuideElement._tree_manager.rebuild()
@shared_task
def generate_product_guide_elements(guide_id: int, queryset_values: dict):
"""Generate guide elements."""
from collection.models import GuideElement, Guide
from product.models import Product
guide = Guide.objects.get(id=guide_id)
try:
for instance in queryset_values:
wine_id = instance.get('id')
wine_qs = Product.objects.filter(id=wine_id)
if wine_qs.exists():
wine = wine_qs.first()
root_node, _ = GuideElement.objects.get_or_create_root_node(guide_id)
if root_node:
wine_region_node, _ = GuideElement.objects.get_or_create_wine_region_node(
root_node.id,
wine.wine_region.id)
if wine_region_node:
yard_node, _ = GuideElement.objects.get_or_create_yard_node(
product_id=wine.id,
wine_region_node_id=wine_region_node.id
)
if yard_node:
wine_color_qs = wine.wine_colors
if wine_color_qs.exists():
wine_color_section, _ = GuideElement.objects.get_or_create_color_wine_section_node(
wine_color_name=wine_color_qs.first().value,
yard_node_id=yard_node.id
)
if wine_color_section:
GuideElement.objects.get_or_create_wine_node(
*get_additional_product_data(
section_node=wine_color_section,
product=wine))
else:
logger.error(
f'METHOD_NAME: {generate_product_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - Wine {wine.id} has not colors.')
else:
logger.error(
f'METHOD_NAME: {generate_product_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - WineRegionNode is not exists.')
else:
logger.error(f'METHOD_NAME: {generate_product_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - RootNode is not exists.')
else:
logger.error(f'METHOD_NAME: {generate_product_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - Product {wine_id} id is not exists.')
except Exception as e:
logger.error(f'METHOD_NAME: {generate_product_guide_elements.__name__}\n'
f'DETAIL: Guide ID {guide_id} - {e}')
else:
guide.update_count_related_objects()
# Update tree indexes
GuideElement._tree_manager.rebuild()

View File

@ -14,6 +14,8 @@ urlpatterns = [
name='guide-list-create'),
path('guides/<int:pk>/', views.GuideElementListView.as_view(),
name='guide-element-list'),
path('guides/<int:pk>/element/<int:element_pk>/advertorial/', views.AdvertorialCreateDestroyView.as_view(),
name='guide-advertorial-create-destroy'),
path('guides/<int:pk>/filters/', views.GuideFilterCreateView.as_view(),
name='guide-filter-list-create'),
] + router.urls

View File

@ -55,6 +55,14 @@ class GuideElementBaseView(generics.GenericAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class AdvertorialBaseView(generics.GenericAPIView):
"""Base view for Advertorial model."""
pagination_class = None
queryset = models.Advertorial.objects.all()
serializer_class = serializers.AdvertorialBaseSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
@ -118,3 +126,9 @@ class GuideElementListView(GuideElementBaseView,
guide = get_object_or_404(models.Guide.objects.all(), pk=self.kwargs.get('pk'))
return models.GuideElement.objects.get_root_node(guide) \
.get_descendants()
class AdvertorialCreateDestroyView(AdvertorialBaseView,
generics.CreateAPIView,
generics.DestroyAPIView):
"""View for model Advertorial for back office users."""

View File

@ -120,7 +120,7 @@ class ProductQuerySet(models.QuerySet):
return self.filter(category=self.model.ONLINE)
def wines(self):
return self.filter(type__index_name__icontains=ProductType.WINE)
return self.filter(product_type__index_name__icontains=ProductType.WINE)
def without_current_product(self, current_product: str):
"""Exclude by current product."""
@ -383,6 +383,11 @@ class Product(GalleryMixin, TranslatedFieldsMixin, BaseAttributes,
if self.main_image:
return self.main_image.get_image_url(thumbnail_key='product_preview')
@property
def wine_region(self):
if self.wine_origins.exists():
return self.wine_origins.first().wine_region
class OnlineProductManager(ProductManager):
"""Extended manger for OnlineProduct model."""

View File

@ -179,3 +179,21 @@ class UnprocessableEntityError(exceptions.APIException):
"""
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
default_detail = _('Unprocessable entity valid.')
class GuideElementError(exceptions.APIException):
"""
The exception should be raised when user tries send guide element that doesn't
valid for guide.
"""
status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Guide element not valid for Guide.')
class AdvertorialError(exceptions.APIException):
"""
The exception should be raised when user tries create advertorial for guide element
that already exists.
"""
status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Advertorial already exists for this guide element.')

View File

@ -140,4 +140,18 @@ def dictfetchall(cursor):
return [
dict(zip(columns, row))
for row in cursor.fetchall()
]
]
def slug_into_section_name(slug: str, postfix: str = 'SectionNode'):
"""
Transform slug into section name, i.e:
like
"EffervescentRoseDeSaigneeSectionNode"
from
"effervescent-rose-de-saignee"
"""
re_exp = r'[\w]+'
result = re.findall(re_exp, slug)
if result:
return f"{''.join([i.capitalize() for i in result])}{postfix}"