From b659308729548d8a2e5df9c49ca90a1546ff152a Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Dec 2019 13:06:59 +0300 Subject: [PATCH 01/14] exclude field from Establishment serializer --- apps/establishment/models.py | 69 ++++++++++++++++++++++++------- apps/establishment/urls/common.py | 8 +++- apps/establishment/views/web.py | 20 +++++++-- project/settings/local.py | 2 +- 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 3cdef691..fe9ad53b 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -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): + 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, restaurant_slug): """ - Return QuerySet with objects that similar to Establishment. - :param establishment_slug: str Establishment slug + Return QuerySet with objects that similar to Restaurant. + :param restaurant_slug: str Establishment slug """ - establishment_qs = self.filter(slug=establishment_slug, - public_mark__isnull=False) - if establishment_qs.exists(): - establishment = establishment_qs.first() + restaurant_qs = self.filter(slug=restaurant_slug, + public_mark__isnull=False) + if restaurant_qs.exists(): + establishment = restaurant_qs.first() 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) + self.similar_base(establishment) + .filter(public_mark__gte=10, + establishment_gallery__is_main=True) .order_by('distance')[:settings.LIMITING_QUERY_OBJECTS] .values('id') ) @@ -234,8 +245,38 @@ 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, winery_slug: str): + """ + Return QuerySet with objects that similar to Winery. + :param establishment_slug: str Establishment slug + """ + winery_qs = self.filter(slug=winery_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() + self.none() def last_reviewed(self, point: Point): """ diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index faa34bd9..5821a4c6 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -9,7 +9,6 @@ urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(), name='recent-reviews'), - path('slug//similar/', views.EstablishmentSimilarListView.as_view(), name='similar'), path('slug//comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), path('slug//comments/create/', views.EstablishmentCommentCreateView.as_view(), name='create-comment'), @@ -17,4 +16,11 @@ urlpatterns = [ name='rud-comment'), path('slug//favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites'), + + # similar establishments + path('slug//similar/restaurants/', views.RestaurantSimilarListView.as_view(), + name='similar-restaurants'), + path('slug//similar/wineries/', views.WinerySimilarListView.as_view(), + name='similar-restaurants'), + ] diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index ba5cb23b..a7b90184 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -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 qs.similar_restaurants(restaurant_slug=self.kwargs.get('slug')) + + +class WinerySimilarListView(EstablishmentSimilarList): + """Resource for getting a list of similar wineries.""" + + def get_queryset(self): + """Override get_queryset method""" + qs = super().get_queryset() + return qs.similar_wineries(winery_slug=self.kwargs.get('slug')) class EstablishmentTypeListView(generics.ListAPIView): diff --git a/project/settings/local.py b/project/settings/local.py index c56f9042..d9c7cab8 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -42,7 +42,7 @@ DATABASES = { 'HOST': os.environ.get('DB_HOSTNAME'), 'PORT': os.environ.get('DB_PORT'), 'OPTIONS': { - 'options': '-c search_path=gm' + 'options': '-c search_path=gm,public' }, }, 'legacy': { From ce5e5f6cb79012f64ec0cbdad985803ea48a24f3 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 12:42:42 +0300 Subject: [PATCH 02/14] fix issue w/ tag_category --- apps/search_indexes/documents/tag_category.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/documents/tag_category.py b/apps/search_indexes/documents/tag_category.py index cd2c8a90..757483bf 100644 --- a/apps/search_indexes/documents/tag_category.py +++ b/apps/search_indexes/documents/tag_category.py @@ -39,4 +39,8 @@ class TagCategoryDocument(Document): to the updating of a lot of items. """ if isinstance(related_instance, News): - return related_instance.tags \ No newline at end of file + tag_categories = [] + for tag in related_instance.tags.all(): + if tag.category not in tag_categories: + tag_categories.append(tag.category) + return tag_categories \ No newline at end of file From 51da45b00ba5518319855b6d0cded477cfd6ddf8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Dec 2019 13:08:28 +0300 Subject: [PATCH 03/14] refactoring --- apps/establishment/models.py | 37 +++++++++++++++++++++---------- apps/establishment/urls/common.py | 2 +- apps/establishment/views/web.py | 8 +++---- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 68e09eb3..9ecf38c2 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -224,12 +224,12 @@ class EstablishmentQuerySet(models.QuerySet): .filter(**filters) \ .annotate_distance(point=establishment.location) - def similar_restaurants(self, restaurant_slug): + def similar_restaurants(self, slug): """ Return QuerySet with objects that similar to Restaurant. :param restaurant_slug: str Establishment slug """ - restaurant_qs = self.filter(slug=restaurant_slug, + restaurant_qs = self.filter(slug=slug, public_mark__isnull=False) if restaurant_qs.exists(): establishment = restaurant_qs.first() @@ -260,12 +260,12 @@ class EstablishmentQuerySet(models.QuerySet): """ return self.filter(wine_origin__wine_sub_region=wine_sub_region).distinct() - def similar_wineries(self, winery_slug: str): + 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=winery_slug) + winery_qs = self.filter(slug=slug) if winery_qs.exists(): winery = winery_qs.first() return self.similar_base(winery) \ @@ -276,7 +276,7 @@ class EstablishmentQuerySet(models.QuerySet): .distinct('distance', 'wine_origins__wine_region', 'wine_origins__wine_sub_region', 'id') else: - self.none() + return self.none() def last_reviewed(self, point: Point): """ @@ -498,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: @@ -871,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' diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 5821a4c6..68ba2b16 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -18,7 +18,7 @@ urlpatterns = [ name='create-destroy-favorites'), # similar establishments - path('slug//similar/restaurants/', views.RestaurantSimilarListView.as_view(), + path('slug//similar/', views.RestaurantSimilarListView.as_view(), name='similar-restaurants'), path('slug//similar/wineries/', views.WinerySimilarListView.as_view(), name='similar-restaurants'), diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index eae34789..9e6dc026 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -88,8 +88,8 @@ class RestaurantSimilarListView(EstablishmentSimilarList): def get_queryset(self): """Override get_queryset method""" - qs = super().get_queryset() - return qs.similar_restaurants(restaurant_slug=self.kwargs.get('slug')) + return EstablishmentMixinView.get_queryset(self) \ + .similar_restaurants(slug=self.kwargs.get('slug')) class WinerySimilarListView(EstablishmentSimilarList): @@ -97,8 +97,8 @@ class WinerySimilarListView(EstablishmentSimilarList): def get_queryset(self): """Override get_queryset method""" - qs = super().get_queryset() - return qs.similar_wineries(winery_slug=self.kwargs.get('slug')) + return EstablishmentMixinView.get_queryset(self) \ + .similar_wineries(slug=self.kwargs.get('slug')) class EstablishmentTypeListView(generics.ListAPIView): From bec4e9be96b9ad4944145d507ca40f1319ec721b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 15:31:57 +0300 Subject: [PATCH 04/14] ordering from center for mobiles --- apps/search_indexes/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 0e93d8fc..387a6eae 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -314,7 +314,8 @@ class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet): filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, - GeoSpatialFilteringFilterBackend, + filters.CustomGeoSpatialFilteringFilterBackend, + GeoSpatialOrderingFilterBackend, ] From 3b85e927a17eb6039474017a6a0dc3e1b2fd15b8 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 15:39:53 +0300 Subject: [PATCH 05/14] BO title field --- .../migrations/0038_news_backoffice_title.py | 18 ++++++++++++++++++ apps/news/models.py | 2 ++ apps/news/serializers.py | 4 ++++ apps/search_indexes/documents/news.py | 1 + 4 files changed, 25 insertions(+) create mode 100644 apps/news/migrations/0038_news_backoffice_title.py diff --git a/apps/news/migrations/0038_news_backoffice_title.py b/apps/news/migrations/0038_news_backoffice_title.py new file mode 100644 index 00000000..05363acb --- /dev/null +++ b/apps/news/migrations/0038_news_backoffice_title.py @@ -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'), + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index 30e4206b..a2db35b4 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -168,6 +168,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"}') diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 86673645..c14e28fe 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -169,9 +169,13 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): fields = NewsBaseSerializer.Meta.fields + ( 'title', + 'backoffice_title', 'subtitle', 'is_published', ) + extra_kwargs = { + 'backoffice_title': {'allow_null': False}, + } class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 3c87e680..535c92f6 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -17,6 +17,7 @@ class NewsDocument(Document): 'name': fields.KeywordField()}) title = fields.ObjectField(attr='title_indexing', properties=OBJECT_FIELD_PROPERTIES) + backoffice_title = fields.KeywordField(analyzer='english') subtitle = fields.ObjectField(attr='subtitle_indexing', properties=OBJECT_FIELD_PROPERTIES) description = fields.ObjectField(attr='description_indexing', From 34173afb05fe8092b25b63c5858ca9d94d03f3e7 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 15:41:33 +0300 Subject: [PATCH 06/14] fix field analyzer --- apps/search_indexes/documents/news.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 535c92f6..2aab01c8 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -17,7 +17,7 @@ class NewsDocument(Document): 'name': fields.KeywordField()}) title = fields.ObjectField(attr='title_indexing', properties=OBJECT_FIELD_PROPERTIES) - backoffice_title = fields.KeywordField(analyzer='english') + backoffice_title = fields.TextField(analyzer='english') subtitle = fields.ObjectField(attr='subtitle_indexing', properties=OBJECT_FIELD_PROPERTIES) description = fields.ObjectField(attr='description_indexing', From 4010c9fedea6ed20d0976f0f4b45dcb9cc337dc0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 19:16:19 +0300 Subject: [PATCH 07/14] News multilang slugs (model && views) --- _dockerfiles/db/Dockerfile | 1 + _dockerfiles/db/hstore.sql | 1 + apps/news/migrations/0039_news_slugs.py | 27 +++++++++++++++++++++++++ apps/news/models.py | 4 ++++ apps/news/views.py | 6 +++++- apps/utils/views.py | 3 +++ project/settings/base.py | 1 + 7 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 _dockerfiles/db/hstore.sql create mode 100644 apps/news/migrations/0039_news_slugs.py diff --git a/_dockerfiles/db/Dockerfile b/_dockerfiles/db/Dockerfile index c3e35955..1a9d28c6 100644 --- a/_dockerfiles/db/Dockerfile +++ b/_dockerfiles/db/Dockerfile @@ -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 \ No newline at end of file diff --git a/_dockerfiles/db/hstore.sql b/_dockerfiles/db/hstore.sql new file mode 100644 index 00000000..97962703 --- /dev/null +++ b/_dockerfiles/db/hstore.sql @@ -0,0 +1 @@ +create extension hstore; \ No newline at end of file diff --git a/apps/news/migrations/0039_news_slugs.py b/apps/news/migrations/0039_news_slugs.py new file mode 100644 index 00000000..e8b996c8 --- /dev/null +++ b/apps/news/migrations/0039_news_slugs.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.7 on 2019-12-10 13:49 + +import django.contrib.postgres.fields.hstore +from django.db import migrations + +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 = [ + 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) + ] diff --git a/apps/news/models.py b/apps/news/models.py index a2db35b4..465ce8ee 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -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): @@ -182,6 +183,9 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi 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, diff --git a/apps/news/views.py b/apps/news/views.py index a4a5c33a..54868e52 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -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): diff --git a/apps/utils/views.py b/apps/utils/views.py index 9ac8ca60..379e501b 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -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 isinstance(self._model, News): + 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): diff --git a/project/settings/base.py b/project/settings/base.py index 5a48c261..31b7b8f7 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -48,6 +48,7 @@ CONTRIB_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.gis', + 'django.contrib.postgres', ] From c1bb7c9b79f1a28b79e60788efcf4060a19afa0e Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 19:23:40 +0300 Subject: [PATCH 08/14] News multilang slug (ES) --- apps/news/migrations/0040_remove_news_slug.py | 17 +++++++++++++++++ apps/news/models.py | 2 -- apps/news/serializers.py | 2 +- apps/search_indexes/documents/establishment.py | 2 +- apps/search_indexes/documents/news.py | 2 +- apps/search_indexes/serializers.py | 2 +- 6 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 apps/news/migrations/0040_remove_news_slug.py diff --git a/apps/news/migrations/0040_remove_news_slug.py b/apps/news/migrations/0040_remove_news_slug.py new file mode 100644 index 00000000..f4ef00bb --- /dev/null +++ b/apps/news/migrations/0040_remove_news_slug.py @@ -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', + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index 465ce8ee..7931b17b 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -181,8 +181,6 @@ 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"}') diff --git a/apps/news/serializers.py b/apps/news/serializers.py index c14e28fe..3e5d3ecb 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -80,7 +80,7 @@ class NewsBaseSerializer(ProjectModelSerializer): 'is_highlighted', 'news_type', 'tags', - 'slug', + 'slugs', 'view_counter', ) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index c6b68ed4..8ae26097 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -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 diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 2aab01c8..f48494fd 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -17,6 +17,7 @@ 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) @@ -50,7 +51,6 @@ class NewsDocument(Document): fields = ( 'id', 'end', - 'slug', 'state', 'is_highlighted', 'template', diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 3b5561fa..81a31afa 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -221,7 +221,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'news_type', 'tags', 'start', - 'slug', + 'slugs', ) @staticmethod From c90f8302ee2f397b488929e498f01f449a0f6b94 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 19:29:11 +0300 Subject: [PATCH 09/14] fix news indexing --- apps/news/models.py | 2 +- apps/search_indexes/documents/news.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/news/models.py b/apps/news/models.py index 7931b17b..f54378b6 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -232,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] diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index f48494fd..05e8c2bb 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -17,7 +17,7 @@ class NewsDocument(Document): 'name': fields.KeywordField()}) title = fields.ObjectField(attr='title_indexing', properties=OBJECT_FIELD_PROPERTIES) - slugs = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES) + slugs = fields.ObjectField() backoffice_title = fields.TextField(analyzer='english') subtitle = fields.ObjectField(attr='subtitle_indexing', properties=OBJECT_FIELD_PROPERTIES) From 77af35f543bcb5151fa4aa74a8c3a5af1b86c0e8 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 19:33:15 +0300 Subject: [PATCH 10/14] many slugs es news fix --- apps/search_indexes/documents/news.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 05e8c2bb..ec89f9ee 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -17,7 +17,7 @@ class NewsDocument(Document): 'name': fields.KeywordField()}) title = fields.ObjectField(attr='title_indexing', properties=OBJECT_FIELD_PROPERTIES) - slugs = fields.ObjectField() + slugs = fields.ListField(fields.ObjectField()) backoffice_title = fields.TextField(analyzer='english') subtitle = fields.ObjectField(attr='subtitle_indexing', properties=OBJECT_FIELD_PROPERTIES) From 401abc568cf0d6b003cb8f52d6782efad725192a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 20:04:23 +0300 Subject: [PATCH 11/14] news unique slug creation --- apps/news/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 3e5d3ecb..12b1faf6 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -177,6 +177,12 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): 'backoffice_title': {'allow_null': False}, } + def validate(self, attrs): + slugs = attrs.get('slugs', {}) + if models.News.objects.filter(slugs__values__contains=[slugs.values()]).exists(): + raise serializers.ValidationError({'slugs': _('News with this slug already exists.')}) + return attrs + class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, NewsDetailSerializer): From 377d8196dc6ee3009b031bec0e4c84710b0b7033 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 20:58:08 +0300 Subject: [PATCH 12/14] fix slugs serialization --- apps/search_indexes/documents/news.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index ec89f9ee..62e3e984 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -17,7 +17,7 @@ class NewsDocument(Document): 'name': fields.KeywordField()}) title = fields.ObjectField(attr='title_indexing', properties=OBJECT_FIELD_PROPERTIES) - slugs = fields.ListField(fields.ObjectField()) + slugs = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES) backoffice_title = fields.TextField(analyzer='english') subtitle = fields.ObjectField(attr='subtitle_indexing', properties=OBJECT_FIELD_PROPERTIES) @@ -45,6 +45,10 @@ 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 From ecef1a217a59196d166361f76bb340b1af3b2a8f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 22:10:37 +0300 Subject: [PATCH 13/14] fix some test --- apps/favorites/tests.py | 3 ++- apps/news/migrations/0039_news_slugs.py | 2 ++ apps/news/serializers.py | 2 +- apps/news/tests.py | 13 +++++++------ apps/utils/tests/tests_translated.py | 5 +++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/favorites/tests.py b/apps/favorites/tests.py index 5f0a2fcd..22953133 100644 --- a/apps/favorites/tests.py +++ b/apps/favorites/tests.py @@ -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") diff --git a/apps/news/migrations/0039_news_slugs.py b/apps/news/migrations/0039_news_slugs.py index e8b996c8..cc9a3194 100644 --- a/apps/news/migrations/0039_news_slugs.py +++ b/apps/news/migrations/0039_news_slugs.py @@ -2,6 +2,7 @@ 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') @@ -18,6 +19,7 @@ class Migration(migrations.Migration): ] operations = [ + HStoreExtension(), migrations.AddField( model_name='news', name='slugs', diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 12b1faf6..da3ea2df 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -179,7 +179,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): def validate(self, attrs): slugs = attrs.get('slugs', {}) - if models.News.objects.filter(slugs__values__contains=[slugs.values()]).exists(): + if models.News.objects.filter(slugs__values__contains=list(slugs.values())).exists(): raise serializers.ValidationError({'slugs': _('News with this slug already exists.')}) return attrs diff --git a/apps/news/tests.py b/apps/news/tests.py index 40f47312..42c4a694 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -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) diff --git a/apps/utils/tests/tests_translated.py b/apps/utils/tests/tests_translated.py index 6249ebd1..6ddcd1e7 100644 --- a/apps/utils/tests/tests_translated.py +++ b/apps/utils/tests/tests_translated.py @@ -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) From 043fcb262b19efa0e8987929f5b366e11f8ddc54 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 23:30:07 +0300 Subject: [PATCH 14/14] fix test #2 --- apps/news/serializers.py | 6 ++++-- apps/utils/views.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index da3ea2df..846bc31a 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -179,7 +179,9 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): def validate(self, attrs): slugs = attrs.get('slugs', {}) - if models.News.objects.filter(slugs__values__contains=list(slugs.values())).exists(): + 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 @@ -262,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(): diff --git a/apps/utils/views.py b/apps/utils/views.py index 379e501b..77f22032 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -125,8 +125,8 @@ class BaseCreateDestroyMixinView(generics.CreateAPIView, generics.DestroyAPIView lookup_field = 'slug' def get_base_object(self): - if isinstance(self._model, News): - get_object_or_404(self._model, slugs__values__contains=[self.kwargs['slug']]) + 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):