From 843230b6415fa52ff3c432b04aded4779b64e116 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 001/115] Filters for news favorites --- apps/news/filters.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/news/filters.py b/apps/news/filters.py index 084c570f..2621f3bf 100644 --- a/apps/news/filters.py +++ b/apps/news/filters.py @@ -16,6 +16,8 @@ class NewsListFilterSet(filters.FilterSet): ), method='by_tag_group' ) + tag_value__exclude = filters.CharFilter(method='exclude_tags') + tag_value__in = filters.CharFilter(method='in_tags') class Meta: """Meta class""" @@ -24,8 +26,18 @@ class NewsListFilterSet(filters.FilterSet): 'title', 'is_highlighted', 'tag_group', + 'tag_value__exclude', + 'tag_value__in', ) + def in_tags(self, queryset, name, value): + tags = value.split('__') + return queryset.filter(tags__value__in=tags) + + def exclude_tags(self, queryset, name, value): + tags = value.split('__') + return queryset.exclude(tags__value__in=tags) + def by_tag_group(self, queryset, name, value): if value == models.News.RECIPES_TAG_VALUE: queryset = queryset.recipe_news() From 4b4dc27a49b807f7ea143bb82a7d889ae4422a75 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 15 Nov 2019 15:59:02 +0300 Subject: [PATCH 002/115] added method to transfer advertisement --- apps/advertisement/models.py | 2 +- apps/advertisement/transfer_data.py | 8 +++-- apps/transfer/models.py | 2 +- apps/transfer/serializers/advertisement.py | 41 ++++++++++++++++++---- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/advertisement/models.py b/apps/advertisement/models.py index 574aff56..18be4516 100644 --- a/apps/advertisement/models.py +++ b/apps/advertisement/models.py @@ -50,7 +50,7 @@ class Advertisement(ProjectBaseMixin): class Meta: verbose_name = _('Advertisement') - verbose_name_plural = _('Advertisement') + verbose_name_plural = _('Advertisements') def __str__(self): return str(self.url) diff --git a/apps/advertisement/transfer_data.py b/apps/advertisement/transfer_data.py index 64e715ce..b6e5ace5 100644 --- a/apps/advertisement/transfer_data.py +++ b/apps/advertisement/transfer_data.py @@ -5,14 +5,18 @@ from transfer.serializers.advertisement import AdvertisementSerializer def transfer_advertisement(): - queryset = Ads.objects.filter(href__isnull=False).values_list('id', 'href', 'attachment_suffix_url') + errors = [] + queryset = Ads.objects.exclude(href__isnull=True) \ + .exclude(attachment_suffix_url__isnull=True) \ + .exclude(site_id__isnull=True) serialized_data = AdvertisementSerializer(data=list(queryset.values()), many=True) if serialized_data.is_valid(): serialized_data.save() else: - pprint(f"News serializer errors: {serialized_data.errors}") + for d in serialized_data.errors: errors.append(d) if d else None + pprint(f"transfer_product errors: {errors}") data_types = { diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 25fdb527..d2e13d89 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -839,7 +839,7 @@ class PageMetadata(MigrateMixin): class Ads(MigrateMixin): using = 'legacy' - site_id = models.IntegerField(blank=True, null=True) + site = models.ForeignKey('Sites', on_delete=models.DO_NOTHING) href = models.CharField(max_length=255, blank=True, null=True) start_at = models.DateTimeField(blank=True, null=True) expire_at = models.DateTimeField(blank=True, null=True) diff --git a/apps/transfer/serializers/advertisement.py b/apps/transfer/serializers/advertisement.py index a25ee51e..443e23e2 100644 --- a/apps/transfer/serializers/advertisement.py +++ b/apps/transfer/serializers/advertisement.py @@ -1,22 +1,51 @@ from rest_framework import serializers +from main.models import SiteSettings +from transfer.models import Sites from advertisement.models import Advertisement -class AdvertisementSerializer(serializers.Serializer): +class AdvertisementSerializer(serializers.ModelSerializer): + id = serializers.IntegerField() href = serializers.CharField() - attachment_suffix_url = serializers.CharField(allow_null=True) + site_id = serializers.PrimaryKeyRelatedField( + queryset=Sites.objects.all()) + start_at = serializers.DateTimeField(allow_null=True) + expire_at = serializers.DateTimeField(allow_null=True) + + class Meta: + """Meta class.""" + model = Advertisement + fields = [ + 'id', + 'href', + 'site_id', + 'start_at', + 'expire_at', + ] def validate(self, data): data.update({ 'old_id': data.pop('id'), 'url': data.pop('href'), - 'image_url': data.pop('attachment_suffix_url'), - 'width': 300, - 'height': 250, + 'site': self.get_site(data.pop('site_id')), + 'start': data.pop('start_at', None), + 'end': data.pop('expire_at', None), }) return data def create(self, validated_data): - return Advertisement.objects.create(**validated_data) \ No newline at end of file + site = validated_data.pop('site') + obj, _ = self.Meta.model.objects.get_or_create(validated_data) + + import ipdb;ipdb.set_trace() + if site and site not in obj.sites.all(): + obj.sites.add(site) + return obj + + def get_site(self, subdomain): + subdomain = subdomain.country_code_2 if isinstance(subdomain, Sites) else subdomain + qs = SiteSettings.objects.filter(subdomain=subdomain) + if qs.exists(): + return qs.first() From 153ccd7bd9daedf08e4dadac2e72a070327f6fb1 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 003/115] International news --- apps/news/models.py | 5 +++++ apps/news/views.py | 11 +++++++++-- project/settings/base.py | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/news/models.py b/apps/news/models.py index 550d5baa..c7fc8017 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -86,6 +86,10 @@ class NewsQuerySet(TranslationQuerysetMixin): """Returns news with tag 'cook' qs.""" return self.filter(tags__value=News.RECIPES_TAG_VALUE) + def international_news(self): + """Returns only international news qs.""" + return self.filter(tags__value=News.INTERNATIONAL_TAG_VALUE) + def published(self): """Return only published news""" now = timezone.now() @@ -151,6 +155,7 @@ class News(BaseAttributes, TranslatedFieldsMixin): (PUBLISHED_EXCLUSIVE, _('Published exclusive')), ) + INTERNATIONAL_TAG_VALUE = 'international' RECIPES_TAG_VALUE = 'cook' old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) diff --git a/apps/news/views.py b/apps/news/views.py index fbc0e455..f7d18101 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -18,7 +18,7 @@ class NewsMixinView: permission_classes = (permissions.AllowAny,) serializer_class = serializers.NewsBaseSerializer - def get_queryset(self): + def get_queryset(self, *args, **kwargs): """Override get_queryset method.""" qs = models.News.objects.published() \ .with_base_related() \ @@ -27,7 +27,10 @@ class NewsMixinView: country_code = self.request.country_code if country_code: - qs = qs.by_country_code(country_code) + if kwargs.get('international_preferred') and country_code in settings.INTERNATIONAL_COUNTRY_CODES: + qs = qs.international_news() + else: + qs = qs.by_country_code(country_code) return qs @@ -37,6 +40,10 @@ class NewsListView(NewsMixinView, generics.ListAPIView): serializer_class = serializers.NewsListSerializer filter_class = filters.NewsListFilterSet + def get_queryset(self, *args, **kwargs): + kwargs.update({'international_preferred': True}) + return super().get_queryset(*args, **kwargs) + class NewsDetailView(NewsMixinView, generics.RetrieveAPIView): """News detail view.""" diff --git a/project/settings/base.py b/project/settings/base.py index 85c7960f..70427ef3 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -493,3 +493,4 @@ FALLBACK_LOCALE = 'en-GB' CAROUSEL_ITEMS = [230, 231, 232] 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'] \ No newline at end of file From c71e7f10dda6b7d6969d3b2990ffc4e38d5a87ff Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 004/115] Search in all locales --- apps/search_indexes/filters.py | 49 +++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 93208a71..a29c186a 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -1,19 +1,22 @@ """Search indexes filters.""" from elasticsearch_dsl.query import Q from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend -from utils.models import get_current_locale +from search_indexes.utils import OBJECT_FIELD_PROPERTIES class CustomSearchFilterBackend(SearchFilterBackend): """Custom SearchFilterBackend.""" @staticmethod - def get_field_name(view, field): - field_name = field + def search_among_all_locales(view, search_kwargs: dict): if hasattr(view, 'search_fields') and hasattr(view, 'translated_search_fields'): - if field in view.translated_search_fields: - field_name = f'{field}.{get_current_locale()}' - return field_name + all_supported_locales = OBJECT_FIELD_PROPERTIES.keys() + fields = search_kwargs.copy().keys() + for field in fields: + if field in view.translated_search_fields: + value = search_kwargs.pop(field) + search_kwargs.update({f'{field}.{locale}': value for locale in all_supported_locales}) + def construct_search(self, request, view): """Construct search. @@ -54,29 +57,43 @@ class CustomSearchFilterBackend(SearchFilterBackend): field, value = __values if field in view.search_fields: # Initial kwargs for the match query - field_kwargs = {self.get_field_name(view, field): {'query': value}} + field_kwargs = {field: {'query': value}} # In case if we deal with structure 2 if isinstance(view.search_fields, dict): extra_field_kwargs = view.search_fields[field] if extra_field_kwargs: - field_kwargs[self.get_field_name(view, field)].update(extra_field_kwargs) + field_kwargs[field].update(extra_field_kwargs) # The match query - __queries.append( - Q("match", **field_kwargs) - ) + self.search_among_all_locales(view, field_kwargs) + if len(field_kwargs.keys()) > 1: + for k, v in field_kwargs.items(): + __queries.append( + Q("match", **{k: v}) + ) + else: + __queries.append( + Q("match", **field_kwargs) + ) else: for field in view.search_fields: # Initial kwargs for the match query - field_kwargs = {self.get_field_name(view, field): {'query': search_term}} + field_kwargs = {field: {'query': search_term}} # In case if we deal with structure 2 if isinstance(view.search_fields, dict): extra_field_kwargs = view.search_fields[field] if extra_field_kwargs: - field_kwargs[self.get_field_name(view, field)].update(extra_field_kwargs) + field_kwargs[field].update(extra_field_kwargs) # The match query - __queries.append( - Q("match", **field_kwargs) - ) + self.search_among_all_locales(view, field_kwargs) + if len(field_kwargs.keys()) > 1: + for k, v in field_kwargs.items(): + __queries.append( + Q("match", **{k: v}) + ) + else: + __queries.append( + Q("match", **field_kwargs) + ) return __queries \ No newline at end of file From 9c635cd60de992526ff9c718fe85204cd51dc7ed Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 005/115] Define search boosts --- apps/search_indexes/views.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 21b9b675..1a898cbc 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -28,8 +28,10 @@ class NewsDocumentViewSet(BaseDocumentViewSet): ] search_fields = { - 'title': {'fuzziness': 'auto:2,5'}, - 'subtitle': {'fuzziness': 'auto:2,5'}, + 'title': {'fuzziness': 'auto:2,5', + 'boost': 3}, + 'subtitle': {'fuzziness': 'auto:2,5', + 'boost': 2}, 'description': {'fuzziness': 'auto:2,5'}, } translated_search_fields = ( @@ -86,11 +88,11 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): search_fields = { 'name': {'fuzziness': 'auto:2,5', - 'boost': '2'}, + 'boost': 4}, 'transliterated_name': {'fuzziness': 'auto:2,5', - 'boost': '2'}, + 'boost': 3}, 'index_name': {'fuzziness': 'auto:2,5', - 'boost': '2'}, + 'boost': 2}, 'description': {'fuzziness': 'auto:2,5'}, } translated_search_fields = ( @@ -216,11 +218,11 @@ class ProductDocumentViewSet(BaseDocumentViewSet): search_fields = { 'name': {'fuzziness': 'auto:2,5', - 'boost': '2'}, + 'boost': 4}, 'transliterated_name': {'fuzziness': 'auto:2,5', - 'boost': '2'}, + 'boost': 3}, 'index_name': {'fuzziness': 'auto:2,5', - 'boost': '2'}, + 'boost': 2}, 'description': {'fuzziness': 'auto:2,5'}, } translated_search_fields = ( From c293ae4b9cbe8386c05b8752f1b1e73959e7b7bc Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 15 Nov 2019 21:04:37 +0300 Subject: [PATCH 006/115] remove debug tracer --- apps/transfer/serializers/advertisement.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/transfer/serializers/advertisement.py b/apps/transfer/serializers/advertisement.py index 443e23e2..958e21a9 100644 --- a/apps/transfer/serializers/advertisement.py +++ b/apps/transfer/serializers/advertisement.py @@ -39,7 +39,6 @@ class AdvertisementSerializer(serializers.ModelSerializer): site = validated_data.pop('site') obj, _ = self.Meta.model.objects.get_or_create(validated_data) - import ipdb;ipdb.set_trace() if site and site not in obj.sites.all(): obj.sites.add(site) return obj From 6f2d41c1ad403ff1c2fe7bc97a5bbeae48cacb9a Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 15 Nov 2019 15:59:02 +0300 Subject: [PATCH 007/115] added method to transfer advertisement --- apps/advertisement/models.py | 2 +- apps/advertisement/transfer_data.py | 8 +++-- apps/transfer/models.py | 2 +- apps/transfer/serializers/advertisement.py | 41 ++++++++++++++++++---- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/advertisement/models.py b/apps/advertisement/models.py index 574aff56..18be4516 100644 --- a/apps/advertisement/models.py +++ b/apps/advertisement/models.py @@ -50,7 +50,7 @@ class Advertisement(ProjectBaseMixin): class Meta: verbose_name = _('Advertisement') - verbose_name_plural = _('Advertisement') + verbose_name_plural = _('Advertisements') def __str__(self): return str(self.url) diff --git a/apps/advertisement/transfer_data.py b/apps/advertisement/transfer_data.py index 64e715ce..b6e5ace5 100644 --- a/apps/advertisement/transfer_data.py +++ b/apps/advertisement/transfer_data.py @@ -5,14 +5,18 @@ from transfer.serializers.advertisement import AdvertisementSerializer def transfer_advertisement(): - queryset = Ads.objects.filter(href__isnull=False).values_list('id', 'href', 'attachment_suffix_url') + errors = [] + queryset = Ads.objects.exclude(href__isnull=True) \ + .exclude(attachment_suffix_url__isnull=True) \ + .exclude(site_id__isnull=True) serialized_data = AdvertisementSerializer(data=list(queryset.values()), many=True) if serialized_data.is_valid(): serialized_data.save() else: - pprint(f"News serializer errors: {serialized_data.errors}") + for d in serialized_data.errors: errors.append(d) if d else None + pprint(f"transfer_product errors: {errors}") data_types = { diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 29ccb4bd..d8be36ce 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -839,7 +839,7 @@ class PageMetadata(MigrateMixin): class Ads(MigrateMixin): using = 'legacy' - site_id = models.IntegerField(blank=True, null=True) + site = models.ForeignKey('Sites', on_delete=models.DO_NOTHING) href = models.CharField(max_length=255, blank=True, null=True) start_at = models.DateTimeField(blank=True, null=True) expire_at = models.DateTimeField(blank=True, null=True) diff --git a/apps/transfer/serializers/advertisement.py b/apps/transfer/serializers/advertisement.py index a25ee51e..443e23e2 100644 --- a/apps/transfer/serializers/advertisement.py +++ b/apps/transfer/serializers/advertisement.py @@ -1,22 +1,51 @@ from rest_framework import serializers +from main.models import SiteSettings +from transfer.models import Sites from advertisement.models import Advertisement -class AdvertisementSerializer(serializers.Serializer): +class AdvertisementSerializer(serializers.ModelSerializer): + id = serializers.IntegerField() href = serializers.CharField() - attachment_suffix_url = serializers.CharField(allow_null=True) + site_id = serializers.PrimaryKeyRelatedField( + queryset=Sites.objects.all()) + start_at = serializers.DateTimeField(allow_null=True) + expire_at = serializers.DateTimeField(allow_null=True) + + class Meta: + """Meta class.""" + model = Advertisement + fields = [ + 'id', + 'href', + 'site_id', + 'start_at', + 'expire_at', + ] def validate(self, data): data.update({ 'old_id': data.pop('id'), 'url': data.pop('href'), - 'image_url': data.pop('attachment_suffix_url'), - 'width': 300, - 'height': 250, + 'site': self.get_site(data.pop('site_id')), + 'start': data.pop('start_at', None), + 'end': data.pop('expire_at', None), }) return data def create(self, validated_data): - return Advertisement.objects.create(**validated_data) \ No newline at end of file + site = validated_data.pop('site') + obj, _ = self.Meta.model.objects.get_or_create(validated_data) + + import ipdb;ipdb.set_trace() + if site and site not in obj.sites.all(): + obj.sites.add(site) + return obj + + def get_site(self, subdomain): + subdomain = subdomain.country_code_2 if isinstance(subdomain, Sites) else subdomain + qs = SiteSettings.objects.filter(subdomain=subdomain) + if qs.exists(): + return qs.first() From 671a92912e56c9cc7edd250d0b521573ae32d0a5 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 15 Nov 2019 21:04:37 +0300 Subject: [PATCH 008/115] remove debug tracer --- apps/transfer/serializers/advertisement.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/transfer/serializers/advertisement.py b/apps/transfer/serializers/advertisement.py index 443e23e2..958e21a9 100644 --- a/apps/transfer/serializers/advertisement.py +++ b/apps/transfer/serializers/advertisement.py @@ -39,7 +39,6 @@ class AdvertisementSerializer(serializers.ModelSerializer): site = validated_data.pop('site') obj, _ = self.Meta.model.objects.get_or_create(validated_data) - import ipdb;ipdb.set_trace() if site and site not in obj.sites.all(): obj.sites.add(site) return obj From 31399cea838f9ae21c702c5973998241ae08c2e8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sat, 16 Nov 2019 00:18:07 +0300 Subject: [PATCH 009/115] added advertisement image import --- apps/advertisement/transfer_data.py | 22 ++++++++++- apps/transfer/serializers/advertisement.py | 45 +++++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/apps/advertisement/transfer_data.py b/apps/advertisement/transfer_data.py index b6e5ace5..f1a5ca38 100644 --- a/apps/advertisement/transfer_data.py +++ b/apps/advertisement/transfer_data.py @@ -1,7 +1,7 @@ from pprint import pprint from transfer.models import Ads -from transfer.serializers.advertisement import AdvertisementSerializer +from transfer.serializers.advertisement import AdvertisementSerializer, AdvertisementImageSerializer def transfer_advertisement(): @@ -19,6 +19,24 @@ def transfer_advertisement(): pprint(f"transfer_product errors: {errors}") +def transfer_page(): + errors = [] + queryset = Ads.objects.exclude(href__isnull=True) \ + .exclude(attachment_suffix_url__isnull=True) \ + .exclude(site_id__isnull=True) + + serialized_data = AdvertisementImageSerializer(data=list(queryset.values()), many=True) + + if serialized_data.is_valid(): + serialized_data.save() + else: + for d in serialized_data.errors: errors.append(d) if d else None + pprint(f"transfer_page errors: {errors}") + + data_types = { - "commercial": [transfer_advertisement] + "commercial": [ + transfer_advertisement, + transfer_page + ] } diff --git a/apps/transfer/serializers/advertisement.py b/apps/transfer/serializers/advertisement.py index 958e21a9..c7bd50f8 100644 --- a/apps/transfer/serializers/advertisement.py +++ b/apps/transfer/serializers/advertisement.py @@ -1,8 +1,11 @@ from rest_framework import serializers from main.models import SiteSettings -from transfer.models import Sites +from transfer.models import Sites, Ads from advertisement.models import Advertisement +from main.models import Page +import requests +from rest_framework import status class AdvertisementSerializer(serializers.ModelSerializer): @@ -48,3 +51,43 @@ class AdvertisementSerializer(serializers.ModelSerializer): qs = SiteSettings.objects.filter(subdomain=subdomain) if qs.exists(): return qs.first() + + +class AdvertisementImageSerializer(AdvertisementSerializer): + + attachment_suffix_url = serializers.CharField() + + class Meta(AdvertisementSerializer.Meta): + model = Page + fields = [ + 'id', + 'attachment_suffix_url', + ] + + def validate(self, data): + data.update({ + 'image_url': self.get_absolute_image_url(data.pop('attachment_suffix_url')), + 'advertisement': self.get_advertisement(data.pop('id')), + }) + return data + + def get_advertisement(self, old_id): + qs = Advertisement.objects.filter(old_id=old_id) + if qs.exists(): + return qs.first() + + def get_absolute_image_url(self, relative_path): + if relative_path: + absolute_image_url = f'https://1dc3f33f6d-3.optimicdn.com/gaultmillau.com/' \ + f'{relative_path}' + response = requests.head(absolute_image_url) + if response.status_code == status.HTTP_200_OK: + return absolute_image_url + + def create(self, validated_data): + advertisement = validated_data.get('advertisement') + image_url = validated_data.get('image_url') + + if advertisement and image_url: + Page.objects.get_or_create(advertisement=advertisement, image_url=image_url, source=Page.MOBILE) + Page.objects.get_or_create(advertisement=advertisement, image_url=image_url, source=Page.WEB) \ No newline at end of file From c725da6a9b09274a63a00d1e23682ae01b296b9b Mon Sep 17 00:00:00 2001 From: "a.feteleu" Date: Sat, 16 Nov 2019 01:08:31 +0300 Subject: [PATCH 010/115] refactored transfer ads --- apps/transfer/serializers/advertisement.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/transfer/serializers/advertisement.py b/apps/transfer/serializers/advertisement.py index c7bd50f8..761a1879 100644 --- a/apps/transfer/serializers/advertisement.py +++ b/apps/transfer/serializers/advertisement.py @@ -14,8 +14,6 @@ class AdvertisementSerializer(serializers.ModelSerializer): href = serializers.CharField() site_id = serializers.PrimaryKeyRelatedField( queryset=Sites.objects.all()) - start_at = serializers.DateTimeField(allow_null=True) - expire_at = serializers.DateTimeField(allow_null=True) class Meta: """Meta class.""" @@ -24,29 +22,27 @@ class AdvertisementSerializer(serializers.ModelSerializer): 'id', 'href', 'site_id', - 'start_at', - 'expire_at', ] def validate(self, data): data.update({ 'old_id': data.pop('id'), 'url': data.pop('href'), - 'site': self.get_site(data.pop('site_id')), - 'start': data.pop('start_at', None), - 'end': data.pop('expire_at', None), + 'site_settings': self.get_site_settings(data.pop('site_id')), }) return data def create(self, validated_data): - site = validated_data.pop('site') - obj, _ = self.Meta.model.objects.get_or_create(validated_data) + site = validated_data.pop('site_settings') + url = validated_data.get('url') + + obj, _ = self.Meta.model.objects.get_or_create(url=url, defaults=validated_data) if site and site not in obj.sites.all(): obj.sites.add(site) return obj - def get_site(self, subdomain): + def get_site_settings(self, subdomain): subdomain = subdomain.country_code_2 if isinstance(subdomain, Sites) else subdomain qs = SiteSettings.objects.filter(subdomain=subdomain) if qs.exists(): @@ -89,5 +85,5 @@ class AdvertisementImageSerializer(AdvertisementSerializer): image_url = validated_data.get('image_url') if advertisement and image_url: - Page.objects.get_or_create(advertisement=advertisement, image_url=image_url, source=Page.MOBILE) - Page.objects.get_or_create(advertisement=advertisement, image_url=image_url, source=Page.WEB) \ No newline at end of file + self.Meta.model.objects.get_or_create(source=Page.MOBILE, **validated_data) + self.Meta.model.objects.get_or_create(source=Page.WEB, **validated_data) From 8b656b9bb63c19f9d31b3d52eb316070df377f7f Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Sat, 16 Nov 2019 09:38:33 +0000 Subject: [PATCH 011/115] Fixed problem with no required period in guest online service response --- apps/booking/views.py | 48 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/apps/booking/views.py b/apps/booking/views.py index 653503e7..cdadc768 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -13,10 +13,43 @@ from utils.methods import get_user_ip class CheckWhetherBookingAvailable(generics.GenericAPIView): """ Checks which service to use if establishmend is managed by any """ + _VALID_GUESTONLINE_PERIODS = {'lunch', 'dinner', 'afternoon', 'breakfast'} + permission_classes = (permissions.AllowAny,) serializer_class = CheckBookingSerializer pagination_class = None + def _fill_period_template(self, period_template, period_name): + period_template_copy = period_template.copy() + period_template_copy['period'] = period_name + return period_template_copy + + def _preprocess_guestonline_response(self, response): + 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') + + period_template = iter(periods_by_name.values()).__next__().copy() + period_template.pop('total_left_seats') + period_template.pop('hours') + period_template.pop('period') + + processed_periods = [ + periods_by_name[period_name] + if period_name in periods_by_name + else self._fill_period_template(period_template, period_name) + for period_name in CheckWhetherBookingAvailable._VALID_GUESTONLINE_PERIODS + ] + + unnamed_periods = filter(lambda period: 'period' not in period, periods) + for unnamed_period in unnamed_periods: + processed_periods.append(unnamed_period) + + response['periods'] = processed_periods + + return response + def get(self, request, *args, **kwargs): is_booking_available = False establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id']) @@ -24,12 +57,12 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): date = request.query_params.get('date') g_service = GuestonlineService() l_service = LastableService() - if (not establishment.lastable_id is None) and l_service \ + if establishment.lastable_id is not None and l_service \ .check_whether_booking_available(establishment.lastable_id, date): is_booking_available = True service = l_service service.service_id = establishment.lastable_id - elif (not establishment.guestonline_id is None) and g_service \ + elif establishment.guestonline_id is not None and g_service \ .check_whether_booking_available(establishment.guestonline_id, **g_service.get_certain_keys(request.query_params, {'date', 'persons'})): @@ -41,7 +74,11 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): 'available': is_booking_available, 'type': service.service if service else None, } - response.update({'details': service.response} if service and service.response else {}) + + 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 {}) return Response(data=response, status=200) @@ -97,8 +134,9 @@ class UpdatePendingBooking(generics.UpdateAPIView): r = service.update_booking(service.get_certain_keys(data, { 'email', 'phone', 'last_name', 'first_name', 'country_code', 'pending_booking_id', 'note', }, { - 'email', 'phone', 'last_name', 'first_name', 'country_code', 'pending_booking_id', - })) + 'email', 'phone', 'last_name', 'first_name', + 'country_code', 'pending_booking_id', + })) if isinstance(r, Response): return r if data.get('newsletter'): From 36d16625ea177db1d64184b20222acb3745e4a2e Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sat, 16 Nov 2019 13:28:03 +0300 Subject: [PATCH 012/115] change filter for Establishment --- apps/tag/management/commands/add_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tag/management/commands/add_tags.py b/apps/tag/management/commands/add_tags.py index 37fdbbea..0fba46f8 100644 --- a/apps/tag/management/commands/add_tags.py +++ b/apps/tag/management/commands/add_tags.py @@ -11,7 +11,7 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): existing_establishment = Establishment.objects.filter( - old_id__isnull=False, tags__isnull=True + old_id__isnull=False ) ESTABLISHMENT = 1 SHOP = 2 From ca61359a81287940efe21c3569898d6e99a04412 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 013/115] Fix product type serializer --- apps/search_indexes/serializers.py | 18 +++++++++++++----- apps/search_indexes/views.py | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index dffd6e77..f926ff55 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -79,6 +79,18 @@ class WineColorDocumentSerializer(serializers.Serializer): return get_translated_value(obj.label) +class ProductTypeDocumentSerializer(serializers.Serializer): + """Product type ES document serializer.""" + + id = serializers.IntegerField() + index_name = serializers.CharField() + name_translated = serializers.SerializerMethodField() + + @staticmethod + def get_name_translated(obj): + return get_translated_value(obj.name) + + class ProductEstablishmentDocumentSerializer(serializers.Serializer): """Related to Product Establishment ES document serializer.""" @@ -202,13 +214,9 @@ class ProductDocumentSerializer(DocumentSerializer): subtypes = ProductSubtypeDocumentSerializer(many=True) wine_region = WineRegionDocumentSerializer(allow_null=True) wine_colors = WineColorDocumentSerializer(many=True) - product_type = serializers.SerializerMethodField() + product_type = ProductTypeDocumentSerializer(allow_null=True) establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True) - @staticmethod - def get_product_type(obj): - return get_translated_value(obj.product_type.name if obj.product_type else {}) - class Meta: """Meta class.""" diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 1a898cbc..a40b13ed 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -251,6 +251,9 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'type': { 'field': 'product_type.index_name', }, + 'product_type': { + 'field': 'product_type.index_name', + }, 'subtype': { 'field': 'subtypes.index_name', 'lookups': [ From bff533e7d5c741464518ede884c6e4f3e0f57277 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 014/115] Tag types --- apps/search_indexes/views.py | 20 ++++++-------------- apps/tag/filters.py | 7 ++++++- apps/tag/models.py | 4 ++++ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index a40b13ed..85bd7c01 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -89,10 +89,6 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): search_fields = { 'name': {'fuzziness': 'auto:2,5', 'boost': 4}, - 'transliterated_name': {'fuzziness': 'auto:2,5', - 'boost': 3}, - 'index_name': {'fuzziness': 'auto:2,5', - 'boost': 2}, 'description': {'fuzziness': 'auto:2,5'}, } translated_search_fields = ( @@ -199,7 +195,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): """Product document ViewSet.""" document = ProductDocument - lookup_field = 'slug' + # lookup_field = 'slug' pagination_class = ProjectMobilePagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.ProductDocumentSerializer @@ -212,8 +208,8 @@ class ProductDocumentViewSet(BaseDocumentViewSet): filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, - GeoSpatialFilteringFilterBackend, - DefaultOrderingFilterBackend, + # GeoSpatialFilteringFilterBackend, +# DefaultOrderingFilterBackend, ] search_fields = { @@ -221,10 +217,9 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'boost': 4}, 'transliterated_name': {'fuzziness': 'auto:2,5', 'boost': 3}, - 'index_name': {'fuzziness': 'auto:2,5', - 'boost': 2}, 'description': {'fuzziness': 'auto:2,5'}, } + translated_search_fields = ( 'description', ) @@ -248,9 +243,6 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'for_establishment': { 'field': 'establishment.slug', }, - 'type': { - 'field': 'product_type.index_name', - }, 'product_type': { 'field': 'product_type.index_name', }, @@ -262,5 +254,5 @@ class ProductDocumentViewSet(BaseDocumentViewSet): ] } } - geo_spatial_filter_fields = { - } \ No newline at end of file + # geo_spatial_filter_fields = { + # } \ No newline at end of file diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 0b1fb829..08d14e94 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -31,13 +31,18 @@ class TagCategoryFilterSet(TagsBaseFilterSet): """TagCategory filterset.""" establishment_type = filters.CharFilter(method='by_establishment_type') + product_type = filters.CharFilter(method='by_product_type') class Meta: """Meta class.""" model = models.TagCategory fields = ('type', - 'establishment_type', ) + 'establishment_type', + 'product_type', ) + + def by_product_type(self, queryset, name, value): + return queryset.by_product_type(value) # todo: filter by establishment type def by_establishment_type(self, queryset, name, value): diff --git a/apps/tag/models.py b/apps/tag/models.py index a35425e8..2505b9ca 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -100,6 +100,10 @@ class TagCategoryQuerySet(models.QuerySet): """Filter by establishment type index name.""" return self.filter(establishment_types__index_name=index_name) + def by_product_type(self, index_name): + """Filter by product type index name.""" + return self.filter(product_types__index_name=index_name) + def with_tags(self, switcher=True): """Filter by existing tags.""" return self.exclude(tags__isnull=switcher) From 5a014ca7fe87e0c4027fb1040744f7e9c2a55434 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 015/115] Fix some product search issues --- apps/search_indexes/serializers.py | 8 ++++++-- apps/search_indexes/views.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index f926ff55..a26b80fc 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -39,7 +39,8 @@ class ProductSubtypeDocumentSerializer(serializers.Serializer): id = serializers.IntegerField() name_translated = serializers.SerializerMethodField() - get_name_translated = lambda obj: get_translated_value(obj.name) + def get_name_translated(self, obj): + return get_translated_value(obj.name) class WineRegionCountryDocumentSerialzer(serializers.Serializer): @@ -64,6 +65,9 @@ class WineRegionDocumentSerializer(serializers.Serializer): name = serializers.CharField() country = WineRegionCountryDocumentSerialzer(allow_null=True) + def get_attribute(self, instance): + return instance.wine_region if instance and instance.wine_region else None + class WineColorDocumentSerializer(serializers.Serializer): """Wine color ES document serializer,""" @@ -211,7 +215,7 @@ class ProductDocumentSerializer(DocumentSerializer): """Product document serializer""" tags = TagsDocumentSerializer(many=True) - subtypes = ProductSubtypeDocumentSerializer(many=True) + subtypes = ProductSubtypeDocumentSerializer(many=True, allow_null=True) wine_region = WineRegionDocumentSerializer(allow_null=True) wine_colors = WineColorDocumentSerializer(many=True) product_type = ProductTypeDocumentSerializer(allow_null=True) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 85bd7c01..dcc6f895 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -89,6 +89,8 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): search_fields = { 'name': {'fuzziness': 'auto:2,5', 'boost': 4}, + 'transliterated_name': {'fuzziness': 'auto:2,5', + 'boost': 3}, 'description': {'fuzziness': 'auto:2,5'}, } translated_search_fields = ( From 94797c354ac0f40d514f44d8a24786586756a228 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 016/115] Fix search results produt name --- 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 b00f3f50..a1179a4f 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -95,13 +95,13 @@ class ProductDocument(Document): }, multi=True ) + name = fields.KeywordField(attr='display_name') class Django: model = models.Product fields = ( 'id', 'category', - 'name', 'available', 'public_mark', 'slug', From 144b5abdbffd2da18265c85ee4796964e4cfe9d1 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 017/115] Fix issue w/ product display name --- apps/product/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/product/models.py b/apps/product/models.py index ba9ccd38..53cd0e61 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -278,7 +278,7 @@ class Product(TranslatedFieldsMixin, BaseAttributes): def display_name(self): name = f'{self.name} ' \ f'({self.vintage if self.vintage else "BSA"})' - if self.establishment.name: + if self.establishment and self.establishment.name: name = f'{self.establishment.name} - ' + name return name From 4e005a7a4d589c21a1bc5f119bc5ab7e1e51cf8a Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 16 Nov 2019 15:56:23 +0300 Subject: [PATCH 018/115] migrate subscribers --- .../migrations/0008_auto_20191116_1135.py | 17 ++++++++ .../migrations/0003_auto_20191116_1248.py | 20 ++++++++++ apps/notification/models.py | 2 +- apps/notification/transfer_data.py | 14 +++---- apps/transfer/serializers/notification.py | 39 ++++++++++++------- 5 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 apps/advertisement/migrations/0008_auto_20191116_1135.py create mode 100644 apps/notification/migrations/0003_auto_20191116_1248.py diff --git a/apps/advertisement/migrations/0008_auto_20191116_1135.py b/apps/advertisement/migrations/0008_auto_20191116_1135.py new file mode 100644 index 00000000..c2a0278c --- /dev/null +++ b/apps/advertisement/migrations/0008_auto_20191116_1135.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.7 on 2019-11-16 11:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('advertisement', '0007_auto_20191115_0750'), + ] + + operations = [ + migrations.AlterModelOptions( + name='advertisement', + options={'verbose_name': 'Advertisement', 'verbose_name_plural': 'Advertisements'}, + ), + ] diff --git a/apps/notification/migrations/0003_auto_20191116_1248.py b/apps/notification/migrations/0003_auto_20191116_1248.py new file mode 100644 index 00000000..2af6a7ae --- /dev/null +++ b/apps/notification/migrations/0003_auto_20191116_1248.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2019-11-16 12:48 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('notification', '0002_subscriber_old_id'), + ] + + operations = [ + migrations.AlterField( + model_name='subscriber', + name='user', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscriber', to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + ] diff --git a/apps/notification/models.py b/apps/notification/models.py index 3e6f7f3a..a5cde50b 100644 --- a/apps/notification/models.py +++ b/apps/notification/models.py @@ -74,7 +74,7 @@ class Subscriber(ProjectBaseMixin): (USABLE, _('Usable')), ) - user = models.OneToOneField( + user = models.ForeignKey( User, blank=True, null=True, diff --git a/apps/notification/transfer_data.py b/apps/notification/transfer_data.py index 487501c3..1712b6ea 100644 --- a/apps/notification/transfer_data.py +++ b/apps/notification/transfer_data.py @@ -1,7 +1,5 @@ from pprint import pprint -from django.db.models import Count - from transfer.models import EmailAddresses, NewsletterSubscriber from transfer.serializers.notification import SubscriberSerializer, NewsletterSubscriberSerializer @@ -25,14 +23,14 @@ def transfer_newsletter_subscriber(): 'email_address__ip', 'email_address__country_code', 'email_address__locale', - 'created_at', + 'updated_at', ) - # serialized_data = NewsletterSubscriberSerializer(data=list(queryset.values()), many=True) - # if serialized_data.is_valid(): - # serialized_data.save() - # else: - # pprint(f'NewsletterSubscriber serializer errors: {serialized_data.errors}') + serialized_data = NewsletterSubscriberSerializer(data=list(queryset), many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f'NewsletterSubscriber serializer errors: {serialized_data.errors}') data_types = { diff --git a/apps/transfer/serializers/notification.py b/apps/transfer/serializers/notification.py index 7eb7bdac..dc0ca4f3 100644 --- a/apps/transfer/serializers/notification.py +++ b/apps/transfer/serializers/notification.py @@ -1,3 +1,4 @@ +from django.db import IntegrityError from rest_framework import serializers from account.models import User @@ -41,10 +42,10 @@ class NewsletterSubscriberSerializer(serializers.Serializer): id = serializers.IntegerField() email_address__email = serializers.CharField() email_address__account_id = serializers.IntegerField(allow_null=True) - email_address__ip = serializers.CharField(allow_null=True) - email_address__country_code = serializers.CharField(allow_null=True) - email_address__locale = serializers.CharField(allow_null=True) - created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') + email_address__ip = serializers.CharField(allow_null=True, allow_blank=True) + email_address__country_code = serializers.CharField(allow_null=True, allow_blank=True) + email_address__locale = serializers.CharField(allow_null=True, allow_blank=True) + updated_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') def validate(self, data): data.update({ @@ -53,18 +54,28 @@ class NewsletterSubscriberSerializer(serializers.Serializer): 'ip_address': data.pop('email_address__ip'), 'country_code': data.pop('email_address__country_code'), 'locale': data.pop('email_address__locale'), - 'created': data.pop('created_at'), + 'created': data.pop('updated_at'), 'user_id': self.get_user(data), }) data.pop('email_address__account_id') return data - # def create(self, validated_data): - # obj, _ = Review.objects.update_or_create( - # old_id=validated_data['old_id'], - # defaults=validated_data, - # ) - # return obj + def create(self, validated_data): + try: + obj = Subscriber.objects.get(email=validated_data['email']) + except Subscriber.DoesNotExist: + obj = Subscriber.objects.create(**validated_data) + else: + current_data = obj.created + if validated_data['created'] > current_data: + obj.ip_address = validated_data['ip_address'] + obj.locale = validated_data['locale'] + obj.country_code = validated_data['country_code'] + obj.old_id = validated_data['old_id'] + obj.created = validated_data['created'] + obj.user_id = validated_data['user_id'] + obj.save() + return obj @staticmethod def get_user(data): @@ -73,6 +84,6 @@ class NewsletterSubscriberSerializer(serializers.Serializer): return None user = User.objects.filter(old_id=data['email_address__account_id']).first() - if not user: - raise ValueError(f"User account not found with old_id {data['email_address__account_id']}") - return user.id + if user: + return user.id + return None From 2e463feb8dbd8f314d8347e44e4f3805425cddc2 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 019/115] Sort countries by name accordingly to locale --- apps/location/views/common.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/location/views/common.py b/apps/location/views/common.py index a1e6f011..99cd4c92 100644 --- a/apps/location/views/common.py +++ b/apps/location/views/common.py @@ -1,8 +1,9 @@ """Location app views.""" from rest_framework import generics from rest_framework import permissions - +from django.db.models.expressions import RawSQL from location import models, serializers +from utils.models import get_current_locale # Mixins @@ -37,7 +38,9 @@ class CountryListView(CountryViewMixin, generics.ListAPIView): """List view for model Country.""" pagination_class = None - + def get_queryset(self): + qs = super().get_queryset().order_by(RawSQL("name->>%s", (get_current_locale(),))) + return qs class CountryRetrieveView(CountryViewMixin, generics.RetrieveAPIView): """Retrieve view for model Country.""" From 78e42550c1c635d6e4e91781f454da031db92d7b Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 16 Nov 2019 16:57:26 +0300 Subject: [PATCH 020/115] add address to establishment similar and collection establishment --- apps/collection/views/common.py | 8 ++++---- apps/establishment/serializers/common.py | 11 +++++++++-- apps/establishment/views/web.py | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/collection/views/common.py b/apps/collection/views/common.py index 5bf8f70e..3d06d861 100644 --- a/apps/collection/views/common.py +++ b/apps/collection/views/common.py @@ -1,11 +1,11 @@ +from django.shortcuts import get_object_or_404 from rest_framework import generics from rest_framework import permissions from collection import models -from utils.pagination import ProjectPageNumberPagination -from django.shortcuts import get_object_or_404 -from establishment.serializers import EstablishmentBaseSerializer from collection.serializers import common as serializers +from establishment.serializers import EstablishmentSimilarSerializer +from utils.pagination import ProjectPageNumberPagination # Mixins @@ -53,7 +53,7 @@ class CollectionEstablishmentListView(CollectionListView): """Retrieve list of establishment for collection.""" lookup_field = 'slug' pagination_class = ProjectPageNumberPagination - serializer_class = EstablishmentBaseSerializer + serializer_class = EstablishmentSimilarSerializer def get_queryset(self): """ diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 1d405be7..7051300f 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -17,6 +17,7 @@ from review.serializers import ReviewShortSerializer class ContactPhonesSerializer(serializers.ModelSerializer): """Contact phone serializer""" + class Meta: model = models.ContactPhone fields = [ @@ -26,6 +27,7 @@ class ContactPhonesSerializer(serializers.ModelSerializer): class ContactEmailsSerializer(serializers.ModelSerializer): """Contact email serializer""" + class Meta: model = models.ContactEmail fields = [ @@ -35,6 +37,7 @@ class ContactEmailsSerializer(serializers.ModelSerializer): class SocialNetworkRelatedSerializers(serializers.ModelSerializer): """Social network serializers.""" + class Meta: model = models.SocialNetwork fields = [ @@ -45,7 +48,6 @@ class SocialNetworkRelatedSerializers(serializers.ModelSerializer): class PlateSerializer(ProjectModelSerializer): - name_translated = TranslatedField() currency = CurrencySerializer(read_only=True) @@ -316,6 +318,12 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): ] +class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): + """Serializer for Establishment model.""" + + address = AddressDetailSerializer(read_only=True) + + class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): """Create comment serializer""" mark = serializers.IntegerField() @@ -379,4 +387,3 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): 'content_object': validated_data.pop('establishment') }) return super().create(validated_data) - diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 2a6fd215..20e8f81a 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -72,7 +72,7 @@ class EstablishmentRecentReviewListView(EstablishmentListView): class EstablishmentSimilarListView(EstablishmentListView): """Resource for getting a list of establishments.""" - serializer_class = serializers.EstablishmentBaseSerializer + serializer_class = serializers.EstablishmentSimilarSerializer pagination_class = EstablishmentPortionPagination def get_queryset(self): From cc67499601dfd61d701b33ce2513d80b78cd7be8 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 021/115] Fix wines db queries --- apps/establishment/serializers/common.py | 24 +++++++++++++++++++++++- apps/location/serializers/common.py | 14 ++++++++++++++ apps/product/models.py | 7 ++++++- apps/product/serializers/common.py | 11 ++++++----- apps/product/views/common.py | 4 ++++ apps/tag/serializers.py | 15 +++++++++++++++ 6 files changed, 68 insertions(+), 7 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 7051300f..858a203a 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -5,7 +5,7 @@ from rest_framework import serializers from comment import models as comment_models from comment.serializers import common as comment_serializers from establishment import models -from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer +from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, CityShortSerializer from main.serializers import AwardSerializer, CurrencySerializer from tag.serializers import TagBaseSerializer from timetable.serialziers import ScheduleRUDSerializer @@ -193,6 +193,28 @@ class EstablishmentShortSerializer(serializers.ModelSerializer): ] +class EstablishmentProductShortSerializer(serializers.ModelSerializer): + """SHORT Serializer for displaying info about an establishment on product page.""" + establishment_type = EstablishmentTypeGeoSerializer() + establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) + address = AddressBaseSerializer() + city = CityShortSerializer(source='address.city', allow_null=True) + + class Meta: + """Meta class.""" + model = models.Establishment + fields = [ + 'id', + 'name', + 'index_name', + 'slug', + 'city', + 'establishment_type', + 'establishment_subtypes', + 'address', + ] + + class EstablishmentProductSerializer(EstablishmentShortSerializer): """Serializer for displaying info about an establishment on product page.""" diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index fe27892c..6f03eaed 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -54,6 +54,20 @@ class RegionSerializer(serializers.ModelSerializer): 'country_id' ] +class CityShortSerializer(serializers.ModelSerializer): + """Short city serializer""" + country = CountrySerializer(read_only=True) + + class Meta: + """Meta class""" + model = models.City + fields = ( + 'id', + 'name', + 'code', + 'country', + ) + class CitySerializer(serializers.ModelSerializer): """City serializer.""" diff --git a/apps/product/models.py b/apps/product/models.py index 53cd0e61..331cfd72 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -82,7 +82,12 @@ class ProductQuerySet(models.QuerySet): def with_extended_related(self): """Returns qs with almost all related objects.""" return self.with_base_related() \ - .prefetch_related('tags', 'standards', 'classifications', 'classifications__standard', + .prefetch_related('tags', 'tags__category', 'tags__category__country', + 'standards', 'classifications', 'classifications__standard', + 'establishment__address', 'establishment__establishment_type', + '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') diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index da2a2344..0195ee5f 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -4,7 +4,7 @@ from rest_framework import serializers from comment.models import Comment from comment.serializers import CommentSerializer -from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer +from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer, EstablishmentProductShortSerializer from gallery.models import Image from product import models from review.serializers import ReviewShortSerializer @@ -12,13 +12,13 @@ from utils import exceptions as utils_exceptions from utils.serializers import TranslatedField, FavoritesCreateSerializer from main.serializers import AwardSerializer from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer -from tag.serializers import TagBaseSerializer, TagCategoryShortSerializer +from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer class ProductTagSerializer(TagBaseSerializer): """Serializer for model Tag.""" - category = TagCategoryShortSerializer(read_only=True) + category = TagCategoryProductSerializer(read_only=True) class Meta(TagBaseSerializer.Meta): """Meta class.""" @@ -88,10 +88,10 @@ class ProductBaseSerializer(serializers.ModelSerializer): name = serializers.CharField(source='display_name', read_only=True) product_type = ProductTypeBaseSerializer(read_only=True) subtypes = ProductSubTypeBaseSerializer(many=True, read_only=True) - establishment_detail = EstablishmentShortSerializer(source='establishment', 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_colors = TagBaseSerializer(many=True, read_only=True) + wine_colors = ProductTagSerializer(many=True, read_only=True) preview_image_url = serializers.URLField(source='preview_main_image_url', allow_null=True, read_only=True) @@ -120,6 +120,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): class ProductDetailSerializer(ProductBaseSerializer): """Product detail serializer.""" description_translated = TranslatedField() + establishment_detail = EstablishmentShortSerializer(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) diff --git a/apps/product/views/common.py b/apps/product/views/common.py index daa46fd7..911d1f0b 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -26,6 +26,10 @@ class ProductListView(ProductBaseView, generics.ListAPIView): serializer_class = serializers.ProductBaseSerializer filter_class = filters.ProductFilterSet + def get_queryset(self): + qs = super().get_queryset().with_extended_related() + return qs + class ProductDetailView(ProductBaseView, generics.RetrieveAPIView): """Detail view fro model Product.""" diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index 02f2e4d3..2cc818a9 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -37,6 +37,21 @@ class TagBackOfficeSerializer(TagBaseSerializer): 'category' ) +class TagCategoryProductSerializer(serializers.ModelSerializer): + """SHORT Serializer for TagCategory""" + + label_translated = TranslatedField() + + class Meta: + """Meta class.""" + + model = models.TagCategory + fields = ( + 'id', + 'label_translated', + 'index_name', + ) + class TagCategoryBaseSerializer(serializers.ModelSerializer): """Serializer for model TagCategory.""" From 8a86018a5e3a963ca8a52089c2dd4da55d06aaf7 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 022/115] Fix wines db queries #2 --- apps/product/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/product/models.py b/apps/product/models.py index 331cfd72..44c57057 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -275,9 +275,8 @@ class Product(TranslatedFieldsMixin, BaseAttributes): @property def related_tags(self): - return self.tags.exclude( - category__index_name__in=['sugar-content', 'wine-color', 'bottles-produced', - 'serial-number', 'grape-variety']) + return self.tags.exclude(category__index_name__in=['sugar-content', 'wine-color', 'bottles-produced', + 'serial-number', 'grape-variety']).prefetch_related('category') @property def display_name(self): From be0d63cf079448cef363220b7c3f03c8456d09cd Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 023/115] Fix products search --- 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 a1179a4f..9e57c607 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -95,7 +95,7 @@ class ProductDocument(Document): }, multi=True ) - name = fields.KeywordField(attr='display_name') + name = fields.TextField(attr='display_name') class Django: model = models.Product From 9d4a672d67dd6d03664867e746751a05c9fdde48 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 024/115] Multilingual textfield products analyzers --- apps/search_indexes/documents/product.py | 4 +++- apps/search_indexes/views.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index 9e57c607..cb0131cb 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -95,7 +95,9 @@ class ProductDocument(Document): }, multi=True ) - name = fields.TextField(attr='display_name') + 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') class Django: model = models.Product diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index dcc6f895..174b1dd8 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -216,7 +216,11 @@ class ProductDocumentViewSet(BaseDocumentViewSet): search_fields = { 'name': {'fuzziness': 'auto:2,5', - 'boost': 4}, + 'boost': 8}, + 'name_ru': {'fuzziness': 'auto:2,5', + 'boost': 6}, + 'name_fr': {'fuzziness': 'auto:2,5', + 'boost': 7}, 'transliterated_name': {'fuzziness': 'auto:2,5', 'boost': 3}, 'description': {'fuzziness': 'auto:2,5'}, From 70b21107448547e904c9af7e5bad28efeb5139ca Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 025/115] Fix booking slots order --- apps/booking/views.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/booking/views.py b/apps/booking/views.py index cdadc768..73f6f55e 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -14,6 +14,12 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): """ Checks which service to use if establishmend is managed by any """ _VALID_GUESTONLINE_PERIODS = {'lunch', 'dinner', 'afternoon', 'breakfast'} + _GUESTONLINE_PERIODS_TO_PRIOR = { + 'breakfast': 1, + 'lunch': 2, + 'afternoon': 3, + 'dinner': 4, + } permission_classes = (permissions.AllowAny,) serializer_class = CheckBookingSerializer @@ -32,7 +38,7 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): period_template = iter(periods_by_name.values()).__next__().copy() period_template.pop('total_left_seats') - period_template.pop('hours') + period_template['hours'] = [] period_template.pop('period') processed_periods = [ @@ -46,7 +52,8 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): for unnamed_period in unnamed_periods: processed_periods.append(unnamed_period) - response['periods'] = processed_periods + response['periods'] = sorted(processed_periods, + key=lambda x: self._GUESTONLINE_PERIODS_TO_PRIOR[x.get('period', 'lunch')]) return response From 45b0b93068b580dcb6f9ce1416a5ac2a4d4566ce Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sat, 16 Nov 2019 22:49:38 +0300 Subject: [PATCH 026/115] add new migrations for tags --- apps/news/views.py | 5 +- apps/search_indexes/__init__.py | 2 +- apps/search_indexes/signals.py | 134 +++++++++---------- apps/tag/management/commands/add_tags.py | 162 +++++++++++++---------- apps/tag/models.py | 1 + project/settings/base.py | 6 +- project/settings/local.py | 10 +- 7 files changed, 171 insertions(+), 149 deletions(-) diff --git a/apps/news/views.py b/apps/news/views.py index f7d18101..d0528132 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -1,11 +1,8 @@ """News app views.""" from django.conf import settings -from django.db.transaction import on_commit from django.shortcuts import get_object_or_404 -from rest_framework import generics, permissions, status -from rest_framework.response import Response +from rest_framework import generics, permissions -from gallery.tasks import delete_image from news import filters, models, serializers from rating.tasks import add_rating from utils.permissions import IsCountryAdmin, IsContentPageManager diff --git a/apps/search_indexes/__init__.py b/apps/search_indexes/__init__.py index 6c812219..c45cbc38 100644 --- a/apps/search_indexes/__init__.py +++ b/apps/search_indexes/__init__.py @@ -1 +1 @@ -from search_indexes.signals import update_document +# from search_indexes.signals import update_document diff --git a/apps/search_indexes/signals.py b/apps/search_indexes/signals.py index 5bee4e17..1c7dbfe0 100644 --- a/apps/search_indexes/signals.py +++ b/apps/search_indexes/signals.py @@ -1,67 +1,67 @@ -"""Search indexes app signals.""" -from django.db.models.signals import post_save -from django.dispatch import receiver -from django_elasticsearch_dsl.registries import registry - - -@receiver(post_save) -def update_document(sender, **kwargs): - from establishment.models import Establishment - app_label = sender._meta.app_label - model_name = sender._meta.model_name - instance = kwargs['instance'] - - app_label_model_name_to_filter = { - ('location','country'): 'address__city__country', - ('location','city'): 'address__city', - ('location', 'address'): 'address', - # todo: remove after migration - ('establishment', 'establishmenttype'): 'establishment_type', - ('establishment', 'establishmentsubtype'): 'establishment_subtypes', - ('tag', 'tag'): 'tags', - } - filter_name = app_label_model_name_to_filter.get((app_label, model_name)) - if filter_name: - qs = Establishment.objects.filter(**{filter_name: instance}) - for product in qs: - registry.update(product) - - -@receiver(post_save) -def update_news(sender, **kwargs): - from news.models import News - app_label = sender._meta.app_label - model_name = sender._meta.model_name - instance = kwargs['instance'] - app_label_model_name_to_filter = { - ('location','country'): 'country', - ('news','newstype'): 'news_type', - ('tag', 'tag'): 'tags', - } - filter_name = app_label_model_name_to_filter.get((app_label, model_name)) - if filter_name: - qs = News.objects.filter(**{filter_name: instance}) - for product in qs: - registry.update(product) - - -@receiver(post_save) -def update_product(sender, **kwargs): - from product.models import Product - app_label = sender._meta.app_label - model_name = sender._meta.model_name - instance = kwargs['instance'] - app_label_model_name_to_filter = { - ('product','productstandard'): 'standards', - ('product', 'producttype'): 'product_type', - ('tag','tag'): 'tags', - ('location', 'wineregion'): 'wine_region', - ('location', 'winesubregion'): 'wine_sub_region', - ('location', 'winevillage'): 'wine_village', - ('establishment', 'establishment'): 'establishment', - } - filter_name = app_label_model_name_to_filter.get((app_label, model_name)) - if filter_name: - qs = Product.objects.filter(**{filter_name: instance}) - for product in qs: - registry.update(product) +# """Search indexes app signals.""" +# from django.db.models.signals import post_save +# from django.dispatch import receiver +# from django_elasticsearch_dsl.registries import registry +# +# +# @receiver(post_save) +# def update_document(sender, **kwargs): +# from establishment.models import Establishment +# app_label = sender._meta.app_label +# model_name = sender._meta.model_name +# instance = kwargs['instance'] +# +# app_label_model_name_to_filter = { +# ('location','country'): 'address__city__country', +# ('location','city'): 'address__city', +# ('location', 'address'): 'address', +# # todo: remove after migration +# ('establishment', 'establishmenttype'): 'establishment_type', +# ('establishment', 'establishmentsubtype'): 'establishment_subtypes', +# ('tag', 'tag'): 'tags', +# } +# filter_name = app_label_model_name_to_filter.get((app_label, model_name)) +# if filter_name: +# qs = Establishment.objects.filter(**{filter_name: instance}) +# for product in qs: +# registry.update(product) +# +# +# @receiver(post_save) +# def update_news(sender, **kwargs): +# from news.models import News +# app_label = sender._meta.app_label +# model_name = sender._meta.model_name +# instance = kwargs['instance'] +# app_label_model_name_to_filter = { +# ('location','country'): 'country', +# ('news','newstype'): 'news_type', +# ('tag', 'tag'): 'tags', +# } +# filter_name = app_label_model_name_to_filter.get((app_label, model_name)) +# if filter_name: +# qs = News.objects.filter(**{filter_name: instance}) +# for product in qs: +# registry.update(product) +# +# +# @receiver(post_save) +# def update_product(sender, **kwargs): +# from product.models import Product +# app_label = sender._meta.app_label +# model_name = sender._meta.model_name +# instance = kwargs['instance'] +# app_label_model_name_to_filter = { +# ('product','productstandard'): 'standards', +# ('product', 'producttype'): 'product_type', +# ('tag','tag'): 'tags', +# ('location', 'wineregion'): 'wine_region', +# ('location', 'winesubregion'): 'wine_sub_region', +# ('location', 'winevillage'): 'wine_village', +# ('establishment', 'establishment'): 'establishment', +# } +# filter_name = app_label_model_name_to_filter.get((app_label, model_name)) +# if filter_name: +# qs = Product.objects.filter(**{filter_name: instance}) +# for product in qs: +# registry.update(product) diff --git a/apps/tag/management/commands/add_tags.py b/apps/tag/management/commands/add_tags.py index 0fba46f8..b0c54e5c 100644 --- a/apps/tag/management/commands/add_tags.py +++ b/apps/tag/management/commands/add_tags.py @@ -3,87 +3,111 @@ from django.core.management.base import BaseCommand from establishment.models import Establishment, EstablishmentType from transfer import models as legacy from tag.models import Tag, TagCategory +from tqdm import tqdm +from django.db import connections +from collections import namedtuple + + +def namedtuplefetchall(cursor): + "Return all rows from a cursor as a namedtuple" + desc = cursor.description + nt_result = namedtuple('Result', [col[0] for col in desc]) + return [nt_result(*row) for row in cursor.fetchall()] + + +def metadata_category_sql(): + with connections['legacy'].cursor() as cursor: + cursor.execute( + '''SELECT + `key`, + establishments.type, + key_value_metadata.`value_type`, + public, + key_value_metadata.id as 'old_id' + FROM metadata + LEFT JOIN establishments + ON metadata.establishment_id=establishments.id + LEFT JOIN key_value_metadata + ON metadata.key=key_value_metadata.key_name + GROUP BY + establishments.type, + `key`, + key_value_metadata.`value_type`, + public, old_id;''' + ) + return namedtuplefetchall(cursor) + + +def metadata_tags_sql(): + with connections['legacy'].cursor() as cursor: + cursor.execute( + """ + SELECT + value, + `key` as category, + establishment_id +FROM metadata +WHERE establishment_id is not null""" + ) + return namedtuplefetchall(cursor) class Command(BaseCommand): help = 'Add tags values from old db to new db' - def handle(self, *args, **kwargs): + def get_type(self, meta): + meta_type = meta.value_type + if not meta.value_type: + if meta.key == 'wineyard_visits': + meta_type = 'list' + elif meta.key in ['private_room', 'outside_sits']: + meta_type = 'bool' + return meta_type + def handle(self, *args, **kwargs): existing_establishment = Establishment.objects.filter( old_id__isnull=False ) - ESTABLISHMENT = 1 - SHOP = 2 - RESTAURANT = 3 - WINEYARD = 4 - MAPPER = { - RESTAURANT: EstablishmentType.RESTAURANT, - WINEYARD: EstablishmentType.PRODUCER, - SHOP: EstablishmentType.ARTISAN + 'Restaurant': EstablishmentType.RESTAURANT, + 'Wineyard': EstablishmentType.PRODUCER, + 'Shop': EstablishmentType.ARTISAN } + # remove old black category + for establishment_tag in tqdm(EstablishmentType.objects.all()): + establishment_tag.tag_categories.remove(*list( + establishment_tag.tag_categories.exclude( + tags__establishments__isnull=False).distinct())) - mapper_values_meta = legacy.KeyValueMetadatumKeyValueMetadatumEstablishments.objects.all() - for key, value in MAPPER.items(): - values_meta_id_list = mapper_values_meta.filter( - key_value_metadatum_establishment_id=key - ).values_list('key_value_metadatum_id') + # created Tag Category + for meta in tqdm(metadata_category_sql()): + category, _ = TagCategory.objects.update_or_create( + index_name=meta.key, + defaults={ + "public": True if meta.public == 1 else False, + "value_type": self.get_type(meta), + "label": {"en-GB": meta.key} + } + ) - est_type, _ = EstablishmentType.objects.get_or_create(index_name=value) + # add to EstablishmentType + est_type = EstablishmentType.objects.get(index_name=MAPPER[meta.type]) + if category not in est_type.tag_categories.all(): + est_type.tag_categories.add(category) - key_value_metadata = legacy.KeyValueMetadata.objects.filter( - id__in=values_meta_id_list) + count = 0 + for meta_tag in tqdm(metadata_tags_sql()): - # create TagCategory - for key_value in key_value_metadata: - tag_category, created = TagCategory.objects.get_or_create( - index_name=key_value.key_name, - ) - - if created: - tag_category.label = { - 'en-GB': key_value.key_name, - 'fr-FR': key_value.key_name, - 'ru-RU': key_value.key_name, - } - tag_category.value_type = key_value.value_type - tag_category.save() - est_type.tag_categories.add( - tag_category - ) - - # create Tag - for tag in key_value.metadata_set.filter( - establishment__id__in=list( - existing_establishment.values_list('old_id', flat=True) - )): - - new_tag, created = Tag.objects.get_or_create( - value=tag.value, - category=tag_category, - ) - if created: - - sp = tag.value.split('_') - value = ' '.join([sp[0].capitalize()] + sp[1:]) - - trans = { - 'en-GB': value, - 'fr-FR': value, - 'ru-RU': value, - } - - aliases = legacy.MetadatumAliases.objects.filter(value=tag.value) - - for alias in aliases: - trans[alias.locale] = alias.meta_alias - - new_tag.label = trans - new_tag.save() - - est = existing_establishment.filter( - old_id=tag.establishment_id).first() - if est: - est.tags.add(new_tag) - est.save() + tag, _ = Tag.objects.update_or_create( + category=TagCategory.objects.get(index_name=meta_tag.category), + value=meta_tag.value, + defaults={ + "label": {"en-GB": meta_tag.value} + } + ) + establishment = existing_establishment.filter(old_id=meta_tag.establishment_id).first() + if establishment: + if tag not in establishment.tags.all(): + establishment.tags.add(tag) + count += 1 + self.stdout.write(self.style.WARNING(f'Created {count} tags to Establishment')) diff --git a/apps/tag/models.py b/apps/tag/models.py index a35425e8..29f18a23 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -37,6 +37,7 @@ class Tag(TranslatedFieldsMixin, models.Model): chosen_tag_settings = models.ManyToManyField(Country, through='ChosenTagSettings') priority = models.PositiveIntegerField(null=True, default=0) + # It does not make sense since in the old base another structure with duplicates old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'), diff --git a/project/settings/base.py b/project/settings/base.py index 70427ef3..4a2d0584 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -66,7 +66,7 @@ PROJECT_APPS = [ 'partner.apps.PartnerConfig', 'product.apps.ProductConfig', 'recipe.apps.RecipeConfig', - 'search_indexes.apps.SearchIndexesConfig', + # 'search_indexes.apps.SearchIndexesConfig', 'translation.apps.TranslationConfig', 'configuration.apps.ConfigurationConfig', 'timetable.apps.TimetableConfig', @@ -79,8 +79,8 @@ PROJECT_APPS = [ EXTERNAL_APPS = [ 'corsheaders', - 'django_elasticsearch_dsl', - 'django_elasticsearch_dsl_drf', + # 'django_elasticsearch_dsl', + # 'django_elasticsearch_dsl_drf', 'django_filters', 'drf_yasg', 'fcm_django', diff --git a/project/settings/local.py b/project/settings/local.py index f9a096fe..605a98c9 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, + # }, } } From ad3966b2aa261751c23dc9b4307cac36b22996a9 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sat, 16 Nov 2019 22:56:32 +0300 Subject: [PATCH 027/115] add method for label --- apps/tag/management/commands/add_tags.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/tag/management/commands/add_tags.py b/apps/tag/management/commands/add_tags.py index b0c54e5c..3c43a14d 100644 --- a/apps/tag/management/commands/add_tags.py +++ b/apps/tag/management/commands/add_tags.py @@ -43,11 +43,11 @@ def metadata_tags_sql(): cursor.execute( """ SELECT - value, - `key` as category, - establishment_id -FROM metadata -WHERE establishment_id is not null""" + value, + `key` as category, + establishment_id + FROM metadata + WHERE establishment_id is not null""" ) return namedtuplefetchall(cursor) @@ -64,6 +64,11 @@ class Command(BaseCommand): meta_type = 'bool' return meta_type + def get_label(self, text): + sp = text.key.split('_') + label = ' '.join([sp[0].capitalize()] + sp[1:]) + return label + def handle(self, *args, **kwargs): existing_establishment = Establishment.objects.filter( old_id__isnull=False @@ -86,7 +91,7 @@ class Command(BaseCommand): defaults={ "public": True if meta.public == 1 else False, "value_type": self.get_type(meta), - "label": {"en-GB": meta.key} + "label": {"en-GB": self.get_label(meta.key)} } ) @@ -102,7 +107,7 @@ class Command(BaseCommand): category=TagCategory.objects.get(index_name=meta_tag.category), value=meta_tag.value, defaults={ - "label": {"en-GB": meta_tag.value} + "label": {"en-GB": self.get_label(meta_tag.value)} } ) establishment = existing_establishment.filter(old_id=meta_tag.establishment_id).first() From b1d176c583eee7640bf734fae00837715a3c8ea8 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sat, 16 Nov 2019 23:01:22 +0300 Subject: [PATCH 028/115] enable elastic --- apps/search_indexes/__init__.py | 2 +- apps/search_indexes/signals.py | 134 ++++++++++++++++---------------- project/settings/base.py | 6 +- 3 files changed, 71 insertions(+), 71 deletions(-) diff --git a/apps/search_indexes/__init__.py b/apps/search_indexes/__init__.py index c45cbc38..6c812219 100644 --- a/apps/search_indexes/__init__.py +++ b/apps/search_indexes/__init__.py @@ -1 +1 @@ -# from search_indexes.signals import update_document +from search_indexes.signals import update_document diff --git a/apps/search_indexes/signals.py b/apps/search_indexes/signals.py index 1c7dbfe0..5bee4e17 100644 --- a/apps/search_indexes/signals.py +++ b/apps/search_indexes/signals.py @@ -1,67 +1,67 @@ -# """Search indexes app signals.""" -# from django.db.models.signals import post_save -# from django.dispatch import receiver -# from django_elasticsearch_dsl.registries import registry -# -# -# @receiver(post_save) -# def update_document(sender, **kwargs): -# from establishment.models import Establishment -# app_label = sender._meta.app_label -# model_name = sender._meta.model_name -# instance = kwargs['instance'] -# -# app_label_model_name_to_filter = { -# ('location','country'): 'address__city__country', -# ('location','city'): 'address__city', -# ('location', 'address'): 'address', -# # todo: remove after migration -# ('establishment', 'establishmenttype'): 'establishment_type', -# ('establishment', 'establishmentsubtype'): 'establishment_subtypes', -# ('tag', 'tag'): 'tags', -# } -# filter_name = app_label_model_name_to_filter.get((app_label, model_name)) -# if filter_name: -# qs = Establishment.objects.filter(**{filter_name: instance}) -# for product in qs: -# registry.update(product) -# -# -# @receiver(post_save) -# def update_news(sender, **kwargs): -# from news.models import News -# app_label = sender._meta.app_label -# model_name = sender._meta.model_name -# instance = kwargs['instance'] -# app_label_model_name_to_filter = { -# ('location','country'): 'country', -# ('news','newstype'): 'news_type', -# ('tag', 'tag'): 'tags', -# } -# filter_name = app_label_model_name_to_filter.get((app_label, model_name)) -# if filter_name: -# qs = News.objects.filter(**{filter_name: instance}) -# for product in qs: -# registry.update(product) -# -# -# @receiver(post_save) -# def update_product(sender, **kwargs): -# from product.models import Product -# app_label = sender._meta.app_label -# model_name = sender._meta.model_name -# instance = kwargs['instance'] -# app_label_model_name_to_filter = { -# ('product','productstandard'): 'standards', -# ('product', 'producttype'): 'product_type', -# ('tag','tag'): 'tags', -# ('location', 'wineregion'): 'wine_region', -# ('location', 'winesubregion'): 'wine_sub_region', -# ('location', 'winevillage'): 'wine_village', -# ('establishment', 'establishment'): 'establishment', -# } -# filter_name = app_label_model_name_to_filter.get((app_label, model_name)) -# if filter_name: -# qs = Product.objects.filter(**{filter_name: instance}) -# for product in qs: -# registry.update(product) +"""Search indexes app signals.""" +from django.db.models.signals import post_save +from django.dispatch import receiver +from django_elasticsearch_dsl.registries import registry + + +@receiver(post_save) +def update_document(sender, **kwargs): + from establishment.models import Establishment + app_label = sender._meta.app_label + model_name = sender._meta.model_name + instance = kwargs['instance'] + + app_label_model_name_to_filter = { + ('location','country'): 'address__city__country', + ('location','city'): 'address__city', + ('location', 'address'): 'address', + # todo: remove after migration + ('establishment', 'establishmenttype'): 'establishment_type', + ('establishment', 'establishmentsubtype'): 'establishment_subtypes', + ('tag', 'tag'): 'tags', + } + filter_name = app_label_model_name_to_filter.get((app_label, model_name)) + if filter_name: + qs = Establishment.objects.filter(**{filter_name: instance}) + for product in qs: + registry.update(product) + + +@receiver(post_save) +def update_news(sender, **kwargs): + from news.models import News + app_label = sender._meta.app_label + model_name = sender._meta.model_name + instance = kwargs['instance'] + app_label_model_name_to_filter = { + ('location','country'): 'country', + ('news','newstype'): 'news_type', + ('tag', 'tag'): 'tags', + } + filter_name = app_label_model_name_to_filter.get((app_label, model_name)) + if filter_name: + qs = News.objects.filter(**{filter_name: instance}) + for product in qs: + registry.update(product) + + +@receiver(post_save) +def update_product(sender, **kwargs): + from product.models import Product + app_label = sender._meta.app_label + model_name = sender._meta.model_name + instance = kwargs['instance'] + app_label_model_name_to_filter = { + ('product','productstandard'): 'standards', + ('product', 'producttype'): 'product_type', + ('tag','tag'): 'tags', + ('location', 'wineregion'): 'wine_region', + ('location', 'winesubregion'): 'wine_sub_region', + ('location', 'winevillage'): 'wine_village', + ('establishment', 'establishment'): 'establishment', + } + filter_name = app_label_model_name_to_filter.get((app_label, model_name)) + if filter_name: + qs = Product.objects.filter(**{filter_name: instance}) + for product in qs: + registry.update(product) diff --git a/project/settings/base.py b/project/settings/base.py index 4a2d0584..70427ef3 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -66,7 +66,7 @@ PROJECT_APPS = [ 'partner.apps.PartnerConfig', 'product.apps.ProductConfig', 'recipe.apps.RecipeConfig', - # 'search_indexes.apps.SearchIndexesConfig', + 'search_indexes.apps.SearchIndexesConfig', 'translation.apps.TranslationConfig', 'configuration.apps.ConfigurationConfig', 'timetable.apps.TimetableConfig', @@ -79,8 +79,8 @@ PROJECT_APPS = [ EXTERNAL_APPS = [ 'corsheaders', - # 'django_elasticsearch_dsl', - # 'django_elasticsearch_dsl_drf', + 'django_elasticsearch_dsl', + 'django_elasticsearch_dsl_drf', 'django_filters', 'drf_yasg', 'fcm_django', From 9f03da9bef1a0e1729e6a8bbb41a2cdbb73fd729 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sat, 16 Nov 2019 23:03:17 +0300 Subject: [PATCH 029/115] fix for tag migration --- apps/tag/management/commands/add_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tag/management/commands/add_tags.py b/apps/tag/management/commands/add_tags.py index 3c43a14d..fd3443ec 100644 --- a/apps/tag/management/commands/add_tags.py +++ b/apps/tag/management/commands/add_tags.py @@ -65,7 +65,7 @@ class Command(BaseCommand): return meta_type def get_label(self, text): - sp = text.key.split('_') + sp = text.split('_') label = ' '.join([sp[0].capitalize()] + sp[1:]) return label From 82c79091a3393ac30a636c6846f8f9db40e18162 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sat, 16 Nov 2019 23:24:22 +0300 Subject: [PATCH 030/115] add some change for add translations for tags --- .../commands/add_tags_translation.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/tag/management/commands/add_tags_translation.py b/apps/tag/management/commands/add_tags_translation.py index a0f19e4c..485a7f11 100644 --- a/apps/tag/management/commands/add_tags_translation.py +++ b/apps/tag/management/commands/add_tags_translation.py @@ -1,19 +1,17 @@ from django.core.management.base import BaseCommand -from establishment.models import Establishment, EstablishmentType +from tag.models import Tag from transfer import models as legacy -from tag.models import Tag, TagCategory +from tqdm import tqdm class Command(BaseCommand): help = 'Add tags translation from old db to new db' - def handle(self, *args, **kwargs): - translation = legacy.MetadatumAliases.objects.all() - # Humanisation for default values - + def humanization_tag(self): + """Humanisation for default values""" tags = Tag.objects.all() - for tag in tags: + for tag in tqdm(tags): value = tag.label for k, v in value.items(): if isinstance(v, str) and '_' in v: @@ -22,10 +20,13 @@ class Command(BaseCommand): tag.label[k] = v tag.save() - for trans in translation: + def handle(self, *args, **kwargs): + translation = legacy.MetadatumAliases.objects.all() + # self.humanization_tag() + for trans in tqdm(translation): tag = Tag.objects.filter(value=trans.value).first() if tag: tag.label.update( {trans.locale: trans.meta_alias} ) - tag.save() \ No newline at end of file + tag.save() From 09db5c95409749fb3f54c4e0d0cdc3d73aeb5fd3 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sat, 16 Nov 2019 23:26:50 +0300 Subject: [PATCH 031/115] minor changes for tags translation command --- apps/tag/management/commands/add_tags_translation.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/tag/management/commands/add_tags_translation.py b/apps/tag/management/commands/add_tags_translation.py index 485a7f11..a168d35e 100644 --- a/apps/tag/management/commands/add_tags_translation.py +++ b/apps/tag/management/commands/add_tags_translation.py @@ -8,8 +8,9 @@ from tqdm import tqdm class Command(BaseCommand): help = 'Add tags translation from old db to new db' - def humanization_tag(self): - """Humanisation for default values""" + @staticmethod + def humanisation_tag(self): + """Humanisation for default values.""" tags = Tag.objects.all() for tag in tqdm(tags): value = tag.label @@ -21,8 +22,9 @@ class Command(BaseCommand): tag.save() def handle(self, *args, **kwargs): + """Translation for existed tags.""" translation = legacy.MetadatumAliases.objects.all() - # self.humanization_tag() + # self.humanisation_tag() for trans in tqdm(translation): tag = Tag.objects.filter(value=trans.value).first() if tag: From a8b3c9aa63f852b1ff3f2273ccbaf1ebea630f63 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 17 Nov 2019 14:22:30 +0300 Subject: [PATCH 032/115] refactored galleries, added transfer script --- .../migrations/0008_auto_20191117_1117.py | 17 ++++ apps/establishment/admin.py | 7 ++ .../commands/fill_establishment_gallery.py | 41 ++++++++ .../migrations/0062_auto_20191117_1117.py | 38 ++++++++ apps/establishment/models.py | 36 ++++++- apps/establishment/serializers/back.py | 44 +++++++++ apps/establishment/serializers/common.py | 16 +++- apps/establishment/urls/back.py | 7 ++ apps/establishment/views/back.py | 43 ++++++++- apps/news/admin.py | 7 ++ apps/news/models.py | 18 +--- apps/news/serializers.py | 79 +--------------- apps/news/views.py | 13 ++- apps/product/admin.py | 13 +-- .../migrations/0014_auto_20191117_1117.py | 19 ++++ apps/product/models.py | 59 ++++-------- apps/product/serializers/common.py | 94 ++++--------------- apps/product/views/back.py | 12 ++- .../migrations/0018_auto_20191117_1117.py | 19 ++++ apps/review/models.py | 22 ++--- apps/transfer/management/commands/transfer.py | 12 +-- apps/utils/models.py | 73 ++++++++++++++ apps/utils/serializers.py | 11 +++ project/settings/base.py | 8 ++ 24 files changed, 454 insertions(+), 254 deletions(-) create mode 100644 apps/advertisement/migrations/0008_auto_20191117_1117.py create mode 100644 apps/establishment/management/commands/fill_establishment_gallery.py create mode 100644 apps/establishment/migrations/0062_auto_20191117_1117.py create mode 100644 apps/product/migrations/0014_auto_20191117_1117.py create mode 100644 apps/review/migrations/0018_auto_20191117_1117.py diff --git a/apps/advertisement/migrations/0008_auto_20191117_1117.py b/apps/advertisement/migrations/0008_auto_20191117_1117.py new file mode 100644 index 00000000..75661c79 --- /dev/null +++ b/apps/advertisement/migrations/0008_auto_20191117_1117.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.7 on 2019-11-17 11:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('advertisement', '0007_auto_20191115_0750'), + ] + + operations = [ + migrations.AlterModelOptions( + name='advertisement', + options={'verbose_name': 'Advertisement', 'verbose_name_plural': 'Advertisements'}, + ), + ] diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index 366d54cf..139e12c6 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -32,6 +32,12 @@ class ContactPhoneInline(admin.TabularInline): extra = 0 +class GalleryImageInline(admin.TabularInline): + """Gallery image inline admin.""" + model = models.EstablishmentGallery + extra = 0 + + class ContactEmailInline(admin.TabularInline): """Contact email inline admin.""" model = models.ContactEmail @@ -59,6 +65,7 @@ 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, ] # inlines = [ # AwardInline, ContactPhoneInline, ContactEmailInline, diff --git a/apps/establishment/management/commands/fill_establishment_gallery.py b/apps/establishment/management/commands/fill_establishment_gallery.py new file mode 100644 index 00000000..cbe7d9e8 --- /dev/null +++ b/apps/establishment/management/commands/fill_establishment_gallery.py @@ -0,0 +1,41 @@ +from django.core.management.base import BaseCommand + +from establishment.models import Establishment, EstablishmentGallery +from gallery.models import Image +import requests +from rest_framework import status + + +class Command(BaseCommand): + help = 'Fill establishment gallery from existing images' + + def handle(self, *args, **kwargs): + count = 0 + not_valid_link_counter = 0 + not_valid_urls = [] + + cdn_prefix = 'https://1dc3f33f6d-3.optimicdn.com/gaultmillau.com/' + establishments = Establishment.objects.exclude(image_url__isnull=True) \ + .exclude(preview_image_url__isnull=True) + for establishment in establishments: + image_url = establishment.image_url.rstrip() + relative_image_path = image_url[len(cdn_prefix):] + + response = requests.head(image_url, allow_redirects=True) + if response.status_code != status.HTTP_200_OK: + not_valid_link_counter += 1 + not_valid_urls.append(image_url) + + image, image_created = Image.objects.get_or_create( + orientation=Image.HORIZONTAL, + title=f'{establishment.name} - {relative_image_path}', + image=relative_image_path) + gallery, _ = EstablishmentGallery.objects.get_or_create(establishment=establishment, + image=image, + is_main=True) + if image_created: + count += 1 + + self.stdout.write(self.style.WARNING(f'Created/updated {count} objects.\n' + f'Not valid link counter: {not_valid_link_counter}\n' + f'List of non valid image url: {not_valid_urls}')) diff --git a/apps/establishment/migrations/0062_auto_20191117_1117.py b/apps/establishment/migrations/0062_auto_20191117_1117.py new file mode 100644 index 00000000..9d0fc2f9 --- /dev/null +++ b/apps/establishment/migrations/0062_auto_20191117_1117.py @@ -0,0 +1,38 @@ +# Generated by Django 2.2.7 on 2019-11-17 11:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0006_merge_20191027_1758'), + ('establishment', '0061_auto_20191114_0550'), + ] + + operations = [ + migrations.AlterModelOptions( + name='establishmentnote', + options={'verbose_name': 'establishment note', 'verbose_name_plural': 'establishment notes'}, + ), + migrations.CreateModel( + name='EstablishmentGallery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_main', models.BooleanField(default=False, verbose_name='Is the main image')), + ('establishment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='establishment_gallery', to='establishment.Establishment', verbose_name='establishment')), + ('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='establishment_gallery', to='gallery.Image', verbose_name='image')), + ], + options={ + 'verbose_name': 'establishment gallery', + 'verbose_name_plural': 'establishment galleries', + 'unique_together': {('establishment', 'is_main'), ('establishment', 'image')}, + }, + ), + migrations.AddField( + model_name='establishment', + name='gallery', + field=models.ManyToManyField(through='establishment.EstablishmentGallery', to='gallery.Image'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 2c85890f..c296fe33 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -22,7 +22,8 @@ from location.models import Address from main.models import Award, Currency from review.models import Review from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, - TranslatedFieldsMixin, BaseAttributes) + TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin, + IntermediateGalleryModelMixin) # todo: establishment type&subtypes check @@ -316,9 +317,11 @@ class EstablishmentQuerySet(models.QuerySet): return self.exclude(address__city__country__in=countries) -class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): +class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): """Establishment model.""" + # todo: delete image URL fields after moving on gallery + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) name = models.CharField(_('name'), max_length=255, default='') transliterated_name = models.CharField(default='', max_length=255, @@ -376,6 +379,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): related_name='establishments', blank=True, default=None, verbose_name=_('Collections')) + gallery = models.ManyToManyField('gallery.Image', through='EstablishmentGallery') preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), max_length=255, blank=True, null=True, default=None) slug = models.SlugField(unique=True, max_length=255, null=True, @@ -544,6 +548,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): """Return list products with type wine""" return self.products.wines() + @property + def main_image(self): + qs = self.establishment_gallery.main_image() + if qs.exists(): + return qs.first().image + class EstablishmentNoteQuerySet(models.QuerySet): """QuerySet for model EstablishmentNote.""" @@ -565,8 +575,26 @@ class EstablishmentNote(ProjectBaseMixin): class Meta: """Meta class.""" - verbose_name_plural = _('product note') - verbose_name = _('product notes') + verbose_name_plural = _('establishment notes') + verbose_name = _('establishment note') + + +class EstablishmentGallery(IntermediateGalleryModelMixin): + + establishment = models.ForeignKey(Establishment, null=True, + related_name='establishment_gallery', + on_delete=models.CASCADE, + verbose_name=_('establishment')) + image = models.ForeignKey('gallery.Image', null=True, + related_name='establishment_gallery', + on_delete=models.CASCADE, + verbose_name=_('image')) + + class Meta: + """Meta class.""" + verbose_name = _('establishment gallery') + verbose_name_plural = _('establishment galleries') + unique_together = (('establishment', 'is_main'), ('establishment', 'image')) class Position(BaseAttributes, TranslatedFieldsMixin): diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 5957cb16..0f9fe4c8 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -9,6 +9,8 @@ from location.serializers import AddressDetailSerializer from main.models import Currency from utils.decorators import with_base_attributes from utils.serializers import TimeZoneChoiceField +from gallery.models import Image +from django.utils.translation import gettext_lazy as _ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): @@ -160,3 +162,45 @@ class EmployeeBackSerializers(serializers.ModelSerializer): 'user', 'name' ] + + +class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer): + """Serializer class for model EstablishmentGallery.""" + + class Meta: + """Meta class""" + + model = models.EstablishmentGallery + fields = [ + 'id', + 'is_main', + ] + + def get_request_kwargs(self): + """Get url kwargs from request.""" + return self.context.get('request').parser_context.get('kwargs') + + def validate(self, attrs): + """Override validate method.""" + establishment_pk = self.get_request_kwargs().get('pk') + image_id = self.get_request_kwargs().get('image_id') + + establishment_qs = models.Establishment.objects.filter(pk=establishment_pk) + image_qs = Image.objects.filter(id=image_id) + + if not establishment_qs.exists(): + raise serializers.ValidationError({'detail': _('Establishment not found')}) + + if not image_qs.exists(): + raise serializers.ValidationError({'detail': _('Image not found')}) + + establishment = establishment_qs.first() + image = image_qs.first() + + if image in establishment.gallery.all(): + raise serializers.ValidationError({'detail': _('Image is already added.')}) + + attrs['establishment'] = establishment + attrs['image'] = image + + return attrs diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 1d405be7..fc625c13 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -13,6 +13,7 @@ from utils import exceptions as utils_exceptions from utils.serializers import (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) from review.serializers import ReviewShortSerializer +from utils.serializers import ImageBaseSerializer class ContactPhonesSerializer(serializers.ModelSerializer): @@ -206,13 +207,18 @@ class EstablishmentProductSerializer(EstablishmentShortSerializer): class EstablishmentBaseSerializer(ProjectModelSerializer): """Base serializer for Establishment model.""" - preview_image = serializers.URLField(source='preview_image_url') address = AddressBaseSerializer() in_favorites = serializers.BooleanField(allow_null=True) tags = TagBaseSerializer(read_only=True, many=True) currency = CurrencySerializer() type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') + image = serializers.URLField(source='image_url', read_only=True) + preview_image = serializers.URLField(source='preview_image_url', + allow_null=True, + read_only=True) + + new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) class Meta: """Meta class.""" @@ -227,13 +233,15 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): 'toque_number', 'public_mark', 'slug', - 'preview_image', 'in_favorites', 'address', 'tags', 'currency', 'type', 'subtypes', + 'image', + 'preview_image', + 'new_image', ] @@ -272,7 +280,6 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" description_translated = TranslatedField() - image = serializers.URLField(source='image_url') awards = AwardSerializer(many=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True) phones = ContactPhonesSerializer(read_only=True, many=True) @@ -288,6 +295,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): range_price_menu = RangePriceSerializer(read_only=True) range_price_carte = RangePriceSerializer(read_only=True) vintage_year = serializers.ReadOnlyField() + gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True) class Meta(EstablishmentBaseSerializer.Meta): """Meta class.""" @@ -313,6 +321,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): 'range_price_carte', 'transportation', 'vintage_year', + 'gallery', ] @@ -379,4 +388,3 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): 'content_object': validated_data.pop('establishment') }) return super().create(validated_data) - diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 14575c46..c8792b5b 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -31,4 +31,11 @@ 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'), + + # gallery + path('/gallery/', views.EstablishmentBackOfficeGalleryListView.as_view(), + name='gallery-list'), + path('/gallery//', + views.EstablishmentBackOfficeGalleryCreateDestroyView.as_view(), + name='gallery-create-destroy'), ] diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 1a547032..5d337b73 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -2,9 +2,10 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions -from utils.permissions import IsCountryAdmin, IsEstablishmentManager from establishment import filters, models, serializers from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer +from utils.permissions import IsCountryAdmin, IsEstablishmentManager +from utils.views import CreateDestroyGalleryViewMixin class EstablishmentMixinViews: @@ -184,3 +185,43 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView): """Establishment subtype retrieve/update/destroy view.""" serializer_class = serializers.EstablishmentSubTypeBaseSerializer queryset = models.EstablishmentSubType.objects.all() + + +class EstablishmentBackOfficeGalleryCreateDestroyView(EstablishmentMixinViews, + CreateDestroyGalleryViewMixin): + """Resource for a create|destroy gallery for product for back-office users.""" + serializer_class = serializers.EstablishmentBackOfficeGallerySerializer + + def get_object(self): + """ + Returns the object the view is displaying. + """ + establishment_qs = self.filter_queryset(self.get_queryset()) + + establishment = get_object_or_404(establishment_qs, pk=self.kwargs['pk']) + gallery = get_object_or_404(establishment.establishment_gallery, image_id=self.kwargs['image_id']) + + # May raise a permission denied + self.check_object_permissions(self.request, gallery) + + return gallery + + +class EstablishmentBackOfficeGalleryListView(EstablishmentMixinViews, + generics.ListAPIView): + """Resource for returning gallery for establishment for back-office users.""" + serializer_class = serializers.ImageBaseSerializer + + def get_object(self): + """Override get_object method.""" + qs = super(EstablishmentBackOfficeGalleryListView, self).get_queryset() + establishment = get_object_or_404(qs, pk=self.kwargs['pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, establishment) + + return establishment + + def get_queryset(self): + """Override get_queryset method.""" + return self.get_object().crop_gallery diff --git a/apps/news/admin.py b/apps/news/admin.py index cc48a887..7687fa92 100644 --- a/apps/news/admin.py +++ b/apps/news/admin.py @@ -24,12 +24,19 @@ def send_email_action(modeladmin, request, queryset): send_email_action.short_description = "Send the selected news by email" +class NewsGalleryInline(admin.TabularInline): + """News gallery inline.""" + model = models.NewsGallery + extra = 0 + + @admin.register(models.News) class NewsAdmin(BaseModelAdminMixin, admin.ModelAdmin): """News admin.""" raw_id_fields = ('address',) actions = [send_email_action] raw_id_fields = ('news_type', 'address', 'country') + inlines = [NewsGalleryInline, ] @admin.register(models.NewsGallery) diff --git a/apps/news/models.py b/apps/news/models.py index c7fc8017..821fc0c0 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -7,7 +7,8 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse from rating.models import Rating, ViewCount -from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin, ProjectBaseMixin +from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, + ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin) from utils.querysets import TranslationQuerysetMixin @@ -124,7 +125,7 @@ class NewsQuerySet(TranslationQuerysetMixin): ) -class News(BaseAttributes, TranslatedFieldsMixin): +class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin): """News model.""" STR_FIELD_NAME = 'title' @@ -248,15 +249,8 @@ class News(BaseAttributes, TranslatedFieldsMixin): return count_value -class NewsGalleryQuerySet(models.QuerySet): - """QuerySet for model News""" +class NewsGallery(IntermediateGalleryModelMixin): - def main_image(self): - """Return objects with flag is_main is True""" - return self.filter(is_main=True) - - -class NewsGallery(models.Model): news = models.ForeignKey(News, null=True, related_name='news_gallery', on_delete=models.CASCADE, @@ -265,10 +259,6 @@ class NewsGallery(models.Model): related_name='news_gallery', on_delete=models.CASCADE, verbose_name=_('gallery')) - is_main = models.BooleanField(default=False, - verbose_name=_('Is the main image')) - - objects = NewsGalleryQuerySet.as_manager() class Meta: """NewsGallery meta class.""" diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 7638847f..3fca14b8 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -10,7 +10,8 @@ from location.serializers import CountrySimpleSerializer, AddressBaseSerializer from news import models from tag.serializers import TagBaseSerializer from utils import exceptions as utils_exceptions -from utils.serializers import TranslatedField, ProjectModelSerializer, FavoritesCreateSerializer +from utils.serializers import (TranslatedField, ProjectModelSerializer, + FavoritesCreateSerializer, ImageBaseSerializer) class AgendaSerializer(ProjectModelSerializer): @@ -47,78 +48,6 @@ class NewsBannerSerializer(ProjectModelSerializer): ) -class CropImageSerializer(serializers.Serializer): - """Serializer for crop images for News object.""" - - preview_url = serializers.SerializerMethodField() - promo_horizontal_web_url = serializers.SerializerMethodField() - promo_horizontal_mobile_url = serializers.SerializerMethodField() - tile_horizontal_web_url = serializers.SerializerMethodField() - tile_horizontal_mobile_url = serializers.SerializerMethodField() - tile_vertical_web_url = serializers.SerializerMethodField() - highlight_vertical_web_url = serializers.SerializerMethodField() - editor_web_url = serializers.SerializerMethodField() - editor_mobile_url = serializers.SerializerMethodField() - - def get_preview_url(self, obj): - """Get crop preview.""" - return obj.instance.get_image_url('news_preview') - - def get_promo_horizontal_web_url(self, obj): - """Get crop promo_horizontal_web.""" - return obj.instance.get_image_url('news_promo_horizontal_web') - - def get_promo_horizontal_mobile_url(self, obj): - """Get crop promo_horizontal_mobile.""" - return obj.instance.get_image_url('news_promo_horizontal_mobile') - - def get_tile_horizontal_web_url(self, obj): - """Get crop tile_horizontal_web.""" - return obj.instance.get_image_url('news_tile_horizontal_web') - - def get_tile_horizontal_mobile_url(self, obj): - """Get crop tile_horizontal_mobile.""" - return obj.instance.get_image_url('news_tile_horizontal_mobile') - - def get_tile_vertical_web_url(self, obj): - """Get crop tile_vertical_web.""" - return obj.instance.get_image_url('news_tile_vertical_web') - - def get_highlight_vertical_web_url(self, obj): - """Get crop highlight_vertical_web.""" - return obj.instance.get_image_url('news_highlight_vertical_web') - - def get_editor_web_url(self, obj): - """Get crop editor_web.""" - return obj.instance.get_image_url('news_editor_web') - - def get_editor_mobile_url(self, obj): - """Get crop editor_mobile.""" - return obj.instance.get_image_url('news_editor_mobile') - - -class NewsImageSerializer(serializers.ModelSerializer): - """Serializer for returning crop images of news image.""" - - orientation_display = serializers.CharField(source='get_orientation_display', - read_only=True) - original_url = serializers.URLField(source='image.url') - auto_crop_images = CropImageSerializer(source='image', allow_null=True) - - class Meta: - model = Image - fields = [ - 'id', - 'title', - 'orientation_display', - 'original_url', - 'auto_crop_images', - ] - extra_kwargs = { - 'orientation': {'write_only': True} - } - - class NewsTypeSerializer(serializers.ModelSerializer): """News type serializer.""" @@ -170,7 +99,7 @@ class NewsSimilarListSerializer(NewsBaseSerializer): class NewsListSerializer(NewsBaseSerializer): """List serializer for News model.""" - image = NewsImageSerializer(source='main_image', allow_null=True) + image = ImageBaseSerializer(source='crop_main_image', allow_null=True) class Meta(NewsBaseSerializer.Meta): """Meta class.""" @@ -188,7 +117,7 @@ class NewsDetailSerializer(NewsBaseSerializer): author = UserBaseSerializer(source='created_by', read_only=True) state_display = serializers.CharField(source='get_state_display', read_only=True) - gallery = NewsImageSerializer(read_only=True, many=True) + gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True) class Meta(NewsBaseSerializer.Meta): """Meta class.""" diff --git a/apps/news/views.py b/apps/news/views.py index f7d18101..7c7a58c6 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -1,15 +1,13 @@ """News app views.""" from django.conf import settings -from django.db.transaction import on_commit from django.shortcuts import get_object_or_404 -from rest_framework import generics, permissions, status -from rest_framework.response import Response +from rest_framework import generics, permissions -from gallery.tasks import delete_image 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.serializers import ImageBaseSerializer class NewsMixinView: @@ -118,9 +116,10 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, return gallery -class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView): +class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, + generics.ListAPIView): """Resource for returning gallery for news for back-office users.""" - serializer_class = serializers.NewsImageSerializer + serializer_class = ImageBaseSerializer def get_object(self): """Override get_object method.""" @@ -134,7 +133,7 @@ class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIVie def get_queryset(self): """Override get_queryset method.""" - return self.get_object().gallery.all() + return self.get_object().crop_gallery class NewsBackOfficeRUDView(NewsBackOfficeMixinView, diff --git a/apps/product/admin.py b/apps/product/admin.py index dafbdbcb..3becfb2d 100644 --- a/apps/product/admin.py +++ b/apps/product/admin.py @@ -4,22 +4,23 @@ from utils.admin import BaseModelAdminMixin from .models import Product, ProductType, ProductSubType, ProductGallery, Unit +class ProductGalleryInline(admin.TabularInline): + """Product gallery inline.""" + model = ProductGallery + extra = 0 + + @admin.register(Product) class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Admin page for model Product.""" search_fields = ('name', ) list_filter = ('available', 'product_type') list_display = ('id', '__str__', 'get_category_display', 'product_type') + inlines = [ProductGalleryInline, ] raw_id_fields = ('subtypes', 'classifications', 'standards', 'tags', 'gallery', 'establishment',) -@admin.register(ProductGallery) -class ProductGalleryAdmin(admin.ModelAdmin): - """Admin page for model ProductGallery.""" - raw_id_fields = ('product', 'image', ) - - @admin.register(ProductType) class ProductTypeAdmin(admin.ModelAdmin): """Admin page for model ProductType.""" diff --git a/apps/product/migrations/0014_auto_20191117_1117.py b/apps/product/migrations/0014_auto_20191117_1117.py new file mode 100644 index 00000000..70476eb5 --- /dev/null +++ b/apps/product/migrations/0014_auto_20191117_1117.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-17 11:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0013_auto_20191113_1512'), + ] + + operations = [ + migrations.AlterField( + model_name='productgallery', + name='image', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_gallery', to='gallery.Image', verbose_name='image'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index ba9ccd38..ed80c08d 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -8,7 +8,8 @@ from django.utils.translation import gettext_lazy as _ from django.core.validators import MaxValueValidator, MinValueValidator from utils.models import (BaseAttributes, ProjectBaseMixin, - TranslatedFieldsMixin, TJSONField) + TranslatedFieldsMixin, TJSONField, + GalleryModelMixin, IntermediateGalleryModelMixin) class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): @@ -125,7 +126,7 @@ class ProductQuerySet(models.QuerySet): ) -class Product(TranslatedFieldsMixin, BaseAttributes): +class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes): """Product models.""" EARLIEST_VINTAGE_YEAR = 1700 @@ -222,16 +223,6 @@ class Product(TranslatedFieldsMixin, BaseAttributes): """Override str dunder method.""" return f'{self.name}' - def clean_fields(self, exclude=None): - super().clean_fields(exclude=exclude) - if self.product_type.index_name == ProductType.WINE and not self.wine_region: - raise ValidationError(_('wine_region field must be specified.')) - if not self.product_type.index_name == ProductType.WINE and self.wine_region: - raise ValidationError(_('wine_region field must not be specified.')) - # if (self.wine_region and self.wine_appellation) and \ - # self.wine_appellation not in self.wine_region.appellations.all(): - # raise ValidationError(_('Wine appellation not exists in wine region.')) - @property def product_type_translated_name(self): """Get translated name of product type.""" @@ -254,20 +245,6 @@ class Product(TranslatedFieldsMixin, BaseAttributes): def bottles_produced(self): return self.tags.filter(category__index_name='bottles-produced') - @property - def main_image(self): - qs = ProductGallery.objects.filter(product=self, is_main=True) - if qs.exists(): - return qs.first().image - - @property - def main_image_url(self): - return self.main_image.image if self.main_image else None - - @property - def preview_main_image_url(self): - return self.main_image.get_image_url('product_preview') if self.main_image else None - @property def related_tags(self): return self.tags.exclude( @@ -282,6 +259,21 @@ class Product(TranslatedFieldsMixin, BaseAttributes): name = f'{self.establishment.name} - ' + name return name + @property + def main_image(self): + qs = self.product_gallery.main_image() + if qs.exists(): + return qs.first().image + + @property + def image_url(self): + return self.main_image.image.url if self.main_image else None + + @property + def preview_image_url(self): + if self.main_image: + return self.main_image.get_image_url(thumbnail_key='product_preview') + class OnlineProductManager(ProductManager): """Extended manger for OnlineProduct model.""" @@ -353,15 +345,8 @@ class ProductStandard(models.Model): verbose_name = _('wine standard') -class ProductGalleryQuerySet(models.QuerySet): - """QuerySet for model Product""" +class ProductGallery(IntermediateGalleryModelMixin): - def main_image(self): - """Return objects with flag is_main is True""" - return self.filter(is_main=True) - - -class ProductGallery(models.Model): product = models.ForeignKey(Product, null=True, related_name='product_gallery', on_delete=models.CASCADE, @@ -369,11 +354,7 @@ class ProductGallery(models.Model): image = models.ForeignKey('gallery.Image', null=True, related_name='product_gallery', on_delete=models.CASCADE, - verbose_name=_('gallery')) - is_main = models.BooleanField(default=False, - verbose_name=_('Is the main image')) - - objects = ProductGalleryQuerySet.as_manager() + verbose_name=_('image')) class Meta: """ProductGallery meta class.""" diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index da2a2344..bb7f8a94 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -9,7 +9,7 @@ from gallery.models import Image from product import models from review.serializers import ReviewShortSerializer from utils import exceptions as utils_exceptions -from utils.serializers import TranslatedField, FavoritesCreateSerializer +from utils.serializers import TranslatedField, FavoritesCreateSerializer, ImageBaseSerializer from main.serializers import AwardSerializer from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer from tag.serializers import TagBaseSerializer, TagCategoryShortSerializer @@ -83,6 +83,15 @@ class ProductStandardBaseSerializer(serializers.ModelSerializer): ) +class ProductCropImageSerializer(serializers.Serializer): + """Serializer for product image.""" + + +class ProductImageSerializer(ImageBaseSerializer): + """Serializer for product image.""" + auto_crop_images = ProductCropImageSerializer(allow_null=True) + + class ProductBaseSerializer(serializers.ModelSerializer): """Product base serializer.""" name = serializers.CharField(source='display_name', read_only=True) @@ -92,8 +101,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): tags = ProductTagSerializer(source='related_tags', many=True, read_only=True) wine_region = WineRegionBaseSerializer(read_only=True) wine_colors = TagBaseSerializer(many=True, read_only=True) - preview_image_url = serializers.URLField(source='preview_main_image_url', - allow_null=True, + preview_image_url = serializers.URLField(allow_null=True, read_only=True) in_favorites = serializers.BooleanField(allow_null=True) @@ -127,9 +135,10 @@ class ProductDetailSerializer(ProductBaseSerializer): wine_sub_region = WineSubRegionBaseSerializer(read_only=True) bottles_produced = TagBaseSerializer(many=True, read_only=True) sugar_contents = TagBaseSerializer(many=True, read_only=True) - image_url = serializers.ImageField(source='main_image_url', - allow_null=True, - read_only=True) + image_url = serializers.URLField(allow_null=True, + read_only=True) + + new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) class Meta(ProductBaseSerializer.Meta): fields = ProductBaseSerializer.Meta.fields + [ @@ -142,6 +151,7 @@ class ProductDetailSerializer(ProductBaseSerializer): 'bottles_produced', 'sugar_contents', 'image_url', + 'new_image', ] @@ -175,78 +185,6 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer): return super().create(validated_data) -# class CropImageSerializer(serializers.Serializer): -# """Serializer for crop images for News object.""" -# -# preview_url = serializers.SerializerMethodField() -# promo_horizontal_web_url = serializers.SerializerMethodField() -# promo_horizontal_mobile_url = serializers.SerializerMethodField() -# tile_horizontal_web_url = serializers.SerializerMethodField() -# tile_horizontal_mobile_url = serializers.SerializerMethodField() -# tile_vertical_web_url = serializers.SerializerMethodField() -# highlight_vertical_web_url = serializers.SerializerMethodField() -# editor_web_url = serializers.SerializerMethodField() -# editor_mobile_url = serializers.SerializerMethodField() -# -# def get_preview_url(self, obj): -# """Get crop preview.""" -# return obj.instance.get_image_url('news_preview') -# -# def get_promo_horizontal_web_url(self, obj): -# """Get crop promo_horizontal_web.""" -# return obj.instance.get_image_url('news_promo_horizontal_web') -# -# def get_promo_horizontal_mobile_url(self, obj): -# """Get crop promo_horizontal_mobile.""" -# return obj.instance.get_image_url('news_promo_horizontal_mobile') -# -# def get_tile_horizontal_web_url(self, obj): -# """Get crop tile_horizontal_web.""" -# return obj.instance.get_image_url('news_tile_horizontal_web') -# -# def get_tile_horizontal_mobile_url(self, obj): -# """Get crop tile_horizontal_mobile.""" -# return obj.instance.get_image_url('news_tile_horizontal_mobile') -# -# def get_tile_vertical_web_url(self, obj): -# """Get crop tile_vertical_web.""" -# return obj.instance.get_image_url('news_tile_vertical_web') -# -# def get_highlight_vertical_web_url(self, obj): -# """Get crop highlight_vertical_web.""" -# return obj.instance.get_image_url('news_highlight_vertical_web') -# -# def get_editor_web_url(self, obj): -# """Get crop editor_web.""" -# return obj.instance.get_image_url('news_editor_web') -# -# def get_editor_mobile_url(self, obj): -# """Get crop editor_mobile.""" -# return obj.instance.get_image_url('news_editor_mobile') - - -class ProductImageSerializer(serializers.ModelSerializer): - """Serializer for returning crop images of product image.""" - - orientation_display = serializers.CharField(source='get_orientation_display', - read_only=True) - original_url = serializers.URLField(source='image.url') - # auto_crop_images = CropImageSerializer(source='image', allow_null=True) - - class Meta: - model = Image - fields = [ - 'id', - 'title', - 'orientation_display', - 'original_url', - # 'auto_crop_images', - ] - extra_kwargs = { - 'orientation': {'write_only': True} - } - - class ProductCommentCreateSerializer(CommentSerializer): """Create comment serializer""" mark = serializers.IntegerField() diff --git a/apps/product/views/back.py b/apps/product/views/back.py index 70ac9c57..2e072fc6 100644 --- a/apps/product/views/back.py +++ b/apps/product/views/back.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from product import serializers, models from product.views import ProductBaseView +from utils.serializers import ImageBaseSerializer from utils.views import CreateDestroyGalleryViewMixin @@ -65,9 +66,11 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, return gallery -class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView): +class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, + generics.ListAPIView): """Resource for returning gallery for product for back-office users.""" - serializer_class = serializers.ProductImageSerializer + serializer_class = ImageBaseSerializer + permission_classes = (permissions.IsAuthenticated,) def get_object(self): """Override get_object method.""" @@ -81,10 +84,11 @@ class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.List def get_queryset(self): """Override get_queryset method.""" - return self.get_object().gallery.all() + return self.get_object().crop_gallery -class ProductDetailBackOfficeView(ProductBackOfficeMixinView, generics.RetrieveUpdateDestroyAPIView): +class ProductDetailBackOfficeView(ProductBackOfficeMixinView, + generics.RetrieveUpdateDestroyAPIView): """Product back-office R/U/D view.""" serializer_class = serializers.ProductBackOfficeDetailSerializer diff --git a/apps/review/migrations/0018_auto_20191117_1117.py b/apps/review/migrations/0018_auto_20191117_1117.py new file mode 100644 index 00000000..a4e4b7b1 --- /dev/null +++ b/apps/review/migrations/0018_auto_20191117_1117.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-17 11:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0017_auto_20191115_0737'), + ] + + operations = [ + migrations.AlterField( + model_name='inquiriesgallery', + name='image', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inquiries_gallery', to='gallery.Image', verbose_name='image'), + ), + ] diff --git a/apps/review/models.py b/apps/review/models.py index 1dadc104..8734f4f6 100644 --- a/apps/review/models.py +++ b/apps/review/models.py @@ -4,8 +4,9 @@ from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from django.utils.translation import gettext_lazy as _ -from utils.models import BaseAttributes, TranslatedFieldsMixin, ProjectBaseMixin -from utils.models import TJSONField +from utils.models import (BaseAttributes, TranslatedFieldsMixin, + ProjectBaseMixin, GalleryModelMixin, + TJSONField, IntermediateGalleryModelMixin) class ReviewQuerySet(models.QuerySet): @@ -92,7 +93,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin): verbose_name_plural = _('Reviews') -class Inquiries(ProjectBaseMixin): +class Inquiries(GalleryModelMixin, ProjectBaseMixin): NONE = 0 DINER = 1 LUNCH = 2 @@ -145,15 +146,7 @@ class GridItems(ProjectBaseMixin): return f'inquiry: {self.inquiry.id}, grid id: {self.id}' -class InquiriesGalleryQuerySet(models.QuerySet): - """QuerySet for model Inquiries""" - - def main_image(self): - """Return objects with flag is_main is True""" - return self.filter(is_main=True) - - -class InquiriesGallery(models.Model): +class InquiriesGallery(IntermediateGalleryModelMixin): old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) inquiry = models.ForeignKey( Inquiries, @@ -167,11 +160,8 @@ class InquiriesGallery(models.Model): null=True, related_name='inquiries_gallery', on_delete=models.CASCADE, - verbose_name=_('gallery'), + verbose_name=_('image'), ) - is_main = models.BooleanField(default=False, verbose_name=_('Is the main image')) - - objects = InquiriesGalleryQuerySet.as_manager() class Meta: verbose_name = _('inquiry gallery') diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index f30a8467..d69dd908 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -30,12 +30,12 @@ class Command(BaseCommand): 'update_country_flag', 'comment', 'inquiries', # №6 - перенос запросов оценок - 'wine_characteristics', - 'product', - 'product_note', - 'souvenir', - 'establishment_note', - 'assemblage', + 'wine_characteristics', # №5 - перенос характиристик вин + 'product', # №5 - перенос продуктов + 'product_note', # №6 - перенос заметок продуктов + 'souvenir', # №5 - перенос продуктов типа - сувениры + 'establishment_note', # №5 - перенос заметок заведений + 'assemblage', # №6 - перенос тегов для типа продуктов - вино 'rating_count', 'product_review', 'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1 diff --git a/apps/utils/models.py b/apps/utils/models.py index cb51b5ca..a325c8ff 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -345,4 +345,77 @@ class GMTokenGenerator(PasswordResetTokenGenerator): return self.get_fields(user, timestamp) +class GalleryModelMixin(models.Model): + """Mixin for models that has gallery.""" + + @property + def crop_gallery(self): + if hasattr(self, 'gallery'): + gallery = [] + images = self.gallery.all() + crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES + if p.startswith(self._meta.model_name.lower())] + for image in images: + d = { + 'id': image.id, + 'title': image.title, + 'original_url': image.image.url, + 'orientation_display': image.get_orientation_display(), + 'auto_crop_images': {}, + } + for crop in crop_parameters: + d['auto_crop_images'].update({crop: image.get_image_url(crop)}) + gallery.append(d) + return gallery + + @property + def crop_main_image(self): + if hasattr(self, 'main_image') and self.main_image: + image = self.main_image + image_property = { + 'id': image.id, + 'title': image.title, + 'original_url': image.image.url, + 'orientation_display': image.get_orientation_display(), + 'auto_crop_images': {}, + } + crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES + if p.startswith(self._meta.model_name.lower())] + for crop in crop_parameters: + image_property['auto_crop_images'].update( + {crop: image.get_image_url(crop)} + ) + return image_property + + class Meta: + """Meta class.""" + abstract = True + + +class IntermediateGalleryModelQuerySet(models.QuerySet): + """Extended QuerySet.""" + + def main_image(self): + """Return objects with flag is_main is True""" + return self.filter(is_main=True) + + +class IntermediateGalleryModelMixin(models.Model): + """Mixin for intermediate gallery model.""" + + is_main = models.BooleanField(default=False, + verbose_name=_('Is the main image')) + + objects = IntermediateGalleryModelQuerySet.as_manager() + + class Meta: + """Meta class.""" + abstract = True + + def __str__(self): + """Overridden str method.""" + if hasattr(self, 'image'): + return self.image.title if self.image.title else self.id + + timezone.datetime.now().date().isoformat() \ No newline at end of file diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 9a7be59d..b78c202c 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -5,6 +5,7 @@ from rest_framework import serializers from utils import models from translation.models import Language from favorites.models import Favorites +from gallery.models import Image class EmptySerializer(serializers.Serializer): @@ -104,3 +105,13 @@ class RecursiveFieldSerializer(serializers.Serializer): def to_representation(self, value): serializer = self.parent.parent.__class__(value, context=self.context) return serializer.data + + +class ImageBaseSerializer(serializers.Serializer): + """Serializer for returning crop images of model image.""" + + id = serializers.IntegerField() + title = serializers.CharField() + original_url = serializers.URLField() + orientation_display = serializers.CharField() + auto_crop_images = serializers.DictField(allow_null=True) diff --git a/project/settings/base.py b/project/settings/base.py index 70427ef3..d91f54e2 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -380,6 +380,14 @@ SORL_THUMBNAIL_ALIASES = { 'news_editor_mobile': {'geometry_string': '343x260', 'crop': 'center'}, # при загрузке через контент эдитор 'avatar_comments_web': {'geometry_string': '116x116', 'crop': 'center'}, # через контент эдитор в мобильном браузерe 'product_preview': {'geometry_string': '300x260', 'crop': 'center'}, + 'establishment_preview': {'geometry_string': '300x260', 'crop': 'center'}, + 'establishment_xsmall': {'geometry_string': '60x34', 'crop': 'center'}, + 'establishment_small': {'geometry_string': '80x45', 'crop': 'center'}, + 'establishment_medium': {'geometry_string': '280x158', 'crop': 'center'}, + 'establishment_large': {'geometry_string': '440x248', 'crop': 'center'}, + 'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'}, + 'establishment_detail': {'geometry_string': '2048x1152', 'crop': 'center'}, + 'establishment_original': {'geometry_string': '1920x1080', 'crop': 'center'}, } From c2c182e992977c899ea9f16a0233a3775becc935 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 17 Nov 2019 14:51:48 +0300 Subject: [PATCH 033/115] remove duplicated migration --- .../migrations/0008_auto_20191117_1117.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 apps/advertisement/migrations/0008_auto_20191117_1117.py diff --git a/apps/advertisement/migrations/0008_auto_20191117_1117.py b/apps/advertisement/migrations/0008_auto_20191117_1117.py deleted file mode 100644 index 75661c79..00000000 --- a/apps/advertisement/migrations/0008_auto_20191117_1117.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.7 on 2019-11-17 11:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('advertisement', '0007_auto_20191115_0750'), - ] - - operations = [ - migrations.AlterModelOptions( - name='advertisement', - options={'verbose_name': 'Advertisement', 'verbose_name_plural': 'Advertisements'}, - ), - ] From 62c4ab8d747fb06286d622b4a7a0706835b0b684 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 17 Nov 2019 20:39:03 +0300 Subject: [PATCH 034/115] added temporary potential bug (change auto_crop_images dict names for News) --- apps/news/models.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/apps/news/models.py b/apps/news/models.py index 821fc0c0..8de503a0 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -10,6 +10,7 @@ from rating.models import Rating, ViewCount from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin) from utils.querysets import TranslationQuerysetMixin +from django.conf import settings class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): @@ -248,6 +249,48 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin): count_value = self.views_count.count return count_value + # todo: remove in future + @property + def crop_gallery(self): + if hasattr(self, 'gallery'): + gallery = [] + images = self.gallery.all() + model_name = self._meta.model_name.lower() + crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES + if p.startswith(model_name)] + for image in images: + d = { + 'id': image.id, + 'title': image.title, + 'original_url': image.image.url, + 'orientation_display': image.get_orientation_display(), + 'auto_crop_images': {}, + } + for crop in crop_parameters: + d['auto_crop_images'].update( + {crop[len(f'{model_name}_'):]: image.get_image_url(crop)}) + gallery.append(d) + return gallery + + @property + def crop_main_image(self): + if hasattr(self, 'main_image') and self.main_image: + image = self.main_image + model_name = self._meta.model_name.lower() + image_property = { + 'id': image.id, + 'title': image.title, + 'original_url': image.image.url, + 'orientation_display': image.get_orientation_display(), + 'auto_crop_images': {}, + } + crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES + if p.startswith(self._meta.model_name.lower())] + for crop in crop_parameters: + image_property['auto_crop_images'].update( + {crop[len(f'{model_name}_'):]: image.get_image_url(crop)}) + return image_property + class NewsGallery(IntermediateGalleryModelMixin): From 0d2f961ad5b9204b25189d7b2fbd0ca1c3a7d04b Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 17 Nov 2019 21:52:07 +0300 Subject: [PATCH 035/115] added temporary potential bug (change auto_crop_images dict names for News) --- apps/news/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/news/models.py b/apps/news/models.py index 8de503a0..7e26de5f 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -268,7 +268,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin): } for crop in crop_parameters: d['auto_crop_images'].update( - {crop[len(f'{model_name}_'):]: image.get_image_url(crop)}) + {f'{crop[len(f"{model_name}_"):]}_url': image.get_image_url(crop)}) gallery.append(d) return gallery @@ -288,7 +288,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin): if p.startswith(self._meta.model_name.lower())] for crop in crop_parameters: image_property['auto_crop_images'].update( - {crop[len(f'{model_name}_'):]: image.get_image_url(crop)}) + {f'{crop[len(f"{model_name}_"):]}_url': image.get_image_url(crop)}) return image_property From 8641188c459e67c8927e178eda4e68e4bcecdec0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 036/115] Chosen tags for artisans --- apps/tag/filters.py | 13 ++++++++++++- apps/tag/models.py | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 08d14e94..47ec26e0 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -52,11 +52,21 @@ class TagCategoryFilterSet(TagsBaseFilterSet): class TagsFilterSet(TagsBaseFilterSet): """Chosen tags filterset.""" + establishment_type = filters.CharFilter(method='by_establishment_type') + class Meta: """Meta class.""" model = models.Tag - fields = ('type',) + fields = ( + 'type', + 'establishment_type', + ) + + def by_establishment_type(self, queryset, name, value): + if value == EstablishmentType.ARTISAN: + return models.Tag.objects.by_category_index_name('shop_category') + return queryset.by_establishment_type(value) # TMP TODO remove it later # Временный хардкод для демонстрации 4 ноября, потом удалить! @@ -71,3 +81,4 @@ class TagsFilterSet(TagsBaseFilterSet): queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct( 'value') return queryset + diff --git a/apps/tag/models.py b/apps/tag/models.py index 7572766f..297afc0f 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -19,9 +19,15 @@ class TagQuerySet(models.QuerySet): return self.filter(models.Q(category__establishment_types__isnull=False) | models.Q(category__establishment_subtypes__isnull=False)) + def by_category_index_name(self, index_name): + return self.filter(category__index_name=index_name) + def order_by_priority(self): return self.order_by('chosentagsettings__priority') + def by_establishment_type(self, index_name): + return self.filter(category__establishment_types__index_name=index_name) + class Tag(TranslatedFieldsMixin, models.Model): """Tag model.""" From 1159dfca6524d77aaf6ab35ecd92e5fc25747357 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 17 Nov 2019 22:55:03 +0300 Subject: [PATCH 037/115] added assemblage to detail view of product, and fix verbose name in ProductType --- .../migrations/0015_auto_20191117_1954.py | 18 ++++++++++++++++++ apps/product/models.py | 6 +++++- apps/product/serializers/common.py | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 apps/product/migrations/0015_auto_20191117_1954.py diff --git a/apps/product/migrations/0015_auto_20191117_1954.py b/apps/product/migrations/0015_auto_20191117_1954.py new file mode 100644 index 00000000..78fbfee0 --- /dev/null +++ b/apps/product/migrations/0015_auto_20191117_1954.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-17 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0014_auto_20191117_1117'), + ] + + operations = [ + migrations.AlterField( + model_name='producttype', + name='tag_categories', + field=models.ManyToManyField(related_name='product_types', to='tag.TagCategory', verbose_name='Tag categories'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index b76340da..4a92a5fa 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -31,7 +31,7 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): use_subtypes = models.BooleanField(_('Use subtypes'), default=True) tag_categories = models.ManyToManyField('tag.TagCategory', related_name='product_types', - verbose_name=_('Tag')) + verbose_name=_('Tag categories')) class Meta: """Meta class.""" @@ -250,6 +250,10 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes): def bottles_produced(self): return self.tags.filter(category__index_name='bottles-produced') + @property + def grape_variety(self): + return self.tags.filter(category__index_name='grape-variety') + @property def related_tags(self): return self.tags.exclude(category__index_name__in=['sugar-content', 'wine-color', 'bottles-produced', diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 8aba842e..6fe2d0b3 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -127,6 +127,7 @@ class ProductDetailSerializer(ProductBaseSerializer): wine_sub_region = WineSubRegionBaseSerializer(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) image_url = serializers.URLField(allow_null=True, read_only=True) @@ -144,6 +145,7 @@ class ProductDetailSerializer(ProductBaseSerializer): 'sugar_contents', 'image_url', 'new_image', + 'grape_variety', ] From 7d26d4a0cfcdd35c39216749017f0e9f7f38d502 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 038/115] Tags for artisans. Chosen & Category --- apps/tag/filters.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 47ec26e0..65440d98 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -46,7 +46,11 @@ class TagCategoryFilterSet(TagsBaseFilterSet): # todo: filter by establishment type def by_establishment_type(self, queryset, name, value): - return queryset.by_establishment_type(value) + if value == EstablishmentType.ARTISAN: + qs = models.TagCategory.objects.filter(index_name='shop_category') + else: + qs = queryset.by_establishment_type(value) + return qs class TagsFilterSet(TagsBaseFilterSet): @@ -65,7 +69,7 @@ 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') + return models.Tag.objects.by_category_index_name('shop_category')[0:8] return queryset.by_establishment_type(value) # TMP TODO remove it later From a305c6ba836c00224942b4b48bdd3489d9aa5971 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 039/115] try to fix tag categories (cherry picked from commit 376d22c) --- apps/tag/models.py | 2 +- project/settings/local.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/tag/models.py b/apps/tag/models.py index 297afc0f..b8abce59 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -109,7 +109,7 @@ class TagCategoryQuerySet(models.QuerySet): def by_product_type(self, index_name): """Filter by product type index name.""" - return self.filter(product_types__index_name=index_name) + return self.filter(tags__products__product_type__index_name=index_name) def with_tags(self, switcher=True): """Filter by existing tags.""" diff --git a/project/settings/local.py b/project/settings/local.py index 605a98c9..f9a096fe 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, + }, } } From d9e46fdfad8d6d1e5911a4361e949882f9b3b1f6 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 040/115] Wine color for wine tags only --- apps/tag/filters.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 65440d98..c127f765 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -3,6 +3,7 @@ from django_filters import rest_framework as filters from establishment.models import EstablishmentType from django.conf import settings from tag import models +from product import models as product_models class TagsBaseFilterSet(filters.FilterSet): @@ -42,7 +43,10 @@ class TagCategoryFilterSet(TagsBaseFilterSet): 'product_type', ) def by_product_type(self, queryset, name, value): - return queryset.by_product_type(value) + if value == product_models.ProductType.WINE: + queryset = queryset.filter(index_name='wine-color') + queryset = queryset.by_product_type(value) + return queryset # todo: filter by establishment type def by_establishment_type(self, queryset, name, value): From cb1366b57b2bcc031d7cce6cf10cb50272c14305 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 18 Nov 2019 00:01:13 +0300 Subject: [PATCH 041/115] added grape variety attr to es --- apps/search_indexes/documents/product.py | 8 ++++++++ apps/search_indexes/serializers.py | 8 +++++--- apps/search_indexes/views.py | 7 +++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index cb0131cb..cbd5c645 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -44,6 +44,14 @@ class ProductDocument(Document): }, multi=True, ) + grape_variety = fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True, + ) wine_region = fields.ObjectField(properties={ 'id': fields.IntegerField(), 'name': fields.KeywordField(), diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index a26b80fc..62646942 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -69,8 +69,8 @@ class WineRegionDocumentSerializer(serializers.Serializer): return instance.wine_region if instance and instance.wine_region else None -class WineColorDocumentSerializer(serializers.Serializer): - """Wine color ES document serializer,""" +class TagDocumentSerializer(serializers.Serializer): + """Tag ES document serializer,""" id = serializers.IntegerField() label_translated = serializers.SerializerMethodField() @@ -217,7 +217,8 @@ class ProductDocumentSerializer(DocumentSerializer): tags = TagsDocumentSerializer(many=True) subtypes = ProductSubtypeDocumentSerializer(many=True, allow_null=True) wine_region = WineRegionDocumentSerializer(allow_null=True) - wine_colors = WineColorDocumentSerializer(many=True) + wine_colors = TagDocumentSerializer(many=True) + grape_variety = TagDocumentSerializer(many=True) product_type = ProductTypeDocumentSerializer(allow_null=True) establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True) @@ -241,5 +242,6 @@ class ProductDocumentSerializer(DocumentSerializer): 'subtypes', 'wine_region', 'wine_colors', + 'grape_variety', 'establishment_detail', ) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 174b1dd8..a9a05d9b 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -243,6 +243,13 @@ class ProductDocumentViewSet(BaseDocumentViewSet): constants.LOOKUP_QUERY_EXCLUDE, ] }, + 'grape_variety_id': { + 'field': 'grape_variety.id', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE, + ] + }, 'wine_from_country_code': { 'field': 'wine_region.country.code', }, From f8c9a5c8302b974f446ae3e582c4881c1b3f118c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 042/115] Wine regions route --- apps/location/models.py | 4 ++++ apps/location/serializers/common.py | 11 +++++++++++ apps/location/urls/common.py | 2 ++ apps/location/views/common.py | 9 +++++++++ 4 files changed, 26 insertions(+) diff --git a/apps/location/models.py b/apps/location/models.py index 77854269..a1e0d60f 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -169,6 +169,9 @@ class Address(models.Model): class WineRegionQuerySet(models.QuerySet): """Wine region queryset.""" + def with_sub_region_related(self): + return self.prefetch_related('wine_sub_region') + class WineRegion(models.Model, TranslatedFieldsMixin): """Wine region model.""" @@ -207,6 +210,7 @@ class WineSubRegion(models.Model): """Wine sub region model.""" name = models.CharField(_('name'), max_length=255) wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT, + related_name='wine_sub_region', verbose_name=_('wine sub region')) old_id = models.PositiveIntegerField(_('old id'), default=None, blank=True, null=True) diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 6f03eaed..fadb6f6c 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -189,3 +189,14 @@ class WineSubRegionBaseSerializer(serializers.ModelSerializer): 'id', 'name', ] + + +class WineRegionSerializer(WineRegionBaseSerializer): + """Wine region w/ subregion serializer""" + + wine_sub_region = WineSubRegionBaseSerializer(allow_null=True, many=True) + + class Meta(WineRegionBaseSerializer.Meta): + fields = WineRegionBaseSerializer.Meta.fields + [ + 'wine_sub_region' + ] diff --git a/apps/location/urls/common.py b/apps/location/urls/common.py index 3b73f607..c3ea21db 100644 --- a/apps/location/urls/common.py +++ b/apps/location/urls/common.py @@ -17,4 +17,6 @@ urlpatterns = [ path('regions/', views.RegionListView.as_view(), name='region-list'), path('regions//', views.RegionRetrieveView.as_view(), name='region-retrieve'), + + path('wine-regions/', views.WineRegionListView.as_view(), name='wine-region-list'), ] diff --git a/apps/location/views/common.py b/apps/location/views/common.py index 99cd4c92..19689329 100644 --- a/apps/location/views/common.py +++ b/apps/location/views/common.py @@ -63,6 +63,15 @@ class RegionListView(RegionViewMixin, generics.ListAPIView): serializer_class = serializers.CountrySerializer +class WineRegionListView(generics.ListAPIView): + """List view for model WineRegion""" + pagination_class = None + model = models.WineRegion + permission_classes = (permissions.AllowAny,) + queryset = models.WineRegion.objects.with_sub_region_related().all() + serializer_class = serializers.WineRegionSerializer + + class RegionDestroyView(RegionViewMixin, generics.DestroyAPIView): """Destroy view for model Country""" serializer_class = serializers.CountrySerializer From 83153b14c1246734d9e58f4e15ae7703500facfb Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 043/115] Add wine_region and wine_sub_region to ES --- apps/search_indexes/documents/product.py | 5 +++- apps/search_indexes/views.py | 34 +++++++++++++----------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index cbd5c645..d6be37a2 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -64,7 +64,10 @@ class ProductDocument(Document): # 'coordinates': fields.GeoPointField(), 'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES), }) - wine_sub_region = fields.ObjectField(properties={'name': fields.KeywordField()}) + 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/views.py b/apps/search_indexes/views.py index a9a05d9b..ad7bf74c 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -197,21 +197,13 @@ class ProductDocumentViewSet(BaseDocumentViewSet): """Product document ViewSet.""" document = ProductDocument - # lookup_field = 'slug' pagination_class = ProjectMobilePagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.ProductDocumentSerializer - # def get_queryset(self): - # qs = super(ProductDocumentViewSet, self).get_queryset() - # qs = qs.filter('match', is_publish=True) - # return qs - filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, - # GeoSpatialFilteringFilterBackend, -# DefaultOrderingFilterBackend, ] search_fields = { @@ -234,14 +226,28 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'slug': 'slug', 'tags_id': { 'field': 'tags.id', - 'lookups': [constants.LOOKUP_QUERY_IN] + 'lookups': [constants.LOOKUP_QUERY_IN], }, 'wine_colors_id': { 'field': 'wine_colors.id', 'lookups': [ constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_EXCLUDE, - ] + ], + }, + 'wine_region_id': { + 'field': 'wine_region.id', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE, + ], + }, + 'wine_sub_region_id': { + 'field': 'wine_sub_region_id', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE, + ], }, 'grape_variety_id': { 'field': 'grape_variety.id', @@ -264,8 +270,6 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'lookups': [ constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_EXCLUDE, - ] - } - } - # geo_spatial_filter_fields = { - # } \ No newline at end of file + ], + }, + } \ No newline at end of file From f4ebd58e095f1fb30cbf90fb424b4733b32da64d Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 044/115] Tune products ES --- 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 d6be37a2..874be5a6 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -28,6 +28,7 @@ class ProductDocument(Document): }, multi=True ) + preview_image_url = fields.KeywordField(attr='preview_image_url') establishment = fields.ObjectField( properties={ 'id': fields.IntegerField(), diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 62646942..32b7cd82 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -229,6 +229,7 @@ class ProductDocumentSerializer(DocumentSerializer): fields = ( 'id', 'category', + 'preview_image_url', 'name', 'available', 'public_mark', From 4412e39e96bbc293657d81dc83f041c5ec8be54f Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 18 Nov 2019 11:19:35 +0300 Subject: [PATCH 045/115] filter by establishment for review --- apps/review/filters.py | 23 +++++++++++++++++++++++ apps/review/views/back.py | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 apps/review/filters.py diff --git a/apps/review/filters.py b/apps/review/filters.py new file mode 100644 index 00000000..4867d1df --- /dev/null +++ b/apps/review/filters.py @@ -0,0 +1,23 @@ +from django.core.validators import EMPTY_VALUES +from django_filters import rest_framework as filters + +from review import models + + +class ReviewFilter(filters.FilterSet): + """Review filter set.""" + + establishment_id = filters.NumberFilter(field_name='object_id', ) + + class Meta: + """Meta class.""" + + model = models.Review + fields = ( + 'establishment_id', + ) + + def by_establishment_id(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_establishment_id(value, content_type='establishment') + return queryset diff --git a/apps/review/views/back.py b/apps/review/views/back.py index c6ec6b67..5e38f7ab 100644 --- a/apps/review/views/back.py +++ b/apps/review/views/back.py @@ -1,7 +1,9 @@ +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, permissions from review import models from review import serializers +from review import filters from utils.permissions import IsReviewerManager, IsRestaurantReviewer @@ -10,6 +12,8 @@ class ReviewLstView(generics.ListCreateAPIView): serializer_class = serializers.ReviewBaseSerializer queryset = models.Review.objects.all() permission_classes = [permissions.IsAuthenticatedOrReadOnly, ] + filter_backends = (DjangoFilterBackend,) + filterset_class = filters.ReviewFilter class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView): From d0fec2591f8805f853f4af4bbc08efe0868a8045 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 18 Nov 2019 12:48:24 +0300 Subject: [PATCH 046/115] added bo endpoints for ads --- apps/advertisement/models.py | 7 +- apps/advertisement/serializers/__init__.py | 1 + apps/advertisement/serializers/back.py | 26 +++++++ apps/advertisement/serializers/common.py | 21 +++++- apps/advertisement/urls/back.py | 18 +++++ apps/advertisement/urls/common.py | 1 + apps/advertisement/views/__init__.py | 1 + apps/advertisement/views/back.py | 86 ++++++++++++++++++++++ apps/main/serializers.py | 4 + project/urls/back.py | 2 + 10 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 apps/advertisement/serializers/back.py create mode 100644 apps/advertisement/urls/back.py create mode 100644 apps/advertisement/views/back.py diff --git a/apps/advertisement/models.py b/apps/advertisement/models.py index 18be4516..9928f3ac 100644 --- a/apps/advertisement/models.py +++ b/apps/advertisement/models.py @@ -2,11 +2,12 @@ import uuid from django.db import models +from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from main.models import Page from translation.models import Language from utils.models import ProjectBaseMixin, ImageMixin, PlatformMixin, URLImageMixin -from main.models import Page class AdvertisementQuerySet(models.QuerySet): @@ -25,6 +26,10 @@ class AdvertisementQuerySet(models.QuerySet): """Filter by locale.""" return self.filter(target_languages__locale=locale) + def valid(self): + """Return only valid advertisements.""" + return self.filter(end__lte=timezone.now()) + class Advertisement(ProjectBaseMixin): """Advertisement model.""" diff --git a/apps/advertisement/serializers/__init__.py b/apps/advertisement/serializers/__init__.py index 393379a8..2c9dae42 100644 --- a/apps/advertisement/serializers/__init__.py +++ b/apps/advertisement/serializers/__init__.py @@ -1,3 +1,4 @@ from .common import * from .mobile import * from .web import * +from .back import * diff --git a/apps/advertisement/serializers/back.py b/apps/advertisement/serializers/back.py new file mode 100644 index 00000000..9dc8b029 --- /dev/null +++ b/apps/advertisement/serializers/back.py @@ -0,0 +1,26 @@ +"""Serializers for back office app advertisements""" +from main.serializers import PageBaseSerializer + + +class AdvertisementPageBaseSerializer(PageBaseSerializer): + """Base serializer for linking page w/ advertisement.""" + + class Meta(PageBaseSerializer.Meta): + """Meta class.""" + + PageBaseSerializer.Meta.extra_kwargs.update({ + 'advertisement': {'write_only': True}, + 'image_url': {'required': True}, + 'width': {'required': True}, + 'height': {'required': True}, + }) + + +class AdvertisementPageListCreateSerializer(AdvertisementPageBaseSerializer): + """Serializer for linking page w/ advertisement.""" + + def create(self, validated_data): + """Overridden create method.""" + + validated_data['advertisement'] = self.context.get('view').get_object() + return super().create(validated_data) diff --git a/apps/advertisement/serializers/common.py b/apps/advertisement/serializers/common.py index 8b87abad..0adb74f9 100644 --- a/apps/advertisement/serializers/common.py +++ b/apps/advertisement/serializers/common.py @@ -3,15 +3,28 @@ from rest_framework import serializers from advertisement import models from translation.serializers import LanguageSerializer -from main.serializers import SiteShortSerializer -from main.serializers import PageBaseSerializer +from main.serializers import SiteShortSerializer, PageBaseSerializer +from translation.models import Language +from main.models import SiteSettings class AdvertisementBaseSerializer(serializers.ModelSerializer): """Base serializer for model Advertisement.""" - languages = LanguageSerializer(many=True, read_only=True) + languages = LanguageSerializer(many=True, read_only=True, + source='target_languages') + target_languages = serializers.PrimaryKeyRelatedField( + queryset=Language.objects.all(), + many=True, + write_only=True + ) sites = SiteShortSerializer(many=True, read_only=True) + target_sites = serializers.PrimaryKeyRelatedField( + queryset=SiteSettings.objects.all(), + many=True, + write_only=True, + source='sites' + ) class Meta: model = models.Advertisement @@ -21,7 +34,9 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer): 'url', 'block_level', 'languages', + 'target_languages', 'sites', + 'target_sites', 'start', 'end', ] diff --git a/apps/advertisement/urls/back.py b/apps/advertisement/urls/back.py new file mode 100644 index 00000000..9868c9fd --- /dev/null +++ b/apps/advertisement/urls/back.py @@ -0,0 +1,18 @@ +"""Advertisement back office urlpaths.""" +from django.urls import path +from advertisement import views +from advertisement.urls.common import common_urlpatterns + + +app_name = 'advertisements' + +urlpatterns = [ + path('', views.AdvertisementListCreateView.as_view(), name='list-create'), + path('/', views.AdvertisementRUDView.as_view(), name='rud'), + path('/page/', views.AdvertisementPageListCreateView.as_view(), + name='page-list-create'), + path('/page//', views.AdvertisementPageRUDView.as_view(), + name='page-rud') +] + +urlpatterns += common_urlpatterns diff --git a/apps/advertisement/urls/common.py b/apps/advertisement/urls/common.py index 323a3b48..420d3d41 100644 --- a/apps/advertisement/urls/common.py +++ b/apps/advertisement/urls/common.py @@ -1,5 +1,6 @@ """Advertisement common urlpaths.""" from django.urls import path +from advertisement import views app_name = 'advertisements' diff --git a/apps/advertisement/views/__init__.py b/apps/advertisement/views/__init__.py index 393379a8..2c9dae42 100644 --- a/apps/advertisement/views/__init__.py +++ b/apps/advertisement/views/__init__.py @@ -1,3 +1,4 @@ from .common import * from .mobile import * from .web import * +from .back import * diff --git a/apps/advertisement/views/back.py b/apps/advertisement/views/back.py new file mode 100644 index 00000000..86553137 --- /dev/null +++ b/apps/advertisement/views/back.py @@ -0,0 +1,86 @@ +"""Back office views for app advertisement""" +from rest_framework import generics +from rest_framework import permissions +from django.shortcuts import get_object_or_404 + +from advertisement.models import Advertisement +from rest_framework.response import Response +from rest_framework import status +from advertisement.serializers import (AdvertisementBaseSerializer, + AdvertisementPageBaseSerializer, + AdvertisementPageListCreateSerializer) + + +class AdvertisementBackOfficeViewMixin(generics.GenericAPIView): + """Base back office advertisement view.""" + + permission_classes = (permissions.IsAuthenticated, ) + + def get_queryset(self): + """Overridden get queryset method.""" + return Advertisement.objects.with_base_related() + + +class AdvertisementListCreateView(AdvertisementBackOfficeViewMixin, generics.ListCreateAPIView): + """List|Create advertisement view.""" + + serializer_class = AdvertisementBaseSerializer + + +class AdvertisementRUDView(AdvertisementBackOfficeViewMixin, + generics.RetrieveUpdateDestroyAPIView): + """Retrieve|Update|Destroy advertisement page view.""" + + serializer_class = AdvertisementBaseSerializer + + def delete(self, request, *args, **kwargs): + instance = self.get_object() + # Delete all object that linked to this advertisement + instance.pages.all().delete() + # Delete an instance + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, + generics.ListCreateAPIView): + """Retrieve|Update|Destroy advertisement page view.""" + + serializer_class = AdvertisementPageListCreateSerializer + + def get_object(self): + """Returns the object the view is displaying.""" + ad_qs = Advertisement.objects.all() + filtered_ad_qs = self.filter_queryset(ad_qs) + + ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs['pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, ad) + + return ad + + def get_queryset(self): + """Overridden get_queryset method.""" + return self.get_object().pages.all() + + +class AdvertisementPageRUDView(AdvertisementBackOfficeViewMixin, + generics.CreateAPIView, + generics.RetrieveUpdateDestroyAPIView): + """Create|Retrieve|Update|Destroy advertisement page view.""" + + serializer_class = AdvertisementPageBaseSerializer + + def get_object(self): + """Returns the object the view is displaying.""" + ad_qs = Advertisement.objects.all() + filtered_ad_qs = self.filter_queryset(ad_qs) + + ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs['ad_pk']) + page = get_object_or_404(ad.pages.all(), pk=self.kwargs['page_pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, page) + + return page diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 0ed2f026..6b8e2ba4 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -186,7 +186,11 @@ class PageBaseSerializer(serializers.ModelSerializer): 'image_url', 'width', 'height', + 'advertisement', ] + extra_kwargs = { + 'establishment': {'write_only': True} + } class PageTypeBaseSerializer(serializers.ModelSerializer): diff --git a/project/urls/back.py b/project/urls/back.py index 8e983ebc..04af4a53 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -13,5 +13,7 @@ urlpatterns = [ path('review/', include('review.urls.back')), path('tags/', include(('tag.urls.back', 'tag'), namespace='tag')), path('products/', include(('product.urls.back', 'product'), namespace='product')), + path('re_blocks/', include(('advertisement.urls.back', 'advertisement'), + namespace='advertisement')), ] From dc0b4ce10715992ed406024c5cf5395fb9bf998b Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 18 Nov 2019 13:12:24 +0300 Subject: [PATCH 047/115] fix collection establishment in_fav --- apps/collection/views/common.py | 8 ++++---- .../migrations/0027_auto_20191118_1011.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 apps/location/migrations/0027_auto_20191118_1011.py diff --git a/apps/collection/views/common.py b/apps/collection/views/common.py index 3d06d861..5fdcf4d6 100644 --- a/apps/collection/views/common.py +++ b/apps/collection/views/common.py @@ -18,8 +18,8 @@ class CollectionViewMixin(generics.GenericAPIView): def get_queryset(self): """Override get_queryset method.""" return models.Collection.objects.published() \ - .by_country_code(code=self.request.country_code) \ - .order_by('-on_top', '-modified') + .by_country_code(code=self.request.country_code) \ + .order_by('-on_top', '-modified') class GuideViewMixin(generics.GenericAPIView): @@ -40,7 +40,7 @@ class CollectionHomePageView(CollectionListView): def get_queryset(self): """Override get_queryset.""" return super(CollectionHomePageView, self).get_queryset() \ - .filter_all_related_gt(3) + .filter_all_related_gt(3) class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView): @@ -66,7 +66,7 @@ class CollectionEstablishmentListView(CollectionListView): # May raise a permission denied self.check_object_permissions(self.request, collection) - return collection.establishments.all() + return collection.establishments.all().annotate_in_favorites(self.request.user) # Guide diff --git a/apps/location/migrations/0027_auto_20191118_1011.py b/apps/location/migrations/0027_auto_20191118_1011.py new file mode 100644 index 00000000..2937ca15 --- /dev/null +++ b/apps/location/migrations/0027_auto_20191118_1011.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.4 on 2019-11-18 10:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0026_country_is_active'), + ] + + operations = [ + migrations.AlterField( + model_name='winesubregion', + name='wine_region', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='wine_sub_region', to='location.WineRegion', verbose_name='wine sub region'), + ), + ] From 303d7f52770d1318418f53cdaed3adb9af06efd6 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 18 Nov 2019 13:51:44 +0300 Subject: [PATCH 048/115] city for favorite establishment --- apps/favorites/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/favorites/views.py b/apps/favorites/views.py index 3cf97246..444048ff 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -2,7 +2,7 @@ from rest_framework import generics from establishment.models import Establishment from establishment.filters import EstablishmentFilter -from establishment.serializers import EstablishmentBaseSerializer +from establishment.serializers import EstablishmentBaseSerializer, EstablishmentSimilarSerializer from news.filters import NewsListFilterSet from news.models import News from news.serializers import NewsBaseSerializer, NewsListSerializer @@ -23,7 +23,7 @@ class FavoritesBaseView(generics.GenericAPIView): class FavoritesEstablishmentListView(generics.ListAPIView): """List views for establishments in favorites.""" - serializer_class = EstablishmentBaseSerializer + serializer_class = EstablishmentSimilarSerializer filter_class = EstablishmentFilter def get_queryset(self): From 9212f85ee5f03a2c807c4f96b520b2347e4f0f0b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 049/115] Visible tags for establishments --- apps/establishment/models.py | 7 ++++++ apps/establishment/serializers/common.py | 2 +- .../search_indexes/documents/establishment.py | 7 ++++++ apps/search_indexes/serializers.py | 2 +- .../tag/migrations/0015_auto_20191118_1210.py | 23 +++++++++++++++++++ apps/tag/models.py | 4 +++- 6 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 apps/tag/migrations/0015_auto_20191118_1210.py diff --git a/apps/establishment/models.py b/apps/establishment/models.py index c296fe33..472c3718 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -20,6 +20,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 TagCategory from review.models import Review from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin, @@ -407,6 +408,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat def __str__(self): return f'id:{self.id}-{self.name}' + @property + def visible_tags(self): + return self.tags.exclude(category__value_type=TagCategory.BOOLEAN)\ + .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', + 'business_tag', 'business_tags_de']) + # todo: recalculate toque_number def recalculate_toque_number(self): toque_number = 0 diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index ef6eff2c..9c1aac4f 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -233,7 +233,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): address = AddressBaseSerializer() in_favorites = serializers.BooleanField(allow_null=True) - tags = TagBaseSerializer(read_only=True, many=True) + tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') currency = CurrencySerializer() type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 2d2706ff..51ce065d 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -48,6 +48,13 @@ class EstablishmentDocument(Document): properties=OBJECT_FIELD_PROPERTIES), }, multi=True) + visible_tags = fields.ObjectField( + properties={ + 'id': fields.IntegerField(attr='id'), + 'label': fields.ObjectField(attr='label_indexing', + properties=OBJECT_FIELD_PROPERTIES), + }, + multi=True) schedule = fields.ListField(fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 32b7cd82..1a3dea7a 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -181,7 +181,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer): establishment_type = EstablishmentTypeSerializer() establishment_subtypes = EstablishmentTypeSerializer(many=True) address = AddressDocumentSerializer(allow_null=True) - tags = TagsDocumentSerializer(many=True) + tags = TagsDocumentSerializer(many=True, source='visible_tags') schedule = ScheduleDocumentSerializer(many=True, allow_null=True) class Meta: diff --git a/apps/tag/migrations/0015_auto_20191118_1210.py b/apps/tag/migrations/0015_auto_20191118_1210.py new file mode 100644 index 00000000..7579059b --- /dev/null +++ b/apps/tag/migrations/0015_auto_20191118_1210.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.7 on 2019-11-18 12:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0014_tag_old_id_meta_product'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='value', + field=models.CharField(blank=True, db_index=True, default=None, max_length=255, null=True, verbose_name='indexing name'), + ), + migrations.AlterField( + model_name='tagcategory', + name='value_type', + field=models.CharField(choices=[('string', 'string'), ('list', 'list'), ('integer', 'integer'), ('float', 'float'), ('percentage', 'percentage'), ('bool', 'boolean')], default='list', max_length=255, verbose_name='value type'), + ), + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index b8abce59..a93c4a1f 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -35,7 +35,7 @@ class Tag(TranslatedFieldsMixin, models.Model): label = TJSONField(blank=True, null=True, default=None, verbose_name=_('label'), help_text='{"en-GB":"some text"}') - value = models.CharField(_('indexing name'), max_length=255, blank=True, + value = models.CharField(_('indexing name'), max_length=255, blank=True, db_index=True, null=True, default=None) category = models.ForeignKey('TagCategory', on_delete=models.CASCADE, null=True, related_name='tags', @@ -124,6 +124,7 @@ class TagCategory(TranslatedFieldsMixin, models.Model): INTEGER = 'integer' FLOAT = 'float' PERCENTAGE = 'percentage' + BOOLEAN = 'bool' VALUE_TYPE_CHOICES = ( (STRING, _('string')), @@ -131,6 +132,7 @@ class TagCategory(TranslatedFieldsMixin, models.Model): (INTEGER, _('integer')), (FLOAT, _('float')), (PERCENTAGE, _('percentage')), + (BOOLEAN, _('boolean')), ) label = TJSONField(blank=True, null=True, default=None, From 89191fcf35ef95ca4e4998a204ffb1e00a3e9253 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 050/115] use another ES server --- docker-compose.elasticsearch.yml | 13 +++++++++++++ project/settings/development.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 docker-compose.elasticsearch.yml diff --git a/docker-compose.elasticsearch.yml b/docker-compose.elasticsearch.yml new file mode 100644 index 00000000..cabe0778 --- /dev/null +++ b/docker-compose.elasticsearch.yml @@ -0,0 +1,13 @@ +version: '3.5' +services: + elasticsearch: + image: elasticsearch:7.3.1 + volumes: + - gm-esdata:/usr/share/elasticsearch/data + hostname: elasticsearch + network_mode: 'host' + environment: + - "ES_JAVA_OPTS=-Xms4g -Xmx4g" + - discovery.type=single-node + - xpack.security.enabled=false + restart: always \ No newline at end of file diff --git a/project/settings/development.py b/project/settings/development.py index 3bc258a1..669bc485 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -21,7 +21,7 @@ DOMAIN_URI = 'gm.id-east.ru' # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { - 'hosts': 'localhost:9200' + 'hosts': '188.68.209.124:9200' # 'hosts': 'elasticsearch:9200' } } From 2a680311ae66f9df8f0b589094e5801db8c56882 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 18 Nov 2019 17:37:10 +0300 Subject: [PATCH 051/115] Revert "use another ES server" This reverts commit 89191fc --- docker-compose.elasticsearch.yml | 13 ------------- project/settings/development.py | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 docker-compose.elasticsearch.yml diff --git a/docker-compose.elasticsearch.yml b/docker-compose.elasticsearch.yml deleted file mode 100644 index cabe0778..00000000 --- a/docker-compose.elasticsearch.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: '3.5' -services: - elasticsearch: - image: elasticsearch:7.3.1 - volumes: - - gm-esdata:/usr/share/elasticsearch/data - hostname: elasticsearch - network_mode: 'host' - environment: - - "ES_JAVA_OPTS=-Xms4g -Xmx4g" - - discovery.type=single-node - - xpack.security.enabled=false - restart: always \ No newline at end of file diff --git a/project/settings/development.py b/project/settings/development.py index 669bc485..3bc258a1 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -21,7 +21,7 @@ DOMAIN_URI = 'gm.id-east.ru' # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { - 'hosts': '188.68.209.124:9200' + 'hosts': 'localhost:9200' # 'hosts': 'elasticsearch:9200' } } From 0d9d45bcc58dcc9bd1f3f436df137a342ce12bd1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 18 Nov 2019 17:24:15 +0300 Subject: [PATCH 052/115] added model Product and bo endpoints --- apps/advertisement/models.py | 6 ++ apps/advertisement/urls/back.py | 4 +- apps/advertisement/views/back.py | 9 --- apps/establishment/admin.py | 13 ++++- apps/establishment/migrations/0063_company.py | 40 +++++++++++++ apps/establishment/models.py | 51 ++++++++++++++++ apps/establishment/serializers/back.py | 58 +++++++++++++------ apps/establishment/serializers/common.py | 39 +++++++++++++ apps/establishment/urls/back.py | 16 ++--- apps/establishment/views/back.py | 56 ++++++++++++++++-- .../migrations/0027_auto_20191118_1313.py | 19 ++++++ apps/main/admin.py | 5 ++ 12 files changed, 273 insertions(+), 43 deletions(-) create mode 100644 apps/establishment/migrations/0063_company.py create mode 100644 apps/location/migrations/0027_auto_20191118_1313.py diff --git a/apps/advertisement/models.py b/apps/advertisement/models.py index 9928f3ac..920e3389 100644 --- a/apps/advertisement/models.py +++ b/apps/advertisement/models.py @@ -60,6 +60,12 @@ class Advertisement(ProjectBaseMixin): def __str__(self): return str(self.url) + def delete(self, using=None, keep_parents=False): + """Overridden delete method.""" + # Delete all related pages. + self.pages.all().delete() + return super().delete(using, keep_parents) + @property def mobile_page(self): """Return mobile page""" diff --git a/apps/advertisement/urls/back.py b/apps/advertisement/urls/back.py index 9868c9fd..2502da0d 100644 --- a/apps/advertisement/urls/back.py +++ b/apps/advertisement/urls/back.py @@ -9,9 +9,9 @@ app_name = 'advertisements' urlpatterns = [ path('', views.AdvertisementListCreateView.as_view(), name='list-create'), path('/', views.AdvertisementRUDView.as_view(), name='rud'), - path('/page/', views.AdvertisementPageListCreateView.as_view(), + path('/pages/', views.AdvertisementPageListCreateView.as_view(), name='page-list-create'), - path('/page//', views.AdvertisementPageRUDView.as_view(), + path('/pages//', views.AdvertisementPageRUDView.as_view(), name='page-rud') ] diff --git a/apps/advertisement/views/back.py b/apps/advertisement/views/back.py index 86553137..d11615ba 100644 --- a/apps/advertisement/views/back.py +++ b/apps/advertisement/views/back.py @@ -33,14 +33,6 @@ class AdvertisementRUDView(AdvertisementBackOfficeViewMixin, serializer_class = AdvertisementBaseSerializer - def delete(self, request, *args, **kwargs): - instance = self.get_object() - # Delete all object that linked to this advertisement - instance.pages.all().delete() - # Delete an instance - self.perform_destroy(instance) - return Response(status=status.HTTP_204_NO_CONTENT) - class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, generics.ListCreateAPIView): @@ -66,7 +58,6 @@ class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, class AdvertisementPageRUDView(AdvertisementBackOfficeViewMixin, - generics.CreateAPIView, generics.RetrieveUpdateDestroyAPIView): """Create|Retrieve|Update|Destroy advertisement page view.""" diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index 139e12c6..e500ed6a 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -59,13 +59,18 @@ class ProductInline(admin.TabularInline): extra = 0 +class CompanyInline(admin.TabularInline): + model = models.Company + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Establishment admin.""" list_display = ['id', '__str__', 'image_tag', ] search_fields = ['id', 'name', 'index_name', 'slug'] list_filter = ['public_mark', 'toque_number'] - inlines = [GalleryImageInline, ] + inlines = [GalleryImageInline, CompanyInline] # inlines = [ # AwardInline, ContactPhoneInline, ContactEmailInline, @@ -114,3 +119,9 @@ class SocialChoiceAdmin(BaseModelAdminMixin, admin.ModelAdmin): class SocialNetworkAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Admin conf for SocialNetwork model.""" raw_id_fields = ('establishment',) + + +@admin.register(models.Company) +class CompanyAdmin(BaseModelAdminMixin, admin.ModelAdmin): + """Admin conf for Company model.""" + raw_id_fields = ['establishment', 'address', ] diff --git a/apps/establishment/migrations/0063_company.py b/apps/establishment/migrations/0063_company.py new file mode 100644 index 00000000..1f9b4aa3 --- /dev/null +++ b/apps/establishment/migrations/0063_company.py @@ -0,0 +1,40 @@ +# Generated by Django 2.2.7 on 2019-11-18 14:19 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0027_auto_20191118_1313'), + ('establishment', '0062_auto_20191117_1117'), + ] + + operations = [ + migrations.CreateModel( + name='Company', + 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='name')), + ('phones', django.contrib.postgres.fields.ArrayField(base_field=phonenumber_field.modelfields.PhoneNumberField(max_length=128), blank=True, default=None, null=True, size=None, verbose_name='contact phones')), + ('faxes', django.contrib.postgres.fields.ArrayField(base_field=phonenumber_field.modelfields.PhoneNumberField(max_length=128), blank=True, default=None, null=True, size=None, verbose_name='fax numbers')), + ('legal_entity', models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='legal entity')), + ('registry_number', models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='registry number')), + ('vat_number', models.CharField(blank=True, default=None, max_length=30, null=True, verbose_name='VAT identification number')), + ('sic_code', models.IntegerField(blank=True, default=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)], verbose_name='sic code')), + ('address', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='companies', to='location.Address', verbose_name='address')), + ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='companies', to='establishment.Establishment', verbose_name='establishment')), + ], + options={ + 'verbose_name': 'company', + 'verbose_name_plural': 'companies', + }, + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 472c3718..3bd9dd00 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -16,6 +16,8 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from timezone_field import TimeZoneField +from django.contrib.postgres.fields import ArrayField +from django.core.validators import MinValueValidator, MaxValueValidator from collection.models import Collection from location.models import Address @@ -408,6 +410,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat def __str__(self): return f'id:{self.id}-{self.name}' + def delete(self, using=None, keep_parents=False): + """Overridden delete method""" + # Delete all related companies + self.companies.all().delete() + return super().delete(using, keep_parents) + @property def visible_tags(self): return self.tags.exclude(category__value_type=TagCategory.BOOLEAN)\ @@ -872,3 +880,46 @@ class RatingStrategy(ProjectBaseMixin): return f'{self.country.code if self.country else "Other country"}. ' \ f'"{self.toque_number}": {self.public_mark_min_value}-' \ f'{self.public_mark_max_value}' + + +class CompanyQuerySet(models.QuerySet): + """QuerySet for model Company.""" + + +class Company(ProjectBaseMixin): + """Establishment company model.""" + + establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT, + related_name='companies', + verbose_name=_('establishment')) + name = models.CharField(max_length=255, verbose_name=_('name')) + phones = ArrayField(PhoneNumberField(max_length=128), + blank=True, null=True, default=None, + verbose_name=_('contact phones')) + faxes = ArrayField(PhoneNumberField(max_length=128), + blank=True, null=True, default=None, + verbose_name=_('fax numbers')) + legal_entity = models.CharField(max_length=255, + blank=True, null=True, default=None, + verbose_name=_('legal entity')) + registry_number = models.CharField(max_length=255, + blank=True, null=True, default=None, + verbose_name=_('registry number')) + vat_number = models.CharField(max_length=30, + blank=True, null=True, default=None, + verbose_name=_('VAT identification number')) + sic_code = models.IntegerField(validators=[MinValueValidator(1), + MaxValueValidator(9999)], + blank=True, null=True, default=True, + verbose_name=_('sic code')) + address = models.ForeignKey(Address, on_delete=models.PROTECT, + blank=True, null=True, default=None, + related_name='companies', + verbose_name=_('address')) + + objects = CompanyQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('company') + verbose_name_plural = _('companies') diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 0f9fe4c8..3b80da72 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,10 +1,7 @@ from rest_framework import serializers from establishment import models -from establishment.serializers import ( - EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer, - ContactPhonesSerializer, SocialNetworkRelatedSerializers, - EstablishmentTypeBaseSerializer) +from establishment import serializers as model_serializers from location.serializers import AddressDetailSerializer from main.models import Currency from utils.decorators import with_base_attributes @@ -13,17 +10,22 @@ from gallery.models import Image from django.utils.translation import gettext_lazy as _ -class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): +class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer): """Establishment create serializer""" type_id = serializers.PrimaryKeyRelatedField( source='establishment_type', - queryset=models.EstablishmentType.objects.all(), write_only=True + queryset=models.EstablishmentType.objects.all(), + write_only=True ) - phones = ContactPhonesSerializer(read_only=True, many=True, ) - emails = ContactEmailsSerializer(read_only=True, many=True, ) - socials = SocialNetworkRelatedSerializers(read_only=True, many=True, ) - type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) + phones = model_serializers.ContactPhonesSerializer(read_only=True, + many=True, ) + emails = model_serializers.ContactEmailsSerializer(read_only=True, + many=True, ) + socials = model_serializers.SocialNetworkRelatedSerializers(read_only=True, + many=True, ) + type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type', + read_only=True) tz = TimeZoneChoiceField() class Meta: @@ -52,7 +54,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): ] -class EstablishmentRUDSerializer(EstablishmentBaseSerializer): +class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): """Establishment create serializer""" type_id = serializers.PrimaryKeyRelatedField( @@ -60,10 +62,13 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer): queryset=models.EstablishmentType.objects.all(), write_only=True ) address = AddressDetailSerializer() - phones = ContactPhonesSerializer(read_only=False, many=True, ) - emails = ContactEmailsSerializer(read_only=False, many=True, ) - socials = SocialNetworkRelatedSerializers(read_only=False, many=True, ) - type = EstablishmentTypeBaseSerializer(source='establishment_type') + phones = model_serializers.ContactPhonesSerializer(read_only=False, + many=True, ) + emails = model_serializers.ContactEmailsSerializer(read_only=False, + many=True, ) + socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False, + many=True, ) + type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type') class Meta: model = models.Establishment @@ -107,7 +112,7 @@ class SocialNetworkSerializers(serializers.ModelSerializer): ] -class PlatesSerializers(PlateSerializer): +class PlatesSerializers(model_serializers.PlateSerializer): """Plates serializers.""" currency_id = serializers.PrimaryKeyRelatedField( @@ -119,14 +124,14 @@ class PlatesSerializers(PlateSerializer): """Meta class.""" model = models.Plate - fields = PlateSerializer.Meta.fields + [ + fields = model_serializers.PlateSerializer.Meta.fields + [ 'name', 'currency_id', 'menu' ] -class ContactPhoneBackSerializers(PlateSerializer): +class ContactPhoneBackSerializers(model_serializers.PlateSerializer): """ContactPhone serializers.""" class Meta: @@ -138,7 +143,7 @@ class ContactPhoneBackSerializers(PlateSerializer): ] -class ContactEmailBackSerializers(PlateSerializer): +class ContactEmailBackSerializers(model_serializers.PlateSerializer): """ContactEmail serializers.""" class Meta: @@ -204,3 +209,18 @@ class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer): attrs['image'] = image return attrs + + +class EstablishmentCompanyListCreateSerializer(model_serializers.CompanyBaseSerializer): + """Serializer for linking page w/ advertisement.""" + + class Meta(model_serializers.CompanyBaseSerializer.Meta): + """Meta class.""" + model_serializers.CompanyBaseSerializer.Meta.extra_kwargs.update({ + 'establishment': {'required': False} + }) + + def create(self, validated_data): + """Overridden create method.""" + validated_data['establishment'] = self.context.get('view').get_object() + return super().create(validated_data) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 9c1aac4f..e4cb389a 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -2,6 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from phonenumber_field.modelfields import PhoneNumberField from comment import models as comment_models from comment.serializers import common as comment_serializers from establishment import models @@ -418,3 +419,41 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): '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) + fax_list = serializers.SerializerMethodField(source='faxes', read_only=True) + address_detail = AddressDetailSerializer(source='address', read_only=True) + + class Meta: + """Meta class.""" + model = models.Company + fields = [ + 'id', + 'establishment', + 'name', + 'phones', + 'faxes', + 'legal_entity', + 'registry_number', + 'vat_number', + 'sic_code', + 'address', + 'phone_list', + 'fax_list', + 'address_detail', + ] + extra_kwargs = { + 'establishment': {'write_only': True}, + 'phones': {'write_only': True}, + 'faxes': {'write_only': True}, + 'address': {'write_only': True} + } + + def get_phone_list(self, instance): + return instance.phones + + def get_fax_list(self, instance): + return instance.faxes diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index c8792b5b..47ac2357 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -13,6 +13,15 @@ urlpatterns = [ name='schedule-rud'), path('/schedule/', views.EstablishmentScheduleCreateView.as_view(), name='schedule-create'), + path('/gallery/', views.EstablishmentGalleryListView.as_view(), + name='gallery-list'), + path('/gallery//', + views.EstablishmentGalleryCreateDestroyView.as_view(), + name='gallery-create-destroy'), + path('/companies/', views.EstablishmentCompanyListCreateView.as_view(), + name='company-list-create'), + path('/companies//', views.EstablishmentCompanyRUDView.as_view(), + name='company-rud'), path('menus/', views.MenuListCreateView.as_view(), name='menu-list'), path('menus//', views.MenuRUDView.as_view(), name='menu-rud'), path('plates/', views.PlateListCreateView.as_view(), name='plates'), @@ -31,11 +40,4 @@ 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'), - - # gallery - path('/gallery/', views.EstablishmentBackOfficeGalleryListView.as_view(), - name='gallery-list'), - path('/gallery//', - views.EstablishmentBackOfficeGalleryCreateDestroyView.as_view(), - name='gallery-create-destroy'), ] diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 5d337b73..24b47720 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -6,6 +6,8 @@ from establishment import filters, models, serializers from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from utils.permissions import IsCountryAdmin, IsEstablishmentManager from utils.views import CreateDestroyGalleryViewMixin +from rest_framework import status +from rest_framework.response import Response class EstablishmentMixinViews: @@ -187,8 +189,8 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView): queryset = models.EstablishmentSubType.objects.all() -class EstablishmentBackOfficeGalleryCreateDestroyView(EstablishmentMixinViews, - CreateDestroyGalleryViewMixin): +class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, + CreateDestroyGalleryViewMixin): """Resource for a create|destroy gallery for product for back-office users.""" serializer_class = serializers.EstablishmentBackOfficeGallerySerializer @@ -207,14 +209,14 @@ class EstablishmentBackOfficeGalleryCreateDestroyView(EstablishmentMixinViews, return gallery -class EstablishmentBackOfficeGalleryListView(EstablishmentMixinViews, - generics.ListAPIView): +class EstablishmentGalleryListView(EstablishmentMixinViews, + generics.ListAPIView): """Resource for returning gallery for establishment for back-office users.""" serializer_class = serializers.ImageBaseSerializer def get_object(self): """Override get_object method.""" - qs = super(EstablishmentBackOfficeGalleryListView, self).get_queryset() + qs = super(EstablishmentGalleryListView, self).get_queryset() establishment = get_object_or_404(qs, pk=self.kwargs['pk']) # May raise a permission denied @@ -225,3 +227,47 @@ class EstablishmentBackOfficeGalleryListView(EstablishmentMixinViews, def get_queryset(self): """Override get_queryset method.""" return self.get_object().crop_gallery + + +class EstablishmentCompanyListCreateView(EstablishmentMixinViews, + generics.ListCreateAPIView): + """List|Create establishment company view.""" + + serializer_class = serializers.EstablishmentCompanyListCreateSerializer + + def get_object(self): + """Returns the object the view is displaying.""" + 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['pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, establishment) + + return establishment + + def get_queryset(self): + """Overridden get_queryset method.""" + return self.get_object().companies.all() + + +class EstablishmentCompanyRUDView(EstablishmentMixinViews, + generics.RetrieveUpdateDestroyAPIView): + """Create|Retrieve|Update|Destroy establishment company view.""" + + serializer_class = serializers.CompanyBaseSerializer + + def get_object(self): + """Returns the object the view is displaying.""" + 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['pk']) + company = get_object_or_404(establishment.companies.all(), pk=self.kwargs['company_pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, company) + + return company + diff --git a/apps/location/migrations/0027_auto_20191118_1313.py b/apps/location/migrations/0027_auto_20191118_1313.py new file mode 100644 index 00000000..8a8beb0c --- /dev/null +++ b/apps/location/migrations/0027_auto_20191118_1313.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-18 13:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0026_country_is_active'), + ] + + operations = [ + migrations.AlterField( + model_name='winesubregion', + name='wine_region', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='wine_sub_region', to='location.WineRegion', verbose_name='wine sub region'), + ), + ] diff --git a/apps/main/admin.py b/apps/main/admin.py index 057515e8..9ec76164 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -40,3 +40,8 @@ class CarouselAdmin(admin.ModelAdmin): @admin.register(models.PageType) class PageTypeAdmin(admin.ModelAdmin): """PageType admin.""" + + +@admin.register(models.Page) +class PageAdmin(admin.ModelAdmin): + """Page admin.""" From 012d2dcb23b4022dc592afa00da2a98531e432bd Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 18 Nov 2019 17:57:28 +0300 Subject: [PATCH 053/115] added validation to Company serializer --- apps/establishment/serializers/common.py | 25 ++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index e4cb389a..9acc2f9e 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -1,20 +1,21 @@ """Establishment serializers.""" from django.utils.translation import ugettext_lazy as _ +from phonenumber_field.phonenumber import to_python as str_to_phonenumber from rest_framework import serializers -from phonenumber_field.modelfields import PhoneNumberField from comment import models as comment_models from comment.serializers import common as comment_serializers from establishment import models -from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, CityShortSerializer +from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \ + CityShortSerializer from main.serializers import AwardSerializer, CurrencySerializer +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 (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) -from review.serializers import ReviewShortSerializer -from utils.serializers import ImageBaseSerializer class ContactPhonesSerializer(serializers.ModelSerializer): @@ -453,7 +454,23 @@ class CompanyBaseSerializer(serializers.ModelSerializer): } def get_phone_list(self, instance): + """Return list of phone numbers.""" return instance.phones def get_fax_list(self, instance): + """Return list of fax numbers.""" return instance.faxes + + def validate(self, attrs): + """Overridden validate method""" + phones = [str_to_phonenumber(phone).as_national for phone in attrs.get('phones')] + faxes = [str_to_phonenumber(fax).as_national for fax in attrs.get('faxes')] + + if faxes: + if models.Company.objects.filter(faxes__overlap=faxes).exists(): + raise serializers.ValidationError({'detail': _('Fax is already reserved.')}) + + if phones: + if models.Company.objects.filter(phones__overlap=phones).exists(): + raise serializers.ValidationError({'detail': _('Phones is already reserved.')}) + return attrs From 3536be839479791b251bccada51ed884c80aeff9 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 18 Nov 2019 18:01:18 +0300 Subject: [PATCH 054/115] ES as separate server --- docker-compose.elasticsearch.yml | 21 +++++++++++++++++++++ project/settings/development.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 docker-compose.elasticsearch.yml diff --git a/docker-compose.elasticsearch.yml b/docker-compose.elasticsearch.yml new file mode 100644 index 00000000..3b412ab1 --- /dev/null +++ b/docker-compose.elasticsearch.yml @@ -0,0 +1,21 @@ +version: '3.5' +services: + elasticsearch: + image: elasticsearch:7.3.1 + volumes: + - gm-esdata:/usr/share/elasticsearch/data + hostname: elasticsearch + network_mode: 'host' + environment: + - "ES_JAVA_OPTS=-Xms4g -Xmx4g" + - discovery.type=single-node + - xpack.security.enabled=false + restart: always + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + mem_limit: 4g \ No newline at end of file diff --git a/project/settings/development.py b/project/settings/development.py index 3bc258a1..669bc485 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -21,7 +21,7 @@ DOMAIN_URI = 'gm.id-east.ru' # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { - 'hosts': 'localhost:9200' + 'hosts': '188.68.209.124:9200' # 'hosts': 'elasticsearch:9200' } } From f81e879ae45b80b73d3e47856a7a068c83b26dd2 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 18 Nov 2019 18:03:21 +0300 Subject: [PATCH 055/115] Revert "ES as separate server" This reverts commit 3536be8 --- docker-compose.elasticsearch.yml | 21 --------------------- project/settings/development.py | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 docker-compose.elasticsearch.yml diff --git a/docker-compose.elasticsearch.yml b/docker-compose.elasticsearch.yml deleted file mode 100644 index 3b412ab1..00000000 --- a/docker-compose.elasticsearch.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: '3.5' -services: - elasticsearch: - image: elasticsearch:7.3.1 - volumes: - - gm-esdata:/usr/share/elasticsearch/data - hostname: elasticsearch - network_mode: 'host' - environment: - - "ES_JAVA_OPTS=-Xms4g -Xmx4g" - - discovery.type=single-node - - xpack.security.enabled=false - restart: always - ulimits: - memlock: - soft: -1 - hard: -1 - nofile: - soft: 65536 - hard: 65536 - mem_limit: 4g \ No newline at end of file diff --git a/project/settings/development.py b/project/settings/development.py index 669bc485..3bc258a1 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -21,7 +21,7 @@ DOMAIN_URI = 'gm.id-east.ru' # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { - 'hosts': '188.68.209.124:9200' + 'hosts': 'localhost:9200' # 'hosts': 'elasticsearch:9200' } } From c7033d4b103c0c3f530998d889a179a7348edfef Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 18 Nov 2019 18:09:19 +0300 Subject: [PATCH 056/115] Revert "Revert "ES as separate server"" This reverts commit f81e879 --- docker-compose.elasticsearch.yml | 21 +++++++++++++++++++++ project/settings/development.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 docker-compose.elasticsearch.yml diff --git a/docker-compose.elasticsearch.yml b/docker-compose.elasticsearch.yml new file mode 100644 index 00000000..3b412ab1 --- /dev/null +++ b/docker-compose.elasticsearch.yml @@ -0,0 +1,21 @@ +version: '3.5' +services: + elasticsearch: + image: elasticsearch:7.3.1 + volumes: + - gm-esdata:/usr/share/elasticsearch/data + hostname: elasticsearch + network_mode: 'host' + environment: + - "ES_JAVA_OPTS=-Xms4g -Xmx4g" + - discovery.type=single-node + - xpack.security.enabled=false + restart: always + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + mem_limit: 4g \ No newline at end of file diff --git a/project/settings/development.py b/project/settings/development.py index 3bc258a1..669bc485 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -21,7 +21,7 @@ DOMAIN_URI = 'gm.id-east.ru' # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { - 'hosts': 'localhost:9200' + 'hosts': '188.68.209.124:9200' # 'hosts': 'elasticsearch:9200' } } From 5c4c4c60c3841cbeb14716b0483ced9f0cc76155 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 18 Nov 2019 20:12:58 +0300 Subject: [PATCH 057/115] ES as separate server --- apps/establishment/models.py | 9 ++++----- apps/news/models.py | 4 ++-- apps/news/serializers.py | 2 +- apps/product/models.py | 8 ++++---- apps/search_indexes/documents/news.py | 7 +++++++ apps/search_indexes/documents/product.py | 8 ++++++++ apps/search_indexes/serializers.py | 4 ++-- apps/utils/models.py | 13 +++++++++++++ 8 files changed, 41 insertions(+), 14 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 3bd9dd00..7fc0b2d6 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -22,11 +22,10 @@ from django.core.validators import MinValueValidator, MaxValueValidator from collection.models import Collection from location.models import Address from main.models import Award, Currency -from tag.models import TagCategory from review.models import Review from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin, - IntermediateGalleryModelMixin) + IntermediateGalleryModelMixin, HasTagsMixin) # todo: establishment type&subtypes check @@ -320,7 +319,7 @@ class EstablishmentQuerySet(models.QuerySet): return self.exclude(address__city__country__in=countries) -class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): +class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin, HasTagsMixin): """Establishment model.""" # todo: delete image URL fields after moving on gallery @@ -418,9 +417,9 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat @property def visible_tags(self): - return self.tags.exclude(category__value_type=TagCategory.BOOLEAN)\ + return super().visible_tags\ .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', - 'business_tag', 'business_tags_de']) + 'business_tag', 'business_tags_de'])\ # todo: recalculate toque_number def recalculate_toque_number(self): diff --git a/apps/news/models.py b/apps/news/models.py index 7e26de5f..d0e79c64 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse from rating.models import Rating, ViewCount -from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, +from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin, ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin) from utils.querysets import TranslationQuerysetMixin from django.conf import settings @@ -126,7 +126,7 @@ class NewsQuerySet(TranslationQuerysetMixin): ) -class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin): +class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin): """News model.""" STR_FIELD_NAME = 'title' diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 3fca14b8..f0390ea5 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -64,7 +64,7 @@ class NewsBaseSerializer(ProjectModelSerializer): title_translated = TranslatedField() subtitle_translated = TranslatedField() news_type = NewsTypeSerializer(read_only=True) - tags = TagBaseSerializer(read_only=True, many=True) + tags = TagBaseSerializer(read_only=True, many=True, source='related_tags') in_favorites = serializers.BooleanField(allow_null=True) view_counter = serializers.IntegerField(read_only=True) diff --git a/apps/product/models.py b/apps/product/models.py index 4a92a5fa..126f59e9 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -7,7 +7,7 @@ from django.db.models import Case, When from django.utils.translation import gettext_lazy as _ from django.core.validators import MaxValueValidator, MinValueValidator -from utils.models import (BaseAttributes, ProjectBaseMixin, +from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin, TranslatedFieldsMixin, TJSONField, GalleryModelMixin, IntermediateGalleryModelMixin) @@ -131,7 +131,7 @@ class ProductQuerySet(models.QuerySet): ) -class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes): +class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsMixin): """Product models.""" EARLIEST_VINTAGE_YEAR = 1700 @@ -256,8 +256,8 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes): @property def related_tags(self): - return self.tags.exclude(category__index_name__in=['sugar-content', 'wine-color', 'bottles-produced', - 'serial-number', 'grape-variety']).prefetch_related('category') + return super().visible_tags.exclude(category__index_name__in=['sugar-content', 'wine-color', + 'bottles-produced','serial-number', 'grape-variety']) @property def display_name(self): diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index ff659416..e39036d3 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -34,6 +34,13 @@ class NewsDocument(Document): 'value': fields.KeywordField() }, multi=True) + visible_tags = fields.ObjectField( + properties={ + 'id': fields.IntegerField(attr='id'), + 'label': fields.ObjectField(attr='label_indexing', + properties=OBJECT_FIELD_PROPERTIES), + }, + multi=True) class Django: diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index 874be5a6..4ac42a56 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -107,6 +107,14 @@ class ProductDocument(Document): }, multi=True ) + related_tags = fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True + ) 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') diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 1a3dea7a..774c4b1b 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -148,7 +148,7 @@ class NewsDocumentSerializer(DocumentSerializer): title_translated = serializers.SerializerMethodField(allow_null=True) subtitle_translated = serializers.SerializerMethodField(allow_null=True) news_type = NewsTypeSerializer() - tags = TagsDocumentSerializer(many=True) + tags = TagsDocumentSerializer(many=True, source='visible_tags') class Meta: """Meta class.""" @@ -214,7 +214,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer): class ProductDocumentSerializer(DocumentSerializer): """Product document serializer""" - tags = TagsDocumentSerializer(many=True) + 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) diff --git a/apps/utils/models.py b/apps/utils/models.py index a325c8ff..e508be6f 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -418,4 +418,17 @@ class IntermediateGalleryModelMixin(models.Model): return self.image.title if self.image.title else self.id +class HasTagsMixin(models.Model): + """Mixin for filtering tags""" + + @property + def visible_tags(self): + return self.tags.filter(category__public=True).prefetch_related('category')\ + .exclude(category__value_type='bool') + + class Meta: + """Meta class.""" + abstract = True + + timezone.datetime.now().date().isoformat() \ No newline at end of file From d080f689cb39c7004233e80d7d684dc79a83895e Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 18 Nov 2019 22:23:23 +0300 Subject: [PATCH 058/115] Change Pagination for collection --- apps/collection/views/common.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/collection/views/common.py b/apps/collection/views/common.py index 5fdcf4d6..8ea20d8d 100644 --- a/apps/collection/views/common.py +++ b/apps/collection/views/common.py @@ -1,11 +1,10 @@ from django.shortcuts import get_object_or_404 -from rest_framework import generics -from rest_framework import permissions +from rest_framework import generics, permissions, pagination from collection import models from collection.serializers import common as serializers from establishment.serializers import EstablishmentSimilarSerializer -from utils.pagination import ProjectPageNumberPagination +from utils.pagination import ProjectPageNumberPagination, ProjectMobilePagination # Mixins @@ -52,7 +51,7 @@ class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView): class CollectionEstablishmentListView(CollectionListView): """Retrieve list of establishment for collection.""" lookup_field = 'slug' - pagination_class = ProjectPageNumberPagination + pagination_class = ProjectMobilePagination serializer_class = EstablishmentSimilarSerializer def get_queryset(self): From f6e5f8488955613db3b0e24fc5d320cd5dee5c67 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 18 Nov 2019 22:41:14 +0300 Subject: [PATCH 059/115] fix issue w/ empty tags --- 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 f0390ea5..4eaeaeb4 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -64,7 +64,7 @@ class NewsBaseSerializer(ProjectModelSerializer): title_translated = TranslatedField() subtitle_translated = TranslatedField() news_type = NewsTypeSerializer(read_only=True) - tags = TagBaseSerializer(read_only=True, many=True, source='related_tags') + tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') in_favorites = serializers.BooleanField(allow_null=True) view_counter = serializers.IntegerField(read_only=True) From 267544a0b1941c6ba422054e62acc52a40f00b7a Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 19 Nov 2019 09:48:25 +0300 Subject: [PATCH 060/115] merge for migration --- .../migrations/0028_merge_20191119_0647.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/location/migrations/0028_merge_20191119_0647.py diff --git a/apps/location/migrations/0028_merge_20191119_0647.py b/apps/location/migrations/0028_merge_20191119_0647.py new file mode 100644 index 00000000..40d79f13 --- /dev/null +++ b/apps/location/migrations/0028_merge_20191119_0647.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.7 on 2019-11-19 06:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0027_auto_20191118_1313'), + ('location', '0027_auto_20191118_1011'), + ] + + operations = [ + ] From c9a0fb6656435d338b457e6a55682ab3c7e85a26 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, 19 Nov 2019 12:17:05 +0300 Subject: [PATCH 061/115] Fix category, poduct-tags --- .../management/commands/add_product_tag.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index ee4829f5..fb4542cc 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -26,15 +26,17 @@ class Command(BaseCommand): 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 - ) + 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 + 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.')) @@ -64,22 +66,24 @@ class Command(BaseCommand): if not tags.exists(): objects.append(Tag(label={"en-GB": t.tag_value}, category=category, - value=t.tag_value, - old_id_meta_product=t.old_id - )) - else: - qs = tags.filter(old_id_meta_product__isnull=True)\ - .update(old_id_meta_product=t.old_id) + value=t.tag_value) + ) + Tag.objects.bulk_create(objects) self.stdout.write(self.style.WARNING(f'Add or get tag objects.')) + def remove_tags_product(self): + print('Begin clear tags product') + products = Product.objects.all() + products.tags.clear() + print('End clear tags product') + def product_sql(self): with connections['legacy'].cursor() as cursor: cursor.execute(''' select - DISTINCT - m.id as old_id_tag, + DISTINCT m.product_id, lower(trim(CONVERT(m.value USING utf8))) as tag_value, trim(CONVERT(v.key_name USING utf8)) as tag_category @@ -90,7 +94,12 @@ class Command(BaseCommand): def add_product_tag(self): for t in tqdm(self.product_sql(), desc='Add product tag'): - tags = Tag.objects.filter(old_id_meta_product=t.old_id_tag) + category = TagCategory.objects.get(index_name=t.tag_category) + + tags = Tag.objects.filter( + category=category, + value=t.tag_value + ) product = Product.objects.get(old_id=t.product_id) for tag in tags: if product not in tag.products.all(): @@ -111,6 +120,7 @@ class Command(BaseCommand): tag.save() def handle(self, *args, **kwargs): + self.remove_tags_product() self.add_category_tag() self.add_tag() self.check_tag() From dbe39ae5cb48420e5383132d385f968057344239 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 19 Nov 2019 14:05:49 +0300 Subject: [PATCH 062/115] Search establishment by region id (cherry picked from commit 7a5774a) --- .../search_indexes/documents/establishment.py | 21 +++++++++++++++++++ apps/search_indexes/views.py | 14 +++++++++++++ 2 files changed, 35 insertions(+) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 51ce065d..8b4e5c3c 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -55,6 +55,27 @@ class EstablishmentDocument(Document): properties=OBJECT_FIELD_PROPERTIES), }, 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_sub_region': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + }), + }, + multi=True + ) schedule = fields.ListField(fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index ad7bf74c..6c8d3cc4 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -124,6 +124,20 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): constants.LOOKUP_QUERY_IN, ] }, + 'wine_region_id': { + 'field': 'products.wine_region.id', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE, + ], + }, + 'wine_sub_region_id': { + 'field': 'products.wine_sub_region_id', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE, + ], + }, 'country_id': { 'field': 'address.city.country.id' }, From 39a406cbc178559369b114bcd7ac561574cdb4ad 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, 19 Nov 2019 15:04:12 +0300 Subject: [PATCH 063/115] Product type tags --- .../management/commands/add_product_tag.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index fb4542cc..74427110 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand from django.db import connections from establishment.management.commands.add_position import namedtuplefetchall from tag.models import Tag, TagCategory -from product.models import Product +from product.models import Product, ProductType from tqdm import tqdm @@ -40,6 +40,29 @@ class Command(BaseCommand): TagCategory.objects.bulk_create(objects) self.stdout.write(self.style.WARNING(f'Add or get tag category objects.')) + + def product_type_category_sql(self): + with connections['legacy'].cursor() as cursor: + cursor.execute(''' + select + DISTINCT + trim(CONVERT(v.key_name USING utf8)) as tag_category + FROM product_metadata m + join product_key_value_metadata v on v.id = m.product_key_value_metadatum_id + join products p on p.id = m.product_id + where UPPER(trim(p.type)) = 'WINE' + ''') + return namedtuplefetchall(cursor) + + def add_type_product_category(self): + for c in tqdm(self.product_type_category_sql(), desc='Add type product category'): + types = ProductType.objects.filter(index_name='wine') + category = TagCategory.objects.get(index_name=c.tag_category) + if types.exists() and category not in types.tag_categories.all(): + types.tag_categories.add(category) + + self.stdout.write(self.style.WARNING(f'Add type product category objects.')) + def tag_sql(self): with connections['legacy'].cursor() as cursor: cursor.execute(''' @@ -122,6 +145,7 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): self.remove_tags_product() self.add_category_tag() + self.add_type_product_category() self.add_tag() self.check_tag() self.add_product_tag() From 7d8e4c96f1c70839ce26cc72cf03fabe41c34362 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Tue, 19 Nov 2019 12:13:43 +0000 Subject: [PATCH 064/115] Added filter by type for bo news list --- apps/news/filters.py | 7 +++++++ apps/news/views.py | 1 + 2 files changed, 8 insertions(+) diff --git a/apps/news/filters.py b/apps/news/filters.py index 2621f3bf..6ade7eeb 100644 --- a/apps/news/filters.py +++ b/apps/news/filters.py @@ -18,6 +18,7 @@ class NewsListFilterSet(filters.FilterSet): ) tag_value__exclude = filters.CharFilter(method='exclude_tags') tag_value__in = filters.CharFilter(method='in_tags') + type = filters.CharFilter(method='by_type') class Meta: """Meta class""" @@ -51,3 +52,9 @@ class NewsListFilterSet(filters.FilterSet): return queryset.filter(**filters) else: return queryset + + def by_type(self, queryset, name, value): + if value: + return queryset.filter(news_type__name=value) + else: + return queryset diff --git a/apps/news/views.py b/apps/news/views.py index 7c7a58c6..3e841246 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -82,6 +82,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView, """Resource for a list of news for back-office users.""" serializer_class = serializers.NewsBackOfficeBaseSerializer + filter_class = filters.NewsListFilterSet create_serializers_class = serializers.NewsBackOfficeDetailSerializer permission_classes = [IsCountryAdmin | IsContentPageManager] From fb0157042a27d44bd330a34542779c80be6511e0 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, 19 Nov 2019 15:45:49 +0300 Subject: [PATCH 065/115] Fix --- apps/product/management/commands/add_product_tag.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index 74427110..33c98b6b 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -99,7 +99,8 @@ class Command(BaseCommand): def remove_tags_product(self): print('Begin clear tags product') products = Product.objects.all() - products.tags.clear() + for p in tqdm(products, desc='Clear tags product'): + p.tags.clear() print('End clear tags product') def product_sql(self): From 68baebcbcc4f91bd9b5e25ef495b9f05d9a75d86 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, 19 Nov 2019 16:39:04 +0300 Subject: [PATCH 066/115] Fix fix --- apps/product/management/commands/add_product_tag.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index 33c98b6b..589c7a83 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -56,10 +56,10 @@ class Command(BaseCommand): def add_type_product_category(self): for c in tqdm(self.product_type_category_sql(), desc='Add type product category'): - types = ProductType.objects.filter(index_name='wine') + type = ProductType.objects.get(index_name='wine') category = TagCategory.objects.get(index_name=c.tag_category) - if types.exists() and category not in types.tag_categories.all(): - types.tag_categories.add(category) + if type and category not in type.tag_categories.all(): + type.tag_categories.add(category) self.stdout.write(self.style.WARNING(f'Add type product category objects.')) From 8b194f83246bdc967e6360bab528b7ea700fdb6b Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 19 Nov 2019 16:40:03 +0300 Subject: [PATCH 067/115] fix transfer review text --- apps/review/transfer_data.py | 18 ++++++++++++------ apps/transfer/serializers/reviews.py | 6 +++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/review/transfer_data.py b/apps/review/transfer_data.py index 9ccb6c80..ea5fba88 100644 --- a/apps/review/transfer_data.py +++ b/apps/review/transfer_data.py @@ -2,10 +2,10 @@ from pprint import pprint from django.db.models import Q -from product.models import Product from account.models import User from account.transfer_data import STOP_LIST from establishment.models import Establishment +from product.models import Product from review.models import Inquiries as NewInquiries, Review from transfer.models import Reviews, ReviewTexts, Inquiries, GridItems, InquiryPhotos from transfer.serializers.grid import GridItemsSerializer @@ -38,7 +38,7 @@ def transfer_reviews(): establishment_id__in=list(establishments), ).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'establishment_id', 'mark', 'vintage') - serialized_data = ReviewSerializer(data=list(queryset.values()), many=True) + serialized_data = ReviewSerializer(data=list(queryset), many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -48,17 +48,23 @@ def transfer_reviews(): def transfer_text_review(): reviews = Review.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) queryset = ReviewTexts.objects.filter( - review_id__in=list(reviews), + review_id__in=list(reviews) ).exclude( - Q(text__isnull=True) | Q(text='') + text__iregex=r'[\s[ \t\n\r\f]*]+' + ).exclude( + text__isnull=True + ).exclude( + text__iexact='' ).values('review_id', 'locale', 'text') - serialized_data = ReviewTextSerializer(data=list(queryset.values()), many=True) + serialized_data = ReviewTextSerializer(data=list(queryset)[20000:30000], many=True) if serialized_data.is_valid(): serialized_data.save() else: pprint(f"ReviewTextSerializer serializer errors: {serialized_data.errors}") + +def make_en_text_review(): for review in Review.objects.filter(old_id__isnull=False): text = review.text if text and 'en-GB' not in text: @@ -106,7 +112,6 @@ def transfer_inquiry_photos(): def transfer_product_reviews(): - products = Product.objects.filter( old_id__isnull=False).values_list('old_id', flat=True) @@ -130,6 +135,7 @@ data_types = { # transfer_languages, transfer_reviews, transfer_text_review, + make_en_text_review, ], 'inquiries': [ transfer_inquiries, diff --git a/apps/transfer/serializers/reviews.py b/apps/transfer/serializers/reviews.py index a6cb1124..25649b12 100644 --- a/apps/transfer/serializers/reviews.py +++ b/apps/transfer/serializers/reviews.py @@ -103,6 +103,9 @@ class ReviewTextSerializer(serializers.Serializer): 'new_text': self.get_text(data), 'review': self.get_review(data), }) + data.pop('review_id') + data.pop('locale') + data.pop('text') return data def create(self, validated_data): @@ -117,7 +120,8 @@ class ReviewTextSerializer(serializers.Serializer): @staticmethod def get_text(data): locale = data['locale'] or 'en-GB' - return {locale: data['text']} + text = data['text'] + return {locale: text} @staticmethod def get_review(data): From d03a151bf81092b082551a9780f8bf2233159c06 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 19 Nov 2019 16:58:12 +0300 Subject: [PATCH 068/115] fix slice review --- apps/review/transfer_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/review/transfer_data.py b/apps/review/transfer_data.py index ea5fba88..6eabca79 100644 --- a/apps/review/transfer_data.py +++ b/apps/review/transfer_data.py @@ -57,7 +57,7 @@ def transfer_text_review(): text__iexact='' ).values('review_id', 'locale', 'text') - serialized_data = ReviewTextSerializer(data=list(queryset)[20000:30000], many=True) + serialized_data = ReviewTextSerializer(data=list(queryset), many=True) if serialized_data.is_valid(): serialized_data.save() else: From 484a9a325067b6e82641d63c4f2f0104b92a6cf4 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 19 Nov 2019 17:37:11 +0300 Subject: [PATCH 069/115] added field - average price to model Product and transfer command --- apps/establishment/serializers/common.py | 4 ++++ .../management/commands/add_average_price.py | 19 +++++++++++++++++++ .../migrations/0016_product_average_price.py | 18 ++++++++++++++++++ apps/product/models.py | 3 +++ apps/product/serializers/common.py | 2 +- apps/search_indexes/documents/product.py | 1 + apps/search_indexes/serializers.py | 1 + apps/transfer/serializers/product.py | 4 ++++ 8 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 apps/product/management/commands/add_average_price.py create mode 100644 apps/product/migrations/0016_product_average_price.py diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 9acc2f9e..0c183477 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -181,6 +181,7 @@ class EstablishmentShortSerializer(serializers.ModelSerializer): city = CitySerializer(source='address.city', allow_null=True) establishment_type = EstablishmentTypeGeoSerializer() establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) + currency = CurrencySerializer(read_only=True) class Meta: """Meta class.""" @@ -193,6 +194,7 @@ class EstablishmentShortSerializer(serializers.ModelSerializer): 'city', 'establishment_type', 'establishment_subtypes', + 'currency', ] @@ -202,6 +204,7 @@ class EstablishmentProductShortSerializer(serializers.ModelSerializer): establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) address = AddressBaseSerializer() city = CityShortSerializer(source='address.city', allow_null=True) + currency_detail = CurrencySerializer(source='currency', read_only=True) class Meta: """Meta class.""" @@ -215,6 +218,7 @@ class EstablishmentProductShortSerializer(serializers.ModelSerializer): 'establishment_type', 'establishment_subtypes', 'address', + 'currency_detail', ] diff --git a/apps/product/management/commands/add_average_price.py b/apps/product/management/commands/add_average_price.py new file mode 100644 index 00000000..38024d81 --- /dev/null +++ b/apps/product/management/commands/add_average_price.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from product.models import Product +from transfer.models import Products + + +class Command(BaseCommand): + help = """Add average price to product from legacy table products.""" + + def handle(self, *args, **kwarg): + update_products = [] + old_products = Products.objects.values_list('id', 'price') + for old_id, price in old_products: + product = Product.objects.get(old_id=old_id) + product.average_price = price + update_products.append(product) + + Product.objects.bulk_update(update_products, ['average_price', ]) + self.stdout.write(self.style.WARNING(f'Updated products: {len(update_products)}')) diff --git a/apps/product/migrations/0016_product_average_price.py b/apps/product/migrations/0016_product_average_price.py new file mode 100644 index 00000000..983e8072 --- /dev/null +++ b/apps/product/migrations/0016_product_average_price.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-19 13:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0015_auto_20191117_1954'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='average_price', + field=models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=14, null=True, verbose_name='average price'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 126f59e9..aec37ae7 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -211,6 +211,9 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsM null=True, blank=True, default=None, validators=[MinValueValidator(EARLIEST_VINTAGE_YEAR), MaxValueValidator(LATEST_VINTAGE_YEAR)]) + average_price = models.DecimalField(max_digits=14, decimal_places=2, + blank=True, null=True, default=None, + verbose_name=_('average price')) gallery = models.ManyToManyField('gallery.Image', through='ProductGallery') reviews = generic.GenericRelation(to='review.Review') comments = generic.GenericRelation(to='comment.Comment') diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 6fe2d0b3..a0a56337 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -130,7 +130,6 @@ class ProductDetailSerializer(ProductBaseSerializer): grape_variety = TagBaseSerializer(many=True, read_only=True) image_url = serializers.URLField(allow_null=True, read_only=True) - new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) class Meta(ProductBaseSerializer.Meta): @@ -146,6 +145,7 @@ class ProductDetailSerializer(ProductBaseSerializer): 'image_url', 'new_image', 'grape_variety', + 'average_price', ] diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index 4ac42a56..35b1956a 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -131,6 +131,7 @@ class ProductDocument(Document): 'state', 'old_unique_key', 'vintage', + 'average_price', ) related_models = [models.ProductType] diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 774c4b1b..a8391ecc 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -245,4 +245,5 @@ class ProductDocumentSerializer(DocumentSerializer): 'wine_colors', 'grape_variety', 'establishment_detail', + 'average_price', ) diff --git a/apps/transfer/serializers/product.py b/apps/transfer/serializers/product.py index f0871c41..a0f3ef8a 100644 --- a/apps/transfer/serializers/product.py +++ b/apps/transfer/serializers/product.py @@ -265,6 +265,7 @@ class ProductSerializer(TransferSerializerMixin): state = serializers.CharField() bottles_produced = serializers.CharField(allow_null=True, allow_blank=True) unique_key = serializers.CharField(allow_null=True) + price = serializers.DecimalField(max_digits=14, decimal_places=2) class Meta: model = models.Product @@ -287,6 +288,7 @@ class ProductSerializer(TransferSerializerMixin): 'state', # done 'bottles_produced', # done 'unique_key', # done + 'price', ) def validate(self, attrs): @@ -308,6 +310,7 @@ class ProductSerializer(TransferSerializerMixin): old_id = attrs.pop('id') state = self.get_state(attrs.pop('state', None)) + attrs['old_id'] = old_id attrs['name'] = name attrs['old_unique_key'] = attrs.pop('unique_key') @@ -332,6 +335,7 @@ class ProductSerializer(TransferSerializerMixin): attrs['wine_village'] = self.get_wine_village(village) attrs['available'] = self.get_availability(state) attrs['slug'] = self.get_slug(name, old_id) + attrs['average_price'] = attrs.pop('price') return attrs def create(self, validated_data): From 0b1b89b6ba2aa327c21a6533ffaf279cda51cc31 Mon Sep 17 00:00:00 2001 From: "a.feteleu" Date: Tue, 19 Nov 2019 17:38:53 +0300 Subject: [PATCH 070/115] added merge migration --- .../management/commands/fill_establishment_gallery.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/establishment/management/commands/fill_establishment_gallery.py b/apps/establishment/management/commands/fill_establishment_gallery.py index cbe7d9e8..bb6edc39 100644 --- a/apps/establishment/management/commands/fill_establishment_gallery.py +++ b/apps/establishment/management/commands/fill_establishment_gallery.py @@ -21,10 +21,10 @@ class Command(BaseCommand): image_url = establishment.image_url.rstrip() relative_image_path = image_url[len(cdn_prefix):] - response = requests.head(image_url, allow_redirects=True) - if response.status_code != status.HTTP_200_OK: - not_valid_link_counter += 1 - not_valid_urls.append(image_url) + #response = requests.head(image_url, allow_redirects=True) + #if response.status_code != status.HTTP_200_OK: + # not_valid_link_counter += 1 + # not_valid_urls.append(image_url) image, image_created = Image.objects.get_or_create( orientation=Image.HORIZONTAL, From a8cc734d209e9ed54bd3d06d50e1bce5bca089e4 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 19 Nov 2019 17:47:12 +0300 Subject: [PATCH 071/115] back user api --- apps/account/serializers/back.py | 32 +++++++++++++- apps/account/urls/back.py | 3 +- apps/account/views/back.py | 31 ++++++++++++- project/settings/local.py | 74 ++++++++++++++++---------------- 4 files changed, 99 insertions(+), 41 deletions(-) diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index c1a1c6d4..57c3fb42 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -1,6 +1,7 @@ """Back account serializers""" from rest_framework import serializers from account import models +from account.models import User class RoleSerializer(serializers.ModelSerializer): @@ -18,4 +19,33 @@ class UserRoleSerializer(serializers.ModelSerializer): fields = [ 'user', 'role' - ] \ No newline at end of file + ] + + +class BackUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = '__all__' + extra_kwargs = { + 'password': {'write_only': True} + } + read_only_fields = ('old_password', 'last_login', 'date_joined') + + def create(self, validated_data): + user = super().create(validated_data) + user.set_password(validated_data['password']) + user.save() + return user + + +class BackDetailUserSerializer(BackUserSerializer): + class Meta: + model = User + exclude = ('password',) + read_only_fields = ('old_password', 'last_login', 'date_joined') + + def create(self, validated_data): + user = super().create(validated_data) + user.set_password(validated_data['password']) + user.save() + return user diff --git a/apps/account/urls/back.py b/apps/account/urls/back.py index ee2e4148..630a4cb9 100644 --- a/apps/account/urls/back.py +++ b/apps/account/urls/back.py @@ -8,5 +8,6 @@ app_name = 'account' urlpatterns = [ path('role/', views.RoleLstView.as_view(), name='role-list-create'), path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'), - + path('user/', views.UserLstView.as_view(), name='user-list-create'), + path('user//', views.UserRUDView.as_view(), name='user-rud'), ] diff --git a/apps/account/views/back.py b/apps/account/views/back.py index 8799f915..4ce4fdfc 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -1,4 +1,7 @@ -from rest_framework import generics +from django_filters.rest_framework import DjangoFilterBackend, filters +from rest_framework import generics, permissions + +from account.models import User from account.serializers import back as serializers from account import models @@ -10,4 +13,28 @@ class RoleLstView(generics.ListCreateAPIView): class UserRoleLstView(generics.ListCreateAPIView): serializer_class = serializers.UserRoleSerializer - queryset = models.Role.objects.all() \ No newline at end of file + queryset = models.Role.objects.all() + + +class UserLstView(generics.ListCreateAPIView): + """User list create view.""" + queryset = User.objects.all() + serializer_class = serializers.BackUserSerializer + permission_classes = (permissions.IsAdminUser,) + filter_backends = (DjangoFilterBackend, filters.OrderingFilter,) + filterset_fields = ( + 'email_confirmed', + 'is_staff', + 'is_active', + 'is_superuser', + 'roles', + ) + ordering_fields = '__all__' + + +class UserRUDView(generics.RetrieveUpdateDestroyAPIView): + """User RUD view.""" + queryset = User.objects.all() + serializer_class = serializers.BackDetailUserSerializer + permission_classes = (permissions.IsAdminUser,) + lookup_field = 'id' diff --git a/project/settings/local.py b/project/settings/local.py index f9a096fe..5a9e7413 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -43,8 +43,8 @@ INSTALLED_APPS.append('transfer.apps.TransferConfig') DATABASES.update({ 'legacy': { 'ENGINE': 'django.db.backends.mysql', - 'HOST': '172.22.0.1', - # 'HOST': 'mysql_db', + # 'HOST': '172.22.0.1', + 'HOST': 'mysql_db', 'PORT': 3306, 'NAME': 'dev', 'USER': 'dev', @@ -52,41 +52,41 @@ DATABASES.update({ # LOGGING -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, - }, - 'handlers': { - 'console': { - 'level': 'DEBUG', - 'filters': ['require_debug_true'], - 'class': 'logging.StreamHandler', - }, - 'null': { - 'class': 'logging.NullHandler', - }, - }, - 'loggers': { - 'django': { - 'handlers': ['console'], - }, - 'py.warnings': { - 'handlers': ['console'], - }, - 'django.db.backends': { - 'handlers': ['console', ], - 'level': 'DEBUG', - 'propagate': False, - }, - } -} +# LOGGING = { +# 'version': 1, +# 'disable_existing_loggers': False, +# 'filters': { +# 'require_debug_false': { +# '()': 'django.utils.log.RequireDebugFalse', +# }, +# 'require_debug_true': { +# '()': 'django.utils.log.RequireDebugTrue', +# }, +# }, +# 'handlers': { +# 'console': { +# 'level': 'DEBUG', +# 'filters': ['require_debug_true'], +# 'class': 'logging.StreamHandler', +# }, +# 'null': { +# 'class': 'logging.NullHandler', +# }, +# }, +# 'loggers': { +# 'django': { +# 'handlers': ['console'], +# }, +# 'py.warnings': { +# 'handlers': ['console'], +# }, +# 'django.db.backends': { +# 'handlers': ['console', ], +# 'level': 'DEBUG', +# 'propagate': False, +# }, +# } +# } # ELASTICSEARCH SETTINGS From 0fe7c50e5b66615ffb8763e3c57c6a0e7b08df25 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 19 Nov 2019 17:50:04 +0300 Subject: [PATCH 072/115] logging --- project/settings/local.py | 70 +++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/project/settings/local.py b/project/settings/local.py index 5a9e7413..959e6149 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -52,41 +52,41 @@ DATABASES.update({ # LOGGING -# LOGGING = { -# 'version': 1, -# 'disable_existing_loggers': False, -# 'filters': { -# 'require_debug_false': { -# '()': 'django.utils.log.RequireDebugFalse', -# }, -# 'require_debug_true': { -# '()': 'django.utils.log.RequireDebugTrue', -# }, -# }, -# 'handlers': { -# 'console': { -# 'level': 'DEBUG', -# 'filters': ['require_debug_true'], -# 'class': 'logging.StreamHandler', -# }, -# 'null': { -# 'class': 'logging.NullHandler', -# }, -# }, -# 'loggers': { -# 'django': { -# 'handlers': ['console'], -# }, -# 'py.warnings': { -# 'handlers': ['console'], -# }, -# 'django.db.backends': { -# 'handlers': ['console', ], -# 'level': 'DEBUG', -# 'propagate': False, -# }, -# } -# } +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + }, + 'null': { + 'class': 'logging.NullHandler', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + }, + 'py.warnings': { + 'handlers': ['console'], + }, + 'django.db.backends': { + 'handlers': ['console', ], + 'level': 'DEBUG', + 'propagate': False, + }, + } +} # ELASTICSEARCH SETTINGS From 9975f3f3b52b9dc18e20ff4e59aa866ca3ad3cfe Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 19 Nov 2019 18:28:53 +0300 Subject: [PATCH 073/115] added bo endpoints for product notes --- apps/account/serializers/common.py | 12 +++++++ apps/establishment/admin.py | 7 +++- apps/establishment/models.py | 4 +-- apps/product/models.py | 8 ++++- apps/product/serializers/back.py | 53 ++++++++++++++++++++++++++++++ apps/product/urls/back.py | 2 ++ apps/product/views/back.py | 47 +++++++++++++++++++++++++- 7 files changed, 128 insertions(+), 5 deletions(-) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 20016297..d2933747 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -99,6 +99,18 @@ class UserBaseSerializer(serializers.ModelSerializer): read_only_fields = fields +class UserShortSerializer(UserSerializer): + """Compact serializer for model User.""" + + class Meta(UserSerializer.Meta): + """Meta class.""" + fields = [ + 'id', + 'fullname', + 'email', + ] + + class ChangePasswordSerializer(serializers.ModelSerializer): """Serializer for model User.""" diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index e500ed6a..e6b0d991 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -64,13 +64,18 @@ class CompanyInline(admin.TabularInline): extra = 0 +class EstablishmentNote(admin.TabularInline): + model = models.EstablishmentNote + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Establishment admin.""" list_display = ['id', '__str__', 'image_tag', ] search_fields = ['id', 'name', 'index_name', 'slug'] list_filter = ['public_mark', 'toque_number'] - inlines = [GalleryImageInline, CompanyInline] + inlines = [GalleryImageInline, CompanyInline, EstablishmentNote] # inlines = [ # AwardInline, ContactPhoneInline, ContactEmailInline, diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 7fc0b2d6..e9f230f1 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -9,15 +9,15 @@ from django.contrib.contenttypes import fields as generic from django.contrib.gis.db.models.functions import Distance from django.contrib.gis.geos import Point from django.contrib.gis.measure import Distance as DistanceMeasure +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.utils import timezone from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from timezone_field import TimeZoneField -from django.contrib.postgres.fields import ArrayField -from django.core.validators import MinValueValidator, MaxValueValidator from collection.models import Collection from location.models import Address diff --git a/apps/product/models.py b/apps/product/models.py index aec37ae7..bd28d533 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -231,6 +231,12 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsM """Override str dunder method.""" return f'{self.name}' + def delete(self, using=None, keep_parents=False): + """Overridden delete method""" + # Delete all related notes + self.notes.all().delete() + return super().delete(using, keep_parents) + @property def product_type_translated_name(self): """Get translated name of product type.""" @@ -431,7 +437,7 @@ class ProductNote(ProjectBaseMixin): old_id = models.PositiveIntegerField(null=True, blank=True) text = models.TextField(verbose_name=_('text')) product = models.ForeignKey(Product, on_delete=models.PROTECT, - related_name='product_notes', + related_name='notes', verbose_name=_('product')) user = models.ForeignKey('account.User', on_delete=models.PROTECT, null=True, diff --git a/apps/product/serializers/back.py b/apps/product/serializers/back.py index 630a815a..ffbf690d 100644 --- a/apps/product/serializers/back.py +++ b/apps/product/serializers/back.py @@ -7,6 +7,7 @@ from product import models from product.serializers import ProductDetailSerializer, ProductTypeBaseSerializer, \ ProductSubTypeBaseSerializer from tag.models import TagCategory +from account.serializers.common import UserShortSerializer class ProductBackOfficeGallerySerializer(serializers.ModelSerializer): @@ -127,3 +128,55 @@ class ProductSubTypeBackOfficeDetailSerializer(ProductSubTypeBaseSerializer): 'name', 'index_name', ] + + +class ProductNoteBaseSerializer(serializers.ModelSerializer): + """Serializer for model ProductNote.""" + + user_detail = UserShortSerializer(read_only=True, source='user') + + class Meta: + """Meta class.""" + model = models.ProductNote + fields = [ + 'id', + 'created', + 'modified', + 'text', + 'user', + 'user_detail', + 'product', + ] + extra_kwargs = { + 'created': {'read_only': True}, + 'modified': {'read_only': True}, + 'product': {'required': False, 'write_only': True}, + 'user': {'required': False, 'write_only': True}, + } + + @property + def serializer_view(self): + """Return view instance.""" + return self.context.get('view') + + +class ProductNoteListCreateSerializer(ProductNoteBaseSerializer): + """Serializer for List|Create action for model ProductNote.""" + + def create(self, validated_data): + """Overridden create method.""" + validated_data['user'] = self.user + validated_data['product'] = self.product + return super().create(validated_data) + + @property + def user(self): + """Return user instance from view.""" + if self.serializer_view: + return self.serializer_view.request.user + + @property + def product(self): + """Return product instance from view.""" + if self.serializer_view: + return self.serializer_view.get_object() diff --git a/apps/product/urls/back.py b/apps/product/urls/back.py index 7d3b1611..fc2aaad0 100644 --- a/apps/product/urls/back.py +++ b/apps/product/urls/back.py @@ -6,6 +6,8 @@ from product import views urlpatterns = [ path('', views.ProductListCreateBackOfficeView.as_view(), name='list-create'), path('/', views.ProductDetailBackOfficeView.as_view(), name='rud'), + path('/notes/', views.ProductNoteListCreateView.as_view(), name='note-list-create'), + path('/notes//', views.ProductNoteRUDView.as_view(), name='note-rud'), path('/gallery/', views.ProductBackOfficeGalleryListView.as_view(), name='gallery-list'), path('/gallery//', views.ProductBackOfficeGalleryCreateDestroyView.as_view(), diff --git a/apps/product/views/back.py b/apps/product/views/back.py index 2e072fc6..ac780849 100644 --- a/apps/product/views/back.py +++ b/apps/product/views/back.py @@ -12,7 +12,7 @@ from utils.views import CreateDestroyGalleryViewMixin class ProductBackOfficeMixinView(ProductBaseView): """Product back-office mixin view.""" - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, ) def get_queryset(self): """Override get_queryset method.""" @@ -135,3 +135,48 @@ class ProductSubTypeRUDBackOfficeView(BackOfficeListCreateMixin, generics.RetrieveUpdateDestroyAPIView): """Product sub type back-office retrieve-update-destroy view.""" serializer_class = serializers.ProductSubTypeBackOfficeDetailSerializer + + +class ProductNoteListCreateView(ProductBackOfficeMixinView, + BackOfficeListCreateMixin, + generics.ListCreateAPIView): + """Retrieve|Update|Destroy product note view.""" + + serializer_class = serializers.ProductNoteListCreateSerializer + + def get_object(self): + """Returns the object the view is displaying.""" + product_qs = models.Product.objects.all() + filtered_product_qs = self.filter_queryset(product_qs) + + product = get_object_or_404(filtered_product_qs, pk=self.kwargs['pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, product) + + return product + + def get_queryset(self): + """Overridden get_queryset method.""" + return self.get_object().notes.all() + + +class ProductNoteRUDView(ProductBackOfficeMixinView, + BackOfficeListCreateMixin, + generics.RetrieveUpdateDestroyAPIView): + """Create|Retrieve|Update|Destroy product note view.""" + + serializer_class = serializers.ProductNoteBaseSerializer + + def get_object(self): + """Returns the object the view is displaying.""" + product_qs = models.Product.objects.all() + filtered_product_qs = self.filter_queryset(product_qs) + + product = get_object_or_404(filtered_product_qs, pk=self.kwargs['pk']) + note = get_object_or_404(product.notes.all(), pk=self.kwargs['note_pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, note) + + return note From 551cb2d5b6334d3b5500653ff3b8cd0f664358e2 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, 19 Nov 2019 18:43:37 +0300 Subject: [PATCH 074/115] Fix index elastic --- project/settings/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/settings/base.py b/project/settings/base.py index d91f54e2..9c365b52 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -501,4 +501,6 @@ FALLBACK_LOCALE = 'en-GB' CAROUSEL_ITEMS = [230, 231, 232] 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'] \ No newline at end of file +INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] + +ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file From c4e9f9e8432129b2619cc4e1c2340ad871fad404 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 19 Nov 2019 18:54:13 +0300 Subject: [PATCH 075/115] Filter products by country --- apps/search_indexes/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 6c8d3cc4..46f25545 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -242,6 +242,9 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'field': 'tags.id', 'lookups': [constants.LOOKUP_QUERY_IN], }, + 'country': { + 'field': 'wine_region.country.code', + }, 'wine_colors_id': { 'field': 'wine_colors.id', 'lookups': [ From 7456e9c3af5097422960e8f6d54ad47f51d3e5cb Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 19 Nov 2019 20:47:10 +0300 Subject: [PATCH 076/115] hardcoded review filter --- apps/review/transfer_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/review/transfer_data.py b/apps/review/transfer_data.py index 6eabca79..f9c74908 100644 --- a/apps/review/transfer_data.py +++ b/apps/review/transfer_data.py @@ -50,7 +50,7 @@ def transfer_text_review(): queryset = ReviewTexts.objects.filter( review_id__in=list(reviews) ).exclude( - text__iregex=r'[\s[ \t\n\r\f]*]+' + id__in=(23183, 25348, 43930, 23199, 26226, 34006) # пробелы вместо текста ).exclude( text__isnull=True ).exclude( From 46d4898428c090927348575e37cadb9230c3417e Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 20 Nov 2019 09:11:17 +0300 Subject: [PATCH 077/115] user back api test --- apps/account/tests/tests_back.py | 49 ++++++++++++++++++++++++++++++++ apps/account/views/back.py | 7 ++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/apps/account/tests/tests_back.py b/apps/account/tests/tests_back.py index d0178159..71fcd632 100644 --- a/apps/account/tests/tests_back.py +++ b/apps/account/tests/tests_back.py @@ -84,3 +84,52 @@ class UserRoleTests(APITestCase): response = self.client.post(url, data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + +class UserTestCase(APITestCase): + + def setUp(self): + self.user_1 = User.objects.create_user( + username='alex', + email='alex@mail.com', + password='alex_password', + is_staff=True, + ) + + self.user_2 = User.objects.create_user( + username='boris', + email='boris@mail.com', + password='boris_password', + ) + + # get tokens + tokens = User.create_jwt_tokens(self.user_1) + self.client.cookies = SimpleCookie( + {'access_token': tokens.get('access_token'), + 'refresh_token': tokens.get('refresh_token')}) + + def test_user_CRUD(self): + response = self.client.get('/api/back/account/user/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + 'username': 'roman', + 'email': 'roman@mail.com', + 'password': 'roman_password', + } + + response = self.client.post('/api/back/account/user/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/back/account/user/{self.user_2.id}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'first_name': 'Boris' + } + + response = self.client.patch(f'/api/back/account/user/{self.user_2.id}/', data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete(f'/api/back/account/user/{self.user_2.id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/account/views/back.py b/apps/account/views/back.py index 4ce4fdfc..b3d77d1e 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -1,9 +1,9 @@ -from django_filters.rest_framework import DjangoFilterBackend, filters +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, permissions +from account import models from account.models import User from account.serializers import back as serializers -from account import models class RoleLstView(generics.ListCreateAPIView): @@ -21,7 +21,7 @@ class UserLstView(generics.ListCreateAPIView): queryset = User.objects.all() serializer_class = serializers.BackUserSerializer permission_classes = (permissions.IsAdminUser,) - filter_backends = (DjangoFilterBackend, filters.OrderingFilter,) + filter_backends = (DjangoFilterBackend,) filterset_fields = ( 'email_confirmed', 'is_staff', @@ -29,7 +29,6 @@ class UserLstView(generics.ListCreateAPIView): 'is_superuser', 'roles', ) - ordering_fields = '__all__' class UserRUDView(generics.RetrieveUpdateDestroyAPIView): From 1a1a2a2d24574a772b8ae3fef098303e7bcdc614 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 20 Nov 2019 09:43:45 +0300 Subject: [PATCH 078/115] added bo endpoints for establishment notes --- .../migrations/0064_auto_20191119_1546.py | 19 +++++++ apps/establishment/models.py | 8 +-- apps/establishment/serializers/back.py | 53 +++++++++++++++++++ apps/establishment/urls/back.py | 4 ++ apps/establishment/views/back.py | 44 ++++++++++++++- .../migrations/0017_auto_20191119_1546.py | 19 +++++++ 6 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 apps/establishment/migrations/0064_auto_20191119_1546.py create mode 100644 apps/product/migrations/0017_auto_20191119_1546.py diff --git a/apps/establishment/migrations/0064_auto_20191119_1546.py b/apps/establishment/migrations/0064_auto_20191119_1546.py new file mode 100644 index 00000000..80d4135b --- /dev/null +++ b/apps/establishment/migrations/0064_auto_20191119_1546.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-19 15:46 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0063_company'), + ] + + operations = [ + migrations.AlterField( + model_name='establishmentnote', + name='establishment', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='notes', to='establishment.Establishment', verbose_name='establishment'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index e9f230f1..3aee6c31 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -413,13 +413,15 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat """Overridden delete method""" # Delete all related companies self.companies.all().delete() + # Delete all related notes + self.notes.all().delete() return super().delete(using, keep_parents) @property def visible_tags(self): return super().visible_tags\ - .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', - 'business_tag', 'business_tags_de'])\ + .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', + 'business_tag', 'business_tags_de'])\ # todo: recalculate toque_number def recalculate_toque_number(self): @@ -578,7 +580,7 @@ class EstablishmentNote(ProjectBaseMixin): old_id = models.PositiveIntegerField(null=True, blank=True) text = models.TextField(verbose_name=_('text')) establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT, - related_name='establishment_notes', + related_name='notes', verbose_name=_('establishment')) user = models.ForeignKey('account.User', on_delete=models.PROTECT, null=True, diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 3b80da72..a78bce07 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -8,6 +8,7 @@ from utils.decorators import with_base_attributes from utils.serializers import TimeZoneChoiceField from gallery.models import Image from django.utils.translation import gettext_lazy as _ +from account.serializers.common import UserShortSerializer class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer): @@ -224,3 +225,55 @@ class EstablishmentCompanyListCreateSerializer(model_serializers.CompanyBaseSeri """Overridden create method.""" validated_data['establishment'] = self.context.get('view').get_object() return super().create(validated_data) + + +class EstablishmentNoteBaseSerializer(serializers.ModelSerializer): + """Serializer for model EstablishmentNote.""" + + user_detail = UserShortSerializer(read_only=True, source='user') + + class Meta: + """Meta class.""" + model = models.EstablishmentNote + fields = [ + 'id', + 'created', + 'modified', + 'text', + 'user', + 'user_detail', + 'establishment', + ] + extra_kwargs = { + 'created': {'read_only': True}, + 'modified': {'read_only': True}, + 'establishment': {'required': False, 'write_only': True}, + 'user': {'required': False, 'write_only': True}, + } + + @property + def serializer_view(self): + """Return view instance.""" + return self.context.get('view') + + +class EstablishmentNoteListCreateSerializer(EstablishmentNoteBaseSerializer): + """Serializer for List|Create action for model EstablishmentNote.""" + + def create(self, validated_data): + """Overridden create method.""" + validated_data['user'] = self.user + validated_data['establishment'] = self.establishment + return super().create(validated_data) + + @property + def user(self): + """Return user instance from view.""" + if self.serializer_view: + return self.serializer_view.request.user + + @property + def establishment(self): + """Return establishment instance from view.""" + if self.serializer_view: + return self.serializer_view.get_object() diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 47ac2357..f06e2187 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -22,6 +22,10 @@ urlpatterns = [ name='company-list-create'), path('/companies//', views.EstablishmentCompanyRUDView.as_view(), name='company-rud'), + path('/notes/', views.EstablishmentNoteListCreateView.as_view(), + name='note-list-create'), + path('/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'), path('plates/', views.PlateListCreateView.as_view(), name='plates'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 24b47720..ad38e806 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -191,7 +191,7 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, CreateDestroyGalleryViewMixin): - """Resource for a create|destroy gallery for product for back-office users.""" + """Resource for a create|destroy gallery for establishment for back-office users.""" serializer_class = serializers.EstablishmentBackOfficeGallerySerializer def get_object(self): @@ -271,3 +271,45 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews, return company + +class EstablishmentNoteListCreateView(EstablishmentMixinViews, + generics.ListCreateAPIView): + """Retrieve|Update|Destroy establishment note view.""" + + serializer_class = serializers.EstablishmentNoteListCreateSerializer + + def get_object(self): + """Returns the object the view is displaying.""" + 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['pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, establishment) + + return establishment + + def get_queryset(self): + """Overridden get_queryset method.""" + return self.get_object().notes.all() + + +class EstablishmentNoteRUDView(EstablishmentMixinViews, + generics.RetrieveUpdateDestroyAPIView): + """Create|Retrieve|Update|Destroy establishment note view.""" + + serializer_class = serializers.EstablishmentNoteBaseSerializer + + def get_object(self): + """Returns the object the view is displaying.""" + 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['pk']) + note = get_object_or_404(establishment.notes.all(), pk=self.kwargs['note_pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, note) + + return note diff --git a/apps/product/migrations/0017_auto_20191119_1546.py b/apps/product/migrations/0017_auto_20191119_1546.py new file mode 100644 index 00000000..38eff085 --- /dev/null +++ b/apps/product/migrations/0017_auto_20191119_1546.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-19 15:46 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0016_product_average_price'), + ] + + operations = [ + migrations.AlterField( + model_name='productnote', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='notes', to='product.Product', verbose_name='product'), + ), + ] From b42b75609a97961c785828b18c2beab41c2582ab 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, 20 Nov 2019 09:59:23 +0300 Subject: [PATCH 079/115] Check elasticsearch dsl autosync --- project/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/settings/base.py b/project/settings/base.py index 9c365b52..165dffec 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -503,4 +503,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'] -ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file +#ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file From d932df4392a306baeea50a2605d124cb23294df6 Mon Sep 17 00:00:00 2001 From: "a.feteleu" Date: Wed, 20 Nov 2019 10:07:23 +0300 Subject: [PATCH 080/115] add unknown migrations from developm server:-( --- .../migrations/0028_merge_20191118_1507.py | 14 ++++++++++++++ .../migrations/0029_merge_20191119_1438.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 apps/location/migrations/0028_merge_20191118_1507.py create mode 100644 apps/location/migrations/0029_merge_20191119_1438.py diff --git a/apps/location/migrations/0028_merge_20191118_1507.py b/apps/location/migrations/0028_merge_20191118_1507.py new file mode 100644 index 00000000..f7b41a63 --- /dev/null +++ b/apps/location/migrations/0028_merge_20191118_1507.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.7 on 2019-11-18 15:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0027_auto_20191118_1313'), + ('location', '0027_auto_20191118_1011'), + ] + + operations = [ + ] diff --git a/apps/location/migrations/0029_merge_20191119_1438.py b/apps/location/migrations/0029_merge_20191119_1438.py new file mode 100644 index 00000000..666abc64 --- /dev/null +++ b/apps/location/migrations/0029_merge_20191119_1438.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.7 on 2019-11-19 14:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0028_merge_20191119_0647'), + ('location', '0028_merge_20191118_1507'), + ] + + operations = [ + ] From 498e8ac126e5646441b3c617cbd53440e7e58be1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 20 Nov 2019 11:56:25 +0300 Subject: [PATCH 081/115] fix product country filter in ES --- apps/search_indexes/documents/product.py | 14 +++++++++++++- apps/search_indexes/views.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index 35b1956a..13943191 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -34,7 +34,19 @@ class ProductDocument(Document): 'id': fields.IntegerField(), 'name': fields.KeywordField(), 'slug': fields.KeywordField(), - # 'city' TODO: city indexing + 'address': fields.ObjectField( + properties={ + 'city': fields.ObjectField( + properties={ + 'country': fields.ObjectField( + properties={ + 'code': fields.KeywordField() + } + ) + } + ) + } + ) } ) wine_colors = fields.ObjectField( diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 46f25545..783754c7 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -243,7 +243,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'lookups': [constants.LOOKUP_QUERY_IN], }, 'country': { - 'field': 'wine_region.country.code', + 'field': 'establishment.address.city.country.code', }, 'wine_colors_id': { 'field': 'wine_colors.id', From 0e6a4eded2847e1dbe173901ab34235a4b09bf2e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 20 Nov 2019 12:10:28 +0300 Subject: [PATCH 082/115] fixed transfer_product_reviews --- apps/review/transfer_data.py | 2 +- apps/transfer/serializers/reviews.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/review/transfer_data.py b/apps/review/transfer_data.py index f9c74908..af70873e 100644 --- a/apps/review/transfer_data.py +++ b/apps/review/transfer_data.py @@ -34,7 +34,7 @@ def transfer_languages(): def transfer_reviews(): establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) - queryset = Reviews.objects.filter( + queryset = Reviews.objects.exclude(product_id__isnull=False).filter( establishment_id__in=list(establishments), ).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'establishment_id', 'mark', 'vintage') diff --git a/apps/transfer/serializers/reviews.py b/apps/transfer/serializers/reviews.py index 25649b12..6e0db860 100644 --- a/apps/transfer/serializers/reviews.py +++ b/apps/transfer/serializers/reviews.py @@ -69,6 +69,7 @@ class ProductReviewSerializer(ReviewSerializer): data.pop('reviewer_id') data.pop('product_id') data.pop('aasm_state') + data.pop('establishment_id') return data def create(self, validated_data): From 7f8f72815e862b0ebf098b5c4ccdd7ec3aedabf5 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Wed, 20 Nov 2019 12:21:55 +0300 Subject: [PATCH 083/115] append city or country to ProductDocumentSerialzier --- apps/search_indexes/documents/product.py | 19 ++++++++++++- apps/search_indexes/serializers.py | 35 ++++++++++++++++++------ apps/utils/models.py | 4 +++ 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index 35b1956a..cdf48ef2 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -33,8 +33,25 @@ class ProductDocument(Document): properties={ 'id': fields.IntegerField(), 'name': fields.KeywordField(), + 'index_name': fields.KeywordField(), 'slug': fields.KeywordField(), - # 'city' TODO: city indexing + 'city': fields.ObjectField( + attr='address.city', + properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + 'code': fields.KeywordField(), + 'country': fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'name': fields.ObjectField(attr='name_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'code': fields.KeywordField(), + 'svg_image': fields.KeywordField(attr='svg_image_indexing') + } + ), + } + ), } ) wine_colors = fields.ObjectField( diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index a8391ecc..288d3577 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -95,14 +95,6 @@ class ProductTypeDocumentSerializer(serializers.Serializer): return get_translated_value(obj.name) -class ProductEstablishmentDocumentSerializer(serializers.Serializer): - """Related to Product Establishment ES document serializer.""" - - id = serializers.IntegerField() - name = serializers.CharField() - slug = serializers.CharField() - - class CityDocumentShortSerializer(serializers.Serializer): """City serializer for ES Document,""" @@ -111,6 +103,33 @@ class CityDocumentShortSerializer(serializers.Serializer): name = serializers.CharField() +class CountryDocumentSerializer(serializers.Serializer): + + id = serializers.IntegerField() + code = serializers.CharField(allow_null=True) + svg_image = serializers.CharField() + name_translated = serializers.SerializerMethodField() + + @staticmethod + def get_name_translated(obj): + return get_translated_value(obj.name) + + +class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer): + + country = CountryDocumentSerializer() + + +class ProductEstablishmentDocumentSerializer(serializers.Serializer): + """Related to Product Establishment ES document serializer.""" + + id = serializers.IntegerField() + name = serializers.CharField() + slug = serializers.CharField() + index_name = serializers.CharField() + city = AnotherCityDocumentShortSerializer() + + class AddressDocumentSerializer(serializers.Serializer): """Address serializer for ES Document.""" diff --git a/apps/utils/models.py b/apps/utils/models.py index e508be6f..f86093af 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -238,6 +238,10 @@ class SVGImageMixin(models.Model): validators=[svg_image_validator, ], verbose_name=_('SVG image')) + @property + def svg_image_indexing(self): + return self.svg_image.url if self.svg_image else None + class Meta: abstract = True From d3faacba3b7fd1b863e9115757e247977c86bed8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 20 Nov 2019 12:41:36 +0300 Subject: [PATCH 084/115] fix product view mixins --- apps/product/models.py | 6 ++++-- apps/product/views/common.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/product/models.py b/apps/product/models.py index bd28d533..2e3d26e7 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -265,8 +265,10 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsM @property def related_tags(self): - return super().visible_tags.exclude(category__index_name__in=['sugar-content', 'wine-color', - 'bottles-produced','serial-number', 'grape-variety']) + return super().visible_tags.exclude(category__index_name__in=[ + 'sugar-content', 'wine-color', 'bottles-produced', + 'serial-number', 'grape-variety'] + ) @property def display_name(self): diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 911d1f0b..8b857ddb 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -17,7 +17,6 @@ class ProductBaseView(generics.GenericAPIView): return Product.objects.published() \ .with_base_related() \ .annotate_in_favorites(self.request.user) \ - .by_country_code(self.request.country_code) \ .order_by('-created') @@ -27,7 +26,8 @@ class ProductListView(ProductBaseView, generics.ListAPIView): filter_class = filters.ProductFilterSet def get_queryset(self): - qs = super().get_queryset().with_extended_related() + qs = super().get_queryset().with_extended_related() \ + .by_country_code(self.request.country_code) return qs From 61e049f5c307e0bd8ca0247d22815fdbbe2c667d Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 20 Nov 2019 12:56:57 +0300 Subject: [PATCH 085/115] review filter by product id --- apps/review/filters.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/review/filters.py b/apps/review/filters.py index 4867d1df..5b215b8e 100644 --- a/apps/review/filters.py +++ b/apps/review/filters.py @@ -8,6 +8,7 @@ class ReviewFilter(filters.FilterSet): """Review filter set.""" establishment_id = filters.NumberFilter(field_name='object_id', ) + product_id = filters.NumberFilter(field_name='object_id', ) class Meta: """Meta class.""" @@ -15,9 +16,15 @@ class ReviewFilter(filters.FilterSet): model = models.Review fields = ( 'establishment_id', + 'product_id', ) def by_establishment_id(self, queryset, name, value): if value not in EMPTY_VALUES: return queryset.by_establishment_id(value, content_type='establishment') return queryset + + def by_product_id(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_establishment_id(value, content_type='product') + return queryset From 2f6d0cf4ed55554c0ee4de234ae0575222b9f02b Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 20 Nov 2019 13:00:50 +0300 Subject: [PATCH 086/115] review filter fix --- apps/review/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/review/filters.py b/apps/review/filters.py index 5b215b8e..8e85a906 100644 --- a/apps/review/filters.py +++ b/apps/review/filters.py @@ -26,5 +26,5 @@ class ReviewFilter(filters.FilterSet): def by_product_id(self, queryset, name, value): if value not in EMPTY_VALUES: - return queryset.by_establishment_id(value, content_type='product') + return queryset.by_product_id(value, content_type='product') return queryset From ef1af8c3ac5c4094f126d0a8a25fe4a19aa0bba6 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 20 Nov 2019 13:27:16 +0300 Subject: [PATCH 087/115] added city gallery --- .../migrations/0030_auto_20191120_1010.py | 34 +++++++++++++ apps/location/models.py | 26 ++++++++-- apps/location/serializers/back.py | 45 +++++++++++++++++ apps/location/urls/back.py | 5 ++ apps/location/views/back.py | 48 ++++++++++++++++++- apps/product/models.py | 2 +- 6 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 apps/location/migrations/0030_auto_20191120_1010.py diff --git a/apps/location/migrations/0030_auto_20191120_1010.py b/apps/location/migrations/0030_auto_20191120_1010.py new file mode 100644 index 00000000..26c1e5ef --- /dev/null +++ b/apps/location/migrations/0030_auto_20191120_1010.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.7 on 2019-11-20 10:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0006_merge_20191027_1758'), + ('location', '0029_merge_20191119_1438'), + ] + + operations = [ + migrations.CreateModel( + name='CityGallery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_main', models.BooleanField(default=False, verbose_name='Is the main image')), + ('city', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='city_gallery', to='location.City', verbose_name='city')), + ('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='city_gallery', to='gallery.Image', verbose_name='image')), + ], + options={ + 'verbose_name': 'city gallery', + 'verbose_name_plural': 'city galleries', + 'unique_together': {('city', 'is_main'), ('city', 'image')}, + }, + ), + migrations.AddField( + model_name='city', + name='gallery', + field=models.ManyToManyField(through='location.CityGallery', to='gallery.Image'), + ), + ] diff --git a/apps/location/models.py b/apps/location/models.py index a1e0d60f..fdc2ef5d 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -8,7 +8,8 @@ from django.utils.translation import gettext_lazy as _ from translation.models import Language from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField, - TranslatedFieldsMixin, get_current_locale) + TranslatedFieldsMixin, get_current_locale, + IntermediateGalleryModelMixin, GalleryModelMixin) class CountryQuerySet(models.QuerySet): @@ -96,9 +97,8 @@ class CityQuerySet(models.QuerySet): return self.filter(country__code=code) -class City(models.Model): +class City(GalleryModelMixin): """Region model.""" - name = models.CharField(_('name'), max_length=250) code = models.CharField(_('code'), max_length=250) region = models.ForeignKey( @@ -111,6 +111,8 @@ class City(models.Model): is_island = models.BooleanField(_('is island'), default=False) old_id = models.IntegerField(null=True, blank=True, default=None) + gallery = models.ManyToManyField('gallery.Image', through='CityGallery') + objects = CityQuerySet.as_manager() class Meta: @@ -121,6 +123,24 @@ class City(models.Model): return self.name +class CityGallery(IntermediateGalleryModelMixin): + """Gallery for model City.""" + city = models.ForeignKey(City, null=True, + related_name='city_gallery', + on_delete=models.CASCADE, + verbose_name=_('city')) + image = models.ForeignKey('gallery.Image', null=True, + related_name='city_gallery', + on_delete=models.CASCADE, + verbose_name=_('image')) + + class Meta: + """CityGallery meta class.""" + verbose_name = _('city gallery') + verbose_name_plural = _('city galleries') + unique_together = (('city', 'is_main'), ('city', 'image')) + + class Address(models.Model): """Address model.""" city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE) diff --git a/apps/location/serializers/back.py b/apps/location/serializers/back.py index c178f7fd..9a263acd 100644 --- a/apps/location/serializers/back.py +++ b/apps/location/serializers/back.py @@ -1,5 +1,8 @@ from location import models from location.serializers import common +from rest_framework import serializers +from gallery.models import Image +from django.utils.translation import gettext_lazy as _ class AddressCreateSerializer(common.AddressDetailSerializer): @@ -18,3 +21,45 @@ class CountryBackSerializer(common.CountrySerializer): 'name', 'country_id' ] + + +class CityGallerySerializer(serializers.ModelSerializer): + """Serializer class for model CityGallery.""" + + class Meta: + """Meta class""" + + model = models.CityGallery + fields = [ + 'id', + 'is_main', + ] + + def get_request_kwargs(self): + """Get url kwargs from request.""" + return self.context.get('request').parser_context.get('kwargs') + + def validate(self, attrs): + """Override validate method.""" + city_pk = self.get_request_kwargs().get('pk') + image_id = self.get_request_kwargs().get('image_id') + + city_qs = models.City.objects.filter(pk=city_pk) + image_qs = Image.objects.filter(id=image_id) + + if not city_qs.exists(): + raise serializers.ValidationError({'detail': _('City not found')}) + + if not image_qs.exists(): + raise serializers.ValidationError({'detail': _('Image not found')}) + + city = city_qs.first() + image = image_qs.first() + + if image in city.gallery.all(): + raise serializers.ValidationError({'detail': _('Image is already added.')}) + + attrs['city'] = city + attrs['image'] = image + + return attrs diff --git a/apps/location/urls/back.py b/apps/location/urls/back.py index 8fd87dc9..c5ef027b 100644 --- a/apps/location/urls/back.py +++ b/apps/location/urls/back.py @@ -11,6 +11,11 @@ urlpatterns = [ path('cities/', views.CityListCreateView.as_view(), name='city-list-create'), path('cities//', views.CityRUDView.as_view(), name='city-retrieve'), + path('cities//gallery/', views.CityGalleryListView.as_view(), + name='gallery-list'), + path('cities//gallery//', + views.CityGalleryCreateDestroyView.as_view(), + name='gallery-create-destroy'), path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'), path('countries//', views.CountryRUDView.as_view(), name='country-retrieve'), diff --git a/apps/location/views/back.py b/apps/location/views/back.py index a4eee929..b6677837 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -4,7 +4,11 @@ from rest_framework import generics from location import models, serializers from location.views import common from utils.permissions import IsCountryAdmin -from rest_framework.permissions import IsAuthenticatedOrReadOnly +from utils.views import CreateDestroyGalleryViewMixin +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from django.shortcuts import get_object_or_404 +from utils.serializers import ImageBaseSerializer + # Address @@ -35,6 +39,48 @@ class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] +class CityGalleryCreateDestroyView(common.CityViewMixin, + CreateDestroyGalleryViewMixin): + """Resource for a create gallery for product for back-office users.""" + serializer_class = serializers.CityGallerySerializer + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] + + def get_object(self): + """ + Returns the object the view is displaying. + """ + city_qs = self.filter_queryset(self.get_queryset()) + + city = get_object_or_404(city_qs, pk=self.kwargs['pk']) + gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs['image_id']) + + # May raise a permission denied + self.check_object_permissions(self.request, gallery) + + return gallery + + +class CityGalleryListView(common.CityViewMixin, + generics.ListAPIView): + """Resource for returning gallery for product for back-office users.""" + serializer_class = ImageBaseSerializer + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] + + def get_object(self): + """Override get_object method.""" + qs = super(CityGalleryListView, self).get_queryset() + city = get_object_or_404(qs, pk=self.kwargs['pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, city) + + return city + + def get_queryset(self): + """Override get_queryset method.""" + return self.get_object().crop_gallery + + # Region class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView): """Create view for model Region""" diff --git a/apps/product/models.py b/apps/product/models.py index 2e3d26e7..6baaf43e 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -365,7 +365,7 @@ class ProductStandard(models.Model): class ProductGallery(IntermediateGalleryModelMixin): - + """Gallery for model Product.""" product = models.ForeignKey(Product, null=True, related_name='product_gallery', on_delete=models.CASCADE, From be581c7b7d490cbea5f11b267541c0292cba2829 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 20 Nov 2019 13:06:04 +0300 Subject: [PATCH 088/115] Dynamic front filters #1 --- apps/location/models.py | 4 ++++ apps/location/views/common.py | 2 +- apps/tag/filters.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/location/models.py b/apps/location/models.py index fdc2ef5d..3f104644 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -192,6 +192,10 @@ class WineRegionQuerySet(models.QuerySet): def with_sub_region_related(self): return self.prefetch_related('wine_sub_region') + def having_wines(self, value = True): + """Return qs with regions, which have any wine related to them""" + return self.exclude(wines__isnull=value) + class WineRegion(models.Model, TranslatedFieldsMixin): """Wine region model.""" diff --git a/apps/location/views/common.py b/apps/location/views/common.py index 19689329..660a1dbe 100644 --- a/apps/location/views/common.py +++ b/apps/location/views/common.py @@ -68,7 +68,7 @@ class WineRegionListView(generics.ListAPIView): pagination_class = None model = models.WineRegion permission_classes = (permissions.AllowAny,) - queryset = models.WineRegion.objects.with_sub_region_related().all() + queryset = models.WineRegion.objects.with_sub_region_related().having_wines() serializer_class = serializers.WineRegionSerializer diff --git a/apps/tag/filters.py b/apps/tag/filters.py index c127f765..8d2343ec 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -44,7 +44,7 @@ class TagCategoryFilterSet(TagsBaseFilterSet): def by_product_type(self, queryset, name, value): if value == product_models.ProductType.WINE: - queryset = queryset.filter(index_name='wine-color') + queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False) queryset = queryset.by_product_type(value) return queryset From c570cfdadd03f6ac338c6915afd4aee6e5119cfa 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, 20 Nov 2019 15:29:03 +0300 Subject: [PATCH 089/115] Fix --- apps/product/management/commands/add_product_tag.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index 589c7a83..18a2da21 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -1,6 +1,7 @@ from django.core.management.base import BaseCommand from django.db import connections from establishment.management.commands.add_position import namedtuplefetchall +from django.db.models import Q from tag.models import Tag, TagCategory from product.models import Product, ProductType from tqdm import tqdm @@ -67,8 +68,7 @@ class Command(BaseCommand): with connections['legacy'].cursor() as cursor: cursor.execute(''' select - DISTINCT - m.id as old_id, + DISTINCT trim(CONVERT(m.value USING utf8)) as tag_value, trim(CONVERT(v.key_name USING utf8)) as tag_category FROM product_metadata m @@ -103,6 +103,14 @@ class Command(BaseCommand): p.tags.clear() print('End clear tags product') + + def remove_tags(self): + print('Begin delete many tags') + Tag.objects.\ + filter(news__isnull=True, establishments__isnull=True).delete() + print('End delete many tags') + + def product_sql(self): with connections['legacy'].cursor() as cursor: cursor.execute(''' @@ -145,6 +153,7 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): self.remove_tags_product() + self.remove_tags() self.add_category_tag() self.add_type_product_category() self.add_tag() From 5a55fa82a2ae1961048c2e3257b3d3b89ebefe3a 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, 20 Nov 2019 15:32:36 +0300 Subject: [PATCH 090/115] Codestyle --- apps/product/management/commands/add_product_tag.py | 1 - project/settings/base.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index 18a2da21..dc54f35c 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -1,7 +1,6 @@ from django.core.management.base import BaseCommand from django.db import connections from establishment.management.commands.add_position import namedtuplefetchall -from django.db.models import Q from tag.models import Tag, TagCategory from product.models import Product, ProductType from tqdm import tqdm diff --git a/project/settings/base.py b/project/settings/base.py index 165dffec..9c365b52 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -503,4 +503,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'] -#ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file +ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file From cf22027dc1bce513d49c3108df97aa1e6aa2081e Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 20 Nov 2019 15:40:10 +0300 Subject: [PATCH 091/115] Search by wildcard --- apps/search_indexes/filters.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index a29c186a..28341502 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -70,10 +70,16 @@ class CustomSearchFilterBackend(SearchFilterBackend): __queries.append( Q("match", **{k: v}) ) + __queries.append( + Q('wildcard', **{k: f'*{search_term.lower()}*'}) + ) else: __queries.append( Q("match", **field_kwargs) ) + __queries.append( + Q('wildcard', **{field: f'*{search_term.lower()}*'}) + ) else: for field in view.search_fields: # Initial kwargs for the match query @@ -92,8 +98,14 @@ class CustomSearchFilterBackend(SearchFilterBackend): __queries.append( Q("match", **{k: v}) ) + __queries.append( + Q('wildcard', **{k: f'*{search_term.lower()}*'}) + ) else: __queries.append( Q("match", **field_kwargs) ) + __queries.append( + Q('wildcard', **{field: f'*{search_term.lower()}*'}) + ) return __queries \ No newline at end of file From bea485f936bf28374580c5bccbaeec337fa5d2a1 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Wed, 20 Nov 2019 15:56:56 +0300 Subject: [PATCH 092/115] city may be null --- apps/search_indexes/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 288d3577..cac4e336 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -119,6 +119,12 @@ class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer): country = CountryDocumentSerializer() + def to_representation(self, instance): + if instance != AttrDict(d={}) or \ + (isinstance(instance, dict) and len(instance) != 0): + return super().to_representation(instance) + return None + class ProductEstablishmentDocumentSerializer(serializers.Serializer): """Related to Product Establishment ES document serializer.""" From 7ba391300049824f3957b3af02bf5f611772abf5 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 20 Nov 2019 16:17:49 +0300 Subject: [PATCH 093/115] award filter --- apps/main/filters.py | 37 +++++++++++++++++++++++++++++++++++++ apps/main/models.py | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 apps/main/filters.py diff --git a/apps/main/filters.py b/apps/main/filters.py new file mode 100644 index 00000000..e3a05a71 --- /dev/null +++ b/apps/main/filters.py @@ -0,0 +1,37 @@ +from django.core.validators import EMPTY_VALUES +from django_filters import rest_framework as filters + +from review import models + + +class AwardFilter(filters.FilterSet): + """Award filter set.""" + + establishment_id = filters.NumberFilter(field_name='object_id', ) + product_id = filters.NumberFilter(field_name='object_id', ) + employee_id = filters.NumberFilter(field_name='object_id', ) + + class Meta: + """Meta class.""" + + model = models.Review + fields = ( + 'establishment_id', + 'product_id', + 'employee_id', + ) + + def by_establishment_id(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_establishment_id(value, content_type='establishment') + return queryset + + def by_product_id(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_product_id(value, content_type='product') + return queryset + + def by_employee_id(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_employee_id(value, content_type='establishmentemployee') + return queryset diff --git a/apps/main/models.py b/apps/main/models.py index 1bd39a6d..61a4d447 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -153,7 +153,7 @@ class Award(TranslatedFieldsMixin, URLImageMixin, models.Model): PUBLISHED = 1 STATE_CHOICES = ( - (WAITING,'waiting'), + (WAITING, 'waiting'), (PUBLISHED, 'published') ) From 2e17d7265d2d071926b853da7fa5daf329622d38 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Wed, 20 Nov 2019 16:29:46 +0300 Subject: [PATCH 094/115] add back api for collection --- apps/collection/serializers/back.py | 61 ++++++++++++++++++++++++++++- apps/collection/urls/back.py | 9 ++--- apps/collection/views/back.py | 54 +++++++++++++++++++------ apps/utils/views.py | 28 +++++++++++++ 4 files changed, 133 insertions(+), 19 deletions(-) diff --git a/apps/collection/serializers/back.py b/apps/collection/serializers/back.py index 4cc76b2f..bb88a778 100644 --- a/apps/collection/serializers/back.py +++ b/apps/collection/serializers/back.py @@ -1,8 +1,14 @@ from rest_framework import serializers + +from collection import models +from collection.serializers.common import CollectionBaseSerializer +from establishment.models import Establishment from location.models import Country from location.serializers import CountrySimpleSerializer -from collection.serializers.common import CollectionBaseSerializer -from collection import models +from product.models import Product +from utils.exceptions import ( + BindingObjectNotFound, RemovedBindingObjectNotFound, ObjectAlreadyAdded +) class CollectionBackOfficeSerializer(CollectionBaseSerializer): @@ -31,3 +37,54 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer): 'start', 'end', ] + + +class CollectionBindObjectSerializer(serializers.Serializer): + """Serializer for binding collection and objects""" + + ESTABLISHMENT = 'establishment' + PRODUCT = 'product' + + TYPE_CHOICES = ( + (ESTABLISHMENT, 'Establishment'), + (PRODUCT, 'Product'), + ) + + type = serializers.ChoiceField(TYPE_CHOICES) + object_id = serializers.IntegerField() + + def validate(self, attrs): + view = self.context.get('view') + request = self.context.get('request') + + obj_type = attrs.get('type') + obj_id = attrs.get('object_id') + + collection = view.get_object() + attrs['collection'] = collection + + if obj_type == self.ESTABLISHMENT: + establishment = Establishment.objects.filter(pk=obj_id).\ + first() + if not establishment: + raise BindingObjectNotFound() + if request.method == 'POST' and collection.establishments.\ + filter(pk=establishment.pk).exists(): + raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not collection.\ + establishments.filter(pk=establishment.pk).\ + exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = establishment + elif obj_type == self.PRODUCT: + product = Product.objects.filter(pk=obj_id).first() + if not product: + raise BindingObjectNotFound() + if request.method == 'POST' and collection.products.\ + filter(pk=product.pk).exists(): + raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not collection.products.\ + filter(pk=product.pk).exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = product + return attrs diff --git a/apps/collection/urls/back.py b/apps/collection/urls/back.py index eee40327..6a6dbd54 100644 --- a/apps/collection/urls/back.py +++ b/apps/collection/urls/back.py @@ -1,11 +1,10 @@ """Collection common urlpaths.""" -from django.urls import path +from rest_framework.routers import SimpleRouter from collection.views import back as views app_name = 'collection' +router = SimpleRouter() +router.register(r'', views.CollectionBackOfficeViewSet) -urlpatterns = [ - path('', views.CollectionListCreateView.as_view(), name='list-create'), - path('/', views.CollectionRUDView.as_view(), name='rud-collection'), -] +urlpatterns = router.urls diff --git a/apps/collection/views/back.py b/apps/collection/views/back.py index 78a2dcb0..a989ec56 100644 --- a/apps/collection/views/back.py +++ b/apps/collection/views/back.py @@ -1,19 +1,49 @@ -from rest_framework import generics, permissions +from rest_framework import permissions +from rest_framework import viewsets, mixins + from collection import models -from collection.serializers import back +from collection.serializers import back as serializers +from utils.views import BindObjectMixin -class CollectionListCreateView(generics.ListCreateAPIView): - """Collection list-create view.""" +class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """ViewSet for Collection model.""" + + pagination_class = None + permission_classes = (permissions.AllowAny,) queryset = models.Collection.objects.all() - serializer_class = back.CollectionBackOfficeSerializer - # todo: conf. permissions by TT - permission_classes = (permissions.IsAuthenticated, ) + serializer_class = serializers.CollectionBackOfficeSerializer -class CollectionRUDView(generics.RetrieveUpdateDestroyAPIView): - """Collection list-create view.""" +class CollectionBackOfficeViewSet(mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, + BindObjectMixin, + CollectionViewSet): + """ViewSet for Collection model for BackOffice users.""" + + permission_classes = (permissions.IsAuthenticated,) queryset = models.Collection.objects.all() - serializer_class = back.CollectionBackOfficeSerializer - # todo: conf. permissions by TT - permission_classes = (permissions.IsAuthenticated, ) + serializer_class = serializers.CollectionBackOfficeSerializer + bind_object_serializer_class = serializers.CollectionBindObjectSerializer + + def perform_binding(self, serializer): + data = serializer.validated_data + collection = data.pop('collection') + obj_type = data.get('type') + related_object = data.get('related_object') + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT: + collection.establishments.add(related_object) + elif obj_type == self.bind_object_serializer_class.PRODUCT: + collection.products.add(related_object) + + def perform_unbinding(self, serializer): + data = serializer.validated_data + collection = data.pop('collection') + obj_type = data.get('type') + related_object = data.get('related_object') + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT: + collection.establishments.remove(related_object) + elif obj_type == self.bind_object_serializer_class.PRODUCT: + collection.products.remove(related_object) diff --git a/apps/utils/views.py b/apps/utils/views.py index d3d09079..870e132f 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -4,6 +4,7 @@ from django.conf import settings from django.db.transaction import on_commit from rest_framework import generics from rest_framework import status +from rest_framework.decorators import action from rest_framework.response import Response from gallery.tasks import delete_image @@ -121,3 +122,30 @@ class CreateDestroyGalleryViewMixin(generics.CreateAPIView, # Delete an instances of Gallery model gallery_obj.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + +# BackOffice user`s views & viewsets +class BindObjectMixin: + """Bind object mixin.""" + + def get_serializer_class(self): + if self.action == 'bind_object': + return self.bind_object_serializer_class + return self.serializer_class + + def perform_binding(self, serializer): + raise NotImplemented + + def perform_unbinding(self, serializer): + raise NotImplemented + + @action(methods=['post', 'delete'], detail=True, url_path='bind-object') + def bind_object(self, request, pk=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + if request.method == 'POST': + self.perform_binding(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + elif request.method == 'DELETE': + self.perform_unbinding(serializer) + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file From a17e83ebe5001d7a22460c8406062f875c8c4fdd 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, 20 Nov 2019 16:36:37 +0300 Subject: [PATCH 095/115] On sync elasticsearch --- project/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/settings/base.py b/project/settings/base.py index 9c365b52..fba7f4b4 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -503,4 +503,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'] -ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file +#ELASTICSEARCH_DSL_AUTOSYNC = False From 824cbe203b06454b896988d0847c89b5b21426c1 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 20 Nov 2019 16:38:11 +0300 Subject: [PATCH 096/115] award back api --- apps/main/filters.py | 7 +++++-- apps/main/serializers.py | 20 ++++++++++++++++++-- apps/main/urls/back.py | 11 +++++++++++ apps/main/views/back.py | 21 +++++++++++++++++++++ apps/review/views/back.py | 4 +--- project/urls/back.py | 2 +- 6 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 apps/main/urls/back.py create mode 100644 apps/main/views/back.py diff --git a/apps/main/filters.py b/apps/main/filters.py index e3a05a71..72636d9e 100644 --- a/apps/main/filters.py +++ b/apps/main/filters.py @@ -1,7 +1,7 @@ from django.core.validators import EMPTY_VALUES from django_filters import rest_framework as filters -from review import models +from main import models class AwardFilter(filters.FilterSet): @@ -14,11 +14,14 @@ class AwardFilter(filters.FilterSet): class Meta: """Meta class.""" - model = models.Review + model = models.Award fields = ( 'establishment_id', 'product_id', 'employee_id', + 'state', + 'award_type', + 'vintage_year', ) def by_establishment_id(self, queryset, name, value): diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 6b8e2ba4..732cfbad 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -38,7 +38,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer): 'route', 'source', 'nested', - ) + ) class CurrencySerializer(ProjectModelSerializer): @@ -145,6 +145,22 @@ class AwardSerializer(AwardBaseSerializer): fields = AwardBaseSerializer.Meta.fields + ['award_type', ] +class BackAwardSerializer(serializers.ModelSerializer): + """Award serializer.""" + + title_translated = serializers.CharField(read_only=True, allow_null=True) + + class Meta: + model = models.Award + fields = [ + 'id', + 'title_translated', + 'vintage_year', + 'award_type', + 'state', + ] + + class CarouselListSerializer(serializers.ModelSerializer): """Serializer for retrieving list of carousel items.""" @@ -202,4 +218,4 @@ class PageTypeBaseSerializer(serializers.ModelSerializer): fields = [ 'id', 'name', - ] \ No newline at end of file + ] diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py new file mode 100644 index 00000000..93debb5e --- /dev/null +++ b/apps/main/urls/back.py @@ -0,0 +1,11 @@ +"""Back main URLs""" +from django.urls import path + +from main.views import back as views + +app_name = 'main' + +urlpatterns = [ + path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), + path('awards//', views.AwardRUDView.as_view(), name='awards-rud'), +] diff --git a/apps/main/views/back.py b/apps/main/views/back.py new file mode 100644 index 00000000..bbbfad53 --- /dev/null +++ b/apps/main/views/back.py @@ -0,0 +1,21 @@ +from rest_framework import generics, permissions + +from main import serializers +from main.filters import AwardFilter +from main.models import Award + + +class AwardLstView(generics.ListCreateAPIView): + """Award list create view.""" + queryset = Award.objects.all() + serializer_class = serializers.BackAwardSerializer + permission_classes = (permissions.IsAdminUser,) + filterset_class = AwardFilter + + +class AwardRUDView(generics.RetrieveUpdateDestroyAPIView): + """Award RUD view.""" + queryset = Award.objects.all() + serializer_class = serializers.BackAwardSerializer + permission_classes = (permissions.IsAdminUser,) + lookup_field = 'id' diff --git a/apps/review/views/back.py b/apps/review/views/back.py index 5e38f7ab..c5d89e11 100644 --- a/apps/review/views/back.py +++ b/apps/review/views/back.py @@ -1,9 +1,8 @@ -from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, permissions +from review import filters from review import models from review import serializers -from review import filters from utils.permissions import IsReviewerManager, IsRestaurantReviewer @@ -12,7 +11,6 @@ class ReviewLstView(generics.ListCreateAPIView): serializer_class = serializers.ReviewBaseSerializer queryset = models.Review.objects.all() permission_classes = [permissions.IsAuthenticatedOrReadOnly, ] - filter_backends = (DjangoFilterBackend,) filterset_class = filters.ReviewFilter diff --git a/project/urls/back.py b/project/urls/back.py index 04af4a53..fdd3d10a 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -15,5 +15,5 @@ urlpatterns = [ path('products/', include(('product.urls.back', 'product'), namespace='product')), path('re_blocks/', include(('advertisement.urls.back', 'advertisement'), namespace='advertisement')), + path('main/', include('main.urls.back')), ] - From cd328186e10dc3644cbf1ebad882e813ba36b650 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 20 Nov 2019 17:01:17 +0300 Subject: [PATCH 097/115] wildcard boosting --- apps/search_indexes/filters.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 28341502..3ef74978 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -71,14 +71,21 @@ class CustomSearchFilterBackend(SearchFilterBackend): Q("match", **{k: v}) ) __queries.append( - Q('wildcard', **{k: f'*{search_term.lower()}*'}) + Q('wildcard', + **{k: { + 'value': f'*{search_term.lower()}*', + 'boost': v.get('boost', 1) + 0.1 + } + } + ) ) else: __queries.append( Q("match", **field_kwargs) ) __queries.append( - Q('wildcard', **{field: f'*{search_term.lower()}*'}) + Q('wildcard', **{field: {'value': f'*{search_term.lower()}*', + 'boost': field_kwargs[field].get('boost', 1) + 0.1}}) ) else: for field in view.search_fields: @@ -99,13 +106,20 @@ class CustomSearchFilterBackend(SearchFilterBackend): Q("match", **{k: v}) ) __queries.append( - Q('wildcard', **{k: f'*{search_term.lower()}*'}) + Q('wildcard', + **{k: { + 'value': f'*{search_term.lower()}*', + 'boost': v.get('boost', 1) + 0.1 + } + } + ) ) else: __queries.append( Q("match", **field_kwargs) ) __queries.append( - Q('wildcard', **{field: f'*{search_term.lower()}*'}) + Q('wildcard', **{field: {'value': f'*{search_term.lower()}*', + 'boost': field_kwargs[field].get('boost', 1) + 0.1}}) ) return __queries \ No newline at end of file From 280da2d7de322b892d7c996b24af3d0ce9d37dd5 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 20 Nov 2019 19:31:43 +0300 Subject: [PATCH 098/115] Estabishment_type field --- apps/establishment/serializers/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 0c183477..3f0ca169 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -241,7 +241,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): in_favorites = serializers.BooleanField(allow_null=True) tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') currency = CurrencySerializer() - type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) + establishment_type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') image = serializers.URLField(source='image_url', read_only=True) preview_image = serializers.URLField(source='preview_image_url', @@ -267,7 +267,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): 'address', 'tags', 'currency', - 'type', + 'establishment_type', 'subtypes', 'image', 'preview_image', From b60dd870f9896cf141b432d201cf352d5f97c55f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 20 Nov 2019 19:32:45 +0300 Subject: [PATCH 099/115] Estabishment_type field #2 --- apps/establishment/serializers/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 3f0ca169..2fd7d9eb 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -241,7 +241,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): in_favorites = serializers.BooleanField(allow_null=True) tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') currency = CurrencySerializer() - establishment_type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) + establishment_type = EstablishmentTypeBaseSerializer(read_only=True) subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') image = serializers.URLField(source='image_url', read_only=True) preview_image = serializers.URLField(source='preview_image_url', From 49940ebab4b04a37359ba5a967d9d547caaf7620 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 20 Nov 2019 19:41:20 +0300 Subject: [PATCH 100/115] Revert "Estabishment_type field #2" This reverts commit b60dd87 --- apps/establishment/serializers/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 2fd7d9eb..3f0ca169 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -241,7 +241,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): in_favorites = serializers.BooleanField(allow_null=True) tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') currency = CurrencySerializer() - establishment_type = EstablishmentTypeBaseSerializer(read_only=True) + establishment_type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') image = serializers.URLField(source='image_url', read_only=True) preview_image = serializers.URLField(source='preview_image_url', From 06b8582976a01fd302fb54497c466ae63620e812 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 20 Nov 2019 19:41:22 +0300 Subject: [PATCH 101/115] Revert "Estabishment_type field" This reverts commit 280da2d --- apps/establishment/serializers/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 3f0ca169..0c183477 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -241,7 +241,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): in_favorites = serializers.BooleanField(allow_null=True) tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') currency = CurrencySerializer() - establishment_type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) + type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') image = serializers.URLField(source='image_url', read_only=True) preview_image = serializers.URLField(source='preview_image_url', @@ -267,7 +267,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): 'address', 'tags', 'currency', - 'establishment_type', + 'type', 'subtypes', 'image', 'preview_image', From 2dc7f1e89de7e06e1916b4222aa49e5248310c34 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 20 Nov 2019 20:35:56 +0300 Subject: [PATCH 102/115] Boost the tempo --- apps/search_indexes/filters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 3ef74978..ab47ef84 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -74,7 +74,7 @@ class CustomSearchFilterBackend(SearchFilterBackend): Q('wildcard', **{k: { 'value': f'*{search_term.lower()}*', - 'boost': v.get('boost', 1) + 0.1 + 'boost': v.get('boost', 1) + 30 } } ) @@ -85,7 +85,7 @@ class CustomSearchFilterBackend(SearchFilterBackend): ) __queries.append( Q('wildcard', **{field: {'value': f'*{search_term.lower()}*', - 'boost': field_kwargs[field].get('boost', 1) + 0.1}}) + 'boost': field_kwargs[field].get('boost', 1) + 30}}) ) else: for field in view.search_fields: @@ -109,7 +109,7 @@ class CustomSearchFilterBackend(SearchFilterBackend): Q('wildcard', **{k: { 'value': f'*{search_term.lower()}*', - 'boost': v.get('boost', 1) + 0.1 + 'boost': v.get('boost', 1) + 30 } } ) @@ -120,6 +120,6 @@ class CustomSearchFilterBackend(SearchFilterBackend): ) __queries.append( Q('wildcard', **{field: {'value': f'*{search_term.lower()}*', - 'boost': field_kwargs[field].get('boost', 1) + 0.1}}) + 'boost': field_kwargs[field].get('boost', 1) + 30}}) ) return __queries \ No newline at end of file From 74c1b7069a22a3ce6c42f09a79622dbd617246bb Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Thu, 21 Nov 2019 00:23:17 +0300 Subject: [PATCH 103/115] fix product tags --- apps/product/management/commands/add_product_tag.py | 9 ++++----- project/settings/base.py | 2 +- project/settings/local.py | 10 +++++----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index dc54f35c..6377fcac 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -40,7 +40,6 @@ class Command(BaseCommand): TagCategory.objects.bulk_create(objects) self.stdout.write(self.style.WARNING(f'Add or get tag category objects.')) - def product_type_category_sql(self): with connections['legacy'].cursor() as cursor: cursor.execute(''' @@ -56,9 +55,9 @@ class Command(BaseCommand): def add_type_product_category(self): for c in tqdm(self.product_type_category_sql(), desc='Add type product category'): - type = ProductType.objects.get(index_name='wine') + type = ProductType.objects.get(index_name=ProductType.WINE) category = TagCategory.objects.get(index_name=c.tag_category) - if type and category not in type.tag_categories.all(): + if category not in type.tag_categories.all(): type.tag_categories.add(category) self.stdout.write(self.style.WARNING(f'Add type product category objects.')) @@ -116,7 +115,7 @@ class Command(BaseCommand): select DISTINCT m.product_id, - lower(trim(CONVERT(m.value USING utf8))) as tag_value, + trim(CONVERT(m.value USING utf8)) as tag_value, trim(CONVERT(v.key_name USING utf8)) as tag_category FROM product_metadata m JOIN product_key_value_metadata v on v.id = m.product_key_value_metadatum_id @@ -133,7 +132,7 @@ class Command(BaseCommand): ) product = Product.objects.get(old_id=t.product_id) for tag in tags: - if product not in tag.products.all(): + if tag not in product.tags.all(): product.tags.add(tag) self.stdout.write(self.style.WARNING(f'Add or get tag objects.')) diff --git a/project/settings/base.py b/project/settings/base.py index fba7f4b4..1772c2c8 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -503,4 +503,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'] -#ELASTICSEARCH_DSL_AUTOSYNC = False +ELASTICSEARCH_DSL_AUTOSYNC = False diff --git a/project/settings/local.py b/project/settings/local.py index 959e6149..3d0c9981 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, + # }, } } From 7b2e8374a0d66c9dcc78f5b9e42ff90b0c45dc27 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Thu, 21 Nov 2019 00:45:48 +0300 Subject: [PATCH 104/115] change settings --- project/settings/base.py | 2 +- project/settings/local.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/project/settings/base.py b/project/settings/base.py index 1772c2c8..0a20bdeb 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -503,4 +503,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'] -ELASTICSEARCH_DSL_AUTOSYNC = False +# ELASTICSEARCH_DSL_AUTOSYNC = False diff --git a/project/settings/local.py b/project/settings/local.py index 3d0c9981..959e6149 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, + }, } } From 009ecf1973d40b49f74e8bbc66c39e3dfe1928b6 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Thu, 21 Nov 2019 08:42:05 +0300 Subject: [PATCH 105/115] fix filter for tag_category --- apps/tag/filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 8d2343ec..7bd22ec2 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: + # queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False) queryset = queryset.by_product_type(value) return queryset From cad0f8df0a08170ea83cf76ea87b4e3e406553bf Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 21 Nov 2019 09:11:52 +0300 Subject: [PATCH 106/115] award test --- apps/main/serializers.py | 11 ++---- apps/main/tests.py | 3 -- apps/main/tests/__init__.py | 0 apps/main/tests/tests_back.py | 73 +++++++++++++++++++++++++++++++++++ apps/main/urls/back.py | 2 +- 5 files changed, 78 insertions(+), 11 deletions(-) delete mode 100644 apps/main/tests.py create mode 100644 apps/main/tests/__init__.py create mode 100644 apps/main/tests/tests_back.py diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 732cfbad..572aff31 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -145,19 +145,16 @@ class AwardSerializer(AwardBaseSerializer): fields = AwardBaseSerializer.Meta.fields + ['award_type', ] -class BackAwardSerializer(serializers.ModelSerializer): +class BackAwardSerializer(AwardBaseSerializer): """Award serializer.""" - title_translated = serializers.CharField(read_only=True, allow_null=True) - class Meta: model = models.Award - fields = [ - 'id', - 'title_translated', - 'vintage_year', + fields = AwardBaseSerializer.Meta.fields + [ 'award_type', 'state', + 'content_type', + 'object_id', ] diff --git a/apps/main/tests.py b/apps/main/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/main/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/main/tests/__init__.py b/apps/main/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/main/tests/tests_back.py b/apps/main/tests/tests_back.py new file mode 100644 index 00000000..e09c7b4b --- /dev/null +++ b/apps/main/tests/tests_back.py @@ -0,0 +1,73 @@ +from http.cookies import SimpleCookie + +from django.contrib.contenttypes.models import ContentType +from rest_framework import status +from rest_framework.test import APITestCase + +from account.models import User +from location.models import Country +from main.models import Award, AwardType + + +class AwardTestCase(APITestCase): + + def setUp(self): + self.user = User.objects.create_user( + username='alex', + email='alex@mail.com', + password='alex_password', + is_staff=True, + ) + + # get tokens + tokens = User.create_jwt_tokens(self.user) + self.client.cookies = SimpleCookie( + {'access_token': tokens.get('access_token'), + 'refresh_token': tokens.get('refresh_token')}) + + self.country_ru = Country.objects.create( + name={'en-GB': 'Russian'}, + code='RU', + ) + + self.content_type = ContentType.objects.get(app_label="establishment", model="establishment") + + self.award_type = AwardType.objects.create( + country=self.country_ru, + name="Test award type", + ) + + self.award = Award.objects.create( + award_type=self.award_type, + vintage_year='2017', + state=Award.PUBLISHED, + object_id=1, + content_type_id=1, + ) + + def test_award_CRUD(self): + response = self.client.get('/api/back/main/awards/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + 'award_type': self.award_type.pk, + 'state': 1, + 'object_id': 1, + 'content_type': 1, + } + + response = self.client.post('/api/back/main/awards/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/back/main/awards/{self.award.id}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'vintage_year': '2019' + } + + response = self.client.patch(f'/api/back/main/awards/{self.award.id}/', data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete(f'/api/back/main/awards/{self.award.id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 93debb5e..d92bddf8 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -7,5 +7,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('awards//', views.AwardRUDView.as_view(), name='awards-rud'), ] From 617552841fa5ff100c4fa3094f8f41da94e3c465 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 21 Nov 2019 09:58:29 +0300 Subject: [PATCH 107/115] refactored product, fix transfer and etc., see changelist --- apps/account/views/common.py | 2 +- apps/account/views/web.py | 2 +- apps/advertisement/views/back.py | 6 +-- apps/authorization/views/common.py | 10 ++-- apps/establishment/admin.py | 10 +++- .../0065_establishment_purchased_products.py | 19 +++++++ apps/establishment/models.py | 13 +++++ apps/establishment/transfer_data.py | 44 +++++++++++++++- apps/establishment/views/back.py | 27 ++++++---- apps/location/views/back.py | 4 +- apps/news/views.py | 8 +-- .../migrations/0018_purchasedproduct.py | 30 +++++++++++ apps/product/models.py | 29 ++++++++++- apps/product/views/back.py | 12 ++--- apps/timetable/serialziers.py | 6 +-- apps/transfer/management/commands/transfer.py | 1 + apps/transfer/models.py | 25 ++++----- apps/transfer/serializers/product.py | 2 +- apps/utils/views.py | 51 ++++++++++--------- 19 files changed, 220 insertions(+), 81 deletions(-) create mode 100644 apps/establishment/migrations/0065_establishment_purchased_products.py create mode 100644 apps/product/migrations/0018_purchasedproduct.py diff --git a/apps/account/views/common.py b/apps/account/views/common.py index d29ce2bb..8b066742 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -63,7 +63,7 @@ class SendConfirmationEmailView(generics.GenericAPIView): return Response(status=status.HTTP_200_OK) -class ConfirmEmailView(JWTGenericViewMixin): +class ConfirmEmailView(JWTGenericViewMixin, generics.GenericAPIView): """View for confirm changing email""" permission_classes = (permissions.AllowAny,) diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 9f2ebcfd..0fc762f5 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -33,7 +33,7 @@ class PasswordResetView(generics.GenericAPIView): return Response(status=status.HTTP_200_OK) -class PasswordResetConfirmView(JWTGenericViewMixin): +class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView): """View for confirmation new password""" serializer_class = serializers.PasswordResetConfirmSerializer permission_classes = (permissions.AllowAny,) diff --git a/apps/advertisement/views/back.py b/apps/advertisement/views/back.py index d11615ba..a2973589 100644 --- a/apps/advertisement/views/back.py +++ b/apps/advertisement/views/back.py @@ -45,7 +45,7 @@ class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, ad_qs = Advertisement.objects.all() filtered_ad_qs = self.filter_queryset(ad_qs) - ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs['pk']) + ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, ad) @@ -68,8 +68,8 @@ class AdvertisementPageRUDView(AdvertisementBackOfficeViewMixin, ad_qs = Advertisement.objects.all() filtered_ad_qs = self.filter_queryset(ad_qs) - ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs['ad_pk']) - page = get_object_or_404(ad.pages.all(), pk=self.kwargs['page_pk']) + ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('ad_pk')) + page = get_object_or_404(ad.pages.all(), pk=self.kwargs.get('page_pk')) # May raise a permission denied self.check_object_permissions(self.request, page) diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index 9d2069f2..8b466acb 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -71,7 +71,7 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin): # Sign in via Facebook -class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin): +class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin, generics.GenericAPIView): """ Implements an endpoint to convert a provider token to an access token @@ -142,7 +142,7 @@ class SignUpView(generics.GenericAPIView): return Response(status=status.HTTP_201_CREATED) -class ConfirmationEmailView(JWTGenericViewMixin): +class ConfirmationEmailView(JWTGenericViewMixin, generics.GenericAPIView): """View for confirmation email""" permission_classes = (permissions.AllowAny, ) @@ -174,7 +174,7 @@ class ConfirmationEmailView(JWTGenericViewMixin): # Login by username|email + password -class LoginByUsernameOrEmailView(JWTGenericViewMixin): +class LoginByUsernameOrEmailView(JWTGenericViewMixin, generics.GenericAPIView): """Login by email and password""" permission_classes = (permissions.AllowAny,) serializer_class = serializers.LoginByUsernameOrEmailSerializer @@ -197,7 +197,7 @@ class LoginByUsernameOrEmailView(JWTGenericViewMixin): # Logout -class LogoutView(JWTGenericViewMixin): +class LogoutView(JWTGenericViewMixin, generics.GenericAPIView): """Logout user""" permission_classes = (IsAuthenticatedAndTokenIsValid, ) @@ -215,7 +215,7 @@ class LogoutView(JWTGenericViewMixin): # Refresh token -class RefreshTokenView(JWTGenericViewMixin): +class RefreshTokenView(JWTGenericViewMixin, generics.GenericAPIView): """Refresh access_token""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.RefreshTokenSerializer diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index e6b0d991..45716f32 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -7,7 +7,7 @@ from comment.models import Comment from utils.admin import BaseModelAdminMixin from establishment import models from main.models import Award -from product.models import Product +from product.models import Product, PurchasedProduct from review import models as review_models @@ -69,13 +69,19 @@ class EstablishmentNote(admin.TabularInline): extra = 0 +class PurchasedProduct(admin.TabularInline): + model = PurchasedProduct + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Establishment admin.""" list_display = ['id', '__str__', 'image_tag', ] search_fields = ['id', 'name', 'index_name', 'slug'] list_filter = ['public_mark', 'toque_number'] - inlines = [GalleryImageInline, CompanyInline, EstablishmentNote] + inlines = [GalleryImageInline, CompanyInline, EstablishmentNote, + PurchasedProduct] # inlines = [ # AwardInline, ContactPhoneInline, ContactEmailInline, diff --git a/apps/establishment/migrations/0065_establishment_purchased_products.py b/apps/establishment/migrations/0065_establishment_purchased_products.py new file mode 100644 index 00000000..25088455 --- /dev/null +++ b/apps/establishment/migrations/0065_establishment_purchased_products.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-20 12:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0018_purchasedproduct'), + ('establishment', '0064_auto_20191119_1546'), + ] + + operations = [ + migrations.AddField( + model_name='establishment', + name='purchased_products', + field=models.ManyToManyField(blank=True, help_text='Attribute from legacy db.\nMust be deleted after the implementation of the market.', related_name='establishments', through='product.PurchasedProduct', to='product.Product', verbose_name='purchased plaques'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 3aee6c31..26046953 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -397,6 +397,13 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat currency = models.ForeignKey(Currency, blank=True, null=True, default=None, on_delete=models.PROTECT, verbose_name=_('currency')) + purchased_products = models.ManyToManyField('product.Product', blank=True, + through='product.PurchasedProduct', + related_name='establishments', + verbose_name=_('purchased plaques'), + help_text=_('Attribute from legacy db.\n' + 'Must be deleted after the ' + 'implementation of the market.')) objects = EstablishmentQuerySet.as_manager() @@ -409,6 +416,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat def __str__(self): return f'id:{self.id}-{self.name}' + def clean_fields(self, exclude=None): + super().clean_fields(exclude) + if self.purchased_products.filter(product_type__index_name='souvenir').exists(): + raise ValidationError( + _('Only souvenirs.')) + def delete(self, using=None, keep_parents=False): """Overridden delete method""" # Delete all related companies diff --git a/apps/establishment/transfer_data.py b/apps/establishment/transfer_data.py index b3dc58f9..741a9989 100644 --- a/apps/establishment/transfer_data.py +++ b/apps/establishment/transfer_data.py @@ -4,7 +4,9 @@ from django.db.models import Q, F from establishment.models import Establishment from location.models import Address -from transfer.models import Establishments, Dishes, EstablishmentNotes +from product.models import PurchasedProduct, Product +from transfer.models import Establishments, Dishes, EstablishmentNotes, \ + EstablishmentMerchandises from transfer.serializers.establishment import EstablishmentSerializer, \ EstablishmentNoteSerializer from transfer.serializers.plate import PlateSerializer @@ -140,6 +142,43 @@ def transfer_establishment_note(): pprint(f"transfer_establishment_note errors: {errors}") +def transfer_purchased_plaques(): + update_products_counter = 0 + already_updated_counter = 0 + not_existed_establishment_counter = 0 + + purchased = EstablishmentMerchandises.objects.values_list( + 'establishment_id', + 'merchandise__vintage', + 'gifted', + 'quantity' + ) + for old_est_id, vintage, gifted, quantity in purchased: + establishment_qs = Establishment.objects.filter(old_id=old_est_id) + product_qs = Product.objects.filter(name='Plaque restaurants', + vintage=vintage) + if establishment_qs.exists() and product_qs.exists(): + product = product_qs.first() + establishment = establishment_qs.first() + + purchases, created = PurchasedProduct.objects.get_or_create( + establishment=establishment, + product=product, + is_gifted=gifted, + quantity=quantity + ) + if created: + update_products_counter += 1 + else: + already_updated_counter += 1 + else: + not_existed_establishment_counter += 1 + + print(f'Updated products: {update_products_counter}\n' + f'Already updated: {already_updated_counter}\n' + f'Not existed establishment: {not_existed_establishment_counter}') + + data_types = { "establishment": [ transfer_establishment, @@ -149,4 +188,7 @@ data_types = { transfer_establishment_addresses ], "menu": [transfer_menu], + "purchased_plaques": [ + transfer_purchased_plaques + ], } diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index ad38e806..d1897397 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -6,6 +6,7 @@ from establishment import filters, models, serializers from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from utils.permissions import IsCountryAdmin, IsEstablishmentManager from utils.views import CreateDestroyGalleryViewMixin +from timetable.models import Timetable from rest_framework import status from rest_framework.response import Response @@ -36,13 +37,14 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): """Establishment schedule RUD view""" serializer_class = ScheduleRUDSerializer + permission_classes = [IsEstablishmentManager] def get_object(self): """ Returns the object the view is displaying. """ - establishment_pk = self.kwargs['pk'] - schedule_id = self.kwargs['schedule_id'] + establishment_pk = self.kwargs.get('pk') + schedule_id = self.kwargs.get('schedule_id') establishment = get_object_or_404(klass=models.Establishment.objects.all(), pk=establishment_pk) @@ -59,6 +61,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentScheduleCreateView(generics.CreateAPIView): """Establishment schedule Create view""" serializer_class = ScheduleCreateSerializer + queryset = Timetable.objects.all() + permission_classes = [IsEstablishmentManager] class MenuListCreateView(generics.ListCreateAPIView): @@ -200,8 +204,9 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, """ establishment_qs = self.filter_queryset(self.get_queryset()) - establishment = get_object_or_404(establishment_qs, pk=self.kwargs['pk']) - gallery = get_object_or_404(establishment.establishment_gallery, image_id=self.kwargs['image_id']) + establishment = get_object_or_404(establishment_qs, pk=self.kwargs.get('pk')) + gallery = get_object_or_404(establishment.establishment_gallery, + image_id=self.kwargs.get('image_id')) # May raise a permission denied self.check_object_permissions(self.request, gallery) @@ -217,7 +222,7 @@ class EstablishmentGalleryListView(EstablishmentMixinViews, def get_object(self): """Override get_object method.""" qs = super(EstablishmentGalleryListView, self).get_queryset() - establishment = get_object_or_404(qs, pk=self.kwargs['pk']) + establishment = get_object_or_404(qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -240,7 +245,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['pk']) + establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -263,8 +268,8 @@ 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['pk']) - company = get_object_or_404(establishment.companies.all(), pk=self.kwargs['company_pk']) + establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk')) + company = get_object_or_404(establishment.companies.all(), pk=self.kwargs.get('company_pk')) # May raise a permission denied self.check_object_permissions(self.request, company) @@ -273,7 +278,7 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews, class EstablishmentNoteListCreateView(EstablishmentMixinViews, - generics.ListCreateAPIView): + generics.ListCreateAPIView): """Retrieve|Update|Destroy establishment note view.""" serializer_class = serializers.EstablishmentNoteListCreateSerializer @@ -283,7 +288,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['pk']) + establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -306,7 +311,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['pk']) + establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk')) note = get_object_or_404(establishment.notes.all(), pk=self.kwargs['note_pk']) # May raise a permission denied diff --git a/apps/location/views/back.py b/apps/location/views/back.py index b6677837..4d420154 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -51,8 +51,8 @@ class CityGalleryCreateDestroyView(common.CityViewMixin, """ city_qs = self.filter_queryset(self.get_queryset()) - city = get_object_or_404(city_qs, pk=self.kwargs['pk']) - gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs['image_id']) + city = get_object_or_404(city_qs, pk=self.kwargs.get('pk')) + gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs.get('image_id')) # May raise a permission denied self.check_object_permissions(self.request, gallery) diff --git a/apps/news/views.py b/apps/news/views.py index 3e841246..638f208b 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -108,8 +108,8 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, """ news_qs = self.filter_queryset(self.get_queryset()) - news = get_object_or_404(news_qs, pk=self.kwargs['pk']) - gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs['image_id']) + news = get_object_or_404(news_qs, pk=self.kwargs.get('pk')) + gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs.get('image_id')) # May raise a permission denied self.check_object_permissions(self.request, gallery) @@ -125,7 +125,7 @@ class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, def get_object(self): """Override get_object method.""" qs = super(NewsBackOfficeGalleryListView, self).get_queryset() - news = get_object_or_404(qs, pk=self.kwargs['pk']) + news = get_object_or_404(qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, news) @@ -160,7 +160,7 @@ class NewsFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPI """ Returns the object the view is displaying. """ - news = get_object_or_404(models.News, slug=self.kwargs['slug']) + news = get_object_or_404(models.News, slug=self.kwargs.get('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) diff --git a/apps/product/migrations/0018_purchasedproduct.py b/apps/product/migrations/0018_purchasedproduct.py new file mode 100644 index 00000000..b1c23646 --- /dev/null +++ b/apps/product/migrations/0018_purchasedproduct.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.7 on 2019-11-20 12:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0064_auto_20191119_1546'), + ('product', '0017_auto_20191119_1546'), + ] + + operations = [ + migrations.CreateModel( + name='PurchasedProduct', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_gifted', models.NullBooleanField(default=None, verbose_name='is gifted')), + ('quantity', models.PositiveSmallIntegerField(verbose_name='quantity')), + ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchased_plaques', to='establishment.Establishment', verbose_name='establishment')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchased_by_establishments', to='product.Product', verbose_name='plaque')), + ], + options={ + 'verbose_name': 'purchased plaque', + 'verbose_name_plural': 'purchased plaques', + 'unique_together': {('establishment', 'product')}, + }, + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 6baaf43e..e562e4a3 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -263,12 +263,17 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsM def grape_variety(self): return self.tags.filter(category__index_name='grape-variety') + @property + def bottle_sizes(self): + return self.tags.filter(category__index_name='bottle_size') + @property def related_tags(self): return super().visible_tags.exclude(category__index_name__in=[ 'sugar-content', 'wine-color', 'bottles-produced', - 'serial-number', 'grape-variety'] - ) + 'serial-number', 'grape-variety', 'serial_number', + 'alcohol_percentage', 'bottle_size', + ]) @property def display_name(self): @@ -315,6 +320,26 @@ class OnlineProduct(Product): verbose_name_plural = _('Online products') +class PurchasedProduct(models.Model): + """Model for storing establishment purchased plaques.""" + + establishment = models.ForeignKey('establishment.Establishment', on_delete=models.CASCADE, + related_name='purchased_plaques', + verbose_name=_('establishment')) + product = models.ForeignKey('product.Product', on_delete=models.CASCADE, + related_name='purchased_by_establishments', + verbose_name=_('plaque')) + is_gifted = models.NullBooleanField(default=None, + verbose_name=_('is gifted')) + quantity = models.PositiveSmallIntegerField(verbose_name=_('quantity')) + + class Meta: + """Meta class.""" + verbose_name = _('purchased plaque') + verbose_name_plural = _('purchased plaques') + unique_together = ('establishment', 'product') + + class Unit(models.Model): """Product unit model.""" name = models.CharField(max_length=255, diff --git a/apps/product/views/back.py b/apps/product/views/back.py index ac780849..fc5e108f 100644 --- a/apps/product/views/back.py +++ b/apps/product/views/back.py @@ -57,8 +57,8 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, """ product_qs = self.filter_queryset(self.get_queryset()) - product = get_object_or_404(product_qs, pk=self.kwargs['pk']) - gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs['image_id']) + product = get_object_or_404(product_qs, pk=self.kwargs.get('pk')) + gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs.get('image_id')) # May raise a permission denied self.check_object_permissions(self.request, gallery) @@ -75,7 +75,7 @@ class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, def get_object(self): """Override get_object method.""" qs = super(ProductBackOfficeGalleryListView, self).get_queryset() - product = get_object_or_404(qs, pk=self.kwargs['pk']) + product = get_object_or_404(qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, product) @@ -149,7 +149,7 @@ class ProductNoteListCreateView(ProductBackOfficeMixinView, product_qs = models.Product.objects.all() filtered_product_qs = self.filter_queryset(product_qs) - product = get_object_or_404(filtered_product_qs, pk=self.kwargs['pk']) + product = get_object_or_404(filtered_product_qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, product) @@ -173,8 +173,8 @@ class ProductNoteRUDView(ProductBackOfficeMixinView, product_qs = models.Product.objects.all() filtered_product_qs = self.filter_queryset(product_qs) - product = get_object_or_404(filtered_product_qs, pk=self.kwargs['pk']) - note = get_object_or_404(product.notes.all(), pk=self.kwargs['note_pk']) + product = get_object_or_404(filtered_product_qs, pk=self.kwargs.get('pk')) + note = get_object_or_404(product.notes.all(), pk=self.kwargs.get('note_pk')) # May raise a permission denied self.check_object_permissions(self.request, note) diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index 37725e1d..533bca70 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -8,6 +8,9 @@ from timetable.models import Timetable class ScheduleRUDSerializer(serializers.ModelSerializer): """Serializer for Establishment model.""" + NULLABLE_FIELDS = ['lunch_start', 'lunch_end', 'dinner_start', + 'dinner_end', 'opening_at', 'closed_at'] + weekday_display = serializers.CharField(source='get_weekday_display', read_only=True) @@ -18,9 +21,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): opening_at = serializers.TimeField(required=False) closed_at = serializers.TimeField(required=False) - NULLABLE_FIELDS = ['lunch_start', 'lunch_end', 'dinner_start', - 'dinner_end', 'opening_at', 'closed_at'] - class Meta: """Meta class.""" model = Timetable diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index d69dd908..9126856f 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -39,6 +39,7 @@ class Command(BaseCommand): 'rating_count', 'product_review', 'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1 + 'purchased_plaques', # №6 - перенос купленных тарелок ] def handle(self, *args, **options): diff --git a/apps/transfer/models.py b/apps/transfer/models.py index d8be36ce..f4a1a800 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -581,22 +581,19 @@ class EstablishmentInfos(MigrateMixin): db_table = 'establishment_infos' -# class EstablishmentMerchandises(MigrateMixin): -# using = 'legacy' -# -# establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True) +class EstablishmentMerchandises(MigrateMixin): + using = 'legacy' -# TODO: модели Merchandises нету в гугл таблице Check Migrations + establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True) + merchandise = models.ForeignKey('Merchandise', models.DO_NOTHING, blank=True, null=True) + gifted = models.NullBooleanField(blank=True, null=True) + quantity = models.IntegerField(blank=True, null=True) + created_at = models.DateTimeField() + updated_at = models.DateTimeField() -# merchandise = models.ForeignKey('Merchandises', models.DO_NOTHING, blank=True, null=True) -# gifted = models.IntegerField(blank=True, null=True) -# quantity = models.IntegerField(blank=True, null=True) -# created_at = models.DateTimeField() -# updated_at = models.DateTimeField() -# -# class Meta: -# managed = False -# db_table = 'establishment_merchandises' + class Meta: + managed = False + db_table = 'establishment_merchandises' class Menus(MigrateMixin): diff --git a/apps/transfer/serializers/product.py b/apps/transfer/serializers/product.py index a0f3ef8a..86c6720a 100644 --- a/apps/transfer/serializers/product.py +++ b/apps/transfer/serializers/product.py @@ -265,7 +265,7 @@ class ProductSerializer(TransferSerializerMixin): state = serializers.CharField() bottles_produced = serializers.CharField(allow_null=True, allow_blank=True) unique_key = serializers.CharField(allow_null=True) - price = serializers.DecimalField(max_digits=14, decimal_places=2) + price = serializers.DecimalField(max_digits=14, decimal_places=2, allow_null=True) class Meta: model = models.Product diff --git a/apps/utils/views.py b/apps/utils/views.py index 870e132f..a8580f59 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -12,7 +12,7 @@ from gallery.tasks import delete_image # JWT # Login base view mixins -class JWTGenericViewMixin(generics.GenericAPIView): +class JWTGenericViewMixin: """JWT view mixin""" ACCESS_TOKEN_HTTP_ONLY = False @@ -39,30 +39,31 @@ class JWTGenericViewMixin(generics.GenericAPIView): """ COOKIES = [] - if hasattr(self.request, 'locale'): - COOKIES.append(self.COOKIE(key='locale', - value=self.request.locale, - http_only=self.ACCESS_TOKEN_HTTP_ONLY, - secure=self.LOCALE_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None)) - if hasattr(self.request, 'country_code'): - COOKIES.append(self.COOKIE(key='country_code', - value=self.request.country_code, - http_only=self.COUNTRY_CODE_HTTP_ONLY, - secure=self.COUNTRY_CODE_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None)) - if access_token: - COOKIES.append(self.COOKIE(key='access_token', - value=access_token, - http_only=self.ACCESS_TOKEN_HTTP_ONLY, - secure=self.ACCESS_TOKEN_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None)) - if refresh_token: - COOKIES.append(self.COOKIE(key='refresh_token', - value=refresh_token, - http_only=self.REFRESH_TOKEN_HTTP_ONLY, - secure=self.REFRESH_TOKEN_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if hasattr(self, 'request'): + if hasattr(self.request, 'locale'): + COOKIES.append(self.COOKIE(key='locale', + value=self.request.locale, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, + secure=self.LOCALE_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if hasattr(self.request, 'country_code'): + COOKIES.append(self.COOKIE(key='country_code', + value=self.request.country_code, + http_only=self.COUNTRY_CODE_HTTP_ONLY, + secure=self.COUNTRY_CODE_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if access_token: + COOKIES.append(self.COOKIE(key='access_token', + value=access_token, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, + secure=self.ACCESS_TOKEN_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if refresh_token: + COOKIES.append(self.COOKIE(key='refresh_token', + value=refresh_token, + http_only=self.REFRESH_TOKEN_HTTP_ONLY, + secure=self.REFRESH_TOKEN_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) return COOKIES def _put_cookies_in_response(self, cookies: list, response: Response): From e303e15f32313bc24c5d5fece870d389a7730c8f Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 21 Nov 2019 10:28:17 +0300 Subject: [PATCH 108/115] added two characteristics to wine detail serializer --- apps/product/models.py | 6 ++++++ apps/product/serializers/common.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/apps/product/models.py b/apps/product/models.py index e562e4a3..f499afee 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -267,6 +267,12 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsM def bottle_sizes(self): return self.tags.filter(category__index_name='bottle_size') + @property + def alcohol_percentage(self): + qs = self.tags.filter(category__index_name='alcohol_percentage') + if qs.exists(): + return qs.first() + @property def related_tags(self): return super().visible_tags.exclude(category__index_name__in=[ diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index a0a56337..14eff642 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -128,6 +128,8 @@ class ProductDetailSerializer(ProductBaseSerializer): bottles_produced = TagBaseSerializer(many=True, read_only=True) sugar_contents = TagBaseSerializer(many=True, read_only=True) grape_variety = TagBaseSerializer(many=True, read_only=True) + bottle_sizes = TagBaseSerializer(many=True, read_only=True) + alcohol_percentage = TagBaseSerializer(read_only=True) image_url = serializers.URLField(allow_null=True, read_only=True) new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) @@ -146,6 +148,8 @@ class ProductDetailSerializer(ProductBaseSerializer): 'new_image', 'grape_variety', 'average_price', + 'bottle_sizes', + 'alcohol_percentage', ] From bebdf1e269ac45a690be942cc832a0bd6541e5d0 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 21 Nov 2019 11:12:52 +0300 Subject: [PATCH 109/115] review test and fixes review crud api --- apps/review/serializers/common.py | 4 ++- apps/review/tests.py | 44 +++++++++++++++++++++++++++++++ apps/review/views/back.py | 2 +- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/apps/review/serializers/common.py b/apps/review/serializers/common.py index 2889db6e..da7b624b 100644 --- a/apps/review/serializers/common.py +++ b/apps/review/serializers/common.py @@ -14,7 +14,9 @@ class ReviewBaseSerializer(serializers.ModelSerializer): 'child', 'published_at', 'vintage', - 'country' + 'country', + 'content_type', + 'object_id', ) diff --git a/apps/review/tests.py b/apps/review/tests.py index e04f5281..eebc1e86 100644 --- a/apps/review/tests.py +++ b/apps/review/tests.py @@ -19,6 +19,7 @@ class BaseTestCase(APITestCase): username=self.username, email=self.email, password=self.password, + is_staff=True, ) tokens = User.create_jwt_tokens(self.user) @@ -61,6 +62,49 @@ class BaseTestCase(APITestCase): ) +class ReviewTestCase(BaseTestCase): + def setUp(self): + super().setUp() + + def test_review_list(self): + response = self.client.get('/api/back/review/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_review_post(self): + test_review = { + 'reviewer': self.user.id, + 'status': Review.READY, + 'vintage': 2019, + 'country': self.country_ru.id, + 'object_id': 1, + 'content_type': 1, + } + response = self.client.post('/api/back/review/', data=test_review) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_review_detail(self): + response = self.client.get(f'/api/back/review/{self.test_review.id}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_review_detail_put(self): + data = { + 'id': self.test_review.id, + 'vintage': 2018, + 'reviewer': self.user.id, + 'status': Review.READY, + 'country': self.country_ru.id, + 'object_id': 1, + 'content_type': 1, + } + + response = self.client.put(f'/api/back/review/{self.test_review.id}/', data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_review_delete(self): + response = self.client.delete(f'/api/back/review/{self.test_review.id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + class InquiriesTestCase(BaseTestCase): def setUp(self): super().setUp() diff --git a/apps/review/views/back.py b/apps/review/views/back.py index c5d89e11..27f9af0d 100644 --- a/apps/review/views/back.py +++ b/apps/review/views/back.py @@ -18,7 +18,7 @@ class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView): """Comment RUD view.""" serializer_class = serializers.ReviewBaseSerializer queryset = models.Review.objects.all() - permission_classes = [IsReviewerManager | IsRestaurantReviewer] + permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer] lookup_field = 'id' From 98468f26041fa4cf2018d51b2d06874f3fa66a05 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: Thu, 21 Nov 2019 11:16:02 +0300 Subject: [PATCH 110/115] Fix ELASTICSEARCH_DSL_AUTOSYNCOC --- project/settings/base.py | 1 - project/settings/development.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/project/settings/base.py b/project/settings/base.py index 0a20bdeb..4299c379 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -503,4 +503,3 @@ ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] -# ELASTICSEARCH_DSL_AUTOSYNC = False diff --git a/project/settings/development.py b/project/settings/development.py index 669bc485..06f1199b 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -33,6 +33,7 @@ ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.product': 'development_product', } +# ELASTICSEARCH_DSL_AUTOSYNC = False sentry_sdk.init( dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093", From e6a143f6ea59225e27734852f5530898f5aaed7a Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 21 Nov 2019 12:29:55 +0300 Subject: [PATCH 111/115] partner model --- apps/partner/admin.py | 1 + .../migrations/0003_auto_20191121_0929.py | 46 +++++++++++++++++++ apps/partner/models.py | 20 ++++++++ 3 files changed, 67 insertions(+) create mode 100644 apps/partner/migrations/0003_auto_20191121_0929.py diff --git a/apps/partner/admin.py b/apps/partner/admin.py index f2973f75..9161837b 100644 --- a/apps/partner/admin.py +++ b/apps/partner/admin.py @@ -6,3 +6,4 @@ from partner import models @admin.register(models.Partner) class PartnerModelAdmin(admin.ModelAdmin): """Model admin for Partner model.""" + raw_id_fields = ('establishment',) diff --git a/apps/partner/migrations/0003_auto_20191121_0929.py b/apps/partner/migrations/0003_auto_20191121_0929.py new file mode 100644 index 00000000..526e14ce --- /dev/null +++ b/apps/partner/migrations/0003_auto_20191121_0929.py @@ -0,0 +1,46 @@ +# Generated by Django 2.2.7 on 2019-11-21 09:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0065_establishment_purchased_products'), + ('partner', '0002_auto_20191101_0939'), + ] + + operations = [ + migrations.AddField( + model_name='partner', + name='establishment', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='partners', to='establishment.Establishment', verbose_name='Establishment'), + preserve_default=False, + ), + migrations.AddField( + model_name='partner', + name='expiry_date', + field=models.DateField(blank=True, null=True, verbose_name='expiry date'), + ), + migrations.AddField( + model_name='partner', + name='old_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'), + ), + migrations.AddField( + model_name='partner', + name='price_per_month', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='price per month'), + ), + migrations.AddField( + model_name='partner', + name='starting_date', + field=models.DateField(blank=True, null=True, verbose_name='starting date'), + ), + migrations.AddField( + model_name='partner', + name='type', + field=models.PositiveSmallIntegerField(choices=[(0, 'Partner'), (1, 'Sponsor')], default=0), + ), + ] diff --git a/apps/partner/models.py b/apps/partner/models.py index 1eb70845..61d13023 100644 --- a/apps/partner/models.py +++ b/apps/partner/models.py @@ -1,13 +1,33 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from establishment.models import Establishment from utils.models import ImageMixin, ProjectBaseMixin class Partner(ProjectBaseMixin): """Partner model.""" + + PARTNER = 0 + SPONSOR = 1 + MODEL_TYPES = ( + (PARTNER, _('Partner')), + (SPONSOR, _('Sponsor')), + ) + + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) url = models.URLField(verbose_name=_('Partner URL')) image = models.URLField(verbose_name=_('Partner image URL'), null=True) + establishment = models.ForeignKey( + Establishment, + verbose_name=_('Establishment'), + related_name='partners', + on_delete=models.CASCADE, + ) + type = models.PositiveSmallIntegerField(choices=MODEL_TYPES, default=PARTNER) + starting_date = models.DateField(_('starting date'), blank=True, null=True) + expiry_date = models.DateField(_('expiry date'), blank=True, null=True) + price_per_month = models.DecimalField(_('price per month'), max_digits=10, decimal_places=2, blank=True, null=True) class Meta: verbose_name = _('partner') From 3f164459a7d9ce59032c5ae50c3d33e87f2b1924 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 21 Nov 2019 12:37:05 +0300 Subject: [PATCH 112/115] added transfer for filling city gallery --- apps/location/transfer_data.py | 41 ++++++++++++++++++- apps/transfer/management/commands/transfer.py | 1 + apps/transfer/models.py | 2 +- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/apps/location/transfer_data.py b/apps/location/transfer_data.py index 0d90461a..658d2d20 100644 --- a/apps/location/transfer_data.py +++ b/apps/location/transfer_data.py @@ -1,8 +1,8 @@ from transfer.serializers import location as location_serializers from transfer import models as transfer_models -from location.models import Country +from location.models import Country, CityGallery, City +from gallery.models import Image from pprint import pprint - from requests import get @@ -179,6 +179,42 @@ def update_flags(): query.save() +def transfer_city_gallery(): + created_counter = 0 + cities_not_exists = {} + gallery_obj_exists_counter = 0 + + city_gallery = transfer_models.CityPhotos.objects.exclude(city__isnull=True) \ + .exclude(city__country_code_2__isnull=True) \ + .exclude(city__country_code_2__iexact='') \ + .exclude(city__region_code__isnull=True) \ + .exclude(city__region_code__iexact='') \ + .values_list('city_id', 'attachment_suffix_url') + for old_city_id, image_suffix_url in city_gallery: + city = City.objects.filter(old_id=old_city_id) + if city.exists(): + city = city.first() + image, _ = Image.objects.get_or_create(image=image_suffix_url, + defaults={ + 'image': image_suffix_url, + 'orientation': Image.HORIZONTAL, + 'title': f'{city.name} - {image_suffix_url}', + }) + city_gallery, created = CityGallery.objects.get_or_create(image=image, + city=city, + is_main=True) + if created: + created_counter += 1 + else: + gallery_obj_exists_counter += 1 + else: + cities_not_exists.update({'city_old_id': old_city_id}) + + print(f'Created: {created_counter}\n' + f'City not exists: {cities_not_exists}\n' + f'Already added: {gallery_obj_exists_counter}') + + data_types = { "dictionaries": [ transfer_countries, @@ -192,4 +228,5 @@ data_types = { "update_country_flag": [ update_flags ], + "fill_city_gallery": [transfer_city_gallery] } diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index 9126856f..2d0ce399 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -40,6 +40,7 @@ class Command(BaseCommand): 'product_review', 'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1 'purchased_plaques', # №6 - перенос купленных тарелок + 'fill_city_gallery', # №3 - перенос галереи городов ] def handle(self, *args, **options): diff --git a/apps/transfer/models.py b/apps/transfer/models.py index f4a1a800..a8190879 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -217,10 +217,10 @@ class CityNames(MigrateMixin): class CityPhotos(MigrateMixin): using = 'legacy' - # city_id = models.IntegerField(blank=True, null=True) city = models.ForeignKey(Cities, models.DO_NOTHING, blank=True, null=True) attachment_file_name = models.CharField(max_length=255, blank=True, null=True) attachment_content_type = models.CharField(max_length=255, blank=True, null=True) + attachment_suffix_url = models.CharField(max_length=255) geometries = models.CharField(max_length=1024, blank=True, null=True) attachment_file_size = models.IntegerField(blank=True, null=True) attachment_updated_at = models.DateTimeField(blank=True, null=True) From d9b88f7ef96ef013611c9d0608f13229ed305dc5 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 21 Nov 2019 13:32:50 +0300 Subject: [PATCH 113/115] partner name field --- apps/partner/migrations/0004_partner_name.py | 18 ++++++++++++++++++ apps/partner/models.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 apps/partner/migrations/0004_partner_name.py diff --git a/apps/partner/migrations/0004_partner_name.py b/apps/partner/migrations/0004_partner_name.py new file mode 100644 index 00000000..ef9ac8e8 --- /dev/null +++ b/apps/partner/migrations/0004_partner_name.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-21 10:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partner', '0003_auto_20191121_0929'), + ] + + operations = [ + migrations.AddField( + model_name='partner', + name='name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='name'), + ), + ] diff --git a/apps/partner/models.py b/apps/partner/models.py index 61d13023..c4af1de5 100644 --- a/apps/partner/models.py +++ b/apps/partner/models.py @@ -16,6 +16,7 @@ class Partner(ProjectBaseMixin): ) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) + name = models.CharField(_('name'), max_length=255, blank=True, null=True) url = models.URLField(verbose_name=_('Partner URL')) image = models.URLField(verbose_name=_('Partner image URL'), null=True) establishment = models.ForeignKey( From a6bfb3ea6b49ed28c66afae2e5d54ab53e6d226a Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 21 Nov 2019 13:47:37 +0300 Subject: [PATCH 114/115] partner migrations --- ...121_0929.py => 0003_auto_20191121_1045.py} | 10 +++++++--- .../migrations/0004_auto_20191121_1045.py | 19 +++++++++++++++++++ apps/partner/migrations/0004_partner_name.py | 18 ------------------ apps/partner/transfer_data.py | 16 ++++++++++++++-- 4 files changed, 40 insertions(+), 23 deletions(-) rename apps/partner/migrations/{0003_auto_20191121_0929.py => 0003_auto_20191121_1045.py} (76%) create mode 100644 apps/partner/migrations/0004_auto_20191121_1045.py delete mode 100644 apps/partner/migrations/0004_partner_name.py diff --git a/apps/partner/migrations/0003_auto_20191121_0929.py b/apps/partner/migrations/0003_auto_20191121_1045.py similarity index 76% rename from apps/partner/migrations/0003_auto_20191121_0929.py rename to apps/partner/migrations/0003_auto_20191121_1045.py index 526e14ce..6d548091 100644 --- a/apps/partner/migrations/0003_auto_20191121_0929.py +++ b/apps/partner/migrations/0003_auto_20191121_1045.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.7 on 2019-11-21 09:29 +# Generated by Django 2.2.7 on 2019-11-21 10:45 from django.db import migrations, models import django.db.models.deletion @@ -15,14 +15,18 @@ class Migration(migrations.Migration): migrations.AddField( model_name='partner', name='establishment', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='partners', to='establishment.Establishment', verbose_name='Establishment'), - preserve_default=False, + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partners', to='establishment.Establishment', verbose_name='Establishment'), ), migrations.AddField( model_name='partner', name='expiry_date', field=models.DateField(blank=True, null=True, verbose_name='expiry date'), ), + migrations.AddField( + model_name='partner', + name='name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='name'), + ), migrations.AddField( model_name='partner', name='old_id', diff --git a/apps/partner/migrations/0004_auto_20191121_1045.py b/apps/partner/migrations/0004_auto_20191121_1045.py new file mode 100644 index 00000000..1906a8c0 --- /dev/null +++ b/apps/partner/migrations/0004_auto_20191121_1045.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-21 10:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('partner', '0003_auto_20191121_1045'), + ] + + operations = [ + migrations.AlterField( + model_name='partner', + name='establishment', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='partners', to='establishment.Establishment', verbose_name='Establishment'), + ), + ] diff --git a/apps/partner/migrations/0004_partner_name.py b/apps/partner/migrations/0004_partner_name.py deleted file mode 100644 index ef9ac8e8..00000000 --- a/apps/partner/migrations/0004_partner_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.7 on 2019-11-21 10:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('partner', '0003_auto_20191121_0929'), - ] - - operations = [ - migrations.AddField( - model_name='partner', - name='name', - field=models.CharField(blank=True, max_length=255, null=True, verbose_name='name'), - ), - ] diff --git a/apps/partner/transfer_data.py b/apps/partner/transfer_data.py index e3219838..f373427a 100644 --- a/apps/partner/transfer_data.py +++ b/apps/partner/transfer_data.py @@ -1,11 +1,23 @@ -from django.db.models import Value, IntegerField, F from pprint import pprint + +from establishment.models import Establishment from transfer.models import EstablishmentBacklinks from transfer.serializers.partner import PartnerSerializer def transfer_partner(): - queryset = EstablishmentBacklinks.objects.filter(type="Partner") + establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) + queryset = EstablishmentBacklinks.objects.filter( + establishment_id__in=list(establishments), + ).values( + 'id', + 'establishment_id', + # 'aasm_state', + # 'created_at', + # 'establishment_id', + # 'mark', + # 'vintage', + ) # queryset = EstablishmentBacklinks.objects.all() # Partner and Sponsor From 9156d9cc35e5d9e13bd7c3c8239f412b0c3ae8a5 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 21 Nov 2019 14:00:17 +0300 Subject: [PATCH 115/115] fix partner migrations --- ...121_1045.py => 0003_auto_20191121_1059.py} | 2 +- .../migrations/0004_auto_20191121_1045.py | 19 --------- apps/partner/models.py | 2 + apps/partner/transfer_data.py | 19 ++++----- apps/transfer/serializers/partner.py | 39 ++++++++++--------- 5 files changed, 33 insertions(+), 48 deletions(-) rename apps/partner/migrations/{0003_auto_20191121_1045.py => 0003_auto_20191121_1059.py} (97%) delete mode 100644 apps/partner/migrations/0004_auto_20191121_1045.py diff --git a/apps/partner/migrations/0003_auto_20191121_1045.py b/apps/partner/migrations/0003_auto_20191121_1059.py similarity index 97% rename from apps/partner/migrations/0003_auto_20191121_1045.py rename to apps/partner/migrations/0003_auto_20191121_1059.py index 6d548091..c2e98fbe 100644 --- a/apps/partner/migrations/0003_auto_20191121_1045.py +++ b/apps/partner/migrations/0003_auto_20191121_1059.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.7 on 2019-11-21 10:45 +# Generated by Django 2.2.7 on 2019-11-21 10:59 from django.db import migrations, models import django.db.models.deletion diff --git a/apps/partner/migrations/0004_auto_20191121_1045.py b/apps/partner/migrations/0004_auto_20191121_1045.py deleted file mode 100644 index 1906a8c0..00000000 --- a/apps/partner/migrations/0004_auto_20191121_1045.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.7 on 2019-11-21 10:45 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('partner', '0003_auto_20191121_1045'), - ] - - operations = [ - migrations.AlterField( - model_name='partner', - name='establishment', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='partners', to='establishment.Establishment', verbose_name='Establishment'), - ), - ] diff --git a/apps/partner/models.py b/apps/partner/models.py index c4af1de5..5c5766c8 100644 --- a/apps/partner/models.py +++ b/apps/partner/models.py @@ -24,6 +24,8 @@ class Partner(ProjectBaseMixin): verbose_name=_('Establishment'), related_name='partners', on_delete=models.CASCADE, + blank=True, + null=True, ) type = models.PositiveSmallIntegerField(choices=MODEL_TYPES, default=PARTNER) starting_date = models.DateField(_('starting date'), blank=True, null=True) diff --git a/apps/partner/transfer_data.py b/apps/partner/transfer_data.py index f373427a..868345a8 100644 --- a/apps/partner/transfer_data.py +++ b/apps/partner/transfer_data.py @@ -12,20 +12,21 @@ def transfer_partner(): ).values( 'id', 'establishment_id', - # 'aasm_state', - # 'created_at', - # 'establishment_id', - # 'mark', - # 'vintage', + 'partnership_name', + 'partnership_icon', + 'backlink_url', + 'created_at', + 'type', + 'starting_date', + 'expiry_date', + 'price_per_month', ) - # queryset = EstablishmentBacklinks.objects.all() # Partner and Sponsor - - serialized_data = PartnerSerializer(data=list(queryset.values()), many=True) + serialized_data = PartnerSerializer(data=list(queryset), many=True) if serialized_data.is_valid(): serialized_data.save() else: - pprint(f"News serializer errors: {serialized_data.errors}") + pprint(f"Partner serializer errors: {serialized_data.errors}") data_types = { diff --git a/apps/transfer/serializers/partner.py b/apps/transfer/serializers/partner.py index 7de61486..69cf308e 100644 --- a/apps/transfer/serializers/partner.py +++ b/apps/transfer/serializers/partner.py @@ -2,27 +2,28 @@ from rest_framework import serializers from partner.models import Partner -class PartnerSerializer(serializers.ModelSerializer): - backlink_url = serializers.CharField(source="url") - partnership_icon = serializers.CharField() - partnership_name = serializers.CharField() +class PartnerSerializer(serializers.Serializer): + pass + # 'id', + # 'establishment_id', + # 'partnership_name', + # 'partnership_icon', + # 'backlink_url', + # 'created_at', + # 'type', + # 'starting_date', + # 'expiry_date', + # 'price_per_month', - class Meta: - model = Partner - fields = ( - "backlink_url", - "partnership_icon", - "partnership_name" - ) - 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) + # 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) partnership_to_image_url = {