Merge remote-tracking branch 'origin/develop' into es_product

This commit is contained in:
alex 2019-12-11 07:56:57 +03:00
commit b5e1344b38
21 changed files with 221 additions and 46 deletions

View File

@ -1,3 +1,4 @@
FROM mdillon/postgis:10
RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8
ENV LANG ru_RU.utf8
COPY hstore.sql /docker-entrypoint-initdb.d

View File

@ -0,0 +1 @@
create extension hstore;

View File

@ -24,8 +24,8 @@ from collection.models import Collection
from location.models import Address
from location.models import WineOriginAddressMixin
from main.models import Award, Currency
from tag.models import Tag
from review.models import Review
from tag.models import Tag
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
IntermediateGalleryModelMixin, HasTagsMixin,
@ -209,23 +209,34 @@ class EstablishmentQuerySet(models.QuerySet):
"""
return self.annotate(mark_similarity=ExpressionWrapper(
mark - F('intermediate_public_mark'),
output_field=models.FloatField()
output_field=models.FloatField(default=0)
))
def similar(self, establishment_slug: str):
"""
Return QuerySet with objects that similar to Establishment.
:param establishment_slug: str Establishment slug
"""
establishment_qs = self.filter(slug=establishment_slug,
public_mark__isnull=False)
if establishment_qs.exists():
establishment = establishment_qs.first()
subquery_filter_by_distance = Subquery(
self.exclude(slug=establishment_slug)
.filter(image_url__isnull=False, public_mark__gte=10)
.has_published_reviews()
def similar_base(self, establishment):
filters = {
'reviews__status': Review.READY,
'establishment_type': establishment.establishment_type,
}
if establishment.establishment_subtypes.exists():
filters.update({'establishment_subtypes__in': establishment.establishment_subtypes.all()})
return self.exclude(id=establishment.id) \
.filter(**filters) \
.annotate_distance(point=establishment.location)
def similar_restaurants(self, slug):
"""
Return QuerySet with objects that similar to Restaurant.
:param restaurant_slug: str Establishment slug
"""
restaurant_qs = self.filter(slug=slug,
public_mark__isnull=False)
if restaurant_qs.exists():
establishment = restaurant_qs.first()
subquery_filter_by_distance = Subquery(
self.similar_base(establishment)
.filter(public_mark__gte=10,
establishment_gallery__is_main=True)
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id')
)
@ -234,6 +245,36 @@ class EstablishmentQuerySet(models.QuerySet):
.annotate_mark_similarity(mark=establishment.public_mark) \
.order_by('mark_similarity') \
.distinct('mark_similarity', 'id')
def by_wine_region(self, wine_region):
"""
Return filtered QuerySet by wine region in wine origin.
:param wine_region: wine region.
"""
return self.filter(wine_origin__wine_region=wine_region).distinct()
def by_wine_sub_region(self, wine_sub_region):
"""
Return filtered QuerySet by wine region in wine origin.
:param wine_sub_region: wine sub region.
"""
return self.filter(wine_origin__wine_sub_region=wine_sub_region).distinct()
def similar_wineries(self, slug: str):
"""
Return QuerySet with objects that similar to Winery.
:param establishment_slug: str Establishment slug
"""
winery_qs = self.filter(slug=slug)
if winery_qs.exists():
winery = winery_qs.first()
return self.similar_base(winery) \
.order_by(F('wine_origins__wine_region').asc(),
F('wine_origins__wine_sub_region').asc()) \
.annotate_distance(point=winery.location) \
.order_by('distance') \
.distinct('distance', 'wine_origins__wine_region',
'wine_origins__wine_sub_region', 'id')
else:
return self.none()
@ -457,15 +498,9 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
def visible_tags(self):
return super().visible_tags \
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de']) \
.exclude(value__in=['rss', 'rss_selection'])
'business_tag', 'business_tags_de', 'tag'])
# todo: recalculate toque_number
@property
def visible_tags_detail(self):
"""Removes some tags from detail Establishment representation"""
return self.visible_tags.exclude(category__index_name__in=['tag'])
def recalculate_toque_number(self):
toque_number = 0
if self.address and self.public_mark:
@ -830,6 +865,25 @@ class ContactEmail(models.Model):
return f'{self.email}'
#
# class Wine(TranslatedFieldsMixin, models.Model):
# """Wine model."""
# establishment = models.ForeignKey(
# 'establishment.Establishment', verbose_name=_('establishment'),
# on_delete=models.CASCADE)
# bottles = models.IntegerField(_('bottles'))
# price_min = models.DecimalField(
# _('price min'), max_digits=14, decimal_places=2)
# price_max = models.DecimalField(
# _('price max'), max_digits=14, decimal_places=2)
# by_glass = models.BooleanField(_('by glass'))
# price_glass_min = models.DecimalField(
# _('price min'), max_digits=14, decimal_places=2)
# price_glass_max = models.DecimalField(
# _('price max'), max_digits=14, decimal_places=2)
#
class Plate(TranslatedFieldsMixin, models.Model):
"""Plate model."""
STR_FIELD_NAME = 'name'

View File

@ -9,7 +9,6 @@ urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
path('slug/<slug:slug>/comments/create/', views.EstablishmentCommentCreateView.as_view(),
name='create-comment'),
@ -17,4 +16,11 @@ urlpatterns = [
name='rud-comment'),
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites'),
# similar establishments
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
name='similar-restaurants'),
path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(),
name='similar-restaurants'),
]

View File

@ -77,16 +77,28 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
return qs.last_reviewed(point=point)
class EstablishmentSimilarListView(EstablishmentListView):
"""Resource for getting a list of establishments."""
class EstablishmentSimilarList(EstablishmentListView):
"""Resource for getting a list of similar establishments."""
serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = EstablishmentPortionPagination
class RestaurantSimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar restaurants."""
def get_queryset(self):
"""Override get_queryset method"""
qs = super().get_queryset()
return qs.similar(establishment_slug=self.kwargs.get('slug'))
return EstablishmentMixinView.get_queryset(self) \
.similar_restaurants(slug=self.kwargs.get('slug'))
class WinerySimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar wineries."""
def get_queryset(self):
"""Override get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \
.similar_wineries(slug=self.kwargs.get('slug'))
class EstablishmentTypeListView(generics.ListAPIView):

