Merge branch 'develop' into feature/migrate-city-objects

# Conflicts:
#	docker-compose.yml
#	project/settings/base.py
This commit is contained in:
littlewolf 2019-10-09 07:54:15 +03:00
commit 22d2396c6b
54 changed files with 752 additions and 374 deletions

View File

@ -77,6 +77,23 @@ class UserSerializer(serializers.ModelSerializer):
return instance
class UserBaseSerializer(serializers.ModelSerializer):
"""Serializer is used to display brief information about the user."""
fullname = serializers.CharField(source='get_full_name', read_only=True)
class Meta:
"""Meta class."""
model = models.User
fields = (
'fullname',
'cropped_image_url',
'image_url',
)
read_only_fields = fields
class ChangePasswordSerializer(serializers.ModelSerializer):
"""Serializer for model User."""
@ -155,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"""

View File

@ -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/<uidb64>/<token>/', views.ConfirmEmailView.as_view(), name='confirm-email'),
]

View File

@ -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)

View File

@ -4,6 +4,20 @@ from django.db import migrations, models
import uuid
def fill_uuid(apps, schemaeditor):
Advertisement = apps.get_model('advertisement', 'Advertisement')
for a in Advertisement.objects.all():
a.uuid = uuid.uuid4()
a.save()
def fill_block_level(apps, schemaeditor):
Advertisement = apps.get_model('advertisement', 'Advertisement')
for a in Advertisement.objects.all():
a.block_level = ''
a.save()
class Migration(migrations.Migration):
dependencies = [
@ -23,6 +37,12 @@ class Migration(migrations.Migration):
field=models.ManyToManyField(to='translation.Language'),
),
migrations.AddField(
model_name='advertisement',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False),
),
migrations.RunPython(fill_uuid, migrations.RunPython.noop),
migrations.AlterField(
model_name='advertisement',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
@ -32,8 +52,14 @@ class Migration(migrations.Migration):
name='block_level',
),
migrations.AddField(
model_name='advertisement',
name='block_level',
field=models.CharField(blank=True, null=True, max_length=10, verbose_name='Block level')
),
migrations.RunPython(fill_block_level, migrations.RunPython.noop),
migrations.AlterField(
model_name='advertisement',
name='block_level',
field=models.CharField(max_length=10, verbose_name='Block level')
)
),
]

View File

@ -4,7 +4,7 @@ from django.contrib.auth import authenticate
from django.contrib.auth import password_validation as password_validators
from django.db.models import Q
from rest_framework import serializers
from rest_framework import validators as rest_validators
from rest_framework.generics import get_object_or_404
from account import models as account_models
from authorization import tasks
@ -81,7 +81,6 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
"""Serializer for login user"""
# REQUEST
username_or_email = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True)
# For cookie properties (Max-Age)
remember = serializers.BooleanField(write_only=True)
@ -101,21 +100,24 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
'refresh_token',
'access_token',
)
extra_kwargs = {
'password': {'write_only': True}
}
def validate(self, attrs):
"""Override validate method"""
username_or_email = attrs.pop('username_or_email')
password = attrs.pop('password')
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
(Q(email=username_or_email)))
Q(email=username_or_email))
if not user_qs.exists():
raise utils_exceptions.UserNotFoundError()
raise utils_exceptions.WrongAuthCredentials()
else:
user = user_qs.first()
authentication = authenticate(username=user.get_username(),
password=password)
if not authentication:
raise utils_exceptions.UserNotFoundError()
raise utils_exceptions.WrongAuthCredentials()
self.instance = user
return attrs
@ -127,10 +129,6 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
return super().to_representation(instance)
class LogoutSerializer(SourceSerializerMixin):
"""Serializer for Logout endpoint."""
class RefreshTokenSerializer(SourceSerializerMixin):
"""Serializer for refresh token view"""
refresh_token = serializers.CharField(read_only=True)
@ -169,7 +167,3 @@ class RefreshTokenSerializer(SourceSerializerMixin):
class OAuth2Serialzier(SourceSerializerMixin):
"""Serializer OAuth2 authorization"""
token = serializers.CharField(max_length=255)
class OAuth2LogoutSerializer(SourceSerializerMixin):
"""Serializer for logout"""

View File

@ -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}')

View File

@ -27,24 +27,6 @@ from utils.permissions import IsAuthenticatedAndTokenIsValid
from utils.views import JWTGenericViewMixin
# Mixins
# JWTAuthView mixin
class JWTAuthViewMixin(JWTGenericViewMixin):
"""Mixin for authentication views"""
def post(self, request, *args, **kwargs):
"""Implement POST method"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_200_OK)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('refresh_token')
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),
response=response)
# OAuth2
class BaseOAuth2ViewMixin(generics.GenericAPIView):
"""BaseMixin for classic auth views"""
@ -112,6 +94,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin):
# Preparing request data
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
request_data = self.prepare_request_data(serializer.validated_data)
source = serializer.validated_data.get('source')
request_data.update({
@ -196,7 +179,7 @@ class ConfirmationEmailView(JWTGenericViewMixin):
# Login by username|email + password
class LoginByUsernameOrEmailView(JWTAuthViewMixin):
class LoginByUsernameOrEmailView(JWTGenericViewMixin):
"""Login by email and password"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.LoginByUsernameOrEmailSerializer
@ -205,10 +188,12 @@ class LoginByUsernameOrEmailView(JWTAuthViewMixin):
"""Implement POST method"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_200_OK)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('refresh_token')
is_permanent = serializer.validated_data.get('remember')
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token,
@ -243,9 +228,11 @@ class RefreshTokenView(JWTGenericViewMixin):
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_201_CREATED)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('refresh_token')
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),

