Merge branch 'develop' into feature/migrate-city-objects
# Conflicts: # docker-compose.yml # project/settings/base.py
This commit is contained in:
commit
22d2396c6b
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
17
apps/news/migrations/0020_remove_news_author.py
Normal file
17
apps/news/migrations/0020_remove_news_author.py
Normal 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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
28
apps/news/tasks.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
0
apps/rating/__init__.py
Normal file
11
apps/rating/admin.py
Normal file
11
apps/rating/admin.py
Normal 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
5
apps/rating/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RatingConfig(AppConfig):
|
||||
name = 'rating'
|
||||
28
apps/rating/migrations/0001_initial.py
Normal file
28
apps/rating/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
22
apps/rating/migrations/0002_auto_20191004_0928.py
Normal file
22
apps/rating/migrations/0002_auto_20191004_0928.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
0
apps/rating/migrations/__init__.py
Normal file
0
apps/rating/migrations/__init__.py
Normal file
22
apps/rating/models.py
Normal file
22
apps/rating/models.py
Normal 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
18
apps/rating/tasks.py
Normal 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
3
apps/rating/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
apps/rating/views.py
Normal file
3
apps/rating/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
@ -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',
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -36,3 +36,4 @@ class Timetable(ProjectBaseMixin):
|
|||
"""Meta class."""
|
||||
verbose_name = _('Timetable')
|
||||
verbose_name_plural = _('Timetables')
|
||||
ordering = ['weekday']
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
20
project/templates/news/news_email.html
Normal file
20
project/templates/news/news_email.html
Normal 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>
|
||||
|
||||
|
|
@ -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')),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user