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
|
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):
|
class ChangePasswordSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for model User."""
|
"""Serializer for model User."""
|
||||||
|
|
||||||
|
|
@ -155,38 +172,6 @@ class ChangeEmailSerializer(serializers.ModelSerializer):
|
||||||
return instance
|
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
|
# Firebase Cloud Messaging serializers
|
||||||
class FCMDeviceSerializer(serializers.ModelSerializer):
|
class FCMDeviceSerializer(serializers.ModelSerializer):
|
||||||
"""FCM Device model serializer"""
|
"""FCM Device model serializer"""
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,6 @@ app_name = 'account'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'),
|
path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'),
|
||||||
path('change-password/', views.ChangePasswordView.as_view(), name='change-password'),
|
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'),
|
path('email/confirm/<uidb64>/<token>/', views.ConfirmEmailView.as_view(), name='confirm-email'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Common account views"""
|
"""Common account views"""
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.http import urlsafe_base64_decode
|
from django.utils.http import urlsafe_base64_decode
|
||||||
from fcm_django.models import FCMDevice
|
from fcm_django.models import FCMDevice
|
||||||
|
|
@ -9,6 +10,7 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from account import models
|
from account import models
|
||||||
from account.serializers import common as serializers
|
from account.serializers import common as serializers
|
||||||
|
from authorization.tasks import send_confirm_email
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.models import GMTokenGenerator
|
from utils.models import GMTokenGenerator
|
||||||
from utils.views import JWTGenericViewMixin
|
from utils.views import JWTGenericViewMixin
|
||||||
|
|
@ -38,19 +40,26 @@ class ChangePasswordView(generics.GenericAPIView):
|
||||||
return Response(status=status.HTTP_200_OK)
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class SendConfirmationEmailView(JWTGenericViewMixin):
|
class SendConfirmationEmailView(generics.GenericAPIView):
|
||||||
"""Confirm email view."""
|
"""Confirm email view."""
|
||||||
serializer_class = serializers.ConfirmEmailSerializer
|
|
||||||
queryset = models.User.objects.all()
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Implement PATCH-method"""
|
"""Override create method"""
|
||||||
# Get user instance
|
user = self.request.user
|
||||||
instance = self.request.user
|
country_code = self.request.country_code
|
||||||
|
|
||||||
serializer = self.get_serializer(data=request.data, instance=instance)
|
if user.email_confirmed:
|
||||||
serializer.is_valid(raise_exception=True)
|
raise utils_exceptions.EmailConfirmedError()
|
||||||
serializer.save()
|
|
||||||
|
# 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)
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,20 @@ from django.db import migrations, models
|
||||||
import uuid
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
@ -23,6 +37,12 @@ class Migration(migrations.Migration):
|
||||||
field=models.ManyToManyField(to='translation.Language'),
|
field=models.ManyToManyField(to='translation.Language'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
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',
|
model_name='advertisement',
|
||||||
name='uuid',
|
name='uuid',
|
||||||
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
|
|
@ -32,8 +52,14 @@ class Migration(migrations.Migration):
|
||||||
name='block_level',
|
name='block_level',
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
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',
|
model_name='advertisement',
|
||||||
name='block_level',
|
name='block_level',
|
||||||
field=models.CharField(max_length=10, verbose_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.contrib.auth import password_validation as password_validators
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from rest_framework import serializers
|
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 account import models as account_models
|
||||||
from authorization import tasks
|
from authorization import tasks
|
||||||
|
|
@ -81,7 +81,6 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
|
||||||
"""Serializer for login user"""
|
"""Serializer for login user"""
|
||||||
# REQUEST
|
# REQUEST
|
||||||
username_or_email = serializers.CharField(write_only=True)
|
username_or_email = serializers.CharField(write_only=True)
|
||||||
password = serializers.CharField(write_only=True)
|
|
||||||
|
|
||||||
# For cookie properties (Max-Age)
|
# For cookie properties (Max-Age)
|
||||||
remember = serializers.BooleanField(write_only=True)
|
remember = serializers.BooleanField(write_only=True)
|
||||||
|
|
@ -101,21 +100,24 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
|
||||||
'refresh_token',
|
'refresh_token',
|
||||||
'access_token',
|
'access_token',
|
||||||
)
|
)
|
||||||
|
extra_kwargs = {
|
||||||
|
'password': {'write_only': True}
|
||||||
|
}
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
"""Override validate method"""
|
"""Override validate method"""
|
||||||
username_or_email = attrs.pop('username_or_email')
|
username_or_email = attrs.pop('username_or_email')
|
||||||
password = attrs.pop('password')
|
password = attrs.pop('password')
|
||||||
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
|
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():
|
if not user_qs.exists():
|
||||||
raise utils_exceptions.UserNotFoundError()
|
raise utils_exceptions.WrongAuthCredentials()
|
||||||
else:
|
else:
|
||||||
user = user_qs.first()
|
user = user_qs.first()
|
||||||
authentication = authenticate(username=user.get_username(),
|
authentication = authenticate(username=user.get_username(),
|
||||||
password=password)
|
password=password)
|
||||||
if not authentication:
|
if not authentication:
|
||||||
raise utils_exceptions.UserNotFoundError()
|
raise utils_exceptions.WrongAuthCredentials()
|
||||||
self.instance = user
|
self.instance = user
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
@ -127,10 +129,6 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
|
||||||
return super().to_representation(instance)
|
return super().to_representation(instance)
|
||||||
|
|
||||||
|
|
||||||
class LogoutSerializer(SourceSerializerMixin):
|
|
||||||
"""Serializer for Logout endpoint."""
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenSerializer(SourceSerializerMixin):
|
class RefreshTokenSerializer(SourceSerializerMixin):
|
||||||
"""Serializer for refresh token view"""
|
"""Serializer for refresh token view"""
|
||||||
refresh_token = serializers.CharField(read_only=True)
|
refresh_token = serializers.CharField(read_only=True)
|
||||||
|
|
@ -169,7 +167,3 @@ class RefreshTokenSerializer(SourceSerializerMixin):
|
||||||
class OAuth2Serialzier(SourceSerializerMixin):
|
class OAuth2Serialzier(SourceSerializerMixin):
|
||||||
"""Serializer OAuth2 authorization"""
|
"""Serializer OAuth2 authorization"""
|
||||||
token = serializers.CharField(max_length=255)
|
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 celery import shared_task
|
||||||
|
|
||||||
from account import models as account_models
|
from account import models as account_models
|
||||||
|
from smtplib import SMTPException
|
||||||
|
|
||||||
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@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."""
|
"""Send verification email to user."""
|
||||||
try:
|
try:
|
||||||
obj = account_models.User.objects.get(id=user_id)
|
obj = account_models.User.objects.get(id=user_id)
|
||||||
obj.send_email(subject=_('Email confirmation'),
|
obj.send_email(subject=_('Email confirmation'),
|
||||||
message=obj.confirm_email_template(country_code))
|
message=obj.confirm_email_template(country_code))
|
||||||
except:
|
except Exception as e:
|
||||||
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
|
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
|
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
|
# OAuth2
|
||||||
class BaseOAuth2ViewMixin(generics.GenericAPIView):
|
class BaseOAuth2ViewMixin(generics.GenericAPIView):
|
||||||
"""BaseMixin for classic auth views"""
|
"""BaseMixin for classic auth views"""
|
||||||
|
|
@ -112,6 +94,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin):
|
||||||
# Preparing request data
|
# Preparing request data
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
request_data = self.prepare_request_data(serializer.validated_data)
|
request_data = self.prepare_request_data(serializer.validated_data)
|
||||||
source = serializer.validated_data.get('source')
|
source = serializer.validated_data.get('source')
|
||||||
request_data.update({
|
request_data.update({
|
||||||
|
|
@ -196,7 +179,7 @@ class ConfirmationEmailView(JWTGenericViewMixin):
|
||||||
|
|
||||||
|
|
||||||
# Login by username|email + password
|
# Login by username|email + password
|
||||||
class LoginByUsernameOrEmailView(JWTAuthViewMixin):
|
class LoginByUsernameOrEmailView(JWTGenericViewMixin):
|
||||||
"""Login by email and password"""
|
"""Login by email and password"""
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = serializers.LoginByUsernameOrEmailSerializer
|
serializer_class = serializers.LoginByUsernameOrEmailSerializer
|
||||||
|
|
@ -205,10 +188,12 @@ class LoginByUsernameOrEmailView(JWTAuthViewMixin):
|
||||||
"""Implement POST method"""
|
"""Implement POST method"""
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
response = Response(serializer.data, status=status.HTTP_200_OK)
|
response = Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
access_token = serializer.data.get('access_token')
|
access_token = serializer.data.get('access_token')
|
||||||
refresh_token = serializer.data.get('refresh_token')
|
refresh_token = serializer.data.get('refresh_token')
|
||||||
is_permanent = serializer.validated_data.get('remember')
|
is_permanent = serializer.validated_data.get('remember')
|
||||||
|
|
||||||
return self._put_cookies_in_response(
|
return self._put_cookies_in_response(
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token,
|
cookies=self._put_data_in_cookies(access_token=access_token,
|
||||||
refresh_token=refresh_token,
|
refresh_token=refresh_token,
|
||||||
|
|
@ -243,9 +228,11 @@ class RefreshTokenView(JWTGenericViewMixin):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
response = Response(serializer.data, status=status.HTTP_201_CREATED)
|
response = Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
access_token = serializer.data.get('access_token')
|
access_token = serializer.data.get('access_token')
|
||||||
refresh_token = serializer.data.get('refresh_token')
|
refresh_token = serializer.data.get('refresh_token')
|
||||||
|
|
||||||
return self._put_cookies_in_response(
|
return self._put_cookies_in_response(
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token,
|
cookies=self._put_data_in_cookies(access_token=access_token,
|
||||||
refresh_token=refresh_token),
|
refresh_token=refresh_token),
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,24 @@ from collection import models
|
||||||
from location import models as location_models
|
from location import models as location_models
|
||||||
|
|
||||||
|
|
||||||
class CollectionSerializer(serializers.ModelSerializer):
|
class CollectionBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Collection serializer"""
|
"""Collection base serializer"""
|
||||||
# RESPONSE
|
# RESPONSE
|
||||||
description_translated = serializers.CharField(read_only=True, allow_null=True)
|
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
|
# COMMON
|
||||||
block_size = serializers.JSONField()
|
block_size = serializers.JSONField()
|
||||||
is_publish = serializers.BooleanField()
|
is_publish = serializers.BooleanField()
|
||||||
|
|
@ -24,18 +37,13 @@ class CollectionSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Collection
|
model = models.Collection
|
||||||
fields = [
|
fields = CollectionBaseSerializer.Meta.fields + [
|
||||||
'id',
|
|
||||||
'name',
|
|
||||||
'description_translated',
|
|
||||||
'start',
|
'start',
|
||||||
'end',
|
'end',
|
||||||
'image_url',
|
|
||||||
'is_publish',
|
'is_publish',
|
||||||
'on_top',
|
'on_top',
|
||||||
'country',
|
'country',
|
||||||
'block_size',
|
'block_size',
|
||||||
'slug',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import json, pytz
|
import json
|
||||||
|
import pytz
|
||||||
from datetime import datetime
|
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 http.cookies import SimpleCookie
|
||||||
|
|
||||||
from collection.models import Collection, Guide
|
from rest_framework import status
|
||||||
from location.models import Country
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
|
from collection.models import Collection, Guide
|
||||||
from establishment.models import Establishment, EstablishmentType
|
from establishment.models import Establishment, EstablishmentType
|
||||||
# Create your tests here.
|
from location.models import Country
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(APITestCase):
|
class BaseTestCase(APITestCase):
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ app_name = 'collection'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.CollectionHomePageView.as_view(), name='list'),
|
path('', views.CollectionHomePageView.as_view(), name='list'),
|
||||||
|
path('<slug:slug>/', views.CollectionDetailView.as_view(), name='detail'),
|
||||||
path('<slug:slug>/establishments/', views.CollectionEstablishmentListView.as_view(),
|
path('<slug:slug>/establishments/', views.CollectionEstablishmentListView.as_view(),
|
||||||
name='detail'),
|
name='detail'),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from rest_framework import permissions
|
||||||
from collection import models
|
from collection import models
|
||||||
from utils.pagination import ProjectPageNumberPagination
|
from utils.pagination import ProjectPageNumberPagination
|
||||||
from django.shortcuts import get_object_or_404
|
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
|
from collection.serializers import common as serializers
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,7 +12,14 @@ from collection.serializers import common as serializers
|
||||||
class CollectionViewMixin(generics.GenericAPIView):
|
class CollectionViewMixin(generics.GenericAPIView):
|
||||||
"""Mixin for Collection view"""
|
"""Mixin for Collection view"""
|
||||||
model = models.Collection
|
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):
|
class GuideViewMixin(generics.GenericAPIView):
|
||||||
|
|
@ -24,40 +31,29 @@ class GuideViewMixin(generics.GenericAPIView):
|
||||||
# Views
|
# Views
|
||||||
# Collections
|
# Collections
|
||||||
class CollectionListView(CollectionViewMixin, generics.ListAPIView):
|
class CollectionListView(CollectionViewMixin, generics.ListAPIView):
|
||||||
"""List Collection view"""
|
"""List Collection view."""
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
serializer_class = serializers.CollectionSerializer
|
|
||||||
|
class CollectionHomePageView(CollectionListView):
|
||||||
|
"""Collection list view for home page."""
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Override get_queryset method"""
|
"""Override get_queryset."""
|
||||||
queryset = models.Collection.objects.published()\
|
return super(CollectionHomePageView, self).get_queryset() \
|
||||||
.by_country_code(code=self.request.country_code)\
|
.filter_all_related_gt(3)
|
||||||
.order_by('-on_top', '-created')
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionHomePageView(CollectionViewMixin, generics.ListAPIView):
|
class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView):
|
||||||
"""List Collection view"""
|
"""Retrieve detail of Collection instance."""
|
||||||
permission_classes = (permissions.AllowAny,)
|
lookup_field = 'slug'
|
||||||
serializer_class = serializers.CollectionSerializer
|
serializer_class = serializers.CollectionBaseSerializer
|
||||||
|
|
||||||
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 CollectionEstablishmentListView(CollectionListView):
|
class CollectionEstablishmentListView(CollectionListView):
|
||||||
"""Retrieve list of establishment for collection."""
|
"""Retrieve list of establishment for collection."""
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
pagination_class = ProjectPageNumberPagination
|
|
||||||
serializer_class = EstablishmentListSerializer
|
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
|
pagination_class = ProjectPageNumberPagination
|
||||||
|
serializer_class = EstablishmentBaseSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
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.contrib.gis.measure import Distance as DistanceMeasure
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
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 import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from elasticsearch_dsl import Q
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
|
|
||||||
from collection.models import Collection
|
from collection.models import Collection
|
||||||
from main.models import MetaDataContent
|
from main.models import Award, MetaDataContent
|
||||||
from location.models import Address
|
from location.models import Address
|
||||||
from review.models import Review
|
from review.models import Review
|
||||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||||
|
|
@ -98,6 +99,16 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
else:
|
else:
|
||||||
return self.none()
|
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):
|
def by_country_code(self, code):
|
||||||
"""Return establishments by country code"""
|
"""Return establishments by country code"""
|
||||||
return self.filter(address__city__country__code=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,
|
slug = models.SlugField(unique=True, max_length=50, null=True,
|
||||||
verbose_name=_('Establishment slug'), editable=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')
|
tags = generic.GenericRelation(to='main.MetaDataContent')
|
||||||
reviews = generic.GenericRelation(to='review.Review')
|
reviews = generic.GenericRelation(to='review.Review')
|
||||||
comments = generic.GenericRelation(to='comment.Comment')
|
comments = generic.GenericRelation(to='comment.Comment')
|
||||||
|
|
@ -330,6 +341,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
||||||
raise ValidationError('Establishment type of subtype does not match')
|
raise ValidationError('Establishment type of subtype does not match')
|
||||||
self.establishment_subtypes.add(establishment_subtype)
|
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
|
@property
|
||||||
def best_price_menu(self):
|
def best_price_menu(self):
|
||||||
return 150
|
return 150
|
||||||
|
|
@ -356,6 +373,11 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
||||||
"""
|
"""
|
||||||
return self.address.coordinates
|
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):
|
class Position(BaseAttributes, TranslatedFieldsMixin):
|
||||||
"""Position model."""
|
"""Position model."""
|
||||||
|
|
@ -409,8 +431,8 @@ class Employee(BaseAttributes):
|
||||||
verbose_name=_('User'))
|
verbose_name=_('User'))
|
||||||
name = models.CharField(max_length=255, verbose_name=_('Last name'))
|
name = models.CharField(max_length=255, verbose_name=_('Last name'))
|
||||||
establishments = models.ManyToManyField(Establishment, related_name='employees',
|
establishments = models.ManyToManyField(Establishment, related_name='employees',
|
||||||
through=EstablishmentEmployee)
|
through=EstablishmentEmployee,)
|
||||||
awards = generic.GenericRelation(to='main.Award')
|
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
|
||||||
tags = generic.GenericRelation(to='main.MetaDataContent')
|
tags = generic.GenericRelation(to='main.MetaDataContent')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from establishment.serializers import (
|
||||||
from utils.decorators import with_base_attributes
|
from utils.decorators import with_base_attributes
|
||||||
|
|
||||||
from main.models import Currency
|
from main.models import Currency
|
||||||
from utils.serializers import TJSONSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
||||||
|
|
@ -89,13 +88,15 @@ class SocialNetworkSerializers(serializers.ModelSerializer):
|
||||||
|
|
||||||
class PlatesSerializers(PlateSerializer):
|
class PlatesSerializers(PlateSerializer):
|
||||||
"""Social network serializers."""
|
"""Social network serializers."""
|
||||||
name = TJSONSerializer
|
|
||||||
currency_id = serializers.PrimaryKeyRelatedField(
|
currency_id = serializers.PrimaryKeyRelatedField(
|
||||||
source='currency',
|
source='currency',
|
||||||
queryset=Currency.objects.all(), write_only=True
|
queryset=Currency.objects.all(), write_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
model = models.Plate
|
model = models.Plate
|
||||||
fields = PlateSerializer.Meta.fields + [
|
fields = PlateSerializer.Meta.fields + [
|
||||||
'name',
|
'name',
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
"""Establishment serializers."""
|
"""Establishment serializers."""
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
from comment import models as comment_models
|
from comment import models as comment_models
|
||||||
from comment.serializers import common as comment_serializers
|
from comment.serializers import common as comment_serializers
|
||||||
from establishment import models
|
from establishment import models
|
||||||
from favorites.models import Favorites
|
from favorites.models import Favorites
|
||||||
from location.serializers import AddressSimpleSerializer
|
from location.serializers import AddressBaseSerializer
|
||||||
from main.models import MetaDataContent
|
from main.models import MetaDataContent
|
||||||
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
|
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
|
||||||
from review import models as review_models
|
from review import models as review_models
|
||||||
from timetable.serialziers import ScheduleRUDSerializer
|
from timetable.serialziers import ScheduleRUDSerializer
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.serializers import TranslatedField
|
from utils.serializers import TranslatedField, ProjectModelSerializer
|
||||||
from utils.serializers import TJSONSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class ContactPhonesSerializer(serializers.ModelSerializer):
|
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)
|
currency = CurrencySerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
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')
|
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
|
||||||
category = TJSONSerializer()
|
|
||||||
category_translated = serializers.CharField(read_only=True)
|
category_translated = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
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')
|
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
|
||||||
category = TJSONSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Menu
|
model = models.Menu
|
||||||
|
|
@ -141,13 +138,14 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
|
||||||
fields = ('id', 'name', 'position_translated', 'awards', 'priority')
|
fields = ('id', 'name', 'position_translated', 'awards', 'priority')
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentBaseSerializer(serializers.ModelSerializer):
|
class EstablishmentBaseSerializer(ProjectModelSerializer):
|
||||||
"""Base serializer for Establishment model."""
|
"""Base serializer for Establishment model."""
|
||||||
|
|
||||||
preview_image = serializers.URLField(source='preview_image_url')
|
preview_image = serializers.URLField(source='preview_image_url')
|
||||||
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
|
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
|
||||||
address = AddressSimpleSerializer()
|
address = AddressBaseSerializer()
|
||||||
tags = MetaDataContentSerializer(many=True)
|
tags = MetaDataContentSerializer(many=True)
|
||||||
|
in_favorites = serializers.BooleanField(allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -168,20 +166,7 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentListSerializer(EstablishmentBaseSerializer):
|
class EstablishmentDetailSerializer(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):
|
|
||||||
"""Serializer for Establishment model."""
|
"""Serializer for Establishment model."""
|
||||||
|
|
||||||
description_translated = TranslatedField()
|
description_translated = TranslatedField()
|
||||||
|
|
@ -198,11 +183,12 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer):
|
||||||
menu = MenuSerializers(source='menu_set', many=True, read_only=True)
|
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_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)
|
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."""
|
"""Meta class."""
|
||||||
|
|
||||||
fields = EstablishmentListSerializer.Meta.fields + [
|
fields = EstablishmentBaseSerializer.Meta.fields + [
|
||||||
'description_translated',
|
'description_translated',
|
||||||
'image',
|
'image',
|
||||||
'subtypes',
|
'subtypes',
|
||||||
|
|
@ -222,18 +208,9 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer):
|
||||||
'best_price_menu',
|
'best_price_menu',
|
||||||
'best_price_carte',
|
'best_price_carte',
|
||||||
'transportation',
|
'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):
|
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer):
|
||||||
"""Create comment serializer"""
|
"""Create comment serializer"""
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
||||||
"""Resource for getting a list of establishments."""
|
"""Resource for getting a list of establishments."""
|
||||||
|
|
||||||
filter_class = filters.EstablishmentFilter
|
filter_class = filters.EstablishmentFilter
|
||||||
serializer_class = serializers.EstablishmentListSerializer
|
serializer_class = serializers.EstablishmentBaseSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden method 'get_queryset'."""
|
"""Overridden method 'get_queryset'."""
|
||||||
|
|
@ -70,7 +70,8 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
|
||||||
|
|
||||||
class EstablishmentSimilarListView(EstablishmentListView):
|
class EstablishmentSimilarListView(EstablishmentListView):
|
||||||
"""Resource for getting a list of establishments."""
|
"""Resource for getting a list of establishments."""
|
||||||
serializer_class = serializers.EstablishmentListSerializer
|
|
||||||
|
serializer_class = serializers.EstablishmentBaseSerializer
|
||||||
pagination_class = EstablishmentPortionPagination
|
pagination_class = EstablishmentPortionPagination
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
@ -96,6 +97,7 @@ class EstablishmentCommentCreateView(generics.CreateAPIView):
|
||||||
|
|
||||||
class EstablishmentCommentListView(generics.ListAPIView):
|
class EstablishmentCommentListView(generics.ListAPIView):
|
||||||
"""View for return list of establishment comments."""
|
"""View for return list of establishment comments."""
|
||||||
|
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = serializers.EstablishmentCommentCreateSerializer
|
serializer_class = serializers.EstablishmentCommentCreateSerializer
|
||||||
|
|
||||||
|
|
@ -153,11 +155,13 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D
|
||||||
|
|
||||||
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
|
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
|
||||||
"""Resource for getting list of nearest establishments."""
|
"""Resource for getting list of nearest establishments."""
|
||||||
serializer_class = serializers.EstablishmentListSerializer
|
|
||||||
|
serializer_class = serializers.EstablishmentBaseSerializer
|
||||||
filter_class = filters.EstablishmentFilter
|
filter_class = filters.EstablishmentFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden method 'get_queryset'."""
|
"""Overridden method 'get_queryset'."""
|
||||||
|
# todo: latitude and longitude
|
||||||
lat = self.request.query_params.get('lat')
|
lat = self.request.query_params.get('lat')
|
||||||
lon = self.request.query_params.get('lon')
|
lon = self.request.query_params.get('lon')
|
||||||
radius = self.request.query_params.get('radius')
|
radius = self.request.query_params.get('radius')
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
"""Views for app favorites."""
|
"""Views for app favorites."""
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment
|
||||||
from establishment.serializers import EstablishmentListSerializer
|
from establishment.serializers import EstablishmentBaseSerializer
|
||||||
from .models import Favorites
|
from .models import Favorites
|
||||||
|
|
||||||
|
|
||||||
class FavoritesBaseView(generics.GenericAPIView):
|
class FavoritesBaseView(generics.GenericAPIView):
|
||||||
"""Base view for Favorites."""
|
"""Base view for Favorites."""
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Override get_queryset method."""
|
"""Override get_queryset method."""
|
||||||
return Favorites.objects.by_user(self.request.user)
|
return Favorites.objects.by_user(self.request.user)
|
||||||
|
|
@ -15,7 +15,8 @@ class FavoritesBaseView(generics.GenericAPIView):
|
||||||
|
|
||||||
class FavoritesEstablishmentListView(generics.ListAPIView):
|
class FavoritesEstablishmentListView(generics.ListAPIView):
|
||||||
"""List views for favorites"""
|
"""List views for favorites"""
|
||||||
serializer_class = EstablishmentListSerializer
|
|
||||||
|
serializer_class = EstablishmentBaseSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Override get_queryset method"""
|
"""Override get_queryset method"""
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ class City(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Address(models.Model):
|
class Address(models.Model):
|
||||||
|
|
||||||
"""Address model."""
|
"""Address model."""
|
||||||
city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE)
|
city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE)
|
||||||
street_name_1 = models.CharField(
|
street_name_1 = models.CharField(
|
||||||
|
|
@ -98,11 +99,11 @@ class Address(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latitude(self):
|
def latitude(self):
|
||||||
return self.coordinates.y
|
return self.coordinates.y if self.coordinates else float(0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def longitude(self):
|
def longitude(self):
|
||||||
return self.coordinates.x
|
return self.coordinates.x if self.coordinates else float(0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location_field_indexing(self):
|
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 import models
|
||||||
from location.serializers import common
|
from location.serializers import common
|
||||||
|
|
||||||
|
|
||||||
class AddressCreateSerializer(common.AddressSerializer):
|
class AddressCreateSerializer(common.AddressDetailSerializer):
|
||||||
"""Address create serializer."""
|
"""Address create serializer."""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Location app common serializers."""
|
"""Location app common serializers."""
|
||||||
from django.contrib.gis.geos import Point
|
from django.contrib.gis.geos import Point
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from location import models
|
from location import models
|
||||||
from utils.serializers import TranslatedField
|
from utils.serializers import TranslatedField
|
||||||
|
|
@ -83,55 +84,18 @@ class CitySerializer(serializers.ModelSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AddressSerializer(serializers.ModelSerializer):
|
class AddressBaseSerializer(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):
|
|
||||||
"""Serializer for address obj in related objects."""
|
"""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:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
|
|
@ -142,4 +106,45 @@ class AddressSimpleSerializer(serializers.ModelSerializer):
|
||||||
'street_name_2',
|
'street_name_2',
|
||||||
'number',
|
'number',
|
||||||
'postal_code',
|
'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."""
|
"""Location app views."""
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework import permissions
|
|
||||||
|
|
||||||
from location import models, serializers
|
from location import models, serializers
|
||||||
from location.views import common
|
from location.views import common
|
||||||
|
|
@ -9,13 +8,13 @@ from location.views import common
|
||||||
# Address
|
# Address
|
||||||
class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView):
|
class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView):
|
||||||
"""Create view for model Address."""
|
"""Create view for model Address."""
|
||||||
serializer_class = serializers.AddressSerializer
|
serializer_class = serializers.AddressDetailSerializer
|
||||||
queryset = models.Address.objects.all()
|
queryset = models.Address.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""RUD view for model Address."""
|
"""RUD view for model Address."""
|
||||||
serializer_class = serializers.AddressSerializer
|
serializer_class = serializers.AddressDetailSerializer
|
||||||
queryset = models.Address.objects.all()
|
queryset = models.Address.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,17 +100,17 @@ class CityUpdateView(CityViewMixin, generics.UpdateAPIView):
|
||||||
# Address
|
# Address
|
||||||
class AddressCreateView(AddressViewMixin, generics.CreateAPIView):
|
class AddressCreateView(AddressViewMixin, generics.CreateAPIView):
|
||||||
"""Create view for model Address"""
|
"""Create view for model Address"""
|
||||||
serializer_class = serializers.AddressSerializer
|
serializer_class = serializers.AddressDetailSerializer
|
||||||
|
|
||||||
|
|
||||||
class AddressRetrieveView(AddressViewMixin, generics.RetrieveAPIView):
|
class AddressRetrieveView(AddressViewMixin, generics.RetrieveAPIView):
|
||||||
"""Retrieve view for model Address"""
|
"""Retrieve view for model Address"""
|
||||||
serializer_class = serializers.AddressSerializer
|
serializer_class = serializers.AddressDetailSerializer
|
||||||
|
|
||||||
|
|
||||||
class AddressListView(AddressViewMixin, generics.ListAPIView):
|
class AddressListView(AddressViewMixin, generics.ListAPIView):
|
||||||
"""List view for model Address"""
|
"""List view for model Address"""
|
||||||
permission_classes = (permissions.AllowAny, )
|
permission_classes = (permissions.AllowAny, )
|
||||||
serializer_class = serializers.AddressSerializer
|
serializer_class = serializers.AddressDetailSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -318,9 +318,9 @@ class Carousel(models.Model):
|
||||||
@property
|
@property
|
||||||
def vintage_year(self):
|
def vintage_year(self):
|
||||||
if hasattr(self.content_object, 'reviews'):
|
if hasattr(self.content_object, 'reviews'):
|
||||||
review_qs = self.content_object.reviews.by_status(Review.READY)
|
last_review = self.content_object.reviews.by_status(Review.READY).last()
|
||||||
if review_qs.exists():
|
if last_review:
|
||||||
return review_qs.last().vintage
|
return last_review.vintage
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def toque_number(self):
|
def toque_number(self):
|
||||||
|
|
@ -337,9 +337,21 @@ class Carousel(models.Model):
|
||||||
if hasattr(self.content_object, 'image_url'):
|
if hasattr(self.content_object, 'image_url'):
|
||||||
return 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
|
@property
|
||||||
def model_name(self):
|
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):
|
class Page(models.Model):
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from rest_framework import serializers
|
||||||
from advertisement.serializers.web import AdvertisementSerializer
|
from advertisement.serializers.web import AdvertisementSerializer
|
||||||
from location.serializers import CountrySerializer
|
from location.serializers import CountrySerializer
|
||||||
from main import models
|
from main import models
|
||||||
|
from establishment.models import Establishment
|
||||||
from utils.serializers import TranslatedField
|
from utils.serializers import TranslatedField
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -141,6 +142,7 @@ class CarouselListSerializer(serializers.ModelSerializer):
|
||||||
image = serializers.URLField(source='image_url')
|
image = serializers.URLField(source='image_url')
|
||||||
awards = AwardBaseSerializer(many=True)
|
awards = AwardBaseSerializer(many=True)
|
||||||
vintage_year = serializers.IntegerField()
|
vintage_year = serializers.IntegerField()
|
||||||
|
last_award = AwardBaseSerializer(source='the_most_recent_award', allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -154,6 +156,8 @@ class CarouselListSerializer(serializers.ModelSerializer):
|
||||||
'public_mark',
|
'public_mark',
|
||||||
'image',
|
'image',
|
||||||
'vintage_year',
|
'vintage_year',
|
||||||
|
'last_award',
|
||||||
|
'slug',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from news import models
|
|
||||||
|
|
||||||
|
from news import models
|
||||||
|
from .tasks import send_email_with_news
|
||||||
|
|
||||||
@admin.register(models.NewsType)
|
@admin.register(models.NewsType)
|
||||||
class NewsTypeAdmin(admin.ModelAdmin):
|
class NewsTypeAdmin(admin.ModelAdmin):
|
||||||
|
|
@ -9,6 +10,17 @@ class NewsTypeAdmin(admin.ModelAdmin):
|
||||||
list_display_links = ['id', 'name']
|
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)
|
@admin.register(models.News)
|
||||||
class NewsAdmin(admin.ModelAdmin):
|
class NewsAdmin(admin.ModelAdmin):
|
||||||
"""News admin."""
|
"""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 django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin
|
from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin
|
||||||
|
from rating.models import Rating
|
||||||
|
|
||||||
|
|
||||||
class NewsType(models.Model):
|
class NewsType(models.Model):
|
||||||
|
|
@ -26,10 +27,24 @@ class NewsType(models.Model):
|
||||||
class NewsQuerySet(models.QuerySet):
|
class NewsQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model News"""
|
"""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):
|
def by_type(self, news_type):
|
||||||
"""Filter News by type"""
|
"""Filter News by type"""
|
||||||
return self.filter(news_type__name=news_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):
|
def by_country_code(self, code):
|
||||||
"""Filter collection by country code."""
|
"""Filter collection by country code."""
|
||||||
return self.filter(country__code=code)
|
return self.filter(country__code=code)
|
||||||
|
|
@ -41,9 +56,16 @@ class NewsQuerySet(models.QuerySet):
|
||||||
models.Q(end__isnull=True)),
|
models.Q(end__isnull=True)),
|
||||||
state__in=self.model.PUBLISHED_STATES, start__lte=now)
|
state__in=self.model.PUBLISHED_STATES, start__lte=now)
|
||||||
|
|
||||||
def with_related(self):
|
# todo: filter by best score
|
||||||
"""Return qs with related objects."""
|
# todo: filter by country?
|
||||||
return self.select_related('news_type', 'country').prefetch_related('tags')
|
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):
|
class News(BaseAttributes, TranslatedFieldsMixin):
|
||||||
|
|
@ -94,15 +116,8 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
||||||
slug = models.SlugField(unique=True, max_length=50,
|
slug = models.SlugField(unique=True, max_length=50,
|
||||||
verbose_name=_('News slug'))
|
verbose_name=_('News slug'))
|
||||||
playlist = models.IntegerField(_('playlist'))
|
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,
|
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
|
||||||
verbose_name=_('State'))
|
verbose_name=_('State'))
|
||||||
author = models.CharField(max_length=255, blank=True, null=True,
|
|
||||||
default=None,verbose_name=_('Author'))
|
|
||||||
|
|
||||||
is_highlighted = models.BooleanField(default=False,
|
is_highlighted = models.BooleanField(default=False,
|
||||||
verbose_name=_('Is highlighted'))
|
verbose_name=_('Is highlighted'))
|
||||||
# TODO: metadata_keys - описание ключей для динамического построения полей метаданных
|
# TODO: metadata_keys - описание ключей для динамического построения полей метаданных
|
||||||
|
|
@ -120,6 +135,8 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
||||||
verbose_name=_('country'))
|
verbose_name=_('country'))
|
||||||
tags = generic.GenericRelation(to='main.MetaDataContent')
|
tags = generic.GenericRelation(to='main.MetaDataContent')
|
||||||
|
|
||||||
|
ratings = generic.GenericRelation(Rating)
|
||||||
|
|
||||||
objects = NewsQuerySet.as_manager()
|
objects = NewsQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -138,3 +155,12 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
||||||
@property
|
@property
|
||||||
def web_url(self):
|
def web_url(self):
|
||||||
return reverse('web:news:rud', kwargs={'slug': self.slug})
|
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."""
|
"""News app common serializers."""
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from account.serializers.common import UserBaseSerializer
|
||||||
from location import models as location_models
|
from location import models as location_models
|
||||||
from location.serializers import CountrySimpleSerializer
|
from location.serializers import CountrySimpleSerializer
|
||||||
from main.serializers import MetaDataContentSerializer
|
from main.serializers import MetaDataContentSerializer
|
||||||
from news import models
|
from news import models
|
||||||
from utils.serializers import TranslatedField
|
from utils.serializers import TranslatedField, ProjectModelSerializer
|
||||||
from account.serializers.common import UserSerializer
|
|
||||||
|
|
||||||
class NewsTypeSerializer(serializers.ModelSerializer):
|
class NewsTypeSerializer(serializers.ModelSerializer):
|
||||||
"""News type serializer."""
|
"""News type serializer."""
|
||||||
|
|
@ -17,7 +18,7 @@ class NewsTypeSerializer(serializers.ModelSerializer):
|
||||||
fields = ('id', 'name')
|
fields = ('id', 'name')
|
||||||
|
|
||||||
|
|
||||||
class NewsBaseSerializer(serializers.ModelSerializer):
|
class NewsBaseSerializer(ProjectModelSerializer):
|
||||||
"""Base serializer for News model."""
|
"""Base serializer for News model."""
|
||||||
|
|
||||||
# read only fields
|
# read only fields
|
||||||
|
|
@ -50,7 +51,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
|
||||||
|
|
||||||
description_translated = TranslatedField()
|
description_translated = TranslatedField()
|
||||||
country = CountrySimpleSerializer(read_only=True)
|
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',
|
state_display = serializers.CharField(source='get_state_display',
|
||||||
read_only=True)
|
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):
|
class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
||||||
"""News back office base serializer."""
|
"""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."""
|
"""News app views."""
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
from news import filters, models, serializers
|
from news import filters, models, serializers
|
||||||
|
from rating.tasks import add_rating
|
||||||
|
|
||||||
class NewsMixinView:
|
class NewsMixinView:
|
||||||
"""News mixin."""
|
"""News mixin."""
|
||||||
|
|
@ -11,7 +11,7 @@ class NewsMixinView:
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
"""Override get_queryset method."""
|
"""Override get_queryset method."""
|
||||||
qs = models.News.objects.with_related().published()\
|
qs = models.News.objects.published().with_base_related()\
|
||||||
.order_by('-is_highlighted', '-created')
|
.order_by('-is_highlighted', '-created')
|
||||||
if self.request.country_code:
|
if self.request.country_code:
|
||||||
qs = qs.by_country_code(self.request.country_code)
|
qs = qs.by_country_code(self.request.country_code)
|
||||||
|
|
@ -20,14 +20,19 @@ class NewsMixinView:
|
||||||
|
|
||||||
class NewsListView(NewsMixinView, generics.ListAPIView):
|
class NewsListView(NewsMixinView, generics.ListAPIView):
|
||||||
"""News list view."""
|
"""News list view."""
|
||||||
|
|
||||||
filter_class = filters.NewsListFilterSet
|
filter_class = filters.NewsListFilterSet
|
||||||
|
|
||||||
|
|
||||||
class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
|
class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
|
||||||
"""News detail view."""
|
"""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):
|
class NewsTypeListView(generics.ListAPIView):
|
||||||
"""NewsType list view."""
|
"""NewsType list view."""
|
||||||
|
|
@ -42,7 +47,7 @@ class NewsBackOfficeMixinView:
|
||||||
"""News back office mixin view."""
|
"""News back office mixin view."""
|
||||||
|
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
queryset = models.News.objects.with_related() \
|
queryset = models.News.objects.with_base_related() \
|
||||||
.order_by('-is_highlighted', '-created')
|
.order_by('-is_highlighted', '-created')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,10 +59,15 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
|
||||||
create_serializers_class = serializers.NewsBackOfficeDetailSerializer
|
create_serializers_class = serializers.NewsBackOfficeDetailSerializer
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
|
"""Override serializer class."""
|
||||||
if self.request.method == 'POST':
|
if self.request.method == 'POST':
|
||||||
return self.create_serializers_class
|
return self.create_serializers_class
|
||||||
return super().get_serializer_class()
|
return super().get_serializer_class()
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Override get_queryset method."""
|
||||||
|
return super().get_queryset().with_extended_related()
|
||||||
|
|
||||||
|
|
||||||
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
|
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
|
||||||
generics.RetrieveUpdateDestroyAPIView):
|
generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
|
@ -65,3 +75,7 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
|
||||||
|
|
||||||
serializer_class = serializers.NewsBackOfficeDetailSerializer
|
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 django.urls import path
|
||||||
from notification.views import common
|
from notification.views import common
|
||||||
|
|
||||||
|
app_name = "notification"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('subscribe/', common.SubscribeView.as_view(), name='subscribe'),
|
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):
|
class EstablishmentDocument(Document):
|
||||||
"""Establishment document."""
|
"""Establishment document."""
|
||||||
|
|
||||||
|
preview_image = fields.KeywordField(attr='preview_image_url')
|
||||||
description = fields.ObjectField(attr='description_indexing',
|
description = fields.ObjectField(attr='description_indexing',
|
||||||
properties=OBJECT_FIELD_PROPERTIES)
|
properties=OBJECT_FIELD_PROPERTIES)
|
||||||
establishment_type = fields.ObjectField(
|
establishment_type = fields.ObjectField(
|
||||||
|
|
@ -50,7 +51,9 @@ class EstablishmentDocument(Document):
|
||||||
fields={'raw': fields.KeywordField()}
|
fields={'raw': fields.KeywordField()}
|
||||||
),
|
),
|
||||||
'number': fields.IntegerField(),
|
'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(
|
'city': fields.ObjectField(
|
||||||
properties={
|
properties={
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
|
|
@ -82,9 +85,10 @@ class EstablishmentDocument(Document):
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'toque_number',
|
'name_translated',
|
||||||
'price_level',
|
'price_level',
|
||||||
'preview_image_url',
|
'toque_number',
|
||||||
|
'public_mark',
|
||||||
'slug',
|
'slug',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,26 @@ NewsIndex.settings(number_of_shards=1, number_of_replicas=1)
|
||||||
class NewsDocument(Document):
|
class NewsDocument(Document):
|
||||||
"""News document."""
|
"""News document."""
|
||||||
|
|
||||||
news_type = fields.NestedField(properties={
|
news_type = fields.ObjectField(properties={'id': fields.IntegerField(),
|
||||||
'id': fields.IntegerField(),
|
'name': fields.KeywordField()})
|
||||||
'name': fields.KeywordField()
|
title = fields.ObjectField(attr='title_indexing',
|
||||||
})
|
properties=OBJECT_FIELD_PROPERTIES)
|
||||||
title = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES)
|
subtitle = fields.ObjectField(attr='subtitle_indexing',
|
||||||
subtitle = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES)
|
properties=OBJECT_FIELD_PROPERTIES)
|
||||||
description = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES)
|
description = fields.ObjectField(attr='description_indexing',
|
||||||
country = fields.NestedField(properties={
|
properties=OBJECT_FIELD_PROPERTIES)
|
||||||
'id': fields.IntegerField(),
|
country = fields.ObjectField(properties={'id': fields.IntegerField(),
|
||||||
'code': fields.KeywordField()
|
'code': fields.KeywordField()})
|
||||||
})
|
|
||||||
web_url = fields.KeywordField(attr='web_url')
|
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:
|
class Django:
|
||||||
|
|
||||||
|
|
@ -32,20 +40,19 @@ class NewsDocument(Document):
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'playlist',
|
'playlist',
|
||||||
|
'start',
|
||||||
|
'end',
|
||||||
|
'slug',
|
||||||
|
'state',
|
||||||
|
'is_highlighted',
|
||||||
|
'image_url',
|
||||||
|
'preview_image_url',
|
||||||
|
'template',
|
||||||
)
|
)
|
||||||
related_models = [models.NewsType]
|
related_models = [models.NewsType]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().published()
|
return super().get_queryset().published().with_base_related()
|
||||||
|
|
||||||
def prepare_title(self, instance):
|
|
||||||
return instance.title
|
|
||||||
|
|
||||||
def prepare_subtitle(self, instance):
|
|
||||||
return instance.subtitle
|
|
||||||
|
|
||||||
def prepare_description(self, instance):
|
|
||||||
return instance.description
|
|
||||||
|
|
||||||
def get_instances_from_related(self, related_instance):
|
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.
|
"""If related_models is set, define how to retrieve the Car instance(s) from the related model.
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,42 @@
|
||||||
"""Search indexes serializers."""
|
"""Search indexes serializers."""
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
|
from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
|
||||||
|
from news.serializers import NewsTypeSerializer
|
||||||
from search_indexes.documents import EstablishmentDocument, NewsDocument
|
from search_indexes.documents import EstablishmentDocument, NewsDocument
|
||||||
from search_indexes.utils import get_translated_value
|
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):
|
class NewsDocumentSerializer(DocumentSerializer):
|
||||||
"""News document serializer."""
|
"""News document serializer."""
|
||||||
|
|
||||||
title_translated = serializers.SerializerMethodField(allow_null=True)
|
title_translated = serializers.SerializerMethodField(allow_null=True)
|
||||||
subtitle_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:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -18,13 +44,14 @@ class NewsDocumentSerializer(DocumentSerializer):
|
||||||
document = NewsDocument
|
document = NewsDocument
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'title',
|
|
||||||
'subtitle',
|
|
||||||
'description',
|
|
||||||
'web_url',
|
|
||||||
'title_translated',
|
'title_translated',
|
||||||
'subtitle_translated',
|
'subtitle_translated',
|
||||||
'description_translated',
|
'is_highlighted',
|
||||||
|
'image_url',
|
||||||
|
'preview_image_url',
|
||||||
|
'news_type',
|
||||||
|
'tags',
|
||||||
|
'slug',
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -35,18 +62,13 @@ class NewsDocumentSerializer(DocumentSerializer):
|
||||||
def get_subtitle_translated(obj):
|
def get_subtitle_translated(obj):
|
||||||
return get_translated_value(obj.subtitle)
|
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):
|
class EstablishmentDocumentSerializer(DocumentSerializer):
|
||||||
"""Establishment document serializer."""
|
"""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:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
|
|
@ -54,43 +76,40 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'public_mark',
|
'name_translated',
|
||||||
'toque_number',
|
|
||||||
'price_level',
|
'price_level',
|
||||||
'description_translated',
|
'toque_number',
|
||||||
'tags',
|
'public_mark',
|
||||||
'address',
|
|
||||||
'collections',
|
|
||||||
'establishment_type',
|
|
||||||
'establishment_subtypes',
|
|
||||||
'preview_image',
|
|
||||||
'slug',
|
'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):
|
# def to_representation(self, instance):
|
||||||
ret = super().to_representation(instance)
|
# ret = super().to_representation(instance)
|
||||||
dict_merge = lambda a, b: a.update(b) or a
|
# 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'] = map(lambda tag: dict_merge(tag, {'label_translated': get_translated_value(tag.pop('label'))}),
|
||||||
ret['tags'])
|
# ret['tags'])
|
||||||
ret['establishment_subtypes'] = map(
|
# ret['establishment_subtypes'] = map(
|
||||||
lambda subtype: dict_merge(subtype, {'name_translated': get_translated_value(subtype.pop('name'))}),
|
# lambda subtype: dict_merge(subtype, {'name_translated': get_translated_value(subtype.pop('name'))}),
|
||||||
ret['establishment_subtypes'])
|
# ret['establishment_subtypes'])
|
||||||
if ret.get('establishment_type'):
|
# if ret.get('establishment_type'):
|
||||||
ret['establishment_type']['name_translated'] = get_translated_value(ret['establishment_type'].pop('name'))
|
# ret['establishment_type']['name_translated'] = get_translated_value(ret['establishment_type'].pop('name'))
|
||||||
if ret.get('address'):
|
# if ret.get('address'):
|
||||||
ret['address']['city']['country']['name_translated'] = get_translated_value(
|
# ret['address']['city']['country']['name_translated'] = get_translated_value(
|
||||||
ret['address']['city']['country'].pop('name'))
|
# ret['address']['city']['country'].pop('name'))
|
||||||
location = ret['address'].pop('location')
|
# location = ret['address'].pop('location')
|
||||||
if location:
|
# if location:
|
||||||
ret['address']['geo_lon'] = location['lon']
|
# ret['address']['geo_lon'] = location['lon']
|
||||||
ret['address']['geo_lat'] = location['lat']
|
# ret['address']['geo_lat'] = location['lat']
|
||||||
|
#
|
||||||
ret['type'] = ret.pop('establishment_type')
|
# ret['type'] = ret.pop('establishment_type')
|
||||||
ret['subtypes'] = ret.pop('establishment_subtypes')
|
# ret['subtypes'] = ret.pop('establishment_subtypes')
|
||||||
|
#
|
||||||
return ret
|
# return ret
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""Search indexes app signals."""
|
"""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.dispatch import receiver
|
||||||
from django_elasticsearch_dsl.registries import registry
|
from django_elasticsearch_dsl.registries import registry
|
||||||
|
|
||||||
|
|
@ -17,17 +17,65 @@ def update_document(sender, **kwargs):
|
||||||
address__city__country=instance)
|
address__city__country=instance)
|
||||||
for establishment in establishments:
|
for establishment in establishments:
|
||||||
registry.update(establishment)
|
registry.update(establishment)
|
||||||
|
|
||||||
if model_name == 'city':
|
if model_name == 'city':
|
||||||
establishments = Establishment.objects.filter(
|
establishments = Establishment.objects.filter(
|
||||||
address__city=instance)
|
address__city=instance)
|
||||||
for establishment in establishments:
|
for establishment in establishments:
|
||||||
registry.update(establishment)
|
registry.update(establishment)
|
||||||
|
|
||||||
if model_name == 'address':
|
if model_name == 'address':
|
||||||
establishments = Establishment.objects.filter(
|
establishments = Establishment.objects.filter(
|
||||||
address=instance)
|
address=instance)
|
||||||
for establishment in establishments:
|
for establishment in establishments:
|
||||||
registry.update(establishment)
|
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."""
|
"""Search indexes app views."""
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from django_elasticsearch_dsl_drf import constants
|
from django_elasticsearch_dsl_drf import constants
|
||||||
from django_elasticsearch_dsl_drf.filter_backends import (FilteringFilterBackend,
|
from django_elasticsearch_dsl_drf.filter_backends import (
|
||||||
GeoSpatialFilteringFilterBackend)
|
FilteringFilterBackend,
|
||||||
|
GeoSpatialFilteringFilterBackend
|
||||||
|
)
|
||||||
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
|
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 import serializers, filters
|
||||||
from search_indexes.documents import EstablishmentDocument, NewsDocument
|
from search_indexes.documents import EstablishmentDocument, NewsDocument
|
||||||
|
from utils.pagination import ProjectPageNumberPagination
|
||||||
|
|
||||||
|
|
||||||
class NewsDocumentViewSet(BaseDocumentViewSet):
|
class NewsDocumentViewSet(BaseDocumentViewSet):
|
||||||
|
|
@ -14,33 +16,44 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
|
||||||
|
|
||||||
document = NewsDocument
|
document = NewsDocument
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
pagination_class = PageNumberPagination
|
pagination_class = ProjectPageNumberPagination
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = serializers.NewsDocumentSerializer
|
serializer_class = serializers.NewsDocumentSerializer
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
filters.CustomSearchFilterBackend,
|
filters.CustomSearchFilterBackend,
|
||||||
|
FilteringFilterBackend,
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = (
|
search_fields = {
|
||||||
'title',
|
'title': {'fuzziness': 'auto:3,4'},
|
||||||
'subtitle',
|
'subtitle': {'fuzziness': 'auto'},
|
||||||
'description',
|
'description': {'fuzziness': 'auto'},
|
||||||
)
|
}
|
||||||
translated_search_fields = (
|
translated_search_fields = (
|
||||||
'title',
|
'title',
|
||||||
'subtitle',
|
'subtitle',
|
||||||
'description',
|
'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
filter_fields = {
|
||||||
|
'tag': {
|
||||||
|
'field': 'tags.id',
|
||||||
|
'lookups': [
|
||||||
|
constants.LOOKUP_QUERY_IN,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'slug': 'slug',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
||||||
"""Establishment document ViewSet."""
|
"""Establishment document ViewSet."""
|
||||||
|
|
||||||
document = EstablishmentDocument
|
document = EstablishmentDocument
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
pagination_class = PageNumberPagination
|
pagination_class = ProjectPageNumberPagination
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = serializers.EstablishmentDocumentSerializer
|
serializer_class = serializers.EstablishmentDocumentSerializer
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
|
|
@ -51,15 +64,20 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
||||||
GeoSpatialFilteringFilterBackend,
|
GeoSpatialFilteringFilterBackend,
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = (
|
search_fields = {
|
||||||
'name',
|
'name': {'fuzziness': 'auto:3,4'},
|
||||||
'description',
|
'name_translated': {'fuzziness': 'auto:3,4'},
|
||||||
)
|
'description': {'fuzziness': 'auto'},
|
||||||
|
}
|
||||||
translated_search_fields = (
|
translated_search_fields = (
|
||||||
'description',
|
'description',
|
||||||
)
|
)
|
||||||
filter_fields = {
|
filter_fields = {
|
||||||
'tag': 'tags.id',
|
'slug': 'slug',
|
||||||
|
'tag': {
|
||||||
|
'field': 'tags.id',
|
||||||
|
'lookups': [constants.LOOKUP_QUERY_IN]
|
||||||
|
},
|
||||||
'toque_number': {
|
'toque_number': {
|
||||||
'field': 'toque_number',
|
'field': 'toque_number',
|
||||||
'lookups': [
|
'lookups': [
|
||||||
|
|
@ -107,7 +125,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
||||||
|
|
||||||
geo_spatial_filter_fields = {
|
geo_spatial_filter_fields = {
|
||||||
'location': {
|
'location': {
|
||||||
'field': 'address.location',
|
'field': 'address.coordinates',
|
||||||
'lookups': [
|
'lookups': [
|
||||||
constants.LOOKUP_FILTER_GEO_BOUNDING_BOX,
|
constants.LOOKUP_FILTER_GEO_BOUNDING_BOX,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,4 @@ class Timetable(ProjectBaseMixin):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
verbose_name = _('Timetable')
|
verbose_name = _('Timetable')
|
||||||
verbose_name_plural = _('Timetables')
|
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
|
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):
|
class FavoritesError(exceptions.APIException):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""Utils app serializer."""
|
"""Utils app serializer."""
|
||||||
from rest_framework import serializers
|
|
||||||
from utils.models import PlatformMixin
|
|
||||||
from django.core import exceptions
|
from django.core import exceptions
|
||||||
|
from rest_framework import serializers
|
||||||
|
from utils import models
|
||||||
from translation.models import Language
|
from translation.models import Language
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,8 +11,8 @@ class EmptySerializer(serializers.Serializer):
|
||||||
|
|
||||||
class SourceSerializerMixin(serializers.Serializer):
|
class SourceSerializerMixin(serializers.Serializer):
|
||||||
"""Base authorization serializer mixin"""
|
"""Base authorization serializer mixin"""
|
||||||
source = serializers.ChoiceField(choices=PlatformMixin.SOURCES,
|
source = serializers.ChoiceField(choices=models.PlatformMixin.SOURCES,
|
||||||
default=PlatformMixin.WEB,
|
default=models.PlatformMixin.WEB,
|
||||||
write_only=True)
|
write_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,18 +25,16 @@ class TranslatedField(serializers.CharField):
|
||||||
read_only=read_only, **kwargs)
|
read_only=read_only, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# todo: view validation in more detail
|
||||||
def validate_tjson(value):
|
def validate_tjson(value):
|
||||||
|
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
raise exceptions.ValidationError(
|
raise exceptions.ValidationError(
|
||||||
'invalid_json',
|
'invalid_json',
|
||||||
code='invalid_json',
|
code='invalid_json',
|
||||||
params={'value': value},
|
params={'value': value},
|
||||||
)
|
)
|
||||||
|
|
||||||
lang_count = Language.objects.filter(locale__in=value.keys()).count()
|
lang_count = Language.objects.filter(locale__in=value.keys()).count()
|
||||||
|
if lang_count != len(value.keys()):
|
||||||
if lang_count == 0:
|
|
||||||
raise exceptions.ValidationError(
|
raise exceptions.ValidationError(
|
||||||
'invalid_translated_keys',
|
'invalid_translated_keys',
|
||||||
code='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]
|
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
|
- POSTGRES_DB=postgres
|
||||||
ports:
|
ports:
|
||||||
- "5436:5432"
|
- "5436:5432"
|
||||||
# networks:
|
|
||||||
# - db-net
|
|
||||||
volumes:
|
volumes:
|
||||||
- gm-db:/var/lib/postgresql/data/
|
- gm-db:/var/lib/postgresql/data/
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
|
|
@ -28,8 +26,7 @@ services:
|
||||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||||
- discovery.type=single-node
|
- discovery.type=single-node
|
||||||
- xpack.security.enabled=false
|
- xpack.security.enabled=false
|
||||||
# networks:
|
|
||||||
# - app-net
|
|
||||||
# RabbitMQ
|
# RabbitMQ
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:latest
|
image: rabbitmq:latest
|
||||||
|
|
@ -83,19 +80,12 @@ services:
|
||||||
- worker
|
- worker
|
||||||
- worker_beat
|
- worker_beat
|
||||||
- elasticsearch
|
- elasticsearch
|
||||||
# networks:
|
|
||||||
# - app-net
|
|
||||||
# - db-net
|
|
||||||
volumes:
|
volumes:
|
||||||
- .:/code
|
- .:/code
|
||||||
- gm-media:/media-data
|
- gm-media:/media-data
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
|
||||||
#networks:
|
|
||||||
# app-net:
|
|
||||||
# db-net:
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
gm-db:
|
gm-db:
|
||||||
name: gm-db
|
name: gm-db
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ PROJECT_APPS = [
|
||||||
'review.apps.ReviewConfig',
|
'review.apps.ReviewConfig',
|
||||||
'comment.apps.CommentConfig',
|
'comment.apps.CommentConfig',
|
||||||
'favorites.apps.FavoritesConfig',
|
'favorites.apps.FavoritesConfig',
|
||||||
|
'rating.apps.RatingConfig',
|
||||||
'transfer.apps.TransferConfig'
|
'transfer.apps.TransferConfig'
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTERNAL_APPS = [
|
EXTERNAL_APPS = [
|
||||||
|
|
@ -289,9 +291,9 @@ SMS_SENDER = 'GM'
|
||||||
|
|
||||||
# EMAIL
|
# EMAIL
|
||||||
EMAIL_USE_TLS = True
|
EMAIL_USE_TLS = True
|
||||||
EMAIL_HOST = 'smtp.gmail.com'
|
EMAIL_HOST = 'smtp.yandex.ru'
|
||||||
EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com'
|
EMAIL_HOST_USER = 't3st.t3stov.t3stovich@yandex.ru'
|
||||||
EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt'
|
EMAIL_HOST_PASSWORD = 'ylhernyutkfbylgk'
|
||||||
EMAIL_PORT = 587
|
EMAIL_PORT = 587
|
||||||
|
|
||||||
# Django Rest Swagger
|
# Django Rest Swagger
|
||||||
|
|
@ -406,6 +408,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
|
||||||
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
|
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
|
||||||
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
||||||
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
||||||
|
NEWS_EMAIL_TEMPLATE = "news/news_email.html"
|
||||||
|
|
||||||
|
|
||||||
# COOKIES
|
# COOKIES
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ ELASTICSEARCH_DSL = {
|
||||||
|
|
||||||
|
|
||||||
ELASTICSEARCH_INDEX_NAMES = {
|
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',
|
'search_indexes.documents.establishment': 'development_establishment',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Local settings."""
|
"""Local settings."""
|
||||||
from .base import *
|
from .base import *
|
||||||
|
import sys
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*', ]
|
ALLOWED_HOSTS = ['*', ]
|
||||||
|
|
||||||
|
|
@ -67,3 +68,7 @@ ELASTICSEARCH_INDEX_NAMES = {
|
||||||
# 'search_indexes.documents.news': 'local_news',
|
# '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('collections/', include('collection.urls.web')),
|
||||||
path('establishments/', include('establishment.urls.web')),
|
path('establishments/', include('establishment.urls.web')),
|
||||||
path('news/', include('news.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('partner/', include('partner.urls.web')),
|
||||||
path('location/', include('location.urls.web')),
|
path('location/', include('location.urls.web')),
|
||||||
path('main/', include('main.urls')),
|
path('main/', include('main.urls')),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user