View File

@ -42,8 +42,9 @@ class BaseTestCase(APITestCase):
start=datetime.fromisoformat("2020-12-03 12:00:00"),
end=datetime.fromisoformat("2020-12-03 12:00:00"),
state=News.PUBLISHED,
slug='test-news'
slugs={'en-GB': 'test-news'}
)
self.slug = next(iter(self.test_news.slugs.values()))
self.test_content_type = ContentType.objects.get(
app_label="news", model="news")

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-10 12:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0037_auto_20191129_1320'),
]
operations = [
migrations.AddField(
model_name='news',
name='backoffice_title',
field=models.TextField(default=None, null=True, verbose_name='Title for searching via BO'),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 2.2.7 on 2019-12-10 13:49
import django.contrib.postgres.fields.hstore
from django.db import migrations
from django.contrib.postgres.operations import HStoreExtension
def migrate_slugs(apps, schemaeditor):
News = apps.get_model('news', 'News')
for news in News.objects.all():
if news.slug:
news.slugs = {'en-GB': news.slug}
news.save()
class Migration(migrations.Migration):
dependencies = [
('news', '0038_news_backoffice_title'),
]
operations = [
HStoreExtension(),
migrations.AddField(
model_name='news',
name='slugs',
field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, default=None, help_text='{"en-GB":"some slug"}', null=True, verbose_name='Slugs for current news obj'),
),
migrations.RunPython(migrate_slugs, migrations.RunPython.noop)
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2019-12-10 16:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0039_news_slugs'),
]
operations = [
migrations.RemoveField(
model_name='news',
name='slug',
),
]

View File

@ -12,6 +12,7 @@ from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, Has
FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin
from django.conf import settings
from django.contrib.postgres.fields import HStoreField
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
@ -168,6 +169,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
title = TJSONField(blank=True, null=True, default=None,
verbose_name=_('title'),
help_text='{"en-GB":"some text"}')
backoffice_title = models.TextField(null=True, default=None,
verbose_name=_('Title for searching via BO'))
subtitle = TJSONField(blank=True, null=True, default=None,
verbose_name=_('subtitle'),
help_text='{"en-GB":"some text"}')
@ -178,8 +181,9 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
verbose_name=_('Start'))
end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('End'))
slug = models.SlugField(unique=True, max_length=255,
verbose_name=_('News slug'))
slugs = HStoreField(null=True, blank=True, default=None,
verbose_name=_('Slugs for current news obj'),
help_text='{"en-GB":"some slug"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
is_highlighted = models.BooleanField(default=False,
@ -228,7 +232,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
@property
def web_url(self):
return reverse('web:news:rud', kwargs={'slug': self.slug})
return reverse('web:news:rud', kwargs={'slug': next(iter(self.slugs.values()))})
def should_read(self, user):
return self.__class__.objects.should_read(self, user)[:3]

View File

@ -80,7 +80,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
'is_highlighted',
'news_type',
'tags',
'slug',
'slugs',
'view_counter',
)
@ -169,9 +169,21 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
fields = NewsBaseSerializer.Meta.fields + (
'title',
'backoffice_title',
'subtitle',
'is_published',
)
extra_kwargs = {
'backoffice_title': {'allow_null': False},
}
def validate(self, attrs):
slugs = attrs.get('slugs', {})
if models.News.objects.filter(
slugs__values__contains=list(slugs.values())
).exclude(id=attrs.get('id', 0)).exists():
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
return attrs
class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
@ -252,7 +264,7 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer):
def validate(self, attrs):
"""Overridden validate method"""
# Check establishment object
news_qs = models.News.objects.filter(slug=self.slug)
news_qs = models.News.objects.filter(slugs__values__contains=[self.slug])
# Check establishment obj by slug from lookup_kwarg
if not news_qs.exists():