View File

@ -4,11 +4,24 @@ from collection import models
from location import models as location_models
class CollectionSerializer(serializers.ModelSerializer):
"""Collection serializer"""
class CollectionBaseSerializer(serializers.ModelSerializer):
"""Collection base serializer"""
# RESPONSE
description_translated = serializers.CharField(read_only=True, allow_null=True)
class Meta:
model = models.Collection
fields = [
'id',
'name',
'description_translated',
'image_url',
'slug',
]
class CollectionSerializer(CollectionBaseSerializer):
"""Collection serializer"""
# COMMON
block_size = serializers.JSONField()
is_publish = serializers.BooleanField()
@ -24,18 +37,13 @@ class CollectionSerializer(serializers.ModelSerializer):
class Meta:
model = models.Collection
fields = [
'id',
'name',
'description_translated',
fields = CollectionBaseSerializer.Meta.fields + [
'start',
'end',
'image_url',
'is_publish',
'on_top',
'country',
'block_size',
'slug',
]

View File

@ -1,15 +1,15 @@
import json, pytz
import json
import pytz
from datetime import datetime
from rest_framework.test import APITestCase
from account.models import User
from rest_framework import status
from http.cookies import SimpleCookie
from collection.models import Collection, Guide
from location.models import Country
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from collection.models import Collection, Guide
from establishment.models import Establishment, EstablishmentType
# Create your tests here.
from location.models import Country
class BaseTestCase(APITestCase):

View File

@ -7,6 +7,7 @@ app_name = 'collection'
urlpatterns = [
path('', views.CollectionHomePageView.as_view(), name='list'),
path('<slug:slug>/', views.CollectionDetailView.as_view(), name='detail'),
path('<slug:slug>/establishments/', views.CollectionEstablishmentListView.as_view(),
name='detail'),

View File

@ -4,7 +4,7 @@ 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 EstablishmentListSerializer
from establishment.serializers import EstablishmentBaseSerializer
from collection.serializers import common as serializers
@ -12,7 +12,14 @@ from collection.serializers import common as serializers
class CollectionViewMixin(generics.GenericAPIView):
"""Mixin for Collection view"""
model = models.Collection
queryset = models.Collection.objects.all()
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CollectionSerializer
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')
class GuideViewMixin(generics.GenericAPIView):
@ -24,40 +31,29 @@ class GuideViewMixin(generics.GenericAPIView):
# Views
# Collections
class CollectionListView(CollectionViewMixin, generics.ListAPIView):
"""List Collection view"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CollectionSerializer
"""List Collection view."""
class CollectionHomePageView(CollectionListView):
"""Collection list view for home page."""
def get_queryset(self):
"""Override get_queryset method"""
queryset = models.Collection.objects.published()\
.by_country_code(code=self.request.country_code)\
.order_by('-on_top', '-created')
return queryset
"""Override get_queryset."""
return super(CollectionHomePageView, self).get_queryset() \
.filter_all_related_gt(3)
class CollectionHomePageView(CollectionViewMixin, generics.ListAPIView):
"""List Collection view"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CollectionSerializer
def get_queryset(self):
"""Override get_queryset method"""
queryset = models.Collection.objects.published()\
.by_country_code(code=self.request.country_code)\
.filter_all_related_gt(3)\
.order_by('-on_top', '-modified')
return queryset
class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView):
"""Retrieve detail of Collection instance."""
lookup_field = 'slug'
serializer_class = serializers.CollectionBaseSerializer
class CollectionEstablishmentListView(CollectionListView):
"""Retrieve list of establishment for collection."""
permission_classes = (permissions.AllowAny,)
pagination_class = ProjectPageNumberPagination
serializer_class = EstablishmentListSerializer
lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination
serializer_class = EstablishmentBaseSerializer
def get_queryset(self):
"""

View File

@ -8,13 +8,14 @@ from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance as DistanceMeasure
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery
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
from main.models import MetaDataContent
from main.models import Award, MetaDataContent
from location.models import Address
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
@ -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)
@ -281,7 +292,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
slug = models.SlugField(unique=True, max_length=50, null=True,
verbose_name=_('Establishment slug'), editable=True)
awards = generic.GenericRelation(to='main.Award')
awards = generic.GenericRelation(to='main.Award', related_query_name='establishment')
tags = generic.GenericRelation(to='main.MetaDataContent')
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
@ -330,6 +341,12 @@ 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()
if last_review:
return last_review.vintage
@property
def best_price_menu(self):
return 150
@ -356,6 +373,11 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""
return self.address.coordinates
@property
def the_most_recent_award(self):
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)).latest(
field_name='vintage_year')
class Position(BaseAttributes, TranslatedFieldsMixin):
"""Position model."""
@ -409,8 +431,8 @@ class Employee(BaseAttributes):
verbose_name=_('User'))
name = models.CharField(max_length=255, verbose_name=_('Last name'))
establishments = models.ManyToManyField(Establishment, related_name='employees',
through=EstablishmentEmployee)
awards = generic.GenericRelation(to='main.Award')
through=EstablishmentEmployee,)
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
tags = generic.GenericRelation(to='main.MetaDataContent')
class Meta:

View File

@ -8,7 +8,6 @@ from establishment.serializers import (
from utils.decorators import with_base_attributes
from main.models import Currency
from utils.serializers import TJSONSerializer
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
@ -89,13 +88,15 @@ class SocialNetworkSerializers(serializers.ModelSerializer):
class PlatesSerializers(PlateSerializer):
"""Social network serializers."""
name = TJSONSerializer
currency_id = serializers.PrimaryKeyRelatedField(
source='currency',
queryset=Currency.objects.all(), write_only=True
)
class Meta:
"""Meta class."""
model = models.Plate
fields = PlateSerializer.Meta.fields + [
'name',

View File

@ -1,18 +1,17 @@
"""Establishment serializers."""
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
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 favorites.models import Favorites
from location.serializers import AddressSimpleSerializer
from location.serializers import AddressBaseSerializer
from main.models import MetaDataContent
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
from review import models as review_models
from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField
from utils.serializers import TJSONSerializer
from utils.serializers import TranslatedField, ProjectModelSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -44,9 +43,9 @@ class SocialNetworkRelatedSerializers(serializers.ModelSerializer):
]
class PlateSerializer(serializers.ModelSerializer):
class PlateSerializer(ProjectModelSerializer):
name_translated = serializers.CharField(allow_null=True, read_only=True)
name_translated = TranslatedField()
currency = CurrencySerializer(read_only=True)
class Meta:
@ -59,9 +58,8 @@ class PlateSerializer(serializers.ModelSerializer):
]
class MenuSerializers(serializers.ModelSerializer):
class MenuSerializers(ProjectModelSerializer):
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
category = TJSONSerializer()
category_translated = serializers.CharField(read_only=True)
class Meta:
@ -75,9 +73,8 @@ class MenuSerializers(serializers.ModelSerializer):
]
class MenuRUDSerializers(serializers.ModelSerializer, ):
class MenuRUDSerializers(ProjectModelSerializer):
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
category = TJSONSerializer()
class Meta:
model = models.Menu
@ -141,13 +138,14 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'position_translated', 'awards', 'priority')
class EstablishmentBaseSerializer(serializers.ModelSerializer):
class EstablishmentBaseSerializer(ProjectModelSerializer):
"""Base serializer for Establishment model."""
preview_image = serializers.URLField(source='preview_image_url')
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
address = AddressSimpleSerializer()
address = AddressBaseSerializer()
tags = MetaDataContentSerializer(many=True)
in_favorites = serializers.BooleanField(allow_null=True)
class Meta:
"""Meta class."""
@ -168,20 +166,7 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer):
]
class EstablishmentListSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
in_favorites = serializers.BooleanField(allow_null=True)
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
fields = EstablishmentBaseSerializer.Meta.fields + [
'in_favorites',
]
class EstablishmentDetailSerializer(EstablishmentListSerializer):
class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
description_translated = TranslatedField()
@ -198,11 +183,12 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer):
menu = MenuSerializers(source='menu_set', many=True, read_only=True)
best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
vintage_year = serializers.ReadOnlyField()
class Meta(EstablishmentListSerializer.Meta):
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
fields = EstablishmentListSerializer.Meta.fields + [
fields = EstablishmentBaseSerializer.Meta.fields + [
'description_translated',
'image',
'subtypes',
@ -222,18 +208,9 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer):
'best_price_menu',
'best_price_carte',
'transportation',
'vintage_year',
]
# def get_in_favorites(self, obj):
# """Get in_favorites status flag"""
# user = self.context.get('request').user
# if user.is_authenticated:
# return obj.id in user.favorites.by_content_type(app_label='establishment',
# model='establishment')\
# .values_list('object_id', flat=True)
# else:
# return False
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer):
"""Create comment serializer"""

View File

@ -28,7 +28,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
"""Resource for getting a list of establishments."""
filter_class = filters.EstablishmentFilter
serializer_class = serializers.EstablishmentListSerializer
serializer_class = serializers.EstablishmentBaseSerializer
def get_queryset(self):
"""Overridden method 'get_queryset'."""
@ -70,7 +70,8 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
class EstablishmentSimilarListView(EstablishmentListView):
"""Resource for getting a list of establishments."""
serializer_class = serializers.EstablishmentListSerializer
serializer_class = serializers.EstablishmentBaseSerializer
pagination_class = EstablishmentPortionPagination
def get_queryset(self):
@ -96,6 +97,7 @@ class EstablishmentCommentCreateView(generics.CreateAPIView):
class EstablishmentCommentListView(generics.ListAPIView):
"""View for return list of establishment comments."""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentCommentCreateSerializer
@ -153,11 +155,13 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
"""Resource for getting list of nearest establishments."""
serializer_class = serializers.EstablishmentListSerializer
serializer_class = serializers.EstablishmentBaseSerializer
filter_class = filters.EstablishmentFilter
def get_queryset(self):
"""Overridden method 'get_queryset'."""
# todo: latitude and longitude
lat = self.request.query_params.get('lat')
lon = self.request.query_params.get('lon')
radius = self.request.query_params.get('radius')

View File

@ -1,13 +1,13 @@
"""Views for app favorites."""
from rest_framework import generics
from establishment.models import Establishment
from establishment.serializers import EstablishmentListSerializer
from establishment.serializers import EstablishmentBaseSerializer
from .models import Favorites
class FavoritesBaseView(generics.GenericAPIView):
"""Base view for Favorites."""
def get_queryset(self):
"""Override get_queryset method."""
return Favorites.objects.by_user(self.request.user)
@ -15,7 +15,8 @@ class FavoritesBaseView(generics.GenericAPIView):
class FavoritesEstablishmentListView(generics.ListAPIView):
"""List views for favorites"""
serializer_class = EstablishmentListSerializer
serializer_class = EstablishmentBaseSerializer
def get_queryset(self):
"""Override get_queryset method"""

View File

@ -71,6 +71,7 @@ class City(models.Model):
class Address(models.Model):
"""Address model."""
city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE)
street_name_1 = models.CharField(
@ -98,11 +99,11 @@ class Address(models.Model):
@property
def latitude(self):
return self.coordinates.y
return self.coordinates.y if self.coordinates else float(0)
@property
def longitude(self):
return self.coordinates.x
return self.coordinates.x if self.coordinates else float(0)
@property
def location_field_indexing(self):

View File

@ -1,11 +1,8 @@
from django.contrib.gis.geos import Point
from rest_framework import serializers
from location import models
from location.serializers import common
class AddressCreateSerializer(common.AddressSerializer):
class AddressCreateSerializer(common.AddressDetailSerializer):
"""Address create serializer."""

View File

@ -1,5 +1,6 @@
"""Location app common serializers."""
from django.contrib.gis.geos import Point
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from location import models
from utils.serializers import TranslatedField
@ -83,55 +84,18 @@ class CitySerializer(serializers.ModelSerializer):
]
class AddressSerializer(serializers.ModelSerializer):
"""Address serializer."""
city_id = serializers.PrimaryKeyRelatedField(
source='city',
queryset=models.City.objects.all())
city = CitySerializer(read_only=True)
geo_lon = serializers.FloatField(allow_null=True)
geo_lat = serializers.FloatField(allow_null=True)
class Meta:
model = models.Address
fields = [
'id',
'city_id',
'city',
'street_name_1',
'street_name_2',
'number',
'postal_code',
'geo_lon',
'geo_lat'
]
def validate(self, attrs):
# if geo_lat and geo_lon was sent
geo_lat = attrs.pop('geo_lat') if 'geo_lat' in attrs else None
geo_lon = attrs.pop('geo_lon') if 'geo_lon' in attrs else None
if geo_lat and geo_lon:
# Point(longitude, latitude)
attrs['coordinates'] = Point(geo_lat, geo_lon)
return attrs
def to_representation(self, instance):
"""Override to_representation method"""
if instance.coordinates and isinstance(instance.coordinates, Point):
# Point(longitude, latitude)
setattr(instance, 'geo_lat', instance.coordinates.x)
setattr(instance, 'geo_lon', instance.coordinates.y)
else:
setattr(instance, 'geo_lat', float(0))
setattr(instance, 'geo_lon', float(0))
return super().to_representation(instance)
class AddressSimpleSerializer(serializers.ModelSerializer):
class AddressBaseSerializer(serializers.ModelSerializer):
"""Serializer for address obj in related objects."""
latitude = serializers.FloatField(allow_null=True)
longitude = serializers.FloatField(allow_null=True)
# todo: remove this fields (backward compatibility)
geo_lon = serializers.FloatField(source='longitude', allow_null=True,
read_only=True)
geo_lat = serializers.FloatField(source='latitude', allow_null=True,
read_only=True)
class Meta:
"""Meta class."""
@ -142,4 +106,45 @@ class AddressSimpleSerializer(serializers.ModelSerializer):
'street_name_2',
'number',
'postal_code',
'latitude',
'longitude',
# todo: remove this fields (backward compatibility)
'geo_lon',
'geo_lat',
)
def validate_latitude(self, value):
if -90 <= value <= 90:
return value
raise serializers.ValidationError(_('Invalid value'))
def validate_longitude(self, value):
if -180 <= value <= 180:
return value
raise serializers.ValidationError(_('Invalid value'))
def validate(self, attrs):
# validate coordinates
latitude = attrs.pop('latitude', None)
longitude = attrs.pop('longitude', None)
if latitude is not None and longitude is not None:
attrs['coordinates'] = Point(longitude, latitude)
return attrs
class AddressDetailSerializer(AddressBaseSerializer):
"""Address serializer."""
city_id = serializers.PrimaryKeyRelatedField(
source='city', write_only=True,
queryset=models.City.objects.all())
city = CitySerializer(read_only=True)
class Meta(AddressBaseSerializer.Meta):
"""Meta class."""
fields = AddressBaseSerializer.Meta.fields + (
'city_id',
'city',
)

View File

@ -1,6 +1,5 @@
"""Location app views."""
from rest_framework import generics
from rest_framework import permissions
from location import models, serializers
from location.views import common
@ -9,13 +8,13 @@ from location.views import common
# Address
class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView):
"""Create view for model Address."""
serializer_class = serializers.AddressSerializer
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Address."""
serializer_class = serializers.AddressSerializer
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()

View File

@ -100,17 +100,17 @@ class CityUpdateView(CityViewMixin, generics.UpdateAPIView):
# Address
class AddressCreateView(AddressViewMixin, generics.CreateAPIView):
"""Create view for model Address"""
serializer_class = serializers.AddressSerializer
serializer_class = serializers.AddressDetailSerializer
class AddressRetrieveView(AddressViewMixin, generics.RetrieveAPIView):
"""Retrieve view for model Address"""
serializer_class = serializers.AddressSerializer
serializer_class = serializers.AddressDetailSerializer
class AddressListView(AddressViewMixin, generics.ListAPIView):
"""List view for model Address"""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.AddressSerializer
serializer_class = serializers.AddressDetailSerializer

View File

@ -318,9 +318,9 @@ class Carousel(models.Model):
@property
def vintage_year(self):
if hasattr(self.content_object, 'reviews'):
review_qs = self.content_object.reviews.by_status(Review.READY)
if review_qs.exists():
return review_qs.last().vintage
last_review = self.content_object.reviews.by_status(Review.READY).last()
if last_review:
return last_review.vintage
@property
def toque_number(self):
@ -337,9 +337,21 @@ class Carousel(models.Model):
if hasattr(self.content_object, 'image_url'):
return self.content_object.image_url
@property
def slug(self):
if hasattr(self.content_object, 'slug'):
return self.content_object.slug
@property
def the_most_recent_award(self):
if hasattr(self.content_object, 'the_most_recent_award'):
return self.content_object.the_most_recent_award
@property
def model_name(self):
return self.content_object.__class__.__name__
if hasattr(self.content_object, 'establishment_type'):
return self.content_object.establishment_type.name_translated
class Page(models.Model):

View File

@ -3,6 +3,7 @@ from rest_framework import serializers
from advertisement.serializers.web import AdvertisementSerializer
from location.serializers import CountrySerializer
from main import models
from establishment.models import Establishment
from utils.serializers import TranslatedField
@ -141,6 +142,7 @@ class CarouselListSerializer(serializers.ModelSerializer):
image = serializers.URLField(source='image_url')
awards = AwardBaseSerializer(many=True)
vintage_year = serializers.IntegerField()
last_award = AwardBaseSerializer(source='the_most_recent_award', allow_null=True)
class Meta:
"""Meta class."""
@ -154,6 +156,8 @@ class CarouselListSerializer(serializers.ModelSerializer):
'public_mark',
'image',
'vintage_year',
'last_award',
'slug',
]

