Merge branch 'develop' of ssh://gl.id-east.ru:222/gm/gm-backend into develop

This commit is contained in:
Dmitriy Kuzmenko 2019-09-27 17:25:51 +03:00
commit 54c392762c
27 changed files with 511 additions and 169 deletions

View File

@ -1,5 +1,6 @@
from rest_framework.test import APITestCase
from account.models import User
from django.urls import reverse
# Create your tests here.
@ -22,7 +23,6 @@ def get_tokens_for_user(
class AuthorizationTests(APITestCase):
def setUp(self):
print("Auth!")
data = get_tokens_for_user()
self.username = data["username"]
self.password = data["password"]
@ -33,7 +33,7 @@ class AuthorizationTests(APITestCase):
'password': self.password,
'remember': True
}
response = self.client.post('/api/auth/login/', data=data)
response = self.client.post(reverse('auth:authorization:login'), data=data)
self.assertEqual(response.data['access_token'], self.tokens.get('access_token'))
self.assertEqual(response.data['refresh_token'], self.tokens.get('refresh_token'))

View File

@ -1,20 +1,23 @@
"""Establishment models."""
from functools import reduce
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.measure import Distance as DistanceMeasure
from django.contrib.gis.geos import Point
from django.conf import settings
from django.contrib.contenttypes import fields as generic
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance as DistanceMeasure
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from location.models import Address
from collection.models import Collection
from main.models import MetaDataContent
from location.models import Address
from review.models import Review
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, URLImageMixin,
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
@ -70,6 +73,20 @@ class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
class EstablishmentQuerySet(models.QuerySet):
"""Extended queryset for Establishment model."""
def with_base_related(self):
"""Return qs with related objects."""
return self.select_related('address').prefetch_related(
models.Prefetch('tags',
MetaDataContent.objects.select_related(
'metadata__category'))
)
def with_extended_related(self):
return self.select_related('establishment_type').\
prefetch_related('establishment_subtypes', 'awards', 'schedule',
'phones').\
prefetch_actual_employees()
def search(self, value, locale=None):
"""Search text in JSON fields."""
if locale is not None:
@ -91,15 +108,20 @@ class EstablishmentQuerySet(models.QuerySet):
"""
return self.filter(is_publish=True)
def annotate_distance(self, point: Point):
def has_published_reviews(self):
"""
Return QuerySet establishments with published reviews.
"""
return self.filter(reviews__status=Review.READY,)
def annotate_distance(self, point: Point = None):
"""
Return QuerySet with annotated field - distance
Description:
"""
return self.annotate(distance=models.Value(
DistanceMeasure(Distance('address__coordinates', point, srid=4236)).m,
output_field=models.FloatField()))
return self.annotate(distance=Distance('address__coordinates', point,
srid=settings.GEO_DEFAULT_SRID))
def annotate_intermediate_public_mark(self):
"""
@ -108,11 +130,11 @@ class EstablishmentQuerySet(models.QuerySet):
If establishments in collection POP and its mark is null, then
intermediate_mark is set to 10;
"""
return self.annotate(intermediate_public_mark=models.Case(
models.When(
return self.annotate(intermediate_public_mark=Case(
When(
collections__collection_type=Collection.POP,
public_mark__isnull=True,
then=10
then=settings.DEFAULT_ESTABLISHMENT_PUBLIC_MARK
),
default='public_mark',
output_field=models.FloatField()))
@ -123,38 +145,50 @@ class EstablishmentQuerySet(models.QuerySet):
Description:
Similarity mark determined by comparison with compared establishment mark
"""
return self.annotate(mark_similarity=models.ExpressionWrapper(
mark - models.F('intermediate_public_mark'),
return self.annotate(mark_similarity=ExpressionWrapper(
mark - F('intermediate_public_mark'),
output_field=models.FloatField()
))
def similar(self, establishment_slug: str, output_objects: int = 12):
def similar(self, establishment_slug: str):
"""
Return QuerySet with objects that similar to Establishment.
:param establishment_slug: str Establishment slug
:param output_objects: int of output objects
"""
establishment_qs = Establishment.objects.filter(slug=establishment_slug,
public_mark__isnull=False)
establishment_qs = self.filter(slug=establishment_slug,
public_mark__isnull=False)
if establishment_qs.exists():
establishment = establishment_qs.first()
# TODO fix error:
# AttributeError: 'NoneType' object has no attribute 'coordinates'
return self.exclude(slug=establishment_slug) \
.filter(is_publish=True,
image_url__isnull=False,
reviews__isnull=False,
reviews__status=Review.READY,
public_mark__gte=10) \
.annotate_distance(point=establishment.address.coordinates) \
subquery_filter_by_distance = Subquery(
self.exclude(slug=establishment_slug)
.filter(image_url__isnull=False, public_mark__gte=10)
.has_published_reviews()
.annotate_distance(point=establishment.location)
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
.values('id')
)
return self.filter(id__in=subquery_filter_by_distance) \
.annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=establishment.public_mark) \
.order_by('distance') \
.order_by('mark_similarity')[:output_objects]
.order_by('mark_similarity')
else:
return self.none()
def last_reviewed(self, point: Point):
"""
Return QuerySet with last reviewed establishments.
:param point: location Point object, needs to ordering
"""
subquery_filter_by_distance = Subquery(
self.filter(image_url__isnull=False, public_mark__gte=10)
.has_published_reviews()
.annotate_distance(point=point)
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
.values('id')
)
return self.filter(id__in=subquery_filter_by_distance) \
.order_by('-reviews__published_at')
def prefetch_actual_employees(self):
"""Prefetch actual employees."""
return self.prefetch_related(
@ -168,10 +202,10 @@ class EstablishmentQuerySet(models.QuerySet):
favorite_establishments = []
if user.is_authenticated:
favorite_establishments = user.favorites.by_content_type(app_label='establishment',
model='establishment')\
model='establishment') \
.values_list('object_id', flat=True)
return self.annotate(in_favorites=models.Case(
models.When(
return self.annotate(in_favorites=Case(
When(
id__in=favorite_establishments,
then=True),
default=False,
@ -195,7 +229,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
name = models.CharField(_('name'), max_length=255, default='')
name_translated = models.CharField(_('Transliterated name'),
max_length=255, default='')
max_length=255, default='')
description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'),
help_text='{"en-GB":"some text"}')
@ -279,12 +313,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
return country.low_price, country.high_price
# todo: make via prefetch
@property
def subtypes(self):
return EstablishmentSubType.objects.filter(
subtype_establishment=self,
establishment_type=self.establishment_type,
establishment_type__use_subtypes=True)
# @property
# def subtypes(self):
# return EstablishmentSubType.objects.filter(
# subtype_establishment=self,
# establishment_type=self.establishment_type,
# establishment_type__use_subtypes=True)
def set_establishment_type(self, establishment_type):
self.establishment_type = establishment_type
@ -309,6 +343,19 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
return [{'id': tag.metadata.id,
'label': tag.metadata.label} for tag in self.tags.all()]
@property
def last_published_review(self):
"""Return last published review"""
return self.reviews.published()\
.order_by('-published_at').first()
@property
def location(self):
"""
Return Point object of establishment location
"""
return self.address.coordinates
class Position(BaseAttributes, TranslatedFieldsMixin):
"""Position model."""

View File

@ -1,16 +1,14 @@
import json
from rest_framework import serializers
from establishment import models
from timetable.models import Timetable
from establishment.serializers import (
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
ContactPhonesSerializer, SocialNetworkRelatedSerializers, EstablishmentDetailSerializer
)
ContactPhonesSerializer, SocialNetworkRelatedSerializers,
EstablishmentTypeSerializer)
from utils.decorators import with_base_attributes
from main.models import Currency
from utils.serializers import TJSONSerializer
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
@ -24,6 +22,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
emails = ContactEmailsSerializer(read_only=True, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=True, many=True, )
slug = serializers.SlugField(required=True, allow_blank=False, max_length=50)
type = EstablishmentTypeSerializer(source='establishment_type')
class Meta:
model = models.Establishment
@ -55,6 +54,7 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer):
phones = ContactPhonesSerializer(read_only=False, many=True, )
emails = ContactEmailsSerializer(read_only=False, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=False, many=True, )
type = EstablishmentTypeSerializer(source='establishment_type')
class Meta:
model = models.Establishment
@ -89,7 +89,7 @@ class SocialNetworkSerializers(serializers.ModelSerializer):
class PlatesSerializers(PlateSerializer):
"""Social network serializers."""
name = serializers.JSONField()
name = TJSONSerializer
currency_id = serializers.PrimaryKeyRelatedField(
source='currency',
queryset=Currency.objects.all(), write_only=True

View File

@ -1,17 +1,18 @@
"""Establishment serializers."""
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from comment import models as comment_models
from comment.serializers import common as comment_serializers
from establishment import models
from favorites.models import Favorites
from location.serializers import AddressSerializer
from location.serializers import AddressSimpleSerializer
from main.models import MetaDataContent
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
from review import models as review_models
from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions
from django.utils.translation import gettext_lazy as _
from utils.serializers import TranslatedField
from utils.serializers import TJSONSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -60,7 +61,7 @@ class PlateSerializer(serializers.ModelSerializer):
class MenuSerializers(serializers.ModelSerializer):
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
category = serializers.JSONField()
category = TJSONSerializer()
category_translated = serializers.CharField(read_only=True)
class Meta:
@ -74,9 +75,9 @@ class MenuSerializers(serializers.ModelSerializer):
]
class MenuRUDSerializers(serializers.ModelSerializer):
class MenuRUDSerializers(serializers.ModelSerializer, ):
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
category = serializers.JSONField()
category = TJSONSerializer()
class Meta:
model = models.Menu
@ -142,11 +143,11 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
class EstablishmentBaseSerializer(serializers.ModelSerializer):
"""Base serializer for Establishment model."""
type = EstablishmentTypeSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeSerializer(many=True)
address = AddressSerializer()
tags = MetaDataContentSerializer(many=True)
preview_image = serializers.URLField(source='preview_image_url')
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
address = AddressSimpleSerializer()
tags = MetaDataContentSerializer(many=True)
class Meta:
"""Meta class."""
@ -159,60 +160,53 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer):
'price_level',
'toque_number',
'public_mark',
'type',
'subtypes',
'slug',
'preview_image',
'in_favorites',
'address',
'tags',
'slug',
]
class EstablishmentListSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
# Annotated fields
in_favorites = serializers.BooleanField(allow_null=True)
preview_image = serializers.URLField(source='preview_image_url')
class Meta:
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
model = models.Establishment
fields = EstablishmentBaseSerializer.Meta.fields + [
'in_favorites',
'preview_image',
]
class EstablishmentDetailSerializer(EstablishmentListSerializer):
"""Serializer for Establishment model."""
description_translated = serializers.CharField(allow_null=True)
description_translated = TranslatedField()
image = serializers.URLField(source='image_url')
type = EstablishmentTypeSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeSerializer(many=True, source='establishment_subtypes')
awards = AwardSerializer(many=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
phones = ContactPhonesSerializer(read_only=True, many=True, )
emails = ContactEmailsSerializer(read_only=True, many=True, )
review = serializers.SerializerMethodField()
phones = ContactPhonesSerializer(read_only=True, many=True)
emails = ContactEmailsSerializer(read_only=True, many=True)
review = ReviewSerializer(source='last_published_review', allow_null=True)
employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees',
many=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_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
slug = serializers.SlugField(required=True, allow_blank=False, max_length=50)
in_favorites = serializers.SerializerMethodField()
image = serializers.URLField(source='image_url')
class Meta:
class Meta(EstablishmentListSerializer.Meta):
"""Meta class."""
model = models.Establishment
fields = EstablishmentListSerializer.Meta.fields + [
'description_translated',
'price_level',
'image',
'subtypes',
'type',
'awards',
'schedule',
'website',
@ -228,23 +222,17 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer):
'best_price_menu',
'best_price_carte',
'transportation',
'slug',
]
def get_review(self, obj):
"""Serializer method for getting last published review"""
return ReviewSerializer(obj.reviews.by_status(status=review_models.Review.READY)
.order_by('-published_at').first()).data
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
# 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):

