diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index e6076117..b68aca7d 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -172,38 +172,6 @@ class ChangeEmailSerializer(serializers.ModelSerializer): return instance -class ConfirmEmailSerializer(serializers.ModelSerializer): - """Confirm user email serializer""" - x = serializers.CharField(default=None) - - class Meta: - """Meta class""" - model = models.User - fields = ( - 'email', - ) - - def validate(self, attrs): - """Override validate method""" - email_confirmed = self.instance.email_confirmed - if email_confirmed: - raise utils_exceptions.EmailConfirmedError() - - return attrs - - def update(self, instance, validated_data): - """ - Override update method - """ - - # Send verification link on user email for change email address - if settings.USE_CELERY: - tasks.confirm_new_email_address.delay(instance.id) - else: - tasks.confirm_new_email_address(instance.id) - return instance - - # Firebase Cloud Messaging serializers class FCMDeviceSerializer(serializers.ModelSerializer): """FCM Device model serializer""" diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index 34583010..4ea2af66 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -8,5 +8,6 @@ app_name = 'account' urlpatterns = [ path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'), path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), + path('email/confirm/', views.SendConfirmationEmailView.as_view(), name='send-confirm-email'), path('email/confirm///', views.ConfirmEmailView.as_view(), name='confirm-email'), ] diff --git a/apps/account/views/common.py b/apps/account/views/common.py index ab62343f..d29ce2bb 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -1,4 +1,5 @@ """Common account views""" +from django.conf import settings from django.utils.encoding import force_text from django.utils.http import urlsafe_base64_decode from fcm_django.models import FCMDevice @@ -9,6 +10,7 @@ from rest_framework.response import Response from account import models from account.serializers import common as serializers +from authorization.tasks import send_confirm_email from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator from utils.views import JWTGenericViewMixin @@ -38,19 +40,26 @@ class ChangePasswordView(generics.GenericAPIView): return Response(status=status.HTTP_200_OK) -class SendConfirmationEmailView(JWTGenericViewMixin): +class SendConfirmationEmailView(generics.GenericAPIView): """Confirm email view.""" - serializer_class = serializers.ConfirmEmailSerializer - queryset = models.User.objects.all() - def patch(self, request, *args, **kwargs): - """Implement PATCH-method""" - # Get user instance - instance = self.request.user + def post(self, request, *args, **kwargs): + """Override create method""" + user = self.request.user + country_code = self.request.country_code - serializer = self.get_serializer(data=request.data, instance=instance) - serializer.is_valid(raise_exception=True) - serializer.save() + if user.email_confirmed: + raise utils_exceptions.EmailConfirmedError() + + # Send verification link on user email for change email address + if settings.USE_CELERY: + send_confirm_email.delay( + user_id=user.id, + country_code=country_code) + else: + send_confirm_email( + user_id=user.id, + country_code=country_code) return Response(status=status.HTTP_200_OK) diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 3e7195a5..ed68ba9f 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -76,39 +76,6 @@ class SignupSerializer(serializers.ModelSerializer): return obj -class ReconfirmSerializer(serializers.ModelSerializer): - - class Meta: - model = account_models.User - fields = ('email',) - - def validate_email(self, value): - """Validate email""" - users = list(account_models.User.objects.filter(email=value.lower()).all()) - if not users: - raise serializers.ValidationError(detail='User with mentioned email does not exist.') - users = list(filter(lambda user: not user.email_confirmed, users)) - if not users: - raise serializers.ValidationError(detail='User with this email is confirmed.') - return value - - def create(self, validated_data): - """Override create method""" - queryset = account_models.User.objects.all() - email = validated_data.get('email').lower() - country_code = self.context.get('request').country_code - obj = get_object_or_404(queryset, email=email) - if settings.USE_CELERY: - tasks.send_confirm_email.delay( - user_id=obj.id, - country_code=country_code) - else: - tasks.send_confirm_email( - user_id=obj.id, - country_code=country_code) - return obj - - class LoginByUsernameOrEmailSerializer(SourceSerializerMixin, serializers.ModelSerializer): """Serializer for login user""" diff --git a/apps/authorization/tasks.py b/apps/authorization/tasks.py index 9947c2a3..c97fbbae 100644 --- a/apps/authorization/tasks.py +++ b/apps/authorization/tasks.py @@ -4,18 +4,19 @@ from django.utils.translation import gettext_lazy as _ from celery import shared_task from account import models as account_models +from smtplib import SMTPException logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO) logger = logging.getLogger(__name__) @shared_task -def send_confirm_email(user_id, country_code): +def send_confirm_email(user_id: int, country_code: str): """Send verification email to user.""" try: obj = account_models.User.objects.get(id=user_id) obj.send_email(subject=_('Email confirmation'), message=obj.confirm_email_template(country_code)) - except: + except Exception as e: logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n' - f'DETAIL: Exception occurred for user: {user_id}') + f'DETAIL: user {user_id}, - {e}') diff --git a/apps/authorization/urls/common.py b/apps/authorization/urls/common.py index 814ce836..4e6e59e1 100644 --- a/apps/authorization/urls/common.py +++ b/apps/authorization/urls/common.py @@ -29,7 +29,6 @@ urlpatterns_oauth2 = [ urlpatterns_jwt = [ path('signup/', views.SignUpView.as_view(), name='signup'), - path('signup/reconfirm/', views.ReconfirmView.as_view(), name='signup-reconfirm'), path('signup/confirm///', views.ConfirmationEmailView.as_view(), name='signup-confirm'), path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'), diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index a2e3e8c9..0b1a58e0 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -147,17 +147,6 @@ class SignUpView(generics.GenericAPIView): return Response(status=status.HTTP_201_CREATED) -class ReconfirmView(generics.CreateAPIView): - """ Resends confirmation email whether user's still not confirmed """ - permission_classes = (permissions.AllowAny,) - serializer_class = serializers.ReconfirmSerializer - - def post(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - return Response(status=status.HTTP_201_CREATED) - - class ConfirmationEmailView(JWTGenericViewMixin): """View for confirmation email""" diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 589c2e22..5c2a0ff0 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -11,6 +11,7 @@ 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 elasticsearch_dsl import Q from phonenumber_field.modelfields import PhoneNumberField from collection.models import Collection @@ -98,6 +99,16 @@ class EstablishmentQuerySet(models.QuerySet): else: return self.none() + def es_search(self, value, locale=None): + """Search text via ElasticSearch.""" + from search_indexes.documents import EstablishmentDocument + search = EstablishmentDocument.search().filter( + Q('match', name=value) | + Q('match', **{f'description.{locale}': value}) + ).execute() + ids = [result.meta.id for result in search] + return self.filter(id__in=ids) + def by_country_code(self, code): """Return establishments by country code""" return self.filter(address__city__country__code=code) @@ -330,7 +341,6 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): raise ValidationError('Establishment type of subtype does not match') self.establishment_subtypes.add(establishment_subtype) - @property def vintage_year(self): last_review = self.reviews.by_status(Review.READY).last() diff --git a/apps/news/models.py b/apps/news/models.py index a4aae03a..6e91b912 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -5,9 +5,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin -from django.contrib.contenttypes.models import ContentType from rating.models import Rating -from random import sample as random_sample class NewsType(models.Model): diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 16321723..2d43154e 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -14,6 +14,7 @@ EstablishmentIndex.settings(number_of_shards=1, number_of_replicas=1) class EstablishmentDocument(Document): """Establishment document.""" + preview_image = fields.KeywordField(attr='preview_image_url') description = fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES) establishment_type = fields.ObjectField( @@ -50,7 +51,9 @@ class EstablishmentDocument(Document): fields={'raw': fields.KeywordField()} ), 'number': fields.IntegerField(), - 'location': fields.GeoPointField(attr='location_field_indexing'), + 'postal_code': fields.KeywordField(), + 'coordinates': fields.GeoPointField(attr='location_field_indexing'), + # todo: remove if not used 'city': fields.ObjectField( properties={ 'id': fields.IntegerField(), @@ -82,9 +85,10 @@ class EstablishmentDocument(Document): fields = ( 'id', 'name', - 'toque_number', + 'name_translated', 'price_level', - 'preview_image_url', + 'toque_number', + 'public_mark', 'slug', ) diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 45e50c42..6e0974d8 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -13,18 +13,26 @@ NewsIndex.settings(number_of_shards=1, number_of_replicas=1) class NewsDocument(Document): """News document.""" - news_type = fields.NestedField(properties={ - 'id': fields.IntegerField(), - 'name': fields.KeywordField() - }) - title = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES) - subtitle = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES) - description = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES) - country = fields.NestedField(properties={ - 'id': fields.IntegerField(), - 'code': fields.KeywordField() - }) + news_type = fields.ObjectField(properties={'id': fields.IntegerField(), + 'name': fields.KeywordField()}) + title = fields.ObjectField(attr='title_indexing', + properties=OBJECT_FIELD_PROPERTIES) + subtitle = fields.ObjectField(attr='subtitle_indexing', + properties=OBJECT_FIELD_PROPERTIES) + description = fields.ObjectField(attr='description_indexing', + properties=OBJECT_FIELD_PROPERTIES) + country = fields.ObjectField(properties={'id': fields.IntegerField(), + 'code': fields.KeywordField()}) web_url = fields.KeywordField(attr='web_url') + tags = fields.ObjectField( + properties={ + 'id': fields.IntegerField(attr='metadata.id'), + 'label': fields.ObjectField(attr='metadata.label_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'category': fields.ObjectField(attr='metadata.category', + properties={'id': fields.IntegerField()}) + }, + multi=True) class Django: @@ -32,20 +40,19 @@ class NewsDocument(Document): fields = ( 'id', 'playlist', + 'start', + 'end', + 'slug', + 'state', + 'is_highlighted', + 'image_url', + 'preview_image_url', + 'template', ) related_models = [models.NewsType] def get_queryset(self): - return super().get_queryset().published() - - def prepare_title(self, instance): - return instance.title - - def prepare_subtitle(self, instance): - return instance.subtitle - - def prepare_description(self, instance): - return instance.description + return super().get_queryset().published().with_base_related() def get_instances_from_related(self, related_instance): """If related_models is set, define how to retrieve the Car instance(s) from the related model. diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 1d8e3ca3..18a1e240 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -1,16 +1,42 @@ """Search indexes serializers.""" from rest_framework import serializers from django_elasticsearch_dsl_drf.serializers import DocumentSerializer +from news.serializers import NewsTypeSerializer from search_indexes.documents import EstablishmentDocument, NewsDocument from search_indexes.utils import get_translated_value +class TagsDocumentSerializer(serializers.Serializer): + """Tags serializer for ES Document.""" + + id = serializers.IntegerField() + label_translated = serializers.SerializerMethodField() + + def get_label_translated(self, obj): + return get_translated_value(obj.label) + + +class AddressDocumentSerializer(serializers.Serializer): + """Address serializer for ES Document.""" + + id = serializers.IntegerField() + street_name_1 = serializers.CharField() + street_name_2 = serializers.CharField() + number = serializers.IntegerField() + postal_code = serializers.CharField() + latitude = serializers.FloatField(allow_null=True, source='coordinates.lat') + longitude = serializers.FloatField(allow_null=True, source='coordinates.lon') + geo_lon = serializers.FloatField(allow_null=True, source='coordinates.lon') + geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat') + + class NewsDocumentSerializer(DocumentSerializer): """News document serializer.""" title_translated = serializers.SerializerMethodField(allow_null=True) subtitle_translated = serializers.SerializerMethodField(allow_null=True) - description_translated = serializers.SerializerMethodField(allow_null=True) + news_type = NewsTypeSerializer() + tags = TagsDocumentSerializer(many=True) class Meta: """Meta class.""" @@ -18,13 +44,14 @@ class NewsDocumentSerializer(DocumentSerializer): document = NewsDocument fields = ( 'id', - 'title', - 'subtitle', - 'description', - 'web_url', 'title_translated', 'subtitle_translated', - 'description_translated', + 'is_highlighted', + 'image_url', + 'preview_image_url', + 'news_type', + 'tags', + 'slug', ) @staticmethod @@ -35,18 +62,13 @@ class NewsDocumentSerializer(DocumentSerializer): def get_subtitle_translated(obj): return get_translated_value(obj.subtitle) - @staticmethod - def get_description_translated(obj): - return get_translated_value(obj.description) - -# todo: country_name_translated class EstablishmentDocumentSerializer(DocumentSerializer): """Establishment document serializer.""" - description_translated = serializers.SerializerMethodField(allow_null=True) + address = AddressDocumentSerializer() + tags = TagsDocumentSerializer(many=True) - preview_image = serializers.URLField(source='preview_image_url') class Meta: """Meta class.""" @@ -54,43 +76,40 @@ class EstablishmentDocumentSerializer(DocumentSerializer): fields = ( 'id', 'name', - 'public_mark', - 'toque_number', + 'name_translated', 'price_level', - 'description_translated', - 'tags', - 'address', - 'collections', - 'establishment_type', - 'establishment_subtypes', - 'preview_image', + 'toque_number', + 'public_mark', 'slug', + 'preview_image', + 'address', + 'tags', + # 'collections', + # 'establishment_type', + # 'establishment_subtypes', ) - @staticmethod - def get_description_translated(obj): - return get_translated_value(obj.description) - def to_representation(self, instance): - ret = super().to_representation(instance) - dict_merge = lambda a, b: a.update(b) or a - - ret['tags'] = map(lambda tag: dict_merge(tag, {'label_translated': get_translated_value(tag.pop('label'))}), - ret['tags']) - ret['establishment_subtypes'] = map( - lambda subtype: dict_merge(subtype, {'name_translated': get_translated_value(subtype.pop('name'))}), - ret['establishment_subtypes']) - if ret.get('establishment_type'): - ret['establishment_type']['name_translated'] = get_translated_value(ret['establishment_type'].pop('name')) - if ret.get('address'): - ret['address']['city']['country']['name_translated'] = get_translated_value( - ret['address']['city']['country'].pop('name')) - location = ret['address'].pop('location') - if location: - ret['address']['geo_lon'] = location['lon'] - ret['address']['geo_lat'] = location['lat'] - - ret['type'] = ret.pop('establishment_type') - ret['subtypes'] = ret.pop('establishment_subtypes') - - return ret \ No newline at end of file + # def to_representation(self, instance): + # ret = super().to_representation(instance) + # dict_merge = lambda a, b: a.update(b) or a + # + # ret['tags'] = map(lambda tag: dict_merge(tag, {'label_translated': get_translated_value(tag.pop('label'))}), + # ret['tags']) + # ret['establishment_subtypes'] = map( + # lambda subtype: dict_merge(subtype, {'name_translated': get_translated_value(subtype.pop('name'))}), + # ret['establishment_subtypes']) + # if ret.get('establishment_type'): + # ret['establishment_type']['name_translated'] = get_translated_value(ret['establishment_type'].pop('name')) + # if ret.get('address'): + # ret['address']['city']['country']['name_translated'] = get_translated_value( + # ret['address']['city']['country'].pop('name')) + # location = ret['address'].pop('location') + # if location: + # ret['address']['geo_lon'] = location['lon'] + # ret['address']['geo_lat'] = location['lat'] + # + # ret['type'] = ret.pop('establishment_type') + # ret['subtypes'] = ret.pop('establishment_subtypes') + # + # return ret \ No newline at end of file diff --git a/apps/search_indexes/signals.py b/apps/search_indexes/signals.py index 2c04b6c6..f7520b57 100644 --- a/apps/search_indexes/signals.py +++ b/apps/search_indexes/signals.py @@ -1,5 +1,5 @@ """Search indexes app signals.""" -from django.db.models.signals import post_save, post_delete +from django.db.models.signals import post_save from django.dispatch import receiver from django_elasticsearch_dsl.registries import registry @@ -17,17 +17,65 @@ def update_document(sender, **kwargs): address__city__country=instance) for establishment in establishments: registry.update(establishment) - if model_name == 'city': establishments = Establishment.objects.filter( address__city=instance) for establishment in establishments: registry.update(establishment) - if model_name == 'address': establishments = Establishment.objects.filter( address=instance) for establishment in establishments: registry.update(establishment) -# todo: delete document + if app_label == 'establishment': + if model_name == 'establishmenttype': + establishments = Establishment.objects.filter( + establishment_type=instance) + for establishment in establishments: + registry.update(establishment) + if model_name == 'establishmentsubtype': + establishments = Establishment.objects.filter( + establishment_subtypes=instance) + for establishment in establishments: + registry.update(establishment) + + if app_label == 'main': + if model_name == 'metadata': + establishments = Establishment.objects.filter(tags__metadata=instance) + for establishment in establishments: + registry.update(establishment) + if model_name == 'metadatacontent': + establishments = Establishment.objects.filter(tags=instance) + for establishment in establishments: + registry.update(establishment) + + +@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'] + + if app_label == 'location': + if model_name == 'country': + qs = News.objects.filter(country=instance) + for news in qs: + registry.update(news) + + if app_label == 'news': + if model_name == 'newstype': + qs = News.objects.filter(news_type=instance) + for news in qs: + registry.update(news) + + if app_label == 'main': + if model_name == 'metadata': + qs = News.objects.filter(tags__metadata=instance) + for news in qs: + registry.update(news) + if model_name == 'metadatacontent': + qs = News.objects.filter(tags=instance) + for news in qs: + registry.update(news) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 2b9f4e65..a4f37006 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -1,8 +1,10 @@ """Search indexes app views.""" from rest_framework import permissions from django_elasticsearch_dsl_drf import constants -from django_elasticsearch_dsl_drf.filter_backends import (FilteringFilterBackend, - GeoSpatialFilteringFilterBackend) +from django_elasticsearch_dsl_drf.filter_backends import ( + FilteringFilterBackend, + GeoSpatialFilteringFilterBackend +) from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from utils.pagination import ProjectPageNumberPagination @@ -22,6 +24,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): filter_backends = [ filters.CustomSearchFilterBackend, + FilteringFilterBackend, ] search_fields = ( @@ -35,6 +38,16 @@ class NewsDocumentViewSet(BaseDocumentViewSet): 'description', ) + filter_fields = { + 'tag': { + 'field': 'tags.id', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + ] + }, + 'slug': 'slug', + } + class EstablishmentDocumentViewSet(BaseDocumentViewSet): """Establishment document ViewSet.""" @@ -60,6 +73,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'description', ) filter_fields = { + 'slug': 'slug', 'tag': { 'field': 'tags.id', 'lookups': [constants.LOOKUP_QUERY_IN] @@ -111,7 +125,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): geo_spatial_filter_fields = { 'location': { - 'field': 'address.location', + 'field': 'address.coordinates', 'lookups': [ constants.LOOKUP_FILTER_GEO_BOUNDING_BOX, ] diff --git a/project/settings/development.py b/project/settings/development.py index 43a60935..ff80b492 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -24,7 +24,7 @@ ELASTICSEARCH_DSL = { ELASTICSEARCH_INDEX_NAMES = { - # 'search_indexes.documents.news': 'development_news', # temporarily disabled + 'search_indexes.documents.news': 'development_news', # temporarily disabled 'search_indexes.documents.establishment': 'development_establishment', }