View File

@ -1,6 +1,7 @@
from django.contrib import admin
from news import models
from news import models
from .tasks import send_email_with_news
@admin.register(models.NewsType)
class NewsTypeAdmin(admin.ModelAdmin):
@ -9,6 +10,17 @@ class NewsTypeAdmin(admin.ModelAdmin):
list_display_links = ['id', 'name']
def send_email_action(modeladmin, request, queryset):
news_ids = list(queryset.values_list("id", flat=True))
send_email_with_news.delay(news_ids)
send_email_action.short_description = "Send the selected news by email"
@admin.register(models.News)
class NewsAdmin(admin.ModelAdmin):
"""News admin."""
actions = [send_email_action]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.4 on 2019-10-02 09:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0019_news_author'),
]
operations = [
migrations.RemoveField(
model_name='news',
name='author',
),
]

View File

@ -5,6 +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 rating.models import Rating
class NewsType(models.Model):
@ -26,10 +27,24 @@ class NewsType(models.Model):
class NewsQuerySet(models.QuerySet):
"""QuerySet for model News"""
def rating_value(self):
return self.annotate(rating=models.Count('ratings__ip', distinct=True))
def with_base_related(self):
"""Return qs with related objects."""
return self.select_related('news_type', 'country').prefetch_related('tags')
def with_extended_related(self):
"""Return qs with related objects."""
return self.select_related('created_by')
def by_type(self, news_type):
"""Filter News by type"""
return self.filter(news_type__name=news_type)
def by_tags(self, tags):
return self.filter(tags__in=tags)
def by_country_code(self, code):
"""Filter collection by country code."""
return self.filter(country__code=code)
@ -41,9 +56,16 @@ class NewsQuerySet(models.QuerySet):
models.Q(end__isnull=True)),
state__in=self.model.PUBLISHED_STATES, start__lte=now)
def with_related(self):
"""Return qs with related objects."""
return self.select_related('news_type', 'country').prefetch_related('tags')
# todo: filter by best score
# todo: filter by country?
def should_read(self, news):
return self.model.objects.exclude(pk=news.pk).published().\
with_base_related().by_type(news.news_type).distinct().order_by('?')
def same_theme(self, news):
return self.model.objects.exclude(pk=news.pk).published().\
with_base_related().by_type(news.news_type).\
by_tags(news.tags.all()).distinct().order_by('-start')
class News(BaseAttributes, TranslatedFieldsMixin):
@ -94,15 +116,8 @@ class News(BaseAttributes, TranslatedFieldsMixin):
slug = models.SlugField(unique=True, max_length=50,
verbose_name=_('News slug'))
playlist = models.IntegerField(_('playlist'))
# author = models.CharField(max_length=255, blank=True, null=True,
# default=None,verbose_name=_('Author'))
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
author = models.CharField(max_length=255, blank=True, null=True,
default=None,verbose_name=_('Author'))
is_highlighted = models.BooleanField(default=False,
verbose_name=_('Is highlighted'))
# TODO: metadata_keys - описание ключей для динамического построения полей метаданных
@ -120,6 +135,8 @@ class News(BaseAttributes, TranslatedFieldsMixin):
verbose_name=_('country'))
tags = generic.GenericRelation(to='main.MetaDataContent')
ratings = generic.GenericRelation(Rating)
objects = NewsQuerySet.as_manager()
class Meta:
@ -138,3 +155,12 @@ class News(BaseAttributes, TranslatedFieldsMixin):
@property
def web_url(self):
return reverse('web:news:rud', kwargs={'slug': self.slug})
@property
def should_read(self):
return self.__class__.objects.should_read(self)[:3]
@property
def same_theme(self):
return self.__class__.objects.same_theme(self)[:3]

