From 8e215b2555509ea7556ad6d8995f4bb1b8b4b68b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 21 Nov 2019 16:40:53 +0300 Subject: [PATCH 001/191] Demonstration carousel item --- apps/main/views/common.py | 2 +- project/settings/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/main/views/common.py b/apps/main/views/common.py index 15f89510..18ee0d8d 100644 --- a/apps/main/views/common.py +++ b/apps/main/views/common.py @@ -70,7 +70,7 @@ class CarouselListView(generics.ListAPIView): def get_queryset(self): country_code = self.request.country_code - if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in ['www', 'main']: + if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in settings.INTERNATIONAL_COUNTRY_CODES: qs = models.Carousel.objects.filter(id__in=settings.CAROUSEL_ITEMS) return qs qs = models.Carousel.objects.is_parsed().active() diff --git a/project/settings/base.py b/project/settings/base.py index 4299c379..a2aa1af1 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -498,7 +498,7 @@ FALLBACK_LOCALE = 'en-GB' # TMP TODO remove it later # Временный хардкод для демонстрации > 15 ноября, потом удалить! -CAROUSEL_ITEMS = [230, 231, 232] +CAROUSEL_ITEMS = [465] ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop'] NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] From 86e29173ba940acb82e9d146bc2a5d595f8d61fe Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 21 Nov 2019 16:45:27 +0300 Subject: [PATCH 002/191] transfer partner --- apps/partner/transfer_data.py | 5 +++ apps/product/transfer_data.py | 50 +++++++++------------- apps/transfer/serializers/partner.py | 62 +++++++++++++++++++--------- 3 files changed, 67 insertions(+), 50 deletions(-) diff --git a/apps/partner/transfer_data.py b/apps/partner/transfer_data.py index 868345a8..acda6c2c 100644 --- a/apps/partner/transfer_data.py +++ b/apps/partner/transfer_data.py @@ -1,11 +1,15 @@ from pprint import pprint from establishment.models import Establishment +from partner.models import Partner from transfer.models import EstablishmentBacklinks from transfer.serializers.partner import PartnerSerializer def transfer_partner(): + """ + Transfer data to Partner model only after transfer Establishment + """ establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) queryset = EstablishmentBacklinks.objects.filter( establishment_id__in=list(establishments), @@ -24,6 +28,7 @@ def transfer_partner(): serialized_data = PartnerSerializer(data=list(queryset), many=True) if serialized_data.is_valid(): + Partner.objects.all().delete() # TODO: закоментить, если требуется сохранить старые записи serialized_data.save() else: pprint(f"Partner serializer errors: {serialized_data.errors}") diff --git a/apps/product/transfer_data.py b/apps/product/transfer_data.py index 8e4a26fa..84a8a524 100644 --- a/apps/product/transfer_data.py +++ b/apps/product/transfer_data.py @@ -2,17 +2,6 @@ from pprint import pprint from transfer import models as transfer_models from transfer.serializers import product as product_serializers -from transfer.serializers.partner import PartnerSerializer - - -def transfer_partner(): - queryset = transfer_models.EstablishmentBacklinks.objects.filter(type="Partner") - - serialized_data = PartnerSerializer(data=list(queryset.values()), many=True) - if serialized_data.is_valid(): - serialized_data.save() - else: - pprint(f"News serializer errors: {serialized_data.errors}") def transfer_wine_color(): @@ -50,8 +39,8 @@ def transfer_wine_bottles_produced(): ) queryset = [vars(query) for query in raw_queryset] serialized_data = product_serializers.WineBottlesProducedSerializer( - data=queryset, - many=True) + data=queryset, + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -69,8 +58,8 @@ def transfer_wine_classification_type(): ) queryset = [vars(query) for query in raw_queryset] serialized_data = product_serializers.WineClassificationTypeSerializer( - data=queryset, - many=True) + data=queryset, + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -79,10 +68,10 @@ def transfer_wine_classification_type(): def transfer_wine_standard(): queryset = transfer_models.ProductClassification.objects.filter(parent_id__isnull=True) \ - .exclude(type='Classification') + .exclude(type='Classification') serialized_data = product_serializers.ProductStandardSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -92,8 +81,8 @@ def transfer_wine_standard(): def transfer_wine_classifications(): queryset = transfer_models.ProductClassification.objects.filter(type='Classification') serialized_data = product_serializers.ProductClassificationSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -104,8 +93,8 @@ def transfer_product(): errors = [] queryset = transfer_models.Products.objects.all() serialized_data = product_serializers.ProductSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -117,8 +106,8 @@ def transfer_product_note(): errors = [] queryset = transfer_models.ProductNotes.objects.exclude(text='') serialized_data = product_serializers.ProductNoteSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -130,8 +119,8 @@ def transfer_plate(): errors = [] queryset = transfer_models.Merchandise.objects.all() serialized_data = product_serializers.PlateSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -143,8 +132,8 @@ def transfer_plate_image(): errors = [] queryset = transfer_models.Merchandise.objects.all() serialized_data = product_serializers.PlateImageSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -153,7 +142,6 @@ def transfer_plate_image(): data_types = { - "partner": [transfer_partner], "wine_characteristics": [ transfer_wine_sugar_content, transfer_wine_color, @@ -161,12 +149,12 @@ data_types = { transfer_wine_classification_type, transfer_wine_standard, transfer_wine_classifications, - ], + ], "product": [ transfer_product, ], "product_note": [ - transfer_product_note, + transfer_product_note, ], "souvenir": [ transfer_plate, diff --git a/apps/transfer/serializers/partner.py b/apps/transfer/serializers/partner.py index 69cf308e..61f56dea 100644 --- a/apps/transfer/serializers/partner.py +++ b/apps/transfer/serializers/partner.py @@ -1,29 +1,53 @@ from rest_framework import serializers + +from establishment.models import Establishment from partner.models import Partner class PartnerSerializer(serializers.Serializer): - pass - # 'id', - # 'establishment_id', - # 'partnership_name', - # 'partnership_icon', - # 'backlink_url', - # 'created_at', - # 'type', - # 'starting_date', - # 'expiry_date', - # 'price_per_month', + id = serializers.IntegerField() + establishment_id = serializers.IntegerField() + partnership_name = serializers.CharField(allow_null=True) + partnership_icon = serializers.CharField(allow_null=True) + backlink_url = serializers.CharField(allow_null=True) + created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') + type = serializers.CharField(allow_null=True) + starting_date = serializers.DateField(allow_null=True) + expiry_date = serializers.DateField(allow_null=True) + price_per_month = serializers.DecimalField(max_digits=10, decimal_places=2, allow_null=True) + def validate(self, data): + data.update({ + 'old_id': data.pop('id'), + 'name': data['partnership_name'], + 'url': data.pop('backlink_url'), + 'image': self.get_image(data), + 'establishment': self.get_establishment(data), + 'type': Partner.PARTNER if data['type'] == 'Partner' else Partner.SPONSOR, + 'created': data.pop('created_at'), + }) + data.pop('partnership_icon') + data.pop('partnership_name') + data.pop('establishment_id') + return data - # def validate(self, data): - # data["image"] = partnership_to_image_url.get(data["partnership_name"]).get(data["partnership_icon"]) - # data.pop("partnership_name") - # data.pop("partnership_icon") - # return data - # - # def create(self, validated_data): - # return Partner.objects.create(**validated_data) + @staticmethod + def get_image(data): + return partnership_to_image_url.get(data['partnership_name']).get(data['partnership_icon']) + + @staticmethod + def get_establishment(data): + establishment = Establishment.objects.filter(old_id=data['establishment_id']).first() + if not establishment: + raise ValueError(f"Establishment not found with old_id {data['establishment_id']}: ") + return establishment + + def create(self, validated_data): + obj, _ = Partner.objects.update_or_create( + old_id=validated_data['old_id'], + defaults=validated_data, + ) + return obj partnership_to_image_url = { From 98e140b0c4778841eda1b7bae2a2034d163539fd Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Thu, 21 Nov 2019 14:12:22 +0000 Subject: [PATCH 003/191] Feature/news web view update --- apps/news/filters.py | 15 +++++++++++++++ apps/news/serializers.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/apps/news/filters.py b/apps/news/filters.py index 6ade7eeb..e8e35307 100644 --- a/apps/news/filters.py +++ b/apps/news/filters.py @@ -20,6 +20,16 @@ class NewsListFilterSet(filters.FilterSet): tag_value__in = filters.CharFilter(method='in_tags') type = filters.CharFilter(method='by_type') + state = filters.NumberFilter() + + SORT_BY_CREATED_CHOICE = "created" + SORT_BY_START_CHOICE = "start" + SORT_BY_CHOICES = ( + (SORT_BY_CREATED_CHOICE, "created"), + (SORT_BY_START_CHOICE, "start"), + ) + sort_by = filters.ChoiceFilter(method='sort_by_field', choices=SORT_BY_CHOICES) + class Meta: """Meta class""" model = models.News @@ -29,6 +39,8 @@ class NewsListFilterSet(filters.FilterSet): 'tag_group', 'tag_value__exclude', 'tag_value__in', + 'state', + 'sort_by', ) def in_tags(self, queryset, name, value): @@ -58,3 +70,6 @@ class NewsListFilterSet(filters.FilterSet): return queryset.filter(news_type__name=value) else: return queryset + + def sort_by_field(self, queryset, name, value): + return queryset.order_by(f'-{value}') diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 4eaeaeb4..4a84fa11 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -162,6 +162,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer): class NewsBackOfficeBaseSerializer(NewsBaseSerializer): """News back office base serializer.""" + is_published = serializers.CharField(source='is_publish', read_only=True) class Meta(NewsBaseSerializer.Meta): """Meta class.""" @@ -169,6 +170,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): fields = NewsBaseSerializer.Meta.fields + ( 'title', 'subtitle', + 'is_published', ) From 4209b38d93c04e713b342be5ffa9ee88759b502f Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Thu, 21 Nov 2019 14:46:56 +0000 Subject: [PATCH 004/191] Fixed is_published filed type in serializer --- apps/news/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 4a84fa11..2264b9ec 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -162,7 +162,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer): class NewsBackOfficeBaseSerializer(NewsBaseSerializer): """News back office base serializer.""" - is_published = serializers.CharField(source='is_publish', read_only=True) + is_published = serializers.BooleanField(source='is_publish', read_only=True) class Meta(NewsBaseSerializer.Meta): """Meta class.""" From 35b6a66c8541548a5cd899a0ba2def440ede7295 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Thu, 21 Nov 2019 18:12:52 +0300 Subject: [PATCH 005/191] In favorites --- apps/establishment/models.py | 6 ++- apps/establishment/views/web.py | 23 +++------ apps/news/models.py | 6 ++- apps/news/views.py | 16 ++---- apps/product/models.py | 5 +- apps/product/views/common.py | 21 ++------ apps/search_indexes/documents/__init__.py | 3 +- .../search_indexes/documents/establishment.py | 1 + apps/search_indexes/documents/news.py | 3 +- apps/search_indexes/documents/product.py | 1 + apps/search_indexes/serializers.py | 24 +++++++-- apps/search_indexes/tasks.py | 50 +++++++++++++++++++ apps/utils/models.py | 8 +++ apps/utils/views.py | 39 ++++++++++++++- docker-compose.yml | 2 +- project/settings/base.py | 11 ++++ requirements/base.txt | 1 + run_celery_beat.sh | 2 +- 18 files changed, 161 insertions(+), 61 deletions(-) create mode 100644 apps/search_indexes/tasks.py diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 3aee6c31..a6737850 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -25,7 +25,8 @@ from main.models import Award, Currency from review.models import Review from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin, - IntermediateGalleryModelMixin, HasTagsMixin) + IntermediateGalleryModelMixin, HasTagsMixin, + FavoritesMixin) # todo: establishment type&subtypes check @@ -319,7 +320,8 @@ class EstablishmentQuerySet(models.QuerySet): return self.exclude(address__city__country__in=countries) -class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin, HasTagsMixin): +class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, + TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin): """Establishment model.""" # todo: delete image URL fields after moving on gallery diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 20e8f81a..4f1fe07c 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -5,12 +5,11 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions from comment import models as comment_models -from establishment import filters -from establishment import models, serializers +from comment.serializers import CommentRUDSerializer +from establishment import filters, models, serializers from main import methods from utils.pagination import EstablishmentPortionPagination -from utils.permissions import IsCountryAdmin -from comment.serializers import CommentRUDSerializer +from utils.views import FavoritesCreateDestroyMixinView class EstablishmentMixinView: @@ -134,21 +133,11 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): return comment_obj -class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView): +class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView): """View for create/destroy establishment from favorites.""" - serializer_class = serializers.EstablishmentFavoritesCreateSerializer - lookup_field = 'slug' - def get_object(self): - """ - Returns the object the view is displaying. - """ - establishment = get_object_or_404(models.Establishment, - slug=self.kwargs['slug']) - favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user)) - # May raise a permission denied - self.check_object_permissions(self.request, favorites) - return favorites + _model = models.Establishment + serializer_class = serializers.EstablishmentFavoritesCreateSerializer class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView): diff --git a/apps/news/models.py b/apps/news/models.py index d0e79c64..8a86e688 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -8,7 +8,8 @@ from rest_framework.reverse import reverse from rating.models import Rating, ViewCount from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin, - ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin) + ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin, + FavoritesMixin) from utils.querysets import TranslationQuerysetMixin from django.conf import settings @@ -126,7 +127,8 @@ class NewsQuerySet(TranslationQuerysetMixin): ) -class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin): +class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, + FavoritesMixin): """News model.""" STR_FIELD_NAME = 'title' diff --git a/apps/news/views.py b/apps/news/views.py index 3e841246..a215947a 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -6,7 +6,7 @@ from rest_framework import generics, permissions from news import filters, models, serializers from rating.tasks import add_rating from utils.permissions import IsCountryAdmin, IsContentPageManager -from utils.views import CreateDestroyGalleryViewMixin +from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView from utils.serializers import ImageBaseSerializer @@ -150,18 +150,8 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView, return self.retrieve(request, *args, **kwargs) -class NewsFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView): +class NewsFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView): """View for create/destroy news from favorites.""" + _model = models.News serializer_class = serializers.NewsFavoritesCreateSerializer - lookup_field = 'slug' - - def get_object(self): - """ - Returns the object the view is displaying. - """ - news = get_object_or_404(models.News, slug=self.kwargs['slug']) - favorites = get_object_or_404(news.favorites.filter(user=self.request.user)) - # May raise a permission denied - self.check_object_permissions(self.request, favorites) - return favorites diff --git a/apps/product/models.py b/apps/product/models.py index 6baaf43e..896eb211 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from django.core.validators import MaxValueValidator, MinValueValidator from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin, - TranslatedFieldsMixin, TJSONField, + TranslatedFieldsMixin, TJSONField, FavoritesMixin, GalleryModelMixin, IntermediateGalleryModelMixin) @@ -131,7 +131,8 @@ class ProductQuerySet(models.QuerySet): ) -class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsMixin): +class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, + HasTagsMixin, FavoritesMixin): """Product models.""" EARLIEST_VINTAGE_YEAR = 1700 diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 8b857ddb..f984a87b 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -3,9 +3,9 @@ from rest_framework import generics, permissions from django.shortcuts import get_object_or_404 from product.models import Product from comment.models import Comment -from product import serializers -from product import filters +from product import filters, serializers from comment.serializers import CommentRUDSerializer +from utils.views import FavoritesCreateDestroyMixinView class ProductBaseView(generics.GenericAPIView): @@ -37,22 +37,11 @@ class ProductDetailView(ProductBaseView, generics.RetrieveAPIView): serializer_class = serializers.ProductDetailSerializer -class CreateFavoriteProductView(generics.CreateAPIView, - generics.DestroyAPIView): +class CreateFavoriteProductView(FavoritesCreateDestroyMixinView): """View for create/destroy product in favorites.""" + + _model = Product serializer_class = serializers.ProductFavoritesCreateSerializer - lookup_field = 'slug' - - def get_object(self): - """ - Returns the object the view is displaying. - """ - product = get_object_or_404(Product, slug=self.kwargs['slug']) - favorites = get_object_or_404(product.favorites.filter(user=self.request.user)) - - # May raise a permission denied - self.check_object_permissions(self.request, favorites) - return favorites class ProductCommentCreateView(generics.CreateAPIView): diff --git a/apps/search_indexes/documents/__init__.py b/apps/search_indexes/documents/__init__.py index 70d17330..c357f29e 100644 --- a/apps/search_indexes/documents/__init__.py +++ b/apps/search_indexes/documents/__init__.py @@ -1,11 +1,12 @@ from search_indexes.documents.establishment import EstablishmentDocument from search_indexes.documents.news import NewsDocument from search_indexes.documents.product import ProductDocument - +from search_indexes.tasks import es_update # todo: make signal to update documents on related fields __all__ = [ 'EstablishmentDocument', 'NewsDocument', 'ProductDocument', + 'es_update', ] \ No newline at end of file diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 8b4e5c3c..45923182 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -113,6 +113,7 @@ class EstablishmentDocument(Document): ), }, ) + favorites_for_users = fields.ListField(field=fields.IntegerField()) class Django: diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index e39036d3..1fa2f9d9 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -41,6 +41,7 @@ class NewsDocument(Document): properties=OBJECT_FIELD_PROPERTIES), }, multi=True) + favorites_for_users = fields.ListField(field=fields.IntegerField()) class Django: @@ -57,7 +58,7 @@ class NewsDocument(Document): related_models = [models.NewsType] def get_queryset(self): - return super().get_queryset().published().with_base_related().sort_by_start() + return super().get_queryset().published().with_base_related() def get_instances_from_related(self, related_instance): """If related_models is set, define how to retrieve the Car instance(s) from the related model. diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index 1a092dac..61c4eeeb 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -148,6 +148,7 @@ class ProductDocument(Document): name = fields.TextField(attr='display_name', analyzer='english') name_ru = fields.TextField(attr='display_name', analyzer='russian') name_fr = fields.TextField(attr='display_name', analyzer='french') + favorites_for_users = fields.ListField(field=fields.IntegerField()) class Django: model = models.Product diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index cac4e336..84782f0c 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -167,7 +167,25 @@ class ScheduleDocumentSerializer(serializers.Serializer): closed_at = serializers.CharField() -class NewsDocumentSerializer(DocumentSerializer): +class InFavoritesMixin(DocumentSerializer): + """Append in_favorites field.""" + + in_favorites = serializers.SerializerMethodField() + + def get_in_favorites(self, obj): + request = self.context['request'] + user = request.user + if user.is_authenticated: + return user.id in obj.favorites_for_users + return False + + class Meta: + """Meta class.""" + + _abstract = True + + +class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer): """News document serializer.""" title_translated = serializers.SerializerMethodField(allow_null=True) @@ -200,7 +218,7 @@ class NewsDocumentSerializer(DocumentSerializer): return get_translated_value(obj.subtitle) -class EstablishmentDocumentSerializer(DocumentSerializer): +class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): """Establishment document serializer.""" establishment_type = EstablishmentTypeSerializer() @@ -236,7 +254,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer): ) -class ProductDocumentSerializer(DocumentSerializer): +class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer): """Product document serializer""" tags = TagsDocumentSerializer(many=True, source='related_tags') diff --git a/apps/search_indexes/tasks.py b/apps/search_indexes/tasks.py new file mode 100644 index 00000000..4935814d --- /dev/null +++ b/apps/search_indexes/tasks.py @@ -0,0 +1,50 @@ +"""SearchIndex tasks.""" +import logging +from django.db import models +from celery.schedules import crontab +from celery.task import periodic_task +from django_elasticsearch_dsl.registries import registry +from django_redis import get_redis_connection +from establishment.models import Establishment +from news.models import News +from product.models import Product + + +logger = logging.getLogger(__name__) + + +@periodic_task(run_every=crontab(minute=1)) +def update_index(): + """Updates ES index.""" + try: + cn = get_redis_connection('es_queue') + for model in [Establishment, News, Product]: + model_name = model.__name__.lower() + while True: + ids = cn.spop(model_name, 500) + if not ids: + break + qs = model.objects.filter(id__in=ids) + try: + doc = registry.get_documents([model]).pop() + except KeyError: + pass + else: + doc().update(qs) + except Exception as ex: + logger.error(f'Updating index failed: {ex}') + + +def es_update(obj): + """Adds object to set of objects for indexing.""" + try: + cn = get_redis_connection('es_queue') + allowed_models = [Establishment, News, Product] + if isinstance(obj, models.QuerySet) and obj.model in allowed_models: + key = obj.model.__name__.lower() + cn.sadd(key, *obj.values_list('id', flat=True)) + elif isinstance(obj, models.Model) and obj.__class__ in allowed_models: + key = obj.__class__.__name__.lower() + cn.sadd(key, obj.id) + except Exception as ex: + logger.warning(f'Send obj to ES failed: {ex}') diff --git a/apps/utils/models.py b/apps/utils/models.py index f86093af..8c186f2f 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -5,6 +5,7 @@ from os.path import exists from django.conf import settings from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.gis.db import models +from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields.jsonb import KeyTextTransform from django.utils import timezone @@ -435,4 +436,11 @@ class HasTagsMixin(models.Model): abstract = True +class FavoritesMixin: + """Append favorites_for_user property.""" + + @property + def favorites_for_users(self): + return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr') + timezone.datetime.now().date().isoformat() \ No newline at end of file diff --git a/apps/utils/views.py b/apps/utils/views.py index d3d09079..0f5f74cd 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -2,11 +2,12 @@ from collections import namedtuple from django.conf import settings from django.db.transaction import on_commit -from rest_framework import generics -from rest_framework import status +from django.shortcuts import get_object_or_404 +from rest_framework import generics, status from rest_framework.response import Response from gallery.tasks import delete_image +from search_indexes.documents import es_update # JWT @@ -121,3 +122,37 @@ class CreateDestroyGalleryViewMixin(generics.CreateAPIView, # Delete an instances of Gallery model gallery_obj.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + +class FavoritesCreateDestroyMixinView(generics.CreateAPIView, + generics.DestroyAPIView): + """Favorites Create Destroy mixin.""" + + _model = None + serializer_class = None + lookup_field = 'slug' + + def get_base_object(self): + return get_object_or_404(self._model, slug=self.kwargs['slug']) + + def get_object(self): + """ + Returns the object the view is displaying. + """ + obj = self.get_base_object() + favorites = get_object_or_404(obj.favorites.filter(user=self.request.user)) + # May raise a permission denied + self.check_object_permissions(self.request, favorites) + return favorites + + def es_update_base_object(self): + es_update(self.get_base_object()) + + def perform_create(self, serializer): + serializer.save() + self.es_update_base_object() + + def perform_destroy(self, instance): + instance.delete() + self.es_update_base_object() + diff --git a/docker-compose.yml b/docker-compose.yml index 48fea8eb..12217d4e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: # Redis redis: - image: redis:2.8.23 + image: redis:latest # Celery worker: diff --git a/project/settings/base.py b/project/settings/base.py index 9c365b52..792ad281 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -250,6 +250,17 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', ) + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, + 'es_queue': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://redis:6379/2' + } +} + # Override default OAuth2 namespace DRFSO2_URL_NAMESPACE = 'auth' SOCIAL_AUTH_URL_NAMESPACE = 'auth' diff --git a/requirements/base.txt b/requirements/base.txt index 8ce99c84..e02e8ac5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -54,5 +54,6 @@ PyYAML==5.1.2 # temp solution redis==3.2.0 +django_redis==4.10.0 # used byes indexing cache kombu==4.6.6 celery==4.3.0 diff --git a/run_celery_beat.sh b/run_celery_beat.sh index 75de842d..e683ef9e 100755 --- a/run_celery_beat.sh +++ b/run_celery_beat.sh @@ -1,4 +1,4 @@ #!/bin/sh sleep 5 -celery -A project worker -B -l info \ No newline at end of file +celery -A project beat -l info \ No newline at end of file From e6b353e864cd303302bddadf16a010ae54b4b956 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 21 Nov 2019 21:59:24 +0300 Subject: [PATCH 006/191] ES Facets in addition to results --- .../search_indexes/documents/establishment.py | 1 + apps/search_indexes/views.py | 65 +++++++++++++++++-- apps/utils/pagination.py | 20 +++++- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 45923182..17f657b1 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -46,6 +46,7 @@ class EstablishmentDocument(Document): 'id': fields.IntegerField(attr='id'), 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'index_name': fields.KeywordField(attr='value'), }, multi=True) visible_tags = fields.ObjectField( diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 783754c7..0be3e007 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -4,13 +4,15 @@ from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf.filter_backends import ( FilteringFilterBackend, GeoSpatialFilteringFilterBackend, - DefaultOrderingFilterBackend, + FacetedSearchFilterBackend, + CompoundSearchFilterBackend, ) +from elasticsearch_dsl import TermsFacet, RangeFacet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from search_indexes import serializers, filters from search_indexes.documents import EstablishmentDocument, NewsDocument from search_indexes.documents.product import ProductDocument -from utils.pagination import ProjectMobilePagination +from utils.pagination import ESDocumentPagination class NewsDocumentViewSet(BaseDocumentViewSet): @@ -18,15 +20,30 @@ class NewsDocumentViewSet(BaseDocumentViewSet): document = NewsDocument lookup_field = 'slug' - pagination_class = ProjectMobilePagination + pagination_class = ESDocumentPagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.NewsDocumentSerializer filter_backends = [ filters.CustomSearchFilterBackend, FilteringFilterBackend, + CompoundSearchFilterBackend, + FacetedSearchFilterBackend, ] + faceted_search_fields = { + 'works_at_weekday': { + 'field': 'works_at_weekday', + 'facet': TermsFacet, + 'enabled': True, + }, + 'state': { + 'field': 'state', + 'facet': TermsFacet, + 'enabled': True, + } + } + search_fields = { 'title': {'fuzziness': 'auto:2,5', 'boost': 3}, @@ -69,8 +86,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): """Establishment document ViewSet.""" document = EstablishmentDocument - lookup_field = 'slug' - pagination_class = ProjectMobilePagination + pagination_class = ESDocumentPagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.EstablishmentDocumentSerializer @@ -83,9 +99,42 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): FilteringFilterBackend, filters.CustomSearchFilterBackend, GeoSpatialFilteringFilterBackend, - # DefaultOrderingFilterBackend, + FacetedSearchFilterBackend, ] + faceted_search_fields = { + 'works_at_weekday': { + 'field': 'works_at_weekday', + 'facet': TermsFacet, + 'enabled': True, + }, + 'toque_number': { + 'field': 'toque_number', + 'enabled': True, + 'facet': TermsFacet, + }, + 'works_at_noon': { + 'field': 'works_at_noon', + 'facet': TermsFacet, + 'enabled': True, + }, + 'works_evening': { + 'field': 'works_evening', + 'facet': TermsFacet, + 'enabled': True, + }, + 'works_now': { + 'field': 'works_now', + 'facet': TermsFacet, + 'enabled': True, + }, + 'tag': { + 'field': 'tags.index_name', + 'facet': TermsFacet, + 'enabled': True, + } + } + search_fields = { 'name': {'fuzziness': 'auto:2,5', 'boost': 4}, @@ -211,13 +260,15 @@ class ProductDocumentViewSet(BaseDocumentViewSet): """Product document ViewSet.""" document = ProductDocument - pagination_class = ProjectMobilePagination + pagination_class = ESDocumentPagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.ProductDocumentSerializer filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, + CompoundSearchFilterBackend, + FacetedSearchFilterBackend, ] search_fields = { diff --git a/apps/utils/pagination.py b/apps/utils/pagination.py index ac83f4f2..89f16148 100644 --- a/apps/utils/pagination.py +++ b/apps/utils/pagination.py @@ -3,7 +3,8 @@ from base64 import b64encode from urllib import parse as urlparse from django.conf import settings -from rest_framework.pagination import PageNumberPagination, CursorPagination +from rest_framework.pagination import CursorPagination +from django_elasticsearch_dsl_drf.pagination import PageNumberPagination class ProjectPageNumberPagination(PageNumberPagination): @@ -48,6 +49,23 @@ class ProjectMobilePagination(ProjectPageNumberPagination): return self.page.previous_page_number() +class ESDocumentPagination(PageNumberPagination): + """Pagination class for ES results. (includes facets)""" + page_size_query_param = 'page_size' + + def get_next_link(self): + """Get next link method.""" + if not self.page.has_next(): + return None + return self.page.next_page_number() + + def get_previous_link(self): + """Get previous link method.""" + if not self.page.has_previous(): + return None + return self.page.previous_page_number() + + class EstablishmentPortionPagination(ProjectMobilePagination): """ Pagination for app establishments with limit page size equal to 12 From 7376c61c4762482740335a5acc1914d524b81a5c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 21 Nov 2019 22:43:10 +0300 Subject: [PATCH 007/191] Facets for establishments --- apps/search_indexes/views.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 0be3e007..5f35279f 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -113,8 +113,8 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, 'facet': TermsFacet, }, - 'works_at_noon': { - 'field': 'works_at_noon', + 'works_noon': { + 'field': 'works_noon', 'facet': TermsFacet, 'enabled': True, }, @@ -132,6 +132,11 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'field': 'tags.index_name', 'facet': TermsFacet, 'enabled': True, + }, + 'wine_region_id': { + 'field': 'products.wine_region.id', + 'facet': TermsFacet, + 'enabled': True, } } From 9f1e9effaf7a2a8451bad611d040e7105e8a7c5d Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 21 Nov 2019 22:46:49 +0300 Subject: [PATCH 008/191] Facets for news --- apps/search_indexes/views.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 5f35279f..2eec6e7f 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -7,7 +7,7 @@ from django_elasticsearch_dsl_drf.filter_backends import ( FacetedSearchFilterBackend, CompoundSearchFilterBackend, ) -from elasticsearch_dsl import TermsFacet, RangeFacet +from elasticsearch_dsl import TermsFacet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from search_indexes import serializers, filters from search_indexes.documents import EstablishmentDocument, NewsDocument @@ -32,16 +32,11 @@ class NewsDocumentViewSet(BaseDocumentViewSet): ] faceted_search_fields = { - 'works_at_weekday': { - 'field': 'works_at_weekday', - 'facet': TermsFacet, + 'tag': { + 'field': 'tags.value', 'enabled': True, + 'facet': TermsFacet, }, - 'state': { - 'field': 'state', - 'facet': TermsFacet, - 'enabled': True, - } } search_fields = { From 735e40973430668eddf015c9206fff0daa036a30 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 21 Nov 2019 22:54:33 +0300 Subject: [PATCH 009/191] Facets for products --- apps/search_indexes/views.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 2eec6e7f..8aeb6d81 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -283,6 +283,19 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'description': {'fuzziness': 'auto:2,5'}, } + faceted_search_fields = { + 'tag': { + 'field': 'wine_colors.id', + 'enabled': True, + 'facet': TermsFacet, + }, + 'wine_region_id': { + 'field': 'wine_region.id', + 'enabled': True, + 'facet': TermsFacet, + }, + } + translated_search_fields = ( 'description', ) From 22e1478a1f8efa21b28a8ef22440948fc0c2dc9d Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 09:31:21 +0300 Subject: [PATCH 010/191] partner back api --- .gitignore | 3 +- apps/partner/serializers/back.py | 9 ++++ apps/partner/tests.py | 86 ++++++++++++++++++++++++++++++-- apps/partner/urls/back.py | 11 ++++ apps/partner/views/back.py | 27 ++++++++++ apps/partner/views/views.py | 1 - 6 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 apps/partner/serializers/back.py create mode 100644 apps/partner/urls/back.py create mode 100644 apps/partner/views/back.py delete mode 100644 apps/partner/views/views.py diff --git a/.gitignore b/.gitignore index 9cb2b74f..78187ac0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ logs/ ./docker-compose.override.yml celerybeat-schedule -local_files \ No newline at end of file +local_files +celerybeat.pid diff --git a/apps/partner/serializers/back.py b/apps/partner/serializers/back.py new file mode 100644 index 00000000..94a7c4b9 --- /dev/null +++ b/apps/partner/serializers/back.py @@ -0,0 +1,9 @@ +"""Back account serializers""" +from rest_framework import serializers +from partner.models import Partner + + +class BackPartnerSerializer(serializers.ModelSerializer): + class Meta: + model = Partner + fields = '__all__' diff --git a/apps/partner/tests.py b/apps/partner/tests.py index 494e7f7e..97931a8a 100644 --- a/apps/partner/tests.py +++ b/apps/partner/tests.py @@ -1,16 +1,94 @@ # Create your tests here. -from rest_framework.test import APITestCase +from http.cookies import SimpleCookie + from rest_framework import status +from rest_framework.test import APITestCase +from account.models import User, Role, UserRole +from establishment.models import EstablishmentType, Establishment +from location.models import Country, Region, City, Address from partner.models import Partner +from translation.models import Language -class PartnerTestCase(APITestCase): +class BaseTestCase(APITestCase): def setUp(self): - self.test_url = "www.example.com" - self.test_partner = Partner.objects.create(url=self.test_url) + self.username = 'test_user' + self.password = 'test_user_password' + self.email = 'test_user@mail.com' + self.user = User.objects.create_user( + username=self.username, + email=self.email, + password=self.password, + is_staff=True, + ) + + tokens = User.create_jwt_tokens(self.user) + self.client.cookies = SimpleCookie({ + 'access_token': tokens.get('access_token'), + 'refresh_token': tokens.get('refresh_token'), + }) + + self.establishment_type = EstablishmentType.objects.create(name="Test establishment type") + self.role = Role.objects.create(role=Role.ESTABLISHMENT_MANAGER) + + self.establishment = Establishment.objects.create( + name="Test establishment", + establishment_type_id=self.establishment_type.id, + is_publish=True, + slug="test", + ) + + self.user_role = UserRole.objects.create( + user=self.user, + role=self.role, + establishment=self.establishment, + ) + + self.partner = Partner.objects.create( + url='www.ya.ru', + establishment=self.establishment, + ) + + +class PartnerWebTestCase(BaseTestCase): def test_partner_list(self): response = self.client.get("/api/web/partner/") self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class PartnerBackTestCase(BaseTestCase): + + def test_partner_list(self): + response = self.client.get('/api/back/partner/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # def test_partner_post(self): + # test_inquiry = { + # 'review': self.test_review.pk, + # 'author': self.user.pk, + # 'comment': 'New test comment', + # } + # response = self.client.post('/api/back/partner/', data=test_inquiry) + # self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_partner_detail(self): + response = self.client.get(f'/api/back/partner/{self.partner.id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # def test_partner_detail_put(self): + # data = { + # 'id': self.test_inquiry.id, + # 'review': self.test_review.pk, + # 'author': self.user.pk, + # 'comment': 'New test comment 2', + # } + # + # response = self.client.put(f'/api/back/partner/{self.partner.id}/', data=data) + # self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_partner_delete(self): + response = self.client.get(f'/api/back/partner/{self.partner.id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/apps/partner/urls/back.py b/apps/partner/urls/back.py new file mode 100644 index 00000000..27de2731 --- /dev/null +++ b/apps/partner/urls/back.py @@ -0,0 +1,11 @@ +"""Back account URLs""" +from django.urls import path + +from partner.views import back as views + +app_name = 'partner' + +urlpatterns = [ + path('', views.PartnerLstView.as_view(), name='partner-list-create'), + path('/', views.PartnerRUDView.as_view(), name='partner-rud'), +] diff --git a/apps/partner/views/back.py b/apps/partner/views/back.py new file mode 100644 index 00000000..e0727a7c --- /dev/null +++ b/apps/partner/views/back.py @@ -0,0 +1,27 @@ +from django_filters.rest_framework import DjangoFilterBackend, filters +from rest_framework import generics, permissions + +from partner.models import Partner +from partner.serializers import back as serializers +from utils.permissions import IsEstablishmentManager + + +class PartnerLstView(generics.ListCreateAPIView): + """Partner list create view.""" + queryset = Partner.objects.all() + serializer_class = serializers.BackPartnerSerializer + permission_classes = [permissions.IsAdminUser | IsEstablishmentManager] + filter_backends = (DjangoFilterBackend, filters.OrderingFilter) + ordering_fields = '__all__' + filterset_fields = ( + 'establishment', + 'type', + ) + + +class PartnerRUDView(generics.RetrieveUpdateDestroyAPIView): + """Partner RUD view.""" + queryset = Partner.objects.all() + serializer_class = serializers.BackPartnerSerializer + permission_classes = [permissions.IsAdminUser | IsEstablishmentManager] + lookup_field = 'id' diff --git a/apps/partner/views/views.py b/apps/partner/views/views.py deleted file mode 100644 index 60f00ef0..00000000 --- a/apps/partner/views/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here. From 0fe3e3f8baab9386a322f7c679668f5e7b29bef5 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 10:17:34 +0300 Subject: [PATCH 011/191] Fix --- apps/utils/pagination.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/utils/pagination.py b/apps/utils/pagination.py index 89f16148..ed5ce89e 100644 --- a/apps/utils/pagination.py +++ b/apps/utils/pagination.py @@ -3,8 +3,8 @@ from base64 import b64encode from urllib import parse as urlparse from django.conf import settings -from rest_framework.pagination import CursorPagination -from django_elasticsearch_dsl_drf.pagination import PageNumberPagination +from rest_framework.pagination import CursorPagination, PageNumberPagination +from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination class ProjectPageNumberPagination(PageNumberPagination): @@ -49,7 +49,7 @@ class ProjectMobilePagination(ProjectPageNumberPagination): return self.page.previous_page_number() -class ESDocumentPagination(PageNumberPagination): +class ESDocumentPagination(ESPagination): """Pagination class for ES results. (includes facets)""" page_size_query_param = 'page_size' From 7e6b2712a9b775aa1b5110ad356fca689c95b897 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 10:24:05 +0300 Subject: [PATCH 012/191] Fix facet --- apps/search_indexes/documents/establishment.py | 3 ++- apps/search_indexes/views.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 17f657b1..d611a964 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -46,7 +46,7 @@ class EstablishmentDocument(Document): 'id': fields.IntegerField(attr='id'), 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), - 'index_name': fields.KeywordField(attr='value'), + 'value': fields.KeywordField(), }, multi=True) visible_tags = fields.ObjectField( @@ -54,6 +54,7 @@ class EstablishmentDocument(Document): 'id': fields.IntegerField(attr='id'), 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), }, multi=True) products = fields.ObjectField( diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 8aeb6d81..66e15208 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -124,7 +124,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.index_name', + 'field': 'tags.value', 'facet': TermsFacet, 'enabled': True, }, From d30d68bf17841cc873d8953b534398126dacb093 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 10:32:01 +0300 Subject: [PATCH 013/191] partner test --- apps/partner/serializers/back.py | 13 +++++++++++- apps/partner/tests.py | 36 ++++++++++++++------------------ apps/partner/views/back.py | 4 ++-- project/urls/back.py | 1 + 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/apps/partner/serializers/back.py b/apps/partner/serializers/back.py index 94a7c4b9..d011e058 100644 --- a/apps/partner/serializers/back.py +++ b/apps/partner/serializers/back.py @@ -6,4 +6,15 @@ from partner.models import Partner class BackPartnerSerializer(serializers.ModelSerializer): class Meta: model = Partner - fields = '__all__' + fields = ( + 'id', + 'old_id', + 'name', + 'url', + 'image', + 'establishment', + 'type', + 'starting_date', + 'expiry_date', + 'price_per_month', + ) diff --git a/apps/partner/tests.py b/apps/partner/tests.py index 97931a8a..3233950b 100644 --- a/apps/partner/tests.py +++ b/apps/partner/tests.py @@ -65,30 +65,26 @@ class PartnerBackTestCase(BaseTestCase): response = self.client.get('/api/back/partner/') self.assertEqual(response.status_code, status.HTTP_200_OK) - # def test_partner_post(self): - # test_inquiry = { - # 'review': self.test_review.pk, - # 'author': self.user.pk, - # 'comment': 'New test comment', - # } - # response = self.client.post('/api/back/partner/', data=test_inquiry) - # self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_partner_post(self): + test_partner = { + 'url': 'http://google.com', + } + response = self.client.post('/api/back/partner/', data=test_partner, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_partner_detail(self): response = self.client.get(f'/api/back/partner/{self.partner.id}/') self.assertEqual(response.status_code, status.HTTP_200_OK) - # def test_partner_detail_put(self): - # data = { - # 'id': self.test_inquiry.id, - # 'review': self.test_review.pk, - # 'author': self.user.pk, - # 'comment': 'New test comment 2', - # } - # - # response = self.client.put(f'/api/back/partner/{self.partner.id}/', data=data) - # self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_partner_detail_put(self): + data = { + 'url': 'http://yandex.com', + 'name': 'Yandex', + } + + response = self.client.put(f'/api/back/partner/{self.partner.id}/', data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) def test_partner_delete(self): - response = self.client.get(f'/api/back/partner/{self.partner.id}/') - self.assertEqual(response.status_code, status.HTTP_200_OK) + response = self.client.delete(f'/api/back/partner/{self.partner.id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/partner/views/back.py b/apps/partner/views/back.py index e0727a7c..1033d0ee 100644 --- a/apps/partner/views/back.py +++ b/apps/partner/views/back.py @@ -10,9 +10,9 @@ class PartnerLstView(generics.ListCreateAPIView): """Partner list create view.""" queryset = Partner.objects.all() serializer_class = serializers.BackPartnerSerializer + pagination_class = None permission_classes = [permissions.IsAdminUser | IsEstablishmentManager] - filter_backends = (DjangoFilterBackend, filters.OrderingFilter) - ordering_fields = '__all__' + filter_backends = (DjangoFilterBackend,) filterset_fields = ( 'establishment', 'type', diff --git a/project/urls/back.py b/project/urls/back.py index fdd3d10a..e7e2b43b 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -16,4 +16,5 @@ urlpatterns = [ path('re_blocks/', include(('advertisement.urls.back', 'advertisement'), namespace='advertisement')), path('main/', include('main.urls.back')), + path('partner/', include('partner.urls.back')), ] From ee781150fd27d37c0b453ec411bc49e43310cf48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 22 Nov 2019 10:38:03 +0300 Subject: [PATCH 014/191] Site settings --- .../management/commands/add_site_settings.py | 70 +++++++++++++++++++ apps/main/models.py | 2 + 2 files changed, 72 insertions(+) create mode 100644 apps/main/management/commands/add_site_settings.py diff --git a/apps/main/management/commands/add_site_settings.py b/apps/main/management/commands/add_site_settings.py new file mode 100644 index 00000000..dac98275 --- /dev/null +++ b/apps/main/management/commands/add_site_settings.py @@ -0,0 +1,70 @@ +from django.core.management.base import BaseCommand +from django.db import connections +from establishment.management.commands.add_position import namedtuplefetchall +from main.models import SiteSettings +from location.models import Country +from tqdm import tqdm + + +class Command(BaseCommand): + help = '''Add add site settings from old db to new db. + Run after country migrate!!!''' + + def site_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select + id, + country_code_2, + pinterest_page_url, + twitter_page_url, + facebook_page_url, + contact_email, + config, + released, + instagram_page_url, + ad_config + from sites as s + ''') + return namedtuplefetchall(cursor) + + def add_site_settings(self): + objects=[] + for s in tqdm(self.site_sql(), desc='Add site settings'): + country = Country.objects.filter(code=s.code) + sites = SiteSettings.objects.filter(country=country) + if not sites.exists(): + objects.append( + SiteSettings( + country=country, + pinterest_page_url=s.pinterest_page_url, + twitter_page_url=s.twitter_page_url, + facebook_page_url=s.facebook_page_url, + instagram_page_url=s.instagram_page_url, + contact_email=s.contact_email, + config=s.config, + ad_config=s.ad_config + ) + ) + SiteSettings.objects.bulk_create(objects) + self.stdout.write(self.style.WARNING(f'Add or get tag category objects.')) + + # def add_category_tag(self): + # objects = [] + # for c in tqdm(self.category_sql(), desc='Add category tags'): + # categories = TagCategory.objects.filter(index_name=c.category) + # if not categories.exists(): + # objects.append( + # TagCategory(label={"en-GB": c.category}, + # value_type=c.value_type, + # index_name=c.category, + # public=True + # ) + # ) + # else: + # categories.update(public=True) + # TagCategory.objects.bulk_create(objects) + # self.stdout.write(self.style.WARNING(f'Add or get tag category objects.')) + + def handle(self, *args, **kwargs): + pass \ No newline at end of file diff --git a/apps/main/models.py b/apps/main/models.py index 61a4d447..fcce88ab 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -69,6 +69,8 @@ class SiteSettings(ProjectBaseMixin): verbose_name=_('AD config')) currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None) + old_id = models.IntegerField(blank=True, null=True) + objects = SiteSettingsQuerySet.as_manager() class Meta: From 3a1e55ba1ffabcb5f0d173909433e939ed5f35b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 22 Nov 2019 10:58:07 +0300 Subject: [PATCH 015/191] Site settings migrate data --- .../management/commands/add_site_settings.py | 34 ++++++------------- .../migrations/0037_sitesettings_old_id.py | 18 ++++++++++ 2 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 apps/main/migrations/0037_sitesettings_old_id.py diff --git a/apps/main/management/commands/add_site_settings.py b/apps/main/management/commands/add_site_settings.py index dac98275..2414a153 100644 --- a/apps/main/management/commands/add_site_settings.py +++ b/apps/main/management/commands/add_site_settings.py @@ -14,8 +14,9 @@ class Command(BaseCommand): with connections['legacy'].cursor() as cursor: cursor.execute(''' select + distinct id, - country_code_2, + country_code_2 as code, pinterest_page_url, twitter_page_url, facebook_page_url, @@ -31,11 +32,12 @@ class Command(BaseCommand): def add_site_settings(self): objects=[] for s in tqdm(self.site_sql(), desc='Add site settings'): - country = Country.objects.filter(code=s.code) - sites = SiteSettings.objects.filter(country=country) + country = Country.objects.filter(code=s.code).first() + sites = SiteSettings.objects.filter(subdomain=s.code) if not sites.exists(): objects.append( SiteSettings( + subdomain=s.code, country=country, pinterest_page_url=s.pinterest_page_url, twitter_page_url=s.twitter_page_url, @@ -43,28 +45,12 @@ class Command(BaseCommand): instagram_page_url=s.instagram_page_url, contact_email=s.contact_email, config=s.config, - ad_config=s.ad_config + ad_config=s.ad_config, + old_id=s.id ) ) - SiteSettings.objects.bulk_create(objects) - self.stdout.write(self.style.WARNING(f'Add or get tag category objects.')) - - # def add_category_tag(self): - # objects = [] - # for c in tqdm(self.category_sql(), desc='Add category tags'): - # categories = TagCategory.objects.filter(index_name=c.category) - # if not categories.exists(): - # objects.append( - # TagCategory(label={"en-GB": c.category}, - # value_type=c.value_type, - # index_name=c.category, - # public=True - # ) - # ) - # else: - # categories.update(public=True) - # TagCategory.objects.bulk_create(objects) - # self.stdout.write(self.style.WARNING(f'Add or get tag category objects.')) + SiteSettings.objects.bulk_create(objects) + self.stdout.write(self.style.WARNING(f'Add or get tag category objects.')) def handle(self, *args, **kwargs): - pass \ No newline at end of file + self.add_site_settings() \ No newline at end of file diff --git a/apps/main/migrations/0037_sitesettings_old_id.py b/apps/main/migrations/0037_sitesettings_old_id.py new file mode 100644 index 00000000..e7ef11e2 --- /dev/null +++ b/apps/main/migrations/0037_sitesettings_old_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-22 07:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0036_auto_20191115_0750'), + ] + + operations = [ + migrations.AddField( + model_name='sitesettings', + name='old_id', + field=models.IntegerField(blank=True, null=True), + ), + ] From a758cad48f37aaa2d7cdbaac6e6462dfc64022cd Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 11:10:24 +0300 Subject: [PATCH 016/191] Fix f swagger --- apps/search_indexes/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 66e15208..b5bda947 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -5,7 +5,6 @@ from django_elasticsearch_dsl_drf.filter_backends import ( FilteringFilterBackend, GeoSpatialFilteringFilterBackend, FacetedSearchFilterBackend, - CompoundSearchFilterBackend, ) from elasticsearch_dsl import TermsFacet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet @@ -27,7 +26,6 @@ class NewsDocumentViewSet(BaseDocumentViewSet): filter_backends = [ filters.CustomSearchFilterBackend, FilteringFilterBackend, - CompoundSearchFilterBackend, FacetedSearchFilterBackend, ] @@ -267,7 +265,6 @@ class ProductDocumentViewSet(BaseDocumentViewSet): filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, - CompoundSearchFilterBackend, FacetedSearchFilterBackend, ] From 806a7d63e492c6abbc915ad57883b2a664729b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 22 Nov 2019 11:43:22 +0300 Subject: [PATCH 017/191] Migrate --- apps/account/migrations/0020_role_site.py | 20 ++++++++++++++++++++ apps/account/models.py | 6 ++---- 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 apps/account/migrations/0020_role_site.py diff --git a/apps/account/migrations/0020_role_site.py b/apps/account/migrations/0020_role_site.py new file mode 100644 index 00000000..8fce5f24 --- /dev/null +++ b/apps/account/migrations/0020_role_site.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2019-11-22 08:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0037_sitesettings_old_id'), + ('account', '0019_auto_20191108_0827'), + ] + + operations = [ + migrations.AddField( + model_name='role', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='Site settings'), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 78c3c284..c212ffda 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -46,10 +46,8 @@ class Role(ProjectBaseMixin): null=False, blank=False) country = models.ForeignKey(Country, verbose_name=_('Country'), null=True, blank=True, on_delete=models.SET_NULL) - # is_list = models.BooleanField(verbose_name=_('list'), default=True, null=False) - # is_create = models.BooleanField(verbose_name=_('create'), default=False, null=False) - # is_update = models.BooleanField(verbose_name=_('update'), default=False, null=False) - # is_delete = models.BooleanField(verbose_name=_('delete'), default=False, null=False) + site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'), + null=True, blank=True, on_delete=models.SET_NULL) class UserManager(BaseUserManager): From ce41b96ec3f77c735c41173131d83e5604729acd Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 22 Nov 2019 12:34:11 +0300 Subject: [PATCH 018/191] update dev settings --- project/settings/development.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/project/settings/development.py b/project/settings/development.py index 06f1199b..057438f5 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -18,6 +18,17 @@ SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, + 'es_queue': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://localhost:6379/2' + } +} + + # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { From 00d7bbe7ec95f39bdca5fc4702e90501875f7218 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 22 Nov 2019 12:45:14 +0300 Subject: [PATCH 019/191] update crontab parameters --- apps/search_indexes/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/tasks.py b/apps/search_indexes/tasks.py index 4935814d..b9fefff7 100644 --- a/apps/search_indexes/tasks.py +++ b/apps/search_indexes/tasks.py @@ -13,7 +13,7 @@ from product.models import Product logger = logging.getLogger(__name__) -@periodic_task(run_every=crontab(minute=1)) +@periodic_task(run_every=crontab(minute='*/1')) def update_index(): """Updates ES index.""" try: From f7703f18b05cc357c47ef134c2284e6c7d93c558 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 22 Nov 2019 13:02:04 +0300 Subject: [PATCH 020/191] refactored upload image endpoint --- apps/gallery/serializers.py | 54 +++++++++++++++++++++++++++++++++++++ apps/utils/models.py | 12 +++++++++ project/settings/base.py | 7 +++++ 3 files changed, 73 insertions(+) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index e817cbd8..1f96dca8 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -1,4 +1,8 @@ +from django.conf import settings +from django.core.validators import MinValueValidator, MaxValueValidator from rest_framework import serializers +from sorl.thumbnail.parsers import parse_crop +from sorl.thumbnail.parsers import ThumbnailParseError from . import models @@ -8,10 +12,21 @@ class ImageSerializer(serializers.ModelSerializer): # REQUEST file = serializers.ImageField(source='image', write_only=True) + width = serializers.IntegerField(write_only=True, required=False) + height = serializers.IntegerField(write_only=True, required=False) + margin = serializers.CharField(write_only=True, allow_null=True, + required=False, + default='center') + quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, + default=settings.THUMBNAIL_QUALITY, + validators=[ + MinValueValidator(1), + MaxValueValidator(100)]) # RESPONSE url = serializers.ImageField(source='image', read_only=True) + cropped_image = serializers.DictField(read_only=True, allow_null=True) orientation_display = serializers.CharField(source='get_orientation_display', read_only=True) @@ -25,7 +40,46 @@ class ImageSerializer(serializers.ModelSerializer): 'orientation', 'orientation_display', 'title', + 'width', + 'height', + 'margin', + 'quality', + 'cropped_image', ] extra_kwargs = { 'orientation': {'write_only': True} } + + def validate(self, attrs): + """Overridden validate method.""" + image = attrs.get('image').image + crop_width = attrs.get('width') + crop_height = attrs.get('height') + margin = attrs.get('margin') + + if crop_height and crop_width and margin: + xy_image = (image.width, image.width) + xy_window = (crop_width, crop_height) + try: + parse_crop(margin, xy_image, xy_window) + except ThumbnailParseError: + raise serializers.ValidationError({'margin': 'Unrecognized crop option: %s' % margin}) + return attrs + + def create(self, validated_data): + """Overridden create method.""" + width = validated_data.pop('width', None) + height = validated_data.pop('height', None) + quality = validated_data.pop('quality') + margin = validated_data.pop('margin') + + instance = super().create(validated_data) + + if instance and width and height: + setattr(instance, + 'cropped_image', + instance.get_cropped_image( + geometry=f'{width}x{height}', + quality=quality, + margin=margin)) + return instance diff --git a/apps/utils/models.py b/apps/utils/models.py index 8c186f2f..59ec2282 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -227,6 +227,18 @@ class SORLImageMixin(models.Model): else: return None + def get_cropped_image(self, geometry: str, quality: int, margin: str) -> dict: + cropped_image = get_thumbnail(self.image, + geometry_string=geometry, + crop=margin, + quality=quality) + return { + 'geometry_string': geometry, + 'crop_url': cropped_image.url, + 'quality': quality, + 'margin': margin + } + image_tag.short_description = _('Image') image_tag.allow_tags = True diff --git a/project/settings/base.py b/project/settings/base.py index 2e29c92d..ef875c72 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -399,6 +399,13 @@ SORL_THUMBNAIL_ALIASES = { 'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'}, 'establishment_detail': {'geometry_string': '2048x1152', 'crop': 'center'}, 'establishment_original': {'geometry_string': '1920x1080', 'crop': 'center'}, + 'city_xsmall': {'geometry_string': '70x70', 'crop': 'center'}, + 'city_small': {'geometry_string': '140x140', 'crop': 'center'}, + 'city_medium': {'geometry_string': '280x280', 'crop': 'center'}, + 'city_large': {'geometry_string': '280x280', 'crop': 'center'}, + 'city_xlarge': {'geometry_string': '560x560', 'crop': 'center'}, + 'city_detail': {'geometry_string': '1120x1120', 'crop': 'center'}, + 'city_original': {'geometry_string': '2048x1536', 'crop': 'center'}, } From 2ebe6e60a3b873b016871ea47c5fe60951c20ded Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 13:54:07 +0300 Subject: [PATCH 021/191] Facet with tags id instead of text values --- apps/search_indexes/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index b5bda947..d6a2938a 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -31,7 +31,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): faceted_search_fields = { 'tag': { - 'field': 'tags.value', + 'field': 'tags.id', 'enabled': True, 'facet': TermsFacet, }, @@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.value', + 'field': 'tags.id', 'facet': TermsFacet, 'enabled': True, }, From 17931eba5303164618c3907164b32d5e7e4ca68e Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 13:54:07 +0300 Subject: [PATCH 022/191] Facet with tags id instead of text values (cherry picked from commit 2ebe6e6) --- apps/search_indexes/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index b5bda947..d6a2938a 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -31,7 +31,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): faceted_search_fields = { 'tag': { - 'field': 'tags.value', + 'field': 'tags.id', 'enabled': True, 'facet': TermsFacet, }, @@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.value', + 'field': 'tags.id', 'facet': TermsFacet, 'enabled': True, }, From 59f44d84f1b311f87b99df25bd0d25ece367554f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 14:27:40 +0300 Subject: [PATCH 023/191] Establishment facet visibble tags only --- apps/search_indexes/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index d6a2938a..e37d19b3 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.id', + 'field': 'visible_tags.id', 'facet': TermsFacet, 'enabled': True, }, From 0231f9770994bed8dab33972c28281e8e70b6f19 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 14:27:40 +0300 Subject: [PATCH 024/191] Establishment facet visibble tags only (cherry picked from commit 59f44d8) --- apps/search_indexes/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index d6a2938a..e37d19b3 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.id', + 'field': 'visible_tags.id', 'facet': TermsFacet, 'enabled': True, }, From 120e686be65364db77a86b48933e588de60a7a09 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Fri, 22 Nov 2019 11:46:24 +0000 Subject: [PATCH 025/191] Changed in_favorites field to read only --- apps/news/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 2264b9ec..c96d05da 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -65,7 +65,7 @@ class NewsBaseSerializer(ProjectModelSerializer): subtitle_translated = TranslatedField() news_type = NewsTypeSerializer(read_only=True) tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') - in_favorites = serializers.BooleanField(allow_null=True) + in_favorites = serializers.BooleanField(allow_null=True, read_only=True) view_counter = serializers.IntegerField(read_only=True) class Meta: From 844c8525e9d73d3386b918d88de6b65d1f07557c Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 22 Nov 2019 14:46:31 +0300 Subject: [PATCH 026/191] refactored, add endpoint - /api/back/gallery//crop/ --- apps/gallery/serializers.py | 78 ++++++++++++++++++++++++++----------- apps/gallery/urls.py | 5 ++- apps/gallery/views.py | 5 +++ 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 1f96dca8..2c2e50d0 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -3,6 +3,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator from rest_framework import serializers from sorl.thumbnail.parsers import parse_crop from sorl.thumbnail.parsers import ThumbnailParseError +from django.utils.translation import gettext_lazy as _ from . import models @@ -12,21 +13,10 @@ class ImageSerializer(serializers.ModelSerializer): # REQUEST file = serializers.ImageField(source='image', write_only=True) - width = serializers.IntegerField(write_only=True, required=False) - height = serializers.IntegerField(write_only=True, required=False) - margin = serializers.CharField(write_only=True, allow_null=True, - required=False, - default='center') - quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, - default=settings.THUMBNAIL_QUALITY, - validators=[ - MinValueValidator(1), - MaxValueValidator(100)]) # RESPONSE url = serializers.ImageField(source='image', read_only=True) - cropped_image = serializers.DictField(read_only=True, allow_null=True) orientation_display = serializers.CharField(source='get_orientation_display', read_only=True) @@ -40,30 +30,55 @@ class ImageSerializer(serializers.ModelSerializer): 'orientation', 'orientation_display', 'title', + ] + extra_kwargs = { + 'orientation': {'write_only': True} + } + + +class CropImageSerializer(ImageSerializer): + """Serializers for image crops.""" + + width = serializers.IntegerField(write_only=True) + height = serializers.IntegerField(write_only=True) + margin = serializers.CharField(write_only=True, allow_null=True, + required=False, + default='center') + quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, + default=settings.THUMBNAIL_QUALITY, + validators=[ + MinValueValidator(1), + MaxValueValidator(100)]) + cropped_image = serializers.DictField(read_only=True, allow_null=True) + + class Meta(ImageSerializer.Meta): + """Meta class.""" + fields = [ + 'id', + 'url', + 'orientation_display', 'width', 'height', 'margin', 'quality', 'cropped_image', ] - extra_kwargs = { - 'orientation': {'write_only': True} - } def validate(self, attrs): """Overridden validate method.""" - image = attrs.get('image').image + file = self._image.image crop_width = attrs.get('width') crop_height = attrs.get('height') margin = attrs.get('margin') if crop_height and crop_width and margin: - xy_image = (image.width, image.width) + xy_image = (file.width, file.width) xy_window = (crop_width, crop_height) try: parse_crop(margin, xy_image, xy_window) + attrs['image'] = file except ThumbnailParseError: - raise serializers.ValidationError({'margin': 'Unrecognized crop option: %s' % margin}) + raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % margin}) return attrs def create(self, validated_data): @@ -73,13 +88,32 @@ class ImageSerializer(serializers.ModelSerializer): quality = validated_data.pop('quality') margin = validated_data.pop('margin') - instance = super().create(validated_data) + image = self._image - if instance and width and height: - setattr(instance, + if image and width and height: + setattr(image, 'cropped_image', - instance.get_cropped_image( + image.get_cropped_image( geometry=f'{width}x{height}', quality=quality, margin=margin)) - return instance + return image + + @property + def view(self): + return self.context.get('view') + + @property + def lookup_field(self): + lookup_field = 'pk' + + if lookup_field in self.view.kwargs: + return self.view.kwargs.get(lookup_field) + + @property + def _image(self): + """Return image from url_kwargs.""" + qs = models.Image.objects.filter(id=self.lookup_field) + if qs.exists(): + return qs.first() + raise serializers.ValidationError({'detail': _('Image not found.')}) diff --git a/apps/gallery/urls.py b/apps/gallery/urls.py index 8258092c..987685cb 100644 --- a/apps/gallery/urls.py +++ b/apps/gallery/urls.py @@ -6,6 +6,7 @@ from . import views app_name = 'gallery' urlpatterns = [ - path('', views.ImageListCreateView.as_view(), name='list-create-image'), - path('/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'), + path('', views.ImageListCreateView.as_view(), name='list-create'), + path('/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy'), + path('/crop/', views.CropImageCreateView.as_view(), name='create-crop'), ] diff --git a/apps/gallery/views.py b/apps/gallery/views.py index 2b155035..1515707f 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -28,3 +28,8 @@ class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView): else: on_commit(lambda: tasks.delete_image(image_id=instance.id)) return Response(status=status.HTTP_204_NO_CONTENT) + + +class CropImageCreateView(ImageBaseView, generics.CreateAPIView): + """Create crop image.""" + serializer_class = serializers.CropImageSerializer From 429825804a60793e3e7ebbca332bcafc01a60b34 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 15:07:50 +0300 Subject: [PATCH 027/191] create destroy mixin --- apps/news/models.py | 1 + apps/news/urls/back.py | 2 +- apps/utils/views.py | 45 +++++++++++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/apps/news/models.py b/apps/news/models.py index 8a86e688..0d510804 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -196,6 +196,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL) ratings = generic.GenericRelation(Rating) favorites = generic.GenericRelation(to='favorites.Favorites') + carousels = generic.GenericRelation(to='main.Carousel') agenda = models.ForeignKey('news.Agenda', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('agenda')) diff --git a/apps/news/urls/back.py b/apps/news/urls/back.py index 9cc3d94a..7e54928f 100644 --- a/apps/news/urls/back.py +++ b/apps/news/urls/back.py @@ -13,4 +13,4 @@ urlpatterns = [ name='gallery-list'), path('/gallery//', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), name='gallery-create-destroy'), -] \ No newline at end of file +] diff --git a/apps/utils/views.py b/apps/utils/views.py index fef14c08..e08e0bf5 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -85,7 +85,7 @@ class JWTGenericViewMixin: value=cookie.value, secure=cookie.secure, httponly=cookie.http_only, - max_age=cookie.max_age,) + max_age=cookie.max_age, ) return response def _get_tokens_from_cookies(self, request, cookies: dict = None): @@ -126,9 +126,8 @@ class CreateDestroyGalleryViewMixin(generics.CreateAPIView, return Response(status=status.HTTP_204_NO_CONTENT) -class FavoritesCreateDestroyMixinView(generics.CreateAPIView, - generics.DestroyAPIView): - """Favorites Create Destroy mixin.""" +class BaseCreateDestroyMixinView(generics.CreateAPIView, generics.DestroyAPIView): + """Base Create Destroy mixin.""" _model = None serializer_class = None @@ -137,16 +136,6 @@ class FavoritesCreateDestroyMixinView(generics.CreateAPIView, def get_base_object(self): return get_object_or_404(self._model, slug=self.kwargs['slug']) - def get_object(self): - """ - Returns the object the view is displaying. - """ - obj = self.get_base_object() - favorites = get_object_or_404(obj.favorites.filter(user=self.request.user)) - # May raise a permission denied - self.check_object_permissions(self.request, favorites) - return favorites - def es_update_base_object(self): es_update(self.get_base_object()) @@ -159,6 +148,34 @@ class FavoritesCreateDestroyMixinView(generics.CreateAPIView, self.es_update_base_object() +class FavoritesCreateDestroyMixinView(BaseCreateDestroyMixinView): + """Favorites Create Destroy mixin.""" + + def get_object(self): + """ + Returns the object the view is displaying. + """ + obj = self.get_base_object() + favorites = get_object_or_404(obj.favorites.filter(user=self.request.user)) + # May raise a permission denied + self.check_object_permissions(self.request, favorites) + return favorites + + +class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): + """Carousel Create Destroy mixin.""" + + def get_object(self): + """ + Returns the object the view is displaying. + """ + obj = self.get_base_object() + carousels = get_object_or_404(obj.carousels.filter(user=self.request.user)) + # May raise a permission denied + self.check_object_permissions(self.request, carousels) + return carousels + + # BackOffice user`s views & viewsets class BindObjectMixin: """Bind object mixin.""" From e0352b7d937e8d8a476ac63ed29cb3387e35f3bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 22 Nov 2019 15:46:59 +0300 Subject: [PATCH 028/191] Back news permissions --- apps/news/migrations/0036_news_site.py | 20 ++++++++++++++++++++ apps/news/models.py | 3 ++- apps/news/serializers.py | 7 +++++-- apps/news/tests.py | 24 ++++++++++++++++-------- apps/news/views.py | 3 ++- apps/utils/permissions.py | 13 +++++++++---- project/settings/local.py | 3 ++- 7 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 apps/news/migrations/0036_news_site.py diff --git a/apps/news/migrations/0036_news_site.py b/apps/news/migrations/0036_news_site.py new file mode 100644 index 00000000..6e819384 --- /dev/null +++ b/apps/news/migrations/0036_news_site.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2019-11-22 09:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0037_sitesettings_old_id'), + ('news', '0035_news_views_count'), + ] + + operations = [ + migrations.AddField( + model_name='news', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site settings'), + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index 8a86e688..20307b32 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -203,7 +203,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi banner = models.ForeignKey('news.NewsBanner', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('banner')) - + site = models.ForeignKey('main.SiteSettings', blank=True, null=True, + on_delete=models.SET_NULL, verbose_name=_('site settings')) objects = NewsQuerySet.as_manager() class Meta: diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 2264b9ec..008ca9ff 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -5,6 +5,7 @@ from rest_framework.fields import SerializerMethodField from account.serializers.common import UserBaseSerializer from gallery.models import Image +from main.models import SiteSettings from location import models as location_models from location.serializers import CountrySimpleSerializer, AddressBaseSerializer from news import models @@ -65,7 +66,6 @@ class NewsBaseSerializer(ProjectModelSerializer): subtitle_translated = TranslatedField() news_type = NewsTypeSerializer(read_only=True) tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') - in_favorites = serializers.BooleanField(allow_null=True) view_counter = serializers.IntegerField(read_only=True) class Meta: @@ -80,7 +80,6 @@ class NewsBaseSerializer(ProjectModelSerializer): 'news_type', 'tags', 'slug', - 'in_favorites', 'view_counter', ) @@ -184,6 +183,9 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, country_id = serializers.PrimaryKeyRelatedField( source='country', write_only=True, queryset=location_models.Country.objects.all()) + site_id = serializers.PrimaryKeyRelatedField( + source='site', write_only=True, + queryset=SiteSettings.objects.all()) template_display = serializers.CharField(source='get_template_display', read_only=True) @@ -195,6 +197,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, 'description', 'news_type_id', 'country_id', + 'site_id', 'template', 'template_display', ) diff --git a/apps/news/tests.py b/apps/news/tests.py index 532a6efc..c2b2e431 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -5,6 +5,7 @@ from rest_framework.test import APITestCase from rest_framework import status from datetime import datetime, timedelta +from main.models import SiteSettings from news.models import NewsType, News from account.models import User, Role, UserRole from translation.models import Language @@ -30,18 +31,22 @@ class BaseTestCase(APITestCase): 'refresh_token': tokens.get('refresh_token')}) self.test_news_type = NewsType.objects.create(name="Test news type") - self.lang = Language.objects.get( + self.lang, created = Language.objects.get_or_create( title='Russia', locale='ru-RU' ) - self.country_ru = Country.objects.get( + self.country_ru, created = Country.objects.get_or_create( name={"en-GB": "Russian"} ) + self.site_ru, created = SiteSettings.objects.get_or_create( + subdomain='ru' + ) + role = Role.objects.create( role=Role.CONTENT_PAGE_MANAGER, - country=self.country_ru + site_id=self.site_ru.id ) role.save() @@ -51,16 +56,18 @@ class BaseTestCase(APITestCase): ) user_role.save() + self.test_news = News.objects.create( created_by=self.user, modified_by=self.user, - title={"en-GB": "Test news"}, + title={"ru-RU": "Test news"}, news_type=self.test_news_type, - description={"en-GB": "Description test news"}, + description={"ru-RU": "Description test news"}, start=datetime.now() + timedelta(hours=-2), end=datetime.now() + timedelta(hours=2), state=News.PUBLISHED, slug='test-news-slug', country=self.country_ru, + site=self.site_ru ) @@ -70,14 +77,15 @@ class NewsTestCase(BaseTestCase): def test_news_post(self): test_news = { - "title": {"en-GB": "Test news POST"}, + "title": {"ru-RU": "Test news POST"}, "news_type_id": self.test_news_type.id, - "description": {"en-GB": "Description test news"}, + "description": {"ru-RU": "Description test news"}, "start": datetime.now() + timedelta(hours=-2), "end": datetime.now() + timedelta(hours=2), "state": News.PUBLISHED, "slug": 'test-news-slug_post', "country_id": self.country_ru.id, + "site_id": self.site_ru.id } url = reverse("back:news:list-create") @@ -107,7 +115,7 @@ class NewsTestCase(BaseTestCase): url = reverse('back:news:retrieve-update-destroy', kwargs={'pk': self.test_news.id}) data = { 'id': self.test_news.id, - 'description': {"en-GB": "Description test news!"}, + 'description': {"ru-RU": "Description test news!"}, 'slug': self.test_news.slug, 'start': self.test_news.start, 'news_type_id': self.test_news.news_type_id, diff --git a/apps/news/views.py b/apps/news/views.py index bdb75fc7..7da672bf 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -84,7 +84,8 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView, serializer_class = serializers.NewsBackOfficeBaseSerializer filter_class = filters.NewsListFilterSet create_serializers_class = serializers.NewsBackOfficeDetailSerializer - permission_classes = [IsCountryAdmin | IsContentPageManager] + # IsCountryAdmin | + permission_classes = [ IsContentPageManager] def get_serializer_class(self): """Override serializer class.""" diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 86a4be6f..c53f3962 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -119,8 +119,14 @@ class IsContentPageManager(IsStandardUser): ] # and request.user.email_confirmed, if hasattr(request, 'user'): + site_id = None + if hasattr(request, 'data'): + site_id = request.data['site_id'] + else: + site_id = request.site_id + role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, - country_id=request.country_id) \ + site_id=site_id) \ .first() # 'Comments moderator' rules = [ @@ -134,12 +140,11 @@ class IsContentPageManager(IsStandardUser): # Read permissions are allowed to any request. role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, - country_id=obj.country_id) \ - .first() # 'Comments moderator' + site_id=obj.site_id) \ + .first() rules = [ UserRole.objects.filter(user=request.user, role=role).exists(), - # and obj.user != request.user, super().has_object_permission(request, view, obj) ] return any(rules) diff --git a/project/settings/local.py b/project/settings/local.py index 959e6149..13ca7213 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -36,7 +36,7 @@ THUMBNAIL_DEBUG = True # ADDED TRANSFER APP -INSTALLED_APPS.append('transfer.apps.TransferConfig') +# INSTALLED_APPS.append('transfer.apps.TransferConfig') # DATABASES @@ -101,6 +101,7 @@ ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.establishment': 'local_establishment', 'search_indexes.documents.product': 'local_product', } +ELASTICSEARCH_DSL_AUTOSYNC = False TESTING = sys.argv[1:2] == ['test'] From f95a082db395b2668671c49e74d28542c4995890 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 22 Nov 2019 16:00:06 +0300 Subject: [PATCH 029/191] update geoip feature --- apps/establishment/views/web.py | 3 +- apps/main/methods.py | 52 +++++++++++++-------------------- apps/main/views/common.py | 5 ++-- apps/main/views/web.py | 3 +- load_geiopdb.sh | 23 --------------- project/settings/base.py | 1 - 6 files changed, 25 insertions(+), 62 deletions(-) delete mode 100755 load_geiopdb.sh diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 4f1fe07c..0b6f1ba0 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -56,12 +56,11 @@ class EstablishmentRecentReviewListView(EstablishmentListView): def get_queryset(self): """Overridden method 'get_queryset'.""" qs = super().get_queryset() - user_ip = methods.get_user_ip(self.request) query_params = self.request.query_params if 'longitude' in query_params and 'latitude' in query_params: longitude, latitude = query_params.get('longitude'), query_params.get('latitude') else: - longitude, latitude = methods.determine_coordinates(user_ip) + longitude, latitude = methods.determine_coordinates(self.request) if not longitude or not latitude: return qs.none() point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID) diff --git a/apps/main/methods.py b/apps/main/methods.py index d5f307eb..f19d595a 100644 --- a/apps/main/methods.py +++ b/apps/main/methods.py @@ -28,31 +28,25 @@ def get_user_ip(request): return ip -def determine_country_code(ip_addr): +def determine_country_code(request): """Determine country code.""" - country_code = None - if ip_addr: - try: - geoip = GeoIP2() - country_code = geoip.country_code(ip_addr) - country_code = country_code.lower() - except GeoIP2Exception as ex: - logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}') - except Exception as ex: - logger.error(f'GEOIP Base exception: {ex}') - return country_code + META = request.META + country_code = META.get('X-GeoIP-Country-Code', + META.get('HTTP_X_GEOIP_COUNTRY_CODE')) + if isinstance(country_code, str): + return country_code.lower() -def determine_coordinates(ip_addr: str) -> Tuple[Optional[float], Optional[float]]: - if ip_addr: - try: - geoip = GeoIP2() - return geoip.coords(ip_addr) - except GeoIP2Exception as ex: - logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}') - except Exception as ex: - logger.warning(f'GEOIP Base exception: {ex}') - return None, None +def determine_coordinates(request): + META = request.META + longitude = META.get('X-GeoIP-Longitude', + META.get('HTTP_X_GEOIP_LONGITUDE')) + latitude = META.get('X-GeoIP-Latitude', + META.get('HTTP_X_GEOIP_LATITUDE')) + try: + return float(longitude), float(latitude) + except (TypeError, ValueError): + return None, None def determine_user_site_url(country_code): @@ -76,15 +70,11 @@ def determine_user_site_url(country_code): return site.site_url -def determine_user_city(ip_addr: str) -> Optional[City]: - try: - geoip = GeoIP2() - return geoip.city(ip_addr) - except GeoIP2Exception as ex: - logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}') - except Exception as ex: - logger.warning(f'GEOIP Base exception: {ex}') - return None +def determine_user_city(request): + META = request.META + city = META.get('X-GeoIP-City', + META.get('HTTP_X_GEOIP_CITY')) + return city def determine_subdivision( diff --git a/apps/main/views/common.py b/apps/main/views/common.py index 18ee0d8d..674d045e 100644 --- a/apps/main/views/common.py +++ b/apps/main/views/common.py @@ -86,9 +86,8 @@ class DetermineLocation(generics.GenericAPIView): serializer_class = EmptySerializer def get(self, request, *args, **kwargs): - user_ip = methods.get_user_ip(request) - longitude, latitude = methods.determine_coordinates(user_ip) - city = methods.determine_user_city(user_ip) + longitude, latitude = methods.determine_coordinates(request) + city = methods.determine_user_city(request) if longitude and latitude and city: return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city}) else: diff --git a/apps/main/views/web.py b/apps/main/views/web.py index e1dc32ef..3a634457 100644 --- a/apps/main/views/web.py +++ b/apps/main/views/web.py @@ -14,8 +14,7 @@ class DetermineSiteView(generics.GenericAPIView): serializer_class = EmptySerializer def get(self, request, *args, **kwargs): - user_ip = methods.get_user_ip(request) - country_code = methods.determine_country_code(user_ip) + country_code = methods.determine_country_code(request) url = methods.determine_user_site_url(country_code) return Response(data={'url': url}) diff --git a/load_geiopdb.sh b/load_geiopdb.sh deleted file mode 100755 index 48d16af1..00000000 --- a/load_geiopdb.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -DB_CITY_URL="https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz" -DB_COUNTRY_URL="https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz" -DIR_PATH="geoip_db" -ARCH_PATH="archive" - -mkdir -p $DIR_PATH -cd $DIR_PATH - -mkdir -p $ARCH_PATH - -find . -not -path "./$ARCH_PATH/*" -type f -name "*.mmdb" -exec mv -t "./$ARCH_PATH/" {} \+ - -filename=$(basename $DB_CITY_URL) -wget -O $filename $DB_CITY_URL -tar xzvf "$filename" - -filename=$(basename $DB_COUNTRY_URL) -wget -O $filename $DB_COUNTRY_URL -tar xzvf "$filename" - -find . -mindepth 1 -type f -name "*.mmdb" -not -path "./$ARCH_PATH/*" -exec mv -t . {} \+ diff --git a/project/settings/base.py b/project/settings/base.py index 2e29c92d..87f85065 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -487,7 +487,6 @@ LIMITING_QUERY_OBJECTS = QUERY_OUTPUT_OBJECTS * 3 # GEO # A Spatial Reference System Identifier GEO_DEFAULT_SRID = 4326 -GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db') # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ From 376c8cdff390f0f08909b05ae08a0a373309a707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 22 Nov 2019 16:00:59 +0300 Subject: [PATCH 030/191] State test news with contentpage permission --- apps/news/tests.py | 3 ++- apps/utils/models.py | 4 ---- apps/utils/permissions.py | 10 ++-------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/apps/news/tests.py b/apps/news/tests.py index c2b2e431..74563ca4 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -119,7 +119,8 @@ class NewsTestCase(BaseTestCase): 'slug': self.test_news.slug, 'start': self.test_news.start, 'news_type_id': self.test_news.news_type_id, - 'country_id': self.country_ru.id + 'country_id': self.country_ru.id, + "site_id": self.site_ru.id } response = self.client.put(url, data=data, format='json') diff --git a/apps/utils/models.py b/apps/utils/models.py index 8c186f2f..ebfd9a81 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -36,10 +36,6 @@ class ProjectBaseMixin(models.Model): abstract = True -def valid(value): - print("Run") - - class TJSONField(JSONField): """Overrided JsonField.""" diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index c53f3962..8ac8f0d0 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -118,15 +118,9 @@ class IsContentPageManager(IsStandardUser): super().has_permission(request, view) ] # and request.user.email_confirmed, - if hasattr(request, 'user'): - site_id = None - if hasattr(request, 'data'): - site_id = request.data['site_id'] - else: - site_id = request.site_id - + if hasattr(request, 'user') and hasattr(request.data, 'site_id'): role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, - site_id=site_id) \ + site_id=request.data.site_id,) \ .first() # 'Comments moderator' rules = [ From e3420d571e639becd7cdd7a36d708dedbf6998fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 22 Nov 2019 16:05:02 +0300 Subject: [PATCH 031/191] Contentpage permission in news --- apps/news/views.py | 4 ++-- apps/utils/permissions.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/news/views.py b/apps/news/views.py index 7da672bf..566cdd50 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -84,8 +84,8 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView, serializer_class = serializers.NewsBackOfficeBaseSerializer filter_class = filters.NewsListFilterSet create_serializers_class = serializers.NewsBackOfficeDetailSerializer - # IsCountryAdmin | - permission_classes = [ IsContentPageManager] + + permission_classes = [IsCountryAdmin | IsContentPageManager] def get_serializer_class(self): """Override serializer class.""" diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 8ac8f0d0..bdb17726 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -156,11 +156,11 @@ class IsCountryAdmin(IsStandardUser): super().has_permission(request, view) ] # and request.user.email_confirmed, - if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): + if hasattr(request.data, 'user') and hasattr(request.data, 'site_id'): # Read permissions are allowed to any request. role = Role.objects.filter(role=Role.COUNTRY_ADMIN, - country_id=request.data.country_id) \ + site_id=request.data.site_id) \ .first() # 'Comments moderator' rules = [ @@ -172,7 +172,7 @@ class IsCountryAdmin(IsStandardUser): def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request. role = Role.objects.filter(role=Role.COUNTRY_ADMIN, - country_id=obj.country_id) \ + site_id=obj.site_id) \ .first() # 'Comments moderator' rules = [ From 7e878956e37c01b807bc7f916462a6a94e619fa3 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 16:22:31 +0300 Subject: [PATCH 032/191] carousel api for news --- apps/news/serializers.py | 23 ++++++++++++++++++++++- apps/news/urls/common.py | 5 ++++- apps/news/views.py | 9 ++++++++- apps/utils/exceptions.py | 8 ++++++++ apps/utils/serializers.py | 26 ++++++++++++++++++++++---- apps/utils/views.py | 5 +++-- 6 files changed, 67 insertions(+), 9 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index c96d05da..f58e7c24 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -11,7 +11,7 @@ from news import models from tag.serializers import TagBaseSerializer from utils import exceptions as utils_exceptions from utils.serializers import (TranslatedField, ProjectModelSerializer, - FavoritesCreateSerializer, ImageBaseSerializer) + FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer) class AgendaSerializer(ProjectModelSerializer): @@ -269,3 +269,24 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer): 'content_object': validated_data.pop('news') }) return super().create(validated_data) + + +class NewsCarouselCreateSerializer(CarouselCreateSerializer): + """Serializer to carousel object w/ model News.""" + + def validate(self, attrs): + news = models.News.objects.filter(slug=self.slug).first() + if not news: + raise serializers.ValidationError({'detail': _('Object not found.')}) + + if news.carousels.exists(): + raise utils_exceptions.CarouselError() + + attrs['news'] = news + return attrs + + def create(self, validated_data, *args, **kwargs): + validated_data.update({ + 'content_object': validated_data.pop('news') + }) + return super().create(validated_data) diff --git a/apps/news/urls/common.py b/apps/news/urls/common.py index b42905eb..e2aae7a1 100644 --- a/apps/news/urls/common.py +++ b/apps/news/urls/common.py @@ -5,5 +5,8 @@ common_urlpatterns = [ path('', views.NewsListView.as_view(), name='list'), path('types/', views.NewsTypeListView.as_view(), name='type'), path('slug//', views.NewsDetailView.as_view(), name='rud'), - path('slug//favorites/', views.NewsFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites') + path('slug//favorites/', views.NewsFavoritesCreateDestroyView.as_view(), + name='create-destroy-favorites'), + path('slug//carousels/', views.NewsCarouselCreateDestroyView.as_view(), + name='create-destroy-carousels'), ] diff --git a/apps/news/views.py b/apps/news/views.py index bdb75fc7..7845ab0d 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -6,7 +6,7 @@ from rest_framework import generics, permissions from news import filters, models, serializers from rating.tasks import add_rating from utils.permissions import IsCountryAdmin, IsContentPageManager -from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView +from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView from utils.serializers import ImageBaseSerializer @@ -155,3 +155,10 @@ class NewsFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView): _model = models.News serializer_class = serializers.NewsFavoritesCreateSerializer + + +class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView): + """View for create/destroy news from carousel.""" + + _model = models.News + serializer_class = serializers.NewsCarouselCreateSerializer diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index 37786ce7..c82ff023 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -135,6 +135,14 @@ class FavoritesError(exceptions.APIException): default_detail = _('Item is already in favorites.') +class CarouselError(exceptions.APIException): + """ + The exception should be thrown when the object is already in carousels. + """ + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Item is already in carousels.') + + class PasswordResetRequestExistedError(exceptions.APIException): """ The exception should be thrown when password reset request diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index b78c202c..634246b7 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -2,10 +2,11 @@ import pytz from django.core import exceptions from rest_framework import serializers -from utils import models -from translation.models import Language + from favorites.models import Favorites -from gallery.models import Image +from main.models import Carousel +from translation.models import Language +from utils import models class EmptySerializer(serializers.Serializer): @@ -80,7 +81,6 @@ class FavoritesCreateSerializer(serializers.ModelSerializer): """Serializer to favorite object.""" class Meta: - """Serializer for model Comment.""" model = Favorites fields = [ 'id', @@ -101,6 +101,24 @@ class FavoritesCreateSerializer(serializers.ModelSerializer): return self.request.parser_context.get('kwargs').get('slug') +class CarouselCreateSerializer(serializers.ModelSerializer): + """Carousel to favorite object.""" + + class Meta: + model = Carousel + fields = [ + 'id', + ] + + @property + def request(self): + return self.context.get('request') + + @property + def slug(self): + return self.request.parser_context.get('kwargs').get('slug') + + class RecursiveFieldSerializer(serializers.Serializer): def to_representation(self, value): serializer = self.parent.parent.__class__(value, context=self.context) diff --git a/apps/utils/views.py b/apps/utils/views.py index e08e0bf5..c09df2a2 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -170,9 +170,10 @@ class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): Returns the object the view is displaying. """ obj = self.get_base_object() - carousels = get_object_or_404(obj.carousels.filter(user=self.request.user)) + carousels = get_object_or_404(obj.carousels.all()) # May raise a permission denied - self.check_object_permissions(self.request, carousels) + # TODO: возможно нужны пермишены + # self.check_object_permissions(self.request, carousels) return carousels From fa183bfc92b343565384b0857dc33fdd11b13c4f Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 17:00:43 +0300 Subject: [PATCH 033/191] carousel back api url --- apps/news/urls/back.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/news/urls/back.py b/apps/news/urls/back.py index 7e54928f..9126b3e9 100644 --- a/apps/news/urls/back.py +++ b/apps/news/urls/back.py @@ -13,4 +13,6 @@ urlpatterns = [ name='gallery-list'), path('/gallery//', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), name='gallery-create-destroy'), + path('slug//carousels/', views.NewsCarouselCreateDestroyView.as_view(), + name='create-destroy-carousels'), ] From a5d5dc133546e6f5862eb8dff0e400fd382e6165 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 17:13:41 +0300 Subject: [PATCH 034/191] establishment carousel web and mobile api --- apps/establishment/models.py | 1 + apps/establishment/serializers/common.py | 23 ++++++++++++++++++++++- apps/establishment/urls/common.py | 4 +++- apps/establishment/views/web.py | 17 ++++++++++++----- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index f0260cf9..f8ffd456 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -395,6 +395,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, verbose_name=_('Tag')) reviews = generic.GenericRelation(to='review.Review') comments = generic.GenericRelation(to='comment.Comment') + carousels = generic.GenericRelation(to='main.Carousel') favorites = generic.GenericRelation(to='favorites.Favorites') currency = models.ForeignKey(Currency, blank=True, null=True, default=None, on_delete=models.PROTECT, diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 0c183477..c85b3123 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -13,7 +13,7 @@ from review.serializers import ReviewShortSerializer from tag.serializers import TagBaseSerializer from timetable.serialziers import ScheduleRUDSerializer from utils import exceptions as utils_exceptions -from utils.serializers import ImageBaseSerializer +from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) @@ -426,6 +426,27 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): return super().create(validated_data) +class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer): + """Serializer to carousel object w/ model News.""" + + def validate(self, attrs): + establishment = models.Establishment.objects.filter(slug=self.slug).first() + if not establishment: + raise serializers.ValidationError({'detail': _('Object not found.')}) + + if establishment.carousels.exists(): + raise utils_exceptions.CarouselError() + + attrs['establishment'] = establishment + return attrs + + def create(self, validated_data, *args, **kwargs): + validated_data.update({ + 'content_object': validated_data.pop('establishment') + }) + return super().create(validated_data) + + class CompanyBaseSerializer(serializers.ModelSerializer): """Company base serializer""" phone_list = serializers.SerializerMethodField(source='phones', read_only=True) diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 49cd3631..3f46df90 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -17,5 +17,7 @@ urlpatterns = [ path('slug//comments//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), path('slug//favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), - name='create-destroy-favorites') + name='create-destroy-favorites'), + path('slug//carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(), + name='create-destroy-carousels') ] diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 0b6f1ba0..bd826e4e 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -9,7 +9,7 @@ from comment.serializers import CommentRUDSerializer from establishment import filters, models, serializers from main import methods from utils.pagination import EstablishmentPortionPagination -from utils.views import FavoritesCreateDestroyMixinView +from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView class EstablishmentMixinView: @@ -34,7 +34,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView): serializer_class = serializers.EstablishmentListRetrieveSerializer def get_queryset(self): - return super().get_queryset().with_schedule()\ + return super().get_queryset().with_schedule() \ .with_extended_address_related().with_currency_related() @@ -105,9 +105,9 @@ class EstablishmentCommentListView(generics.ListAPIView): establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug']) return comment_models.Comment.objects.by_content_type(app_label='establishment', - model='establishment')\ - .by_object_id(object_id=establishment.pk)\ - .order_by('-created') + model='establishment') \ + .by_object_id(object_id=establishment.pk) \ + .order_by('-created') class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): @@ -139,6 +139,13 @@ class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView): serializer_class = serializers.EstablishmentFavoritesCreateSerializer +class EstablishmentCarouselCreateDestroyView(CarouselCreateDestroyMixinView): + """View for create/destroy establishment from carousel.""" + + _model = models.Establishment + serializer_class = serializers.EstablishmentCarouselCreateSerializer + + class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView): """Resource for getting list of nearest establishments.""" From e565406729509fa9f3028c63ece228cdfbe0b985 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Fri, 22 Nov 2019 14:21:25 +0000 Subject: [PATCH 035/191] Feature/bo employee update --- apps/establishment/filters.py | 20 +++++++ .../migrations/0066_auto_20191122_1144.py | 28 +++++++++ .../migrations/0067_auto_20191122_1244.py | 39 ++++++++++++ apps/establishment/models.py | 59 +++++++++++++++++-- apps/establishment/serializers/back.py | 46 ++++++++++++++- apps/establishment/serializers/common.py | 57 +++++++++++++++++- apps/establishment/urls/back.py | 8 +++ apps/establishment/views/back.py | 44 +++++++++++++- 8 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 apps/establishment/migrations/0066_auto_20191122_1144.py create mode 100644 apps/establishment/migrations/0067_auto_20191122_1244.py diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index adbcae76..db419989 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -55,3 +55,23 @@ class EstablishmentTypeTagFilter(filters.FilterSet): fields = ( 'type_id', ) + + +class EmployeeBackFilter(filters.FilterSet): + """Employee filter set.""" + + search = filters.CharFilter(method='search_by_name_or_last_name') + + class Meta: + """Meta class.""" + + model = models.Employee + fields = ( + 'search', + ) + + def search_by_name_or_last_name(self, queryset, name, value): + """Search by name or last name.""" + if value not in EMPTY_VALUES: + return queryset.search_by_name_or_last_name(value) + return queryset diff --git a/apps/establishment/migrations/0066_auto_20191122_1144.py b/apps/establishment/migrations/0066_auto_20191122_1144.py new file mode 100644 index 00000000..edff3333 --- /dev/null +++ b/apps/establishment/migrations/0066_auto_20191122_1144.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.7 on 2019-11-22 11:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0065_establishment_purchased_products'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='last_name', + field=models.CharField(default=None, max_length=255, null=True, verbose_name='Last Name'), + ), + migrations.AddField( + model_name='establishmentemployee', + name='status', + field=models.CharField(choices=[('I', 'Idle'), ('A', 'Accepted'), ('D', 'Declined')], default='I', max_length=1), + ), + migrations.AlterField( + model_name='employee', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + ] diff --git a/apps/establishment/migrations/0067_auto_20191122_1244.py b/apps/establishment/migrations/0067_auto_20191122_1244.py new file mode 100644 index 00000000..8dfdc3a4 --- /dev/null +++ b/apps/establishment/migrations/0067_auto_20191122_1244.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.7 on 2019-11-22 12:44 + +from django.db import migrations, models +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0066_auto_20191122_1144'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='birth_date', + field=models.DateTimeField(default=None, null=True, verbose_name='Birth date'), + ), + migrations.AddField( + model_name='employee', + name='email', + field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='Email'), + ), + migrations.AddField( + model_name='employee', + name='phone', + field=phonenumber_field.modelfields.PhoneNumberField(default=None, max_length=128, null=True), + ), + migrations.AddField( + model_name='employee', + name='sex', + field=models.PositiveSmallIntegerField(choices=[(0, 'Male'), (1, 'Female')], default=None, null=True, verbose_name='Sex'), + ), + migrations.AddField( + model_name='employee', + name='toque_number', + field=models.PositiveSmallIntegerField(default=None, null=True, verbose_name='Toque number'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index f8ffd456..d22aebaa 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,6 +1,7 @@ """Establishment models.""" from datetime import datetime from functools import reduce +from typing import List from operator import or_ import elasticsearch_dsl @@ -435,11 +436,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, @property def visible_tags(self): - return super().visible_tags\ - .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', - 'business_tag', 'business_tags_de'])\ + return super().visible_tags \ + .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', + 'business_tag', 'business_tags_de']) \ + \ + # todo: recalculate toque_number - # todo: recalculate toque_number def recalculate_toque_number(self): toque_number = 0 if self.address and self.public_mark: @@ -612,7 +614,6 @@ class EstablishmentNote(ProjectBaseMixin): class EstablishmentGallery(IntermediateGalleryModelMixin): - establishment = models.ForeignKey(Establishment, null=True, related_name='establishment_gallery', on_delete=models.CASCADE, @@ -663,6 +664,16 @@ class EstablishmentEmployeeQuerySet(models.QuerySet): class EstablishmentEmployee(BaseAttributes): """EstablishmentEmployee model.""" + IDLE = 'I' + ACCEPTED = 'A' + DECLINED = 'D' + + STATUS_CHOICES = ( + (IDLE, 'Idle'), + (ACCEPTED, 'Accepted'), + (DECLINED, 'Declined'), + ) + establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT, verbose_name=_('Establishment')) employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT, @@ -673,19 +684,53 @@ class EstablishmentEmployee(BaseAttributes): verbose_name=_('To date')) position = models.ForeignKey(Position, on_delete=models.PROTECT, verbose_name=_('Position')) + + status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=IDLE) + # old_id = affiliations_id old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) objects = EstablishmentEmployeeQuerySet.as_manager() +class EmployeeQuerySet(models.QuerySet): + + def _generic_search(self, value, filter_fields_names: List[str]): + """Generic method for searching value in specified fields""" + filters = [ + {f'{field}__icontains': value} + for field in filter_fields_names + ] + return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters])) + + def search_by_name_or_last_name(self, value): + """Search by name or last_name.""" + return self._generic_search(value, ['name', 'last_name']) + + class Employee(BaseAttributes): """Employee model.""" user = models.OneToOneField('account.User', on_delete=models.PROTECT, null=True, blank=True, default=None, verbose_name=_('User')) - name = models.CharField(max_length=255, verbose_name=_('Last name')) + name = models.CharField(max_length=255, verbose_name=_('Name')) + last_name = models.CharField(max_length=255, verbose_name=_('Last Name'), null=True, default=None) + + # SEX CHOICES + MALE = 0 + FEMALE = 1 + + SEX_CHOICES = ( + (MALE, _('Male')), + (FEMALE, _('Female')) + ) + sex = models.PositiveSmallIntegerField(choices=SEX_CHOICES, verbose_name=_('Sex'), null=True, default=None) + birth_date = models.DateTimeField(editable=True, verbose_name=_('Birth date'), null=True, default=None) + email = models.EmailField(blank=True, null=True, default=None, verbose_name=_('Email')) + phone = PhoneNumberField(null=True, default=None) + toque_number = models.PositiveSmallIntegerField(verbose_name=_('Toque number'), null=True, default=None) + establishments = models.ManyToManyField(Establishment, related_name='employees', through=EstablishmentEmployee, ) awards = generic.GenericRelation(to='main.Award', related_query_name='employees') @@ -694,6 +739,8 @@ class Employee(BaseAttributes): # old_id = profile_id old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) + objects = EmployeeQuerySet.as_manager() + class Meta: """Meta class.""" diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index a78bce07..52af0574 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -2,8 +2,9 @@ from rest_framework import serializers from establishment import models from establishment import serializers as model_serializers -from location.serializers import AddressDetailSerializer +from location.serializers import AddressDetailSerializer, TranslatedField from main.models import Currency +from main.serializers import AwardSerializer from utils.decorators import with_base_attributes from utils.serializers import TimeZoneChoiceField from gallery.models import Image @@ -161,12 +162,53 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer): class EmployeeBackSerializers(serializers.ModelSerializer): """Employee serializers.""" + awards = AwardSerializer(many=True) + class Meta: model = models.Employee fields = [ 'id', 'user', - 'name' + 'name', + 'last_name', + 'sex', + 'birth_date', + 'email', + 'phone', + 'toque_number', + 'awards', + ] + + +class PositionBackSerializer(serializers.ModelSerializer): + """Position Back serializer.""" + + name_translated = TranslatedField() + + class Meta: + model = models.Position + fields = [ + 'id', + 'name_translated', + 'priority', + 'index_name', + ] + + +class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer): + """Establishment Employee serializer.""" + + employee = EmployeeBackSerializers() + position = PositionBackSerializer() + + class Meta: + model = models.EstablishmentEmployee + fields = [ + 'id', + 'employee', + 'from_date', + 'to_date', + 'position', ] diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index c85b3123..9419c449 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -168,12 +168,51 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer): awards = AwardSerializer(source='employee.awards', many=True) priority = serializers.IntegerField(source='position.priority') position_index_name = serializers.CharField(source='position.index_name') + status = serializers.CharField() class Meta: """Meta class.""" model = models.Employee - fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name') + fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name', 'status') + + +class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer): + """Serializer for establishment employee relation.""" + + class Meta: + """Meta class.""" + + model = models.EstablishmentEmployee + fields = ('id',) + + def _validate_entity(self, entity_id_param: str, entity_class): + entity_id = self.context.get('request').parser_context.get('kwargs').get(entity_id_param) + entity_qs = entity_class.objects.filter(id=entity_id) + if not entity_qs.exists(): + raise serializers.ValidationError({'detail': _(f'{entity_class.__name__} not found.')}) + return entity_qs.first() + + def validate(self, attrs): + """Override validate method""" + establishment = self._validate_entity("establishment_id", models.Establishment) + employee = self._validate_entity("employee_id", models.Employee) + position = self._validate_entity("position_id", models.Position) + + attrs['establishment'] = establishment + attrs['employee'] = employee + attrs['position'] = position + + return attrs + + def create(self, validated_data, *args, **kwargs): + """Override create method""" + validated_data.update({ + 'employee': validated_data.pop('employee'), + 'establishment': validated_data.pop('establishment'), + 'position': validated_data.pop("position") + }) + return super().create(validated_data) class EstablishmentShortSerializer(serializers.ModelSerializer): @@ -396,6 +435,22 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer return super().create(validated_data) +class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer): + """Retrieve/Update/Destroy comment serializer.""" + + class Meta: + """Meta class.""" + model = comment_models.Comment + fields = [ + 'id', + 'created', + 'text', + 'mark', + 'nickname', + 'profile_pic', + ] + + class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): """Serializer to favorite object w/ model Establishment.""" diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index f06e2187..a06adc3b 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -38,8 +38,16 @@ urlpatterns = [ path('phones//', views.PhonesRUDView.as_view(), name='phones-rud'), path('emails/', views.EmailListCreateView.as_view(), name='emails'), path('emails//', views.EmailRUDView.as_view(), name='emails-rud'), + path('/employees/', views.EstablishmentEmployeeListView.as_view(), + name='establishment-employees'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), path('employees//', views.EmployeeRUDView.as_view(), name='employees-rud'), + path('/employee//position/', + views.EstablishmentEmployeeCreateView.as_view(), + name='employees-establishment-create'), + path('/employee/', + views.EstablishmentEmployeeDeleteView.as_view(), + name='employees-establishment-delete'), path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'), path('types//', views.EstablishmentTypeRUDView.as_view(), name='type-rud'), path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index d1897397..312bb171 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -1,7 +1,9 @@ """Establishment app views.""" +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 -from rest_framework import generics, permissions +from rest_framework import generics, permissions, status +from utils.permissions import IsCountryAdmin, IsEstablishmentManager from establishment import filters, models, serializers from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from utils.permissions import IsCountryAdmin, IsEstablishmentManager @@ -43,8 +45,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): """ Returns the object the view is displaying. """ - establishment_pk = self.kwargs.get('pk') - schedule_id = self.kwargs.get('schedule_id') + establishment_pk = self.kwargs['pk'] + schedule_id = self.kwargs['schedule_id'] establishment = get_object_or_404(klass=models.Establishment.objects.all(), pk=establishment_pk) @@ -156,11 +158,23 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView): class EmployeeListCreateView(generics.ListCreateAPIView): """Emplyoee list create view.""" + permission_classes = (permissions.AllowAny, ) + filter_class = filters.EmployeeBackFilter serializer_class = serializers.EmployeeBackSerializers queryset = models.Employee.objects.all() pagination_class = None +class EstablishmentEmployeeListView(generics.ListAPIView): + """Establishment emplyoees list view.""" + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.EstablishmentEmployeeBackSerializer + + def get_queryset(self): + establishment_id = self.kwargs['establishment_id'] + return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id) + + class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): """Employee RUD view.""" serializer_class = serializers.EmployeeBackSerializers @@ -318,3 +332,27 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews, self.check_object_permissions(self.request, note) return note + + +class EstablishmentEmployeeCreateView(generics.CreateAPIView): + serializer_class = serializers.EstablishmentEmployeeCreateSerializer + queryset = models.EstablishmentEmployee.objects.all() + # TODO send email to all admins and add endpoint for changing status + + +class EstablishmentEmployeeDeleteView(generics.DestroyAPIView): + + def _get_object_to_delete(self, establishment_id, employee_id): + result_qs = models.EstablishmentEmployee\ + .objects\ + .filter(establishment_id=establishment_id, employee_id=employee_id) + if not result_qs.exists(): + raise Http404 + return result_qs.first() + + def delete(self, request, *args, **kwargs): + establishment_id = self.kwargs["establishment_id"] + employee_id = self.kwargs["employee_id"] + object_to_delete = self._get_object_to_delete(establishment_id, employee_id) + object_to_delete.delete() + return HttpResponse(status=status.HTTP_204_NO_CONTENT) From c2bb9f0814d6bbcc41f65966c75108469edcdca0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 18:21:37 +0300 Subject: [PATCH 036/191] boost detail establishment speed --- apps/establishment/models.py | 6 ++++-- project/settings/production.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index f0260cf9..edf8d33d 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -118,11 +118,13 @@ class EstablishmentQuerySet(models.QuerySet): 'address__city__country') def with_extended_related(self): - return self.select_related('establishment_type'). \ + return self.with_extended_address_related().select_related('establishment_type'). \ prefetch_related('establishment_subtypes', 'awards', 'schedule', - 'phones'). \ + 'phones', 'gallery', 'menu_set', 'menu_set__plate_set', + 'menu_set__plate_set__currency', 'currency'). \ prefetch_actual_employees() + def with_type_related(self): return self.prefetch_related('establishment_subtypes') diff --git a/project/settings/production.py b/project/settings/production.py index 3192acea..e745f3bc 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -51,4 +51,6 @@ GUESTONLINE_SERVICE = 'https://api.guestonline.fr/' GUESTONLINE_TOKEN = '' LASTABLE_SERVICE = '' LASTABLE_TOKEN = '' -LASTABLE_PROXY = '' \ No newline at end of file +LASTABLE_PROXY = '' + +THUMBNAIL_FORCE_OVERWRITE = True # see: https://github.com/jazzband/sorl-thumbnail/issues/351 \ No newline at end of file From 586f2a7a0356fb247499c7c18db523ca60eeb6e2 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 18:21:37 +0300 Subject: [PATCH 037/191] boost detail establishment speed (cherry picked from commit c2bb9f0) --- apps/establishment/models.py | 6 ++++-- project/settings/production.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index d22aebaa..cb490aa4 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -119,11 +119,13 @@ class EstablishmentQuerySet(models.QuerySet): 'address__city__country') def with_extended_related(self): - return self.select_related('establishment_type'). \ + return self.with_extended_address_related().select_related('establishment_type'). \ prefetch_related('establishment_subtypes', 'awards', 'schedule', - 'phones'). \ + 'phones', 'gallery', 'menu_set', 'menu_set__plate_set', + 'menu_set__plate_set__currency', 'currency'). \ prefetch_actual_employees() + def with_type_related(self): return self.prefetch_related('establishment_subtypes') diff --git a/project/settings/production.py b/project/settings/production.py index 3192acea..e745f3bc 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -51,4 +51,6 @@ GUESTONLINE_SERVICE = 'https://api.guestonline.fr/' GUESTONLINE_TOKEN = '' LASTABLE_SERVICE = '' LASTABLE_TOKEN = '' -LASTABLE_PROXY = '' \ No newline at end of file +LASTABLE_PROXY = '' + +THUMBNAIL_FORCE_OVERWRITE = True # see: https://github.com/jazzband/sorl-thumbnail/issues/351 \ No newline at end of file From c83c07b9e1384795bfe9dd8c4f500c62fcdcdc90 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Fri, 22 Nov 2019 18:33:39 +0300 Subject: [PATCH 038/191] add site features inline admin --- apps/main/admin.py | 6 ++++++ make_data_migration.sh | 14 ++++++++++++++ project/settings/local.py | 11 ++++++----- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100755 make_data_migration.sh diff --git a/apps/main/admin.py b/apps/main/admin.py index 9ec76164..4b7038e7 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -3,9 +3,15 @@ from django.contrib import admin from main import models +class SiteSettingsInline(admin.TabularInline): + model = models.SiteFeature + extra = 1 + + @admin.register(models.SiteSettings) class SiteSettingsAdmin(admin.ModelAdmin): """Site settings admin conf.""" + inlines = [SiteSettingsInline,] @admin.register(models.Feature) diff --git a/make_data_migration.sh b/make_data_migration.sh new file mode 100755 index 00000000..c92f74e7 --- /dev/null +++ b/make_data_migration.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +./manage.py transfer -a +#./manage.py transfer -d +./manage.py transfer -e +./manage.py transfer --fill_city_gallery +./manage.py transfer -l +./manage.py transfer --product +./manage.py transfer --souvenir +./manage.py transfer --establishment_note +./manage.py transfer --product_note +./manage.py transfer --wine_characteristics +./manage.py transfer --inquiries +./manage.py transfer --assemblage +./manage.py transfer --purchased_plaques \ No newline at end of file diff --git a/project/settings/local.py b/project/settings/local.py index 959e6149..f88a72b1 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -80,11 +80,11 @@ LOGGING = { 'py.warnings': { 'handlers': ['console'], }, - 'django.db.backends': { - 'handlers': ['console', ], - 'level': 'DEBUG', - 'propagate': False, - }, + # 'django.db.backends': { + # 'handlers': ['console', ], + # 'level': 'DEBUG', + # 'propagate': False, + # }, } } @@ -106,3 +106,4 @@ ELASTICSEARCH_INDEX_NAMES = { TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} +ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file From 65bb22bb218c6ff8e720b0c93a5c920e9b7ada82 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 22 Nov 2019 19:04:40 +0300 Subject: [PATCH 039/191] reassign sorl-thumbnail engine --- apps/gallery/serializers.py | 24 ++++++++++++------------ apps/utils/models.py | 9 +++++---- apps/utils/thumbnail_engine.py | 19 +++++++++++++++++++ project/settings/base.py | 1 + 4 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 apps/utils/thumbnail_engine.py diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 2c2e50d0..538988c9 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -39,11 +39,11 @@ class ImageSerializer(serializers.ModelSerializer): class CropImageSerializer(ImageSerializer): """Serializers for image crops.""" - width = serializers.IntegerField(write_only=True) - height = serializers.IntegerField(write_only=True) - margin = serializers.CharField(write_only=True, allow_null=True, - required=False, - default='center') + width = serializers.IntegerField(write_only=True, required=False) + height = serializers.IntegerField(write_only=True, required=False) + crop = serializers.CharField(write_only=True, allow_null=True, + required=False, + default='center') quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, default=settings.THUMBNAIL_QUALITY, validators=[ @@ -59,7 +59,7 @@ class CropImageSerializer(ImageSerializer): 'orientation_display', 'width', 'height', - 'margin', + 'crop', 'quality', 'cropped_image', ] @@ -69,16 +69,16 @@ class CropImageSerializer(ImageSerializer): file = self._image.image crop_width = attrs.get('width') crop_height = attrs.get('height') - margin = attrs.get('margin') + crop = attrs.get('crop') - if crop_height and crop_width and margin: + if crop_height and crop_width and crop: xy_image = (file.width, file.width) xy_window = (crop_width, crop_height) try: - parse_crop(margin, xy_image, xy_window) + parse_crop(crop, xy_image, xy_window) attrs['image'] = file except ThumbnailParseError: - raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % margin}) + raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % crop}) return attrs def create(self, validated_data): @@ -86,7 +86,7 @@ class CropImageSerializer(ImageSerializer): width = validated_data.pop('width', None) height = validated_data.pop('height', None) quality = validated_data.pop('quality') - margin = validated_data.pop('margin') + crop = validated_data.pop('crop') image = self._image @@ -96,7 +96,7 @@ class CropImageSerializer(ImageSerializer): image.get_cropped_image( geometry=f'{width}x{height}', quality=quality, - margin=margin)) + crop=crop)) return image @property diff --git a/apps/utils/models.py b/apps/utils/models.py index 59ec2282..adbe19d0 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -11,11 +11,11 @@ from django.contrib.postgres.fields.jsonb import KeyTextTransform from django.utils import timezone from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _, get_language -from configuration.models import TranslationSettings from easy_thumbnails.fields import ThumbnailerImageField from sorl.thumbnail import get_thumbnail from sorl.thumbnail.fields import ImageField as SORLImageField +from configuration.models import TranslationSettings from utils.methods import image_path, svg_image_path from utils.validators import svg_image_validator @@ -227,16 +227,16 @@ class SORLImageMixin(models.Model): else: return None - def get_cropped_image(self, geometry: str, quality: int, margin: str) -> dict: + def get_cropped_image(self, geometry: str, quality: int, crop: str) -> dict: cropped_image = get_thumbnail(self.image, geometry_string=geometry, - crop=margin, + crop=crop, quality=quality) return { 'geometry_string': geometry, 'crop_url': cropped_image.url, 'quality': quality, - 'margin': margin + 'crop': crop } image_tag.short_description = _('Image') @@ -455,4 +455,5 @@ class FavoritesMixin: def favorites_for_users(self): return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr') + timezone.datetime.now().date().isoformat() \ No newline at end of file diff --git a/apps/utils/thumbnail_engine.py b/apps/utils/thumbnail_engine.py new file mode 100644 index 00000000..f55d58f8 --- /dev/null +++ b/apps/utils/thumbnail_engine.py @@ -0,0 +1,19 @@ +"""Overridden thumbnail engine.""" +from sorl.thumbnail.engines.pil_engine import Engine as PILEngine + + +class GMEngine(PILEngine): + + def create(self, image, geometry, options): + """ + Processing conductor, returns the thumbnail as an image engine instance + """ + image = self.cropbox(image, geometry, options) + image = self.orientation(image, geometry, options) + image = self.colorspace(image, geometry, options) + image = self.remove_border(image, options) + image = self.crop(image, geometry, options) + image = self.rounded(image, geometry, options) + image = self.blur(image, geometry, options) + image = self.padding(image, geometry, options) + return image diff --git a/project/settings/base.py b/project/settings/base.py index 0868c116..ca4232ae 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -520,3 +520,4 @@ ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] +THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine' From d9743d84657d069aec4953a569970c539e1b7bc0 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 22 Nov 2019 22:47:41 +0300 Subject: [PATCH 040/191] refactored serializer --- apps/gallery/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 538988c9..36360180 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -41,10 +41,10 @@ class CropImageSerializer(ImageSerializer): width = serializers.IntegerField(write_only=True, required=False) height = serializers.IntegerField(write_only=True, required=False) - crop = serializers.CharField(write_only=True, allow_null=True, + crop = serializers.CharField(write_only=True, required=False, default='center') - quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, + quality = serializers.IntegerField(write_only=True, required=False, default=settings.THUMBNAIL_QUALITY, validators=[ MinValueValidator(1), @@ -71,7 +71,7 @@ class CropImageSerializer(ImageSerializer): crop_height = attrs.get('height') crop = attrs.get('crop') - if crop_height and crop_width and crop: + if (crop_height and crop_width) and (crop and crop != 'smart'): xy_image = (file.width, file.width) xy_window = (crop_width, crop_height) try: From 9b61ff31147043edbe331315db88d6bba3593dc2 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sat, 23 Nov 2019 14:58:49 +0300 Subject: [PATCH 041/191] added COOKIE_DOMAIN parameter to settings --- apps/utils/views.py | 22 ++++++---------------- project/settings/base.py | 2 ++ project/settings/development.py | 2 ++ project/settings/local.py | 11 +---------- project/settings/production.py | 6 +----- project/settings/stage.py | 4 ++-- 6 files changed, 14 insertions(+), 33 deletions(-) diff --git a/apps/utils/views.py b/apps/utils/views.py index c09df2a2..8becc9ea 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -70,22 +70,12 @@ class JWTGenericViewMixin: def _put_cookies_in_response(self, cookies: list, response: Response): """Update COOKIES in response from namedtuple""" for cookie in cookies: - # todo: remove config for develop - from os import environ - configuration = environ.get('SETTINGS_CONFIGURATION', None) - if configuration == 'development' or configuration == 'stage': - response.set_cookie(key=cookie.key, - value=cookie.value, - secure=cookie.secure, - httponly=cookie.http_only, - max_age=cookie.max_age, - domain='.id-east.ru') - else: - response.set_cookie(key=cookie.key, - value=cookie.value, - secure=cookie.secure, - httponly=cookie.http_only, - max_age=cookie.max_age, ) + response.set_cookie(key=cookie.key, + value=cookie.value, + secure=cookie.secure, + httponly=cookie.http_only, + max_age=cookie.max_age, + domain=settings.COOKIE_DOMAIN) return response def _get_tokens_from_cookies(self, request, cookies: dict = None): diff --git a/project/settings/base.py b/project/settings/base.py index ca4232ae..2c6f0cf0 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -521,3 +521,5 @@ NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', ' INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine' + +COOKIE_DOMAIN = None diff --git a/project/settings/development.py b/project/settings/development.py index 057438f5..f850aad7 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -71,3 +71,5 @@ INSTALLED_APPS.append('transfer.apps.TransferConfig') BROKER_URL = 'redis://localhost:6379/1' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL + +COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index f88a72b1..7784be90 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -5,18 +5,15 @@ import sys ALLOWED_HOSTS = ['*', ] - SEND_SMS = False SMS_CODE_SHOW = True USE_CELERY = True - SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'testserver.com:8000' DOMAIN_URI = '0.0.0.0:8000' - # CELERY # RabbitMQ # BROKER_URL = 'amqp://rabbitmq:5672' @@ -25,20 +22,16 @@ BROKER_URL = 'redis://redis:6379/1' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL - # MEDIA MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/' MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) - # SORL thumbnails THUMBNAIL_DEBUG = True - # ADDED TRANSFER APP INSTALLED_APPS.append('transfer.apps.TransferConfig') - # DATABASES DATABASES.update({ 'legacy': { @@ -88,7 +81,6 @@ LOGGING = { } } - # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { @@ -102,8 +94,7 @@ ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.product': 'local_product', } - TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} -ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file +ELASTICSEARCH_DSL_AUTOSYNC = False diff --git a/project/settings/production.py b/project/settings/production.py index e745f3bc..997d6526 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -20,7 +20,6 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'gaultmillau.com' DOMAIN_URI = 'next.gaultmillau.com' - # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { @@ -28,20 +27,17 @@ ELASTICSEARCH_DSL = { } } - ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.news': 'development_news', # temporarily disabled 'search_indexes.documents.establishment': 'development_establishment', 'search_indexes.documents.product': 'development_product', } - sentry_sdk.init( dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093", integrations=[DjangoIntegration()] ) - BROKER_URL = 'redis://redis:6379/1' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL @@ -53,4 +49,4 @@ LASTABLE_SERVICE = '' LASTABLE_TOKEN = '' LASTABLE_PROXY = '' -THUMBNAIL_FORCE_OVERWRITE = True # see: https://github.com/jazzband/sorl-thumbnail/issues/351 \ No newline at end of file +COOKIE_DOMAIN = '.gaultmillau.com' diff --git a/project/settings/stage.py b/project/settings/stage.py index 49a7ae0f..95285034 100644 --- a/project/settings/stage.py +++ b/project/settings/stage.py @@ -13,7 +13,6 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm-stage.id-east.ru' - # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { @@ -21,8 +20,9 @@ ELASTICSEARCH_DSL = { } } - ELASTICSEARCH_INDEX_NAMES = { # 'search_indexes.documents.news': 'stage_news', #temporarily disabled 'search_indexes.documents.establishment': 'stage_establishment', } + +COOKIE_DOMAIN = '.id-east.ru' From 58db94e2d08d8ca4eed46eaa04cb815b84700de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 09:33:22 +0300 Subject: [PATCH 042/191] Permission --- apps/utils/permissions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index bdb17726..3a226dbd 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -121,11 +121,10 @@ class IsContentPageManager(IsStandardUser): if hasattr(request, 'user') and hasattr(request.data, 'site_id'): role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, site_id=request.data.site_id,) \ - .first() # 'Comments moderator' + .first() rules = [ UserRole.objects.filter(user=request.user, role=role).exists(), - # and obj.user != request.user, super().has_permission(request, view) ] return any(rules) From 49e5e4ea9141a035627c4aaf829e270dfd77d8a3 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Nov 2019 09:38:50 +0300 Subject: [PATCH 043/191] test establishment and news carousel --- apps/establishment/tests.py | 25 +++++++++++++++++++++++++ apps/news/tests.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 3534608c..6d456a45 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -441,3 +441,28 @@ class EstablishmentWebFavoriteTests(ChildTestCase): f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + +class EstablishmentCarouselTests(ChildTestCase): + + def test_web_carousel_CR(self): + data = { + "object_id": self.establishment.id + } + + response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/carousels/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.delete(f'/api/web/establishments/slug/{self.establishment.slug}/carousels/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_mobile_carousel_CR(self): + data = { + "object_id": self.establishment.id + } + + response = self.client.post(f'/api/mobile/establishments/slug/{self.establishment.slug}/carousels/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.delete(f'/api/mobile/establishments/slug/{self.establishment.slug}/carousels/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/news/tests.py b/apps/news/tests.py index 532a6efc..a87e457a 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -30,12 +30,12 @@ class BaseTestCase(APITestCase): 'refresh_token': tokens.get('refresh_token')}) self.test_news_type = NewsType.objects.create(name="Test news type") - self.lang = Language.objects.get( + self.lang = Language.objects.create( title='Russia', locale='ru-RU' ) - self.country_ru = Country.objects.get( + self.country_ru = Country.objects.create( name={"en-GB": "Russian"} ) @@ -128,3 +128,28 @@ class NewsTestCase(BaseTestCase): response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + +class NewsCarouselTests(BaseTestCase): + + def test_web_carousel_CR(self): + data = { + "object_id": self.test_news.id + } + + response = self.client.post(f'/api/web/news/slug/{self.test_news.slug}/carousels/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/carousels/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_mobile_carousel_CR(self): + data = { + "object_id": self.test_news.id + } + + response = self.client.post(f'/api/mobile/news/slug/{self.test_news.slug}/carousels/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.delete(f'/api/mobile/news/slug/{self.test_news.slug}/carousels/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) From 13eb8abac643c1886d87860cc10f65167cc102ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 09:49:17 +0300 Subject: [PATCH 044/191] fix country or site --- apps/utils/permissions.py | 98 +++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 3a226dbd..406c1692 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -117,29 +117,50 @@ class IsContentPageManager(IsStandardUser): rules = [ super().has_permission(request, view) ] - # and request.user.email_confirmed, - if hasattr(request, 'user') and hasattr(request.data, 'site_id'): - role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, - site_id=request.data.site_id,) \ - .first() - rules = [ - UserRole.objects.filter(user=request.user, role=role).exists(), - super().has_permission(request, view) - ] + if hasattr(request, 'user'): + if hasattr(request.data, 'site_id'): + role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, + site_id=request.data.site_id,) \ + .first() + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_permission(request, view) + ] + elif hasattr(request.data, 'country_id'): + role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, + country_id=request.data.country_id) \ + .first() + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_permission(request, view) + ] + return any(rules) def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request. + if hasattr(obj, 'site_id'): + role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, + site_id=obj.site_id) \ + .first() - role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, - site_id=obj.site_id) \ - .first() + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_object_permission(request, view, obj) + ] + elif hasattr(obj, 'country_id'): + role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, + country_id=obj.country_id) \ + .first() + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_object_permission(request, view, obj) + ] - rules = [ - UserRole.objects.filter(user=request.user, role=role).exists(), - super().has_object_permission(request, view, obj) - ] return any(rules) @@ -148,36 +169,55 @@ class IsCountryAdmin(IsStandardUser): Object-level permission to only allow owners of an object to edit it. Assumes the model instance has an `owner` attribute. """ - def has_permission(self, request, view): rules = [ super().has_permission(request, view) ] # and request.user.email_confirmed, - if hasattr(request.data, 'user') and hasattr(request.data, 'site_id'): - # Read permissions are allowed to any request. + if hasattr(request.data, 'user'): + if hasattr(request.data, 'site_id'): + # Read permissions are allowed to any request. + + role = Role.objects.filter(role=Role.COUNTRY_ADMIN, + site_id=request.data.site_id) \ + .first() + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_permission(request, view) + ] + elif hasattr(request.data, 'country_id'): role = Role.objects.filter(role=Role.COUNTRY_ADMIN, - site_id=request.data.site_id) \ - .first() # 'Comments moderator' + country_id=request.data.country_id) \ + .first() rules = [ UserRole.objects.filter(user=request.user, role=role).exists(), super().has_permission(request, view) - ] + ] return any(rules) def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request. - role = Role.objects.filter(role=Role.COUNTRY_ADMIN, - site_id=obj.site_id) \ - .first() # 'Comments moderator' + if hasattr(obj, 'site_id'): + role = Role.objects.filter(role=Role.COUNTRY_ADMIN, + site_id=obj.site_id) \ + .first() + + rules = [ + super().has_object_permission(request, view, obj) + ] + elif hasattr(obj, 'country_id'): + role = Role.objects.filter(role=Role.COUNTRY_ADMIN, + country_id=obj.country_id) \ + .first() + + rules = [ + super().has_object_permission(request, view, obj) + ] - rules = [ - super().has_object_permission(request, view, obj) - ] - # and request.user.email_confirmed, if hasattr(request, 'user') and request.user.is_authenticated: rules = [ UserRole.objects.filter(user=request.user, role=role).exists(), From 6bb0eac409b1c63623317c18a9934ee338c138d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 10:45:20 +0300 Subject: [PATCH 045/191] Fix establishment timetable serailizer for permission --- apps/partner/serializers/back.py | 1 + apps/timetable/serialziers.py | 3 +++ apps/utils/permissions.py | 36 +++++++++++++++++++------------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/partner/serializers/back.py b/apps/partner/serializers/back.py index d011e058..e9e03fe0 100644 --- a/apps/partner/serializers/back.py +++ b/apps/partner/serializers/back.py @@ -13,6 +13,7 @@ class BackPartnerSerializer(serializers.ModelSerializer): 'url', 'image', 'establishment', + 'establishment_id', 'type', 'starting_date', 'expiry_date', diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index 533bca70..f7ae4204 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -20,6 +20,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): dinner_end = serializers.TimeField(required=False) opening_at = serializers.TimeField(required=False) closed_at = serializers.TimeField(required=False) + establishment_id = serializers.ReadOnlyField(source='establishment.id') class Meta: """Meta class.""" @@ -34,8 +35,10 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): 'dinner_end', 'opening_at', 'closed_at', + 'establishment_id' ] + def validate(self, attrs): """Override validate method""" establishment_pk = self.context.get('request')\ diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 406c1692..7b8ff845 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -280,30 +280,36 @@ class IsEstablishmentManager(IsStandardUser): super().has_permission(request, view) ] - # and request.user.email_confirmed, - if hasattr(request.data, 'user') and hasattr(request.data, 'establishment_id'): - role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ - .first() # 'Comments moderator' + if hasattr(request.data, 'user'): + if hasattr(request.data, 'establishment_id'): + role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ + .first() - rules = [ - UserRole.objects.filter(user=request.user, role=role, - establishment_id=request.data.establishment_id - ).exists(), - super().has_permission(request, view) - ] + rules = [ + UserRole.objects.filter(user=request.user, role=role, + establishment_id=request.data.establishment_id + ).exists(), + super().has_permission(request, view) + ] return any(rules) def has_object_permission(self, request, view, obj): - role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ - .first() # 'Comments moderator' rules = [ - UserRole.objects.filter(user=request.user, role=role, - establishment_id=obj.establishment_id - ).exists(), super().has_object_permission(request, view, obj) ] + role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ + .first() + + if hasattr(obj, 'establishment_id'): + rules = [ + UserRole.objects.filter(user=request.user, role=role, + establishment_id=obj.establishment_id + ).exists(), + super().has_object_permission(request, view, obj) + ] + return any(rules) From 29782fe876d143486fa042cb0d8470ed1904282e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 10:45:57 +0300 Subject: [PATCH 046/191] Fix --- apps/timetable/serialziers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index f7ae4204..48c8374d 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -20,6 +20,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): dinner_end = serializers.TimeField(required=False) opening_at = serializers.TimeField(required=False) closed_at = serializers.TimeField(required=False) + # For permission!! establishment_id = serializers.ReadOnlyField(source='establishment.id') class Meta: @@ -38,7 +39,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): 'establishment_id' ] - def validate(self, attrs): """Override validate method""" establishment_pk = self.context.get('request')\ From 86500b6b88a4f4fd83bf80b0627758ac524dc3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 11:13:05 +0300 Subject: [PATCH 047/191] Fix site_id for comment --- apps/comment/migrations/0007_comment_site.py | 20 ++++++++++++++++++++ apps/comment/models.py | 3 ++- apps/comment/tests.py | 12 +++++++++--- apps/utils/permissions.py | 8 ++++---- apps/utils/tests/tests_permissions.py | 6 ++---- 5 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 apps/comment/migrations/0007_comment_site.py diff --git a/apps/comment/migrations/0007_comment_site.py b/apps/comment/migrations/0007_comment_site.py new file mode 100644 index 00000000..c19629df --- /dev/null +++ b/apps/comment/migrations/0007_comment_site.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2019-11-25 08:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0037_sitesettings_old_id'), + ('comment', '0006_comment_is_publish'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site'), + ), + ] diff --git a/apps/comment/models.py b/apps/comment/models.py index 421a05fb..fa27d3f9 100644 --- a/apps/comment/models.py +++ b/apps/comment/models.py @@ -35,7 +35,8 @@ class Comment(ProjectBaseMixin): user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User')) old_id = models.IntegerField(null=True, blank=True, default=None) is_publish = models.BooleanField(default=False, verbose_name=_('Publish status')) - + site = models.ForeignKey('main.SiteSettings', blank=True, null=True, + on_delete=models.SET_NULL, verbose_name=_('site')) content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') diff --git a/apps/comment/tests.py b/apps/comment/tests.py index ff3bd393..832b86cb 100644 --- a/apps/comment/tests.py +++ b/apps/comment/tests.py @@ -8,6 +8,7 @@ from account.models import Role, User, UserRole from authorization.tests.tests_authorization import get_tokens_for_user from comment.models import Comment from utils.tests.tests_permissions import BasePermissionTests +from main.models import SiteSettings class CommentModeratorPermissionTests(BasePermissionTests): @@ -33,11 +34,16 @@ class CommentModeratorPermissionTests(BasePermissionTests): self.content_type = ContentType.objects.get(app_label='location', model='country') self.user_test = get_tokens_for_user() + + self.site_ru, created = SiteSettings.objects.get_or_create( + subdomain='ru' + ) + self.comment = Comment.objects.create(text='Test comment', mark=1, user=self.user_test["user"], object_id=self.country_ru.pk, content_type_id=self.content_type.id, - country=self.country_ru + site=self.site_ru ) self.comment.save() self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id}) @@ -50,7 +56,7 @@ class CommentModeratorPermissionTests(BasePermissionTests): "user": self.user_test["user"].id, "object_id": self.country_ru.pk, "content_type": self.content_type.id, - "country_id": self.country_ru.id + "site_id": self.site_ru.id } response = self.client.post(self.url, format='json', data=comment) @@ -61,7 +67,7 @@ class CommentModeratorPermissionTests(BasePermissionTests): "user": self.moderator.id, "object_id": self.country_ru.id, "content_type": self.content_type.id, - "country_id": self.country_ru.id + "site_id": self.site_ru.id } tokens = User.create_jwt_tokens(self.moderator) diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 7b8ff845..a32e302c 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -321,13 +321,13 @@ class IsReviewerManager(IsStandardUser): ] # and request.user.email_confirmed, - if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): + if hasattr(request.data, 'user') and hasattr(request.data, 'site_id'): role = Role.objects.filter(role=Role.REVIEWER_MANGER) \ - .first() # 'Comments moderator' + .first() rules = [ UserRole.objects.filter(user=request.user, role=role, - establishment_id=request.data.country_id + establishment_id=request.data.site_id ).exists(), super().has_permission(request, view) ] @@ -335,7 +335,7 @@ class IsReviewerManager(IsStandardUser): def has_object_permission(self, request, view, obj): role = Role.objects.filter(role=Role.REVIEWER_MANGER, - country_id=obj.country_id) \ + country_id=obj.site_id) \ .first() rules = [ diff --git a/apps/utils/tests/tests_permissions.py b/apps/utils/tests/tests_permissions.py index 3bba7b7d..18eeb95b 100644 --- a/apps/utils/tests/tests_permissions.py +++ b/apps/utils/tests/tests_permissions.py @@ -5,15 +5,13 @@ from translation.models import Language class BasePermissionTests(APITestCase): def setUp(self): - self.lang = Language.objects.get( + self.lang, created = Language.objects.get_or_create( title='Russia', locale='ru-RU' ) - self.lang.save() - self.country_ru = Country.objects.get( + self.country_ru, created = Country.objects.get_or_create( name={"en-GB": "Russian"} ) - self.country_ru.save() From 28a3e022f679e28a7d6b75facecdb643244b509a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 12:13:14 +0300 Subject: [PATCH 048/191] Fix comment test and permission --- apps/comment/tests.py | 15 ++++++++------- apps/comment/views/back.py | 6 +++--- apps/utils/permissions.py | 32 ++++++++++++++++++++------------ 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/apps/comment/tests.py b/apps/comment/tests.py index 832b86cb..79bed42f 100644 --- a/apps/comment/tests.py +++ b/apps/comment/tests.py @@ -15,9 +15,13 @@ class CommentModeratorPermissionTests(BasePermissionTests): def setUp(self): super().setUp() + self.site_ru, created = SiteSettings.objects.get_or_create( + subdomain='ru' + ) + self.role = Role.objects.create( role=2, - country=self.country_ru + site=self.site_ru ) self.role.save() @@ -35,10 +39,6 @@ class CommentModeratorPermissionTests(BasePermissionTests): self.user_test = get_tokens_for_user() - self.site_ru, created = SiteSettings.objects.get_or_create( - subdomain='ru' - ) - self.comment = Comment.objects.create(text='Test comment', mark=1, user=self.user_test["user"], object_id=self.country_ru.pk, @@ -89,8 +89,9 @@ class CommentModeratorPermissionTests(BasePermissionTests): "text": "test text moderator", "mark": 1, "user": self.moderator.id, - "object_id": self.comment.country_id, - "content_type": self.content_type.id + "object_id": self.country_ru.id, + "content_type": self.content_type.id, + 'site_id': self.site_ru.id } response = self.client.put(self.url, data=data, format='json') diff --git a/apps/comment/views/back.py b/apps/comment/views/back.py index 3b96cbd2..a46b70cb 100644 --- a/apps/comment/views/back.py +++ b/apps/comment/views/back.py @@ -8,13 +8,13 @@ class CommentLstView(generics.ListCreateAPIView): """Comment list create view.""" serializer_class = serializers.CommentBaseSerializer queryset = models.Comment.objects.all() - permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin] + # permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin] class CommentRUDView(generics.RetrieveUpdateDestroyAPIView): """Comment RUD view.""" serializer_class = serializers.CommentBaseSerializer queryset = models.Comment.objects.all() - - permission_classes = [IsCountryAdmin | IsCommentModerator] + permission_classes = [IsCommentModerator] + # permission_classes = [IsCountryAdmin | IsCommentModerator] lookup_field = 'id' diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index a32e302c..321d72d3 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -100,7 +100,10 @@ class IsStandardUser(IsGuest): if hasattr(obj, 'user'): rules = [ - obj.user == request.user and obj.user.email_confirmed, + obj.user == request.user + and obj.user.email_confirmed + and request.user.is_authenticated, + super().has_object_permission(request, view, obj) ] @@ -244,13 +247,12 @@ class IsCommentModerator(IsStandardUser): super().has_permission(request, view) ] - # and request.user.email_confirmed, - if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): + if any(rules) and hasattr(request.data, 'site_id'): # Read permissions are allowed to any request. role = Role.objects.filter(role=Role.COMMENTS_MODERATOR, - country_id=request.data.country_id) \ - .first() # 'Comments moderator' + site_id=request.data.site_id) \ + .first() rules = [ UserRole.objects.filter(user=request.user, role=role).exists(), @@ -260,16 +262,22 @@ class IsCommentModerator(IsStandardUser): return any(rules) def has_object_permission(self, request, view, obj): - # Read permissions are allowed to any request. - role = Role.objects.filter(role=Role.COMMENTS_MODERATOR, - country_id=obj.country_id) \ - .first() # 'Comments moderator' rules = [ - UserRole.objects.filter(user=request.user, role=role).exists() and - obj.user != request.user, super().has_object_permission(request, view, obj) ] + + if request.user.is_authenticated: + + role = Role.objects.filter(role=Role.COMMENTS_MODERATOR, + site_id=obj.site_id) \ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists() and + obj.user != request.user, + super().has_object_permission(request, view, obj) + ] return any(rules) @@ -335,7 +343,7 @@ class IsReviewerManager(IsStandardUser): def has_object_permission(self, request, view, obj): role = Role.objects.filter(role=Role.REVIEWER_MANGER, - country_id=obj.site_id) \ + country_id=obj.country_id) \ .first() rules = [ From 43fc16f8416f765cc4596694f466b68248983d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 12:29:53 +0300 Subject: [PATCH 049/191] Fix country test --- apps/location/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/location/tests.py b/apps/location/tests.py index 3eaefd85..960f7f2b 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -25,12 +25,12 @@ class BaseTestCase(APITestCase): {'access_token': tokens.get('access_token'), 'refresh_token': tokens.get('refresh_token')}) - self.lang = Language.objects.get( + self.lang, created = Language.objects.get_or_create( title='Russia', locale='ru-RU' ) - self.country_ru = Country.objects.get( + self.country_ru, created = Country.objects.get_or_create( name={"en-GB": "Russian"} ) @@ -72,7 +72,7 @@ class CountryTests(BaseTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { - 'name': json.dumps({"en-GB": "Test new country"}) + 'name': json.dumps({"ru-RU": "Test new country"}) } response = self.client.patch(f'/api/back/location/countries/{response_data["id"]}/', data=update_data) From 49c6dc8f1d31c463b9fc8dfa455733577e0b9aaa Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 25 Nov 2019 14:21:05 +0300 Subject: [PATCH 050/191] static to Amazon --- project/settings/amazon_s3.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project/settings/amazon_s3.py b/project/settings/amazon_s3.py index c793dd77..c96da5d9 100644 --- a/project/settings/amazon_s3.py +++ b/project/settings/amazon_s3.py @@ -13,9 +13,9 @@ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'} AWS_S3_ADDRESSING_STYLE = 'path' # Static settings -# PUBLIC_STATIC_LOCATION = 'static' -# STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' -# STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' +PUBLIC_STATIC_LOCATION = 'static' +STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' +STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' # Public media settings MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/' From a660ffcc313623d69e437afb6f27734c372f717a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 25 Nov 2019 14:26:37 +0300 Subject: [PATCH 051/191] static to Amazon #2 --- project/settings/amazon_s3.py | 2 +- project/settings/production.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/project/settings/amazon_s3.py b/project/settings/amazon_s3.py index c96da5d9..b602618d 100644 --- a/project/settings/amazon_s3.py +++ b/project/settings/amazon_s3.py @@ -13,7 +13,7 @@ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'} AWS_S3_ADDRESSING_STYLE = 'path' # Static settings -PUBLIC_STATIC_LOCATION = 'static' +PUBLIC_STATIC_LOCATION = 'static-dev' STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' diff --git a/project/settings/production.py b/project/settings/production.py index 997d6526..7ef2dc62 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -4,6 +4,11 @@ from .amazon_s3 import * import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration + +PUBLIC_STATIC_LOCATION = 'static' +STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' +STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False From 842ad47ec04f6f45ac93c5b56e40e1a5d80439a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 14:34:57 +0300 Subject: [PATCH 052/191] Fix test translated --- apps/utils/tests/tests_translated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/utils/tests/tests_translated.py b/apps/utils/tests/tests_translated.py index 557c8b5d..7f6f6594 100644 --- a/apps/utils/tests/tests_translated.py +++ b/apps/utils/tests/tests_translated.py @@ -41,7 +41,7 @@ class TranslateFieldTests(BaseTestCase): self.news_type.save() - self.country_ru = Country.objects.get( + self.country_ru, created = Country.objects.get_or_create( name={"en-GB": "Russian"} ) From 17f67d020ab0436bda47621a190dafc841435a50 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Nov 2019 15:00:53 +0300 Subject: [PATCH 053/191] back carousel api and test --- apps/establishment/serializers/common.py | 2 +- apps/establishment/tests.py | 17 +++-------------- apps/establishment/urls/back.py | 2 ++ apps/establishment/urls/common.py | 2 -- apps/news/serializers.py | 2 +- apps/news/tests.py | 17 +++-------------- apps/news/urls/back.py | 3 +-- apps/news/urls/common.py | 2 -- apps/utils/serializers.py | 4 ++-- apps/utils/views.py | 5 +++++ 10 files changed, 18 insertions(+), 38 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 9419c449..cb102ff1 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -485,7 +485,7 @@ class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer): """Serializer to carousel object w/ model News.""" def validate(self, attrs): - establishment = models.Establishment.objects.filter(slug=self.slug).first() + establishment = models.Establishment.objects.filter(pk=self.pk).first() if not establishment: raise serializers.ValidationError({'detail': _('Object not found.')}) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 6d456a45..c2613d30 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -445,24 +445,13 @@ class EstablishmentWebFavoriteTests(ChildTestCase): class EstablishmentCarouselTests(ChildTestCase): - def test_web_carousel_CR(self): + def test_back_carousel_CR(self): data = { "object_id": self.establishment.id } - response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/carousels/', data=data) + response = self.client.post(f'/api/back/establishments/{self.establishment.id}/carousels/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.delete(f'/api/web/establishments/slug/{self.establishment.slug}/carousels/') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_mobile_carousel_CR(self): - data = { - "object_id": self.establishment.id - } - - response = self.client.post(f'/api/mobile/establishments/slug/{self.establishment.slug}/carousels/', data=data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - response = self.client.delete(f'/api/mobile/establishments/slug/{self.establishment.slug}/carousels/') + response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/carousels/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index a06adc3b..63bb286d 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -9,6 +9,8 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListCreateView.as_view(), name='list'), path('/', views.EstablishmentRUDView.as_view(), name='detail'), + path('/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(), + name='create-destroy-carousels'), path('/schedule//', views.EstablishmentScheduleRUDView.as_view(), name='schedule-rud'), path('/schedule/', views.EstablishmentScheduleCreateView.as_view(), diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 3f46df90..e37c38f8 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -18,6 +18,4 @@ urlpatterns = [ name='rud-comment'), path('slug//favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites'), - path('slug//carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(), - name='create-destroy-carousels') ] diff --git a/apps/news/serializers.py b/apps/news/serializers.py index f58e7c24..cc5087f6 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -275,7 +275,7 @@ class NewsCarouselCreateSerializer(CarouselCreateSerializer): """Serializer to carousel object w/ model News.""" def validate(self, attrs): - news = models.News.objects.filter(slug=self.slug).first() + news = models.News.objects.filter(pk=self.pk).first() if not news: raise serializers.ValidationError({'detail': _('Object not found.')}) diff --git a/apps/news/tests.py b/apps/news/tests.py index a87e457a..1754c15f 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -132,24 +132,13 @@ class NewsTestCase(BaseTestCase): class NewsCarouselTests(BaseTestCase): - def test_web_carousel_CR(self): + def test_back_carousel_CR(self): data = { "object_id": self.test_news.id } - response = self.client.post(f'/api/web/news/slug/{self.test_news.slug}/carousels/', data=data) + response = self.client.post(f'/api/back/news/{self.test_news.id}/carousels/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/carousels/') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_mobile_carousel_CR(self): - data = { - "object_id": self.test_news.id - } - - response = self.client.post(f'/api/mobile/news/slug/{self.test_news.slug}/carousels/', data=data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - response = self.client.delete(f'/api/mobile/news/slug/{self.test_news.slug}/carousels/') + response = self.client.delete(f'/api/back/news/{self.test_news.id}/carousels/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/news/urls/back.py b/apps/news/urls/back.py index 9126b3e9..982e7810 100644 --- a/apps/news/urls/back.py +++ b/apps/news/urls/back.py @@ -13,6 +13,5 @@ urlpatterns = [ name='gallery-list'), path('/gallery//', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), name='gallery-create-destroy'), - path('slug//carousels/', views.NewsCarouselCreateDestroyView.as_view(), - name='create-destroy-carousels'), + path('/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'), ] diff --git a/apps/news/urls/common.py b/apps/news/urls/common.py index e2aae7a1..f5c809de 100644 --- a/apps/news/urls/common.py +++ b/apps/news/urls/common.py @@ -7,6 +7,4 @@ common_urlpatterns = [ path('slug//', views.NewsDetailView.as_view(), name='rud'), path('slug//favorites/', views.NewsFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites'), - path('slug//carousels/', views.NewsCarouselCreateDestroyView.as_view(), - name='create-destroy-carousels'), ] diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 634246b7..f55b69bc 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -115,8 +115,8 @@ class CarouselCreateSerializer(serializers.ModelSerializer): return self.context.get('request') @property - def slug(self): - return self.request.parser_context.get('kwargs').get('slug') + def pk(self): + return self.request.parser_context.get('kwargs').get('pk') class RecursiveFieldSerializer(serializers.Serializer): diff --git a/apps/utils/views.py b/apps/utils/views.py index 8becc9ea..478a3cb2 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -155,6 +155,11 @@ class FavoritesCreateDestroyMixinView(BaseCreateDestroyMixinView): class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): """Carousel Create Destroy mixin.""" + lookup_field = 'id' + + def get_base_object(self): + return get_object_or_404(self._model, id=self.kwargs['pk']) + def get_object(self): """ Returns the object the view is displaying. From 1711057ceed60eb87857d52b149fbb4e6cd43384 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Mon, 25 Nov 2019 12:15:51 +0000 Subject: [PATCH 054/191] Feature/bo establishment employee features --- apps/establishment/serializers/back.py | 3 ++- apps/establishment/urls/back.py | 1 + apps/establishment/views/back.py | 9 +++++++++ apps/utils/decorators.py | 4 +++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 52af0574..dd16e861 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -162,7 +162,7 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer): class EmployeeBackSerializers(serializers.ModelSerializer): """Employee serializers.""" - awards = AwardSerializer(many=True) + awards = AwardSerializer(many=True, read_only=True) class Meta: model = models.Employee @@ -209,6 +209,7 @@ class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer): 'from_date', 'to_date', 'position', + 'status', ] diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 63bb286d..d9b2fbd7 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -54,4 +54,5 @@ urlpatterns = [ path('types//', views.EstablishmentTypeRUDView.as_view(), name='type-rud'), path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'), path('subtypes//', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'), + path('positions/', views.EstablishmentPositionListView.as_view(), name='position-list'), ] diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 312bb171..d3afbf2e 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -356,3 +356,12 @@ class EstablishmentEmployeeDeleteView(generics.DestroyAPIView): object_to_delete = self._get_object_to_delete(establishment_id, employee_id) object_to_delete.delete() return HttpResponse(status=status.HTTP_204_NO_CONTENT) + + +class EstablishmentPositionListView(generics.ListAPIView): + """Establishment positions list view.""" + + pagination_class = None + permission_classes = (permissions.AllowAny, ) + queryset = models.Position.objects.all() + serializer_class = serializers.PositionBackSerializer diff --git a/apps/utils/decorators.py b/apps/utils/decorators.py index c48a26c7..18bed79b 100644 --- a/apps/utils/decorators.py +++ b/apps/utils/decorators.py @@ -1,3 +1,5 @@ +from django.contrib.auth.models import AbstractUser + def with_base_attributes(cls): @@ -8,7 +10,7 @@ def with_base_attributes(cls): if request and hasattr(request, "user"): user = request.user - if user is not None: + if user is not None and isinstance(user, AbstractUser): data.update({'modified_by': user}) if not self.instance: From 08d624db05a226cfb6a1e24761a8907d736c07ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 16:19:38 +0300 Subject: [PATCH 055/191] Fix schedule test and permissions --- apps/establishment/tests.py | 42 +++++++++++++++++++++++++--- apps/utils/permissions.py | 8 ++++-- apps/utils/tests/tests_translated.py | 1 - 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 3534608c..833f9b0a 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -10,6 +10,8 @@ from translation.models import Language from account.models import Role, UserRole from location.models import Country, Address, City, Region from pytz import timezone as py_tz +from main.models import SiteSettings +from timetable.models import Timetable class BaseTestCase(APITestCase): @@ -336,24 +338,56 @@ class MenuTests(ChildTestCase): class EstablishmentShedulerTests(ChildTestCase): - def test_shedule_CRUD(self): + def setUp(self): + super().setUp() + + self.lang, created = Language.objects.get_or_create( + title='Russia', + locale='ru-RU' + ) + + self.country_ru, created = Country.objects.get_or_create( + name={"en-GB": "Russian"} + ) + + self.site_ru, created = SiteSettings.objects.get_or_create( + subdomain='ru' + ) + + role, created = Role.objects.get_or_create( + role=Role.ESTABLISHMENT_MANAGER, + country_id=self.country_ru.id, + site_id=self.site_ru.id + ) + + user_role, created = UserRole.objects.get_or_create( + user=self.user, + role=role, + establishment_id=self.establishment.id + ) + user_role.save() + + def test_schedule_CRUD(self): data = { 'weekday': 1 } + response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) + schedule = response.data - response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/1/') + response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/') self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { 'weekday': 2 } - response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/schedule/1/', data=update_data) + response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/', + data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/schedule/1/') + response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 321d72d3..30055c44 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -304,7 +304,9 @@ class IsEstablishmentManager(IsStandardUser): def has_object_permission(self, request, view, obj): rules = [ - super().has_object_permission(request, view, obj) + # special! + super().has_permission(request, view) + # super().has_object_permission(request, view, obj) ] role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ @@ -315,7 +317,9 @@ class IsEstablishmentManager(IsStandardUser): UserRole.objects.filter(user=request.user, role=role, establishment_id=obj.establishment_id ).exists(), - super().has_object_permission(request, view, obj) + # special! + super().has_permission(request, view) + # super().has_object_permission(request, view, obj) ] return any(rules) diff --git a/apps/utils/tests/tests_translated.py b/apps/utils/tests/tests_translated.py index 7f6f6594..6249ebd1 100644 --- a/apps/utils/tests/tests_translated.py +++ b/apps/utils/tests/tests_translated.py @@ -40,7 +40,6 @@ class TranslateFieldTests(BaseTestCase): self.news_type = NewsType.objects.create(name="Test news type") self.news_type.save() - self.country_ru, created = Country.objects.get_or_create( name={"en-GB": "Russian"} ) From 82d3b152c77e386b4871938fbaee087dc928e620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 25 Nov 2019 16:46:53 +0300 Subject: [PATCH 056/191] Fix test --- apps/establishment/tests.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 833f9b0a..743245d8 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -280,13 +280,13 @@ class PlateTests(ChildTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) menu = Menu.objects.create( - category=json.dumps({"en-GB": "Test category"}), + category=json.dumps({"ru-RU": "Test category"}), establishment=self.establishment ) currency = Currency.objects.create(name="Test currency") data = { - 'name': json.dumps({"en-GB": "Test plate"}), + 'name': json.dumps({"ru-RU": "Test plate"}), 'establishment': self.establishment.id, 'price': 10, 'menu': menu.id, @@ -300,7 +300,7 @@ class PlateTests(ChildTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { - 'name': json.dumps({"en-GB": "Test new plate"}) + 'name': json.dumps({"ru-RU": "Test new plate"}) } response = self.client.patch('/api/back/establishments/plates/1/', data=update_data) @@ -316,7 +316,7 @@ class MenuTests(ChildTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) data = { - 'category': json.dumps({"en-GB": "Test category"}), + 'category': json.dumps({"ru-RU": "Test category"}), 'establishment': self.establishment.id } @@ -327,7 +327,7 @@ class MenuTests(ChildTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { - 'category': json.dumps({"en-GB": "Test new category"}) + 'category': json.dumps({"ru-RU": "Test new category"}) } response = self.client.patch('/api/back/establishments/menus/1/', data=update_data) From 26867bc2bb954a045b0f39260dbcfb701afd2243 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 25 Nov 2019 18:46:47 +0300 Subject: [PATCH 057/191] Unlimited facets for search results (cherry picked from commit 1dac727) --- apps/search_indexes/utils.py | 2 ++ apps/search_indexes/views.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/utils.py b/apps/search_indexes/utils.py index d30e7de3..d4c4f68a 100644 --- a/apps/search_indexes/utils.py +++ b/apps/search_indexes/utils.py @@ -2,6 +2,8 @@ from django_elasticsearch_dsl import fields from utils.models import get_current_locale, get_default_locale +FACET_MAX_RESPONSE = 9999999 # Unlimited + ALL_LOCALES_LIST = [ 'hr-HR', 'ro-RO', diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index e37d19b3..1cf1d3c2 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -8,7 +8,7 @@ from django_elasticsearch_dsl_drf.filter_backends import ( ) from elasticsearch_dsl import TermsFacet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet -from search_indexes import serializers, filters +from search_indexes import serializers, filters, utils from search_indexes.documents import EstablishmentDocument, NewsDocument from search_indexes.documents.product import ProductDocument from utils.pagination import ESDocumentPagination @@ -34,6 +34,9 @@ class NewsDocumentViewSet(BaseDocumentViewSet): 'field': 'tags.id', 'enabled': True, 'facet': TermsFacet, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, }, } @@ -125,11 +128,17 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'field': 'visible_tags.id', 'facet': TermsFacet, 'enabled': True, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, }, 'wine_region_id': { 'field': 'products.wine_region.id', 'facet': TermsFacet, 'enabled': True, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, } } @@ -285,11 +294,17 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'field': 'wine_colors.id', 'enabled': True, 'facet': TermsFacet, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, }, 'wine_region_id': { 'field': 'wine_region.id', 'enabled': True, 'facet': TermsFacet, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, }, } From 62b7befda7bf3fde82de72fe5ba9bdd88aecbccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Tue, 26 Nov 2019 15:45:11 +0300 Subject: [PATCH 058/191] SiteFeatures migrate --- .../management/commands/add_site_features.py | 94 +++++++++++++++++++ apps/main/migrations/0038_feature_old_id.py | 18 ++++ .../migrations/0039_sitefeature_old_id.py | 18 ++++ apps/main/models.py | 2 + 4 files changed, 132 insertions(+) create mode 100644 apps/main/management/commands/add_site_features.py create mode 100644 apps/main/migrations/0038_feature_old_id.py create mode 100644 apps/main/migrations/0039_sitefeature_old_id.py diff --git a/apps/main/management/commands/add_site_features.py b/apps/main/management/commands/add_site_features.py new file mode 100644 index 00000000..9a4f6e1b --- /dev/null +++ b/apps/main/management/commands/add_site_features.py @@ -0,0 +1,94 @@ +from django.core.management.base import BaseCommand +from django.db import connections +from django.utils.text import slugify +from establishment.management.commands.add_position import namedtuplefetchall +from main.models import SiteSettings, Feature, SiteFeature +from location.models import Country +from tqdm import tqdm + + +class Command(BaseCommand): + help = '''Add site_features for old db to new db. + Run after command add_site_settings!''' + + def site_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select s.id, s.country_code_2 + from sites as s + ''') + return namedtuplefetchall(cursor) + + def update_site_old_id(self): + for a in tqdm(self.site_sql(), desc='Update old_id site: '): + country = Country.objects.filter(code=a.country_code_2).first() + SiteSettings.objects.filter(country=country, old_id__isnull=True)\ + .update(old_id=a.id) + self.stdout.write(self.style.WARNING(f'Updated old_id site.')) + + def feature_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select f.id, slug + from features as f + ''') + return namedtuplefetchall(cursor) + + def add_feature(self): + objects = [] + for a in tqdm(self.feature_sql(), desc='Add feature: '): + features = Feature.objects.filter(slug=slugify(a.slug)).update(old_id=a.id) + if features == 0: + objects.append( + Feature(slug=slugify(a.slug), old_id=a.id) + ) + Feature.objects.bulk_create(objects) + self.stdout.write(self.style.WARNING(f'Created feature objects.')) + + def site_features_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select s.id as old_site_feature, + s.site_id, + case when s.state = 'published' + then True + else False + end as published, + s.feature_id, + c.country_code_2 + from features as f + join site_features s on s.feature_id=f.id + join sites c on c.id = s.site_id + ''') + return namedtuplefetchall(cursor) + + def add_site_features(self): + objects = [] + for a in tqdm(self.site_features_sql(), desc='Add site feature: '): + site = SiteSettings.objects.get(old_id=a.site_id, + subdomain=a.country_code_2) + feature = Feature.objects.get(old_id=a.feature_id) + + site_features = SiteFeature.objects\ + .filter(site_settings=site, + feature=feature + ).update(old_id=a.old_site_feature, published=a.published) + + if site_features == 0: + objects.append( + SiteFeature(site_settings=site, + feature=feature, + published=a.published, + main=False, + old_id=a.old_site_feature + ) + ) + SiteFeature.objects.bulk_create(objects) + self.stdout.write(self.style.WARNING(f'Site feature add objects.')) + + def handle(self, *args, **kwargs): + self.update_site_old_id() + self.add_feature() + self.add_site_features() + + diff --git a/apps/main/migrations/0038_feature_old_id.py b/apps/main/migrations/0038_feature_old_id.py new file mode 100644 index 00000000..a4a05c06 --- /dev/null +++ b/apps/main/migrations/0038_feature_old_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-26 11:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0037_sitesettings_old_id'), + ] + + operations = [ + migrations.AddField( + model_name='feature', + name='old_id', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/apps/main/migrations/0039_sitefeature_old_id.py b/apps/main/migrations/0039_sitefeature_old_id.py new file mode 100644 index 00000000..10ed25e4 --- /dev/null +++ b/apps/main/migrations/0039_sitefeature_old_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-26 12:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0038_feature_old_id'), + ] + + operations = [ + migrations.AddField( + model_name='sitefeature', + name='old_id', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index fcce88ab..f9c1225f 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -107,6 +107,7 @@ class Feature(ProjectBaseMixin, PlatformMixin): priority = models.IntegerField(unique=True, null=True, default=None) route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None) site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature') + old_id = models.IntegerField(null=True, blank=True) class Meta: """Meta class.""" @@ -138,6 +139,7 @@ class SiteFeature(ProjectBaseMixin): published = models.BooleanField(default=False, verbose_name=_('Published')) main = models.BooleanField(default=False, verbose_name=_('Main')) nested = models.ManyToManyField('self', symmetrical=False) + old_id = models.IntegerField(null=True, blank=True) objects = SiteFeatureQuerySet.as_manager() From 8d4de4649666c7b82fd13121dacecd1133e59dd0 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 26 Nov 2019 17:02:26 +0300 Subject: [PATCH 059/191] commenting out GuideElements and GuideSections from transfer models --- apps/transfer/models.py | 73 +++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/apps/transfer/models.py b/apps/transfer/models.py index a8190879..37f9217a 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -369,44 +369,45 @@ class GuideFilters(MigrateMixin): db_table = 'guide_filters' -# -# class GuideSections(MigrateMixin): -# using = 'legacy' -# -# type = models.CharField(max_length=255) -# key_name = models.CharField(max_length=255, blank=True, null=True) -# value_name = models.CharField(max_length=255, blank=True, null=True) -# right = models.IntegerField(blank=True, null=True) -# created_at = models.DateTimeField() -# updated_at = models.DateTimeField() +class GuideSections(MigrateMixin): + using = 'legacy' + + type = models.CharField(max_length=255) + key_name = models.CharField(max_length=255, blank=True, null=True) + value_name = models.CharField(max_length=255, blank=True, null=True) + right = models.IntegerField(blank=True, null=True) + created_at = models.DateTimeField() + updated_at = models.DateTimeField() + + class Meta: + managed = False + db_table = 'guide_elements' -# class GuideElements(MigrateMixin): -# using = 'legacy' -# -# type = models.CharField(max_length=255) -# establishment = models.ForeignKey(Establishments, models.DO_NOTHING, blank=True, null=True) -# review = models.ForeignKey('Reviews', models.DO_NOTHING, blank=True, null=True) -# review_text = models.ForeignKey('ReviewTexts', models.DO_NOTHING, blank=True, null=True) -# wine_region = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True) -# wine = models.ForeignKey('Products', models.DO_NOTHING, blank=True, null=True) -# color = models.CharField(max_length=255, blank=True, null=True) -# order_number = models.IntegerField(blank=True, null=True) -# guide_ad = models.ForeignKey(GuideAds, models.DO_NOTHING, blank=True, null=True) -# city = models.ForeignKey(Cities, models.DO_NOTHING, blank=True, null=True) -# section = models.ForeignKey('GuideSections', models.DO_NOTHING, blank=True, null=True) -# guide_id = models.IntegerField(blank=True, null=True) -# parent_id = models.IntegerField(blank=True, null=True) -# lft = models.IntegerField() -# rgt = models.IntegerField() -# depth = models.IntegerField() -# children_count = models.IntegerField() -# created_at = models.DateTimeField() -# updated_at = models.DateTimeField() -# -# class Meta: -# managed = False -# db_table = 'guide_elements' +class GuideElements(MigrateMixin): + using = 'legacy' + + type = models.CharField(max_length=255) + establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True) + review = models.ForeignKey('Reviews', models.DO_NOTHING, blank=True, null=True) + review_text = models.ForeignKey('ReviewTexts', models.DO_NOTHING, blank=True, null=True) + wine_region = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True) + wine = models.ForeignKey('Products', models.DO_NOTHING, blank=True, null=True) + color = models.CharField(max_length=255, blank=True, null=True) + order_number = models.IntegerField(blank=True, null=True) + guide_ad = models.ForeignKey(GuideAds, models.DO_NOTHING, blank=True, null=True) + city = models.ForeignKey(Cities, models.DO_NOTHING, blank=True, null=True) + section = models.ForeignKey('GuideSections', models.DO_NOTHING, blank=True, null=True) + guide_id = models.IntegerField(blank=True, null=True) + parent = models.ForeignKey('self', models.DO_NOTHING, blank=True, null=True) + lft = models.IntegerField() + rgt = models.IntegerField() + depth = models.IntegerField() + children_count = models.IntegerField() + + class Meta: + managed = False + db_table = 'guide_elements' class Establishments(MigrateMixin): From 7bc93d55d78e09c29e68edfda692c5c694d0fe17 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 26 Nov 2019 18:38:49 +0300 Subject: [PATCH 060/191] Fix wine colors facet --- apps/search_indexes/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 1cf1d3c2..a40abc6e 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -124,8 +124,8 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'facet': TermsFacet, 'enabled': True, }, - 'tag': { - 'field': 'visible_tags.id', + 'wine_colors': { + 'field': 'wine_colors.id', 'facet': TermsFacet, 'enabled': True, 'options': { From 8c83e7d1c688b978bd6ec2cfa45402ab1b44234d Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 26 Nov 2019 18:41:42 +0300 Subject: [PATCH 061/191] Revert "Fix wine colors facet" This reverts commit 7bc93d5 --- apps/search_indexes/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index a40abc6e..1cf1d3c2 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -124,8 +124,8 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'facet': TermsFacet, 'enabled': True, }, - 'wine_colors': { - 'field': 'wine_colors.id', + 'tag': { + 'field': 'visible_tags.id', 'facet': TermsFacet, 'enabled': True, 'options': { From cc5763136bf5049c5d5e5a4d7b126aec02f10470 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 26 Nov 2019 18:50:52 +0300 Subject: [PATCH 062/191] Wine colors establishment facet --- apps/search_indexes/documents/establishment.py | 8 ++++++++ apps/search_indexes/views.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index d611a964..e53b93de 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -71,6 +71,14 @@ class EstablishmentDocument(Document): # 'coordinates': fields.GeoPointField(), 'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES), }), + 'wine_colors': fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True, + ), 'wine_sub_region': fields.ObjectField(properties={ 'id': fields.IntegerField(), 'name': fields.KeywordField(), diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 1cf1d3c2..69efe6ff 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -132,6 +132,14 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'size': utils.FACET_MAX_RESPONSE, }, }, + 'wine_colors': { + 'field': 'products.wine_colors.id', + 'facet': TermsFacet, + 'enabled': True, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, + }, 'wine_region_id': { 'field': 'products.wine_region.id', 'facet': TermsFacet, From f19aaf3f4c300c5fead1757745b0dcb539aed61d Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 26 Nov 2019 18:54:54 +0300 Subject: [PATCH 063/191] Wine colors search --- apps/search_indexes/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 69efe6ff..37ed46fb 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -188,6 +188,13 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): constants.LOOKUP_QUERY_IN, ] }, + 'wine_colors_id': { + 'field': 'products.wine_colors.id', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE, + ], + }, 'wine_region_id': { 'field': 'products.wine_region.id', 'lookups': [ From b6b1d8ab2a7f5d0b7566f5721a58209cb82a905a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Wed, 27 Nov 2019 11:34:20 +0300 Subject: [PATCH 064/191] Fix requirements base.txt --- requirements/base.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index e02e8ac5..94e7ca27 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,20 +2,20 @@ Django[bcrypt]==2.2.7 psycopg2-binary==2.8.3 pytz==2019.1 sqlparse==0.3.0 -requests -django-solo -easy-thumbnails -fcm-django -django-easy-select2 -bootstrap-admin +requests==2.22.0 +django-solo==1.1.3 +easy-thumbnails==2.6 +fcm-django==0.3.2 +django-easy-select2==1.5.6 +bootstrap-admin==0.4.3 drf-yasg==1.16.0 -timezonefinder +timezonefinder==4.1.0 PySocks!=1.5.7,>=1.5.6; djangorestframework==3.9.4 -markdown +Markdown==3.1.1 django-filter==2.1.0 -djangorestframework-xml +djangorestframework-xml==1.4.0 geoip2==2.9.0 pycountry==19.8.18 django-phonenumber-field[phonenumbers]==2.1.0 From ca7d7f0fa8df35d36226fb4a31fdd0142d0c7c33 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 27 Nov 2019 13:57:12 +0300 Subject: [PATCH 065/191] return only wine-color tags --- apps/tag/filters.py | 4 ++-- apps/tag/models.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 7bd22ec2..a7d9da9d 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -43,8 +43,8 @@ class TagCategoryFilterSet(TagsBaseFilterSet): 'product_type', ) def by_product_type(self, queryset, name, value): - # if value == product_models.ProductType.WINE: - # queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False) + if value == product_models.ProductType.WINE: + return queryset.wine_tags_category().filter(tags__products__isnull=False) queryset = queryset.by_product_type(value) return queryset diff --git a/apps/tag/models.py b/apps/tag/models.py index a93c4a1f..12517815 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -111,6 +111,9 @@ class TagCategoryQuerySet(models.QuerySet): """Filter by product type index name.""" return self.filter(tags__products__product_type__index_name=index_name) + def wine_tags_category(self): + return self.filter(index_name='wine-color') + def with_tags(self, switcher=True): """Filter by existing tags.""" return self.exclude(tags__isnull=switcher) From 883b214d742ac68119eaae1c9942058a27599b18 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Wed, 27 Nov 2019 12:49:33 +0000 Subject: [PATCH 066/191] Added is_international property to news --- apps/news/models.py | 4 ++++ apps/news/serializers.py | 1 + 2 files changed, 5 insertions(+) diff --git a/apps/news/models.py b/apps/news/models.py index 66dabc90..2aa571c0 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -221,6 +221,10 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi def is_publish(self): return self.state in self.PUBLISHED_STATES + @property + def is_international(self): + return self.INTERNATIONAL_TAG_VALUE in map(lambda tag: tag.value, self.tags.all()) + @property def web_url(self): return reverse('web:news:rud', kwargs={'slug': self.slug}) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index c80ce9a5..86673645 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -201,6 +201,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, 'site_id', 'template', 'template_display', + 'is_international', ) From 9b35da1eca2dc5ba2ab2cab02121e81f3b000c26 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 27 Nov 2019 17:03:27 +0300 Subject: [PATCH 067/191] add artisan subtype --- .../commands/add_artisan_subtype.py | 48 ++++++++ apps/establishment/models.py | 1 - docker-compose.local.yml | 106 ++++++++++++++++++ env | 9 ++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 apps/establishment/management/commands/add_artisan_subtype.py create mode 100644 docker-compose.local.yml create mode 100644 env diff --git a/apps/establishment/management/commands/add_artisan_subtype.py b/apps/establishment/management/commands/add_artisan_subtype.py new file mode 100644 index 00000000..8616f2ab --- /dev/null +++ b/apps/establishment/management/commands/add_artisan_subtype.py @@ -0,0 +1,48 @@ +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from establishment.models import Establishment, EstablishmentSubType, EstablishmentType +from transfer.models import Metadata + + +class Command(BaseCommand): + help = 'Add subtype for establishment artisan' + + def handle(self, *args, **options): + artisans = Establishment.objects.artisans().filter( + old_id__isnull=False, + ).prefetch_related('tags') + + old_tags = Metadata.objects.filter( + establishment__in=list(artisans.values_list('old_id', flat=True)), + key='shop_category', + ) + + tags = [] + for tag in tqdm(old_tags): + tags.append(tag.value) + subtypes = set(tags) + + es_type, _ = EstablishmentType.objects.get_or_create( + index_name='artisan', + defaults={ + 'index_name': 'artisan', + 'name': {'en-GB': 'artisan'}, + } + ) + for artisan in tqdm(artisans): + artisan_tags = artisan.tags.all() + for t in artisan_tags: + if t.value in subtypes: + tag = 'coffee_shop' if t.value == 'coffe_shop' else t.value + subtype, _ = EstablishmentSubType.objects.get_or_create( + index_name=tag, + defaults={ + 'index_name': tag, + 'name': {'en-GB': tag}, + } + ) + artisan.tags.add(subtype) + artisan.save() + + self.stdout.write(self.style.WARNING(f'Artisans subtype updated.')) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index cb490aa4..c5533d52 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -125,7 +125,6 @@ class EstablishmentQuerySet(models.QuerySet): 'menu_set__plate_set__currency', 'currency'). \ prefetch_actual_employees() - def with_type_related(self): return self.prefetch_related('establishment_subtypes') diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 00000000..1a2d9236 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,106 @@ +version: '3.5' +services: + + # Legacy MySQL DB + mysql_db: + image: mysql:5.7 + ports: + - "3306:3306" + environment: + MYSQL_DATABASE: dev + MYSQL_USER: dev + MYSQL_PASSWORD: octosecret123 + MYSQL_ROOT_PASSWORD: rootPassword + volumes: + - gm-mysql_db:/var/lib/mysql + + + # PostgreSQL database + db: + build: + context: ./_dockerfiles/db + dockerfile: Dockerfile + hostname: db + env_file: + - env + ports: + - "5436:5432" + volumes: + - gm-db:/var/lib/postgresql/data/ + - ./local_files/dump_alex.sql:/dump_alex.sql +# - ./local_files/docker-entrypoint.sh:/docker-entrypoint-initdb.d/docker-entrypoint.sh + + + elasticsearch: + image: elasticsearch:7.3.1 + volumes: + - gm-esdata:/usr/share/elasticsearch/data + hostname: elasticsearch + ports: + - 9200:9200 + - 9300:9300 + environment: + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - discovery.type=single-node + - xpack.security.enabled=false + + + # Redis + redis: + image: redis:alpine + + + # Celery + worker: + build: . + command: ./run_celery.sh + env_file: + - env + volumes: + - .:/code + links: + - db + - redis + + + worker_beat: + build: . + command: ./run_celery_beat.sh + env_file: + - env + volumes: + - .:/code + links: + - db + - redis + + + # App: G&M + gm_app: + build: . + command: python manage.py runserver 0.0.0.0:8000 + env_file: + - env + depends_on: + - mysql_db + - db + - redis + - worker + - worker_beat + - elasticsearch + volumes: + - .:/code + - gm-media:/media-data + ports: + - "8000:8000" + + +volumes: + gm-mysql_db: + name: gm-mysql_db + gm-db: + name: gm-db + gm-media: + name: gm-media + gm-esdata: + diff --git a/env b/env new file mode 100644 index 00000000..67e9f44f --- /dev/null +++ b/env @@ -0,0 +1,9 @@ +SETTINGS_CONFIGURATION=local +DB_NAME=gm +DB_USERNAME=gm +DB_HOSTNAME=db +DB_PORT=5432 +DB_PASSWORD=gm +POSTGRES_USER=gm +POSTGRES_PASSWORD=gm +POSTGRES_DB=gm \ No newline at end of file From c2e111f75b0e72e6ed398009c8c452f69720446a Mon Sep 17 00:00:00 2001 From: littlewolf Date: Wed, 27 Nov 2019 18:12:22 +0300 Subject: [PATCH 068/191] Fix queryset --- apps/partner/views/common.py | 2 +- docker-compose.mysql.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/partner/views/common.py b/apps/partner/views/common.py index a5d0338c..3c45fb6b 100644 --- a/apps/partner/views/common.py +++ b/apps/partner/views/common.py @@ -8,7 +8,7 @@ from partner.serializers import common as serializers # Mixins class PartnerViewMixin(generics.GenericAPIView): """View mixin for Partner views""" - queryset = models.Partner.objects.all() + queryset = models.Partner.objects.distinct("name") # Views diff --git a/docker-compose.mysql.yml b/docker-compose.mysql.yml index bd81ecb2..106cabb3 100644 --- a/docker-compose.mysql.yml +++ b/docker-compose.mysql.yml @@ -13,6 +13,9 @@ services: MYSQL_ROOT_PASSWORD: rootPassword volumes: - gm-mysql_db:/var/lib/mysql + - .:/code + + # PostgreSQL database From 78d414533686da4a31b1410dbf926ea5c90d90dc Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 Nov 2019 09:13:38 +0300 Subject: [PATCH 069/191] artisan subtype --- apps/establishment/management/commands/add_artisan_subtype.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/establishment/management/commands/add_artisan_subtype.py b/apps/establishment/management/commands/add_artisan_subtype.py index 8616f2ab..ac93d81f 100644 --- a/apps/establishment/management/commands/add_artisan_subtype.py +++ b/apps/establishment/management/commands/add_artisan_subtype.py @@ -40,9 +40,10 @@ class Command(BaseCommand): defaults={ 'index_name': tag, 'name': {'en-GB': tag}, + 'establishment_type': es_type, } ) - artisan.tags.add(subtype) + artisan.establishment_subtypes.add(subtype) artisan.save() self.stdout.write(self.style.WARNING(f'Artisans subtype updated.')) From 749c4a637bf5f914da0de9bd80a9dce45ef73a82 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 Nov 2019 09:19:53 +0300 Subject: [PATCH 070/191] artisan subtype fix --- .../commands/add_artisan_subtype.py | 2 +- docker-compose.local.yml | 106 ------------------ env | 9 -- 3 files changed, 1 insertion(+), 116 deletions(-) delete mode 100644 docker-compose.local.yml delete mode 100644 env diff --git a/apps/establishment/management/commands/add_artisan_subtype.py b/apps/establishment/management/commands/add_artisan_subtype.py index ac93d81f..f7283f4f 100644 --- a/apps/establishment/management/commands/add_artisan_subtype.py +++ b/apps/establishment/management/commands/add_artisan_subtype.py @@ -39,7 +39,7 @@ class Command(BaseCommand): index_name=tag, defaults={ 'index_name': tag, - 'name': {'en-GB': tag}, + 'name': {'en-GB': ' '.join(tag.split('_')).capitalize()}, 'establishment_type': es_type, } ) diff --git a/docker-compose.local.yml b/docker-compose.local.yml deleted file mode 100644 index 1a2d9236..00000000 --- a/docker-compose.local.yml +++ /dev/null @@ -1,106 +0,0 @@ -version: '3.5' -services: - - # Legacy MySQL DB - mysql_db: - image: mysql:5.7 - ports: - - "3306:3306" - environment: - MYSQL_DATABASE: dev - MYSQL_USER: dev - MYSQL_PASSWORD: octosecret123 - MYSQL_ROOT_PASSWORD: rootPassword - volumes: - - gm-mysql_db:/var/lib/mysql - - - # PostgreSQL database - db: - build: - context: ./_dockerfiles/db - dockerfile: Dockerfile - hostname: db - env_file: - - env - ports: - - "5436:5432" - volumes: - - gm-db:/var/lib/postgresql/data/ - - ./local_files/dump_alex.sql:/dump_alex.sql -# - ./local_files/docker-entrypoint.sh:/docker-entrypoint-initdb.d/docker-entrypoint.sh - - - elasticsearch: - image: elasticsearch:7.3.1 - volumes: - - gm-esdata:/usr/share/elasticsearch/data - hostname: elasticsearch - ports: - - 9200:9200 - - 9300:9300 - environment: - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - - discovery.type=single-node - - xpack.security.enabled=false - - - # Redis - redis: - image: redis:alpine - - - # Celery - worker: - build: . - command: ./run_celery.sh - env_file: - - env - volumes: - - .:/code - links: - - db - - redis - - - worker_beat: - build: . - command: ./run_celery_beat.sh - env_file: - - env - volumes: - - .:/code - links: - - db - - redis - - - # App: G&M - gm_app: - build: . - command: python manage.py runserver 0.0.0.0:8000 - env_file: - - env - depends_on: - - mysql_db - - db - - redis - - worker - - worker_beat - - elasticsearch - volumes: - - .:/code - - gm-media:/media-data - ports: - - "8000:8000" - - -volumes: - gm-mysql_db: - name: gm-mysql_db - gm-db: - name: gm-db - gm-media: - name: gm-media - gm-esdata: - diff --git a/env b/env deleted file mode 100644 index 67e9f44f..00000000 --- a/env +++ /dev/null @@ -1,9 +0,0 @@ -SETTINGS_CONFIGURATION=local -DB_NAME=gm -DB_USERNAME=gm -DB_HOSTNAME=db -DB_PORT=5432 -DB_PASSWORD=gm -POSTGRES_USER=gm -POSTGRES_PASSWORD=gm -POSTGRES_DB=gm \ No newline at end of file From 19190eb1e0b26c663e3ad3a7603018982738c9a6 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 14:00:33 +0300 Subject: [PATCH 071/191] Fix hardcoded tags (cherry picked from commit 4e5291d) --- apps/tag/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index a7d9da9d..5e2b31a7 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -86,7 +86,7 @@ class TagsFilterSet(TagsBaseFilterSet): if self.NEWS in value: queryset = queryset.for_news().filter(value__in=settings.NEWS_CHOSEN_TAGS).distinct('value') if self.ESTABLISHMENT in value: - queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct( + queryset = queryset.for_establishments().filter(category__value_type='list').filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct( 'value') return queryset From 4bc3af29a048e1963dca74e281b3935cf9a3f3b5 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 14:24:39 +0300 Subject: [PATCH 072/191] Fix booking --- apps/booking/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/booking/views.py b/apps/booking/views.py index 73f6f55e..c2a143a6 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -84,8 +84,8 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): service_response = self._preprocess_guestonline_response(service.response) \ if establishment.guestonline_id is not None \ - else service.response - response.update({'details': service_response} if service and service.response else {}) + else service.response if service else None + response.update({'details': service_response}) return Response(data=response, status=200) From f225336121d6087684fe14f143f422e6be2980d4 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 14:45:52 +0300 Subject: [PATCH 073/191] Remove facets from mobile --- apps/search_indexes/urls.py | 4 +++- apps/search_indexes/views.py | 27 ++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/urls.py b/apps/search_indexes/urls.py index 70e21369..902ccfeb 100644 --- a/apps/search_indexes/urls.py +++ b/apps/search_indexes/urls.py @@ -6,9 +6,11 @@ from search_indexes import views router = routers.SimpleRouter() # router.register(r'news', views.NewsDocumentViewSet, basename='news') # temporarily disabled router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment') -router.register(r'mobile/establishments', views.EstablishmentDocumentViewSet, basename='establishment-mobile') +router.register(r'mobile/establishments', views.MobileEstablishmentDocumentViewSet, basename='establishment-mobile') router.register(r'news', views.NewsDocumentViewSet, basename='news') +router.register(r'mobile/news', views.MobileNewsDocumentViewSet, basename='news-mobile') router.register(r'products', views.ProductDocumentViewSet, basename='product') +router.register(r'mobile/products', views.MobileProductDocumentViewSet, basename='product-mobile') urlpatterns = router.urls diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 37ed46fb..e352f9be 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -78,6 +78,14 @@ class NewsDocumentViewSet(BaseDocumentViewSet): } +class MobileNewsDocumentViewSet(NewsDocumentViewSet): + + filter_backends = [ + filters.CustomSearchFilterBackend, + FilteringFilterBackend, + ] + + class EstablishmentDocumentViewSet(BaseDocumentViewSet): """Establishment document ViewSet.""" @@ -278,6 +286,15 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): } +class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet): + + filter_backends = [ + FilteringFilterBackend, + filters.CustomSearchFilterBackend, + GeoSpatialFilteringFilterBackend, + ] + + class ProductDocumentViewSet(BaseDocumentViewSet): """Product document ViewSet.""" @@ -380,4 +397,12 @@ class ProductDocumentViewSet(BaseDocumentViewSet): constants.LOOKUP_QUERY_EXCLUDE, ], }, - } \ No newline at end of file + } + + +class MobileProductDocumentViewSet(ProductDocumentViewSet): + + filter_backends = [ + FilteringFilterBackend, + filters.CustomSearchFilterBackend, + ] From 03b7ae678ef112e09f2e82a8ed68f3437dfaf7dd Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 14:59:41 +0300 Subject: [PATCH 074/191] All facets are global now --- apps/search_indexes/views.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index e352f9be..7f8520d7 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -34,6 +34,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): 'field': 'tags.id', 'enabled': True, 'facet': TermsFacet, + 'global': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, @@ -111,31 +112,37 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'field': 'works_at_weekday', 'facet': TermsFacet, 'enabled': True, + 'global': True, }, 'toque_number': { 'field': 'toque_number', 'enabled': True, 'facet': TermsFacet, + 'global': True, }, 'works_noon': { 'field': 'works_noon', 'facet': TermsFacet, 'enabled': True, + 'global': True, }, 'works_evening': { 'field': 'works_evening', 'facet': TermsFacet, 'enabled': True, + 'global': True, }, 'works_now': { 'field': 'works_now', 'facet': TermsFacet, 'enabled': True, + 'global': True, }, 'tag': { 'field': 'visible_tags.id', 'facet': TermsFacet, 'enabled': True, + 'global': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, @@ -144,6 +151,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'field': 'products.wine_colors.id', 'facet': TermsFacet, 'enabled': True, + 'global': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, @@ -152,6 +160,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'field': 'products.wine_region.id', 'facet': TermsFacet, 'enabled': True, + 'global': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, @@ -325,6 +334,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'tag': { 'field': 'wine_colors.id', 'enabled': True, + 'global': True, 'facet': TermsFacet, 'options': { 'size': utils.FACET_MAX_RESPONSE, @@ -333,6 +343,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'wine_region_id': { 'field': 'wine_region.id', 'enabled': True, + 'global': True, 'facet': TermsFacet, 'options': { 'size': utils.FACET_MAX_RESPONSE, From a8ff626179fd89e60a9dade65528e1307efaf8ab Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 Nov 2019 15:10:40 +0300 Subject: [PATCH 075/191] ReviewBackSerializer --- apps/review/serializers/back.py | 20 +++++++++++++++++++- apps/review/serializers/common.py | 2 ++ apps/review/views/back.py | 5 +++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/review/serializers/back.py b/apps/review/serializers/back.py index c72cb205..674e1edc 100644 --- a/apps/review/serializers/back.py +++ b/apps/review/serializers/back.py @@ -1,3 +1,21 @@ """Review app back serializers.""" -from review import models from rest_framework import serializers + +from review import models + + +class ReviewBackSerializer(serializers.ModelSerializer): + class Meta: + model = models.Review + fields = ( + 'id', + 'reviewer', + 'text', + 'status', + # 'child', + 'published_at', + 'vintage', + # 'country', + 'content_type', + 'object_id', + ) diff --git a/apps/review/serializers/common.py b/apps/review/serializers/common.py index da7b624b..b1025d34 100644 --- a/apps/review/serializers/common.py +++ b/apps/review/serializers/common.py @@ -33,6 +33,7 @@ class ReviewShortSerializer(ReviewBaseSerializer): class InquiriesBaseSerializer(serializers.ModelSerializer): """Serializer for model Inquiries.""" + class Meta: model = Inquiries fields = ( @@ -56,6 +57,7 @@ class InquiriesBaseSerializer(serializers.ModelSerializer): class GridItemsBaseSerializer(serializers.ModelSerializer): """Serializer for model GridItems.""" + class Meta: model = GridItems fields = ( diff --git a/apps/review/views/back.py b/apps/review/views/back.py index 27f9af0d..511c91f9 100644 --- a/apps/review/views/back.py +++ b/apps/review/views/back.py @@ -4,11 +4,12 @@ from review import filters from review import models from review import serializers from utils.permissions import IsReviewerManager, IsRestaurantReviewer +from review.serializers.back import ReviewBackSerializer class ReviewLstView(generics.ListCreateAPIView): """Comment list create view.""" - serializer_class = serializers.ReviewBaseSerializer + serializer_class = ReviewBackSerializer queryset = models.Review.objects.all() permission_classes = [permissions.IsAuthenticatedOrReadOnly, ] filterset_class = filters.ReviewFilter @@ -16,7 +17,7 @@ class ReviewLstView(generics.ListCreateAPIView): class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView): """Comment RUD view.""" - serializer_class = serializers.ReviewBaseSerializer + serializer_class = ReviewBackSerializer queryset = models.Review.objects.all() permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer] lookup_field = 'id' From 5a3093be5193b79ad120e1e6eeb249491f7119ec Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 Nov 2019 15:19:13 +0300 Subject: [PATCH 076/191] content type get api --- apps/main/serializers.py | 9 +++++++++ apps/main/urls/back.py | 1 + apps/main/views/back.py | 17 +++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 572aff31..256333b4 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -1,4 +1,5 @@ """Main app serializers.""" +from django.contrib.contenttypes.models import ContentType from rest_framework import serializers from location.serializers import CountrySerializer @@ -216,3 +217,11 @@ class PageTypeBaseSerializer(serializers.ModelSerializer): 'id', 'name', ] + + +class ContentTypeBackSerializer(serializers.ModelSerializer): + """Serializer fro model ContentType.""" + + class Meta: + model = ContentType + fields = '__all__' diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index d92bddf8..8424d236 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -8,4 +8,5 @@ app_name = 'main' urlpatterns = [ path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards//', views.AwardRUDView.as_view(), name='awards-rud'), + path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'), ] diff --git a/apps/main/views/back.py b/apps/main/views/back.py index bbbfad53..5d82d88a 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -1,3 +1,5 @@ +from django.contrib.contenttypes.models import ContentType +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, permissions from main import serializers @@ -19,3 +21,18 @@ class AwardRUDView(generics.RetrieveUpdateDestroyAPIView): serializer_class = serializers.BackAwardSerializer permission_classes = (permissions.IsAdminUser,) lookup_field = 'id' + + +class ContentTypeView(generics.ListAPIView): + """ContentType list view""" + queryset = ContentType.objects.all() + serializer_class = serializers.ContentTypeBackSerializer + permission_classes = (permissions.IsAdminUser,) + filter_backends = (DjangoFilterBackend, ) + ordering_fields = '__all__' + lookup_field = 'id' + filterset_fields = ( + 'id', + 'model', + 'app_label', + ) From c81dbf2d8eb6fab6e3f0e01d08eca946182016ff Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 Nov 2019 15:29:42 +0300 Subject: [PATCH 077/191] main back test --- apps/main/tests/tests_back.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/main/tests/tests_back.py b/apps/main/tests/tests_back.py index e09c7b4b..781eafab 100644 --- a/apps/main/tests/tests_back.py +++ b/apps/main/tests/tests_back.py @@ -9,7 +9,7 @@ from location.models import Country from main.models import Award, AwardType -class AwardTestCase(APITestCase): +class BaseTestCase(APITestCase): def setUp(self): self.user = User.objects.create_user( @@ -25,6 +25,12 @@ class AwardTestCase(APITestCase): {'access_token': tokens.get('access_token'), 'refresh_token': tokens.get('refresh_token')}) + +class AwardTestCase(BaseTestCase): + + def setUp(self): + super().setUp() + self.country_ru = Country.objects.create( name={'en-GB': 'Russian'}, code='RU', @@ -71,3 +77,13 @@ class AwardTestCase(APITestCase): response = self.client.delete(f'/api/back/main/awards/{self.award.id}/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + +class ContentTypeTestCase(BaseTestCase): + + def setUp(self): + super().setUp() + + def test_content_type_list(self): + response = self.client.get('/api/back/main/content_type/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) From eaeebae72a1655f4c3a0b0d2cd6c909c65190a76 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 28 Nov 2019 15:31:47 +0300 Subject: [PATCH 078/191] in progress --- .../commands/check_guide_dependencies.py | 120 ++++++++ .../migrations/0018_auto_20191127_1047.py | 78 +++++ apps/collection/models.py | 134 ++++++++- apps/collection/transfer_data.py | 37 +++ apps/transfer/management/commands/transfer.py | 2 + apps/transfer/models.py | 8 + apps/transfer/serializers/guide.py | 283 ++++++++++++++++++ requirements/base.txt | 3 + 8 files changed, 652 insertions(+), 13 deletions(-) create mode 100644 apps/collection/management/commands/check_guide_dependencies.py create mode 100644 apps/collection/migrations/0018_auto_20191127_1047.py create mode 100644 apps/collection/transfer_data.py create mode 100644 apps/transfer/serializers/guide.py diff --git a/apps/collection/management/commands/check_guide_dependencies.py b/apps/collection/management/commands/check_guide_dependencies.py new file mode 100644 index 00000000..1f2afb9d --- /dev/null +++ b/apps/collection/management/commands/check_guide_dependencies.py @@ -0,0 +1,120 @@ +import re +from pprint import pprint + +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from establishment.models import Establishment +from location.models import City +from location.models import WineRegion +from product.models import Product +from review.models import Review +from tag.models import Tag +from transfer.models import GuideElements + + +def decorator(f): + def decorate(self): + print(f'{"-"*20}start {f.__name__}{"-"*20}') + f(self) + print(f'{"-"*20}end {f.__name__}{"-"*20}\n') + return decorate + + +class Command(BaseCommand): + help = """Check guide dependencies.""" + + @decorator + def count_of_guide_relative_dependencies(self): + for field in GuideElements._meta.fields: + if field.name not in ['id', 'lft', 'rgt', 'depth', + 'children_count', 'parent', 'order_number']: + filters = {f'{field.name}__isnull': False, } + qs = GuideElements.objects.filter(**filters).values_list(field.name, flat=True) + print(f"COUNT OF {field.name}'s: {len(set(qs))}") + + @decorator + def check_regions(self): + wine_region_old_ids = set(GuideElements.objects.filter(wine_region_id__isnull=False) + .values_list('wine_region_id', flat=True)) + not_existed_wine_regions = [] + for old_id in tqdm(wine_region_old_ids): + if not WineRegion.objects.filter(old_id=old_id).exists(): + not_existed_wine_regions.append(old_id) + print(f'NOT EXISTED WINE REGIONS: {len(not_existed_wine_regions)}') + pprint(f'{not_existed_wine_regions}') + + @decorator + def check_establishments(self): + establishment_old_ids = set(GuideElements.objects.filter(establishment_id__isnull=False) + .values_list('establishment_id', flat=True)) + not_existed_establishments = [] + for old_id in tqdm(establishment_old_ids): + if not Establishment.objects.filter(old_id=old_id).exists(): + not_existed_establishments.append(old_id) + print(f'NOT EXISTED ESTABLISHMENTS: {len(not_existed_establishments)}') + pprint(f'{not_existed_establishments}') + + @decorator + def check_reviews(self): + review_old_ids = set(GuideElements.objects.filter(review_id__isnull=False) + .values_list('review_id', flat=True)) + not_existed_reviews = [] + for old_id in tqdm(review_old_ids): + if not Review.objects.filter(old_id=old_id).exists(): + not_existed_reviews.append(old_id) + print(f'NOT EXISTED REVIEWS: {len(not_existed_reviews)}') + pprint(f'{not_existed_reviews}') + + @decorator + def check_wines(self): + wine_old_ids = set(GuideElements.objects.filter(wine_id__isnull=False) + .values_list('wine_id', flat=True)) + not_existed_wines = [] + for old_id in tqdm(wine_old_ids): + if not Product.objects.filter(old_id=old_id).exists(): + not_existed_wines.append(old_id) + print(f'NOT EXISTED WINES: {len(not_existed_wines)}') + pprint(f'{not_existed_wines}') + + @decorator + def check_wine_color(self): + raw_wine_color_nodes = set(GuideElements.objects.exclude(color__iexact='') + .filter(color__isnull=False) + .values_list('color', flat=True)) + raw_wine_colors = [i[:-11] for i in raw_wine_color_nodes] + raw_wine_color_index_names = [] + re_exp = '[A-Z][^A-Z]*' + for raw_wine_color in tqdm(raw_wine_colors): + result = re.findall(re_exp, rf'{raw_wine_color}') + if result and len(result) >= 2: + wine_color = '-'.join(result) + else: + wine_color = result[0] + raw_wine_color_index_names.append(wine_color.lower()) + not_existed_wine_colors = [] + for index_name in raw_wine_color_index_names: + if not Tag.objects.filter(value=index_name).exists(): + not_existed_wine_colors.append(index_name) + print(f'NOT EXISTED WINE COLOR: {len(not_existed_wine_colors)}') + pprint(f'{not_existed_wine_colors}') + + @decorator + def check_cities(self): + city_old_ids = set(GuideElements.objects.filter(city_id__isnull=False) + .values_list('city_id', flat=True)) + not_existed_cities = [] + for old_id in tqdm(city_old_ids): + if not City.objects.filter(old_id=old_id).exists(): + not_existed_cities.append(old_id) + print(f'NOT EXISTED CITIES: {len(not_existed_cities)}') + pprint(f'{not_existed_cities}') + + def handle(self, *args, **kwargs): + self.count_of_guide_relative_dependencies() + self.check_regions() + self.check_establishments() + self.check_reviews() + self.check_wines() + self.check_wine_color() + self.check_cities() diff --git a/apps/collection/migrations/0018_auto_20191127_1047.py b/apps/collection/migrations/0018_auto_20191127_1047.py new file mode 100644 index 00000000..c6f54ee2 --- /dev/null +++ b/apps/collection/migrations/0018_auto_20191127_1047.py @@ -0,0 +1,78 @@ +# Generated by Django 2.2.7 on 2019-11-27 10:47 + +import django.contrib.postgres.fields.jsonb +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0039_sitefeature_old_id'), + ('collection', '0017_collection_old_id'), + ] + + operations = [ + migrations.CreateModel( + name='GuideType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.SlugField(max_length=255, unique=True, verbose_name='code')), + ], + options={ + 'verbose_name': 'guide type', + 'verbose_name_plural': 'guide types', + }, + ), + migrations.RemoveField( + model_name='guide', + name='advertorials', + ), + migrations.RemoveField( + model_name='guide', + name='collection', + ), + migrations.RemoveField( + model_name='guide', + name='parent', + ), + migrations.AddField( + model_name='guide', + name='old_id', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='guide', + name='site', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site settings'), + ), + migrations.AddField( + model_name='guide', + name='slug', + field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='slug'), + ), + migrations.AddField( + model_name='guide', + name='state', + field=models.PositiveSmallIntegerField(choices=[(0, 'built'), (1, 'waiting'), (2, 'removing'), (3, 'building')], default=1, verbose_name='state'), + ), + migrations.AddField( + model_name='guide', + name='vintage', + field=models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(1900), django.core.validators.MaxValueValidator(2100)], verbose_name='guide vintage year'), + ), + migrations.AddField( + model_name='guide', + name='guide_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='collection.GuideType', verbose_name='type'), + ), + migrations.AlterField( + model_name='guide', + name='start', + field=models.DateTimeField(null=True, verbose_name='start'), + ), + ] diff --git a/apps/collection/models.py b/apps/collection/models.py index d98f8a59..af8b2a33 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -1,6 +1,8 @@ from django.contrib.contenttypes.fields import ContentType from django.contrib.postgres.fields import JSONField +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +import re from django.utils.translation import gettext_lazy as _ from utils.models import ProjectBaseMixin, URLImageMixin @@ -85,26 +87,64 @@ class Collection(ProjectBaseMixin, CollectionDateMixin, verbose_name_plural = _('collections') +class GuideTypeQuerySet(models.QuerySet): + """QuerySet for model GuideType.""" + + +class GuideType(ProjectBaseMixin): + """GuideType model.""" + + name = models.SlugField(max_length=255, unique=True, + verbose_name=_('code')) + + objects = GuideTypeQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('guide type') + verbose_name_plural = _('guide types') + + def __str__(self): + """Overridden str dunder method.""" + return self.name + + class GuideQuerySet(models.QuerySet): """QuerySet for Guide.""" - def by_collection_id(self, collection_id): - """Filter by collection id""" - return self.filter(collection=collection_id) - class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin): """Guide model.""" - parent = models.ForeignKey( - 'self', verbose_name=_('parent'), on_delete=models.CASCADE, - null=True, blank=True, default=None + BUILT = 0 + WAITING = 1 + REMOVING = 2 + BUILDING = 3 + + STATE_CHOICES = ( + (BUILT, 'built'), + (WAITING, 'waiting'), + (REMOVING, 'removing'), + (BUILDING, 'building'), + ) - advertorials = JSONField( - _('advertorials'), null=True, blank=True, - default=None, help_text='{"key":"value"}') - collection = models.ForeignKey(Collection, on_delete=models.CASCADE, - null=True, blank=True, default=None, - verbose_name=_('collection')) + + start = models.DateTimeField(null=True, + verbose_name=_('start')) + vintage = models.IntegerField(validators=[MinValueValidator(1900), + MaxValueValidator(2100)], + null=True, + verbose_name=_('guide vintage year')) + slug = models.SlugField(max_length=255, unique=True, null=True, + verbose_name=_('slug')) + guide_type = models.ForeignKey('GuideType', on_delete=models.PROTECT, + null=True, + verbose_name=_('type')) + site = models.ForeignKey('main.SiteSettings', on_delete=models.SET_NULL, + null=True, + verbose_name=_('site settings')) + state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, + verbose_name=_('state')) + old_id = models.IntegerField(blank=True, null=True) objects = GuideQuerySet.as_manager() @@ -116,3 +156,71 @@ class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin): def __str__(self): """String method.""" return f'{self.name}' + + +class AdvertorialQuerySet(models.QuerySet): + """QuerySet for model Advertorial.""" + + +class Advertorial(ProjectBaseMixin): + """Guide advertorial model.""" + number_of_pages = models.PositiveIntegerField( + verbose_name=_('number of pages'), + help_text=_('the total number of reserved pages')) + right_pages = models.PositiveIntegerField( + verbose_name=_('number of right pages'), + help_text=_('the number of right pages (which are part of total number).')) + old_id = models.IntegerField(blank=True, null=True) + + objects = AdvertorialQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('advertorial') + verbose_name_plural = _('advertorials') + + +class GuideFilterQuerySet(models.QuerySet): + """QuerySet for model GuideFilter.""" + + +class GuideFilter(ProjectBaseMixin): + """Guide filter model.""" + establishment_type_json = JSONField(blank=True, null=True, + verbose_name='establishment types') + country_code_json = JSONField(blank=True, null=True, + verbose_name='countries') + region_code_json = JSONField(blank=True, null=True, + verbose_name='regions') + sub_region_code_json = JSONField(blank=True, null=True, + verbose_name='sub regions') + wine_region_json = JSONField(blank=True, null=True, + verbose_name='wine regions') + wine_classification_json = JSONField(blank=True, null=True, + verbose_name='wine classifications') + wine_color_json = JSONField(blank=True, null=True, + verbose_name='wine colors') + wine_type_json = JSONField(blank=True, null=True, + verbose_name='wine types') + with_mark = models.BooleanField(default=True, + verbose_name=_('with mark'), + help_text=_('exclude empty marks?')) + locale_json = JSONField(blank=True, null=True, + verbose_name='locales') + max_mark = models.PositiveSmallIntegerField(verbose_name=_('max mark'), + help_text=_('mark under')) + min_mark = models.PositiveSmallIntegerField(verbose_name=_('min mark'), + help_text=_('mark over')) + review_vintage_json = JSONField(verbose_name='review vintage years') + review_state_json = JSONField(blank=True, null=True, + verbose_name='review states') + guide = models.OneToOneField(Guide, on_delete=models.CASCADE, + verbose_name=_('guide')) + old_id = models.IntegerField(blank=True, null=True) + + objects = GuideFilterQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('guide filter') + verbose_name_plural = _('guide filters') diff --git a/apps/collection/transfer_data.py b/apps/collection/transfer_data.py new file mode 100644 index 00000000..9307e6a0 --- /dev/null +++ b/apps/collection/transfer_data.py @@ -0,0 +1,37 @@ +from pprint import pprint +from transfer.models import Guides, GuideFilters +from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer + + +def transfer_guide(): + """Transfer Guide model.""" + queryset = Guides.objects.exclude(title__icontains='test') + serialized_data = GuideSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"transfer guide errors: {serialized_data.errors}") + + +def transfer_guide_filter(): + """Transfer GuideFilter model.""" + queryset = GuideFilters.objects.all() + serialized_data = GuideFilterSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"transfer guide filter errors: {serialized_data.errors}") + + +data_types = { + 'guides': [ + transfer_guide, + ], + 'guide_filters': [ + transfer_guide_filter, + ] +} diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index 2d0ce399..3b562b0d 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -41,6 +41,8 @@ class Command(BaseCommand): 'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1 'purchased_plaques', # №6 - перенос купленных тарелок 'fill_city_gallery', # №3 - перенос галереи городов + 'guides', + 'guide_filters', ] def handle(self, *args, **options): diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 37f9217a..1ac40e80 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -5,11 +5,19 @@ # * Make sure each ForeignKey has `on_delete` set to the desired behavior. # * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table # Feel free to rename the models, but don't rename db_table values or field names. +import yaml from django.contrib.gis.db import models from transfer.mixins import MigrateMixin +def convert_entry(loader, node): + return {e[0]: e[1] for e in loader.construct_pairs(node)} + + +yaml.add_constructor('!ruby/hash:ActiveSupport::HashWithIndifferentAccess', convert_entry) + + # models.ForeignKey(ForeignModel, models.DO_NOTHING, blank=True, null=True) class Sites(MigrateMixin): diff --git a/apps/transfer/serializers/guide.py b/apps/transfer/serializers/guide.py new file mode 100644 index 00000000..5accbe64 --- /dev/null +++ b/apps/transfer/serializers/guide.py @@ -0,0 +1,283 @@ +from itertools import chain + +import yaml +from pycountry import countries, subdivisions +from rest_framework import serializers + +from collection.models import Guide, GuideType, GuideFilter +from establishment.models import EstablishmentType +from location.models import Country, Region +from main.models import SiteSettings +from transfer.mixins import TransferSerializerMixin + + +class GuideSerializer(TransferSerializerMixin): + id = serializers.IntegerField() + title = serializers.CharField() + vintage = serializers.IntegerField() + slug = serializers.CharField() + state = serializers.CharField() + site_id = serializers.IntegerField() + inserter_field = serializers.CharField() + + class Meta: + model = Guide + fields = ( + 'id', + 'title', + 'vintage', + 'slug', + 'state', + 'site_id', + 'inserter_field', + ) + + def validate(self, attrs): + """Overridden validate method.""" + attrs['old_id'] = attrs.pop('id') + attrs['name'] = attrs.pop('title') + attrs['vintage'] = int(attrs.pop('vintage')) + attrs['state'] = self.get_state(attrs.pop('state')) + attrs['site'] = self.get_site(attrs.pop('site_id')) + attrs['guide_type'] = self.get_guide_type(attrs.pop('inserter_field')) + return attrs + + def get_state(self, state: str): + if state == 'built': + return Guide.BUILT + elif state == 'removing': + return Guide.REMOVING + elif state == 'building': + return Guide.BUILDING + else: + return Guide.WAITING + + def get_site(self, site_id): + qs = SiteSettings.objects.filter(old_id=site_id) + if qs.exists(): + return qs.first() + + def get_guide_type(self, inserter_field): + guide_type, _ = GuideType.objects.get_or_create(name=inserter_field) + return guide_type + + +class GuideFilterSerializer(TransferSerializerMixin): + id = serializers.IntegerField() + year = serializers.CharField() + establishment_type = serializers.CharField(allow_null=True) + countries = serializers.CharField(allow_null=True) + regions = serializers.CharField(allow_null=True) + subregions = serializers.CharField(allow_null=True) + wine_regions = serializers.CharField(allow_null=True) + wine_classifications = serializers.CharField(allow_null=True) + wine_colors = serializers.CharField(allow_null=True) + wine_types = serializers.CharField(allow_null=True) + locales = serializers.CharField(allow_null=True) + states = serializers.CharField(allow_null=True) + + max_mark = serializers.IntegerField(allow_null=True) + min_mark = serializers.IntegerField(allow_null=True) + marks_only = serializers.NullBooleanField() + guide_id = serializers.IntegerField() + + class Meta: + model = GuideFilter + fields = ( + 'id', + 'year', + 'establishment_type', + 'countries', + 'regions', + 'subregions', + 'wine_regions', + 'wine_classifications', + 'wine_colors', + 'wine_types', + 'max_mark', + 'min_mark', + 'marks_only', + 'locales', + 'states', + 'guide_id', + ) + + @staticmethod + def parse_ruby_helper(raw_value: str): + """Parse RubyActiveSupport records""" + def convert_entry(loader, node): + return {e[0]: e[1] for e in loader.construct_pairs(node)} + + loader = yaml.Loader + loader.add_constructor('!ruby/hash:ActiveSupport::HashWithIndifferentAccess', convert_entry) + return yaml.load(raw_value, Loader=loader) if raw_value else None + + @staticmethod + def get_country_alpha_2(country_code_alpha3: str): + country = countries.get(alpha_3=country_code_alpha3.upper()) + return {'code_alpha_2': country.alpha2.lower() if country else None, + 'name': country.name if country else None,} + + @staticmethod + def parse_dictionary(dictionary: dict): + """ + Exclude root values from dictionary. + Convert {key_2: [value_1, value_2]} into + [value_1, value_2] + """ + return list(chain.from_iterable(dictionary.values())) + + @staticmethod + def parse_nested_dictionary(dictionary: dict): + """ + Exclude root values from dictionary. + Convert {key_1: {key_2: [value_1, value_2]}} into + [value_1, value_2] + """ + l = [] + for i in dictionary: + l.append(list(chain.from_iterable(list(dictionary[i].values())))) + return list(chain.from_iterable(l)) + + def get_country(self, code_alpha_3: str) -> Country: + country = self.get_country_alpha_2(code_alpha_3) + country_name = country['name'] + country_code = country['code_alpha_2'] + if country_name and country_code: + country, _ = Country.objects.get_or_create( + code__icontains=country_code, + name__contains={'en-GB': country_name}, + defaults={ + 'code': country_code, + 'name': {'en-GB': country_name} + } + ) + return country + + def get_region(self, region_code_alpha_3: str, + country_code_alpha_3: str, + sub_region_code_alpha_3: str = None): + country = self.get_country(country_code_alpha_3) + country_code_alpha_2 = country.code.upper() + region_qs = Region.objects.filter(code__iexact=region_code_alpha_3, + country__code__iexact=country_code_alpha_2) + + # If region isn't existed, check sub region for parent_code (region code) + if not region_qs.exists() and sub_region_code_alpha_3: + # sub region + subdivision = subdivisions.get( + code=f"{country_code_alpha_2}-{sub_region_code_alpha_3}") + if subdivision: + subdivision_region = subdivisions.get(parent_code=subdivision.parent_code) + obj = Region.objects.create( + name=subdivision_region.name, + code=subdivision_region.code, + country=country) + return obj + else: + return region_qs.first() + + def validate_year(self, value): + return self.parse_ruby_helper(value) + + def validate_establishment_type(self, value): + return self.parse_ruby_helper(value) + + def validate_countries(self, value): + return self.parse_ruby_helper(value) + + def validate_regions(self, value): + return self.parse_ruby_helper(value) + + def validate_sub_regions(self, value): + return self.parse_ruby_helper(value) + + def validate_wine_regions(self, value): + return self.parse_ruby_helper(value) + + def validate_wine_classifications(self, value): + return self.parse_ruby_helper(value) + + def validate_wine_colors(self, value): + return self.parse_ruby_helper(value) + + def validate_wine_types(self, value): + return self.parse_ruby_helper(value) + + def validate_locales(self, value): + return self.parse_ruby_helper(value) + + def validate_states(self, value): + return self.parse_ruby_helper(value) + + def validate(self, attrs): + sub_regions = attrs.pop('subregions') + regions = attrs.pop('regions') + + attrs['old_id'] = attrs.pop('id') + attrs['review_vintage_json'] = self.get_review_vintage(self.attrs.pop('year')) + attrs['establishment_type_json'] = self.get_establishment_type_ids( + self.attrs.pop('establishment_type')) + attrs['country_code_json'] = self.get_country_ids(attrs.pop('country_json')) + attrs['region_json'] = self.get_region_ids(regions, sub_regions) + attrs['sub_region_json'] = self.get_sub_region_ids(regions, sub_regions) + return attrs + + def get_review_vintage(self, year): + return {'vintage': [int(i) for i in set(year) if i.isdigit()]} + + def get_establishment_type_ids(self, establishment_types): + establishment_type_ids = [] + for establishment_type in establishment_types: + establishment_type_qs = EstablishmentType.objects.filter(index_name__iexact=establishment_type) + if not establishment_type_qs.exists(): + obj = EstablishmentType.objects.create( + name={'en-GB': establishment_type.capitalize}, + index_name=establishment_type.lower()) + establishment_type_ids.append(obj.id) + return {'id': establishment_type_ids} + + def get_country_ids(self, country_codes_alpha_3): + country_ids = [] + for code_alpha_3 in country_codes_alpha_3: + country_ids.append(self.get_country(code_alpha_3).id) + return {'id': country_ids} + + def get_region_ids(self, regions, sub_regions): + region_ids = [] + for country_code_alpha_3 in regions: + region_codes = regions[country_code_alpha_3] + for region_code_alpha_3 in region_codes: + region = self.get_region( + region_code_alpha_3=region_code_alpha_3, + country_code_alpha_3=country_code_alpha_3, + sub_region_code_alpha_3=sub_regions[country_code_alpha_3][region_code_alpha_3]) + if region: + region_ids.append(region.id) + return {'id': region_ids} + + def get_sub_region_ids(self, sub_regions): + sub_region_ids = [] + for country_code_alpha_3 in sub_regions: + for region_code_alpha_3 in sub_regions[country_code_alpha_3]: + region_codes = sub_regions[country_code_alpha_3][region_code_alpha_3] + for sub_region_code_alpha_3 in region_codes: + region = self.get_region( + region_code_alpha_3=region_code_alpha_3, + country_code_alpha_3=country_code_alpha_3, + sub_region_code_alpha_3=sub_region_code_alpha_3) + if region: + subdivision = subdivisions.get(parent_code=region.code.upper()) + if subdivision: + sub_region = Region.objects.create( + name=subdivision.name, + code=subdivisions.code, + parent_region=region, + country=region.country) + sub_region_ids.append(sub_region.id) + + return {'id': sub_region_ids} + + +# {'FRA': ['H', 'U']} REGIONS +# {'FRA': {'N': ['32']}} SUB REGIONS diff --git a/requirements/base.txt b/requirements/base.txt index 94e7ca27..aadc7301 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -57,3 +57,6 @@ redis==3.2.0 django_redis==4.10.0 # used byes indexing cache kombu==4.6.6 celery==4.3.0 + +# country information +pycountry==19.8.18 From 2cca32e4cf4380256e22b19806a670a9e3d6a97c Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 Nov 2019 15:37:26 +0300 Subject: [PATCH 079/191] mark in review back serializer --- apps/review/serializers/back.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/review/serializers/back.py b/apps/review/serializers/back.py index 674e1edc..ef8f5c8d 100644 --- a/apps/review/serializers/back.py +++ b/apps/review/serializers/back.py @@ -12,6 +12,7 @@ class ReviewBackSerializer(serializers.ModelSerializer): 'reviewer', 'text', 'status', + 'mark', # 'child', 'published_at', 'vintage', From 9c59052e0b0302227ec8f5b8c34bbbc52895cf12 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 Nov 2019 15:55:24 +0300 Subject: [PATCH 080/191] add review priority command start --- apps/review/management/__init__.py | 0 apps/review/management/commands/__init__.py | 0 .../commands/add_review_priority.py | 24 +++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 apps/review/management/__init__.py create mode 100644 apps/review/management/commands/__init__.py create mode 100644 apps/review/management/commands/add_review_priority.py diff --git a/apps/review/management/__init__.py b/apps/review/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/review/management/commands/__init__.py b/apps/review/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/review/management/commands/add_review_priority.py b/apps/review/management/commands/add_review_priority.py new file mode 100644 index 00000000..0583efbd --- /dev/null +++ b/apps/review/management/commands/add_review_priority.py @@ -0,0 +1,24 @@ +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from review.models import Review +from transfer.models import Reviews + + +class Command(BaseCommand): + help = '''Add review priority from old db to new db.''' + + def handle(self, *args, **kwargs): + reviews = Review.objects.all().values_list('old_id', flat=True) + queryset = Reviews.objects.exclude(product_id__isnull=False).filter( + id__in=list(reviews), + ).values_list('id', 'priority') + + for old_id, priority in tqdm(queryset, desc='Add priority to reviews'): + review = Review.objects.filter(old_id=old_id).first() + if review: + print(priority) + + self.stdout.write(self.style.WARNING(f'Priority added to review objects.')) + + From 6df9ffb0249a3a0bd1442c29c97d99e6528b6d34 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 Nov 2019 16:56:15 +0300 Subject: [PATCH 081/191] add review priority fin --- .../management/commands/add_review_priority.py | 3 ++- apps/review/migrations/0019_review_priority.py | 18 ++++++++++++++++++ apps/review/models.py | 2 +- apps/review/serializers/back.py | 1 + apps/review/serializers/common.py | 1 + 5 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 apps/review/migrations/0019_review_priority.py diff --git a/apps/review/management/commands/add_review_priority.py b/apps/review/management/commands/add_review_priority.py index 0583efbd..4c43bc52 100644 --- a/apps/review/management/commands/add_review_priority.py +++ b/apps/review/management/commands/add_review_priority.py @@ -17,7 +17,8 @@ class Command(BaseCommand): for old_id, priority in tqdm(queryset, desc='Add priority to reviews'): review = Review.objects.filter(old_id=old_id).first() if review: - print(priority) + review.priority = priority + review.save() self.stdout.write(self.style.WARNING(f'Priority added to review objects.')) diff --git a/apps/review/migrations/0019_review_priority.py b/apps/review/migrations/0019_review_priority.py new file mode 100644 index 00000000..980aa7a2 --- /dev/null +++ b/apps/review/migrations/0019_review_priority.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-28 13:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0018_auto_20191117_1117'), + ] + + operations = [ + migrations.AddField( + model_name='review', + name='priority', + field=models.PositiveSmallIntegerField(blank=True, default=None, null=True, verbose_name='Priority'), + ), + ] diff --git a/apps/review/models.py b/apps/review/models.py index 8734f4f6..bb344fc5 100644 --- a/apps/review/models.py +++ b/apps/review/models.py @@ -39,7 +39,6 @@ class Review(BaseAttributes, TranslatedFieldsMixin): (TO_REVIEW, _('To review')), (READY, _('Ready')), ) - reviewer = models.ForeignKey( 'account.User', related_name='reviews', @@ -83,6 +82,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin): ) vintage = models.IntegerField(_('Year of review'), validators=[MinValueValidator(1900), MaxValueValidator(2100)]) mark = models.FloatField(verbose_name=_('mark'), blank=True, null=True, default=None) + priority = models.PositiveSmallIntegerField(_('Priority'), blank=True, null=True, default=None) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) objects = ReviewQuerySet.as_manager() diff --git a/apps/review/serializers/back.py b/apps/review/serializers/back.py index ef8f5c8d..6c851796 100644 --- a/apps/review/serializers/back.py +++ b/apps/review/serializers/back.py @@ -13,6 +13,7 @@ class ReviewBackSerializer(serializers.ModelSerializer): 'text', 'status', 'mark', + 'priority', # 'child', 'published_at', 'vintage', diff --git a/apps/review/serializers/common.py b/apps/review/serializers/common.py index b1025d34..e714fff7 100644 --- a/apps/review/serializers/common.py +++ b/apps/review/serializers/common.py @@ -10,6 +10,7 @@ class ReviewBaseSerializer(serializers.ModelSerializer): 'id', 'reviewer', 'text', + 'priority', 'status', 'child', 'published_at', From 41611c5b9b28038ab35ae24eaed6b97d58a4421d Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 28 Nov 2019 18:37:12 +0300 Subject: [PATCH 082/191] added sites to back office endpoints --- apps/main/urls/back.py | 4 +++- apps/main/urls/common.py | 4 ++-- apps/main/urls/web.py | 5 +++-- apps/main/views/__init__.py | 4 ++++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 8424d236..2cb72aef 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -1,7 +1,7 @@ """Back main URLs""" from django.urls import path -from main.views import back as views +from main import views app_name = 'main' @@ -9,4 +9,6 @@ urlpatterns = [ path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards//', views.AwardRUDView.as_view(), name='awards-rud'), path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'), + path('sites/', views.SiteListView.as_view(), name='site-list'), + path('site-settings//', views.SiteSettingsView.as_view(), name='site-settings'), ] diff --git a/apps/main/urls/common.py b/apps/main/urls/common.py index 964442f9..6b8f26ce 100644 --- a/apps/main/urls/common.py +++ b/apps/main/urls/common.py @@ -1,6 +1,6 @@ """Main app urls.""" from django.urls import path -from main.views.common import * +from main.views import * app = 'main' @@ -8,5 +8,5 @@ common_urlpatterns = [ path('awards/', AwardView.as_view(), name='awards_list'), path('awards//', AwardRetrieveView.as_view(), name='awards_retrieve'), path('carousel/', CarouselListView.as_view(), name='carousel-list'), - path('determine-location/', DetermineLocation.as_view(), name='determine-location') + path('determine-location/', DetermineLocation.as_view(), name='determine-location'), ] diff --git a/apps/main/urls/web.py b/apps/main/urls/web.py index 2126b0c0..50ac9d1f 100644 --- a/apps/main/urls/web.py +++ b/apps/main/urls/web.py @@ -1,11 +1,12 @@ -from main.urls.common import common_urlpatterns from django.urls import path +from main.urls.common import common_urlpatterns from main.views.web import DetermineSiteView, SiteListView, SiteSettingsView urlpatterns = [ path('determine-site/', DetermineSiteView.as_view(), name='determine-site'), path('sites/', SiteListView.as_view(), name='site-list'), - path('site-settings//', SiteSettingsView.as_view(), name='site-settings'), ] + path('site-settings//', SiteSettingsView.as_view(), name='site-settings'), +] urlpatterns.extend(common_urlpatterns) diff --git a/apps/main/views/__init__.py b/apps/main/views/__init__.py index e69de29b..d1a35297 100644 --- a/apps/main/views/__init__.py +++ b/apps/main/views/__init__.py @@ -0,0 +1,4 @@ +from .common import * +from .back import * +from .mobile import * +from .web import * From 5660c20d5076ece1f982d642cd2f483e2e470b32 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 28 Nov 2019 18:52:57 +0300 Subject: [PATCH 083/191] added id field to site BO serializers --- apps/main/serializers.py | 30 +++++++++++++++++++++++++++--- apps/main/urls/back.py | 5 +++-- apps/main/views/__init__.py | 2 +- apps/main/views/back.py | 11 +++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 256333b4..410eb6bb 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -72,7 +72,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer): """Meta class.""" model = models.SiteSettings - fields = ( + fields = [ 'country_code', 'time_format', 'subdomain', @@ -86,7 +86,17 @@ class SiteSettingsSerializer(serializers.ModelSerializer): 'published_features', 'currency', 'country_name', - ) + ] + + +class SiteSettingsBackOfficeSerializer(SiteSettingsSerializer): + """Site settings serializer for back office.""" + + class Meta(SiteSettingsSerializer.Meta): + """Meta class.""" + fields = SiteSettingsSerializer.Meta.fields + [ + 'id', + ] class SiteSerializer(serializers.ModelSerializer): @@ -95,7 +105,11 @@ class SiteSerializer(serializers.ModelSerializer): class Meta: """Meta class.""" model = models.SiteSettings - fields = ('subdomain', 'site_url', 'country') + fields = [ + 'subdomain', + 'site_url', + 'country' + ] class SiteShortSerializer(serializers.ModelSerializer): @@ -108,6 +122,16 @@ class SiteShortSerializer(serializers.ModelSerializer): ] +class SiteBackOfficeSerializer(SiteSerializer): + """Serializer for back office.""" + + class Meta(SiteSerializer.Meta): + """Meta class.""" + fields = SiteSerializer.Meta.fields + [ + 'id', + ] + + # class SiteFeatureSerializer(serializers.ModelSerializer): # """Site feature serializer.""" # diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 2cb72aef..40011aa2 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -9,6 +9,7 @@ urlpatterns = [ path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards//', views.AwardRUDView.as_view(), name='awards-rud'), path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'), - path('sites/', views.SiteListView.as_view(), name='site-list'), - path('site-settings//', views.SiteSettingsView.as_view(), name='site-settings'), + path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list'), + path('site-settings//', views.SiteSettingsBackOfficeView.as_view(), + name='site-settings'), ] diff --git a/apps/main/views/__init__.py b/apps/main/views/__init__.py index d1a35297..2c9dae42 100644 --- a/apps/main/views/__init__.py +++ b/apps/main/views/__init__.py @@ -1,4 +1,4 @@ from .common import * -from .back import * from .mobile import * from .web import * +from .back import * diff --git a/apps/main/views/back.py b/apps/main/views/back.py index 5d82d88a..de47825b 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -5,6 +5,7 @@ from rest_framework import generics, permissions from main import serializers from main.filters import AwardFilter from main.models import Award +from main.views import SiteSettingsView, SiteListView class AwardLstView(generics.ListCreateAPIView): @@ -36,3 +37,13 @@ class ContentTypeView(generics.ListAPIView): 'model', 'app_label', ) + + +class SiteSettingsBackOfficeView(SiteSettingsView): + """Site settings View.""" + serializer_class = serializers.SiteSettingsBackOfficeSerializer + + +class SiteListBackOfficeView(SiteListView): + """Site settings View.""" + serializer_class = serializers.SiteBackOfficeSerializer From 9b02b855d60fd3424c3967cb87e6152e13663689 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 15:47:11 +0300 Subject: [PATCH 084/191] Revert "All facets are global now" This reverts commit 03b7ae6 --- apps/search_indexes/views.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 7f8520d7..e352f9be 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -34,7 +34,6 @@ class NewsDocumentViewSet(BaseDocumentViewSet): 'field': 'tags.id', 'enabled': True, 'facet': TermsFacet, - 'global': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, @@ -112,37 +111,31 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'field': 'works_at_weekday', 'facet': TermsFacet, 'enabled': True, - 'global': True, }, 'toque_number': { 'field': 'toque_number', 'enabled': True, 'facet': TermsFacet, - 'global': True, }, 'works_noon': { 'field': 'works_noon', 'facet': TermsFacet, 'enabled': True, - 'global': True, }, 'works_evening': { 'field': 'works_evening', 'facet': TermsFacet, 'enabled': True, - 'global': True, }, 'works_now': { 'field': 'works_now', 'facet': TermsFacet, 'enabled': True, - 'global': True, }, 'tag': { 'field': 'visible_tags.id', 'facet': TermsFacet, 'enabled': True, - 'global': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, @@ -151,7 +144,6 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'field': 'products.wine_colors.id', 'facet': TermsFacet, 'enabled': True, - 'global': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, @@ -160,7 +152,6 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'field': 'products.wine_region.id', 'facet': TermsFacet, 'enabled': True, - 'global': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, @@ -334,7 +325,6 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'tag': { 'field': 'wine_colors.id', 'enabled': True, - 'global': True, 'facet': TermsFacet, 'options': { 'size': utils.FACET_MAX_RESPONSE, @@ -343,7 +333,6 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'wine_region_id': { 'field': 'wine_region.id', 'enabled': True, - 'global': True, 'facet': TermsFacet, 'options': { 'size': utils.FACET_MAX_RESPONSE, From 68626e9fd5d203ba678ca52cac81b167ec61ab36 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 21:24:39 +0300 Subject: [PATCH 085/191] Another agg ES response strategy --- apps/search_indexes/filters.py | 43 +++++++++++++++++++++++++++++++++- apps/search_indexes/views.py | 28 ++++++++++++++++++---- apps/utils/pagination.py | 18 +++++++++++++- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index ab47ef84..5966d540 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -1,7 +1,48 @@ """Search indexes filters.""" from elasticsearch_dsl.query import Q -from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend +from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, FacetedSearchFilterBackend from search_indexes.utils import OBJECT_FIELD_PROPERTIES +from six import iteritems + + +class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): + + def __init__(self): + self.facets_computed = {} + + def aggregate(self, request, queryset, view): + """Aggregate. + + :param request: + :param queryset: + :param view: + :return: + """ + def makefilter(cur_facet): + def myfilter(x): + return cur_facet['facet']._params['field'] != next(iter(x._params)) + return myfilter + __facets = self.construct_facets(request, view) + for __field, __facet in iteritems(__facets): + agg = __facet['facet'].get_aggregation() + agg_filter = Q('match_all') + if __facet['global']: + queryset.aggs.bucket( + '_filter_' + __field, + 'global' + ).bucket(__field, agg) + else: + qs = queryset._clone() + filterer = makefilter(__facet) + qs.query._proxied._params['must'] = list(filter(filterer, qs.query._proxied._params['must'])) + facet_name = '_filter_' + __field + qs.aggs.bucket( + facet_name, + 'filter', + filter=agg_filter + ).bucket(__field, agg) + self.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]}) + return queryset class CustomSearchFilterBackend(SearchFilterBackend): diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index e352f9be..37ac5928 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -14,7 +14,25 @@ from search_indexes.documents.product import ProductDocument from utils.pagination import ESDocumentPagination -class NewsDocumentViewSet(BaseDocumentViewSet): +class FacetedResponseMixin: + def filter_queryset(self, queryset): + """ + Given a queryset, filter it with whichever filter backend is in use. + + You are unlikely to want to override this method, although you may need + to call it either from a list view, or from a custom `get_object` + method if you want to apply the configured filtering backend to the + default queryset. + """ + for backend in list(self.filter_backends): + bc = backend() + queryset = bc.filter_queryset(self.request, queryset, self) + if hasattr(bc, 'facets_computed'): + setattr(self, 'facets_computed', bc.facets_computed) + return queryset + + +class NewsDocumentViewSet(BaseDocumentViewSet, FacetedResponseMixin): """News document ViewSet.""" document = NewsDocument @@ -26,7 +44,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): filter_backends = [ filters.CustomSearchFilterBackend, FilteringFilterBackend, - FacetedSearchFilterBackend, + filters.CustomFacetedSearchFilterBackend, ] faceted_search_fields = { @@ -86,7 +104,7 @@ class MobileNewsDocumentViewSet(NewsDocumentViewSet): ] -class EstablishmentDocumentViewSet(BaseDocumentViewSet): +class EstablishmentDocumentViewSet(BaseDocumentViewSet, FacetedResponseMixin): """Establishment document ViewSet.""" document = EstablishmentDocument @@ -103,7 +121,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): FilteringFilterBackend, filters.CustomSearchFilterBackend, GeoSpatialFilteringFilterBackend, - FacetedSearchFilterBackend, + filters.CustomFacetedSearchFilterBackend, ] faceted_search_fields = { @@ -306,7 +324,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, - FacetedSearchFilterBackend, + filters.CustomFacetedSearchFilterBackend, ] search_fields = { diff --git a/apps/utils/pagination.py b/apps/utils/pagination.py index ed5ce89e..199d55b6 100644 --- a/apps/utils/pagination.py +++ b/apps/utils/pagination.py @@ -6,7 +6,6 @@ from django.conf import settings from rest_framework.pagination import CursorPagination, PageNumberPagination from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination - class ProjectPageNumberPagination(PageNumberPagination): """Customized pagination class.""" @@ -65,6 +64,23 @@ class ESDocumentPagination(ESPagination): return None return self.page.previous_page_number() + def get_facets(self, page=None): + """Get facets. + + :param page: + :return: + """ + if page is None: + page = self.page + + if hasattr(self, 'facets_computed'): + ret = {} + for filter_field, bucket_data in self.facets_computed.items(): + ret.update({filter_field: bucket_data.__dict__['_d_']}) + return ret + elif hasattr(page, 'facets') and hasattr(page.facets, '_d_'): + return page.facets._d_ + class EstablishmentPortionPagination(ProjectMobilePagination): """ From 9cf82f03fe582103e24f50af6b81f75673906f4b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 21:31:12 +0300 Subject: [PATCH 086/191] Another agg ES response strategy #2 --- apps/search_indexes/views.py | 47 ++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 37ac5928..4d97a70e 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -4,7 +4,6 @@ from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf.filter_backends import ( FilteringFilterBackend, GeoSpatialFilteringFilterBackend, - FacetedSearchFilterBackend, ) from elasticsearch_dsl import TermsFacet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet @@ -14,25 +13,7 @@ from search_indexes.documents.product import ProductDocument from utils.pagination import ESDocumentPagination -class FacetedResponseMixin: - def filter_queryset(self, queryset): - """ - Given a queryset, filter it with whichever filter backend is in use. - - You are unlikely to want to override this method, although you may need - to call it either from a list view, or from a custom `get_object` - method if you want to apply the configured filtering backend to the - default queryset. - """ - for backend in list(self.filter_backends): - bc = backend() - queryset = bc.filter_queryset(self.request, queryset, self) - if hasattr(bc, 'facets_computed'): - setattr(self, 'facets_computed', bc.facets_computed) - return queryset - - -class NewsDocumentViewSet(BaseDocumentViewSet, FacetedResponseMixin): +class NewsDocumentViewSet(BaseDocumentViewSet): """News document ViewSet.""" document = NewsDocument @@ -95,6 +76,14 @@ class NewsDocumentViewSet(BaseDocumentViewSet, FacetedResponseMixin): }, } + def filter_queryset(self, queryset): + for backend in list(self.filter_backends): + bc = backend() + queryset = bc.filter_queryset(self.request, queryset, self) + if hasattr(bc, 'facets_computed'): + setattr(self.paginator, 'facets_computed', bc.facets_computed) + return queryset + class MobileNewsDocumentViewSet(NewsDocumentViewSet): @@ -104,7 +93,7 @@ class MobileNewsDocumentViewSet(NewsDocumentViewSet): ] -class EstablishmentDocumentViewSet(BaseDocumentViewSet, FacetedResponseMixin): +class EstablishmentDocumentViewSet(BaseDocumentViewSet): """Establishment document ViewSet.""" document = EstablishmentDocument @@ -303,6 +292,14 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet, FacetedResponseMixin): } } + def filter_queryset(self, queryset): + for backend in list(self.filter_backends): + bc = backend() + queryset = bc.filter_queryset(self.request, queryset, self) + if hasattr(bc, 'facets_computed'): + setattr(self.paginator, 'facets_computed', bc.facets_computed) + return queryset + class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet): @@ -417,6 +414,14 @@ class ProductDocumentViewSet(BaseDocumentViewSet): }, } + def filter_queryset(self, queryset): + for backend in list(self.filter_backends): + bc = backend() + queryset = bc.filter_queryset(self.request, queryset, self) + if hasattr(bc, 'facets_computed'): + setattr(self.paginator, 'facets_computed', bc.facets_computed) + return queryset + class MobileProductDocumentViewSet(ProductDocumentViewSet): From 02cacc0ffeca87408bcbcc68ce7462e82352718f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 22:38:26 +0300 Subject: [PATCH 087/191] Fix issue with immutable instances --- apps/search_indexes/filters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 5966d540..a2a1bd08 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -32,7 +32,8 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): 'global' ).bucket(__field, agg) else: - qs = queryset._clone() + qs = queryset.__copy__() + qs.query = queryset.query._clone() filterer = makefilter(__facet) qs.query._proxied._params['must'] = list(filter(filterer, qs.query._proxied._params['must'])) facet_name = '_filter_' + __field From e924b359c89f69fc27a0ad408274b28aa9675abb Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 22:55:52 +0300 Subject: [PATCH 088/191] Fix issue w/ tags --- apps/search_indexes/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 4d97a70e..e2404fbb 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -140,7 +140,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'visible_tags.id', + 'field': 'tags.id', 'facet': TermsFacet, 'enabled': True, 'options': { From 846176131eab22976b99bbf47031c7a993f66bd2 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 23:33:09 +0300 Subject: [PATCH 089/191] Fix issue w/ tags #2 --- apps/search_indexes/filters.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index a2a1bd08..229bbff7 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -35,7 +35,12 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): qs = queryset.__copy__() qs.query = queryset.query._clone() filterer = makefilter(__facet) - qs.query._proxied._params['must'] = list(filter(filterer, qs.query._proxied._params['must'])) + for param_type in ['must', 'must_not', 'should']: + qs.query._proxied._params[param_type] = list( + filter( + filterer, qs.query._proxied._params[param_type] + ) + ) facet_name = '_filter_' + __field qs.aggs.bucket( facet_name, From 678377c234193b5f5dca82606d5a49930fbd0a35 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 28 Nov 2019 23:43:18 +0300 Subject: [PATCH 090/191] Fix issue w/ tags #3 --- apps/search_indexes/filters.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 229bbff7..c1f8a578 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -41,6 +41,9 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): filterer, qs.query._proxied._params[param_type] ) ) + if not len(qs.query._proxied._params['should']) \ + and qs.query._proxied._params.get('minimum_should_match'): + qs.query._proxied._params.pop('minimum_should_match') facet_name = '_filter_' + __field qs.aggs.bucket( facet_name, From 4f6cbb768425359b785b9b433ed8796e1725f23c Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 29 Nov 2019 09:55:06 +0300 Subject: [PATCH 091/191] ReviewBackSerializer --- .../commands/add_review_priority.py | 2 -- apps/review/serializers/back.py | 33 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/review/management/commands/add_review_priority.py b/apps/review/management/commands/add_review_priority.py index 4c43bc52..97525cf9 100644 --- a/apps/review/management/commands/add_review_priority.py +++ b/apps/review/management/commands/add_review_priority.py @@ -21,5 +21,3 @@ class Command(BaseCommand): review.save() self.stdout.write(self.style.WARNING(f'Priority added to review objects.')) - - diff --git a/apps/review/serializers/back.py b/apps/review/serializers/back.py index 6c851796..05a5d702 100644 --- a/apps/review/serializers/back.py +++ b/apps/review/serializers/back.py @@ -1,15 +1,43 @@ """Review app back serializers.""" +from django.contrib.contenttypes.models import ContentType from rest_framework import serializers -from review import models +from account.models import User +from review.models import Review + + +class _ReviewerSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ( + 'id', + 'username', + 'first_name', + 'last_name', + 'email', + ) + + +class _ContentTypeSerializer(serializers.ModelSerializer): + class Meta: + model = ContentType + fields = ( + 'id', + 'app_label', + 'model', + ) class ReviewBackSerializer(serializers.ModelSerializer): + reviewer_data = _ReviewerSerializer(read_only=True, source='reviewer') + content_type_data = _ContentTypeSerializer(read_only=True, source='content_type') + class Meta: - model = models.Review + model = Review fields = ( 'id', 'reviewer', + 'reviewer_data', 'text', 'status', 'mark', @@ -19,5 +47,6 @@ class ReviewBackSerializer(serializers.ModelSerializer): 'vintage', # 'country', 'content_type', + 'content_type_data', 'object_id', ) From c769049160182e63637e43913fc12a736cc061ab Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 29 Nov 2019 13:01:29 +0300 Subject: [PATCH 092/191] Email layout. Redirect to main from logo --- project/templates/account/change_email.html | 2 +- project/templates/account/password_change_email.html | 2 +- project/templates/account/password_reset_email.html | 2 +- project/templates/authorization/confirm_email.html | 2 +- project/templates/news/news_email.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/project/templates/account/change_email.html b/project/templates/account/change_email.html index efe92766..9f412a8f 100644 --- a/project/templates/account/change_email.html +++ b/project/templates/account/change_email.html @@ -19,7 +19,7 @@
diff --git a/project/templates/account/password_change_email.html b/project/templates/account/password_change_email.html index 77cad83f..d82eb967 100644 --- a/project/templates/account/password_change_email.html +++ b/project/templates/account/password_change_email.html @@ -19,7 +19,7 @@
diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index 4d61147d..c290c3c6 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -19,7 +19,7 @@
diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index c05c85b0..8b1332d0 100644 --- a/project/templates/authorization/confirm_email.html +++ b/project/templates/authorization/confirm_email.html @@ -19,7 +19,7 @@
diff --git a/project/templates/news/news_email.html b/project/templates/news/news_email.html index c2ae227c..27b8086c 100644 --- a/project/templates/news/news_email.html +++ b/project/templates/news/news_email.html @@ -18,7 +18,7 @@
From 0fb28467fbb99fa51a2ebf2cf5bc8afcac2b5d29 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 29 Nov 2019 13:26:05 +0300 Subject: [PATCH 093/191] updated serializer --- _dockerfiles/db/Dockerfile | 2 +- apps/location/models.py | 2 +- apps/transfer/serializers/guide.py | 207 ++++++++++++++++++----------- 3 files changed, 133 insertions(+), 78 deletions(-) diff --git a/_dockerfiles/db/Dockerfile b/_dockerfiles/db/Dockerfile index 45c707d9..e8a9ded3 100644 --- a/_dockerfiles/db/Dockerfile +++ b/_dockerfiles/db/Dockerfile @@ -1,3 +1,3 @@ -FROM mdillon/postgis:9.5 +FROM mdillon/postgis:latest RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8 ENV LANG ru_RU.utf8 diff --git a/apps/location/models.py b/apps/location/models.py index 3f104644..50f09b92 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -197,7 +197,7 @@ class WineRegionQuerySet(models.QuerySet): return self.exclude(wines__isnull=value) -class WineRegion(models.Model, TranslatedFieldsMixin): +class WineRegion(TranslatedFieldsMixin, models.Model): """Wine region model.""" name = models.CharField(_('name'), max_length=255) country = models.ForeignKey(Country, on_delete=models.PROTECT, diff --git a/apps/transfer/serializers/guide.py b/apps/transfer/serializers/guide.py index 5accbe64..692ebedc 100644 --- a/apps/transfer/serializers/guide.py +++ b/apps/transfer/serializers/guide.py @@ -6,9 +6,11 @@ from rest_framework import serializers from collection.models import Guide, GuideType, GuideFilter from establishment.models import EstablishmentType -from location.models import Country, Region +from location.models import Country, Region, WineRegion from main.models import SiteSettings from transfer.mixins import TransferSerializerMixin +from translation.models import Language +from review.models import Review class GuideSerializer(TransferSerializerMixin): @@ -70,9 +72,6 @@ class GuideFilterSerializer(TransferSerializerMixin): regions = serializers.CharField(allow_null=True) subregions = serializers.CharField(allow_null=True) wine_regions = serializers.CharField(allow_null=True) - wine_classifications = serializers.CharField(allow_null=True) - wine_colors = serializers.CharField(allow_null=True) - wine_types = serializers.CharField(allow_null=True) locales = serializers.CharField(allow_null=True) states = serializers.CharField(allow_null=True) @@ -91,9 +90,6 @@ class GuideFilterSerializer(TransferSerializerMixin): 'regions', 'subregions', 'wine_regions', - 'wine_classifications', - 'wine_colors', - 'wine_types', 'max_mark', 'min_mark', 'marks_only', @@ -112,12 +108,6 @@ class GuideFilterSerializer(TransferSerializerMixin): loader.add_constructor('!ruby/hash:ActiveSupport::HashWithIndifferentAccess', convert_entry) return yaml.load(raw_value, Loader=loader) if raw_value else None - @staticmethod - def get_country_alpha_2(country_code_alpha3: str): - country = countries.get(alpha_3=country_code_alpha3.upper()) - return {'code_alpha_2': country.alpha2.lower() if country else None, - 'name': country.name if country else None,} - @staticmethod def parse_dictionary(dictionary: dict): """ @@ -140,19 +130,20 @@ class GuideFilterSerializer(TransferSerializerMixin): return list(chain.from_iterable(l)) def get_country(self, code_alpha_3: str) -> Country: - country = self.get_country_alpha_2(code_alpha_3) - country_name = country['name'] - country_code = country['code_alpha_2'] - if country_name and country_code: - country, _ = Country.objects.get_or_create( - code__icontains=country_code, - name__contains={'en-GB': country_name}, - defaults={ - 'code': country_code, - 'name': {'en-GB': country_name} - } - ) - return country + country = countries.get(alpha_3=code_alpha_3.upper()) + if country: + country_name = country.name + country_code = country.alpha_2 + if country_name and country_code: + country, _ = Country.objects.get_or_create( + code__icontains=country_code, + name__contains={'en-GB': country_name}, + defaults={ + 'code': country_code, + 'name': {'en-GB': country_name} + } + ) + return country def get_region(self, region_code_alpha_3: str, country_code_alpha_3: str, @@ -162,20 +153,21 @@ class GuideFilterSerializer(TransferSerializerMixin): region_qs = Region.objects.filter(code__iexact=region_code_alpha_3, country__code__iexact=country_code_alpha_2) + if region_qs.exists(): + return region_qs.first() + # If region isn't existed, check sub region for parent_code (region code) - if not region_qs.exists() and sub_region_code_alpha_3: + if sub_region_code_alpha_3: # sub region subdivision = subdivisions.get( code=f"{country_code_alpha_2}-{sub_region_code_alpha_3}") if subdivision: - subdivision_region = subdivisions.get(parent_code=subdivision.parent_code) + subdivision_region = subdivisions.get(code=subdivision.parent_code) obj = Region.objects.create( name=subdivision_region.name, code=subdivision_region.code, country=country) return obj - else: - return region_qs.first() def validate_year(self, value): return self.parse_ruby_helper(value) @@ -189,7 +181,7 @@ class GuideFilterSerializer(TransferSerializerMixin): def validate_regions(self, value): return self.parse_ruby_helper(value) - def validate_sub_regions(self, value): + def validate_subregions(self, value): return self.parse_ruby_helper(value) def validate_wine_regions(self, value): @@ -215,69 +207,132 @@ class GuideFilterSerializer(TransferSerializerMixin): regions = attrs.pop('regions') attrs['old_id'] = attrs.pop('id') - attrs['review_vintage_json'] = self.get_review_vintage(self.attrs.pop('year')) + attrs['review_vintage_json'] = self.get_review_vintage(attrs.pop('year')) attrs['establishment_type_json'] = self.get_establishment_type_ids( - self.attrs.pop('establishment_type')) - attrs['country_code_json'] = self.get_country_ids(attrs.pop('country_json')) - attrs['region_json'] = self.get_region_ids(regions, sub_regions) - attrs['sub_region_json'] = self.get_sub_region_ids(regions, sub_regions) + attrs.pop('establishment_type')) + attrs['country_code_json'] = self.get_country_ids(attrs.pop('countries')) + attrs['region_json'] = self.get_region_ids(regions=regions, + sub_regions=sub_regions) + attrs['sub_region_json'] = self.get_sub_region_ids(sub_regions) + attrs['wine_region_json'] = self.get_wine_region_ids(attrs.pop('wine_regions')) + attrs['with_mark'] = attrs.pop('marks_only') + attrs['locale_json'] = self.get_locale_ids(attrs.pop('locales')) + attrs['review_state_json'] = self.get_review_state(attrs.pop('states')) + attrs['guide'] = self.get_guide(attrs.pop('guide_id')) return attrs def get_review_vintage(self, year): - return {'vintage': [int(i) for i in set(year) if i.isdigit()]} + if hasattr(year, '__iter__'): + return {'vintage': set(int(i) for i in set(year) if i.isdigit())} + return {'vintage': {year}} def get_establishment_type_ids(self, establishment_types): establishment_type_ids = [] - for establishment_type in establishment_types: - establishment_type_qs = EstablishmentType.objects.filter(index_name__iexact=establishment_type) - if not establishment_type_qs.exists(): - obj = EstablishmentType.objects.create( - name={'en-GB': establishment_type.capitalize}, - index_name=establishment_type.lower()) + if establishment_types: + for establishment_type in establishment_types: + establishment_type_qs = EstablishmentType.objects.filter(index_name__iexact=establishment_type) + if not establishment_type_qs.exists(): + obj = EstablishmentType.objects.create( + name={'en-GB': establishment_type.capitalize()}, + index_name=establishment_type.lower()) + else: + obj = establishment_type_qs.first() establishment_type_ids.append(obj.id) - return {'id': establishment_type_ids} + return {'id': set(establishment_type_ids)} def get_country_ids(self, country_codes_alpha_3): country_ids = [] - for code_alpha_3 in country_codes_alpha_3: - country_ids.append(self.get_country(code_alpha_3).id) - return {'id': country_ids} + if country_codes_alpha_3: + for code_alpha_3 in country_codes_alpha_3: + country = self.get_country(code_alpha_3) + if not country: + raise serializers.ValidationError({'detail': f'Country with alpha code 3 -' + f'{code_alpha_3}, is not found.'}) + country_ids.append(country.id) + return {'id': set(country_ids)} def get_region_ids(self, regions, sub_regions): region_ids = [] - for country_code_alpha_3 in regions: - region_codes = regions[country_code_alpha_3] - for region_code_alpha_3 in region_codes: - region = self.get_region( - region_code_alpha_3=region_code_alpha_3, - country_code_alpha_3=country_code_alpha_3, - sub_region_code_alpha_3=sub_regions[country_code_alpha_3][region_code_alpha_3]) - if region: - region_ids.append(region.id) + if regions: + for country_code_alpha_3 in regions: + for region_code_alpha_3 in regions[country_code_alpha_3]: + # Get region from sub region code. + if not sub_regions or country_code_alpha_3 not in sub_regions: + raise serializers.ValidationError({'detail': f'Sub regions is blanked.'}) + if region_code_alpha_3 in sub_regions[country_code_alpha_3]: + for sub_region_code_alpha_3 in sub_regions[country_code_alpha_3][region_code_alpha_3]: + region = self.get_region( + region_code_alpha_3=region_code_alpha_3, + country_code_alpha_3=country_code_alpha_3, + sub_region_code_alpha_3=sub_region_code_alpha_3) + if region: + region_ids.append(region.id) return {'id': region_ids} def get_sub_region_ids(self, sub_regions): sub_region_ids = [] - for country_code_alpha_3 in sub_regions: - for region_code_alpha_3 in sub_regions[country_code_alpha_3]: - region_codes = sub_regions[country_code_alpha_3][region_code_alpha_3] - for sub_region_code_alpha_3 in region_codes: - region = self.get_region( - region_code_alpha_3=region_code_alpha_3, - country_code_alpha_3=country_code_alpha_3, - sub_region_code_alpha_3=sub_region_code_alpha_3) - if region: - subdivision = subdivisions.get(parent_code=region.code.upper()) - if subdivision: - sub_region = Region.objects.create( - name=subdivision.name, - code=subdivisions.code, - parent_region=region, - country=region.country) - sub_region_ids.append(sub_region.id) + if sub_regions: + for country_code_alpha_3 in sub_regions: + # FRA etc. + for region_code_alpha_3 in sub_regions[country_code_alpha_3]: + # B, C, A etc. + for sub_region_code_alpha_3 in sub_regions[country_code_alpha_3][region_code_alpha_3]: + # 24, 32 etc. + # Get parent region + region = self.get_region( + region_code_alpha_3=region_code_alpha_3, + country_code_alpha_3=country_code_alpha_3, + sub_region_code_alpha_3=sub_region_code_alpha_3) + if region: + sub_region_qs = Region.objects.filter(parent_region__code=region.code) + if sub_region_qs.exists(): + sub_region_ids.append(sub_region_qs.first().id) + else: + subdivision = subdivisions.get(code=region.code.upper()) + if subdivision: + sub_region = Region.objects.get_or_create( + name=subdivision.name, + code=subdivisions.parent_code, + parent_region=region, + country=region.country) + sub_region_ids.append(sub_region.id) + return {'id': set(sub_region_ids)} - return {'id': sub_region_ids} + def get_wine_region_ids(self, wine_regions): + wine_region_ids = [] + if wine_regions: + for wine_region in wine_regions: + qs = WineRegion.objects.filter(name__iexact=wine_region) + if not qs.exists(): + raise serializers.ValidationError({ + 'detail': f'Wine region - {wine_region}, is not found.'}) + wine_region_ids.append(qs.first().id) + return {'id': set(wine_region_ids)} + def get_locale_ids(self, locales): + locale_ids = [] + if locales: + for locale in [locale for locale in locales if locale]: + qs = Language.objects.filter(locale=locale) + if not qs.exists(): + raise serializers.ValidationError({ + 'detail': f'{locale} is not found.'}) + return {'id': set(locale_ids)} -# {'FRA': ['H', 'U']} REGIONS -# {'FRA': {'N': ['32']}} SUB REGIONS + def get_review_state(self, states): + review_states = [] + if states: + for state in [state for state in states if state]: + if state == 'published': + review_states.append(Review.READY) + else: + review_states.append(Review.TO_INVESTIGATE) + return {'state': set(review_states)} + + def get_guide(self, old_guide_id: int): + qs = Guide.objects.filter(old_id=old_guide_id) + if not qs.exists(): + raise serializers.ValidationError({'detail': f'Guide with old id - ' + f'{old_guide_id}, ' + f'is not found.'}) + return qs.first() From 1a57d0edd830e7c16d9301641111bd61ff6a1e65 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 29 Nov 2019 13:46:20 +0300 Subject: [PATCH 094/191] Paginator custom facets code optimization --- apps/search_indexes/filters.py | 3 ++- apps/search_indexes/views.py | 28 ++++------------------------ 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index c1f8a578..c85b1ad0 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -23,6 +23,7 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): return cur_facet['facet']._params['field'] != next(iter(x._params)) return myfilter __facets = self.construct_facets(request, view) + setattr(view.paginator, 'facets_computed', {}) for __field, __facet in iteritems(__facets): agg = __facet['facet'].get_aggregation() agg_filter = Q('match_all') @@ -50,7 +51,7 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): 'filter', filter=agg_filter ).bucket(__field, agg) - self.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]}) + view.paginator.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]}) return queryset diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index e2404fbb..74cd4649 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -4,6 +4,7 @@ from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf.filter_backends import ( FilteringFilterBackend, GeoSpatialFilteringFilterBackend, + GeoSpatialOrderingFilterBackend, ) from elasticsearch_dsl import TermsFacet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet @@ -26,6 +27,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): filters.CustomSearchFilterBackend, FilteringFilterBackend, filters.CustomFacetedSearchFilterBackend, + GeoSpatialOrderingFilterBackend, ] faceted_search_fields = { @@ -76,14 +78,6 @@ class NewsDocumentViewSet(BaseDocumentViewSet): }, } - def filter_queryset(self, queryset): - for backend in list(self.filter_backends): - bc = backend() - queryset = bc.filter_queryset(self.request, queryset, self) - if hasattr(bc, 'facets_computed'): - setattr(self.paginator, 'facets_computed', bc.facets_computed) - return queryset - class MobileNewsDocumentViewSet(NewsDocumentViewSet): @@ -111,6 +105,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): filters.CustomSearchFilterBackend, GeoSpatialFilteringFilterBackend, filters.CustomFacetedSearchFilterBackend, + GeoSpatialOrderingFilterBackend, ] faceted_search_fields = { @@ -292,14 +287,6 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): } } - def filter_queryset(self, queryset): - for backend in list(self.filter_backends): - bc = backend() - queryset = bc.filter_queryset(self.request, queryset, self) - if hasattr(bc, 'facets_computed'): - setattr(self.paginator, 'facets_computed', bc.facets_computed) - return queryset - class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet): @@ -322,6 +309,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): FilteringFilterBackend, filters.CustomSearchFilterBackend, filters.CustomFacetedSearchFilterBackend, + GeoSpatialOrderingFilterBackend, ] search_fields = { @@ -414,14 +402,6 @@ class ProductDocumentViewSet(BaseDocumentViewSet): }, } - def filter_queryset(self, queryset): - for backend in list(self.filter_backends): - bc = backend() - queryset = bc.filter_queryset(self.request, queryset, self) - if hasattr(bc, 'facets_computed'): - setattr(self.paginator, 'facets_computed', bc.facets_computed) - return queryset - class MobileProductDocumentViewSet(ProductDocumentViewSet): From 52872ce364100254a0eee5fbc77eb1f68f8aac0f Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 29 Nov 2019 14:09:00 +0300 Subject: [PATCH 095/191] updated --- .gitignore | 2 + apps/transfer/serializers/guide.py | 70 +++++++++++++++--------------- project/settings/local.py | 19 ++++++-- 3 files changed, 53 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 78187ac0..1bb5c4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ logs/ celerybeat-schedule local_files celerybeat.pid +/gm_viktor.dump +/docker-compose.dump.yml diff --git a/apps/transfer/serializers/guide.py b/apps/transfer/serializers/guide.py index 692ebedc..3fd5cc32 100644 --- a/apps/transfer/serializers/guide.py +++ b/apps/transfer/serializers/guide.py @@ -257,16 +257,15 @@ class GuideFilterSerializer(TransferSerializerMixin): for country_code_alpha_3 in regions: for region_code_alpha_3 in regions[country_code_alpha_3]: # Get region from sub region code. - if not sub_regions or country_code_alpha_3 not in sub_regions: - raise serializers.ValidationError({'detail': f'Sub regions is blanked.'}) - if region_code_alpha_3 in sub_regions[country_code_alpha_3]: - for sub_region_code_alpha_3 in sub_regions[country_code_alpha_3][region_code_alpha_3]: - region = self.get_region( - region_code_alpha_3=region_code_alpha_3, - country_code_alpha_3=country_code_alpha_3, - sub_region_code_alpha_3=sub_region_code_alpha_3) - if region: - region_ids.append(region.id) + if sub_regions and country_code_alpha_3 in sub_regions: + if region_code_alpha_3 in sub_regions[country_code_alpha_3]: + for sub_region_code_alpha_3 in sub_regions[country_code_alpha_3][region_code_alpha_3]: + region = self.get_region( + region_code_alpha_3=region_code_alpha_3, + country_code_alpha_3=country_code_alpha_3, + sub_region_code_alpha_3=sub_region_code_alpha_3) + if region: + region_ids.append(region.id) return {'id': region_ids} def get_sub_region_ids(self, sub_regions): @@ -274,28 +273,30 @@ class GuideFilterSerializer(TransferSerializerMixin): if sub_regions: for country_code_alpha_3 in sub_regions: # FRA etc. - for region_code_alpha_3 in sub_regions[country_code_alpha_3]: - # B, C, A etc. - for sub_region_code_alpha_3 in sub_regions[country_code_alpha_3][region_code_alpha_3]: - # 24, 32 etc. - # Get parent region - region = self.get_region( - region_code_alpha_3=region_code_alpha_3, - country_code_alpha_3=country_code_alpha_3, - sub_region_code_alpha_3=sub_region_code_alpha_3) - if region: - sub_region_qs = Region.objects.filter(parent_region__code=region.code) - if sub_region_qs.exists(): - sub_region_ids.append(sub_region_qs.first().id) - else: - subdivision = subdivisions.get(code=region.code.upper()) - if subdivision: - sub_region = Region.objects.get_or_create( - name=subdivision.name, - code=subdivisions.parent_code, - parent_region=region, - country=region.country) - sub_region_ids.append(sub_region.id) + if country_code_alpha_3 in sub_regions: + for region_code_alpha_3 in sub_regions[country_code_alpha_3]: + # B, C, A etc. + if region_code_alpha_3 in sub_regions[country_code_alpha_3]: + for sub_region_code_alpha_3 in sub_regions[country_code_alpha_3][region_code_alpha_3]: + # 24, 32 etc. + # Get parent region + region = self.get_region( + region_code_alpha_3=region_code_alpha_3, + country_code_alpha_3=country_code_alpha_3, + sub_region_code_alpha_3=sub_region_code_alpha_3) + if region: + sub_region_qs = Region.objects.filter(parent_region__code=region.code) + if sub_region_qs.exists(): + sub_region_ids.append(sub_region_qs.first().id) + else: + subdivision = subdivisions.get(code=region.code.upper()) + if subdivision: + sub_region = Region.objects.get_or_create( + name=subdivision.name, + code=subdivisions.parent_code, + parent_region=region, + country=region.country) + sub_region_ids.append(sub_region.id) return {'id': set(sub_region_ids)} def get_wine_region_ids(self, wine_regions): @@ -316,7 +317,7 @@ class GuideFilterSerializer(TransferSerializerMixin): qs = Language.objects.filter(locale=locale) if not qs.exists(): raise serializers.ValidationError({ - 'detail': f'{locale} is not found.'}) + 'detail': f'Language with locale - {locale}, is not found.'}) return {'id': set(locale_ids)} def get_review_state(self, states): @@ -332,7 +333,6 @@ class GuideFilterSerializer(TransferSerializerMixin): def get_guide(self, old_guide_id: int): qs = Guide.objects.filter(old_id=old_guide_id) if not qs.exists(): - raise serializers.ValidationError({'detail': f'Guide with old id - ' - f'{old_guide_id}, ' + raise serializers.ValidationError({'detail': f'Guide with old_id - {old_guide_id}, ' f'is not found.'}) return qs.first() diff --git a/project/settings/local.py b/project/settings/local.py index c8974c40..b610aba2 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -30,10 +30,21 @@ MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) THUMBNAIL_DEBUG = True # ADDED TRANSFER APP -# INSTALLED_APPS.append('transfer.apps.TransferConfig') +INSTALLED_APPS.append('transfer.apps.TransferConfig') # DATABASES -DATABASES.update({ +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': os.environ.get('DB_NAME'), + 'USER': os.environ.get('DB_USERNAME'), + 'PASSWORD': os.environ.get('DB_PASSWORD'), + 'HOST': os.environ.get('DB_HOSTNAME'), + 'PORT': os.environ.get('DB_PORT'), + 'OPTIONS': { + 'options': '-c search_path=gm' + }, + }, 'legacy': { 'ENGINE': 'django.db.backends.mysql', # 'HOST': '172.22.0.1', @@ -41,7 +52,9 @@ DATABASES.update({ 'PORT': 3306, 'NAME': 'dev', 'USER': 'dev', - 'PASSWORD': 'octosecret123'}}) + 'PASSWORD': 'octosecret123' + }, +} # LOGGING From 376f815c97719d32b9c3e5decc825ed7287b057a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 29 Nov 2019 14:37:10 +0300 Subject: [PATCH 096/191] Order within bounding box from center --- apps/search_indexes/filters.py | 23 ++++++++++++++++++++++- apps/search_indexes/views.py | 10 ++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index c85b1ad0..47af1698 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -1,10 +1,31 @@ """Search indexes filters.""" from elasticsearch_dsl.query import Q -from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, FacetedSearchFilterBackend +from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, \ + FacetedSearchFilterBackend, GeoSpatialFilteringFilterBackend from search_indexes.utils import OBJECT_FIELD_PROPERTIES from six import iteritems +class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): + """Automatically adds centering and sorting within bounding box.""" + + @staticmethod + def calculate_center(a, b): + return (a[0] + b[0]) / 2, (a[1] + b[1]) / 2 + + def filter_queryset(self, request, queryset, view): + ret = super().filter_queryset(request, queryset, view) + bb = request.query_params.get('location__geo_bounding_box') + if bb: + center = self.calculate_center(*map(lambda p: list(map(lambda x: float(x),p.split(','))), bb.split('__'))) + request.GET._mutable = True + request.query_params.update({ + 'ordering': f'location__{center[0]}__{center[1]}__km' + }) + request.GET._mutable = False + return ret + + class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): def __init__(self): diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 74cd4649..7f262762 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -103,7 +103,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, - GeoSpatialFilteringFilterBackend, + filters.CustomGeoSpatialFilteringFilterBackend, filters.CustomFacetedSearchFilterBackend, GeoSpatialOrderingFilterBackend, ] @@ -287,6 +287,12 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): } } + geo_spatial_ordering_fields = { + 'location': { + 'field': 'address.coordinates', + }, + } + class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet): @@ -309,7 +315,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): FilteringFilterBackend, filters.CustomSearchFilterBackend, filters.CustomFacetedSearchFilterBackend, - GeoSpatialOrderingFilterBackend, + # GeoSpatialOrderingFilterBackend, ] search_fields = { From aebdb0fb20fc312f7613902aee0103aac093c55a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 29 Nov 2019 15:50:42 +0300 Subject: [PATCH 097/191] Add id field to api/web/main/site-settings/{domain} --- apps/main/views/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/main/views/web.py b/apps/main/views/web.py index 3a634457..86c550da 100644 --- a/apps/main/views/web.py +++ b/apps/main/views/web.py @@ -25,7 +25,7 @@ class SiteSettingsView(generics.RetrieveAPIView): lookup_field = 'subdomain' permission_classes = (permissions.AllowAny,) queryset = models.SiteSettings.objects.all() - serializer_class = serializers.SiteSettingsSerializer + serializer_class = serializers.SiteSettingsBackOfficeSerializer class SiteListView(generics.ListAPIView): From a102b68173e930aa5891f91dac559a6699317938 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 29 Nov 2019 16:22:02 +0300 Subject: [PATCH 098/191] News start field is nullable now --- .../news/migrations/0037_auto_20191129_1320.py | 18 ++++++++++++++++++ apps/news/models.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 apps/news/migrations/0037_auto_20191129_1320.py diff --git a/apps/news/migrations/0037_auto_20191129_1320.py b/apps/news/migrations/0037_auto_20191129_1320.py new file mode 100644 index 00000000..91c9a898 --- /dev/null +++ b/apps/news/migrations/0037_auto_20191129_1320.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-29 13:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0036_news_site'), + ] + + operations = [ + migrations.AlterField( + model_name='news', + name='start', + field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Start'), + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index 2aa571c0..30e4206b 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -174,7 +174,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi description = TJSONField(blank=True, null=True, default=None, verbose_name=_('description'), help_text='{"en-GB":"some text"}') - start = models.DateTimeField(verbose_name=_('Start')) + start = models.DateTimeField(blank=True, null=True, default=None, + verbose_name=_('Start')) end = models.DateTimeField(blank=True, null=True, default=None, verbose_name=_('End')) slug = models.SlugField(unique=True, max_length=255, From 4e3a990ac60a8035557e761e910eb9421a7f3cf6 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 29 Nov 2019 16:44:24 +0300 Subject: [PATCH 099/191] added command to fix existed collections --- .../management/commands/fix_collection.py | 32 +++++++++++++++++++ .../management/commands/import_collection.py | 7 ++-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 apps/collection/management/commands/fix_collection.py diff --git a/apps/collection/management/commands/fix_collection.py b/apps/collection/management/commands/fix_collection.py new file mode 100644 index 00000000..9162f8f7 --- /dev/null +++ b/apps/collection/management/commands/fix_collection.py @@ -0,0 +1,32 @@ +from django.conf import settings +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from collection.models import Collection + + +class Command(BaseCommand): + help = """Fix existed collections.""" + + def handle(self, *args, **kwarg): + update_collections = [] + collections = Collection.objects.values_list('id', 'collection_type', 'description') + for id, collection_type, description in tqdm(collections): + collection = Collection.objects.get(id=id) + description = collection.description + collection_updated = False + + if isinstance(description, str): + if description.lower().find('pop') != -1: + collection.collection_type = Collection.POP + collection_updated = True + + if not isinstance(description, dict): + collection.description = {settings.FALLBACK_LOCALE: collection.description} + collection_updated = True + + if collection_updated: + update_collections.append(collection) + + Collection.objects.bulk_update(update_collections, ['collection_type', 'description', ]) + self.stdout.write(self.style.WARNING(f'Updated products: {len(update_collections)}')) diff --git a/apps/collection/management/commands/import_collection.py b/apps/collection/management/commands/import_collection.py index 67d1cacf..f8f1702e 100644 --- a/apps/collection/management/commands/import_collection.py +++ b/apps/collection/management/commands/import_collection.py @@ -3,6 +3,7 @@ from establishment.models import Establishment from location.models import Country, Language from transfer.models import Collections from collection.models import Collection +from django.conf import settings from news.models import News @@ -93,9 +94,11 @@ class Command(BaseCommand): country = Country.objects.filter(code=obj['country_code']).first() if country: objects.append( - Collection(name={"en-GB": obj['title']}, collection_type=Collection.ORDINARY, + Collection(name={settings.FALLBACK_LOCALE: obj['title']}, + collection_type=Collection.POP if obj['description'].lower().find('pop') != -1 + else Collection.ORDINARY, country=country, - description=obj['description'], + description={settings.FALLBACK_LOCALE: obj['description']}, slug=obj['slug'], old_id=obj['collection_id'], start=obj['start'], image_url='https://s3.eu-central-1.amazonaws.com/gm-test.com/media/'+obj['attachment_suffix_url'] From 530b7e2ab61b337cf11e59a25441c05a550358f7 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 29 Nov 2019 18:31:10 +0300 Subject: [PATCH 100/191] Fix search aggs --- apps/search_indexes/filters.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 47af1698..ec45350c 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -58,12 +58,14 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): qs.query = queryset.query._clone() filterer = makefilter(__facet) for param_type in ['must', 'must_not', 'should']: - qs.query._proxied._params[param_type] = list( - filter( - filterer, qs.query._proxied._params[param_type] + if qs.query._proxied._params.get(param_type): + qs.query._proxied._params[param_type] = list( + filter( + filterer, qs.query._proxied._params[param_type] + ) ) - ) - if not len(qs.query._proxied._params['should']) \ + sh = qs.query._proxied._params.get('should') + if (not sh or not len(sh)) \ and qs.query._proxied._params.get('minimum_should_match'): qs.query._proxied._params.pop('minimum_should_match') facet_name = '_filter_' + __field From a9d45ab7bbeb16e6f7413e5e8588b7d1e10f324a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 29 Nov 2019 18:43:41 +0300 Subject: [PATCH 101/191] Start news field s timefield --- apps/search_indexes/documents/news.py | 3 +-- apps/search_indexes/serializers.py | 1 + apps/search_indexes/views.py | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 1fa2f9d9..3c87e680 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -42,13 +42,12 @@ class NewsDocument(Document): }, multi=True) favorites_for_users = fields.ListField(field=fields.IntegerField()) - + start = fields.DateField(attr='start') class Django: model = models.News fields = ( 'id', - 'start', 'end', 'slug', 'state', diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 84782f0c..e5a249a7 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -206,6 +206,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'preview_image_url', 'news_type', 'tags', + 'start', 'slug', ) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 7f262762..1f04a9a9 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -5,6 +5,7 @@ from django_elasticsearch_dsl_drf.filter_backends import ( FilteringFilterBackend, GeoSpatialFilteringFilterBackend, GeoSpatialOrderingFilterBackend, + OrderingFilterBackend, ) from elasticsearch_dsl import TermsFacet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet @@ -27,9 +28,15 @@ class NewsDocumentViewSet(BaseDocumentViewSet): filters.CustomSearchFilterBackend, FilteringFilterBackend, filters.CustomFacetedSearchFilterBackend, - GeoSpatialOrderingFilterBackend, + OrderingFilterBackend ] + ordering_fields = { + 'start': { + 'field': 'start', + }, + } + faceted_search_fields = { 'tag': { 'field': 'tags.id', From 20b3e7f4616ad57d8600ff7d349904d53a480621 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 2 Dec 2019 12:07:24 +0300 Subject: [PATCH 102/191] status display field --- apps/review/serializers/back.py | 2 ++ apps/review/views/back.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/review/serializers/back.py b/apps/review/serializers/back.py index 05a5d702..75df94e2 100644 --- a/apps/review/serializers/back.py +++ b/apps/review/serializers/back.py @@ -31,6 +31,7 @@ class _ContentTypeSerializer(serializers.ModelSerializer): class ReviewBackSerializer(serializers.ModelSerializer): reviewer_data = _ReviewerSerializer(read_only=True, source='reviewer') content_type_data = _ContentTypeSerializer(read_only=True, source='content_type') + status_display = serializers.CharField(read_only=True, source='get_status_display') class Meta: model = Review @@ -40,6 +41,7 @@ class ReviewBackSerializer(serializers.ModelSerializer): 'reviewer_data', 'text', 'status', + 'status_display', 'mark', 'priority', # 'child', diff --git a/apps/review/views/back.py b/apps/review/views/back.py index 511c91f9..caf12b62 100644 --- a/apps/review/views/back.py +++ b/apps/review/views/back.py @@ -8,7 +8,14 @@ from review.serializers.back import ReviewBackSerializer class ReviewLstView(generics.ListCreateAPIView): - """Comment list create view.""" + """Review list create view. + + status values: + + TO_INVESTIGATE = 0 + TO_REVIEW = 1 + READY = 2 + """ serializer_class = ReviewBackSerializer queryset = models.Review.objects.all() permission_classes = [permissions.IsAuthenticatedOrReadOnly, ] @@ -16,7 +23,14 @@ class ReviewLstView(generics.ListCreateAPIView): class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView): - """Comment RUD view.""" + """Review RUD view. + + status values: + + TO_INVESTIGATE = 0 + TO_REVIEW = 1 + READY = 2 + """ serializer_class = ReviewBackSerializer queryset = models.Review.objects.all() permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer] From fccffa93e36e3c47fec834fe32748520c31d0420 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 12:45:58 +0300 Subject: [PATCH 103/191] Order news by -start --- apps/news/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/news/views.py b/apps/news/views.py index 6573b5d5..a4a5c33a 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -21,7 +21,7 @@ class NewsMixinView: qs = models.News.objects.published() \ .with_base_related() \ .annotate_in_favorites(self.request.user) \ - .order_by('-is_highlighted', '-created') + .order_by('-is_highlighted', '-start') country_code = self.request.country_code if country_code: From 076d14fd5bf29eef97d09a0dc783ca0fbec904c4 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 2 Dec 2019 13:51:48 +0300 Subject: [PATCH 104/191] added filter model --- .gitignore | 2 +- .../0019_advertorial_guidefilter.py | 41 +++++++ apps/collection/models.py | 28 +++-- apps/collection/transfer_data.py | 12 ++- apps/transfer/mixins.py | 6 ++ apps/transfer/models.py | 2 +- apps/transfer/serializers/guide.py | 102 ++++++++++-------- 7 files changed, 127 insertions(+), 66 deletions(-) create mode 100644 apps/collection/migrations/0019_advertorial_guidefilter.py diff --git a/.gitignore b/.gitignore index 1bb5c4b0..90e4f23f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,9 +22,9 @@ logs/ # dev ./docker-compose.override.yml - celerybeat-schedule local_files celerybeat.pid /gm_viktor.dump /docker-compose.dump.yml +/gm_production_20191029.sql diff --git a/apps/collection/migrations/0019_advertorial_guidefilter.py b/apps/collection/migrations/0019_advertorial_guidefilter.py new file mode 100644 index 00000000..180a1e0e --- /dev/null +++ b/apps/collection/migrations/0019_advertorial_guidefilter.py @@ -0,0 +1,41 @@ +# Generated by Django 2.2.7 on 2019-12-02 10:11 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('collection', '0018_auto_20191127_1047'), + ] + + operations = [ + migrations.CreateModel( + name='GuideFilter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('establishment_type_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='establishment types')), + ('country_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='countries')), + ('region_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='regions')), + ('sub_region_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='sub regions')), + ('wine_region_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='wine regions')), + ('with_mark', models.BooleanField(default=True, help_text='exclude empty marks?', verbose_name='with mark')), + ('locale_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='locales')), + ('max_mark', models.FloatField(help_text='mark under', null=True, verbose_name='max mark')), + ('min_mark', models.FloatField(help_text='mark over', null=True, verbose_name='min mark')), + ('review_vintage_json', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='review vintage years')), + ('review_state_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='review states')), + ('old_id', models.IntegerField(blank=True, null=True)), + ('guide', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='collection.Guide', verbose_name='guide')), + ], + options={ + 'verbose_name': 'guide filter', + 'verbose_name_plural': 'guide filters', + }, + ), + ] diff --git a/apps/collection/models.py b/apps/collection/models.py index af8b2a33..7d037a85 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -188,29 +188,25 @@ class GuideFilter(ProjectBaseMixin): """Guide filter model.""" establishment_type_json = JSONField(blank=True, null=True, verbose_name='establishment types') - country_code_json = JSONField(blank=True, null=True, - verbose_name='countries') - region_code_json = JSONField(blank=True, null=True, - verbose_name='regions') - sub_region_code_json = JSONField(blank=True, null=True, - verbose_name='sub regions') + country_json = JSONField(blank=True, null=True, + verbose_name='countries') + region_json = JSONField(blank=True, null=True, + verbose_name='regions') + sub_region_json = JSONField(blank=True, null=True, + verbose_name='sub regions') wine_region_json = JSONField(blank=True, null=True, verbose_name='wine regions') - wine_classification_json = JSONField(blank=True, null=True, - verbose_name='wine classifications') - wine_color_json = JSONField(blank=True, null=True, - verbose_name='wine colors') - wine_type_json = JSONField(blank=True, null=True, - verbose_name='wine types') with_mark = models.BooleanField(default=True, verbose_name=_('with mark'), help_text=_('exclude empty marks?')) locale_json = JSONField(blank=True, null=True, verbose_name='locales') - max_mark = models.PositiveSmallIntegerField(verbose_name=_('max mark'), - help_text=_('mark under')) - min_mark = models.PositiveSmallIntegerField(verbose_name=_('min mark'), - help_text=_('mark over')) + max_mark = models.FloatField(verbose_name=_('max mark'), + null=True, + help_text=_('mark under')) + min_mark = models.FloatField(verbose_name=_('min mark'), + null=True, + help_text=_('mark over')) review_vintage_json = JSONField(verbose_name='review vintage years') review_state_json = JSONField(blank=True, null=True, verbose_name='review states') diff --git a/apps/collection/transfer_data.py b/apps/collection/transfer_data.py index 9307e6a0..9eeec541 100644 --- a/apps/collection/transfer_data.py +++ b/apps/collection/transfer_data.py @@ -5,6 +5,7 @@ from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer def transfer_guide(): """Transfer Guide model.""" + errors = [] queryset = Guides.objects.exclude(title__icontains='test') serialized_data = GuideSerializer( data=list(queryset.values()), @@ -12,19 +13,24 @@ def transfer_guide(): if serialized_data.is_valid(): serialized_data.save() else: - pprint(f"transfer guide errors: {serialized_data.errors}") + for d in serialized_data.errors: errors.append(d) if d else None + pprint(f"transfer_guide errors: {errors}") def transfer_guide_filter(): """Transfer GuideFilter model.""" - queryset = GuideFilters.objects.all() + errors = [] + queryset = GuideFilters.objects.exclude(guide__title__icontains='test') \ + .exclude(guide__id__isnull=True) serialized_data = GuideFilterSerializer( data=list(queryset.values()), many=True) if serialized_data.is_valid(): serialized_data.save() else: - pprint(f"transfer guide filter errors: {serialized_data.errors}") + for d in serialized_data.errors: errors.append(d) if d else None + pprint(f"transfer_guide_filter errors: {errors}\n" + f"COUNT: {len(errors)}") data_types = { diff --git a/apps/transfer/mixins.py b/apps/transfer/mixins.py index 7b43b210..30537a14 100644 --- a/apps/transfer/mixins.py +++ b/apps/transfer/mixins.py @@ -39,6 +39,12 @@ class TransferSerializerMixin(serializers.ModelSerializer): qs = self.Meta.model.objects.filter(**validated_data) if not qs.exists(): return super().create(validated_data) + # try: + # qs = self.Meta.model.objects.filter(**validated_data) + # if not qs.exists(): + # return super().create(validated_data) + # except Exception: + # breakpoint() @property def tag_category(self): diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 1ac40e80..fa3d898b 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -370,7 +370,7 @@ class GuideFilters(MigrateMixin): states = models.CharField(max_length=255, blank=True, null=True) created_at = models.DateTimeField() updated_at = models.DateTimeField() - guide_id = models.IntegerField(blank=True, null=True) + guide = models.ForeignKey(Guides, models.DO_NOTHING, blank=True, null=True) class Meta: managed = False diff --git a/apps/transfer/serializers/guide.py b/apps/transfer/serializers/guide.py index 3fd5cc32..c49e34b2 100644 --- a/apps/transfer/serializers/guide.py +++ b/apps/transfer/serializers/guide.py @@ -1,5 +1,5 @@ from itertools import chain - +from django.utils.text import slugify import yaml from pycountry import countries, subdivisions from rest_framework import serializers @@ -66,7 +66,7 @@ class GuideSerializer(TransferSerializerMixin): class GuideFilterSerializer(TransferSerializerMixin): id = serializers.IntegerField() - year = serializers.CharField() + year = serializers.CharField(allow_null=True) establishment_type = serializers.CharField(allow_null=True) countries = serializers.CharField(allow_null=True) regions = serializers.CharField(allow_null=True) @@ -75,8 +75,8 @@ class GuideFilterSerializer(TransferSerializerMixin): locales = serializers.CharField(allow_null=True) states = serializers.CharField(allow_null=True) - max_mark = serializers.IntegerField(allow_null=True) - min_mark = serializers.IntegerField(allow_null=True) + max_mark = serializers.FloatField(allow_null=True) + min_mark = serializers.FloatField(allow_null=True) marks_only = serializers.NullBooleanField() guide_id = serializers.IntegerField() @@ -149,25 +149,33 @@ class GuideFilterSerializer(TransferSerializerMixin): country_code_alpha_3: str, sub_region_code_alpha_3: str = None): country = self.get_country(country_code_alpha_3) - country_code_alpha_2 = country.code.upper() - region_qs = Region.objects.filter(code__iexact=region_code_alpha_3, - country__code__iexact=country_code_alpha_2) + if country: + country_code_alpha_2 = country.code.upper() + region_qs = Region.objects.filter(code__iexact=region_code_alpha_3, + country__code__iexact=country_code_alpha_2) - if region_qs.exists(): - return region_qs.first() + if region_qs.exists(): + return region_qs.first() - # If region isn't existed, check sub region for parent_code (region code) - if sub_region_code_alpha_3: - # sub region - subdivision = subdivisions.get( - code=f"{country_code_alpha_2}-{sub_region_code_alpha_3}") - if subdivision: - subdivision_region = subdivisions.get(code=subdivision.parent_code) - obj = Region.objects.create( - name=subdivision_region.name, - code=subdivision_region.code, - country=country) - return obj + # If region isn't existed, check sub region for parent_code (region code) + if sub_region_code_alpha_3: + # sub region + subdivision = subdivisions.get( + code=f"{country_code_alpha_2}-{sub_region_code_alpha_3}") + if subdivision: + # try with parent code + subdivision_region = subdivisions.get(code=subdivision.__dict__.get('_fields') + .get('parent_code')) + if not subdivision_region: + # try with parent + subdivision_region = subdivisions.get(code=subdivision.__dict__.get('_fields') + .get('parent')) + if subdivision_region: + obj = Region.objects.create( + name=subdivision_region.name, + code=subdivision_region.code, + country=country) + return obj def validate_year(self, value): return self.parse_ruby_helper(value) @@ -210,12 +218,12 @@ class GuideFilterSerializer(TransferSerializerMixin): attrs['review_vintage_json'] = self.get_review_vintage(attrs.pop('year')) attrs['establishment_type_json'] = self.get_establishment_type_ids( attrs.pop('establishment_type')) - attrs['country_code_json'] = self.get_country_ids(attrs.pop('countries')) + attrs['country_json'] = self.get_country_ids(attrs.pop('countries')) attrs['region_json'] = self.get_region_ids(regions=regions, sub_regions=sub_regions) attrs['sub_region_json'] = self.get_sub_region_ids(sub_regions) attrs['wine_region_json'] = self.get_wine_region_ids(attrs.pop('wine_regions')) - attrs['with_mark'] = attrs.pop('marks_only') + attrs['with_mark'] = attrs.pop('marks_only') or True attrs['locale_json'] = self.get_locale_ids(attrs.pop('locales')) attrs['review_state_json'] = self.get_review_state(attrs.pop('states')) attrs['guide'] = self.get_guide(attrs.pop('guide_id')) @@ -223,8 +231,8 @@ class GuideFilterSerializer(TransferSerializerMixin): def get_review_vintage(self, year): if hasattr(year, '__iter__'): - return {'vintage': set(int(i) for i in set(year) if i.isdigit())} - return {'vintage': {year}} + return {'vintage': list(set(int(i) for i in set(year) if i.isdigit()))} + return {'vintage': [year, ]} def get_establishment_type_ids(self, establishment_types): establishment_type_ids = [] @@ -234,22 +242,24 @@ class GuideFilterSerializer(TransferSerializerMixin): if not establishment_type_qs.exists(): obj = EstablishmentType.objects.create( name={'en-GB': establishment_type.capitalize()}, - index_name=establishment_type.lower()) + index_name=slugify(establishment_type)) else: obj = establishment_type_qs.first() establishment_type_ids.append(obj.id) - return {'id': set(establishment_type_ids)} + return {'id': list(set(establishment_type_ids))} def get_country_ids(self, country_codes_alpha_3): country_ids = [] if country_codes_alpha_3: for code_alpha_3 in country_codes_alpha_3: - country = self.get_country(code_alpha_3) - if not country: - raise serializers.ValidationError({'detail': f'Country with alpha code 3 -' - f'{code_alpha_3}, is not found.'}) - country_ids.append(country.id) - return {'id': set(country_ids)} + # Code can be an empty string. + if code_alpha_3 and not code_alpha_3 == 'AAA': + country = self.get_country(code_alpha_3) + if not country: + raise serializers.ValidationError({'detail': f'Country with alpha code 3 -' + f'{code_alpha_3}, is not found.'}) + country_ids.append(country.id) + return {'id': list(set(country_ids))} def get_region_ids(self, regions, sub_regions): region_ids = [] @@ -266,7 +276,7 @@ class GuideFilterSerializer(TransferSerializerMixin): sub_region_code_alpha_3=sub_region_code_alpha_3) if region: region_ids.append(region.id) - return {'id': region_ids} + return {'id': list(set(region_ids))} def get_sub_region_ids(self, sub_regions): sub_region_ids = [] @@ -291,13 +301,13 @@ class GuideFilterSerializer(TransferSerializerMixin): else: subdivision = subdivisions.get(code=region.code.upper()) if subdivision: - sub_region = Region.objects.get_or_create( + sub_region, _ = Region.objects.get_or_create( name=subdivision.name, - code=subdivisions.parent_code, + code=subdivision.code, parent_region=region, country=region.country) sub_region_ids.append(sub_region.id) - return {'id': set(sub_region_ids)} + return {'id': list(set(sub_region_ids))} def get_wine_region_ids(self, wine_regions): wine_region_ids = [] @@ -308,17 +318,21 @@ class GuideFilterSerializer(TransferSerializerMixin): raise serializers.ValidationError({ 'detail': f'Wine region - {wine_region}, is not found.'}) wine_region_ids.append(qs.first().id) - return {'id': set(wine_region_ids)} + return {'id': list(set(wine_region_ids))} def get_locale_ids(self, locales): locale_ids = [] if locales: for locale in [locale for locale in locales if locale]: - qs = Language.objects.filter(locale=locale) + if len(locale) == 2: + qs = Language.objects.filter(locale__startswith=locale) + else: + qs = Language.objects.filter(locale=locale) if not qs.exists(): raise serializers.ValidationError({ 'detail': f'Language with locale - {locale}, is not found.'}) - return {'id': set(locale_ids)} + locale_ids.extend(qs.values_list('id', flat=True)) + return {'id': list(set(locale_ids))} def get_review_state(self, states): review_states = [] @@ -328,11 +342,9 @@ class GuideFilterSerializer(TransferSerializerMixin): review_states.append(Review.READY) else: review_states.append(Review.TO_INVESTIGATE) - return {'state': set(review_states)} + return {'state': list(set(review_states))} def get_guide(self, old_guide_id: int): qs = Guide.objects.filter(old_id=old_guide_id) - if not qs.exists(): - raise serializers.ValidationError({'detail': f'Guide with old_id - {old_guide_id}, ' - f'is not found.'}) - return qs.first() + if qs.exists(): + return qs.first() From 5b11b6b8fcf11e325543472b33fecb0ed00b7e47 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 13:52:53 +0300 Subject: [PATCH 105/191] Add created to products search results --- apps/search_indexes/documents/product.py | 1 + apps/search_indexes/serializers.py | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index 61c4eeeb..ad99e178 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -149,6 +149,7 @@ class ProductDocument(Document): name_ru = fields.TextField(attr='display_name', analyzer='russian') name_fr = fields.TextField(attr='display_name', analyzer='french') favorites_for_users = fields.ListField(field=fields.IntegerField()) + created = fields.DateField(attr='create') # publishing date (?) class Django: model = models.Product diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index e5a249a7..7f1999a2 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -290,4 +290,5 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'grape_variety', 'establishment_detail', 'average_price', + 'created', ) From c492ec530b8c352fb589c16b2fd46ca6f9faeaf2 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 13:59:03 +0300 Subject: [PATCH 106/191] Fix typo --- apps/search_indexes/documents/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index ad99e178..853f72a2 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -149,7 +149,7 @@ class ProductDocument(Document): name_ru = fields.TextField(attr='display_name', analyzer='russian') name_fr = fields.TextField(attr='display_name', analyzer='french') favorites_for_users = fields.ListField(field=fields.IntegerField()) - created = fields.DateField(attr='create') # publishing date (?) + created = fields.DateField(attr='created') # publishing date (?) class Django: model = models.Product From b868efb137a910774e68e6187a91fc817ab5b9b4 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 14:28:01 +0300 Subject: [PATCH 107/191] order bu created --- apps/search_indexes/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 1f04a9a9..a5b952d7 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -322,9 +322,16 @@ class ProductDocumentViewSet(BaseDocumentViewSet): FilteringFilterBackend, filters.CustomSearchFilterBackend, filters.CustomFacetedSearchFilterBackend, + OrderingFilterBackend, # GeoSpatialOrderingFilterBackend, ] + ordering_fields = { + 'created': { + 'field': 'created', + }, + } + search_fields = { 'name': {'fuzziness': 'auto:2,5', 'boost': 8}, From 5ee7c314c0c961777e1dcbfe959e30cd79239ad1 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 14:31:24 +0300 Subject: [PATCH 108/191] Fix product document serialization --- apps/search_indexes/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 7f1999a2..b45bd2e3 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -135,6 +135,9 @@ class ProductEstablishmentDocumentSerializer(serializers.Serializer): index_name = serializers.CharField() city = AnotherCityDocumentShortSerializer() + def get_attribute(self, instance): + return instance.establishment if instance and instance.establishment else None + class AddressDocumentSerializer(serializers.Serializer): """Address serializer for ES Document.""" From cbe6a25d79bb463c4c5d8a3c3eddd3a6aaeeacbb Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 14:42:33 +0300 Subject: [PATCH 109/191] Fix booking --- apps/booking/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/booking/views.py b/apps/booking/views.py index c2a143a6..4dd76c65 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -34,7 +34,7 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): periods = response['periods'] periods_by_name = {period['period']: period for period in periods if 'period' in period} if not periods_by_name: - raise ValueError('Empty guestonline response') + return None period_template = iter(periods_by_name.values()).__next__().copy() period_template.pop('total_left_seats') From b3d01c8887eeda55527a407c435ef438d71c4feb Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 15:06:43 +0300 Subject: [PATCH 110/191] Fix booking #2 --- apps/booking/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/booking/views.py b/apps/booking/views.py index 4dd76c65..06ef9273 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -86,6 +86,8 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): if establishment.guestonline_id is not None \ else service.response if service else None response.update({'details': service_response}) + if service_response is None: + response['available'] = False return Response(data=response, status=200) From 6d9686b64a96f9ffe52fa40ef55afe576ea063a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 2 Dec 2019 15:12:17 +0300 Subject: [PATCH 111/191] Fix error --- apps/account/models.py | 4 +++- apps/account/serializers/back.py | 19 ++++++++++--------- apps/account/views/back.py | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index c212ffda..956baa56 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -287,7 +287,9 @@ class User(AbstractUser): class UserRole(ProjectBaseMixin): """UserRole model.""" - user = models.ForeignKey(User, verbose_name=_('User'), on_delete=models.CASCADE) + user = models.ForeignKey('account.User', + verbose_name=_('User'), + on_delete=models.CASCADE) role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.SET_NULL, null=True) establishment = models.ForeignKey(Establishment, verbose_name=_('Establishment'), on_delete=models.SET_NULL, null=True, blank=True) diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index 57c3fb42..b2316734 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -13,15 +13,6 @@ class RoleSerializer(serializers.ModelSerializer): ] -class UserRoleSerializer(serializers.ModelSerializer): - class Meta: - model = models.UserRole - fields = [ - 'user', - 'role' - ] - - class BackUserSerializer(serializers.ModelSerializer): class Meta: model = User @@ -49,3 +40,13 @@ class BackDetailUserSerializer(BackUserSerializer): user.set_password(validated_data['password']) user.save() return user + + +class UserRoleSerializer(serializers.ModelSerializer): + class Meta: + model = models.UserRole + fields = [ + 'role', + 'user', + 'establishment' + ] diff --git a/apps/account/views/back.py b/apps/account/views/back.py index b3d77d1e..80775b3a 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -13,7 +13,7 @@ class RoleLstView(generics.ListCreateAPIView): class UserRoleLstView(generics.ListCreateAPIView): serializer_class = serializers.UserRoleSerializer - queryset = models.Role.objects.all() + queryset = models.UserRole.objects.all() class UserLstView(generics.ListCreateAPIView): From a72deaec37916ed9ea462fe74075ea9a645885c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 2 Dec 2019 17:24:51 +0300 Subject: [PATCH 112/191] Site site settings --- apps/main/serializers.py | 9 ++++++++- apps/main/urls/back.py | 2 +- apps/main/views/back.py | 5 +++++ apps/main/views/web.py | 4 ++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 410eb6bb..c0bf14ad 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -108,7 +108,14 @@ class SiteSerializer(serializers.ModelSerializer): fields = [ 'subdomain', 'site_url', - 'country' + 'country', + 'default_site', + 'pinterest_page_url', + 'twitter_page_url', + 'facebook_page_url', + 'instagram_page_url', + 'contact_email', + 'currency' ] diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 40011aa2..25347434 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -9,7 +9,7 @@ urlpatterns = [ path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards//', views.AwardRUDView.as_view(), name='awards-rud'), path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'), - path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list'), + path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'), path('site-settings//', views.SiteSettingsBackOfficeView.as_view(), name='site-settings'), ] diff --git a/apps/main/views/back.py b/apps/main/views/back.py index de47825b..95ac3b24 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -44,6 +44,11 @@ class SiteSettingsBackOfficeView(SiteSettingsView): serializer_class = serializers.SiteSettingsBackOfficeSerializer +# class SiteSettingsBackRUDView(generics.RetrieveUpdateDestroyAPIView): +# """Site settings RUD View.""" +# serializer_class = serializers.SiteSettingsBackOfficeSerializer + + class SiteListBackOfficeView(SiteListView): """Site settings View.""" serializer_class = serializers.SiteBackOfficeSerializer diff --git a/apps/main/views/web.py b/apps/main/views/web.py index 86c550da..12e23649 100644 --- a/apps/main/views/web.py +++ b/apps/main/views/web.py @@ -19,7 +19,7 @@ class DetermineSiteView(generics.GenericAPIView): return Response(data={'url': url}) -class SiteSettingsView(generics.RetrieveAPIView): +class SiteSettingsView(generics.RetrieveUpdateDestroyAPIView): """Site settings View.""" lookup_field = 'subdomain' @@ -28,7 +28,7 @@ class SiteSettingsView(generics.RetrieveAPIView): serializer_class = serializers.SiteSettingsBackOfficeSerializer -class SiteListView(generics.ListAPIView): +class SiteListView(generics.ListCreateAPIView): """Site settings View.""" pagination_class = None From e02db4958afd5f2b344d67cdd7b5e76833a06844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 2 Dec 2019 17:40:01 +0300 Subject: [PATCH 113/191] Add feature serializer --- apps/main/serializers.py | 16 ++++++++++++++++ apps/main/urls/back.py | 2 ++ apps/main/views/back.py | 15 ++++++++++----- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index c0bf14ad..561a50c0 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -139,6 +139,22 @@ class SiteBackOfficeSerializer(SiteSerializer): ] +class FeatureSerializer(serializers.ModelSerializer): + """Site feature serializer.""" + + class Meta: + """Meta class.""" + + model = models.Feature + fields = ( + 'id', + 'slug', + 'priority', + 'route', + 'site_settings', + ) + + # class SiteFeatureSerializer(serializers.ModelSerializer): # """Site feature serializer.""" # diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 25347434..99e6a50f 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -12,4 +12,6 @@ urlpatterns = [ path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'), path('site-settings//', views.SiteSettingsBackOfficeView.as_view(), name='site-settings'), + path('feature/', views.FeatureBackView.as_view(), name='feature-list-create'), + path('feature//', views.FeatureRUDBackView.as_view(), name='feature-rud') ] diff --git a/apps/main/views/back.py b/apps/main/views/back.py index 95ac3b24..76c99e3d 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -39,16 +39,21 @@ class ContentTypeView(generics.ListAPIView): ) +class FeatureBackView(generics.ListCreateAPIView): + """Feature list or create View.""" + serializer_class = serializers.FeatureSerializer + + +class FeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView): + """Feature RUD View.""" + serializer_class = serializers.FeatureSerializer + + class SiteSettingsBackOfficeView(SiteSettingsView): """Site settings View.""" serializer_class = serializers.SiteSettingsBackOfficeSerializer -# class SiteSettingsBackRUDView(generics.RetrieveUpdateDestroyAPIView): -# """Site settings RUD View.""" -# serializer_class = serializers.SiteSettingsBackOfficeSerializer - - class SiteListBackOfficeView(SiteListView): """Site settings View.""" serializer_class = serializers.SiteBackOfficeSerializer From 69dbaf40dc20e16b88f9314437dd0dd52cef8b45 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Mon, 2 Dec 2019 18:05:57 +0300 Subject: [PATCH 114/191] update Dockerfile --- .dockerignore | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 9 ++-- 2 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..e2ee1d61 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,113 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +_*/ + +.git/ +.idea/ +_files/ + diff --git a/Dockerfile b/Dockerfile index 3564a933..c2b4c4dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ FROM python:3.7 ENV PYTHONUNBUFFERED 1 RUN apt-get update; apt-get --assume-yes install binutils libproj-dev gdal-bin gettext -RUN mkdir /code +RUN mkdir /code /requirements +ADD ./requirements /requirements +RUN pip install --no-cache-dir -r /requirements/base.txt && \ + pip install --no-cache-dir -r /requirements/development.txt WORKDIR /code -ADD . /code/ -RUN pip install --no-cache-dir -r /code/requirements/base.txt && \ - pip install --no-cache-dir -r /code/requirements/development.txt \ No newline at end of file +ADD . /code/ \ No newline at end of file From 3b05a552aaf477e4ba1b8cc38bfe48e8b20422c6 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 18:36:44 +0300 Subject: [PATCH 115/191] Tz field for establishments (cherry picked from commit a537f04) --- apps/establishment/models.py | 5 +++++ apps/establishment/serializers/common.py | 3 ++- apps/search_indexes/documents/establishment.py | 1 + apps/search_indexes/serializers.py | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index c5533d52..c5da4094 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -541,6 +541,11 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, time_at_est_tz = now_at_est_tz.time() return schedule_for_today.ending_time > time_at_est_tz > schedule_for_today.opening_time + @property + def timezone_as_str(self): + """ Returns tz in str format""" + return self.tz.localize(datetime.now()).strftime('%z') + @property def tags_indexing(self): return [{'id': tag.metadata.id, diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index cb102ff1..7364d02c 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -286,7 +286,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): preview_image = serializers.URLField(source='preview_image_url', allow_null=True, read_only=True) - + tz = serializers.CharField(read_only=True, source='timezone_as_str') new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) class Meta: @@ -311,6 +311,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): 'image', 'preview_image', 'new_image', + 'tz', ] diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index e53b93de..b1983b61 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -124,6 +124,7 @@ class EstablishmentDocument(Document): }, ) favorites_for_users = fields.ListField(field=fields.IntegerField()) + tz = fields.KeywordField(attr='timezone_as_str') class Django: diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index b45bd2e3..654d3354 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -251,6 +251,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'works_noon', 'works_evening', 'works_at_weekday', + 'tz', # 'works_now', # 'collections', # 'establishment_type', From ab2304a6faa527c76af22f709b6bfeb5954965fe Mon Sep 17 00:00:00 2001 From: dormantman Date: Mon, 2 Dec 2019 20:50:47 +0300 Subject: [PATCH 116/191] Fixed a bug related to inaccurate calc of the visual center of the map --- apps/search_indexes/filters.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index ec45350c..ee03c7a5 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -10,14 +10,27 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): """Automatically adds centering and sorting within bounding box.""" @staticmethod - def calculate_center(a, b): - return (a[0] + b[0]) / 2, (a[1] + b[1]) / 2 + def calculate_center(first, second): + if second[1] < 0 <= first[1]: + reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) + diff = (reverse_first + reverse_second) / 2 + + if reverse_first < reverse_second: + result_part = -180 + (180 + second[1] - diff) + + else: + result_part = 180 - (180 - first[1] - diff) + + else: + result_part = (first[1] + second[1]) / 2 + + return round((first[0] + second[0]) / 2, 2), round(result_part, 2) def filter_queryset(self, request, queryset, view): ret = super().filter_queryset(request, queryset, view) bb = request.query_params.get('location__geo_bounding_box') if bb: - center = self.calculate_center(*map(lambda p: list(map(lambda x: float(x),p.split(','))), bb.split('__'))) + center = self.calculate_center(*map(lambda point: list(map(float, point.split(','))), bb.split('__'))) request.GET._mutable = True request.query_params.update({ 'ordering': f'location__{center[0]}__{center[1]}__km' From 4d54c84b515f4fc3e17d952e07991d03b5c129a1 Mon Sep 17 00:00:00 2001 From: dormantman Date: Mon, 2 Dec 2019 20:52:32 +0300 Subject: [PATCH 117/191] Removed rounding of coordinates --- apps/search_indexes/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index ee03c7a5..7a3993c0 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -24,7 +24,7 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): else: result_part = (first[1] + second[1]) / 2 - return round((first[0] + second[0]) / 2, 2), round(result_part, 2) + return (first[0] + second[0]) / 2, result_part def filter_queryset(self, request, queryset, view): ret = super().filter_queryset(request, queryset, view) From 140ba040e7a376adc59b21176cc75bcdf3dab1c6 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 21:26:55 +0300 Subject: [PATCH 118/191] Extra fields for establishments --- apps/establishment/models.py | 10 +++++++++- apps/establishment/serializers/common.py | 4 ++++ apps/establishment/views/web.py | 4 +++- .../search_indexes/documents/establishment.py | 20 ++++++++++++++++++- apps/search_indexes/serializers.py | 4 ++++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index c5da4094..74d40921 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -14,7 +14,7 @@ from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models -from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q +from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch from django.utils import timezone from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField @@ -23,6 +23,7 @@ from timezone_field import TimeZoneField from collection.models import Collection from location.models import Address from main.models import Award, Currency +from tag.models import Tag from review.models import Review from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin, @@ -321,6 +322,13 @@ class EstablishmentQuerySet(models.QuerySet): """Exclude countries.""" return self.exclude(address__city__country__in=countries) + def with_certain_tag_category_related(self, index_name, attr_name): + """Includes extra tags.""" + return self.prefetch_related( + Prefetch('tags', queryset=Tag.objects.filter(category__index_name=index_name), + to_attr=attr_name) + ) + class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin): diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 7364d02c..6a88c5d4 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -320,12 +320,16 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer): address = AddressDetailSerializer() schedule = ScheduleRUDSerializer(many=True, allow_null=True) + restaurant_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) + restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True) class Meta(EstablishmentBaseSerializer.Meta): """Meta class.""" fields = EstablishmentBaseSerializer.Meta.fields + [ 'schedule', + 'restaurant_category', + 'restaurant_cuisine', ] diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index bd826e4e..5c73feb6 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -35,7 +35,9 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView): def get_queryset(self): return super().get_queryset().with_schedule() \ - .with_extended_address_related().with_currency_related() + .with_extended_address_related().with_currency_related() \ + .with_certain_tag_category_related('category', 'restaurant_category') \ + .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView): diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index b1983b61..5e14888c 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -49,6 +49,22 @@ class EstablishmentDocument(Document): 'value': fields.KeywordField(), }, multi=True) + restaurant_category = fields.ObjectField( + properties={ + 'id': fields.IntegerField(attr='id'), + 'label': fields.ObjectField(attr='label_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True) + restaurant_cuisine = fields.ObjectField( + properties={ + 'id': fields.IntegerField(attr='id'), + 'label': fields.ObjectField(attr='label_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True) visible_tags = fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), @@ -142,4 +158,6 @@ class EstablishmentDocument(Document): ) def get_queryset(self): - return super().get_queryset().with_es_related() + return super().get_queryset().with_es_related() \ + .with_certain_tag_category_related('category', 'restaurant_category') \ + .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 654d3354..62b5560f 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -229,6 +229,8 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): establishment_subtypes = EstablishmentTypeSerializer(many=True) address = AddressDocumentSerializer(allow_null=True) tags = TagsDocumentSerializer(many=True, source='visible_tags') + restaurant_category = TagsDocumentSerializer(many=True) + restaurant_cuisine = TagsDocumentSerializer(many=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True) class Meta: @@ -247,6 +249,8 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'preview_image', 'address', 'tags', + 'restaurant_category', + 'restaurant_cuisine', 'schedule', 'works_noon', 'works_evening', From e981b48fa0f7822e8a682a7ee7471414a750af5d Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 21:32:03 +0300 Subject: [PATCH 119/191] Fix extra tags indexing --- apps/establishment/models.py | 8 ++++++++ apps/search_indexes/documents/establishment.py | 8 +++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 74d40921..e270832b 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -602,6 +602,14 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, if qs.exists(): return qs.first().image + @property + def restaurant_category_indexing(self): + return self.tags.filter(category__index_name='category') + + @property + def restaurant_cuisine_indexing(self): + return self.tags.filter(category__index_name='cuisine') + class EstablishmentNoteQuerySet(models.QuerySet): """QuerySet for model EstablishmentNote.""" diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 5e14888c..9cfc27d3 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -56,7 +56,7 @@ class EstablishmentDocument(Document): properties=OBJECT_FIELD_PROPERTIES), 'value': fields.KeywordField(), }, - multi=True) + multi=True, attr='restaurant_category_indexing') restaurant_cuisine = fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), @@ -64,7 +64,7 @@ class EstablishmentDocument(Document): properties=OBJECT_FIELD_PROPERTIES), 'value': fields.KeywordField(), }, - multi=True) + multi=True, attr='restaurant_cuisine_indexing') visible_tags = fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), @@ -158,6 +158,4 @@ class EstablishmentDocument(Document): ) def get_queryset(self): - return super().get_queryset().with_es_related() \ - .with_certain_tag_category_related('category', 'restaurant_category') \ - .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') + return super().get_queryset().with_es_related() From e40156908bc691e015bb64f2c8ea941dde3ac9ad Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 21:38:21 +0300 Subject: [PATCH 120/191] Artisan category tags for establishments --- apps/establishment/models.py | 4 ++++ apps/establishment/serializers/common.py | 2 ++ apps/establishment/views/web.py | 1 + apps/search_indexes/documents/establishment.py | 8 ++++++++ apps/search_indexes/serializers.py | 2 ++ 5 files changed, 17 insertions(+) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index e270832b..140188d7 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -610,6 +610,10 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, def restaurant_cuisine_indexing(self): return self.tags.filter(category__index_name='cuisine') + @property + def artisan_category_indexing(self): + return self.tags.filter(category__index_name='shop_category') + class EstablishmentNoteQuerySet(models.QuerySet): """QuerySet for model EstablishmentNote.""" diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 6a88c5d4..37432a21 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -322,6 +322,7 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer): schedule = ScheduleRUDSerializer(many=True, allow_null=True) restaurant_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True) + artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) class Meta(EstablishmentBaseSerializer.Meta): """Meta class.""" @@ -330,6 +331,7 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer): 'schedule', 'restaurant_category', 'restaurant_cuisine', + 'artisan_category', ] diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 5c73feb6..cfd4880e 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -38,6 +38,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView): .with_extended_address_related().with_currency_related() \ .with_certain_tag_category_related('category', 'restaurant_category') \ .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ + .with_ceratin_tag_category_related('shop_category', 'artisan_category') class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView): diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 9cfc27d3..9e1d0a82 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -65,6 +65,14 @@ class EstablishmentDocument(Document): 'value': fields.KeywordField(), }, multi=True, attr='restaurant_cuisine_indexing') + artisan_category = fields.ObjectField( + properties={ + 'id': fields.IntegerField(attr='id'), + 'label': fields.ObjectField(attr='label_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True, attr='artisan_category_indexing') visible_tags = fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 62b5560f..1a7c7132 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -231,6 +231,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): tags = TagsDocumentSerializer(many=True, source='visible_tags') restaurant_category = TagsDocumentSerializer(many=True) restaurant_cuisine = TagsDocumentSerializer(many=True) + artisan_category = TagsDocumentSerializer(many=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True) class Meta: @@ -251,6 +252,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'tags', 'restaurant_category', 'restaurant_cuisine', + 'artisan_category', 'schedule', 'works_noon', 'works_evening', From ed28ea176731db2f12cb8941433c681db33d5e6c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 2 Dec 2019 22:10:38 +0300 Subject: [PATCH 121/191] Fix missing tags --- apps/search_indexes/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 1a7c7132..67a2bff0 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -229,9 +229,9 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): establishment_subtypes = EstablishmentTypeSerializer(many=True) address = AddressDocumentSerializer(allow_null=True) tags = TagsDocumentSerializer(many=True, source='visible_tags') - restaurant_category = TagsDocumentSerializer(many=True) - restaurant_cuisine = TagsDocumentSerializer(many=True) - artisan_category = TagsDocumentSerializer(many=True) + restaurant_category = TagsDocumentSerializer(many=True, allow_null=True) + restaurant_cuisine = TagsDocumentSerializer(many=True, allow_null=True) + artisan_category = TagsDocumentSerializer(many=True, allow_null=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True) class Meta: From b6d0815ed06bf1207d5490ea219d872ccc856e83 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Tue, 3 Dec 2019 09:59:11 +0300 Subject: [PATCH 122/191] add news author migration --- apps/news/management/commands/add_author.py | 29 +++++++++++++++++++++ apps/news/transfer_data.py | 1 + apps/transfer/serializers/news.py | 9 +++++++ docker-compose.mysql.yml | 2 +- project/settings/local.py | 3 +++ 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 apps/news/management/commands/add_author.py diff --git a/apps/news/management/commands/add_author.py b/apps/news/management/commands/add_author.py new file mode 100644 index 00000000..0313d7b6 --- /dev/null +++ b/apps/news/management/commands/add_author.py @@ -0,0 +1,29 @@ +from django.core.management.base import BaseCommand +from django.db.models import F +from tqdm import tqdm + +from account.models import User +from news.models import News +from transfer.models import PageTexts + + +class Command(BaseCommand): + help = 'Add author of News' + + def handle(self, *args, **kwargs): + count = 0 + news_list = News.objects.filter(created_by__isnull=True) + + for news in tqdm(news_list, desc="Find author for exist news"): + old_news = PageTexts.objects.filter(id=news.old_id).annotate( + account_id=F('page__account_id'), + ).first() + if old_news: + user = User.objects.filter(old_id=old_news.account_id).first() + if user: + news.created_by = user + news.modified_by = user + news.save() + count += 1 + + self.stdout.write(self.style.WARNING(f'Update {count} objects.')) diff --git a/apps/news/transfer_data.py b/apps/news/transfer_data.py index bc0d3711..33aeedfd 100644 --- a/apps/news/transfer_data.py +++ b/apps/news/transfer_data.py @@ -38,6 +38,7 @@ def transfer_news(): image=F('page__attachment_suffix_url'), template=F('page__template'), tags=GroupConcat('page__tags__id'), + account_id=F('page__account_id'), ) serialized_data = NewsSerializer(data=list(queryset.values()), many=True) diff --git a/apps/transfer/serializers/news.py b/apps/transfer/serializers/news.py index 4dc7d913..4ef03184 100644 --- a/apps/transfer/serializers/news.py +++ b/apps/transfer/serializers/news.py @@ -7,10 +7,12 @@ from tag.models import Tag from transfer.models import PageMetadata from utils.legacy_parser import parse_legacy_news_content from utils.slug_generator import generate_unique_slug +from account.models import User class NewsSerializer(serializers.Serializer): id = serializers.IntegerField() + account_id = serializers.IntegerField(allow_null=True) tag_cat_id = serializers.IntegerField() news_type_id = serializers.IntegerField() news_title = serializers.CharField() @@ -39,6 +41,8 @@ class NewsSerializer(serializers.Serializer): 'state': self.get_state(validated_data), 'template': self.get_template(validated_data), 'country': self.get_country(validated_data), + 'created_by': self.get_account(validated_data), + 'modified_by': self.get_account(validated_data), } obj = News.objects.create(**payload) @@ -126,3 +130,8 @@ class NewsSerializer(serializers.Serializer): else: content = {data['locale']: data['title']} return content + + @staticmethod + def get_account(data): + """Get account""" + return User.objects.filter(old_id=data['account_id']).first() diff --git a/docker-compose.mysql.yml b/docker-compose.mysql.yml index bd81ecb2..a8900226 100644 --- a/docker-compose.mysql.yml +++ b/docker-compose.mysql.yml @@ -5,7 +5,7 @@ services: mysql_db: image: mysql:5.7 ports: - - "3306:3306" + - "3316:3306" environment: MYSQL_DATABASE: dev MYSQL_USER: dev diff --git a/project/settings/local.py b/project/settings/local.py index c8974c40..6a592a46 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -99,3 +99,6 @@ TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} ELASTICSEARCH_DSL_AUTOSYNC = False + +# INSTALLED APPS +INSTALLED_APPS.append('transfer.apps.TransferConfig') \ No newline at end of file From fdc63f2677f34c2f5c4543fdd8f1ae6de2c2f5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Tue, 3 Dec 2019 10:20:13 +0300 Subject: [PATCH 123/191] Add API methods for site-feature --- apps/main/serializers.py | 55 +++++++++++++--------------------------- apps/main/urls/back.py | 8 +++++- apps/main/views/back.py | 10 ++++++++ 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 561a50c0..b93382d5 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -21,27 +21,6 @@ class FeatureSerializer(serializers.ModelSerializer): ) -class SiteFeatureSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(source='feature.id') - slug = serializers.CharField(source='feature.slug') - priority = serializers.IntegerField(source='feature.priority') - route = serializers.CharField(source='feature.route.name') - source = serializers.IntegerField(source='feature.source') - nested = RecursiveFieldSerializer(many=True, allow_null=True) - - class Meta: - """Meta class.""" - model = models.SiteFeature - fields = ('main', - 'id', - 'slug', - 'priority', - 'route', - 'source', - 'nested', - ) - - class CurrencySerializer(ProjectModelSerializer): """Currency serializer.""" @@ -56,6 +35,23 @@ class CurrencySerializer(ProjectModelSerializer): ] +class SiteFeatureSerializer(serializers.ModelSerializer): + """Site feature serializer.""" + + class Meta: + """Meta class.""" + + model = models.SiteFeature + fields = ( + 'id', + 'site_settings', + 'feature', + 'published', + 'main', + 'nested' + ) + + class SiteSettingsSerializer(serializers.ModelSerializer): """Site settings serializer.""" @@ -140,7 +136,7 @@ class SiteBackOfficeSerializer(SiteSerializer): class FeatureSerializer(serializers.ModelSerializer): - """Site feature serializer.""" + """Feature serializer.""" class Meta: """Meta class.""" @@ -155,21 +151,6 @@ class FeatureSerializer(serializers.ModelSerializer): ) -# class SiteFeatureSerializer(serializers.ModelSerializer): -# """Site feature serializer.""" -# -# class Meta: -# """Meta class.""" -# -# model = models.SiteFeature -# fields = ( -# 'id', -# 'published', -# 'site_settings', -# 'feature', -# ) - - class AwardBaseSerializer(serializers.ModelSerializer): """Award base serializer.""" diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 99e6a50f..609e61f7 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -13,5 +13,11 @@ urlpatterns = [ path('site-settings//', views.SiteSettingsBackOfficeView.as_view(), name='site-settings'), path('feature/', views.FeatureBackView.as_view(), name='feature-list-create'), - path('feature//', views.FeatureRUDBackView.as_view(), name='feature-rud') + path('feature//', views.FeatureRUDBackView.as_view(), name='feature-rud'), + path('site-feature/', views.SiteFeatureBackView.as_view(), + name='site-feature-list-create'), + path('site-feature//', views.SiteFeatureRUDBackView.as_view(), + name='site-feature-rud'), ] + + diff --git a/apps/main/views/back.py b/apps/main/views/back.py index 76c99e3d..adc0196a 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -44,11 +44,21 @@ class FeatureBackView(generics.ListCreateAPIView): serializer_class = serializers.FeatureSerializer +class SiteFeatureBackView(generics.ListCreateAPIView): + """Feature list or create View.""" + serializer_class = serializers.SiteFeatureSerializer + + class FeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView): """Feature RUD View.""" serializer_class = serializers.FeatureSerializer +class SiteFeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView): + """Feature RUD View.""" + serializer_class = serializers.SiteFeatureSerializer + + class SiteSettingsBackOfficeView(SiteSettingsView): """Site settings View.""" serializer_class = serializers.SiteSettingsBackOfficeSerializer From 4b9561fe752e55c851316923e8b84f11b8841295 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 3 Dec 2019 11:50:36 +0300 Subject: [PATCH 124/191] Fix typo --- apps/establishment/views/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index cfd4880e..15421ee7 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -38,7 +38,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView): .with_extended_address_related().with_currency_related() \ .with_certain_tag_category_related('category', 'restaurant_category') \ .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ - .with_ceratin_tag_category_related('shop_category', 'artisan_category') + .with_certain_tag_category_related('shop_category', 'artisan_category') class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView): From 201e02b4b794a37050769b01008372d9c186c23a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 3 Dec 2019 13:29:37 +0300 Subject: [PATCH 125/191] remove empty favs --- apps/tag/filters.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 5e2b31a7..46470ca1 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -73,7 +73,10 @@ class TagsFilterSet(TagsBaseFilterSet): def by_establishment_type(self, queryset, name, value): if value == EstablishmentType.ARTISAN: - return models.Tag.objects.by_category_index_name('shop_category')[0:8] + qs = models.Tag.objects.by_category_index_name('shop_category') + if self.request.country_code and self.request.country_code not in settings.INTERNATIONAL_COUNTRY_CODES: + qs = qs.filter(establishments__address__city__country__code=self.request.country_code).distinct('id') + return qs.exclude(establishments__isnull=True)[0:8] return queryset.by_establishment_type(value) # TMP TODO remove it later From e7de51373d99a8b3d9a31f13545488fb84c0e287 Mon Sep 17 00:00:00 2001 From: dormantman Date: Tue, 3 Dec 2019 14:39:25 +0300 Subject: [PATCH 126/191] Correction of finding the visual center of points --- apps/search_indexes/filters.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 7a3993c0..4537124d 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -11,7 +11,7 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): @staticmethod def calculate_center(first, second): - if second[1] < 0 <= first[1]: + if second[1] < 0 < first[1]: reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) diff = (reverse_first + reverse_second) / 2 @@ -21,6 +21,16 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): else: result_part = 180 - (180 - first[1] - diff) + elif second[1] < 0 > first[1]: + diff = abs(abs(second[1]) - abs(first[1])) + + if diff > 90: + reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) + result_part = (reverse_first + reverse_second) / 2 + + else: + result_part = (first[1] + second[1]) / 2 + else: result_part = (first[1] + second[1]) / 2 From 637903e6135304021d28c443e4dc8e1bfbe1a43a Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Tue, 3 Dec 2019 15:15:02 +0300 Subject: [PATCH 127/191] fix scheduler --- apps/account/serializers/common.py | 4 +++ .../management/commands/fix_scheduler.py | 36 +++++++++++++++++++ apps/transfer/serializers/establishment.py | 2 +- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 apps/establishment/management/commands/fix_scheduler.py diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index d2933747..d2ce0974 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -92,7 +92,11 @@ class UserBaseSerializer(serializers.ModelSerializer): model = models.User fields = ( + 'id', 'fullname', + 'first_name', + 'last_name', + 'email', 'cropped_image_url', 'image_url', ) diff --git a/apps/establishment/management/commands/fix_scheduler.py b/apps/establishment/management/commands/fix_scheduler.py new file mode 100644 index 00000000..78422935 --- /dev/null +++ b/apps/establishment/management/commands/fix_scheduler.py @@ -0,0 +1,36 @@ +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from establishment.models import Establishment +from transfer.models import Establishments +from transfer.serializers.establishment import EstablishmentSerializer +from timetable.models import Timetable +from django.db import transaction + + +class Command(BaseCommand): + help = 'Fix scheduler' + + @transaction.atomic + def handle(self, *args, **kwargs): + count = 0 + establishments = Establishment.objects.all() + old_est_list = Establishments.objects.prefetch_related( + 'schedules_set', + ) + # remove old records of Timetable + Timetable.objects.all().delete() + + for est in tqdm(establishments, desc="Fix scheduler"): + old_est = old_est_list.filter(id=est.old_id).first() + + if old_est and old_est.schedules_set.exists(): + old_schedule = old_est.schedules_set.first() + timetable = old_schedule.timetable + if timetable: + new_schedules = EstablishmentSerializer.get_schedules(timetable) + est.schedule.add(*new_schedules) + est.save() + count += 1 + + self.stdout.write(self.style.WARNING(f'Update {count} objects.')) diff --git a/apps/transfer/serializers/establishment.py b/apps/transfer/serializers/establishment.py index 0e5f55d4..a287d61b 100644 --- a/apps/transfer/serializers/establishment.py +++ b/apps/transfer/serializers/establishment.py @@ -125,7 +125,7 @@ class EstablishmentSerializer(serializers.ModelSerializer): weekdays = { 'su': Timetable.SUNDAY, 'mo': Timetable.MONDAY, - 'tu': Timetable.THURSDAY, + 'tu': Timetable.TUESDAY, 'we': Timetable.WEDNESDAY, 'th': Timetable.THURSDAY, 'fr': Timetable.FRIDAY, From f5d5ce922fe398d6ef68ea96614c43ee9fba66b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Tue, 3 Dec 2019 15:23:41 +0300 Subject: [PATCH 128/191] Old role migrate --- apps/account/migrations/0021_oldrole.py | 24 +++++++++++++++++++ .../migrations/0022_auto_20191203_1149.py | 18 ++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 apps/account/migrations/0021_oldrole.py create mode 100644 apps/account/migrations/0022_auto_20191203_1149.py diff --git a/apps/account/migrations/0021_oldrole.py b/apps/account/migrations/0021_oldrole.py new file mode 100644 index 00000000..9a8fd4e9 --- /dev/null +++ b/apps/account/migrations/0021_oldrole.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.7 on 2019-12-03 10:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0020_role_site'), + ] + + operations = [ + migrations.CreateModel( + name='OldRole', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('new_role', models.CharField(max_length=512, verbose_name='New role')), + ('old_role', models.CharField(max_length=512, verbose_name='Old role')), + ], + options={ + 'unique_together': {('new_role', 'old_role')}, + }, + ), + ] diff --git a/apps/account/migrations/0022_auto_20191203_1149.py b/apps/account/migrations/0022_auto_20191203_1149.py new file mode 100644 index 00000000..f6b554ba --- /dev/null +++ b/apps/account/migrations/0022_auto_20191203_1149.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-12-03 11:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0021_oldrole'), + ] + + operations = [ + migrations.AlterField( + model_name='oldrole', + name='old_role', + field=models.CharField(max_length=512, null=True, verbose_name='Old role'), + ), + ] From 5ecb5d214b51df9c8058edc1995099096db2e0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Tue, 3 Dec 2019 15:24:02 +0300 Subject: [PATCH 129/191] Old role migrate1 --- .../management/commands/add_affilations.py | 45 +++++++++++++++++++ apps/account/models.py | 8 ++++ 2 files changed, 53 insertions(+) create mode 100644 apps/account/management/commands/add_affilations.py diff --git a/apps/account/management/commands/add_affilations.py b/apps/account/management/commands/add_affilations.py new file mode 100644 index 00000000..4485d6f1 --- /dev/null +++ b/apps/account/management/commands/add_affilations.py @@ -0,0 +1,45 @@ +from account.models import OldRole +from django.core.management.base import BaseCommand +from django.db import connections +from establishment.management.commands.add_position import namedtuplefetchall +from tqdm import tqdm + + +class Command(BaseCommand): + help = '''Add site affilations from old db to new db. + Run after migrate {}!!!''' + + def map_role_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select distinct + case when role = 'news_editor' then 'CONTENT_PAGE_MANAGER' + when role in ('reviewer', 'reviwer', 'reviewer_manager') then 'REVIEWER_MANGER' + when role = 'admin' then 'SUPERUSER' + when role ='community_manager' then 'COUNTRY_ADMIN' + when role = 'site_admin' then 'COUNTRY_ADMIN' + when role = 'wine_reviewer' then 'WINERY_REVIEWER' + when role in ('salesman', 'sales_man') then 'SALES_MAN' + when role = 'seller' then 'SELLER' + else role + end as new_role, + case when role = 'GUEST' then null else role end as role + from + ( + SELECT + DISTINCT + COALESCE(role, 'GUEST') as role + FROM site_affiliations AS sa + ) t + ''') + return namedtuplefetchall(cursor) + + def handle(self, *args, **kwargs): + objects = [] + OldRole.objects.all().delete() + for s in tqdm(self.map_role_sql(), desc='Add permissions old'): + objects.append( + OldRole(new_role=s.new_role, old_role=s.role) + ) + OldRole.objects.bulk_create(objects) + self.stdout.write(self.style.WARNING(f'Migrated old roles.')) \ No newline at end of file diff --git a/apps/account/models.py b/apps/account/models.py index c212ffda..d422b772 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -291,3 +291,11 @@ class UserRole(ProjectBaseMixin): role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.SET_NULL, null=True) establishment = models.ForeignKey(Establishment, verbose_name=_('Establishment'), on_delete=models.SET_NULL, null=True, blank=True) + + +class OldRole(models.Model): + new_role = models.CharField(verbose_name=_('New role'), max_length=512) + old_role = models.CharField(verbose_name=_('Old role'), max_length=512, null=True) + + class Meta: + unique_together = ('new_role', 'old_role') \ No newline at end of file From 4940a4472db324a19faa01aa7a0be04ef575ee3e Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Tue, 3 Dec 2019 15:54:15 +0300 Subject: [PATCH 130/191] add additional fields to UserBaseSerializer --- apps/account/serializers/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index d2ce0974..09136934 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -93,6 +93,7 @@ class UserBaseSerializer(serializers.ModelSerializer): model = models.User fields = ( 'id', + 'username', 'fullname', 'first_name', 'last_name', From 4797225beb75f8d19b34fb838c2d371537bd5805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Tue, 3 Dec 2019 16:40:50 +0300 Subject: [PATCH 131/191] model --- apps/account/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/account/models.py b/apps/account/models.py index d422b772..0a3cd3a6 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -32,6 +32,9 @@ class Role(ProjectBaseMixin): ESTABLISHMENT_MANAGER = 5 REVIEWER_MANGER = 6 RESTAURANT_REVIEWER = 7 + SALES_MAN = 8 + WINERY_REVIEWER = 9 + SELLER = 10 ROLE_CHOICES = ( (STANDARD_USER, 'Standard user'), @@ -40,7 +43,10 @@ class Role(ProjectBaseMixin): (CONTENT_PAGE_MANAGER, 'Content page manager'), (ESTABLISHMENT_MANAGER, 'Establishment manager'), (REVIEWER_MANGER, 'Reviewer manager'), - (RESTAURANT_REVIEWER, 'Restaurant reviewer') + (RESTAURANT_REVIEWER, 'Restaurant reviewer'), + (SALES_MAN, 'Sales man'), + (WINERY_REVIEWER, 'Winery reviewer'), + (SELLER, 'Seller') ) role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES, null=False, blank=False) From fee8a3b1209e165e988975e9a21c5e04842d93a0 Mon Sep 17 00:00:00 2001 From: dormantman Date: Tue, 3 Dec 2019 17:16:02 +0300 Subject: [PATCH 132/191] Correction of finding the center of points with positive --- apps/search_indexes/filters.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 4537124d..ce77bc06 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -21,15 +21,9 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): else: result_part = 180 - (180 - first[1] - diff) - elif second[1] < 0 > first[1]: - diff = abs(abs(second[1]) - abs(first[1])) - - if diff > 90: - reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) - result_part = (reverse_first + reverse_second) / 2 - - else: - result_part = (first[1] + second[1]) / 2 + elif second[1] < 0 > first[1] or second[1] > 0 < first[1]: + reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) + result_part = ((reverse_first + reverse_second) / 2) * (-1 + (second[1] < 0) * 2) else: result_part = (first[1] + second[1]) / 2 From cc7754a9d78cba712fad35afdd4581c7289e8fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Tue, 3 Dec 2019 17:58:46 +0300 Subject: [PATCH 133/191] Old role migrate1 --- .../management/commands/add_affilations.py | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/apps/account/management/commands/add_affilations.py b/apps/account/management/commands/add_affilations.py index 4485d6f1..550847f2 100644 --- a/apps/account/management/commands/add_affilations.py +++ b/apps/account/management/commands/add_affilations.py @@ -1,4 +1,5 @@ -from account.models import OldRole +from account.models import OldRole, Role +from main.models import SiteSettings from django.core.management.base import BaseCommand from django.db import connections from establishment.management.commands.add_position import namedtuplefetchall @@ -34,12 +35,51 @@ class Command(BaseCommand): ''') return namedtuplefetchall(cursor) - def handle(self, *args, **kwargs): + def add_old_roles(self): objects = [] OldRole.objects.all().delete() for s in tqdm(self.map_role_sql(), desc='Add permissions old'): - objects.append( - OldRole(new_role=s.new_role, old_role=s.role) - ) + objects.append( + OldRole(new_role=s.new_role, old_role=s.role) + ) OldRole.objects.bulk_create(objects) - self.stdout.write(self.style.WARNING(f'Migrated old roles.')) \ No newline at end of file + self.stdout.write(self.style.WARNING(f'Migrated old roles.')) + + def site_role_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select site_id, + role + from + ( + SELECT + DISTINCT + site_id, + COALESCE(role, 'GUEST') as role + FROM site_affiliations AS sa + ) t + where t.role not in ('admin', 'GUEST') + ''') + return namedtuplefetchall(cursor) + + def add_site_role(self): + objects = [] + for s in tqdm(self.site_role_sql(), desc='Add site role'): + old_role = OldRole.objects.get(old_role=s.role) + role_choice = getattr(Role, old_role.new_role) + sites = SiteSettings.objects.filter(old_id=s.site_id) + for site in sites: + role = Role.objects.filter(site=site, role=role_choice) + if not role.exists(): + objects.append( + Role(site=site, role=role_choice) + ) + + Role.objects.bulk_create(objects) + self.stdout.write(self.style.WARNING(f'Added site roles.')) + + + + def handle(self, *args, **kwargs): + # self.add_old_roles() + self.add_site_role() \ No newline at end of file From 7cf34f0741738ac03c16980e001c46adc38f1668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Tue, 3 Dec 2019 18:30:33 +0300 Subject: [PATCH 134/191] fix --- apps/main/serializers.py | 69 ++++++++++++---------------------------- apps/main/views/back.py | 4 +-- 2 files changed, 23 insertions(+), 50 deletions(-) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index b93382d5..a41a643f 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -17,10 +17,11 @@ class FeatureSerializer(serializers.ModelSerializer): fields = ( 'id', 'slug', - 'priority' + 'priority', + 'route', + 'site_settings', ) - class CurrencySerializer(ProjectModelSerializer): """Currency serializer.""" @@ -36,20 +37,24 @@ class CurrencySerializer(ProjectModelSerializer): class SiteFeatureSerializer(serializers.ModelSerializer): - """Site feature serializer.""" + id = serializers.IntegerField(source='feature.id') + slug = serializers.CharField(source='feature.slug') + priority = serializers.IntegerField(source='feature.priority') + route = serializers.CharField(source='feature.route.name') + source = serializers.IntegerField(source='feature.source') + nested = RecursiveFieldSerializer(many=True, allow_null=True) class Meta: """Meta class.""" - model = models.SiteFeature - fields = ( - 'id', - 'site_settings', - 'feature', - 'published', - 'main', - 'nested' - ) + fields = ('main', + 'id', + 'slug', + 'priority', + 'route', + 'source', + 'nested', + ) class SiteSettingsSerializer(serializers.ModelSerializer): @@ -95,23 +100,15 @@ class SiteSettingsBackOfficeSerializer(SiteSettingsSerializer): ] -class SiteSerializer(serializers.ModelSerializer): +class SiteSerializer(SiteSettingsSerializer): country = CountrySerializer() class Meta: """Meta class.""" model = models.SiteSettings - fields = [ - 'subdomain', - 'site_url', - 'country', - 'default_site', - 'pinterest_page_url', - 'twitter_page_url', - 'facebook_page_url', - 'instagram_page_url', - 'contact_email', - 'currency' + fields = SiteSettingsSerializer.Meta.fields + [ + 'id', + 'country' ] @@ -125,30 +122,6 @@ class SiteShortSerializer(serializers.ModelSerializer): ] -class SiteBackOfficeSerializer(SiteSerializer): - """Serializer for back office.""" - - class Meta(SiteSerializer.Meta): - """Meta class.""" - fields = SiteSerializer.Meta.fields + [ - 'id', - ] - - -class FeatureSerializer(serializers.ModelSerializer): - """Feature serializer.""" - - class Meta: - """Meta class.""" - - model = models.Feature - fields = ( - 'id', - 'slug', - 'priority', - 'route', - 'site_settings', - ) class AwardBaseSerializer(serializers.ModelSerializer): diff --git a/apps/main/views/back.py b/apps/main/views/back.py index adc0196a..1faf79a0 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -61,9 +61,9 @@ class SiteFeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView): class SiteSettingsBackOfficeView(SiteSettingsView): """Site settings View.""" - serializer_class = serializers.SiteSettingsBackOfficeSerializer + serializer_class = serializers.SiteSerializer class SiteListBackOfficeView(SiteListView): """Site settings View.""" - serializer_class = serializers.SiteBackOfficeSerializer + serializer_class = serializers.SiteSerializer From 848008cae13c05b15b04cebd3540174529487584 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 3 Dec 2019 19:02:41 +0300 Subject: [PATCH 135/191] tags dynamic filters --- apps/search_indexes/filters.py | 122 ++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 23 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index ce77bc06..3101fdc2 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -4,6 +4,7 @@ from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, \ FacetedSearchFilterBackend, GeoSpatialFilteringFilterBackend from search_indexes.utils import OBJECT_FIELD_PROPERTIES from six import iteritems +from functools import reduce class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): @@ -56,10 +57,30 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): :param view: :return: """ - def makefilter(cur_facet): - def myfilter(x): + def make_filter(cur_facet): + def _filter(x): return cur_facet['facet']._params['field'] != next(iter(x._params)) - return myfilter + return _filter + + def make_tags_filter(cur_facet, tags_to_remove_ids): + def _filter(x): + if hasattr(x, '_params') and (x._params.get('must') or x._params.get('should')): + ret = [] + for t in ['must', 'should']: + terms = x._params.get(t) + if terms: + for term in terms: + if cur_facet['facet']._params['field'] != next(iter(term._params)): + return True # different fields. preserve filter + else: + ret.append(next(iter(term._params.values())) not in tags_to_remove_ids) + return all(ret) + if cur_facet['facet']._params['field'] != next(iter(x._params)): + return True # different fields. preserve filter + else: + return next(iter(x._params.values())) not in tags_to_remove_ids + return _filter + __facets = self.construct_facets(request, view) setattr(view.paginator, 'facets_computed', {}) for __field, __facet in iteritems(__facets): @@ -71,29 +92,84 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): 'global' ).bucket(__field, agg) else: - qs = queryset.__copy__() - qs.query = queryset.query._clone() - filterer = makefilter(__facet) - for param_type in ['must', 'must_not', 'should']: - if qs.query._proxied._params.get(param_type): - qs.query._proxied._params[param_type] = list( - filter( - filterer, qs.query._proxied._params[param_type] + if __field != 'tag' or not request.query_params.getlist('tags_id__in'): + qs = queryset.__copy__() + qs.query = queryset.query._clone() + filterer = make_filter(__facet) + for param_type in ['must', 'must_not', 'should']: + if qs.query._proxied._params.get(param_type): + qs.query._proxied._params[param_type] = list( + filter( + filterer, qs.query._proxied._params[param_type] + ) ) - ) - sh = qs.query._proxied._params.get('should') - if (not sh or not len(sh)) \ - and qs.query._proxied._params.get('minimum_should_match'): - qs.query._proxied._params.pop('minimum_should_match') - facet_name = '_filter_' + __field - qs.aggs.bucket( - facet_name, - 'filter', - filter=agg_filter - ).bucket(__field, agg) - view.paginator.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]}) + sh = qs.query._proxied._params.get('should') + if (not sh or not len(sh)) \ + and qs.query._proxied._params.get('minimum_should_match'): + qs.query._proxied._params.pop('minimum_should_match') + facet_name = '_filter_' + __field + qs.aggs.bucket( + facet_name, + 'filter', + filter=agg_filter + ).bucket(__field, agg) + view.paginator.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]}) + else: + tag_facets = [] + facet_name = '_filter_' + __field + for category_tags_ids in request.query_params.getlist('tags_id__in'): + tags_to_remove = category_tags_ids.split('__') + qs = queryset.__copy__() + qs.query = queryset.query._clone() + filterer = make_tags_filter(__facet, tags_to_remove) + for param_type in ['must', 'should']: + if qs.query._proxied._params.get(param_type): + if qs.query._proxied._params.get(param_type): + qs.query._proxied._params[param_type] = list( + filter( + filterer, qs.query._proxied._params[param_type] + ) + ) + sh = qs.query._proxied._params.get('should') + if (not sh or not len(sh)) \ + and qs.query._proxied._params.get('minimum_should_match'): + qs.query._proxied._params.pop('minimum_should_match') + qs.aggs.bucket( + facet_name, + 'filter', + filter=agg_filter + ).bucket(__field, agg) + tag_facets.append(qs.execute().aggregations[facet_name]) + view.paginator.facets_computed.update({facet_name: self.merge_buckets(tag_facets)}) return queryset + @staticmethod + def merge_buckets(buckets: list): + """Reduces all buckets preserving class""" + result_bucket = buckets[0] + for bucket in buckets[1:]: + for tag in bucket.tag.buckets._l_: + if tag not in result_bucket.tag.buckets._l_: + result_bucket.tag.buckets._l_.append(tag) + def reducer(prev, cur): + try: + index = list(map(lambda x: x['key'], prev)).index(cur['key']) + if cur['doc_count'] < prev[index]['doc_count']: + prev[index]['doc_count'] = cur['doc_count'] + except ValueError: + prev.append(cur) + return prev + + result_bucket.tag.buckets._l_ = list(reduce( + reducer, result_bucket.tag.buckets._l_, [] + )) + result_bucket.doc_count = reduce( + lambda prev, cur: prev + cur['doc_count'], + result_bucket.tag.buckets._l_, + 0 + ) + return result_bucket + class CustomSearchFilterBackend(SearchFilterBackend): """Custom SearchFilterBackend.""" From 7596652a3f3d5944fec5528baa991e836f58fd95 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 3 Dec 2019 19:31:05 +0300 Subject: [PATCH 136/191] category tag tags filter by obj type --- apps/tag/serializers.py | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index 2cc818a9..5b6f390c 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -1,9 +1,12 @@ """Tag serializers.""" from rest_framework import serializers +from rest_framework.fields import SerializerMethodField + from establishment.models import (Establishment, EstablishmentType, EstablishmentSubType) from news.models import News, NewsType from tag import models +from tag.models import Tag from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound, RemovedBindingObjectNotFound) from utils.serializers import TranslatedField @@ -12,6 +15,9 @@ from utils.serializers import TranslatedField class TagBaseSerializer(serializers.ModelSerializer): """Serializer for model Tag.""" + def get_extra_kwargs(self): + return super().get_extra_kwargs() + label_translated = TranslatedField() index_name = serializers.CharField(source='value', read_only=True, allow_null=True) @@ -37,6 +43,7 @@ class TagBackOfficeSerializer(TagBaseSerializer): 'category' ) + class TagCategoryProductSerializer(serializers.ModelSerializer): """SHORT Serializer for TagCategory""" @@ -57,7 +64,7 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer): """Serializer for model TagCategory.""" label_translated = TranslatedField() - tags = TagBaseSerializer(many=True, read_only=True) + tags = SerializerMethodField() class Meta: """Meta class.""" @@ -70,6 +77,22 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer): 'tags', ) + def get_tags(self, obj): + query_params = dict(self.context['request'].query_params) + if len(query_params) > 1: + return None + + tags = Tag.objects.all() + + if 'establishment_type' in query_params: + types = query_params['establishment_type'] + tags = tags.filter(establishments__establishment_type__index_name__in=types).distinct() + elif 'product_type' in query_params: + types = query_params['product_type'] + tags = tags.filter(products__product_type__index_name__in=types).distinct() + + return TagBaseSerializer(instance=tags, many=True, read_only=True).data + class TagCategoryShortSerializer(serializers.ModelSerializer): """Serializer for model TagCategory.""" @@ -174,15 +197,15 @@ class TagCategoryBindObjectSerializer(serializers.Serializer): attrs['tag_category'] = tag_category if obj_type == self.ESTABLISHMENT_TYPE: - establishment_type = EstablishmentType.objects.filter(pk=obj_id).\ + establishment_type = EstablishmentType.objects.filter(pk=obj_id). \ first() if not establishment_type: raise BindingObjectNotFound() - if request.method == 'POST' and tag_category.establishment_types.\ + if request.method == 'POST' and tag_category.establishment_types. \ filter(pk=establishment_type.pk).exists(): raise ObjectAlreadyAdded() - if request.method == 'DELETE' and not tag_category.\ - establishment_types.filter(pk=establishment_type.pk).\ + if request.method == 'DELETE' and not tag_category. \ + establishment_types.filter(pk=establishment_type.pk). \ exists(): raise RemovedBindingObjectNotFound() attrs['related_object'] = establishment_type @@ -190,10 +213,10 @@ class TagCategoryBindObjectSerializer(serializers.Serializer): news_type = NewsType.objects.filter(pk=obj_id).first() if not news_type: raise BindingObjectNotFound() - if request.method == 'POST' and tag_category.news_types.\ + if request.method == 'POST' and tag_category.news_types. \ filter(pk=news_type.pk).exists(): raise ObjectAlreadyAdded() - if request.method == 'DELETE' and not tag_category.news_types.\ + if request.method == 'DELETE' and not tag_category.news_types. \ filter(pk=news_type.pk).exists(): raise RemovedBindingObjectNotFound() attrs['related_object'] = news_type From c00075feec564c87e13cdb6fbe8da998c89c45dd Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 3 Dec 2019 19:39:20 +0300 Subject: [PATCH 137/191] empty booking response --- apps/booking/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/booking/views.py b/apps/booking/views.py index 06ef9273..b3f85bff 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -34,7 +34,7 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): periods = response['periods'] periods_by_name = {period['period']: period for period in periods if 'period' in period} if not periods_by_name: - return None + return response period_template = iter(periods_by_name.values()).__next__().copy() period_template.pop('total_left_seats') From 013cd864c29d621a1e25cddb821d384e5fa6a935 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 3 Dec 2019 20:48:35 +0300 Subject: [PATCH 138/191] tags dynamic filters #2 --- apps/search_indexes/filters.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 3101fdc2..a12c37f7 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -149,25 +149,23 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): result_bucket = buckets[0] for bucket in buckets[1:]: for tag in bucket.tag.buckets._l_: - if tag not in result_bucket.tag.buckets._l_: - result_bucket.tag.buckets._l_.append(tag) + result_bucket.tag.buckets.append(tag) + def reducer(prev, cur): - try: - index = list(map(lambda x: x['key'], prev)).index(cur['key']) - if cur['doc_count'] < prev[index]['doc_count']: - prev[index]['doc_count'] = cur['doc_count'] - except ValueError: + """Unique by key""" + if not len(list(filter(lambda x: x['key'] == cur['key'], prev))): prev.append(cur) return prev - result_bucket.tag.buckets._l_ = list(reduce( - reducer, result_bucket.tag.buckets._l_, [] - )) - result_bucket.doc_count = reduce( - lambda prev, cur: prev + cur['doc_count'], + buckets_count = len(buckets) + result_bucket.tag.buckets = list(filter(lambda t: t is not None, [ + tag if len(list(filter(lambda t: t['key'] == tag['key'], result_bucket.tag.buckets._l_))) == buckets_count else None + for tag in result_bucket.tag.buckets._l_])) # here we remove tags which don't present in any bucket + result_bucket.tag.buckets = list(reduce( + reducer, result_bucket.tag.buckets._l_, - 0 - ) + [] + )) return result_bucket From 9c84842c111fd5a65a970369f36ad34ed2d6344c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 3 Dec 2019 22:35:38 +0300 Subject: [PATCH 139/191] tags dynamic filters #3 --- apps/search_indexes/filters.py | 37 ++++++++++++---------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index a12c37f7..ec67fe7a 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -4,7 +4,7 @@ from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, \ FacetedSearchFilterBackend, GeoSpatialFilteringFilterBackend from search_indexes.utils import OBJECT_FIELD_PROPERTIES from six import iteritems -from functools import reduce +from tag.models import TagCategory class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): @@ -92,7 +92,7 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): 'global' ).bucket(__field, agg) else: - if __field != 'tag' or not request.query_params.getlist('tags_id__in'): + if __field != 'tag': qs = queryset.__copy__() qs.query = queryset.query._clone() filterer = make_filter(__facet) @@ -116,9 +116,11 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): view.paginator.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]}) else: tag_facets = [] + preserve_ids = [] facet_name = '_filter_' + __field - for category_tags_ids in request.query_params.getlist('tags_id__in'): - tags_to_remove = category_tags_ids.split('__') + for category in TagCategory.objects.prefetch_related('tags').filter(public=True, + value_type=TagCategory.LIST): + tags_to_remove = list(map(lambda t: t.id, category.tags.all())) qs = queryset.__copy__() qs.query = queryset.query._clone() filterer = make_tags_filter(__facet, tags_to_remove) @@ -140,32 +142,19 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): filter=agg_filter ).bucket(__field, agg) tag_facets.append(qs.execute().aggregations[facet_name]) - view.paginator.facets_computed.update({facet_name: self.merge_buckets(tag_facets)}) + preserve_ids.append(tags_to_remove) + view.paginator.facets_computed.update({facet_name: self.merge_buckets(tag_facets, preserve_ids)}) return queryset @staticmethod - def merge_buckets(buckets: list): + def merge_buckets(buckets: list, presrve_ids: list): """Reduces all buckets preserving class""" result_bucket = buckets[0] - for bucket in buckets[1:]: + result_bucket.tag.buckets = list(filter(lambda x: x in presrve_ids[0], result_bucket.tag.buckets._l_)) + for bucket, ids in list(zip(buckets, presrve_ids))[1:]: for tag in bucket.tag.buckets._l_: - result_bucket.tag.buckets.append(tag) - - def reducer(prev, cur): - """Unique by key""" - if not len(list(filter(lambda x: x['key'] == cur['key'], prev))): - prev.append(cur) - return prev - - buckets_count = len(buckets) - result_bucket.tag.buckets = list(filter(lambda t: t is not None, [ - tag if len(list(filter(lambda t: t['key'] == tag['key'], result_bucket.tag.buckets._l_))) == buckets_count else None - for tag in result_bucket.tag.buckets._l_])) # here we remove tags which don't present in any bucket - result_bucket.tag.buckets = list(reduce( - reducer, - result_bucket.tag.buckets._l_, - [] - )) + if tag['key'] in ids: + result_bucket.tag.buckets.append(tag) return result_bucket From eaf5c41e1db19fbfc60246f2eef04a55be0b8b3b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 3 Dec 2019 22:53:31 +0300 Subject: [PATCH 140/191] tags dynamic filters #4 --- apps/search_indexes/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index ec67fe7a..8040e4b4 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -150,7 +150,7 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): def merge_buckets(buckets: list, presrve_ids: list): """Reduces all buckets preserving class""" result_bucket = buckets[0] - result_bucket.tag.buckets = list(filter(lambda x: x in presrve_ids[0], result_bucket.tag.buckets._l_)) + result_bucket.tag.buckets = list(filter(lambda x: x['key'] in presrve_ids[0], result_bucket.tag.buckets._l_)) for bucket, ids in list(zip(buckets, presrve_ids))[1:]: for tag in bucket.tag.buckets._l_: if tag['key'] in ids: From f9fb2aa17ea661f7a1aae97872d624b9f8e0bd5b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 3 Dec 2019 23:49:45 +0300 Subject: [PATCH 141/191] tags dynamic filters #4 --- apps/search_indexes/filters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 8040e4b4..bc48b029 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -120,7 +120,7 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): facet_name = '_filter_' + __field for category in TagCategory.objects.prefetch_related('tags').filter(public=True, value_type=TagCategory.LIST): - tags_to_remove = list(map(lambda t: t.id, category.tags.all())) + tags_to_remove = list(map(lambda t: str(t.id), category.tags.all())) qs = queryset.__copy__() qs.query = queryset.query._clone() filterer = make_tags_filter(__facet, tags_to_remove) @@ -142,16 +142,16 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): filter=agg_filter ).bucket(__field, agg) tag_facets.append(qs.execute().aggregations[facet_name]) - preserve_ids.append(tags_to_remove) + preserve_ids.append(list(map(int, tags_to_remove))) view.paginator.facets_computed.update({facet_name: self.merge_buckets(tag_facets, preserve_ids)}) return queryset @staticmethod - def merge_buckets(buckets: list, presrve_ids: list): + def merge_buckets(buckets: list, preserve_ids: list): """Reduces all buckets preserving class""" result_bucket = buckets[0] - result_bucket.tag.buckets = list(filter(lambda x: x['key'] in presrve_ids[0], result_bucket.tag.buckets._l_)) - for bucket, ids in list(zip(buckets, presrve_ids))[1:]: + result_bucket.tag.buckets = list(filter(lambda x: x['key'] in preserve_ids[0], result_bucket.tag.buckets._l_)) + for bucket, ids in list(zip(buckets, preserve_ids))[1:]: for tag in bucket.tag.buckets._l_: if tag['key'] in ids: result_bucket.tag.buckets.append(tag) From 9f882ca65643aa0455b2892737f89588f51d2107 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 00:33:18 +0300 Subject: [PATCH 142/191] tags dynamic filters #4 (boost search via ES) --- apps/search_indexes/documents/__init__.py | 2 ++ apps/search_indexes/documents/tag_category.py | 33 +++++++++++++++++++ apps/search_indexes/filters.py | 9 +++-- project/settings/development.py | 1 + project/settings/local.py | 1 + project/settings/production.py | 1 + project/settings/stage.py | 1 + 7 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 apps/search_indexes/documents/tag_category.py diff --git a/apps/search_indexes/documents/__init__.py b/apps/search_indexes/documents/__init__.py index c357f29e..60a9b580 100644 --- a/apps/search_indexes/documents/__init__.py +++ b/apps/search_indexes/documents/__init__.py @@ -1,6 +1,7 @@ from search_indexes.documents.establishment import EstablishmentDocument from search_indexes.documents.news import NewsDocument from search_indexes.documents.product import ProductDocument +from search_indexes.documents.tag_category import TagCategoryDocument from search_indexes.tasks import es_update # todo: make signal to update documents on related fields @@ -8,5 +9,6 @@ __all__ = [ 'EstablishmentDocument', 'NewsDocument', 'ProductDocument', + 'TagCategoryDocument', 'es_update', ] \ No newline at end of file diff --git a/apps/search_indexes/documents/tag_category.py b/apps/search_indexes/documents/tag_category.py new file mode 100644 index 00000000..ed69c8b7 --- /dev/null +++ b/apps/search_indexes/documents/tag_category.py @@ -0,0 +1,33 @@ +"""Product app documents.""" +from django.conf import settings +from django_elasticsearch_dsl import Document, Index, fields +from tag import models + +TagCategoryIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'tag_category')) +TagCategoryIndex.settings(number_of_shards=2, number_of_replicas=2) + + +@TagCategoryIndex.doc_type +class TagCategoryDocument(Document): + """TagCategory document.""" + + tags = fields.ListField(fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'value': fields.KeywordField(), + }, + )) + + class Django: + model = models.TagCategory + fields = ( + 'id', + 'index_name', + 'public', + 'value_type' + ) + related_models = [models.Tag] + + + def get_queryset(self): + return super().get_queryset().with_base_related() diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index bc48b029..27df0699 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -4,6 +4,7 @@ from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, \ FacetedSearchFilterBackend, GeoSpatialFilteringFilterBackend from search_indexes.utils import OBJECT_FIELD_PROPERTIES from six import iteritems +from search_indexes.documents import TagCategoryDocument from tag.models import TagCategory @@ -118,9 +119,11 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): tag_facets = [] preserve_ids = [] facet_name = '_filter_' + __field - for category in TagCategory.objects.prefetch_related('tags').filter(public=True, - value_type=TagCategory.LIST): - tags_to_remove = list(map(lambda t: str(t.id), category.tags.all())) + all_tag_categories = TagCategoryDocument.search() \ + .filter('term', public=True) \ + .filter('term', value_type=TagCategory.LIST) + for category in all_tag_categories: + tags_to_remove = list(map(lambda t: str(t.id), category.tags)) qs = queryset.__copy__() qs.query = queryset.query._clone() filterer = make_tags_filter(__facet, tags_to_remove) diff --git a/project/settings/development.py b/project/settings/development.py index f850aad7..88b88789 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -42,6 +42,7 @@ ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.news': 'development_news', 'search_indexes.documents.establishment': 'development_establishment', 'search_indexes.documents.product': 'development_product', + 'search_indexes.documents.tag_category': 'development_tag_category', } # ELASTICSEARCH_DSL_AUTOSYNC = False diff --git a/project/settings/local.py b/project/settings/local.py index 6a592a46..644098bb 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -92,6 +92,7 @@ ELASTICSEARCH_INDEX_NAMES = { # 'search_indexes.documents.news': 'local_news', 'search_indexes.documents.establishment': 'local_establishment', 'search_indexes.documents.product': 'local_product', + 'search_indexes.documents.tag_category': 'local_tag_category', } ELASTICSEARCH_DSL_AUTOSYNC = False diff --git a/project/settings/production.py b/project/settings/production.py index 7ef2dc62..3020c6f0 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -36,6 +36,7 @@ ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.news': 'development_news', # temporarily disabled 'search_indexes.documents.establishment': 'development_establishment', 'search_indexes.documents.product': 'development_product', + 'search_indexes.documents.tag_category': 'development_tag_category', } sentry_sdk.init( diff --git a/project/settings/stage.py b/project/settings/stage.py index 95285034..dbc6844a 100644 --- a/project/settings/stage.py +++ b/project/settings/stage.py @@ -23,6 +23,7 @@ ELASTICSEARCH_DSL = { ELASTICSEARCH_INDEX_NAMES = { # 'search_indexes.documents.news': 'stage_news', #temporarily disabled 'search_indexes.documents.establishment': 'stage_establishment', + 'search_indexes.documents.tag_category': 'stage_tag_category', } COOKIE_DOMAIN = '.id-east.ru' From f5079940148d135ba361a0dbc084a3aa31588075 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 00:49:46 +0300 Subject: [PATCH 143/191] tags dynamic filters #5 (add wine-colors) --- apps/search_indexes/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 27df0699..644b47fd 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -121,7 +121,7 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): facet_name = '_filter_' + __field all_tag_categories = TagCategoryDocument.search() \ .filter('term', public=True) \ - .filter('term', value_type=TagCategory.LIST) + .filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color')) for category in all_tag_categories: tags_to_remove = list(map(lambda t: str(t.id), category.tags)) qs = queryset.__copy__() From 7590a73c5de31145658c6c114a8bce8ab91ad2b7 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 4 Dec 2019 09:23:21 +0300 Subject: [PATCH 144/191] tags fix mtm --- apps/tag/models.py | 2 +- apps/tag/serializers.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/tag/models.py b/apps/tag/models.py index 12517815..b718d83c 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -47,7 +47,7 @@ class Tag(TranslatedFieldsMixin, models.Model): old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'), - blank=True, null=True, default=None) + blank=True, null=True, default=None) objects = TagQuerySet.as_manager() diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index 5b6f390c..eb73291f 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -2,11 +2,9 @@ from rest_framework import serializers from rest_framework.fields import SerializerMethodField -from establishment.models import (Establishment, EstablishmentType, - EstablishmentSubType) +from establishment.models import (Establishment, EstablishmentType) from news.models import News, NewsType from tag import models -from tag.models import Tag from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound, RemovedBindingObjectNotFound) from utils.serializers import TranslatedField @@ -79,18 +77,21 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer): def get_tags(self, obj): query_params = dict(self.context['request'].query_params) + if len(query_params) > 1: - return None - - tags = Tag.objects.all() + return [] + params = {} if 'establishment_type' in query_params: - types = query_params['establishment_type'] - tags = tags.filter(establishments__establishment_type__index_name__in=types).distinct() + params = { + 'establishments__isnull': False, + } elif 'product_type' in query_params: - types = query_params['product_type'] - tags = tags.filter(products__product_type__index_name__in=types).distinct() + params = { + 'products__isnull': False, + } + tags = obj.tags.filter(**params).distinct() return TagBaseSerializer(instance=tags, many=True, read_only=True).data From 96e08a0900cd492669e3e3ab3a629b6738649731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Wed, 4 Dec 2019 12:24:23 +0300 Subject: [PATCH 145/191] Migrate complete --- .../management/commands/add_affilations.py | 77 +++++++++++++++++-- .../migrations/0023_auto_20191204_0916.py | 22 ++++++ apps/account/models.py | 2 + 3 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 apps/account/migrations/0023_auto_20191204_0916.py diff --git a/apps/account/management/commands/add_affilations.py b/apps/account/management/commands/add_affilations.py index 550847f2..591e676d 100644 --- a/apps/account/management/commands/add_affilations.py +++ b/apps/account/management/commands/add_affilations.py @@ -1,14 +1,15 @@ -from account.models import OldRole, Role +from account.models import OldRole, Role, User, UserRole from main.models import SiteSettings from django.core.management.base import BaseCommand -from django.db import connections +from django.db import connections, transaction +from django.db.models import Prefetch from establishment.management.commands.add_position import namedtuplefetchall from tqdm import tqdm class Command(BaseCommand): help = '''Add site affilations from old db to new db. - Run after migrate {}!!!''' + Run after migrate account models!!!''' def map_role_sql(self): with connections['legacy'].cursor() as cursor: @@ -78,8 +79,74 @@ class Command(BaseCommand): Role.objects.bulk_create(objects) self.stdout.write(self.style.WARNING(f'Added site roles.')) + def update_site_role(self): + roles = Role.objects.filter(country__isnull=True).select_related('site')\ + .filter(site__id__isnull=False).select_for_update() + with transaction.atomic(): + for role in tqdm(roles, desc='Update role country'): + role.country = role.site.country + role.save() + self.stdout.write(self.style.WARNING(f'Updated site roles.')) + def user_role_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select t.* + from + ( + SELECT + site_id, + account_id, + COALESCE(role, 'GUEST') as role + FROM site_affiliations AS sa + ) t + join accounts a on a.id = t.account_id + where t.role not in ('admin', 'GUEST') + ''') + return namedtuplefetchall(cursor) + + def add_role_user(self): + for s in tqdm(self.user_role_sql(), desc='Add role to user'): + sites = SiteSettings.objects.filter(old_id=s.site_id) + old_role = OldRole.objects.get(old_role=s.role) + role_choice = getattr(Role, old_role.new_role) + roles = Role.objects.filter(site__in=[site for site in sites], role=role_choice) + users = User.objects.filter(old_id=s.account_id) + for user in users: + for role in roles: + user_role = UserRole.objects.get_or_create(user=user, + role=role) + self.stdout.write(self.style.WARNING(f'Added users roles.')) + + def superuser_role_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select t.* + from + ( + SELECT + site_id, + account_id, + COALESCE(role, 'GUEST') as role + FROM site_affiliations AS sa + ) t + join accounts a on a.id = t.account_id + where t.role in ('admin') + ''') + return namedtuplefetchall(cursor) + + def add_superuser(self): + for s in tqdm(self.superuser_role_sql(), desc='Add superuser'): + users = User.objects.filter(old_id=s.account_id).select_for_update() + with transaction.atomic(): + for user in users: + user.is_superuser = True + user.save() + self.stdout.write(self.style.WARNING(f'Added superuser.')) def handle(self, *args, **kwargs): - # self.add_old_roles() - self.add_site_role() \ No newline at end of file + self.add_old_roles() + self.add_site_role() + self.update_site_role() + self.add_role_user() + self.add_superuser() \ No newline at end of file diff --git a/apps/account/migrations/0023_auto_20191204_0916.py b/apps/account/migrations/0023_auto_20191204_0916.py new file mode 100644 index 00000000..68d313a0 --- /dev/null +++ b/apps/account/migrations/0023_auto_20191204_0916.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.7 on 2019-12-04 09:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0022_auto_20191203_1149'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='role', + field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager'), (6, 'Reviewer manager'), (7, 'Restaurant reviewer'), (8, 'Sales man'), (9, 'Winery reviewer'), (10, 'Seller')], verbose_name='Role'), + ), + migrations.AlterUniqueTogether( + name='userrole', + unique_together={('user', 'role')}, + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 0a3cd3a6..3d3de56e 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -297,6 +297,8 @@ class UserRole(ProjectBaseMixin): role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.SET_NULL, null=True) establishment = models.ForeignKey(Establishment, verbose_name=_('Establishment'), on_delete=models.SET_NULL, null=True, blank=True) + class Meta: + unique_together = ['user', 'role'] class OldRole(models.Model): From 4d36fc0f0cf6b953324f9ff12181cd5c7f51d005 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Wed, 4 Dec 2019 12:38:07 +0300 Subject: [PATCH 146/191] fix fabric --- fabfile.py | 2 +- make_data_migration.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fabfile.py b/fabfile.py index e8a52f85..f9896200 100644 --- a/fabfile.py +++ b/fabfile.py @@ -54,7 +54,7 @@ def collectstatic(): def deploy(branch=None): role = env.roles[0] - if env.roledefs[role]['branch'] != 'develop': + if env.roledefs[role]['branch'] == 'develop': fetch() install_requirements() migrate() diff --git a/make_data_migration.sh b/make_data_migration.sh index c92f74e7..bed1afb7 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash ./manage.py transfer -a -#./manage.py transfer -d +./manage.py transfer -d ./manage.py transfer -e ./manage.py transfer --fill_city_gallery ./manage.py transfer -l From a81f3f079ae873da6795c75a7fd49b91e3be6635 Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 4 Dec 2019 13:37:49 +0300 Subject: [PATCH 147/191] Changed url id to slug in back-office --- apps/establishment/serializers/back.py | 6 +++- apps/establishment/serializers/common.py | 4 ++- apps/establishment/tests.py | 36 ++++++++++++------------ apps/establishment/urls/back.py | 20 ++++++------- apps/establishment/views/back.py | 25 ++++++++++------ apps/timetable/serialziers.py | 8 +++++- apps/utils/serializers.py | 4 +++ apps/utils/views.py | 6 +++- 8 files changed, 69 insertions(+), 40 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index dd16e861..a1b6cc4a 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -232,9 +232,13 @@ class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer): def validate(self, attrs): """Override validate method.""" establishment_pk = self.get_request_kwargs().get('pk') + establishment_slug = self.get_request_kwargs().get('slug') + + search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug} + image_id = self.get_request_kwargs().get('image_id') - establishment_qs = models.Establishment.objects.filter(pk=establishment_pk) + establishment_qs = models.Establishment.objects.filter(**search_kwargs) image_qs = Image.objects.filter(id=image_id) if not establishment_qs.exists(): diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 7364d02c..e8a68cc0 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -486,7 +486,9 @@ class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer): """Serializer to carousel object w/ model News.""" def validate(self, attrs): - establishment = models.Establishment.objects.filter(pk=self.pk).first() + search_kwargs = {'pk': self.pk} if self.pk else {'slug': self.slug} + + establishment = models.Establishment.objects.filter(**search_kwargs).first() if not establishment: raise serializers.ValidationError({'detail': _('Object not found.')}) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index bd96b052..6bc23ccc 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -104,18 +104,18 @@ class EstablishmentBTests(BaseTestCase): response = self.client.post('/api/back/establishments/', data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.get(f'/api/back/establishments/{self.establishment.id}/', format='json') + response = self.client.get(f'/api/back/establishments/slug/{self.establishment.slug}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { 'name': 'Test new establishment' } - response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/', + response = self.client.patch(f'/api/back/establishments/slug/{self.establishment.slug}/', data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/', + response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -372,22 +372,22 @@ class EstablishmentShedulerTests(ChildTestCase): 'weekday': 1 } - response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data) + response = self.client.post(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) schedule = response.data - response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/') + response = self.client.get(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/') self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { 'weekday': 2 } - response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/', + response = self.client.patch(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/', data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/') + response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -410,21 +410,21 @@ class EstablishmentWebTagTests(BaseTestCase): class EstablishmentWebSlugTests(ChildTestCase): def test_slug_Read(self): - response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/', format='json') + response = self.client.get(f'/api/web/establishments/{self.establishment.id}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) class EstablishmentWebSimilarTests(ChildTestCase): def test_similar_Read(self): - response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/similar/', format='json') + response = self.client.get(f'/api/web/establishments/{self.establishment.id}/similar/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) class EstablishmentWebCommentsTests(ChildTestCase): def test_comments_CRUD(self): - response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/comments/', format='json') + response = self.client.get(f'/api/web/establishments/{self.establishment.id}/comments/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) data = { @@ -433,13 +433,13 @@ class EstablishmentWebCommentsTests(ChildTestCase): 'mark': 4 } - response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/comments/create/', + response = self.client.post(f'/api/web/establishments/{self.establishment.id}/comments/create/', data=data) comment = response.json() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', + response = self.client.get(f'/api/web/establishments/{self.establishment.id}/comments/{comment["id"]}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -448,12 +448,12 @@ class EstablishmentWebCommentsTests(ChildTestCase): } response = self.client.patch( - f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', + f'/api/web/establishments/{self.establishment.id}/comments/{comment["id"]}/', data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) response = self.client.delete( - f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', + f'/api/web/establishments/{self.establishment.id}/comments/{comment["id"]}/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -466,13 +466,13 @@ class EstablishmentWebFavoriteTests(ChildTestCase): "object_id": self.establishment.id } - response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', + response = self.client.post(f'/api/web/establishments/{self.establishment.id}/favorites/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) response = self.client.delete( - f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', + f'/api/web/establishments/{self.establishment.id}/favorites/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -484,8 +484,8 @@ class EstablishmentCarouselTests(ChildTestCase): "object_id": self.establishment.id } - response = self.client.post(f'/api/back/establishments/{self.establishment.id}/carousels/', data=data) + response = self.client.post(f'/api/back/establishments/slug/{self.establishment.slug}/carousels/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/carousels/') + response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/carousels/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index d9b2fbd7..ce1c7b27 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -8,25 +8,25 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListCreateView.as_view(), name='list'), - path('/', views.EstablishmentRUDView.as_view(), name='detail'), - path('/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(), + path('slug//', views.EstablishmentRUDView.as_view(), name='detail'), + path('slug//carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'), - path('/schedule//', views.EstablishmentScheduleRUDView.as_view(), + path('slug//schedule//', views.EstablishmentScheduleRUDView.as_view(), name='schedule-rud'), - path('/schedule/', views.EstablishmentScheduleCreateView.as_view(), + path('slug//schedule/', views.EstablishmentScheduleCreateView.as_view(), name='schedule-create'), - path('/gallery/', views.EstablishmentGalleryListView.as_view(), + path('slug//gallery/', views.EstablishmentGalleryListView.as_view(), name='gallery-list'), - path('/gallery//', + path('slug//gallery//', views.EstablishmentGalleryCreateDestroyView.as_view(), name='gallery-create-destroy'), - path('/companies/', views.EstablishmentCompanyListCreateView.as_view(), + path('slug//companies/', views.EstablishmentCompanyListCreateView.as_view(), name='company-list-create'), - path('/companies//', views.EstablishmentCompanyRUDView.as_view(), + path('slug//companies//', views.EstablishmentCompanyRUDView.as_view(), name='company-rud'), - path('/notes/', views.EstablishmentNoteListCreateView.as_view(), + path('slug//notes/', views.EstablishmentNoteListCreateView.as_view(), name='note-list-create'), - path('/notes//', views.EstablishmentNoteRUDView.as_view(), + path('slug//notes//', views.EstablishmentNoteRUDView.as_view(), name='note-rud'), path('menus/', views.MenuListCreateView.as_view(), name='menu-list'), path('menus//', views.MenuRUDView.as_view(), name='menu-rud'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index d3afbf2e..b92929c1 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -31,6 +31,7 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView): + lookup_field = 'slug' queryset = models.Establishment.objects.all() serializer_class = serializers.EstablishmentRUDSerializer permission_classes = [IsCountryAdmin | IsEstablishmentManager] @@ -38,6 +39,7 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): """Establishment schedule RUD view""" + lookup_field = 'slug' serializer_class = ScheduleRUDSerializer permission_classes = [IsEstablishmentManager] @@ -45,11 +47,11 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): """ Returns the object the view is displaying. """ - establishment_pk = self.kwargs['pk'] + establishment_slug = self.kwargs['slug'] schedule_id = self.kwargs['schedule_id'] establishment = get_object_or_404(klass=models.Establishment.objects.all(), - pk=establishment_pk) + slug=establishment_slug) schedule = get_object_or_404(klass=establishment.schedule, id=schedule_id) @@ -62,6 +64,7 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentScheduleCreateView(generics.CreateAPIView): """Establishment schedule Create view""" + lookup_field = 'slug' serializer_class = ScheduleCreateSerializer queryset = Timetable.objects.all() permission_classes = [IsEstablishmentManager] @@ -210,6 +213,7 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, CreateDestroyGalleryViewMixin): """Resource for a create|destroy gallery for establishment for back-office users.""" + lookup_field = 'slug' serializer_class = serializers.EstablishmentBackOfficeGallerySerializer def get_object(self): @@ -218,7 +222,7 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, """ establishment_qs = self.filter_queryset(self.get_queryset()) - establishment = get_object_or_404(establishment_qs, pk=self.kwargs.get('pk')) + establishment = get_object_or_404(establishment_qs, slug=self.kwargs.get('slug')) gallery = get_object_or_404(establishment.establishment_gallery, image_id=self.kwargs.get('image_id')) @@ -231,12 +235,13 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, class EstablishmentGalleryListView(EstablishmentMixinViews, generics.ListAPIView): """Resource for returning gallery for establishment for back-office users.""" + lookup_field = 'slug' serializer_class = serializers.ImageBaseSerializer def get_object(self): """Override get_object method.""" qs = super(EstablishmentGalleryListView, self).get_queryset() - establishment = get_object_or_404(qs, pk=self.kwargs.get('pk')) + establishment = get_object_or_404(qs, slug=self.kwargs.get('slug')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -252,6 +257,7 @@ class EstablishmentCompanyListCreateView(EstablishmentMixinViews, generics.ListCreateAPIView): """List|Create establishment company view.""" + lookup_field = 'slug' serializer_class = serializers.EstablishmentCompanyListCreateSerializer def get_object(self): @@ -259,7 +265,7 @@ class EstablishmentCompanyListCreateView(EstablishmentMixinViews, establishment_qs = models.Establishment.objects.all() filtered_ad_qs = self.filter_queryset(establishment_qs) - establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk')) + establishment = get_object_or_404(filtered_ad_qs, slug=self.kwargs.get('slug')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -275,6 +281,7 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestroyAPIView): """Create|Retrieve|Update|Destroy establishment company view.""" + lookup_field = 'slug' serializer_class = serializers.CompanyBaseSerializer def get_object(self): @@ -282,7 +289,7 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews, establishment_qs = models.Establishment.objects.all() filtered_ad_qs = self.filter_queryset(establishment_qs) - establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk')) + establishment = get_object_or_404(filtered_ad_qs, slug=self.kwargs.get('slug')) company = get_object_or_404(establishment.companies.all(), pk=self.kwargs.get('company_pk')) # May raise a permission denied @@ -295,6 +302,7 @@ class EstablishmentNoteListCreateView(EstablishmentMixinViews, generics.ListCreateAPIView): """Retrieve|Update|Destroy establishment note view.""" + lookup_field = 'slug' serializer_class = serializers.EstablishmentNoteListCreateSerializer def get_object(self): @@ -302,7 +310,7 @@ class EstablishmentNoteListCreateView(EstablishmentMixinViews, establishment_qs = models.Establishment.objects.all() filtered_establishment_qs = self.filter_queryset(establishment_qs) - establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk')) + establishment = get_object_or_404(filtered_establishment_qs, slug=self.kwargs.get('slug')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -318,6 +326,7 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestroyAPIView): """Create|Retrieve|Update|Destroy establishment note view.""" + lookup_field = 'slug' serializer_class = serializers.EstablishmentNoteBaseSerializer def get_object(self): @@ -325,7 +334,7 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews, establishment_qs = models.Establishment.objects.all() filtered_establishment_qs = self.filter_queryset(establishment_qs) - establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk')) + establishment = get_object_or_404(filtered_establishment_qs, slug=self.kwargs.get('slug')) note = get_object_or_404(establishment.notes.all(), pk=self.kwargs['note_pk']) # May raise a permission denied diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index 48c8374d..305be0ec 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -45,8 +45,14 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): .parser_context.get('view')\ .kwargs.get('pk') + establishment_slug = self.context.get('request')\ + .parser_context.get('view')\ + .kwargs.get('slug') + + search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug} + # Check if establishment exists. - establishment_qs = Establishment.objects.filter(pk=establishment_pk) + establishment_qs = Establishment.objects.filter(**search_kwargs) if not establishment_qs.exists(): raise serializers.ValidationError({'detail': _('Establishment not found.')}) attrs['establishment'] = establishment_qs.first() diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index f55b69bc..15a58f7b 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -118,6 +118,10 @@ class CarouselCreateSerializer(serializers.ModelSerializer): def pk(self): return self.request.parser_context.get('kwargs').get('pk') + @property + def slug(self): + return self.request.parser_context.get('kwargs').get('slug') + class RecursiveFieldSerializer(serializers.Serializer): def to_representation(self, value): diff --git a/apps/utils/views.py b/apps/utils/views.py index 478a3cb2..db10dbfd 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -158,7 +158,11 @@ class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): lookup_field = 'id' def get_base_object(self): - return get_object_or_404(self._model, id=self.kwargs['pk']) + search_kwargs = { + 'id': self.kwargs.get('pk'), + 'slug': self.kwargs.get('slug'), + } + return get_object_or_404(self._model, **search_kwargs) def get_object(self): """ From c5992afec0e096b7f477d574652aeba74b29cfe0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 13:50:55 +0300 Subject: [PATCH 148/191] add opening_at to schedule search results --- apps/search_indexes/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 67a2bff0..7bd3995a 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -168,6 +168,7 @@ class ScheduleDocumentSerializer(serializers.Serializer): weekday = serializers.IntegerField() weekday_display = serializers.CharField() closed_at = serializers.CharField() + opening_at = serializers.CharField() class InFavoritesMixin(DocumentSerializer): From 552e08e24249de183d24a966a5d4adeed96d28b9 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 13:52:39 +0300 Subject: [PATCH 149/191] opening_at for establishment --- apps/search_indexes/documents/establishment.py | 1 + apps/timetable/models.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 9e1d0a82..aca81f3f 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -116,6 +116,7 @@ class EstablishmentDocument(Document): 'weekday': fields.IntegerField(attr='weekday'), 'weekday_display': fields.KeywordField(attr='get_weekday_display'), 'closed_at': fields.KeywordField(attr='closed_at_str'), + 'opening_at': fields.KeywordField(attr='opening_at_str'), } )) address = fields.ObjectField( diff --git a/apps/timetable/models.py b/apps/timetable/models.py index cf7f8d94..90a6ae38 100644 --- a/apps/timetable/models.py +++ b/apps/timetable/models.py @@ -39,6 +39,10 @@ class Timetable(ProjectBaseMixin): def closed_at_str(self): return str(self.closed_at) if self.closed_at else None + @property + def opening_at_str(self): + return str(self.opening_at) if self.opening_at else None + @property def opening_time(self): return self.opening_at or self.lunch_start or self.dinner_start From 627f7562ec2fcdacc84013139d7ee594d68f45b8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 4 Dec 2019 14:21:54 +0300 Subject: [PATCH 150/191] finalizing transfer guides --- apps/collection/admin.py | 21 ++ ...ntsectioncategory_guidewinecolorsection.py | 56 ++++ .../migrations/0021_guideelementtype.py | 24 ++ .../migrations/0022_guideelement.py | 48 +++ .../collection/migrations/0023_advertorial.py | 31 ++ apps/collection/models.py | 126 +++++++- apps/collection/transfer_data.py | 286 +++++++++++++++++- apps/transfer/management/commands/transfer.py | 6 + apps/transfer/mixins.py | 6 - apps/transfer/models.py | 4 +- apps/transfer/serializers/guide.py | 28 +- project/settings/base.py | 1 + requirements/base.txt | 3 + 13 files changed, 615 insertions(+), 25 deletions(-) create mode 100644 apps/collection/migrations/0020_guideelementsection_guideelementsectioncategory_guidewinecolorsection.py create mode 100644 apps/collection/migrations/0021_guideelementtype.py create mode 100644 apps/collection/migrations/0022_guideelement.py create mode 100644 apps/collection/migrations/0023_advertorial.py diff --git a/apps/collection/admin.py b/apps/collection/admin.py index 2e2c22e2..1b876e91 100644 --- a/apps/collection/admin.py +++ b/apps/collection/admin.py @@ -1,4 +1,6 @@ from django.contrib.gis import admin +from mptt.admin import DraggableMPTTAdmin, TreeRelatedFieldListFilter +from utils.admin import BaseModelAdminMixin from collection import models @@ -11,3 +13,22 @@ class CollectionAdmin(admin.ModelAdmin): @admin.register(models.Guide) class GuideAdmin(admin.ModelAdmin): """Guide admin.""" + + +@admin.register(models.GuideElementType) +class GuideElementType(admin.ModelAdmin): + """Guide element admin.""" + + +@admin.register(models.GuideElement) +class GuideElementAdmin(DraggableMPTTAdmin, BaseModelAdminMixin, admin.ModelAdmin): + """Guide element admin.""" + raw_id_fields = [ + 'guide_element_type', 'establishment', 'review', + 'wine_region', 'product', 'city', + 'wine_color_section', 'section', 'guide', + 'parent', + ] + # list_filter = ( + # ('parent', TreeRelatedFieldListFilter), + # ) diff --git a/apps/collection/migrations/0020_guideelementsection_guideelementsectioncategory_guidewinecolorsection.py b/apps/collection/migrations/0020_guideelementsection_guideelementsectioncategory_guidewinecolorsection.py new file mode 100644 index 00000000..06aaddfb --- /dev/null +++ b/apps/collection/migrations/0020_guideelementsection_guideelementsectioncategory_guidewinecolorsection.py @@ -0,0 +1,56 @@ +# Generated by Django 2.2.7 on 2019-12-02 14:05 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('collection', '0019_advertorial_guidefilter'), + ] + + operations = [ + migrations.CreateModel( + name='GuideElementSectionCategory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=255, verbose_name='category name')), + ], + options={ + 'verbose_name': 'guide element section category', + 'verbose_name_plural': 'guide element section categories', + }, + ), + migrations.CreateModel( + name='GuideWineColorSection', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=255, verbose_name='section name')), + ], + options={ + 'verbose_name': 'guide wine color section', + 'verbose_name_plural': 'guide wine color sections', + }, + ), + migrations.CreateModel( + name='GuideElementSection', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=255, verbose_name='section name')), + ('old_id', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='collection.GuideElementSectionCategory', verbose_name='category')), + ], + options={ + 'verbose_name': 'guide element section', + 'verbose_name_plural': 'guide element sections', + }, + ), + ] diff --git a/apps/collection/migrations/0021_guideelementtype.py b/apps/collection/migrations/0021_guideelementtype.py new file mode 100644 index 00000000..7a199051 --- /dev/null +++ b/apps/collection/migrations/0021_guideelementtype.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.7 on 2019-12-02 14:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('collection', '0020_guideelementsection_guideelementsectioncategory_guidewinecolorsection'), + ] + + operations = [ + migrations.CreateModel( + name='GuideElementType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='name')), + ], + options={ + 'verbose_name': 'guide element type', + 'verbose_name_plural': 'guide element types', + }, + ), + ] diff --git a/apps/collection/migrations/0022_guideelement.py b/apps/collection/migrations/0022_guideelement.py new file mode 100644 index 00000000..77c2220c --- /dev/null +++ b/apps/collection/migrations/0022_guideelement.py @@ -0,0 +1,48 @@ +# Generated by Django 2.2.7 on 2019-12-02 14:44 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import mptt.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0018_auto_20191117_1117'), + ('product', '0018_purchasedproduct'), + ('location', '0030_auto_20191120_1010'), + ('establishment', '0067_auto_20191122_1244'), + ('collection', '0021_guideelementtype'), + ] + + operations = [ + migrations.CreateModel( + name='GuideElement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('priority', models.IntegerField(blank=True, default=None, null=True)), + ('old_id', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id')), + ('lft', models.PositiveIntegerField(db_index=True, editable=False)), + ('rght', models.PositiveIntegerField(db_index=True, editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(db_index=True, editable=False)), + ('city', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.City')), + ('establishment', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment')), + ('guide', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='collection.Guide')), + ('guide_element_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='collection.GuideElementType', verbose_name='guide element type')), + ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='collection.GuideElement')), + ('product', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='product.Product')), + ('review', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='review.Review')), + ('section', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='collection.GuideElementSection')), + ('wine_color_section', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='collection.GuideWineColorSection')), + ('wine_region', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.WineRegion')), + ], + options={ + 'verbose_name': 'guide element', + 'verbose_name_plural': 'guide elements', + }, + ), + ] diff --git a/apps/collection/migrations/0023_advertorial.py b/apps/collection/migrations/0023_advertorial.py new file mode 100644 index 00000000..4917729f --- /dev/null +++ b/apps/collection/migrations/0023_advertorial.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.7 on 2019-12-03 13:20 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('collection', '0022_guideelement'), + ] + + operations = [ + migrations.CreateModel( + name='Advertorial', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('number_of_pages', models.PositiveIntegerField(help_text='the total number of reserved pages', verbose_name='number of pages')), + ('right_pages', models.PositiveIntegerField(help_text='the number of right pages (which are part of total number).', verbose_name='number of right pages')), + ('old_id', models.IntegerField(blank=True, null=True)), + ('guide_element', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='advertorial', to='collection.GuideElement', verbose_name='guide element')), + ], + options={ + 'verbose_name': 'advertorial', + 'verbose_name_plural': 'advertorials', + }, + ), + ] diff --git a/apps/collection/models.py b/apps/collection/models.py index 7d037a85..58ca9f07 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -1,8 +1,9 @@ +import re +from mptt.models import MPTTModel, TreeForeignKey from django.contrib.contenttypes.fields import ContentType from django.contrib.postgres.fields import JSONField from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -import re from django.utils.translation import gettext_lazy as _ from utils.models import ProjectBaseMixin, URLImageMixin @@ -170,6 +171,9 @@ class Advertorial(ProjectBaseMixin): right_pages = models.PositiveIntegerField( verbose_name=_('number of right pages'), help_text=_('the number of right pages (which are part of total number).')) + guide_element = models.OneToOneField('GuideElement', on_delete=models.CASCADE, + related_name='advertorial', + verbose_name=_('guide element')) old_id = models.IntegerField(blank=True, null=True) objects = AdvertorialQuerySet.as_manager() @@ -220,3 +224,123 @@ class GuideFilter(ProjectBaseMixin): """Meta class.""" verbose_name = _('guide filter') verbose_name_plural = _('guide filters') + + +class GuideElementType(models.Model): + """Model for type of guide elements.""" + + name = models.CharField(max_length=50, + verbose_name=_('name')) + + class Meta: + """Meta class.""" + verbose_name = _('guide element type') + verbose_name_plural = _('guide element types') + + def __str__(self): + """Overridden str dunder.""" + return self.name + + +class GuideWineColorSectionQuerySet(models.QuerySet): + """QuerySet for model GuideWineColorSection.""" + + +class GuideWineColorSection(ProjectBaseMixin): + """Sections for wine colors.""" + + name = models.CharField(max_length=255, verbose_name=_('section name')) + + objects = GuideWineColorSectionQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('guide wine color section') + verbose_name_plural = _('guide wine color sections') + + +class GuideElementSectionCategoryQuerySet(models.QuerySet): + """QuerySet for model GuideElementSectionCategory.""" + + +class GuideElementSectionCategory(ProjectBaseMixin): + """Section category for guide element.""" + + name = models.CharField(max_length=255, + verbose_name=_('category name')) + + objects = GuideElementSectionCategoryQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('guide element section category') + verbose_name_plural = _('guide element section categories') + + +class GuideElementSectionQuerySet(models.QuerySet): + """QuerySet for model GuideElementSection.""" + + +class GuideElementSection(ProjectBaseMixin): + """Sections for guide element.""" + + name = models.CharField(max_length=255, verbose_name=_('section name')) + category = models.ForeignKey(GuideElementSectionCategory, on_delete=models.PROTECT, + verbose_name=_('category')) + old_id = models.PositiveIntegerField(blank=True, null=True, default=None, + verbose_name=_('old id')) + + objects = GuideElementSectionQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('guide element section') + verbose_name_plural = _('guide element sections') + + +class GuideElementQuerySet(models.QuerySet): + """QuerySet for model Guide elements.""" + + +class GuideElement(ProjectBaseMixin, MPTTModel): + """Frozen state of elements of guide instance.""" + + guide_element_type = models.ForeignKey('GuideElementType', on_delete=models.SET_NULL, + null=True, + verbose_name=_('guide element type')) + establishment = models.ForeignKey('establishment.Establishment', on_delete=models.SET_NULL, + null=True, blank=True, default=None) + review = models.ForeignKey('review.Review', on_delete=models.SET_NULL, + null=True, blank=True, default=None) + wine_region = models.ForeignKey('location.WineRegion', on_delete=models.SET_NULL, + null=True, blank=True, default=None) + product = models.ForeignKey('product.Product', on_delete=models.SET_NULL, + null=True, blank=True, default=None) + priority = models.IntegerField(null=True, blank=True, default=None) + city = models.ForeignKey('location.City', on_delete=models.SET_NULL, + null=True, blank=True, default=None) + wine_color_section = models.ForeignKey('GuideWineColorSection', on_delete=models.SET_NULL, + null=True, blank=True, default=None) + section = models.ForeignKey('GuideElementSection', on_delete=models.SET_NULL, + null=True, blank=True, default=None) + guide = models.ForeignKey('Guide', on_delete=models.SET_NULL, + null=True, blank=True, default=None) + parent = TreeForeignKey('self', on_delete=models.CASCADE, + null=True, blank=True, + related_name='children') + old_id = models.PositiveIntegerField(blank=True, null=True, default=None, + verbose_name=_('old id')) + + objects = GuideElementQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('guide element') + verbose_name_plural = _('guide elements') + + class MPTTMeta: + order_insertion_by = ['guide_element_type'] + + def __str__(self): + """Overridden dunder method.""" + return self.guide_element_type.name if self.guide_element_type else self.id diff --git a/apps/collection/transfer_data.py b/apps/collection/transfer_data.py index 9eeec541..5d705b53 100644 --- a/apps/collection/transfer_data.py +++ b/apps/collection/transfer_data.py @@ -1,6 +1,15 @@ from pprint import pprint -from transfer.models import Guides, GuideFilters +from tqdm import tqdm +from establishment.models import Establishment +from review.models import Review +from location.models import WineRegion, City +from product.models import Product +from transfer.models import Guides, GuideFilters, GuideSections, GuideElements, \ + GuideAds from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer +from collection.models import GuideElementSection, GuideElementSectionCategory, \ + GuideWineColorSection, GuideElementType, GuideElement, \ + Guide, Advertorial def transfer_guide(): @@ -14,14 +23,15 @@ def transfer_guide(): serialized_data.save() else: for d in serialized_data.errors: errors.append(d) if d else None - pprint(f"transfer_guide errors: {errors}") + pprint(f"ERRORS: {errors}") + print(f'COUNT OF SERIALIZED OBJECTS: {queryset.values().count()}') def transfer_guide_filter(): """Transfer GuideFilter model.""" errors = [] queryset = GuideFilters.objects.exclude(guide__title__icontains='test') \ - .exclude(guide__id__isnull=True) + .exclude(guide__isnull=True) serialized_data = GuideFilterSerializer( data=list(queryset.values()), many=True) @@ -29,8 +39,250 @@ def transfer_guide_filter(): serialized_data.save() else: for d in serialized_data.errors: errors.append(d) if d else None - pprint(f"transfer_guide_filter errors: {errors}\n" - f"COUNT: {len(errors)}") + pprint(f'ERRORS: {errors}') + print(f"COUNT: {len(errors)}") + print(f'COUNT OF SERIALIZED OBJECTS: {queryset.values().count()}') + + +def transfer_guide_element_section(): + """Transfer GuideSections model.""" + created_count = 0 + category, _ = GuideElementSectionCategory.objects.get_or_create( + name='shop_category') + queryset_values = GuideSections.objects.values_list('id', 'value_name') + for old_id, section_name in tqdm(queryset_values): + obj, created = GuideElementSection.objects.get_or_create( + name=section_name, + category=category, + old_id=old_id, + ) + if created: created_count += 1 + print(f'OBJECTS CREATED: {created_count}') + + +def transfer_guide_wine_color_section(): + """Transfer GuideElements model (only wine color sections).""" + created_count = 0 + queryset_values = GuideElements.objects.raw( + """ + select distinct(color), + 1 as id + from guide_elements where color is not null; + """ + ) + for section_name in tqdm([i.color for i in queryset_values]): + obj, created = GuideWineColorSection.objects.get_or_create( + name=section_name + ) + if created: created_count += 1 + print(f'OBJECTS CREATED: {created_count}') + + +def transfer_guide_element_type(): + """Transfer GuideElements model (only element types).""" + created_count = 0 + queryset_values = GuideElements.objects.raw( + """ + select distinct(type), + 1 as id + from guide_elements; + """ + ) + for element_type in tqdm([i.type for i in queryset_values]): + obj, created = GuideElementType.objects.get_or_create( + name=element_type + ) + if created: created_count += 1 + print(f'OBJECTS CREATED: {created_count}') + + +def transfer_guide_elements_bulk(): + """Transfer Guide elements via bulk_create.""" + def get_guide_element_type(guide_element_type: str): + if guide_element_type: + qs = GuideElementType.objects.filter(name__iexact=guide_element_type) + if qs.exists(): + return qs.first() + + def get_establishment(old_id: int): + if old_id: + qs = Establishment.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + def get_review(old_id: int): + if old_id: + qs = Review.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + def get_wine_region(old_id: int): + if old_id: + qs = WineRegion.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + def get_wine(old_id: int): + if old_id: + qs = Product.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + def get_wine_color_section(color_section: str): + if color_section: + qs = GuideWineColorSection.objects.filter(name__iexact=color_section) + if qs.exists(): + return qs.first() + + def get_city(old_id: int): + if old_id: + qs = City.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + def get_guide_element_section(old_id: int): + if old_id: + qs = GuideElementSection.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + def get_guide(old_id): + if old_id: + qs = Guide.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + def get_parent(old_id): + if old_id: + qs = GuideElement.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + objects_to_update = [] + base_queryset = GuideElements.objects.all() + + for old_id, type, establishment_id, review_id, wine_region_id, \ + wine_id, color, order_number, city_id, section_id, guide_id \ + in tqdm(base_queryset.filter(parent_id__isnull=True) + .values_list('id', 'type', 'establishment_id', + 'review_id', 'wine_region_id', 'wine_id', + 'color', 'order_number', 'city_id', + 'section_id', 'guide_id'), + desc='Check parent guide elements'): + if not GuideElement.objects.filter(old_id=old_id).exists(): + guide = GuideElement( + old_id=old_id, + guide_element_type=get_guide_element_type(type), + establishment=get_establishment(establishment_id), + review=get_review(review_id), + wine_region=get_wine_region(wine_region_id), + product=get_wine(wine_id), + wine_color_section=get_wine_color_section(color), + priority=order_number, + city=get_city(city_id), + section=get_guide_element_section(section_id), + parent=None, + lft=1, + rght=1, + tree_id=1, + level=1, + ) + # check old guide + if not guide_id: + objects_to_update.append(guide) + else: + old_guide = Guides.objects.exclude(title__icontains='test') \ + .filter(id=guide_id) + if old_guide.exists(): + guide.guide = get_guide(guide_id) + objects_to_update.append(guide) + + # create parents + GuideElement.objects.bulk_create(objects_to_update) + pprint(f'CREATED PARENT GUIDE ELEMENTS W/ OLD_ID: {[i.old_id for i in objects_to_update]}') + print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}') + + # attach child guide elements + queryset_values = base_queryset.filter(parent_id__isnull=False) \ + .order_by('-parent_id') \ + .values_list('id', 'type', 'establishment_id', + 'review_id', 'wine_region_id', 'wine_id', + 'color', 'order_number', 'city_id', + 'section_id', 'guide_id', 'parent_id') + for old_id, type, establishment_id, review_id, wine_region_id, \ + wine_id, color, order_number, city_id, section_id, guide_id, parent_id \ + in tqdm(sorted(queryset_values, key=lambda value: value[len(value)-1]), + desc='Check child guide elements'): + if not GuideElement.objects.filter(old_id=old_id).exists(): + # check old guide + if guide_id: + old_guide = Guides.objects.exclude(title__icontains='test') \ + .filter(id=guide_id) + if old_guide.exists(): + GuideElement.objects.create( + old_id=old_id, + guide_element_type=get_guide_element_type(type), + establishment=get_establishment(establishment_id), + review=get_review(review_id), + wine_region=get_wine_region(wine_region_id), + product=get_wine(wine_id), + wine_color_section=get_wine_color_section(color), + priority=order_number, + city=get_city(city_id), + section=get_guide_element_section(section_id), + parent=get_parent(parent_id), + lft=1, + rght=1, + tree_id=1, + level=1, + guide=get_guide(guide_id), + ) + + pprint(f'CREATED CHILD GUIDE ELEMENTS W/ OLD_ID: {[i.old_id for i in objects_to_update]}') + print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}') + + # rebuild trees + GuideElement._tree_manager.rebuild() + + +def transfer_guide_element_advertorials(): + """Transfer Guide Advertorials model.""" + def get_guide_element(old_id: int): + if old_id: + qs = GuideElement.objects.filter(old_id=old_id) + legacy_qs = GuideElements.objects.exclude(guide__isnull=True) \ + .exclude(guide__title__icontains='test') \ + .filter(id=guide_ad_node_id) + if qs.exists() and legacy_qs.exists(): + return qs.first() + elif legacy_qs.exists() and not qs.exists(): + raise ValueError(f'Guide element was not transfer correctly - {old_id}.') + + objects_to_update = [] + advertorials = GuideAds.objects.exclude(nb_pages__isnull=True) \ + .exclude(nb_right_pages__isnull=True) \ + .exclude(guide_ad_node_id__isnull=True) \ + .values_list('id', 'nb_pages', 'nb_right_pages', + 'guide_ad_node_id') + for old_id, nb_pages, nb_right_pages, guide_ad_node_id in tqdm(advertorials): + # check guide element + guide_element = get_guide_element(guide_ad_node_id) + + if not Advertorial.objects.filter(old_id=old_id).exists() and guide_element: + objects_to_update.append( + Advertorial( + old_id=old_id, + number_of_pages=nb_pages, + right_pages=nb_right_pages, + guide_element=guide_element, + ) + ) + + # create related child + Advertorial.objects.bulk_create(objects_to_update) + + pprint(f'CREATED ADVERTORIALS W/ OLD_ID: {[i.old_id for i in objects_to_update]}') + print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}') data_types = { @@ -39,5 +291,29 @@ data_types = { ], 'guide_filters': [ transfer_guide_filter, + ], + 'guide_element_sections': [ + transfer_guide_element_section, + ], + 'guide_wine_color_sections': [ + transfer_guide_wine_color_section, + ], + 'guide_element_types': [ + transfer_guide_element_type, + ], + 'guide_elements_bulk': [ + transfer_guide_elements_bulk, + ], + 'guide_element_advertorials': [ + transfer_guide_element_advertorials + ], + 'guide_complete': [ + transfer_guide, # transfer guides from Guides + transfer_guide_filter, # transfer guide filters from GuideFilters + transfer_guide_element_section, # partial transfer element section from GuideSections + transfer_guide_wine_color_section, # partial transfer wine color section from GuideSections + transfer_guide_element_type, # partial transfer section types from GuideElements + transfer_guide_elements_bulk, # transfer result of GuideFilters from GuideElements + transfer_guide_element_advertorials, # transfer advertorials that linked to GuideElements ] } diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index 3b562b0d..d59bacfc 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -43,6 +43,12 @@ class Command(BaseCommand): 'fill_city_gallery', # №3 - перенос галереи городов 'guides', 'guide_filters', + 'guide_element_sections', + 'guide_wine_color_sections', + 'guide_element_types', + 'guide_elements_bulk', + 'guide_element_advertorials', + 'guide_complete', ] def handle(self, *args, **options): diff --git a/apps/transfer/mixins.py b/apps/transfer/mixins.py index 30537a14..7b43b210 100644 --- a/apps/transfer/mixins.py +++ b/apps/transfer/mixins.py @@ -39,12 +39,6 @@ class TransferSerializerMixin(serializers.ModelSerializer): qs = self.Meta.model.objects.filter(**validated_data) if not qs.exists(): return super().create(validated_data) - # try: - # qs = self.Meta.model.objects.filter(**validated_data) - # if not qs.exists(): - # return super().create(validated_data) - # except Exception: - # breakpoint() @property def tag_category(self): diff --git a/apps/transfer/models.py b/apps/transfer/models.py index fa3d898b..2ebcfb65 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -389,7 +389,7 @@ class GuideSections(MigrateMixin): class Meta: managed = False - db_table = 'guide_elements' + db_table = 'guide_sections' class GuideElements(MigrateMixin): @@ -406,7 +406,7 @@ class GuideElements(MigrateMixin): guide_ad = models.ForeignKey(GuideAds, models.DO_NOTHING, blank=True, null=True) city = models.ForeignKey(Cities, models.DO_NOTHING, blank=True, null=True) section = models.ForeignKey('GuideSections', models.DO_NOTHING, blank=True, null=True) - guide_id = models.IntegerField(blank=True, null=True) + guide = models.ForeignKey('Guides', models.DO_NOTHING, blank=True, null=True) parent = models.ForeignKey('self', models.DO_NOTHING, blank=True, null=True) lft = models.IntegerField() rgt = models.IntegerField() diff --git a/apps/transfer/serializers/guide.py b/apps/transfer/serializers/guide.py index c49e34b2..f0cddd02 100644 --- a/apps/transfer/serializers/guide.py +++ b/apps/transfer/serializers/guide.py @@ -1,16 +1,17 @@ from itertools import chain -from django.utils.text import slugify + import yaml +from django.utils.text import slugify from pycountry import countries, subdivisions from rest_framework import serializers -from collection.models import Guide, GuideType, GuideFilter +from collection import models from establishment.models import EstablishmentType from location.models import Country, Region, WineRegion from main.models import SiteSettings +from review.models import Review from transfer.mixins import TransferSerializerMixin from translation.models import Language -from review.models import Review class GuideSerializer(TransferSerializerMixin): @@ -23,7 +24,7 @@ class GuideSerializer(TransferSerializerMixin): inserter_field = serializers.CharField() class Meta: - model = Guide + model = models.Guide fields = ( 'id', 'title', @@ -46,13 +47,13 @@ class GuideSerializer(TransferSerializerMixin): def get_state(self, state: str): if state == 'built': - return Guide.BUILT + return models.Guide.BUILT elif state == 'removing': - return Guide.REMOVING + return models.Guide.REMOVING elif state == 'building': - return Guide.BUILDING + return models.Guide.BUILDING else: - return Guide.WAITING + return models.Guide.WAITING def get_site(self, site_id): qs = SiteSettings.objects.filter(old_id=site_id) @@ -60,7 +61,7 @@ class GuideSerializer(TransferSerializerMixin): return qs.first() def get_guide_type(self, inserter_field): - guide_type, _ = GuideType.objects.get_or_create(name=inserter_field) + guide_type, _ = models.GuideType.objects.get_or_create(name=inserter_field) return guide_type @@ -81,7 +82,7 @@ class GuideFilterSerializer(TransferSerializerMixin): guide_id = serializers.IntegerField() class Meta: - model = GuideFilter + model = models.GuideFilter fields = ( 'id', 'year', @@ -98,6 +99,11 @@ class GuideFilterSerializer(TransferSerializerMixin): 'guide_id', ) + def create(self, validated_data): + qs = self.Meta.model.objects.filter(guide=validated_data.get('guide')) + if not qs.exists(): + return super().create(validated_data) + @staticmethod def parse_ruby_helper(raw_value: str): """Parse RubyActiveSupport records""" @@ -345,6 +351,6 @@ class GuideFilterSerializer(TransferSerializerMixin): return {'state': list(set(review_states))} def get_guide(self, old_guide_id: int): - qs = Guide.objects.filter(old_id=old_guide_id) + qs = models.Guide.objects.filter(old_id=old_guide_id) if qs.exists(): return qs.first() diff --git a/project/settings/base.py b/project/settings/base.py index 2c6f0cf0..6648f0af 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -99,6 +99,7 @@ EXTERNAL_APPS = [ 'storages', 'sorl.thumbnail', 'timezonefinder', + 'mptt', ] diff --git a/requirements/base.txt b/requirements/base.txt index aadc7301..90e5b2d5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -60,3 +60,6 @@ celery==4.3.0 # country information pycountry==19.8.18 + +# sql-tree +django-mptt==0.9.1 From 4202eb679b5a938d0a454cfcffa11b429cd05578 Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 4 Dec 2019 15:48:54 +0300 Subject: [PATCH 151/191] Corrections for tests --- apps/establishment/tests.py | 18 +++++++++--------- apps/establishment/views/web.py | 1 + apps/tag/serializers.py | 1 + apps/utils/views.py | 11 +++++++---- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 6bc23ccc..f46cccc3 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -410,21 +410,21 @@ class EstablishmentWebTagTests(BaseTestCase): class EstablishmentWebSlugTests(ChildTestCase): def test_slug_Read(self): - response = self.client.get(f'/api/web/establishments/{self.establishment.id}/', format='json') + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) class EstablishmentWebSimilarTests(ChildTestCase): def test_similar_Read(self): - response = self.client.get(f'/api/web/establishments/{self.establishment.id}/similar/', format='json') + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/similar/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) class EstablishmentWebCommentsTests(ChildTestCase): def test_comments_CRUD(self): - response = self.client.get(f'/api/web/establishments/{self.establishment.id}/comments/', format='json') + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/comments/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) data = { @@ -433,13 +433,13 @@ class EstablishmentWebCommentsTests(ChildTestCase): 'mark': 4 } - response = self.client.post(f'/api/web/establishments/{self.establishment.id}/comments/create/', + response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/comments/create/', data=data) comment = response.json() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.get(f'/api/web/establishments/{self.establishment.id}/comments/{comment["id"]}/', + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -448,12 +448,12 @@ class EstablishmentWebCommentsTests(ChildTestCase): } response = self.client.patch( - f'/api/web/establishments/{self.establishment.id}/comments/{comment["id"]}/', + f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) response = self.client.delete( - f'/api/web/establishments/{self.establishment.id}/comments/{comment["id"]}/', + f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -466,13 +466,13 @@ class EstablishmentWebFavoriteTests(ChildTestCase): "object_id": self.establishment.id } - response = self.client.post(f'/api/web/establishments/{self.establishment.id}/favorites/', + response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) response = self.client.delete( - f'/api/web/establishments/{self.establishment.id}/favorites/', + f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index bd826e4e..65a56a5b 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -142,6 +142,7 @@ class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView): class EstablishmentCarouselCreateDestroyView(CarouselCreateDestroyMixinView): """View for create/destroy establishment from carousel.""" + lookup_field = 'slug' _model = models.Establishment serializer_class = serializers.EstablishmentCarouselCreateSerializer diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index 2cc818a9..27590b18 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -37,6 +37,7 @@ class TagBackOfficeSerializer(TagBaseSerializer): 'category' ) + class TagCategoryProductSerializer(serializers.ModelSerializer): """SHORT Serializer for TagCategory""" diff --git a/apps/utils/views.py b/apps/utils/views.py index db10dbfd..e158357e 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -158,10 +158,10 @@ class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): lookup_field = 'id' def get_base_object(self): - search_kwargs = { - 'id': self.kwargs.get('pk'), - 'slug': self.kwargs.get('slug'), - } + establishment_pk = self.kwargs.get('pk') + establishment_slug = self.kwargs.get('slug') + + search_kwargs = {'id': establishment_pk} if establishment_pk else {'slug': establishment_slug} return get_object_or_404(self._model, **search_kwargs) def get_object(self): @@ -175,6 +175,9 @@ class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): # self.check_object_permissions(self.request, carousels) return carousels + def perform_destroy(self, instance): + instance.delete() + self.es_update_base_object() # BackOffice user`s views & viewsets class BindObjectMixin: From 72d52e5c19747094183e4c115e3f34d40c967dc9 Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 4 Dec 2019 15:51:03 +0300 Subject: [PATCH 152/191] Removed unusable destroy function --- apps/utils/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/utils/views.py b/apps/utils/views.py index e158357e..9ac8ca60 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -175,9 +175,6 @@ class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): # self.check_object_permissions(self.request, carousels) return carousels - def perform_destroy(self, instance): - instance.delete() - self.es_update_base_object() # BackOffice user`s views & viewsets class BindObjectMixin: From b2fb341c101e28410a71624b7ecf014c78a23347 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 17:13:38 +0300 Subject: [PATCH 153/191] fix wines faceted search --- apps/search_indexes/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index a5b952d7..5fa845f2 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -346,7 +346,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): faceted_search_fields = { 'tag': { - 'field': 'wine_colors.id', + 'field': 'tags.id', 'enabled': True, 'facet': TermsFacet, 'options': { From 1ae11f0b617894edb71d3ebdf904cafa0b152386 Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 4 Dec 2019 17:27:49 +0300 Subject: [PATCH 154/191] System tags removed --- apps/establishment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 140188d7..b708de63 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -447,7 +447,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, def visible_tags(self): return super().visible_tags \ .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', - 'business_tag', 'business_tags_de']) \ + 'business_tag', 'business_tags_de', 'tag', ]) \ \ # todo: recalculate toque_number From d0378ad14e49700971b1bfc34fbba6546870b345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Wed, 4 Dec 2019 17:27:51 +0300 Subject: [PATCH 155/191] Fix options --- apps/establishment/views/back.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index d3afbf2e..6fa4d821 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -165,7 +165,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView): pagination_class = None -class EstablishmentEmployeeListView(generics.ListAPIView): +class EstablishmentEmployeeListView(generics.ListCreateAPIView): """Establishment emplyoees list view.""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.EstablishmentEmployeeBackSerializer From dee353199dd38340ed3dead4a740817a23e3c16c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 17:30:54 +0300 Subject: [PATCH 156/191] add schedule to establishments list --- apps/establishment/serializers/common.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 37432a21..e6e489ee 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -405,6 +405,12 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" address = AddressDetailSerializer(read_only=True) + schedule = ScheduleRUDSerializer(many=True, allow_null=True) + + class Meta(EstablishmentBaseSerializer.Meta): + fields = EstablishmentBaseSerializer.Meta.fields + [ + 'schedule', + ] class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): From 69b40f21b7b1683839c844675d66b11949531c3a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 18:26:03 +0300 Subject: [PATCH 157/191] last comment for mobile establishment detail --- apps/establishment/models.py | 14 ++++++++++++++ apps/establishment/serializers/common.py | 13 +++++++++++++ apps/establishment/urls/common.py | 1 - apps/establishment/urls/mobile.py | 3 ++- apps/establishment/urls/web.py | 6 +++++- apps/establishment/views/web.py | 7 +++++++ 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index b708de63..6c9080f0 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -251,6 +251,15 @@ class EstablishmentQuerySet(models.QuerySet): return self.filter(id__in=subquery_filter_by_distance) \ .order_by('-reviews__published_at') + def prefetch_comments(self): + """Prefetch last comment.""" + from comment.models import Comment + return self.prefetch_related( + models.Prefetch('comments', + queryset=Comment.objects.exclude(is_publish=False).order_by('-created'), + to_attr='comments_prefetched') + ) + def prefetch_actual_employees(self): """Prefetch actual employees.""" return self.prefetch_related( @@ -614,6 +623,11 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, def artisan_category_indexing(self): return self.tags.filter(category__index_name='shop_category') + @property + def last_comment(self): + if hasattr(self, 'comments_prefetched'): + return self.comments_prefetched[0] + class EstablishmentNoteQuerySet(models.QuerySet): """QuerySet for model EstablishmentNote.""" diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index e6e489ee..042a0a96 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -401,6 +401,19 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): ] +class MobileEstablishmentDetailSerializer(EstablishmentDetailSerializer): + """Serializer for Establishment model for mobiles.""" + + last_comment = comment_serializers.CommentRUDSerializer(allow_null=True) + + class Meta(EstablishmentDetailSerializer.Meta): + """Meta class.""" + + fields = EstablishmentDetailSerializer.Meta.fields + [ + 'last_comment', + ] + + class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index e37c38f8..faa34bd9 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -9,7 +9,6 @@ urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(), name='recent-reviews'), - path('slug//', views.EstablishmentRetrieveView.as_view(), name='detail'), path('slug//similar/', views.EstablishmentSimilarListView.as_view(), name='similar'), path('slug//comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), path('slug//comments/create/', views.EstablishmentCommentCreateView.as_view(), diff --git a/apps/establishment/urls/mobile.py b/apps/establishment/urls/mobile.py index 2803be18..165323a9 100644 --- a/apps/establishment/urls/mobile.py +++ b/apps/establishment/urls/mobile.py @@ -5,7 +5,8 @@ from establishment import views from establishment.urls.common import urlpatterns as common_urlpatterns urlpatterns = [ - path('geo/', views.EstablishmentNearestRetrieveView.as_view(), name='nearest-establishments-list') + path('geo/', views.EstablishmentNearestRetrieveView.as_view(), name='nearest-establishments-list'), + path('slug//', views.EstablishmentMobileRetrieveView.as_view(), name='mobile-detail'), ] urlpatterns.extend(common_urlpatterns) diff --git a/apps/establishment/urls/web.py b/apps/establishment/urls/web.py index b4d1942d..4fa6595e 100644 --- a/apps/establishment/urls/web.py +++ b/apps/establishment/urls/web.py @@ -1,7 +1,11 @@ """Establishment app web urlconf.""" from establishment.urls.common import urlpatterns as common_urlpatterns +from django.urls import path +from establishment import views -urlpatterns = [] +urlpatterns = [ + path('slug//', views.EstablishmentRetrieveView.as_view(), name='web-detail'), +] urlpatterns.extend(common_urlpatterns) diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 15421ee7..ba5cb23b 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -51,6 +51,13 @@ class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView return super().get_queryset().with_extended_related() +class EstablishmentMobileRetrieveView(EstablishmentRetrieveView): + serializer_class = serializers.MobileEstablishmentDetailSerializer + + def get_queryset(self): + return super().get_queryset().prefetch_comments() + + class EstablishmentRecentReviewListView(EstablishmentListView): """List view for last reviewed establishments.""" From e0ad819caa943a2731d5ef788d0ca220a2419823 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 18:50:13 +0300 Subject: [PATCH 158/191] fix empty comments --- apps/establishment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 6c9080f0..c598de55 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -625,7 +625,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, @property def last_comment(self): - if hasattr(self, 'comments_prefetched'): + if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched): return self.comments_prefetched[0] From f44a4bb29d0626edbeedcf10e193d103b64fc649 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 4 Dec 2019 19:37:05 +0300 Subject: [PATCH 159/191] Establishments favs type --- apps/establishment/serializers/common.py | 2 ++ apps/favorites/views.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 042a0a96..da58225b 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -419,10 +419,12 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): address = AddressDetailSerializer(read_only=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True) + establishment_type = EstablishmentTypeGeoSerializer() class Meta(EstablishmentBaseSerializer.Meta): fields = EstablishmentBaseSerializer.Meta.fields + [ 'schedule', + 'establishment_type', ] diff --git a/apps/favorites/views.py b/apps/favorites/views.py index 444048ff..53a05469 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -29,7 +29,7 @@ class FavoritesEstablishmentListView(generics.ListAPIView): def get_queryset(self): """Override get_queryset method""" return Establishment.objects.filter(favorites__user=self.request.user) \ - .order_by('-favorites') + .order_by('-favorites').with_base_related() class FavoritesProductListView(generics.ListAPIView): From 29ff10fb4becbbf8ab329c3a35ef33e94db2c0a6 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 4 Dec 2019 20:13:09 +0300 Subject: [PATCH 160/191] added wine origins --- apps/establishment/admin.py | 1 + .../commands/add_establishment_wine_origin.py | 30 +++++++++++ apps/establishment/models.py | 7 ++- apps/establishment/serializers/common.py | 7 +++ apps/location/admin.py | 12 +++++ ...mentwineoriginaddress_wineoriginaddress.py | 42 +++++++++++++++ apps/location/models.py | 53 +++++++++++++++++- apps/location/serializers/common.py | 54 +++++++++++++++++-- apps/product/admin.py | 1 + .../commands/add_wine_origin_address.py | 44 +++++++++++++++ .../migrations/0019_auto_20191204_1420.py | 21 ++++++++ apps/product/models.py | 15 ++---- apps/product/serializers/common.py | 10 ++-- apps/transfer/serializers/product.py | 24 +++++++-- 14 files changed, 294 insertions(+), 27 deletions(-) create mode 100644 apps/establishment/management/commands/add_establishment_wine_origin.py create mode 100644 apps/location/migrations/0031_establishmentwineoriginaddress_wineoriginaddress.py create mode 100644 apps/product/management/commands/add_wine_origin_address.py create mode 100644 apps/product/migrations/0019_auto_20191204_1420.py diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index 45716f32..2c94676f 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -136,3 +136,4 @@ class SocialNetworkAdmin(BaseModelAdminMixin, admin.ModelAdmin): class CompanyAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Admin conf for Company model.""" raw_id_fields = ['establishment', 'address', ] + diff --git a/apps/establishment/management/commands/add_establishment_wine_origin.py b/apps/establishment/management/commands/add_establishment_wine_origin.py new file mode 100644 index 00000000..cbb37907 --- /dev/null +++ b/apps/establishment/management/commands/add_establishment_wine_origin.py @@ -0,0 +1,30 @@ +from django.core.management.base import BaseCommand + +from location.models import WineOriginAddress, EstablishmentWineOriginAddress +from product.models import Product + + +class Command(BaseCommand): + help = 'Add to establishment wine origin object.' + + def handle(self, *args, **kwarg): + create_counter = 0 + + for product in Product.objects.exclude(establishment__isnull=True): + establishment = product.establishment + if product.wine_origins.exists(): + for wine_origin in product.wine_origins.all(): + wine_region = wine_origin.wine_region + wine_sub_region = wine_origin.wine_sub_region + if not EstablishmentWineOriginAddress.objects.filter(establishment=establishment, + wine_region=wine_region, + wine_sub_region=wine_sub_region) \ + .exists(): + EstablishmentWineOriginAddress.objects.create( + establishment=establishment, + wine_region=wine_origin.wine_region, + wine_sub_region=wine_origin.wine_sub_region, + ) + create_counter += 1 + + self.stdout.write(self.style.WARNING(f'COUNT CREATED OBJECTS: {create_counter}')) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index cb490aa4..7b028365 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,8 +1,8 @@ """Establishment models.""" from datetime import datetime from functools import reduce -from typing import List from operator import or_ +from typing import List import elasticsearch_dsl from django.conf import settings @@ -22,6 +22,7 @@ from timezone_field import TimeZoneField from collection.models import Collection from location.models import Address +from location.models import WineOriginAddressMixin from main.models import Award, Currency from review.models import Review from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, @@ -590,6 +591,10 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, if qs.exists(): return qs.first().image + @property + def wine_origins_unique(self): + return self.wine_origins.distinct('wine_region') + class EstablishmentNoteQuerySet(models.QuerySet): """QuerySet for model EstablishmentNote.""" diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index cb102ff1..6822e1e1 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -16,6 +16,8 @@ from utils import exceptions as utils_exceptions from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) +from location.serializers import EstablishmentWineRegionBaseSerializer, \ + EstablishmentWineOriginBaseSerializer class ContactPhonesSerializer(serializers.ModelSerializer): @@ -283,6 +285,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') image = serializers.URLField(source='image_url', read_only=True) + wine_regions = EstablishmentWineRegionBaseSerializer(many=True, source='wine_origins_unique', + read_only=True, allow_null=True) preview_image = serializers.URLField(source='preview_image_url', allow_null=True, read_only=True) @@ -311,6 +315,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): 'image', 'preview_image', 'new_image', + 'wine_regions', ] @@ -365,6 +370,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): range_price_carte = RangePriceSerializer(read_only=True) vintage_year = serializers.ReadOnlyField() gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True) + wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True) class Meta(EstablishmentBaseSerializer.Meta): """Meta class.""" @@ -391,6 +397,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): 'transportation', 'vintage_year', 'gallery', + 'wine_origins', ] diff --git a/apps/location/admin.py b/apps/location/admin.py index a7610a65..0f7a3a8d 100644 --- a/apps/location/admin.py +++ b/apps/location/admin.py @@ -45,3 +45,15 @@ class AddressAdmin(admin.OSMGeoAdmin): def geo_lat(self, item): if isinstance(item.coordinates, Point): return item.coordinates.y + + +@admin.register(models.EstablishmentWineOriginAddress) +class EstablishmentWineOriginAddress(admin.ModelAdmin): + """Admin model for EstablishmentWineOriginAddress.""" + raw_id_fields = ['establishment', ] + + +@admin.register(models.WineOriginAddress) +class WineOriginAddress(admin.ModelAdmin): + """Admin page for model WineOriginAddress.""" + raw_id_fields = ['product', ] diff --git a/apps/location/migrations/0031_establishmentwineoriginaddress_wineoriginaddress.py b/apps/location/migrations/0031_establishmentwineoriginaddress_wineoriginaddress.py new file mode 100644 index 00000000..c583399e --- /dev/null +++ b/apps/location/migrations/0031_establishmentwineoriginaddress_wineoriginaddress.py @@ -0,0 +1,42 @@ +# Generated by Django 2.2.7 on 2019-12-04 14:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0067_auto_20191122_1244'), + ('product', '0019_auto_20191204_1420'), + ('location', '0030_auto_20191120_1010'), + ] + + operations = [ + migrations.CreateModel( + name='WineOriginAddress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wine_origins', to='product.Product', verbose_name='product')), + ('wine_region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.WineRegion', verbose_name='wine region')), + ('wine_sub_region', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='location.WineSubRegion', verbose_name='wine sub region')), + ], + options={ + 'verbose_name': 'wine origin address', + 'verbose_name_plural': 'wine origin addresses', + }, + ), + migrations.CreateModel( + name='EstablishmentWineOriginAddress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wine_origins', to='establishment.Establishment', verbose_name='product')), + ('wine_region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.WineRegion', verbose_name='wine region')), + ('wine_sub_region', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='location.WineSubRegion', verbose_name='wine sub region')), + ], + options={ + 'verbose_name': 'establishment wine origin address', + 'verbose_name_plural': 'establishment wine origin addresses', + }, + ), + ] diff --git a/apps/location/models.py b/apps/location/models.py index 50f09b92..fd023443 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -192,9 +192,9 @@ class WineRegionQuerySet(models.QuerySet): def with_sub_region_related(self): return self.prefetch_related('wine_sub_region') - def having_wines(self, value = True): + def having_wines(self, value=True): """Return qs with regions, which have any wine related to them""" - return self.exclude(wines__isnull=value) + return self.exclude(wineoriginaddress__product__isnull=value) class WineRegion(TranslatedFieldsMixin, models.Model): @@ -278,6 +278,55 @@ class WineVillage(models.Model): return self.name +class WineOriginAddressMixin(models.Model): + """Model for wine origin address.""" + wine_region = models.ForeignKey('location.WineRegion', on_delete=models.CASCADE, + verbose_name=_('wine region')) + wine_sub_region = models.ForeignKey('location.WineSubRegion', on_delete=models.CASCADE, + blank=True, null=True, default=None, + verbose_name=_('wine sub region')) + + class Meta: + """Meta class.""" + abstract = True + + +class EstablishmentWineOriginAddressQuerySet(models.QuerySet): + """QuerySet for EstablishmentWineOriginAddress model.""" + + +class EstablishmentWineOriginAddress(WineOriginAddressMixin): + """Establishment wine origin address model.""" + establishment = models.ForeignKey('establishment.Establishment', on_delete=models.CASCADE, + related_name='wine_origins', + verbose_name=_('product')) + + objects = EstablishmentWineOriginAddressQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('establishment wine origin address') + verbose_name_plural = _('establishment wine origin addresses') + + +class WineOriginAddressQuerySet(models.QuerySet): + """QuerySet for WineOriginAddress model.""" + + +class WineOriginAddress(WineOriginAddressMixin): + """Wine origin address model.""" + product = models.ForeignKey('product.Product', on_delete=models.CASCADE, + related_name='wine_origins', + verbose_name=_('product')) + + objects = WineOriginAddressQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('wine origin address') + verbose_name_plural = _('wine origin addresses') + + # todo: Make recalculate price levels @receiver(post_save, sender=Country) def run_recalculate_price_levels(sender, instance, **kwargs): diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index fadb6f6c..6255e67f 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -191,10 +191,58 @@ class WineSubRegionBaseSerializer(serializers.ModelSerializer): ] -class WineRegionSerializer(WineRegionBaseSerializer): - """Wine region w/ subregion serializer""" +class EstablishmentWineRegionBaseSerializer(serializers.ModelSerializer): + """Establishment wine region origin serializer.""" - wine_sub_region = WineSubRegionBaseSerializer(allow_null=True, many=True) + id = serializers.IntegerField(source='wine_region.id') + name = serializers.CharField(source='wine_region.name') + country = CountrySerializer(source='wine_region.country') + + class Meta: + """Meta class.""" + model = models.EstablishmentWineOriginAddress + fields = [ + 'id', + 'name', + 'country', + ] + + +class EstablishmentWineOriginBaseSerializer(serializers.ModelSerializer): + """Serializer for intermediate model EstablishmentWineOrigin.""" + wine_region = WineRegionBaseSerializer() + wine_sub_region = WineSubRegionBaseSerializer(allow_null=True) + + class Meta: + """Meta class.""" + model = models.EstablishmentWineOriginAddress + fields = [ + 'wine_region', + 'wine_sub_region', + ] + + +class WineOriginRegionBaseSerializer(EstablishmentWineRegionBaseSerializer): + """Product wine region origin serializer.""" + + class Meta(EstablishmentWineRegionBaseSerializer.Meta): + """Meta class.""" + model = models.WineOriginAddress + + +class WineOriginBaseSerializer(EstablishmentWineOriginBaseSerializer): + """Serializer for intermediate model ProductWineOrigin.""" + + class Meta(EstablishmentWineOriginBaseSerializer.Meta): + """Meta class.""" + model = models.WineOriginAddress + + +class WineRegionSerializer(serializers.ModelSerializer): + """Wine region w/ sub region serializer""" + + wine_sub_region = WineSubRegionBaseSerializer(source='wine_region.wine_sub_region', + allow_null=True, many=True) class Meta(WineRegionBaseSerializer.Meta): fields = WineRegionBaseSerializer.Meta.fields + [ diff --git a/apps/product/admin.py b/apps/product/admin.py index 3becfb2d..182be5c3 100644 --- a/apps/product/admin.py +++ b/apps/product/admin.py @@ -1,5 +1,6 @@ """Product admin conf.""" from django.contrib import admin + from utils.admin import BaseModelAdminMixin from .models import Product, ProductType, ProductSubType, ProductGallery, Unit diff --git a/apps/product/management/commands/add_wine_origin_address.py b/apps/product/management/commands/add_wine_origin_address.py new file mode 100644 index 00000000..d47d7412 --- /dev/null +++ b/apps/product/management/commands/add_wine_origin_address.py @@ -0,0 +1,44 @@ +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from location.models import WineOriginAddress +from product.models import Product +from transfer.models import Products +from transfer.serializers.product import ProductSerializer + + +class Command(BaseCommand): + help = 'Add to product wine origin object.' + + def handle(self, *args, **kwarg): + def get_product(old_id: int): + if old_id: + qs = Product.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + objects_to_create = [] + products = Products.objects.exclude(wine_region_id__isnull=True) \ + .values_list('id', 'wine_sub_region_id', 'wine_region_id') + for old_id, wine_sub_region_id, wine_region_id in tqdm(products): + product = get_product(old_id) + if product: + wine_sub_region = ProductSerializer.get_wine_sub_region(wine_sub_region_id) + wine_region = ProductSerializer.get_wine_region(wine_region_id) + if wine_region: + filters = { + 'product': product, + 'wine_region': wine_region} + wine_origin_address = WineOriginAddress( + product=product, + wine_region=wine_region) + + if wine_sub_region: + filters.update({'wine_sub_region': wine_sub_region}) + wine_origin_address.wine_sub_region = wine_sub_region + + if not WineOriginAddress.objects.filter(**filters).exists(): + objects_to_create.append(wine_origin_address) + + WineOriginAddress.objects.bulk_create(objects_to_create) + self.stdout.write(self.style.WARNING(f'COUNT CREATED OBJECTS: {len(objects_to_create)}')) diff --git a/apps/product/migrations/0019_auto_20191204_1420.py b/apps/product/migrations/0019_auto_20191204_1420.py new file mode 100644 index 00000000..514fdd61 --- /dev/null +++ b/apps/product/migrations/0019_auto_20191204_1420.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.7 on 2019-12-04 14:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0018_purchasedproduct'), + ] + + operations = [ + migrations.RemoveField( + model_name='product', + name='wine_region', + ), + migrations.RemoveField( + model_name='product', + name='wine_sub_region', + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 561280fc..a6129c48 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -2,11 +2,12 @@ from django.contrib.contenttypes import fields as generic from django.contrib.gis.db import models as gis_models from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Case, When from django.utils.translation import gettext_lazy as _ -from django.core.validators import MaxValueValidator, MinValueValidator +from location.models import WineOriginAddressMixin from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin, TranslatedFieldsMixin, TJSONField, FavoritesMixin, GalleryModelMixin, IntermediateGalleryModelMixin) @@ -89,8 +90,8 @@ class ProductQuerySet(models.QuerySet): 'establishment__address__city', 'establishment__address__city__country', 'establishment__establishment_subtypes', 'product_gallery', 'gallery', 'product_type', 'subtypes', - 'classifications__classification_type', 'classifications__tags') \ - .select_related('wine_region', 'wine_sub_region') + 'classifications__classification_type', 'classifications__tags', + 'wine_origins__wine_region', 'wine_origins__wine_sub_region', ) def common(self): return self.filter(category=self.model.COMMON) @@ -176,14 +177,6 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, verbose_name=_('establishment')) public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, verbose_name=_('public mark'), ) - wine_region = models.ForeignKey('location.WineRegion', on_delete=models.PROTECT, - related_name='wines', - blank=True, null=True, default=None, - verbose_name=_('wine region')) - wine_sub_region = models.ForeignKey('location.WineSubRegion', on_delete=models.PROTECT, - related_name='wines', - blank=True, null=True, default=None, - verbose_name=_('wine sub region')) classifications = models.ManyToManyField('ProductClassification', blank=True, verbose_name=_('classifications')) diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 14eff642..983cc2ab 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -11,7 +11,7 @@ from review.serializers import ReviewShortSerializer from utils import exceptions as utils_exceptions from utils.serializers import TranslatedField, FavoritesCreateSerializer, ImageBaseSerializer from main.serializers import AwardSerializer -from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer +from location.serializers import WineOriginRegionBaseSerializer, WineOriginBaseSerializer from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer @@ -90,7 +90,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): subtypes = ProductSubTypeBaseSerializer(many=True, read_only=True) establishment_detail = EstablishmentProductShortSerializer(source='establishment', read_only=True) tags = ProductTagSerializer(source='related_tags', many=True, read_only=True) - wine_region = WineRegionBaseSerializer(read_only=True) + wine_regions = WineOriginRegionBaseSerializer(many=True, source='wine_origins', read_only=True) wine_colors = TagBaseSerializer(many=True, read_only=True) preview_image_url = serializers.URLField(allow_null=True, read_only=True) @@ -110,7 +110,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): 'vintage', 'tags', 'preview_image_url', - 'wine_region', + 'wine_regions', 'wine_colors', 'in_favorites', ] @@ -124,7 +124,7 @@ class ProductDetailSerializer(ProductBaseSerializer): awards = AwardSerializer(many=True, read_only=True) classifications = ProductClassificationBaseSerializer(many=True, read_only=True) standards = ProductStandardBaseSerializer(many=True, read_only=True) - wine_sub_region = WineSubRegionBaseSerializer(read_only=True) + wine_origins = WineOriginBaseSerializer(many=True, read_only=True) bottles_produced = TagBaseSerializer(many=True, read_only=True) sugar_contents = TagBaseSerializer(many=True, read_only=True) grape_variety = TagBaseSerializer(many=True, read_only=True) @@ -141,7 +141,7 @@ class ProductDetailSerializer(ProductBaseSerializer): 'awards', 'classifications', 'standards', - 'wine_sub_region', + 'wine_origins', 'bottles_produced', 'sugar_contents', 'image_url', diff --git a/apps/transfer/serializers/product.py b/apps/transfer/serializers/product.py index 86c6720a..90a3c884 100644 --- a/apps/transfer/serializers/product.py +++ b/apps/transfer/serializers/product.py @@ -340,6 +340,9 @@ class ProductSerializer(TransferSerializerMixin): def create(self, validated_data): qs = self.Meta.model.objects.filter(old_id=validated_data.get('old_id')) + wine_region = validated_data.pop('wine_region') + wine_sub_region = validated_data.pop('wine_sub_region') + # classifications classifications = [validated_data.pop('wine_classification', None)] # standards @@ -361,6 +364,11 @@ class ProductSerializer(TransferSerializerMixin): obj.standards.add(*[i for i in standards if i and i not in obj.standards.all()]) # adding tags obj.tags.add(*[i for i in tags if i and i not in obj.tags.all()]) + # checking wine origin address + wine_origin_address, _ = location_models.WineOriginAddress.objects.get_or_create( + product=obj, + wine_region=wine_region, + wine_sub_region=wine_sub_region) return obj def get_name(self, name, brand): @@ -390,17 +398,23 @@ class ProductSerializer(TransferSerializerMixin): if classification_qs.exists(): return classification_qs.first() - def get_wine_region(self, wine_region): + @staticmethod + def get_wine_region(wine_region): if wine_region: + old_id = wine_region if not isinstance(wine_region, transfer_models.WineLocations) \ + else wine_region.id wine_region_qs = location_models.WineRegion.objects.filter( - old_id=wine_region.id) + old_id=old_id) if wine_region_qs.exists(): return wine_region_qs.first() - def get_wine_sub_region(self, wine_sub_region_id): - if wine_sub_region_id: + @staticmethod + def get_wine_sub_region(wine_sub_region): + if wine_sub_region: + old_id = wine_sub_region if not isinstance(wine_sub_region, transfer_models.WineLocations) \ + else wine_sub_region.id sub_region_qs = location_models.WineSubRegion.objects.filter( - old_id=wine_sub_region_id) + old_id=old_id) if sub_region_qs.exists(): return sub_region_qs.first() From 1da7c40a9db257f21dc17a0b10873981462a75c8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 4 Dec 2019 20:18:47 +0300 Subject: [PATCH 161/191] fix merge --- project/settings/local.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/project/settings/local.py b/project/settings/local.py index 5a89823d..c56f9042 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -113,6 +113,3 @@ TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} ELASTICSEARCH_DSL_AUTOSYNC = False - -# INSTALLED APPS -INSTALLED_APPS.append('transfer.apps.TransferConfig') \ No newline at end of file From c0bc66c749f089b82a935654b90150a691540dc2 Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 4 Dec 2019 21:02:08 +0300 Subject: [PATCH 162/191] Hide unusable tags --- apps/establishment/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index c5da4094..e19135e5 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -440,6 +440,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, return super().visible_tags \ .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', 'business_tag', 'business_tags_de']) \ + .exclude(value__in=['rss', 'rss_selection']) \ \ # todo: recalculate toque_number From 1d9ee76a5c622c497630f6de775f1fe269fcba59 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Dec 2019 09:43:55 +0300 Subject: [PATCH 163/191] fix es for establishments --- apps/collection/serializers/common.py | 3 - apps/product/serializers/back.py | 5 +- .../search_indexes/documents/establishment.py | 64 +++++++++++++------ apps/search_indexes/documents/product.py | 38 ++++++----- apps/search_indexes/serializers.py | 21 ++++++ apps/search_indexes/views.py | 36 ++++++++--- 6 files changed, 117 insertions(+), 50 deletions(-) diff --git a/apps/collection/serializers/common.py b/apps/collection/serializers/common.py index 846236d5..1b043f3c 100644 --- a/apps/collection/serializers/common.py +++ b/apps/collection/serializers/common.py @@ -56,7 +56,4 @@ class GuideSerializer(serializers.ModelSerializer): 'name', 'start', 'end', - 'parent', - 'advertorials', - 'collection' ] diff --git a/apps/product/serializers/back.py b/apps/product/serializers/back.py index ffbf690d..55dc5ebc 100644 --- a/apps/product/serializers/back.py +++ b/apps/product/serializers/back.py @@ -62,8 +62,9 @@ class ProductBackOfficeDetailSerializer(ProductDetailSerializer): 'available', 'product_type', 'establishment', - 'wine_region', - 'wine_sub_region', + # todo: need fix + # 'wine_region', + # 'wine_sub_region', 'wine_village', 'state', ] diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index aca81f3f..c6b68ed4 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -83,33 +83,59 @@ class EstablishmentDocument(Document): multi=True) products = fields.ObjectField( properties={ - 'wine_region': fields.ObjectField(properties={ - 'id': fields.IntegerField(), - 'name': fields.KeywordField(), - 'country': fields.ObjectField(properties={ - 'id': fields.IntegerField(), - 'name': fields.ObjectField(attr='name_indexing', - properties=OBJECT_FIELD_PROPERTIES), - 'code': fields.KeywordField(), - }), - # 'coordinates': fields.GeoPointField(), - 'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES), - }), + 'wine_origins': fields.ListField( + fields.ObjectField( + properties={ + 'wine_region': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + 'country': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.ObjectField(attr='name_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'code': fields.KeywordField(), + }), + # 'coordinates': fields.GeoPointField(), + 'description': fields.ObjectField(attr='description_indexing', + properties=OBJECT_FIELD_PROPERTIES) + + }), + 'wine_sub_region': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + }), + })), 'wine_colors': fields.ObjectField( properties={ 'id': fields.IntegerField(), 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), 'value': fields.KeywordField(), }, - multi=True, - ), - 'wine_sub_region': fields.ObjectField(properties={ - 'id': fields.IntegerField(), - 'name': fields.KeywordField(), - }), - }, + multi=True,)}, multi=True ) + wine_origins = fields.ListField( + fields.ObjectField( + properties={ + 'wine_region': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + 'country': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.ObjectField(attr='name_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'code': fields.KeywordField(), + }), + # 'coordinates': fields.GeoPointField(), + 'description': fields.ObjectField(attr='description_indexing', + properties=OBJECT_FIELD_PROPERTIES) + + }), + 'wine_sub_region': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + })}) + ) schedule = fields.ListField(fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index 853f72a2..c9459dd8 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -83,22 +83,28 @@ class ProductDocument(Document): }, multi=True, ) - wine_region = fields.ObjectField(properties={ - 'id': fields.IntegerField(), - 'name': fields.KeywordField(), - 'country': fields.ObjectField(properties={ - 'id': fields.IntegerField(), - 'name': fields.ObjectField(attr='name_indexing', - properties=OBJECT_FIELD_PROPERTIES), - 'code': fields.KeywordField(), - }), - # 'coordinates': fields.GeoPointField(), - 'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES), - }) - wine_sub_region = fields.ObjectField(properties={ - 'id': fields.IntegerField(), - 'name': fields.KeywordField(), - }) + wine_origins = fields.ListField( + fields.ObjectField( + properties={ + 'wine_region': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + 'country': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.ObjectField(attr='name_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'code': fields.KeywordField(), + }), + # 'coordinates': fields.GeoPointField(), + 'description': fields.ObjectField(attr='description_indexing', + properties=OBJECT_FIELD_PROPERTIES) + + }), + 'wine_sub_region': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + })}) + ) classifications = fields.ObjectField( # TODO properties={ 'classification_type': fields.ObjectField(properties={}), diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 7bd3995a..4357d8c1 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -69,6 +69,16 @@ class WineRegionDocumentSerializer(serializers.Serializer): return instance.wine_region if instance and instance.wine_region else None +class WineSubRegionDocumentSerializer(serializers.Serializer): + """Wine region ES document serializer.""" + + id = serializers.IntegerField() + name = serializers.CharField() + + def get_attribute(self, instance): + return instance.wine_sub_region if instance and instance.wine_sub_region else None + + class TagDocumentSerializer(serializers.Serializer): """Tag ES document serializer,""" @@ -223,6 +233,13 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer): return get_translated_value(obj.subtitle) +class WineOriginSerializer(serializers.Serializer): + """Wine origin serializer.""" + + wine_region = WineRegionDocumentSerializer() + wine_sub_region = WineSubRegionDocumentSerializer(allow_null=True) + + class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): """Establishment document serializer.""" @@ -234,6 +251,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): restaurant_cuisine = TagsDocumentSerializer(many=True, allow_null=True) artisan_category = TagsDocumentSerializer(many=True, allow_null=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True) + wine_origins = WineOriginSerializer(many=True) class Meta: """Meta class.""" @@ -259,6 +277,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'works_evening', 'works_at_weekday', 'tz', + 'wine_origins', # 'works_now', # 'collections', # 'establishment_type', @@ -276,6 +295,7 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer): grape_variety = TagDocumentSerializer(many=True) product_type = ProductTypeDocumentSerializer(allow_null=True) establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True) + wine_origins = WineOriginSerializer(many=True) class Meta: """Meta class.""" @@ -302,4 +322,5 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'establishment_detail', 'average_price', 'created', + 'wine_origins', ) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 5fa845f2..0e93d8fc 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -158,7 +158,15 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): }, }, 'wine_region_id': { - 'field': 'products.wine_region.id', + 'field': 'wine_origins.wine_region.id', + 'facet': TermsFacet, + 'enabled': True, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, + }, + 'wine_sub_region_id': { + 'field': 'wine_origins.wine_sub_region.id', 'facet': TermsFacet, 'enabled': True, 'options': { @@ -213,14 +221,14 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): ], }, 'wine_region_id': { - 'field': 'products.wine_region.id', + 'field': 'wine_origins.wine_region.id', 'lookups': [ constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_EXCLUDE, ], }, 'wine_sub_region_id': { - 'field': 'products.wine_sub_region_id', + 'field': 'wine_origins.wine_sub_region.id', 'lookups': [ constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_EXCLUDE, @@ -354,13 +362,21 @@ class ProductDocumentViewSet(BaseDocumentViewSet): }, }, 'wine_region_id': { - 'field': 'wine_region.id', - 'enabled': True, + 'field': 'wine_origins.wine_region.id', 'facet': TermsFacet, + 'enabled': True, 'options': { 'size': utils.FACET_MAX_RESPONSE, }, }, + 'wine_sub_region_id': { + 'field': 'wine_origins.wine_sub_region.id', + 'facet': TermsFacet, + 'enabled': True, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, + } } translated_search_fields = ( @@ -384,14 +400,14 @@ class ProductDocumentViewSet(BaseDocumentViewSet): ], }, 'wine_region_id': { - 'field': 'wine_region.id', + 'field': 'wine_origins.wine_region.id', 'lookups': [ constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_EXCLUDE, ], }, 'wine_sub_region_id': { - 'field': 'wine_sub_region_id', + 'field': 'wine_origins.wine_sub_region.id', 'lookups': [ constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_EXCLUDE, @@ -404,9 +420,9 @@ class ProductDocumentViewSet(BaseDocumentViewSet): constants.LOOKUP_QUERY_EXCLUDE, ] }, - 'wine_from_country_code': { - 'field': 'wine_region.country.code', - }, + # 'wine_from_country_code': { + # 'field': 'wine_origins.wine_region.country.code', + # }, 'for_establishment': { 'field': 'establishment.slug', }, From d114b93b14445e2b313b0486a4106488ca60c65e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Dec 2019 09:48:09 +0300 Subject: [PATCH 164/191] fix es for products --- apps/search_indexes/serializers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 4357d8c1..a43ebaf7 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -290,7 +290,6 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer): tags = TagsDocumentSerializer(many=True, source='related_tags') subtypes = ProductSubtypeDocumentSerializer(many=True, allow_null=True) - wine_region = WineRegionDocumentSerializer(allow_null=True) wine_colors = TagDocumentSerializer(many=True) grape_variety = TagDocumentSerializer(many=True) product_type = ProductTypeDocumentSerializer(allow_null=True) @@ -316,7 +315,6 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'tags', 'product_type', 'subtypes', - 'wine_region', 'wine_colors', 'grape_variety', 'establishment_detail', From 1e0044cbd154729b29d52c7c56103a1d2cab1d48 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 5 Dec 2019 12:11:44 +0300 Subject: [PATCH 165/191] artisans tags for favs (cherry picked from commit 7081e52) --- apps/establishment/serializers/common.py | 2 ++ apps/favorites/views.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 4a419fee..23fc24df 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -427,11 +427,13 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): address = AddressDetailSerializer(read_only=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True) establishment_type = EstablishmentTypeGeoSerializer() + artisan_category = TagBaseSerializer(many=True, allow_null=True) class Meta(EstablishmentBaseSerializer.Meta): fields = EstablishmentBaseSerializer.Meta.fields + [ 'schedule', 'establishment_type', + 'artisan_category', ] diff --git a/apps/favorites/views.py b/apps/favorites/views.py index 53a05469..bee25ced 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -29,7 +29,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView): def get_queryset(self): """Override get_queryset method""" return Establishment.objects.filter(favorites__user=self.request.user) \ - .order_by('-favorites').with_base_related() + .order_by('-favorites').with_base_related() \ + .with_certain_tag_category_related('shop_category', 'artisan_category') class FavoritesProductListView(generics.ListAPIView): From ca80f7a7971b1f6b04233c29faa53f3854c8c153 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 5 Dec 2019 13:14:27 +0300 Subject: [PATCH 166/191] possible fix center calculation --- apps/search_indexes/filters.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 644b47fd..cc2bb55a 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -13,24 +13,24 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): @staticmethod def calculate_center(first, second): - if second[1] < 0 < first[1]: - reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) + if second[0] < 0 < first[0]: + reverse_first, reverse_second = 180 - abs(first[0]), 180 - abs(second[0]) diff = (reverse_first + reverse_second) / 2 if reverse_first < reverse_second: - result_part = -180 + (180 + second[1] - diff) + result_part = -180 + (180 + second[0] - diff) else: - result_part = 180 - (180 - first[1] - diff) + result_part = 180 - (180 - first[0] - diff) - elif second[1] < 0 > first[1] or second[1] > 0 < first[1]: + elif second[0] < 0 > first[0] or second[0] > 0 < first[0]: reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) result_part = ((reverse_first + reverse_second) / 2) * (-1 + (second[1] < 0) * 2) else: - result_part = (first[1] + second[1]) / 2 + result_part = (first[0] + second[0]) / 2 - return (first[0] + second[0]) / 2, result_part + return result_part, (first[1] + second[1]) / 2 def filter_queryset(self, request, queryset, view): ret = super().filter_queryset(request, queryset, view) From 66fc2fae995d3dc5a61d69a1414c732261dd2609 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 5 Dec 2019 13:15:51 +0300 Subject: [PATCH 167/191] Revert "possible fix center calculation" This reverts commit ca80f7a --- apps/search_indexes/filters.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index cc2bb55a..644b47fd 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -13,24 +13,24 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): @staticmethod def calculate_center(first, second): - if second[0] < 0 < first[0]: - reverse_first, reverse_second = 180 - abs(first[0]), 180 - abs(second[0]) + if second[1] < 0 < first[1]: + reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) diff = (reverse_first + reverse_second) / 2 if reverse_first < reverse_second: - result_part = -180 + (180 + second[0] - diff) + result_part = -180 + (180 + second[1] - diff) else: - result_part = 180 - (180 - first[0] - diff) + result_part = 180 - (180 - first[1] - diff) - elif second[0] < 0 > first[0] or second[0] > 0 < first[0]: + elif second[1] < 0 > first[1] or second[1] > 0 < first[1]: reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) result_part = ((reverse_first + reverse_second) / 2) * (-1 + (second[1] < 0) * 2) else: - result_part = (first[0] + second[0]) / 2 + result_part = (first[1] + second[1]) / 2 - return result_part, (first[1] + second[1]) / 2 + return (first[0] + second[0]) / 2, result_part def filter_queryset(self, request, queryset, view): ret = super().filter_queryset(request, queryset, view) From 3e7922e0fcae0a978979637f3194725f07bb0a68 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 5 Dec 2019 13:30:43 +0300 Subject: [PATCH 168/191] temp fix --- apps/search_indexes/filters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 644b47fd..3d55bdb4 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -30,7 +30,8 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): else: result_part = (first[1] + second[1]) / 2 - return (first[0] + second[0]) / 2, result_part + # return (first[0] + second[0]) / 2, result_part + return (first[0] + second[0]) / 2, (first[1] + second[1]) / 2 def filter_queryset(self, request, queryset, view): ret = super().filter_queryset(request, queryset, view) From 3afb51d4728b9d760f31bdbd40c49899d03546a5 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Dec 2019 14:11:46 +0300 Subject: [PATCH 169/191] fix establishment admin page --- apps/establishment/admin.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index 2c94676f..81301ddc 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -35,6 +35,7 @@ class ContactPhoneInline(admin.TabularInline): class GalleryImageInline(admin.TabularInline): """Gallery image inline admin.""" model = models.EstablishmentGallery + raw_id_fields = ['image', ] extra = 0 @@ -61,17 +62,20 @@ class ProductInline(admin.TabularInline): class CompanyInline(admin.TabularInline): model = models.Company + raw_id_fields = ['establishment', 'address'] extra = 0 class EstablishmentNote(admin.TabularInline): model = models.EstablishmentNote extra = 0 + raw_id_fields = ['user', ] -class PurchasedProduct(admin.TabularInline): +class PurchasedProductInline(admin.TabularInline): model = PurchasedProduct extra = 0 + raw_id_fields = ['product', ] @admin.register(models.Establishment) @@ -80,13 +84,12 @@ class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin): list_display = ['id', '__str__', 'image_tag', ] search_fields = ['id', 'name', 'index_name', 'slug'] list_filter = ['public_mark', 'toque_number'] - inlines = [GalleryImageInline, CompanyInline, EstablishmentNote, - PurchasedProduct] - + inlines = [CompanyInline, EstablishmentNote, GalleryImageInline, + PurchasedProductInline, ] # inlines = [ # AwardInline, ContactPhoneInline, ContactEmailInline, # ReviewInline, CommentInline, ProductInline] - raw_id_fields = ('address',) + raw_id_fields = ('address', 'collections', 'tags', 'schedule') @admin.register(models.Position) From fe101f3cb062945340175eabefff2ba41f2a4225 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 5 Dec 2019 14:16:44 +0300 Subject: [PATCH 170/191] try another calc strategy (cherry picked from commit 9ab17f7) --- apps/search_indexes/filters.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 3d55bdb4..857d44d2 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -13,25 +13,13 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): @staticmethod def calculate_center(first, second): - if second[1] < 0 < first[1]: - reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) - diff = (reverse_first + reverse_second) / 2 - - if reverse_first < reverse_second: - result_part = -180 + (180 + second[1] - diff) - - else: - result_part = 180 - (180 - first[1] - diff) - - elif second[1] < 0 > first[1] or second[1] > 0 < first[1]: - reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) - result_part = ((reverse_first + reverse_second) / 2) * (-1 + (second[1] < 0) * 2) - + if second[1] < first[1]: + res_longtitude = first[1] + (first[1] - second[1]) / 2 else: - result_part = (first[1] + second[1]) / 2 + res_longtitude = first[1] + (second[1] - first[1]) / 2 # return (first[0] + second[0]) / 2, result_part - return (first[0] + second[0]) / 2, (first[1] + second[1]) / 2 + return (first[0] + second[0]) / 2, res_longtitude def filter_queryset(self, request, queryset, view): ret = super().filter_queryset(request, queryset, view) From aa8304acaf4a9ebf2d5d3a9b3d65b959c344ef9c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 5 Dec 2019 14:19:56 +0300 Subject: [PATCH 171/191] Revert "try another calc strategy" This reverts commit fe101f3 --- apps/search_indexes/filters.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 857d44d2..3d55bdb4 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -13,13 +13,25 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): @staticmethod def calculate_center(first, second): - if second[1] < first[1]: - res_longtitude = first[1] + (first[1] - second[1]) / 2 + if second[1] < 0 < first[1]: + reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) + diff = (reverse_first + reverse_second) / 2 + + if reverse_first < reverse_second: + result_part = -180 + (180 + second[1] - diff) + + else: + result_part = 180 - (180 - first[1] - diff) + + elif second[1] < 0 > first[1] or second[1] > 0 < first[1]: + reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) + result_part = ((reverse_first + reverse_second) / 2) * (-1 + (second[1] < 0) * 2) + else: - res_longtitude = first[1] + (second[1] - first[1]) / 2 + result_part = (first[1] + second[1]) / 2 # return (first[0] + second[0]) / 2, result_part - return (first[0] + second[0]) / 2, res_longtitude + return (first[0] + second[0]) / 2, (first[1] + second[1]) / 2 def filter_queryset(self, request, queryset, view): ret = super().filter_queryset(request, queryset, view) From f9479dd7a61e0761e3d8ab5322c5c932a76372c0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 5 Dec 2019 14:30:41 +0300 Subject: [PATCH 172/191] calc center again --- apps/search_indexes/filters.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 3d55bdb4..63f1dbb8 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -13,25 +13,13 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend): @staticmethod def calculate_center(first, second): - if second[1] < 0 < first[1]: - reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) - diff = (reverse_first + reverse_second) / 2 - - if reverse_first < reverse_second: - result_part = -180 + (180 + second[1] - diff) - - else: - result_part = 180 - (180 - first[1] - diff) - - elif second[1] < 0 > first[1] or second[1] > 0 < first[1]: - reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1]) - result_part = ((reverse_first + reverse_second) / 2) * (-1 + (second[1] < 0) * 2) - + if second[1] < first[1]: + res_longtitude = first[1] + (360 + abs(first[1]) - abs(second[1])) / 2 else: - result_part = (first[1] + second[1]) / 2 + res_longtitude = first[1] + (second[1] - first[1]) / 2 # return (first[0] + second[0]) / 2, result_part - return (first[0] + second[0]) / 2, (first[1] + second[1]) / 2 + return (first[0] + second[0]) / 2, res_longtitude def filter_queryset(self, request, queryset, view): ret = super().filter_queryset(request, queryset, view) From f06596b5b64cc6539ab5729ec4164cc5f17c7d31 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Thu, 5 Dec 2019 15:23:16 +0300 Subject: [PATCH 173/191] fix review --- apps/review/transfer_data.py | 4 ---- apps/transfer/serializers/reviews.py | 7 +++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/review/transfer_data.py b/apps/review/transfer_data.py index af70873e..20c5712d 100644 --- a/apps/review/transfer_data.py +++ b/apps/review/transfer_data.py @@ -115,12 +115,8 @@ def transfer_product_reviews(): products = Product.objects.filter( old_id__isnull=False).values_list('old_id', flat=True) - users = User.objects.filter( - old_id__isnull=False).values_list('old_id', flat=True) - queryset = Reviews.objects.filter( product_id__in=list(products), - reviewer_id__in=list(users), ).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'product_id', 'mark', 'vintage') serialized_data = ProductReviewSerializer(data=list(queryset.values()), many=True) diff --git a/apps/transfer/serializers/reviews.py b/apps/transfer/serializers/reviews.py index 6e0db860..6d3f50b4 100644 --- a/apps/transfer/serializers/reviews.py +++ b/apps/transfer/serializers/reviews.py @@ -55,7 +55,7 @@ class ProductReviewSerializer(ReviewSerializer): product_id = serializers.IntegerField() created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') aasm_state = serializers.CharField(allow_null=True) - reviewer_id = serializers.IntegerField() + reviewer_id = serializers.IntegerField(allow_null=True) id = serializers.IntegerField() def validate(self, data): @@ -82,9 +82,8 @@ class ProductReviewSerializer(ReviewSerializer): @staticmethod def get_reviewer(data): user = User.objects.filter(old_id=data['reviewer_id']).first() - if not user: - raise ValueError(f"User account not found with old_id {data['reviewer_id']}") - return user + if user: + return user @staticmethod def get_product(data): From 202f5715f3058a79f5e6f1be5881e83bb2587fbd Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 5 Dec 2019 17:12:43 +0300 Subject: [PATCH 174/191] remove `tag` category from detail --- apps/establishment/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 2d5ba007..3cdef691 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -457,9 +457,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, def visible_tags(self): return super().visible_tags \ .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', - 'business_tag', 'business_tags_de']) \ - .exclude(value__in=['rss', 'rss_selection']) \ - \ + 'business_tag', 'business_tags_de', 'tag']) # todo: recalculate toque_number def recalculate_toque_number(self): From 73c66c7357fb9845852f564e720e497ef85e3b32 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Thu, 5 Dec 2019 17:19:38 +0300 Subject: [PATCH 175/191] add_publick_mark --- .../management/commands/add_public_mark.py | 25 +++++++++++++++++++ make_data_migration.sh | 1 + 2 files changed, 26 insertions(+) create mode 100644 apps/product/management/commands/add_public_mark.py diff --git a/apps/product/management/commands/add_public_mark.py b/apps/product/management/commands/add_public_mark.py new file mode 100644 index 00000000..b6bcc296 --- /dev/null +++ b/apps/product/management/commands/add_public_mark.py @@ -0,0 +1,25 @@ +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from product.models import Product + + +class Command(BaseCommand): + help = """Add public_mark to product from reviews.""" + + def handle(self, *args, **kwarg): + update_products = [] + products = Product.objects.filter( + public_mark__isnull=True, + reviews__isnull=False).distinct() + + for product in tqdm(products): + review = product.reviews.published().filter( + mark__isnull=False).order_by('-published_at').first() + if review: + product.public_mark = review.mark + update_products.append(product) + + Product.objects.bulk_update(update_products, ['public_mark', ]) + self.stdout.write( + self.style.WARNING(f'Updated products: {len(update_products)}')) \ No newline at end of file diff --git a/make_data_migration.sh b/make_data_migration.sh index bed1afb7..70e27ccb 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -2,6 +2,7 @@ ./manage.py transfer -a ./manage.py transfer -d ./manage.py transfer -e +./manage.py upd_transportation ./manage.py transfer --fill_city_gallery ./manage.py transfer -l ./manage.py transfer --product From 5d76e8a02bce6c9a5acef222ebde496ee83f63af Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 6 Dec 2019 08:20:23 +0300 Subject: [PATCH 176/191] fix address command --- .../management/commands/fix_address.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 apps/location/management/commands/fix_address.py diff --git a/apps/location/management/commands/fix_address.py b/apps/location/management/commands/fix_address.py new file mode 100644 index 00000000..5fcdf1b9 --- /dev/null +++ b/apps/location/management/commands/fix_address.py @@ -0,0 +1,29 @@ +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from location.models import Address +from transfer.models import Locations + + +class Command(BaseCommand): + help = """Fix address, clear number field and fill street_name_1 like in old db""" + + def handle(self, *args, **kwarg): + addresses = Address.objects.filter( + old_id__isnull=False + ).values_list('old_id', flat=True) + + old_addresses = Locations.objects.filter( + id__in=list(addresses) + ).values_list('id', 'address') + + update_address = [] + for idx, address in tqdm(old_addresses): + new_address = Address.objects.filter(old_id=idx).first() + if new_address: + new_address.number = 0 + new_address.street_name_1 = address + update_address.append(new_address) + + Address.objects.bulk_update(update_address, ['number', 'street_name_1']) + self.stdout.write(self.style.WARNING(f'Updated addresses: {len(update_address)}')) From 48ae6a437d8d63bf5fa911ba38bc87fe118fe218 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 6 Dec 2019 10:29:43 +0300 Subject: [PATCH 177/191] street_name_2 clear --- apps/location/management/commands/fix_address.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/location/management/commands/fix_address.py b/apps/location/management/commands/fix_address.py index 5fcdf1b9..5ecacba6 100644 --- a/apps/location/management/commands/fix_address.py +++ b/apps/location/management/commands/fix_address.py @@ -22,8 +22,9 @@ class Command(BaseCommand): new_address = Address.objects.filter(old_id=idx).first() if new_address: new_address.number = 0 + new_address.street_name_2 = '' new_address.street_name_1 = address update_address.append(new_address) - Address.objects.bulk_update(update_address, ['number', 'street_name_1']) + Address.objects.bulk_update(update_address, ['number', 'street_name_1', 'street_name_2']) self.stdout.write(self.style.WARNING(f'Updated addresses: {len(update_address)}')) From 7635eafb0906291896a3b426b99b2f0c56ac039d Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 6 Dec 2019 13:11:29 +0300 Subject: [PATCH 178/191] booking urls for mobile (cherry picked from commit 0df3425) --- apps/booking/urls/__init__.py | 0 apps/booking/{urls.py => urls/common.py} | 0 apps/booking/urls/mobile.py | 8 ++++++++ apps/booking/urls/web.py | 8 ++++++++ project/urls/mobile.py | 1 + project/urls/web.py | 2 +- 6 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 apps/booking/urls/__init__.py rename apps/booking/{urls.py => urls/common.py} (100%) create mode 100644 apps/booking/urls/mobile.py create mode 100644 apps/booking/urls/web.py diff --git a/apps/booking/urls/__init__.py b/apps/booking/urls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/booking/urls.py b/apps/booking/urls/common.py similarity index 100% rename from apps/booking/urls.py rename to apps/booking/urls/common.py diff --git a/apps/booking/urls/mobile.py b/apps/booking/urls/mobile.py new file mode 100644 index 00000000..57e58c8f --- /dev/null +++ b/apps/booking/urls/mobile.py @@ -0,0 +1,8 @@ +from booking.urls import common as common_views +app = 'booking' + + +urlpatterns_api = [] + +urlpatterns = urlpatterns_api + \ + common_views.urlpatterns diff --git a/apps/booking/urls/web.py b/apps/booking/urls/web.py new file mode 100644 index 00000000..57e58c8f --- /dev/null +++ b/apps/booking/urls/web.py @@ -0,0 +1,8 @@ +from booking.urls import common as common_views +app = 'booking' + + +urlpatterns_api = [] + +urlpatterns = urlpatterns_api + \ + common_views.urlpatterns diff --git a/project/urls/mobile.py b/project/urls/mobile.py index 4fa53ad9..4368189e 100644 --- a/project/urls/mobile.py +++ b/project/urls/mobile.py @@ -3,6 +3,7 @@ from django.urls import path, include app_name = 'mobile' urlpatterns = [ + path('booking/', include('booking.urls.web')), path('establishments/', include('establishment.urls.mobile')), path('location/', include('location.urls.mobile')), path('main/', include('main.urls.mobile')), diff --git a/project/urls/web.py b/project/urls/web.py index c5a609e2..99c12937 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -19,7 +19,7 @@ app_name = 'web' urlpatterns = [ path('account/', include('account.urls.web')), - path('booking/', include('booking.urls')), + path('booking/', include('booking.urls.web')), path('re_blocks/', include('advertisement.urls.web')), path('collections/', include('collection.urls.web')), path('establishments/', include('establishment.urls.web')), From c272dc1255e1e51b90aa9973a0196aaa9a8a8686 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Dec 2019 13:15:27 +0300 Subject: [PATCH 179/191] increase JWT token lifetime --- project/settings/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/project/settings/base.py b/project/settings/base.py index 6648f0af..1d2c5744 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -414,10 +414,10 @@ SORL_THUMBNAIL_ALIASES = { SIMPLE_JWT = { # Increase access token lifetime b.c. front-end dev's cant send multiple # requests to API in one HTTP request. - 'ACCESS_TOKEN_LIFETIME': timedelta(days=30), - 'ACCESS_TOKEN_LIFETIME_SECONDS': 21600, # 6 hours in seconds - 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), - 'REFRESH_TOKEN_LIFETIME_SECONDS': 2592000, # 30 days in seconds + 'ACCESS_TOKEN_LIFETIME': timedelta(days=182), + 'ACCESS_TOKEN_LIFETIME_SECONDS': 15770000, # 6 months + 'REFRESH_TOKEN_LIFETIME': timedelta(days=182), + 'REFRESH_TOKEN_LIFETIME_SECONDS': 15770000, # 6 months 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, @@ -453,7 +453,7 @@ NOTIFICATION_PASSWORD_TEMPLATE = 'account/password_change_email.html' # COOKIES -COOKIES_MAX_AGE = 2628000 # 30 days +COOKIES_MAX_AGE = 15730000 # 6 months SESSION_COOKIE_SAMESITE = None From 91b528feb9cf846aa7ad13f29d0686be1fa57829 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 6 Dec 2019 14:03:06 +0300 Subject: [PATCH 180/191] tags for establishments --- apps/establishment/models.py | 8 +++++++- apps/establishment/serializers/common.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 3cdef691..fb401ddd 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -457,9 +457,15 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, def visible_tags(self): return super().visible_tags \ .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', - 'business_tag', 'business_tags_de', 'tag']) + 'business_tag', 'business_tags_de']) \ + .exclude(value__in=['rss', 'rss_selection']) # todo: recalculate toque_number + @property + def visible_tags_detail(self): + """Removes some tags from detail Establishment representation""" + return self.visible_tags.exclude(category__index_name__in=['tag']) + def recalculate_toque_number(self): toque_number = 0 if self.address and self.public_mark: diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 23fc24df..4025eb96 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -369,7 +369,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees', many=True) address = AddressDetailSerializer(read_only=True) - + tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags_detail') menu = MenuSerializers(source='menu_set', many=True, read_only=True) best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) From e08296c52d1eef433da9494204e5de87b1ed5c0c Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 6 Dec 2019 14:12:26 +0300 Subject: [PATCH 181/191] address to product detail web api --- apps/establishment/serializers/common.py | 26 +++++++++++++++++++++++- apps/product/serializers/common.py | 12 +++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 23fc24df..0b2cd6be 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -17,7 +17,7 @@ from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) from location.serializers import EstablishmentWineRegionBaseSerializer, \ - EstablishmentWineOriginBaseSerializer + EstablishmentWineOriginBaseSerializer class ContactPhonesSerializer(serializers.ModelSerializer): @@ -239,6 +239,30 @@ class EstablishmentShortSerializer(serializers.ModelSerializer): ] +class _EstablishmentAddressShortSerializer(serializers.ModelSerializer): + """Short serializer for establishment.""" + city = CitySerializer(source='address.city', allow_null=True) + establishment_type = EstablishmentTypeGeoSerializer() + establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) + currency = CurrencySerializer(read_only=True) + address = AddressBaseSerializer(read_only=True) + + class Meta: + """Meta class.""" + model = models.Establishment + fields = [ + 'id', + 'name', + 'index_name', + 'slug', + 'city', + 'establishment_type', + 'establishment_subtypes', + 'currency', + 'address', + ] + + class EstablishmentProductShortSerializer(serializers.ModelSerializer): """SHORT Serializer for displaying info about an establishment on product page.""" establishment_type = EstablishmentTypeGeoSerializer() diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 983cc2ab..d794ca93 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -4,15 +4,15 @@ from rest_framework import serializers from comment.models import Comment from comment.serializers import CommentSerializer -from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer, EstablishmentProductShortSerializer -from gallery.models import Image +from establishment.serializers import EstablishmentProductShortSerializer +from establishment.serializers.common import _EstablishmentAddressShortSerializer +from location.serializers import WineOriginRegionBaseSerializer, WineOriginBaseSerializer +from main.serializers import AwardSerializer from product import models from review.serializers import ReviewShortSerializer +from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer from utils import exceptions as utils_exceptions from utils.serializers import TranslatedField, FavoritesCreateSerializer, ImageBaseSerializer -from main.serializers import AwardSerializer -from location.serializers import WineOriginRegionBaseSerializer, WineOriginBaseSerializer -from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer class ProductTagSerializer(TagBaseSerializer): @@ -119,7 +119,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): class ProductDetailSerializer(ProductBaseSerializer): """Product detail serializer.""" description_translated = TranslatedField() - establishment_detail = EstablishmentShortSerializer(source='establishment', read_only=True) + establishment_detail = _EstablishmentAddressShortSerializer(source='establishment', read_only=True) review = ReviewShortSerializer(source='last_published_review', read_only=True) awards = AwardSerializer(many=True, read_only=True) classifications = ProductClassificationBaseSerializer(many=True, read_only=True) From 1372538acf7e722a97ca4064dea3eaf00bb51654 Mon Sep 17 00:00:00 2001 From: dormantman Date: Thu, 5 Dec 2019 18:10:24 +0300 Subject: [PATCH 182/191] Merge branch 'fix/dublicate-products' of /home/dormantman/PycharmProjects/gm-backend with conflicts. --- .gitignore | 6 ++---- apps/product/views/common.py | 2 ++ project/settings/local.py | 36 +++++++++++++----------------------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 90e4f23f..ea6151b0 100644 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,8 @@ logs/ /geoip_db/ # dev -./docker-compose.override.yml +docker-compose.override.yml + celerybeat-schedule local_files celerybeat.pid -/gm_viktor.dump -/docker-compose.dump.yml -/gm_production_20191029.sql diff --git a/apps/product/views/common.py b/apps/product/views/common.py index f984a87b..764a0c97 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -26,6 +26,8 @@ class ProductListView(ProductBaseView, generics.ListAPIView): filter_class = filters.ProductFilterSet def get_queryset(self): + print(super().get_queryset()) + qs = super().get_queryset().with_extended_related() \ .by_country_code(self.request.country_code) return qs diff --git a/project/settings/local.py b/project/settings/local.py index c56f9042..fe521d92 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -30,31 +30,18 @@ MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) THUMBNAIL_DEBUG = True # ADDED TRANSFER APP -INSTALLED_APPS.append('transfer.apps.TransferConfig') +# INSTALLED_APPS.append('transfer.apps.TransferConfig') # DATABASES -DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': os.environ.get('DB_NAME'), - 'USER': os.environ.get('DB_USERNAME'), - 'PASSWORD': os.environ.get('DB_PASSWORD'), - 'HOST': os.environ.get('DB_HOSTNAME'), - 'PORT': os.environ.get('DB_PORT'), - 'OPTIONS': { - 'options': '-c search_path=gm' - }, - }, - 'legacy': { - 'ENGINE': 'django.db.backends.mysql', - # 'HOST': '172.22.0.1', - 'HOST': 'mysql_db', - 'PORT': 3306, - 'NAME': 'dev', - 'USER': 'dev', - 'PASSWORD': 'octosecret123' - }, -} +# DATABASES.update({ +# 'legacy': { +# 'ENGINE': 'django.db.backends.mysql', +# # 'HOST': '172.22.0.1', +# 'HOST': 'mysql_db', +# 'PORT': 3306, +# 'NAME': 'dev', +# 'USER': 'dev', +# 'PASSWORD': 'octosecret123'}}) # LOGGING @@ -113,3 +100,6 @@ TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} ELASTICSEARCH_DSL_AUTOSYNC = False + +# INSTALLED APPS +INSTALLED_APPS.append('transfer.apps.TransferConfig') \ No newline at end of file From 0c5140d35944328b2916b6eabaa04ec8677b23a5 Mon Sep 17 00:00:00 2001 From: dormantman Date: Fri, 6 Dec 2019 15:03:18 +0300 Subject: [PATCH 183/191] Added current product exclude --- apps/product/filters.py | 6 ++++++ apps/product/models.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/apps/product/filters.py b/apps/product/filters.py index c7e87dda..1d226356 100644 --- a/apps/product/filters.py +++ b/apps/product/filters.py @@ -9,6 +9,7 @@ class ProductFilterSet(filters.FilterSet): """Product filter set.""" establishment_id = filters.NumberFilter() + current_product = filters.CharFilter(method='without_current_product') product_type = filters.CharFilter(method='by_product_type') product_subtype = filters.CharFilter(method='by_product_subtype') @@ -21,6 +22,11 @@ class ProductFilterSet(filters.FilterSet): 'product_subtype', ] + def without_current_product(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.without_current_product(value) + return queryset + def by_product_type(self, queryset, name, value): if value not in EMPTY_VALUES: return queryset.by_product_type(value) diff --git a/apps/product/models.py b/apps/product/models.py index a6129c48..6d3e4954 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -102,6 +102,11 @@ class ProductQuerySet(models.QuerySet): def wines(self): return self.filter(type__index_name__icontains=ProductType.WINE) + def without_current_product(self, current_product: str): + """Exclude by current product.""" + kwargs = {'pk': int(current_product)} if current_product.isdigit() else {'slug': current_product} + return self.exclude(**kwargs) + def by_product_type(self, product_type: str): """Filter by type.""" return self.filter(product_type__index_name__icontains=product_type) From 6a9356aaa6b9efc7f5a41ff32a58c2b37196bf76 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 6 Dec 2019 15:10:48 +0300 Subject: [PATCH 184/191] fix merge conflicts --- .gitignore | 6 ++++-- apps/product/views/common.py | 2 -- project/settings/local.py | 36 +++++++++++++++++++++++------------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index ea6151b0..90e4f23f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,10 @@ logs/ /geoip_db/ # dev -docker-compose.override.yml - +./docker-compose.override.yml celerybeat-schedule local_files celerybeat.pid +/gm_viktor.dump +/docker-compose.dump.yml +/gm_production_20191029.sql diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 764a0c97..f984a87b 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -26,8 +26,6 @@ class ProductListView(ProductBaseView, generics.ListAPIView): filter_class = filters.ProductFilterSet def get_queryset(self): - print(super().get_queryset()) - qs = super().get_queryset().with_extended_related() \ .by_country_code(self.request.country_code) return qs diff --git a/project/settings/local.py b/project/settings/local.py index fe521d92..c56f9042 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -30,18 +30,31 @@ MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) THUMBNAIL_DEBUG = True # ADDED TRANSFER APP -# INSTALLED_APPS.append('transfer.apps.TransferConfig') +INSTALLED_APPS.append('transfer.apps.TransferConfig') # DATABASES -# DATABASES.update({ -# 'legacy': { -# 'ENGINE': 'django.db.backends.mysql', -# # 'HOST': '172.22.0.1', -# 'HOST': 'mysql_db', -# 'PORT': 3306, -# 'NAME': 'dev', -# 'USER': 'dev', -# 'PASSWORD': 'octosecret123'}}) +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': os.environ.get('DB_NAME'), + 'USER': os.environ.get('DB_USERNAME'), + 'PASSWORD': os.environ.get('DB_PASSWORD'), + 'HOST': os.environ.get('DB_HOSTNAME'), + 'PORT': os.environ.get('DB_PORT'), + 'OPTIONS': { + 'options': '-c search_path=gm' + }, + }, + 'legacy': { + 'ENGINE': 'django.db.backends.mysql', + # 'HOST': '172.22.0.1', + 'HOST': 'mysql_db', + 'PORT': 3306, + 'NAME': 'dev', + 'USER': 'dev', + 'PASSWORD': 'octosecret123' + }, +} # LOGGING @@ -100,6 +113,3 @@ TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} ELASTICSEARCH_DSL_AUTOSYNC = False - -# INSTALLED APPS -INSTALLED_APPS.append('transfer.apps.TransferConfig') \ No newline at end of file From 351d4920121049e0b00c3563abd6d48091969da0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 6 Dec 2019 15:45:46 +0300 Subject: [PATCH 185/191] command to remove relative news description images --- .../news/management/commands/rm_empty_images.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 apps/news/management/commands/rm_empty_images.py diff --git a/apps/news/management/commands/rm_empty_images.py b/apps/news/management/commands/rm_empty_images.py new file mode 100644 index 00000000..3a48e2e4 --- /dev/null +++ b/apps/news/management/commands/rm_empty_images.py @@ -0,0 +1,17 @@ +from django.core.management.base import BaseCommand + +from news.models import News +import re + +class Command(BaseCommand): + help = 'Removes empty img html tags from news description' + + relative_img_regex = re.compile(r'\', re.I) + + def handle(self, *args, **kwargs): + for news in News.objects.all(): + if isinstance(news.description, dict): + news.description = {locale: self.relative_img_regex.sub('', rich_text) + for locale, rich_text in news.description.items()} + self.stdout.write(self.style.WARNING(f'Replaced {news} empty img html tags...\n')) + news.save() \ No newline at end of file From bdb82d0fb010632fcfb64b9a056e3e163f4407cc Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 6 Dec 2019 16:40:39 +0300 Subject: [PATCH 186/191] add migration command --- make_data_migration.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/make_data_migration.sh b/make_data_migration.sh index 70e27ccb..e6cec3e1 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -12,4 +12,5 @@ ./manage.py transfer --wine_characteristics ./manage.py transfer --inquiries ./manage.py transfer --assemblage -./manage.py transfer --purchased_plaques \ No newline at end of file +./manage.py transfer --purchased_plaques +./manage.py rm_empty_images \ No newline at end of file From 8af8cf92e97d97c52a0617a05cd7c99d6f511996 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Dec 2019 17:00:54 +0300 Subject: [PATCH 187/191] added objects related statistic in Collection backoffice serializer --- apps/collection/models.py | 36 +++++++++++++++++++++++++++++ apps/collection/serializers/back.py | 4 ++++ 2 files changed, 40 insertions(+) diff --git a/apps/collection/models.py b/apps/collection/models.py index 58ca9f07..7acd9991 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -87,6 +87,42 @@ class Collection(ProjectBaseMixin, CollectionDateMixin, verbose_name = _('collection') verbose_name_plural = _('collections') + @property + def _related_objects(self) -> list: + """Return list of related objects.""" + related_objects = [] + # get related objects + for related_object in self._meta.related_objects: + related_objects.append(related_object) + return related_objects + + @property + def count_related_objects(self) -> int: + """Return count of related objects.""" + counter = 0 + # count of related objects + for related_object in [related_object.name for related_object in self._related_objects]: + counter += getattr(self, f'{related_object}').count() + return counter + + @property + def related_object_names(self) -> list: + """Return related object names.""" + raw_object_names = [] + for related_object in [related_object.name for related_object in self._related_objects]: + instances = getattr(self, f'{related_object}') + if instances.exists(): + for instance in instances.all(): + raw_object_names.append(instance.slug if hasattr(instance, 'slug') else None) + + # parse slugs + object_names = [] + re_pattern = r'[\w]+' + for raw_name in raw_object_names: + result = re.findall(re_pattern, raw_name) + if result: object_names.append(' '.join(result).capitalize()) + return set(object_names) + class GuideTypeQuerySet(models.QuerySet): """QuerySet for model GuideType.""" diff --git a/apps/collection/serializers/back.py b/apps/collection/serializers/back.py index bb88a778..48c25f6c 100644 --- a/apps/collection/serializers/back.py +++ b/apps/collection/serializers/back.py @@ -19,6 +19,8 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer): collection_type_display = serializers.CharField( source='get_collection_type_display', read_only=True) country = CountrySimpleSerializer(read_only=True) + count_related_objects = serializers.IntegerField(read_only=True) + related_object_names = serializers.ListField(read_only=True) class Meta: model = models.Collection @@ -36,6 +38,8 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer): 'slug', 'start', 'end', + 'count_related_objects', + 'related_object_names', ] From f1fa8a58de3c3d7a9eb6d90b99b8d57332e3e6f1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Dec 2019 17:37:33 +0300 Subject: [PATCH 188/191] small refactoring --- apps/establishment/views/web.py | 5 +---- apps/product/views/common.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 71994522..74c20451 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -114,10 +114,7 @@ class EstablishmentCommentListView(generics.ListAPIView): """Override get_queryset method""" establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug']) - return comment_models.Comment.objects.by_content_type(app_label='establishment', - model='establishment') \ - .by_object_id(object_id=establishment.pk) \ - .order_by('-created') + return establishment.comments.order_by('-created') class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): diff --git a/apps/product/views/common.py b/apps/product/views/common.py index f984a87b..650c1dfe 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -60,10 +60,7 @@ class ProductCommentListView(generics.ListAPIView): def get_queryset(self): """Override get_queryset method""" product = get_object_or_404(Product, slug=self.kwargs['slug']) - return Comment.objects.by_content_type(app_label='product', - model='product') \ - .by_object_id(object_id=product.pk) \ - .order_by('-created') + return product.comments.order_by('-created') class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView): From 831cb4bc81a48f7418f645d3d0a4998e3c8abda1 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Fri, 6 Dec 2019 18:17:16 +0300 Subject: [PATCH 189/191] add home_page --- _dockerfiles/db/Dockerfile | 2 +- .../commands/add_home_page_carousel.py | 37 +++++++++++++++++++ apps/transfer/models.py | 2 +- make_data_migration.sh | 2 + project/settings/base.py | 3 ++ project/settings/local.py | 2 +- 6 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 apps/main/management/commands/add_home_page_carousel.py diff --git a/_dockerfiles/db/Dockerfile b/_dockerfiles/db/Dockerfile index e8a9ded3..c3e35955 100644 --- a/_dockerfiles/db/Dockerfile +++ b/_dockerfiles/db/Dockerfile @@ -1,3 +1,3 @@ -FROM mdillon/postgis:latest +FROM mdillon/postgis:10 RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8 ENV LANG ru_RU.utf8 diff --git a/apps/main/management/commands/add_home_page_carousel.py b/apps/main/management/commands/add_home_page_carousel.py new file mode 100644 index 00000000..5d4f2703 --- /dev/null +++ b/apps/main/management/commands/add_home_page_carousel.py @@ -0,0 +1,37 @@ +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from establishment.models import Establishment +from main.models import Carousel +from transfer.models import HomePages +from location.models import Country +from django.db.models import F + + +class Command(BaseCommand): + help = '''Add establishment form HomePage to Carousel!''' + + @staticmethod + def get_country(country_code): + return Country.objects.filter(code__iexact=country_code).first() + + def handle(self, *args, **kwargs): + objects = [] + deleted = 0 + hp_list = HomePages.objects.annotate( + country=F('site__country_code_2'), + ).all() + for hm in tqdm(hp_list, desc='Add home_page.establishments to carousel'): + est = Establishment.objects.filter(old_id=hm.selection_of_week).first() + if est: + if est.carousels.exists(): + est.carousels.all().delete() + deleted += 1 + carousel = Carousel( + content_object=est, + country=self.get_country(hm.country) + ) + objects.append(carousel) + Carousel.objects.bulk_create(objects) + self.stdout.write( + self.style.WARNING(f'Created {len(objects)}/Deleted {deleted} carousel objects.')) \ No newline at end of file diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 2ebcfb65..019f38aa 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -1000,7 +1000,7 @@ class ProductNotes(MigrateMixin): db_table = 'product_notes' -class HomePages(models.Model): +class HomePages(MigrateMixin): using = 'legacy' site = models.ForeignKey(Sites, models.DO_NOTHING, blank=True, null=True) diff --git a/make_data_migration.sh b/make_data_migration.sh index 70e27ccb..13fba591 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -2,6 +2,8 @@ ./manage.py transfer -a ./manage.py transfer -d ./manage.py transfer -e +./manage.py transfer -n +./manage.py rm_empty_images # команда для удаления картинок с относительным урлом из news.description ./manage.py upd_transportation ./manage.py transfer --fill_city_gallery ./manage.py transfer -l diff --git a/project/settings/base.py b/project/settings/base.py index 1d2c5744..76af8966 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -524,3 +524,6 @@ INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine' COOKIE_DOMAIN = None + +ELASTICSEARCH_DSL = {} +ELASTICSEARCH_INDEX_NAMES = {} diff --git a/project/settings/local.py b/project/settings/local.py index c56f9042..d9c7cab8 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -42,7 +42,7 @@ DATABASES = { 'HOST': os.environ.get('DB_HOSTNAME'), 'PORT': os.environ.get('DB_PORT'), 'OPTIONS': { - 'options': '-c search_path=gm' + 'options': '-c search_path=gm,public' }, }, 'legacy': { From b5a113c875c4245a82db8d1c69d50ddbc54e4c51 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 6 Dec 2019 18:53:40 +0300 Subject: [PATCH 190/191] user last visit middleware --- apps/utils/middleware.py | 11 ++++++++++- project/settings/base.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/utils/middleware.py b/apps/utils/middleware.py index f3936169..1a421322 100644 --- a/apps/utils/middleware.py +++ b/apps/utils/middleware.py @@ -1,5 +1,6 @@ """Custom middleware.""" -from django.utils import translation +from django.utils import translation, timezone +from account.models import User from configuration.models import TranslationSettings from translation.models import Language @@ -12,6 +13,14 @@ def get_locale(cookie_dict): def get_country_code(cookie_dict): return cookie_dict.get('country_code') +def user_last_visit(get_response): + """Updates user last visit w/ current""" + def middleware(request): + response = get_response(request) + if request.user.is_authenticated: + User.objects.filter(pk=request.user.pk).update(last_login=timezone.now()) + return response + return middleware def parse_cookies(get_response): """Parse cookies.""" diff --git a/project/settings/base.py b/project/settings/base.py index 76af8966..5a48c261 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -118,6 +118,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middleware.parse_cookies', + 'utils.middleware.user_last_visit', ] ROOT_URLCONF = 'project.urls' From 8451ccf17ecf1f4ee7b60d3c58ac0cd40e790417 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 6 Dec 2019 19:17:38 +0300 Subject: [PATCH 191/191] add ordering & optimize serializer --- apps/account/serializers/back.py | 19 ++++++++++++++++++- apps/account/views/back.py | 13 +++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index b2316734..699210e7 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -16,7 +16,24 @@ class RoleSerializer(serializers.ModelSerializer): class BackUserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = '__all__' + fields = ( + 'id', + 'last_login', + 'is_superuser', + 'username', + 'last_name', + 'first_name', + 'is_active', + 'date_joined', + 'image_url', + 'cropped_image_url', + 'email', + 'email_confirmed', + 'unconfirmed_email', + 'email_confirmed', + 'newsletter', + 'roles', + ) extra_kwargs = { 'password': {'write_only': True} } diff --git a/apps/account/views/back.py b/apps/account/views/back.py index 80775b3a..fbbc986e 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -1,5 +1,6 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, permissions +from rest_framework.filters import OrderingFilter from account import models from account.models import User @@ -18,10 +19,10 @@ class UserRoleLstView(generics.ListCreateAPIView): class UserLstView(generics.ListCreateAPIView): """User list create view.""" - queryset = User.objects.all() + queryset = User.objects.prefetch_related('roles') serializer_class = serializers.BackUserSerializer permission_classes = (permissions.IsAdminUser,) - filter_backends = (DjangoFilterBackend,) + filter_backends = (DjangoFilterBackend, OrderingFilter) filterset_fields = ( 'email_confirmed', 'is_staff', @@ -29,6 +30,14 @@ class UserLstView(generics.ListCreateAPIView): 'is_superuser', 'roles', ) + ordering_fields = ( + 'email_confirmed', + 'is_staff', + 'is_active', + 'is_superuser', + 'roles', + 'last_login' + ) class UserRUDView(generics.RetrieveUpdateDestroyAPIView):