View File

@ -5,7 +5,6 @@ from rest_framework import status
from http.cookies import SimpleCookie
from main.models import Currency
from establishment.models import Establishment, EstablishmentType, Menu
# Create your tests here.

View File

@ -8,6 +8,8 @@ app_name = 'establishment'
urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('tags/', views.EstablishmentTagListView.as_view(), name='tags'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
path('slug/<slug:slug>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),

View File

@ -4,10 +4,17 @@ from rest_framework import generics
from establishment import models
from establishment import serializers
from establishment.views.common import EstablishmentMixin
class EstablishmentListCreateView(EstablishmentMixin, generics.ListCreateAPIView):
class EstablishmentMixinViews:
"""Establishment mixin."""
def get_queryset(self):
"""Overrided method 'get_queryset'."""
return models.Establishment.objects.published().with_base_related()
class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAPIView):
"""Establishment list/create view."""
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentListCreateSerializer

View File

@ -1,16 +0,0 @@
"""Establishment app views."""
from rest_framework import permissions
from establishment import models
class EstablishmentMixin:
"""Establishment mixin."""
permission_classes = (permissions.AllowAny,)
def get_queryset(self):
"""Overrided method 'get_queryset'."""
return models.Establishment.objects.published() \
.prefetch_actual_employees()

View File

@ -1,5 +1,5 @@
"""Establishment app views."""
from django.conf import settings
from django.contrib.gis.geos import Point
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
@ -7,36 +7,76 @@ from rest_framework import generics, permissions
from comment import models as comment_models
from establishment import filters
from establishment import models, serializers
from establishment.views import EstablishmentMixin
from main import methods
from main.models import MetaDataContent
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.pagination import EstablishmentPortionPagination
class EstablishmentListView(EstablishmentMixin, generics.ListAPIView):
class EstablishmentMixinView:
"""Establishment mixin."""
permission_classes = (permissions.AllowAny,)
def get_queryset(self):
"""Overrided method 'get_queryset'."""
return models.Establishment.objects.published().with_base_related().\
annotate_in_favorites(self.request.user)
class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
"""Resource for getting a list of establishments."""
serializer_class = serializers.EstablishmentListSerializer
filter_class = filters.EstablishmentFilter
serializer_class = serializers.EstablishmentListSerializer
def get_queryset(self):
"""Overridden method 'get_queryset'."""
qs = super(EstablishmentListView, self).get_queryset()
return qs.by_country_code(code=self.request.country_code) \
.annotate_in_favorites(user=self.request.user)
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
"""Resource for getting a establishment."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentDetailSerializer
def get_queryset(self):
return super().get_queryset().with_extended_related()
class EstablishmentRecentReviewListView(EstablishmentListView):
"""List view for last reviewed establishments."""
pagination_class = EstablishmentPortionPagination
def get_queryset(self):
"""Overridden method 'get_queryset'."""
qs = super().get_queryset()
user_ip = methods.get_user_ip(self.request)
query_params = self.request.query_params
if 'longitude' in query_params and 'latitude' in query_params:
longitude, latitude = query_params.get('longitude'), query_params.get('latitude')
else:
longitude, latitude = methods.determine_coordinates(user_ip)
if not longitude or not latitude:
return qs.none()
point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID)
return qs.last_reviewed(point=point)
class EstablishmentSimilarListView(EstablishmentListView):
"""Resource for getting a list of establishments."""
serializer_class = serializers.EstablishmentListSerializer
pagination_class = EstablishmentPortionPagination
def get_queryset(self):
"""Override get_queryset method"""
return super().get_queryset().similar(establishment_slug=self.kwargs.get('slug'))
class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView):
"""Resource for getting a establishment."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentDetailSerializer
qs = super().get_queryset()
return qs.similar(establishment_slug=self.kwargs.get('slug'))
class EstablishmentTypeListView(generics.ListAPIView):

View File

@ -1,7 +1,6 @@
"""Location app common serializers."""
from django.contrib.gis.geos import Point
from rest_framework import serializers
from location import models
from utils.serializers import TranslatedField
@ -86,6 +85,7 @@ class CitySerializer(serializers.ModelSerializer):
class AddressSerializer(serializers.ModelSerializer):
"""Address serializer."""
city_id = serializers.PrimaryKeyRelatedField(
source='city',
queryset=models.City.objects.all())
@ -128,3 +128,18 @@ class AddressSerializer(serializers.ModelSerializer):
setattr(instance, 'geo_lon', float(0))
return super().to_representation(instance)
class AddressSimpleSerializer(serializers.ModelSerializer):
"""Serializer for address obj in related objects."""
class Meta:
"""Meta class."""
model = models.Address
fields = (
'id',
'street_name_1',
'street_name_2',
'number',
'postal_code',
)

View File

@ -1,9 +1,10 @@
"""Main app methods."""
import logging
from django.conf import settings
from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
from main import models
from main import models
logger = logging.getLogger(__name__)
@ -38,6 +39,19 @@ def determine_country_code(ip_addr):
return country_code
def determine_coordinates(ip_addr):
longitude, latitude = None, None
if ip_addr:
try:
geoip = GeoIP2()
longitude, latitude = geoip.coords(ip_addr)
except GeoIP2Exception as ex:
logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}')
except Exception as ex:
logger.error(f'GEOIP Base exception: {ex}')
return longitude, latitude
def determine_user_site_url(country_code):
"""Determine user's site url."""
try:

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.4 on 2019-09-27 08:45
from django.db import migrations
from django.core.validators import EMPTY_VALUES
def fill_slug(apps,schemaeditor):
News = apps.get_model('news', 'News')
for news in News.objects.all():
if news.slug in EMPTY_VALUES:
news.slug = f'Slug_{news.id}'
news.save()
class Migration(migrations.Migration):
dependencies = [
('news', '0013_auto_20190924_0806'),
]
operations = [
migrations.RunPython(fill_slug, migrations.RunPython.noop)
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-27 08:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0014_auto_20190927_0845'),
]
operations = [
migrations.AlterField(
model_name='news',
name='slug',
field=models.SlugField(unique=True, verbose_name='News slug'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-27 13:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0015_auto_20190927_0853'),
]
operations = [
migrations.AddField(
model_name='news',
name='template',
field=models.PositiveIntegerField(choices=[(0, 'newspaper'), (1, 'main.pdf.erb'), (2, 'main')], default=0),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.4 on 2019-09-27 14:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0016_news_template'),
]
operations = [
migrations.RemoveField(
model_name='news',
name='is_publish',
),
migrations.AddField(
model_name='news',
name='state',
field=models.PositiveSmallIntegerField(choices=[(0, 'Waiting'), (1, 'Hidden'), (2, 'Published'), (3, 'Published exclusive')], default=0, verbose_name='State'),
),
]

View File

@ -39,7 +39,7 @@ class NewsQuerySet(models.QuerySet):
now = timezone.now()
return self.filter(models.Q(models.Q(end__gte=now) |
models.Q(end__isnull=True)),
is_publish=True, start__lte=now)
state__in=[self.model.PUBLISHED_STATES], start__lte=now)
def with_related(self):
"""Return qs with related objects."""
@ -49,6 +49,34 @@ class NewsQuerySet(models.QuerySet):
class News(BaseAttributes, TranslatedFieldsMixin):
"""News model."""
STR_FIELD_NAME = 'title'
# TEMPLATE CHOICES
NEWSPAPER = 0
MAIN_PDF_ERB = 1
MAIN = 2
TEMPLATE_CHOICES = (
(NEWSPAPER, 'newspaper'),
(MAIN_PDF_ERB, 'main.pdf.erb'),
(MAIN, 'main'),
)
# STATE CHOICES
WAITING = 0
HIDDEN = 1
PUBLISHED = 2
PUBLISHED_EXCLUSIVE = 3
PUBLISHED_STATES = [PUBLISHED, PUBLISHED_EXCLUSIVE]
STATE_CHOICES = (
(WAITING, _('Waiting')),
(HIDDEN, _('Hidden')),
(PUBLISHED, _('Published')),
(PUBLISHED_EXCLUSIVE, _('Published exclusive')),
)
news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT,
verbose_name=_('news type'))
title = TJSONField(blank=True, null=True, default=None,
@ -63,11 +91,11 @@ class News(BaseAttributes, TranslatedFieldsMixin):
start = models.DateTimeField(verbose_name=_('Start'))
end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('End'))
slug = models.SlugField(unique=True, max_length=50, null=True,
verbose_name=_('News slug'), editable=True,)
slug = models.SlugField(unique=True, max_length=50,
verbose_name=_('News slug'))
playlist = models.IntegerField(_('playlist'))
is_publish = models.BooleanField(default=False,
verbose_name=_('Publish status'))
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
author = models.CharField(max_length=255, blank=True, null=True,
default=None,verbose_name=_('Author'))
is_highlighted = models.BooleanField(default=False,
@ -78,6 +106,7 @@ class News(BaseAttributes, TranslatedFieldsMixin):
verbose_name=_('Image URL path'))
preview_image_url = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Preview image URL path'))
template = models.PositiveIntegerField(choices=TEMPLATE_CHOICES, default=NEWSPAPER)
address = models.ForeignKey('location.Address', blank=True, null=True,
default=None, verbose_name=_('address'),
on_delete=models.SET_NULL)
@ -97,6 +126,10 @@ class News(BaseAttributes, TranslatedFieldsMixin):
def __str__(self):
return f'news: {self.slug}'
@property
def is_publish(self):
return self.state in self.PUBLISHED_STATES
@property
def web_url(self):
return reverse('web:news:rud', kwargs={'slug': self.slug})

View File

@ -28,8 +28,6 @@ class NewsBaseSerializer(serializers.ModelSerializer):
news_type = NewsTypeSerializer(read_only=True)
tags = MetaDataContentSerializer(read_only=True, many=True)
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
class Meta:
"""Meta class."""
@ -52,6 +50,8 @@ class NewsDetailSerializer(NewsBaseSerializer):
description_translated = TranslatedField()
country = CountrySimpleSerializer(read_only=True)
state_display = serializers.CharField(source='get_state_display',
read_only=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
@ -62,6 +62,8 @@ class NewsDetailSerializer(NewsBaseSerializer):
'end',
'playlist',
'is_publish',
'state',
'state_display',
'author',
'country',
)
@ -86,10 +88,11 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
news_type_id = serializers.PrimaryKeyRelatedField(
source='news_type', write_only=True,
queryset=models.NewsType.objects.all())
country_id = serializers.PrimaryKeyRelatedField(
source='country', write_only=True,
queryset=location_models.Country.objects.all())
template_display = serializers.CharField(source='get_template_display',
read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
"""Meta class."""
@ -99,5 +102,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'description',
'news_type_id',
'country_id',
'template',
'template_display',
)

View File

@ -23,6 +23,10 @@ class ReviewQuerySet(models.QuerySet):
"""Filter by status"""
return self.filter(status=status)
def published(self):
"""Return published reviews"""
return self.filter(status=Review.READY)
class Review(BaseAttributes, TranslatedFieldsMixin):
"""Review model"""

View File

@ -46,6 +46,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
description_translated = serializers.SerializerMethodField(allow_null=True)
preview_image = serializers.URLField(source='preview_image_url')
class Meta:
"""Meta class."""
@ -53,7 +54,6 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
fields = (
'id',
'name',
'description',
'public_mark',
'toque_number',
'price_level',
@ -63,10 +63,34 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
'collections',
'establishment_type',
'establishment_subtypes',
'preview_image_url',
'preview_image',
'slug',
)
@staticmethod
def get_description_translated(obj):
return get_translated_value(obj.description)
def to_representation(self, instance):
ret = super().to_representation(instance)
dict_merge = lambda a, b: a.update(b) or a
ret['tags'] = map(lambda tag: dict_merge(tag, {'label_translated': get_translated_value(tag.pop('label'))}),
ret['tags'])
ret['establishment_subtypes'] = map(
lambda subtype: dict_merge(subtype, {'name_translated': get_translated_value(subtype.pop('name'))}),
ret['establishment_subtypes'])
if ret.get('establishment_type'):
ret['establishment_type']['name_translated'] = get_translated_value(ret['establishment_type'].pop('name'))
if ret.get('address'):
ret['address']['city']['country']['name_translated'] = get_translated_value(
ret['address']['city']['country'].pop('name'))
location = ret['address'].pop('location')
if location:
ret['address']['geo_lon'] = location['lon']
ret['address']['geo_lat'] = location['lat']
ret['type'] = ret.pop('establishment_type')
ret['subtypes'] = ret.pop('establishment_subtypes')
return ret

View File

@ -6,5 +6,6 @@ from search_indexes import views
router = routers.SimpleRouter()
# router.register(r'news', views.NewsDocumentViewSet, basename='news') # temporarily disabled
router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment')
router.register(r'mobile/establishments', views.EstablishmentDocumentViewSet, basename='establishment-mobile')
urlpatterns = router.urls

View File

@ -17,4 +17,6 @@ def get_translated_value(value):
return None
elif not isinstance(value, dict):
field_dict = value.to_dict()
elif isinstance(value, dict):
field_dict = value
return field_dict.get(get_current_language())

View File

@ -19,6 +19,7 @@ def parse_cookies(get_response):
cookie_dict = request.COOKIES
# processing locale cookie
locale = get_locale(cookie_dict)
# todo: don't use DB!!! Use cache
if not Language.objects.filter(locale=locale).exists():
locale = TranslationSettings.get_solo().default_language
translation.activate(locale)
@ -31,14 +32,3 @@ def parse_cookies(get_response):
response = get_response(request)
return response
return middleware
class CORSMiddleware:
"""Added parameter {Access-Control-Allow-Origin: *} to response"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response["Access-Control-Allow-Origin"] = '*'
return response

View File

@ -10,6 +10,8 @@ from django.utils.translation import ugettext_lazy as _, get_language
from easy_thumbnails.fields import ThumbnailerImageField
from utils.methods import image_path, svg_image_path
from utils.validators import svg_image_validator
from django.db.models.fields import Field
from django.core import exceptions
class ProjectBaseMixin(models.Model):
@ -26,6 +28,10 @@ class ProjectBaseMixin(models.Model):
abstract = True
def valid(value):
print("Run")
class TJSONField(JSONField):
"""Overrided JsonField."""
@ -52,6 +58,7 @@ def translate_field(self, field_name):
if isinstance(field, dict):
return field.get(to_locale(get_language()))
return None
return translate
@ -70,6 +77,7 @@ def index_field(self, field_name):
for key, value in field.items():
setattr(obj, key, value)
return obj
return index
@ -236,7 +244,8 @@ class LocaleManagerMixin(models.Manager):
queryset = self.filter(**filters)
# Prepare field for annotator
localized_fields = {f'{field}_{prefix}': KeyTextTransform(f'{locale}', field) for field in fields}
localized_fields = {f'{field}_{prefix}': KeyTextTransform(f'{locale}', field) for field in
fields}
# Annotate them
for _ in fields:
@ -245,7 +254,6 @@ class LocaleManagerMixin(models.Manager):
class GMTokenGenerator(PasswordResetTokenGenerator):
CHANGE_EMAIL = 0
RESET_PASSWORD = 1
CHANGE_PASSWORD = 2
@ -268,10 +276,10 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
"""
fields = [str(timestamp), str(user.is_active), str(user.pk)]
if self.purpose == self.CHANGE_EMAIL or \
self.purpose == self.CONFIRM_EMAIL:
self.purpose == self.CONFIRM_EMAIL:
fields.extend([str(user.email_confirmed), str(user.email)])
elif self.purpose == self.RESET_PASSWORD or \
self.purpose == self.CHANGE_PASSWORD:
self.purpose == self.CHANGE_PASSWORD:
fields.append(str(user.password))
return fields

View File

@ -1,6 +1,8 @@
"""Pagination settings."""
from base64 import b64encode
from urllib import parse as urlparse
from django.conf import settings
from rest_framework.pagination import PageNumberPagination, CursorPagination
@ -44,3 +46,10 @@ class ProjectMobilePagination(ProjectPageNumberPagination):
if not self.page.has_previous():
return None
return self.page.previous_page_number()
class EstablishmentPortionPagination(ProjectMobilePagination):
"""
Pagination for app establishments with limit page size equal to 12
"""
page_size = settings.LIMITING_OUTPUT_OBJECTS

View File

@ -1,6 +1,8 @@
"""Utils app serializer."""
from rest_framework import serializers
from utils.models import PlatformMixin
from django.core import exceptions
from translation.models import Language
class EmptySerializer(serializers.Serializer):
@ -21,3 +23,26 @@ class TranslatedField(serializers.CharField):
**kwargs):
super().__init__(allow_null=allow_null, required=required,
read_only=read_only, **kwargs)
def validate_tjson(value):
if not isinstance(value, dict):
raise exceptions.ValidationError(
'invalid_json',
code='invalid_json',
params={'value': value},
)
lang_count = Language.objects.filter(locale__in=value.keys()).count()
if lang_count == 0:
raise exceptions.ValidationError(
'invalid_translated_keys',
code='invalid_translated_keys',
params={'value': value},
)
class TJSONSerializer(serializers.JSONField):
validators = [validate_tjson]