View File

@ -1,11 +1,12 @@
"""News app common serializers."""
from rest_framework import serializers
from account.serializers.common import UserBaseSerializer
from location import models as location_models
from location.serializers import CountrySimpleSerializer
from main.serializers import MetaDataContentSerializer
from news import models
from utils.serializers import TranslatedField
from account.serializers.common import UserSerializer
from utils.serializers import TranslatedField, ProjectModelSerializer
class NewsTypeSerializer(serializers.ModelSerializer):
"""News type serializer."""
@ -17,7 +18,7 @@ class NewsTypeSerializer(serializers.ModelSerializer):
fields = ('id', 'name')
class NewsBaseSerializer(serializers.ModelSerializer):
class NewsBaseSerializer(ProjectModelSerializer):
"""Base serializer for News model."""
# read only fields
@ -50,7 +51,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
description_translated = TranslatedField()
country = CountrySimpleSerializer(read_only=True)
author = UserSerializer(source='created_by')
author = UserBaseSerializer(source='created_by', read_only=True)
state_display = serializers.CharField(source='get_state_display',
read_only=True)
@ -70,6 +71,21 @@ class NewsDetailSerializer(NewsBaseSerializer):
)
class NewsDetailWebSerializer(NewsDetailSerializer):
"""News detail serializer for web users.."""
same_theme = NewsBaseSerializer(many=True, read_only=True)
should_read = NewsBaseSerializer(many=True, read_only=True)
class Meta(NewsDetailSerializer.Meta):
"""Meta class."""
fields = NewsDetailSerializer.Meta.fields + (
'same_theme',
'should_read',
)
class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
"""News back office base serializer."""