View File

@ -66,10 +66,11 @@ class BaseTestCase(APITestCase):
start=datetime.now() + timedelta(hours=-2),
end=datetime.now() + timedelta(hours=2),
state=News.PUBLISHED,
slug='test-news-slug',
slugs={'en-GB': 'test-news-slug'},
country=self.country_ru,
site=self.site_ru
)
self.slug = next(iter(self.test_news.slugs.values()))
class NewsTestCase(BaseTestCase):
@ -84,7 +85,7 @@ class NewsTestCase(BaseTestCase):
"start": datetime.now() + timedelta(hours=-2),
"end": datetime.now() + timedelta(hours=2),
"state": News.PUBLISHED,
"slug": 'test-news-slug_post',
"slugs": {'en-GB': 'test-news-slug_post'},
"country_id": self.country_ru.id,
"site_id": self.site_ru.id
}
@ -97,7 +98,7 @@ class NewsTestCase(BaseTestCase):
response = self.client.get(reverse('web:news:list'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.get(f"/api/web/news/slug/{self.test_news.slug}/")
response = self.client.get(f"/api/web/news/slug/{self.slug}/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.get("/api/web/news/types/")
@ -117,7 +118,7 @@ class NewsTestCase(BaseTestCase):
data = {
'id': self.test_news.id,
'description': {"ru-RU": "Description test news!"},
'slug': self.test_news.slug,
'slugs': self.test_news.slugs,
'start': self.test_news.start,
'news_type_id': self.test_news.news_type_id,
'country_id': self.country_ru.id,
@ -133,10 +134,10 @@ class NewsTestCase(BaseTestCase):
"object_id": self.test_news.id
}
response = self.client.post(f'/api/web/news/slug/{self.test_news.slug}/favorites/', data=data)
response = self.client.post(f'/api/web/news/slug/{self.slug}/favorites/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json')
response = self.client.delete(f'/api/web/news/slug/{self.slug}/favorites/', format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -31,6 +31,10 @@ class NewsMixinView:
qs = qs.by_country_code(country_code)
return qs
def get_object(self):
return self.get_queryset() \
.filter(slugs__values__contains=[self.kwargs['slug']]).first()
class NewsListView(NewsMixinView, generics.ListAPIView):
"""News list view."""
@ -46,7 +50,7 @@ class NewsListView(NewsMixinView, generics.ListAPIView):
class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
"""News detail view."""
lookup_field = 'slug'
lookup_field = None
serializer_class = serializers.NewsDetailWebSerializer
def get_queryset(self):

View File

@ -7,7 +7,7 @@ from establishment import models
EstablishmentIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__,
'establishment'))
EstablishmentIndex.settings(number_of_shards=1, number_of_replicas=1)
EstablishmentIndex.settings(number_of_shards=5, number_of_replicas=2)
@EstablishmentIndex.doc_type

View File

@ -17,6 +17,8 @@ class NewsDocument(Document):
'name': fields.KeywordField()})
title = fields.ObjectField(attr='title_indexing',
properties=OBJECT_FIELD_PROPERTIES)
slugs = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES)
backoffice_title = fields.TextField(analyzer='english')
subtitle = fields.ObjectField(attr='subtitle_indexing',
properties=OBJECT_FIELD_PROPERTIES)
description = fields.ObjectField(attr='description_indexing',
@ -43,13 +45,16 @@ class NewsDocument(Document):
multi=True)
favorites_for_users = fields.ListField(field=fields.IntegerField())
start = fields.DateField(attr='start')
def prepare_slugs(self, instance):
return {locale: instance.slugs.get(locale) for locale in OBJECT_FIELD_PROPERTIES}
class Django:
model = models.News
fields = (
'id',
'end',
'slug',
'state',
'is_highlighted',
'template',

View File

@ -39,4 +39,8 @@ class TagCategoryDocument(Document):
to the updating of a lot of items.
"""
if isinstance(related_instance, News):
return related_instance.tags
tag_categories = []
for tag in related_instance.tags.all():
if tag.category not in tag_categories:
tag_categories.append(tag.category)
return tag_categories

View File

@ -225,7 +225,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'news_type',
'tags',
'start',
'slug',
'slugs',
)
@staticmethod

View File

@ -314,7 +314,8 @@ class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet):
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
filters.CustomGeoSpatialFilteringFilterBackend,
GeoSpatialOrderingFilterBackend,
]

View File

@ -56,17 +56,18 @@ class TranslateFieldTests(BaseTestCase):
start=datetime.now(pytz.utc) + timedelta(hours=-13),
end=datetime.now(pytz.utc) + timedelta(hours=13),
news_type=self.news_type,
slug='test',
slugs={'en-GB': 'test'},
state=News.PUBLISHED,
country=self.country_ru,
)
self.slug = next(iter(self.news_item.slugs.values()))
self.news_item.save()
def test_model_field(self):
self.assertTrue(hasattr(self.news_item, "title_translated"))
def test_read_locale(self):
response = self.client.get(f"/api/web/news/slug/{self.news_item.slug}/", format='json')
response = self.client.get(f"/api/web/news/slug/{self.slug}/", format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
news_data = response.json()
self.assertIn("title_translated", news_data)

View File

@ -9,6 +9,7 @@ from rest_framework.response import Response
from gallery.tasks import delete_image
from search_indexes.documents import es_update
from news.models import News
# JWT
@ -124,6 +125,8 @@ class BaseCreateDestroyMixinView(generics.CreateAPIView, generics.DestroyAPIView
lookup_field = 'slug'
def get_base_object(self):
if 'slugs' in [f.name for f in self._model._meta.get_fields()]: # slugs instead of `slug`
return get_object_or_404(self._model, slugs__values__contains=[self.kwargs['slug']])
return get_object_or_404(self._model, slug=self.kwargs['slug'])
def es_update_base_object(self):

View File

@ -48,6 +48,7 @@ CONTRIB_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.gis',
'django.contrib.postgres',
]