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/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 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..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 @@ -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) @@ -191,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.""" @@ -316,6 +340,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 +409,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): 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/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.""" 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/product/models.py b/apps/product/models.py index ba9ccd38..44c57057 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') @@ -270,15 +275,14 @@ 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): 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 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/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py index b00f3f50..cb0131cb 100644 --- a/apps/search_indexes/documents/product.py +++ b/apps/search_indexes/documents/product.py @@ -95,13 +95,15 @@ class ProductDocument(Document): }, 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') class Django: model = models.Product fields = ( 'id', 'category', - 'name', 'available', 'public_mark', 'slug', diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index dffd6e77..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,""" @@ -79,6 +83,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.""" @@ -199,16 +215,12 @@ 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 = 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..174b1dd8 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -91,8 +91,6 @@ class EstablishmentDocumentViewSet(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 = ( @@ -199,7 +197,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,19 +210,22 @@ class ProductDocumentViewSet(BaseDocumentViewSet): filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, - GeoSpatialFilteringFilterBackend, - DefaultOrderingFilterBackend, + # GeoSpatialFilteringFilterBackend, +# DefaultOrderingFilterBackend, ] 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}, - 'index_name': {'fuzziness': 'auto:2,5', - 'boost': 2}, 'description': {'fuzziness': 'auto:2,5'}, } + translated_search_fields = ( 'description', ) @@ -248,7 +249,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet): 'for_establishment': { 'field': 'establishment.slug', }, - 'type': { + 'product_type': { 'field': 'product_type.index_name', }, 'subtype': { @@ -259,5 +260,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 29f18a23..7572766f 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -101,6 +101,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) 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.""" 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