28
apps/news/tasks.py Normal file
View File

@ -0,0 +1,28 @@
from celery import shared_task
from django.core.mail import send_mail
from notification.models import Subscriber
from news import models
from django.template.loader import render_to_string
from django.conf import settings
from smtplib import SMTPException
@shared_task
def send_email_with_news(news_ids):
subscribers = Subscriber.objects.filter(state=Subscriber.USABLE)
sent_news = models.News.objects.filter(id__in=news_ids)
for s in subscribers:
try:
for n in sent_news:
send_mail("G&M News", render_to_string(settings.NEWS_EMAIL_TEMPLATE,
{"title": n.title.get(s.locale),
"subtitle": n.subtitle.get(s.locale),
"description": n.description.get(s.locale),
"code": s.update_code,
"domain_uri": settings.DOMAIN_URI,
"country_code": s.country_code}),
settings.EMAIL_HOST_USER, [s.send_to], fail_silently=False)
except SMTPException:
continue

View File

@ -1,7 +1,7 @@
"""News app views."""
from rest_framework import generics, permissions
from news import filters, models, serializers
from rating.tasks import add_rating
class NewsMixinView:
"""News mixin."""
@ -11,7 +11,7 @@ class NewsMixinView:
def get_queryset(self, *args, **kwargs):
"""Override get_queryset method."""
qs = models.News.objects.with_related().published()\
qs = models.News.objects.published().with_base_related()\
.order_by('-is_highlighted', '-created')
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
@ -20,14 +20,19 @@ class NewsMixinView:
class NewsListView(NewsMixinView, generics.ListAPIView):
"""News list view."""
filter_class = filters.NewsListFilterSet
class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
"""News detail view."""
lookup_field = 'slug'
serializer_class = serializers.NewsDetailSerializer
lookup_field = 'slug'
serializer_class = serializers.NewsDetailWebSerializer
def get_queryset(self):
"""Override get_queryset method."""
return super().get_queryset().with_extended_related()
class NewsTypeListView(generics.ListAPIView):
"""NewsType list view."""
@ -42,7 +47,7 @@ class NewsBackOfficeMixinView:
"""News back office mixin view."""
permission_classes = (permissions.IsAuthenticated,)
queryset = models.News.objects.with_related() \
queryset = models.News.objects.with_base_related() \
.order_by('-is_highlighted', '-created')
@ -54,10 +59,15 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
create_serializers_class = serializers.NewsBackOfficeDetailSerializer
def get_serializer_class(self):
"""Override serializer class."""
if self.request.method == 'POST':
return self.create_serializers_class
return super().get_serializer_class()
def get_queryset(self):
"""Override get_queryset method."""
return super().get_queryset().with_extended_related()
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
generics.RetrieveUpdateDestroyAPIView):
@ -65,3 +75,7 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
serializer_class = serializers.NewsBackOfficeDetailSerializer
def get(self, request, pk, *args, **kwargs):
add_rating(remote_addr=request.META.get('REMOTE_ADDR'),
pk=pk, model='news', app_label='news')
return self.retrieve(request, *args, **kwargs)