View File

@ -1,5 +1,5 @@
import pytz
from datetime import datetime
from datetime import datetime, timedelta
from rest_framework.test import APITestCase
from rest_framework import status
@ -8,6 +8,11 @@ from http.cookies import SimpleCookie
from account.models import User
from news.models import News, NewsType
from django.test import TestCase
from translation.models import Language
from django.core import exceptions
from .serializers import validate_tjson
from establishment.models import Establishment, EstablishmentType, Employee
@ -47,17 +52,18 @@ class TranslateFieldTests(BaseTestCase):
},
description={"en-GB": "Test description"},
playlist=1,
start=datetime.now(pytz.utc),
end=datetime.now(pytz.utc),
start=datetime.now(pytz.utc) + timedelta(hours=-13),
end=datetime.now(pytz.utc) + timedelta(hours=13),
is_publish=True,
news_type=self.news_type
news_type=self.news_type,
slug='test',
)
def test_model_field(self):
self.assertIsNotNone(getattr(self.news_item, "title_translated", None))
def test_read_locale(self):
response = self.client.get(f"/api/web/news/{self.news_item.id}/", format='json')
response = self.client.get(f"/api/web/news/slug/{self.news_item.slug}/", format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
news_data = response.json()
@ -118,3 +124,36 @@ class BaseAttributeTests(BaseTestCase):
employee.refresh_from_db()
self.assertEqual(modify_user, employee.modified_by)
self.assertEqual(self.user, employee.created_by)
class ValidJSONTest(TestCase):
def test_valid_json(self):
lang = Language.objects.create(title='English', locale='en-GB')
lang.save()
data = 'str'
with self.assertRaises(exceptions.ValidationError) as err:
validate_tjson(data)
self.assertEqual(err.exception.code, 'invalid_json')
data = {
"string": "value"
}
with self.assertRaises(exceptions.ValidationError) as err:
validate_tjson(data)
self.assertEqual(err.exception.code, 'invalid_translated_keys')
data = {
"en-GB": "English"
}
try:
validate_tjson(data)
self.assertTrue(True)
except exceptions.ValidationError:
self.assert_(False, "Test json translated FAILED")

View File

@ -327,15 +327,30 @@ FCM_DJANGO_SETTINGS = {
# Thumbnail settings
THUMBNAIL_ALIASES = {
'': {
'tiny': {'size': (100, 0), },
'small': {'size': (480, 0), },
'middle': {'size': (700, 0), },
'large': {'size': (1500, 0), },
'default': {'size': (300, 200), 'crop': True},
'gallery': {'size': (240, 160), 'crop': True},
'establishment_preview': {'size': (300, 280), 'crop': True},
}
'news_preview': {
'web': {'size': (300, 260), }
},
'news_promo_horizontal': {
'web': {'size': (1900, 600), },
'mobile': {'size': (375, 260), },
},
'news_tile_horizontal': {
'web': {'size': (300, 275), },
'mobile': {'size': (343, 180), },
},
'news_tile_vertical': {
'web': {'size': (300, 380), },
},
'news_highlight_vertical': {
'web': {'size': (460, 630), },
},
'news_editor': {
'web': {'size': (940, 430), }, # при загрузке через контент эдитор
'mobile': {'size': (343, 260), }, # через контент эдитор в мобильном браузерe
},
'avatar_comments': {
'web': {'size': (116, 116), },
},
}
# Password reset
@ -412,3 +427,14 @@ SOLO_CACHE_TIMEOUT = 300
SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/'
SITE_NAME = 'Gault & Millau'
# Used in annotations for establishments.
DEFAULT_ESTABLISHMENT_PUBLIC_MARK = 10
# Limit output objects (see in pagination classes).
LIMITING_OUTPUT_OBJECTS = 12
# Need to restrict objects to sort (3 times more then expected).
LIMITING_QUERY_NUMBER = LIMITING_OUTPUT_OBJECTS * 3
# GEO
# A Spatial Reference System Identifier
GEO_DEFAULT_SRID = 4326