View File

@ -2,6 +2,7 @@
from django.urls import path
from notification.views import common
app_name = "notification"
urlpatterns = [
path('subscribe/', common.SubscribeView.as_view(), name='subscribe'),

0
apps/rating/__init__.py Normal file
View File

11
apps/rating/admin.py Normal file
View File

@ -0,0 +1,11 @@
from django.contrib import admin
from rating import models
from rating import tasks
@admin.register(models.Rating)
class RatingAdmin(admin.ModelAdmin):
"""Rating type admin conf."""
list_display = ['name', 'ip']
list_display_links = ['name']

5
apps/rating/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class RatingConfig(AppConfig):
name = 'rating'

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.4 on 2019-10-02 11:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Rating',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('ip', models.GenericIPAddressField(verbose_name='ip')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'verbose_name': 'rating',
},
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.4 on 2019-10-04 09:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('rating', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='rating',
options={},
),
migrations.AlterUniqueTogether(
name='rating',
unique_together={('ip', 'object_id', 'content_type')},
),
]

View File

22
apps/rating/models.py Normal file
View File

@ -0,0 +1,22 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import gettext_lazy as _
class Rating(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
ip = models.GenericIPAddressField(verbose_name=_('ip'))
class Meta:
unique_together = ('ip', 'object_id', 'content_type')
@property
def name(self):
# Check if Generic obj has name or title
if hasattr(self.content_object, 'name'):
return self.content_object.name
if hasattr(self.content_object, 'title'):
return self.content_object.title_translated

18
apps/rating/tasks.py Normal file
View File

@ -0,0 +1,18 @@
from celery import shared_task
from rating.models import Rating
from django.contrib.contenttypes.models import ContentType
def add_rating(remote_addr, pk, model, app_label):
add.apply_async(
(remote_addr, pk, model, app_label), countdown=60 * 60
)
@shared_task
def add(remote_addr, pk, model, app_label):
content_type = ContentType.objects.get(app_label=app_label, model=model)
Rating.objects.get_or_create(
ip=remote_addr, object_id=pk, content_type=content_type)

3
apps/rating/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
apps/rating/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -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',
)

View File

@ -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.

View File

@ -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
# 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

View File

@ -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)

View File

@ -1,12 +1,14 @@
"""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 django_elasticsearch_dsl_drf.pagination import PageNumberPagination
from search_indexes import serializers, filters
from search_indexes.documents import EstablishmentDocument, NewsDocument
from utils.pagination import ProjectPageNumberPagination
class NewsDocumentViewSet(BaseDocumentViewSet):
@ -14,33 +16,44 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
document = NewsDocument
lookup_field = 'slug'
pagination_class = PageNumberPagination
pagination_class = ProjectPageNumberPagination
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsDocumentSerializer
ordering = ('id',)
filter_backends = [
filters.CustomSearchFilterBackend,
FilteringFilterBackend,
]
search_fields = (
'title',
'subtitle',
'description',
)
search_fields = {
'title': {'fuzziness': 'auto:3,4'},
'subtitle': {'fuzziness': 'auto'},
'description': {'fuzziness': 'auto'},
}
translated_search_fields = (
'title',
'subtitle',
'description',
)
filter_fields = {
'tag': {
'field': 'tags.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
]
},
'slug': 'slug',
}
class EstablishmentDocumentViewSet(BaseDocumentViewSet):
"""Establishment document ViewSet."""
document = EstablishmentDocument
lookup_field = 'slug'
pagination_class = PageNumberPagination
pagination_class = ProjectPageNumberPagination
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentDocumentSerializer
ordering = ('id',)
@ -51,15 +64,20 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
GeoSpatialFilteringFilterBackend,
]
search_fields = (
'name',
'description',
)
search_fields = {
'name': {'fuzziness': 'auto:3,4'},
'name_translated': {'fuzziness': 'auto:3,4'},
'description': {'fuzziness': 'auto'},
}
translated_search_fields = (
'description',
)
filter_fields = {
'tag': 'tags.id',
'slug': 'slug',
'tag': {
'field': 'tags.id',
'lookups': [constants.LOOKUP_QUERY_IN]
},
'toque_number': {
'field': 'toque_number',
'lookups': [
@ -107,7 +125,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
geo_spatial_filter_fields = {
'location': {
'field': 'address.location',
'field': 'address.coordinates',
'lookups': [
constants.LOOKUP_FILTER_GEO_BOUNDING_BOX,
]

View File

@ -36,3 +36,4 @@ class Timetable(ProjectBaseMixin):
"""Meta class."""
verbose_name = _('Timetable')
verbose_name_plural = _('Timetables')
ordering = ['weekday']

View File

@ -123,7 +123,7 @@ class WrongAuthCredentials(AuthErrorMixin):
"""
The exception should be raised when credentials is not valid for this user
"""
default_detail = _('Wrong authorization credentials')
default_detail = _('Incorrect login or password.')
class FavoritesError(exceptions.APIException):

View File

@ -1,7 +1,7 @@
"""Utils app serializer."""
from rest_framework import serializers
from utils.models import PlatformMixin
from django.core import exceptions
from rest_framework import serializers
from utils import models
from translation.models import Language
@ -11,8 +11,8 @@ class EmptySerializer(serializers.Serializer):
class SourceSerializerMixin(serializers.Serializer):
"""Base authorization serializer mixin"""
source = serializers.ChoiceField(choices=PlatformMixin.SOURCES,
default=PlatformMixin.WEB,
source = serializers.ChoiceField(choices=models.PlatformMixin.SOURCES,
default=models.PlatformMixin.WEB,
write_only=True)
@ -25,18 +25,16 @@ class TranslatedField(serializers.CharField):
read_only=read_only, **kwargs)
# todo: view validation in more detail
def validate_tjson(value):
if not isinstance(value, dict):
raise exceptions.ValidationError(
'invalid_json',
code='invalid_json',
params={'value': value},
)
lang_count = Language.objects.filter(locale__in=value.keys()).count()
if lang_count == 0:
if lang_count != len(value.keys()):
raise exceptions.ValidationError(
'invalid_translated_keys',
code='invalid_translated_keys',
@ -44,5 +42,13 @@ def validate_tjson(value):
)
class TJSONSerializer(serializers.JSONField):
class TJSONField(serializers.JSONField):
"""Custom serializer's JSONField for model's TJSONField."""
validators = [validate_tjson]
class ProjectModelSerializer(serializers.ModelSerializer):
"""Overrided ModelSerializer."""
serializers.ModelSerializer.serializer_field_mapping[models.TJSONField] = TJSONField

View File

@ -12,8 +12,6 @@ services:
- POSTGRES_DB=postgres
ports:
- "5436:5432"
# networks:
# - db-net
volumes:
- gm-db:/var/lib/postgresql/data/
elasticsearch:
@ -28,8 +26,7 @@ services:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.type=single-node
- xpack.security.enabled=false
# networks:
# - app-net
# RabbitMQ
rabbitmq:
image: rabbitmq:latest
@ -83,19 +80,12 @@ services:
- worker
- worker_beat
- elasticsearch
# networks:
# - app-net
# - db-net
volumes:
- .:/code
- gm-media:/media-data
ports:
- "8000:8000"
#networks:
# app-net:
# db-net:
volumes:
gm-db:
name: gm-db

View File

@ -71,7 +71,9 @@ PROJECT_APPS = [
'review.apps.ReviewConfig',
'comment.apps.CommentConfig',
'favorites.apps.FavoritesConfig',
'rating.apps.RatingConfig',
'transfer.apps.TransferConfig'
]
EXTERNAL_APPS = [
@ -289,9 +291,9 @@ SMS_SENDER = 'GM'
# EMAIL
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com'
EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt'
EMAIL_HOST = 'smtp.yandex.ru'
EMAIL_HOST_USER = 't3st.t3stov.t3stovich@yandex.ru'
EMAIL_HOST_PASSWORD = 'ylhernyutkfbylgk'
EMAIL_PORT = 587
# Django Rest Swagger
@ -406,6 +408,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
NEWS_EMAIL_TEMPLATE = "news/news_email.html"
# COOKIES

View File

@ -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',
}

View File

@ -1,5 +1,6 @@
"""Local settings."""
from .base import *
import sys
ALLOWED_HOSTS = ['*', ]
@ -65,5 +66,9 @@ ELASTICSEARCH_DSL = {
ELASTICSEARCH_INDEX_NAMES = {
# 'search_indexes.documents.news': 'local_news',
'search_indexes.documents.establishment': 'local_establishment',
}
'search_indexes.documents.establishment': 'local_establishment',
}
TESTING = sys.argv[1:2] == ['test']
if TESTING:
ELASTICSEARCH_INDEX_NAMES = {}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
{% if subtitle %}
<h3>{{ subtitle }}</h3>
{% endif %}
<p>{{ description }} </p>
https://{{ country_code }}.{{ domain_uri }}{% url 'web:notification:unsubscribe' code %}
</body>
</html>

View File

@ -23,7 +23,7 @@ urlpatterns = [
path('collections/', include('collection.urls.web')),
path('establishments/', include('establishment.urls.web')),
path('news/', include('news.urls.web')),
path('notifications/', include('notification.urls.web')),
path('notifications/', include(('notification.urls.web', "notification"), namespace='notification')),
path('partner/', include('partner.urls.web')),
path('location/', include('location.urls.web')),
path('main/', include('main.urls')),