From 1687e804929ee22e218ce98b33e9a72e82f99f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Thu, 21 Nov 2019 13:13:19 +0300 Subject: [PATCH 01/45] Check serial number for product --- .../management/commands/add_product_tag.py | 11 +++++++++++ .../migrations/0019_product_serial_number.py | 18 ++++++++++++++++++ apps/product/models.py | 4 ++++ 3 files changed, 33 insertions(+) create mode 100644 apps/product/migrations/0019_product_serial_number.py diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index 6377fcac..9ee09bdd 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -149,6 +149,17 @@ class Command(BaseCommand): tag.label = new_label tag.save() + + def check_serail_number(self): + category = TagCategory.objects.get(index_name='serial_number') + tags = Tag.objects.filter(category=category, products__isnull=False) + for tag in tqdm(tags, desc='Update serial number for product'): + tag.products.all().update(serial_number=tag.value) + + self.stdout.write(self.style.WARNING(f'Check serial number product end.')) + + + def handle(self, *args, **kwargs): self.remove_tags_product() self.remove_tags() diff --git a/apps/product/migrations/0019_product_serial_number.py b/apps/product/migrations/0019_product_serial_number.py new file mode 100644 index 00000000..eb8bef34 --- /dev/null +++ b/apps/product/migrations/0019_product_serial_number.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-21 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0018_purchasedproduct'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='serial_number', + field=models.CharField(default=None, max_length=255, null=True, verbose_name='Serial number'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index f499afee..3d27559f 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -219,6 +219,10 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsM comments = generic.GenericRelation(to='comment.Comment') awards = generic.GenericRelation(to='main.Award', related_query_name='product') + serial_number = models.CharField(max_length=255, + default=None, null=True, + verbose_name=_('Serial number')) + objects = ProductManager.from_queryset(ProductQuerySet)() class Meta: From 938b9436eb4e7a142a3cd78e3a11da2a0a873f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Thu, 21 Nov 2019 13:25:31 +0300 Subject: [PATCH 02/45] Fix name def --- apps/product/management/commands/add_product_tag.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index 9ee09bdd..1ff823b0 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -149,8 +149,7 @@ class Command(BaseCommand): tag.label = new_label tag.save() - - def check_serail_number(self): + def check_serial_number(self): category = TagCategory.objects.get(index_name='serial_number') tags = Tag.objects.filter(category=category, products__isnull=False) for tag in tqdm(tags, desc='Update serial number for product'): @@ -158,8 +157,6 @@ class Command(BaseCommand): self.stdout.write(self.style.WARNING(f'Check serial number product end.')) - - def handle(self, *args, **kwargs): self.remove_tags_product() self.remove_tags() @@ -168,3 +165,4 @@ class Command(BaseCommand): self.add_tag() self.check_tag() self.add_product_tag() + self.check_serial_number() From 9a2f7fec39b8e9d170e417dde53e49c3312e8098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Thu, 21 Nov 2019 14:33:27 +0300 Subject: [PATCH 03/45] fix --- apps/product/management/commands/add_product_tag.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index 1ff823b0..e430166d 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -101,14 +101,12 @@ class Command(BaseCommand): p.tags.clear() print('End clear tags product') - def remove_tags(self): print('Begin delete many tags') Tag.objects.\ filter(news__isnull=True, establishments__isnull=True).delete() print('End delete many tags') - def product_sql(self): with connections['legacy'].cursor() as cursor: cursor.execute(''' From a2087213a4265b82eddb36e1d80cd8643bb794f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Thu, 21 Nov 2019 15:22:22 +0300 Subject: [PATCH 04/45] Check serial number command --- .../management/commands/add_product_tag.py | 11 +--------- .../commands/check_serial_number.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 apps/product/management/commands/check_serial_number.py diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index e430166d..01f5d7f7 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -7,7 +7,7 @@ from tqdm import tqdm class Command(BaseCommand): - help = '''Add add product tags networks from old db to new db. + help = '''Add product tags networks from old db to new db. Run after add_product!!!''' def category_sql(self): @@ -147,14 +147,6 @@ class Command(BaseCommand): tag.label = new_label tag.save() - def check_serial_number(self): - category = TagCategory.objects.get(index_name='serial_number') - tags = Tag.objects.filter(category=category, products__isnull=False) - for tag in tqdm(tags, desc='Update serial number for product'): - tag.products.all().update(serial_number=tag.value) - - self.stdout.write(self.style.WARNING(f'Check serial number product end.')) - def handle(self, *args, **kwargs): self.remove_tags_product() self.remove_tags() @@ -163,4 +155,3 @@ class Command(BaseCommand): self.add_tag() self.check_tag() self.add_product_tag() - self.check_serial_number() diff --git a/apps/product/management/commands/check_serial_number.py b/apps/product/management/commands/check_serial_number.py new file mode 100644 index 00000000..ae2e43a3 --- /dev/null +++ b/apps/product/management/commands/check_serial_number.py @@ -0,0 +1,22 @@ +from django.core.management.base import BaseCommand +from django.db import connections +from establishment.management.commands.add_position import namedtuplefetchall +from tag.models import Tag, TagCategory +from product.models import Product, ProductType +from tqdm import tqdm + + +class Command(BaseCommand): + help = '''Check product serial number from old db to new db. + Run after add_product_tag!!!''' + + def check_serial_number(self): + category = TagCategory.objects.get(index_name='serial_number') + tags = Tag.objects.filter(category=category, products__isnull=False) + for tag in tqdm(tags, desc='Update serial number for product'): + tag.products.all().update(serial_number=tag.value) + + self.stdout.write(self.style.WARNING(f'Check serial number product end.')) + + def handle(self, *args, **kwargs): + self.check_serial_number() From 5236bca874fe03d14c550ae9496c0037ce4cebcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Thu, 21 Nov 2019 15:22:43 +0300 Subject: [PATCH 05/45] Check serial number command --- apps/product/management/commands/check_serial_number.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/product/management/commands/check_serial_number.py b/apps/product/management/commands/check_serial_number.py index ae2e43a3..75d1a693 100644 --- a/apps/product/management/commands/check_serial_number.py +++ b/apps/product/management/commands/check_serial_number.py @@ -1,8 +1,5 @@ from django.core.management.base import BaseCommand -from django.db import connections -from establishment.management.commands.add_position import namedtuplefetchall from tag.models import Tag, TagCategory -from product.models import Product, ProductType from tqdm import tqdm From 26b8dcd082f997883cc56628a84e254c647b26b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Thu, 21 Nov 2019 15:26:43 +0300 Subject: [PATCH 06/45] Check serial number command --- apps/product/management/commands/check_serial_number.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/product/management/commands/check_serial_number.py b/apps/product/management/commands/check_serial_number.py index 75d1a693..d9caf69a 100644 --- a/apps/product/management/commands/check_serial_number.py +++ b/apps/product/management/commands/check_serial_number.py @@ -12,6 +12,7 @@ class Command(BaseCommand): tags = Tag.objects.filter(category=category, products__isnull=False) for tag in tqdm(tags, desc='Update serial number for product'): tag.products.all().update(serial_number=tag.value) + tag.products.clear() self.stdout.write(self.style.WARNING(f'Check serial number product end.')) From 93056cf524994e5a4d3a7a33d11cd01a28fa84b3 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 3 Dec 2019 13:35:52 +0300 Subject: [PATCH 07/45] Add filter Add queryset Add settings --- _dockerfiles/db/Dockerfile | 2 +- apps/location/filters.py | 24 ++++++++++++++++++++++++ apps/location/models.py | 15 +++++++++++++++ apps/location/views/back.py | 4 ++++ docker-compose.mysql.yml | 1 + project/settings/local.py | 14 +++++++++++++- 6 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 apps/location/filters.py diff --git a/_dockerfiles/db/Dockerfile b/_dockerfiles/db/Dockerfile index 45c707d9..e8a9ded3 100644 --- a/_dockerfiles/db/Dockerfile +++ b/_dockerfiles/db/Dockerfile @@ -1,3 +1,3 @@ -FROM mdillon/postgis:9.5 +FROM mdillon/postgis:latest RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8 ENV LANG ru_RU.utf8 diff --git a/apps/location/filters.py b/apps/location/filters.py new file mode 100644 index 00000000..5e95db44 --- /dev/null +++ b/apps/location/filters.py @@ -0,0 +1,24 @@ +from django.core.validators import EMPTY_VALUES +from django_filters import rest_framework as filters + +from location import models + + +class CityBackFilter(filters.FilterSet): + """Employee filter set.""" + + search = filters.CharFilter(method='search_by_name') + + class Meta: + """Meta class.""" + + model = models.City + fields = ( + 'search', + ) + + def search_by_name(self, queryset, name, value): + """Search by name or last name.""" + if value not in EMPTY_VALUES: + return queryset.search_by_name(value) + return queryset diff --git a/apps/location/models.py b/apps/location/models.py index 3f104644..32a6cf4f 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -5,6 +5,9 @@ from django.db.models.signals import post_save from django.db.transaction import on_commit from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ +from functools import reduce +from typing import List + from translation.models import Language from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField, @@ -92,6 +95,18 @@ class Region(models.Model): class CityQuerySet(models.QuerySet): """Extended queryset for City model.""" + def _generic_search(self, value, filter_fields_names: List[str]): + """Generic method for searching value in specified fields""" + filters = [ + {f'{field}__icontains': value} + for field in filter_fields_names + ] + return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters])) + + def search_by_name(self, value): + """Search by name or last_name.""" + return self._generic_search(value, ['name', 'code', 'postal_code']) + def by_country_code(self, code): """Return establishments by country code""" return self.filter(country__code=code) diff --git a/apps/location/views/back.py b/apps/location/views/back.py index 4d420154..8406dee3 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -9,6 +9,8 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly from django.shortcuts import get_object_or_404 from utils.serializers import ImageBaseSerializer +from location import filters + # Address @@ -31,6 +33,8 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): """Create view for model City.""" serializer_class = serializers.CitySerializer permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] + queryset = models.City.objects.all() + filter_class = filters.CityBackFilter class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): diff --git a/docker-compose.mysql.yml b/docker-compose.mysql.yml index a8900226..800a9644 100644 --- a/docker-compose.mysql.yml +++ b/docker-compose.mysql.yml @@ -29,6 +29,7 @@ services: - "5436:5432" volumes: - gm-db:/var/lib/postgresql/data/ + - .:/code elasticsearch: diff --git a/project/settings/local.py b/project/settings/local.py index 6a592a46..989af4ef 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -41,7 +41,19 @@ DATABASES.update({ 'PORT': 3306, 'NAME': 'dev', 'USER': 'dev', - 'PASSWORD': 'octosecret123'}}) + 'PASSWORD': 'octosecret123'}, + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': os.environ.get('DB_NAME'), + 'USER': os.environ.get('DB_USERNAME'), + 'PASSWORD': os.environ.get('DB_PASSWORD'), + 'HOST': os.environ.get('DB_HOSTNAME'), + 'PORT': os.environ.get('DB_PORT'), + 'OPTIONS': { + 'options': '-c search_path=gm' + }, + }, +}) # LOGGING From 88b190d76db506d76f47a8259e5dc1501b101816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 6 Dec 2019 09:45:38 +0300 Subject: [PATCH 08/45] winery choice --- apps/account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/account/models.py b/apps/account/models.py index 205171d0..53163d95 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -33,7 +33,7 @@ class Role(ProjectBaseMixin): REVIEWER_MANGER = 6 RESTAURANT_REVIEWER = 7 SALES_MAN = 8 - WINERY_REVIEWER = 9 + WINERY_REVIEWER = 9 # Establishments subtype "winery" SELLER = 10 ROLE_CHOICES = ( From 8c8acd3e191ccd93a4cdbf3dcc722775d81e0f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 6 Dec 2019 11:13:07 +0300 Subject: [PATCH 09/45] Models --- .../0024_role_establishment_subtype.py | 20 +++++++++++++++++++ apps/account/models.py | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 apps/account/migrations/0024_role_establishment_subtype.py diff --git a/apps/account/migrations/0024_role_establishment_subtype.py b/apps/account/migrations/0024_role_establishment_subtype.py new file mode 100644 index 00000000..3b9062c5 --- /dev/null +++ b/apps/account/migrations/0024_role_establishment_subtype.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2019-12-06 06:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0067_auto_20191122_1244'), + ('account', '0023_auto_20191204_0916'), + ] + + operations = [ + migrations.AddField( + model_name='role', + name='establishment_subtype', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.EstablishmentSubType', verbose_name='Establishment subtype'), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 53163d95..8f6c6233 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -1,5 +1,6 @@ """Account models""" from datetime import datetime +from tabnanny import verbose from django.conf import settings from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager @@ -15,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.authtoken.models import Token from authorization.models import Application -from establishment.models import Establishment +from establishment.models import Establishment, EstablishmentSubType from location.models import Country from main.models import SiteSettings from utils.models import GMTokenGenerator @@ -54,6 +55,9 @@ class Role(ProjectBaseMixin): null=True, blank=True, on_delete=models.SET_NULL) site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'), null=True, blank=True, on_delete=models.SET_NULL) + establishment_subtype = models.ForeignKey(EstablishmentSubType, + verbose_name=_('Establishment subtype'), + null=True, blank=True, on_delete=models.SET_NULL) class UserManager(BaseUserManager): From b659308729548d8a2e5df9c49ca90a1546ff152a Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Dec 2019 13:06:59 +0300 Subject: [PATCH 10/45] 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 e824b5ee65cc85e626e64073eb3a9f26998b94b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Fri, 6 Dec 2019 16:51:57 +0300 Subject: [PATCH 11/45] winery permission --- apps/establishment/models.py | 19 ----------- apps/establishment/tests.py | 57 ++++++++++++++++++++++++++++++-- apps/establishment/views/back.py | 28 ++++++++-------- apps/utils/permissions.py | 52 ++++++++++++++++++++++++++--- 4 files changed, 116 insertions(+), 40 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 3cdef691..20db3710 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -824,25 +824,6 @@ 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/tests.py b/apps/establishment/tests.py index bd96b052..86606d6b 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -4,7 +4,8 @@ from account.models import User from rest_framework import status from http.cookies import SimpleCookie from main.models import Currency -from establishment.models import Establishment, EstablishmentType, Menu, SocialChoice, SocialNetwork +from establishment.models import Establishment, EstablishmentType, EstablishmentSubType,\ + Menu, SocialChoice, SocialNetwork # Create your tests here. from translation.models import Language from account.models import Role, UserRole @@ -87,7 +88,59 @@ class BaseTestCase(APITestCase): ) -class EstablishmentBTests(BaseTestCase): +class WineryBackTests(BaseTestCase): + def setUp(self): + super().setUp() + self.user_role.delete() + self.role.delete() + + def test_establishment_CRUD(self): + params = {'page': 1, 'page_size': 1, } + response = self.client.get('/api/back/establishments/', params, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.establishment_subtype = EstablishmentSubType.objects.create( + name={"en-GB":"some text"}, + index_name='Index name', + establishment_type_id=self.establishment_type.id + ) + + self.establishment_subtype.save() + self.role = Role.objects.create(role=Role.WINERY_REVIEWER, + establishment_subtype_id=self.establishment_subtype.id) + self.role.save() + self.establishment.add_establishment_subtype(self.establishment_subtype) + + data = { + 'name': 'Test establishment', + 'type_id': self.establishment_type.id, + 'is_publish': True, + 'slug': 'test-establishment-slug', + 'tz': py_tz('Europe/Moscow').zone, + 'address_id': self.address.id + } + + response = self.client.post('/api/back/establishments/', data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/back/establishments/{self.establishment.id}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'name': 'Test new establishment' + } + + response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/', + data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/', + format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + + +class EstablishmentBackTests(BaseTestCase): def test_establishment_CRUD(self): params = {'page': 1, 'page_size': 1, } response = self.client.get('/api/back/establishments/', params, format='json') diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 6fa4d821..e7cd8f73 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -3,10 +3,9 @@ from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions, status -from utils.permissions import IsCountryAdmin, IsEstablishmentManager from establishment import filters, models, serializers from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer -from utils.permissions import IsCountryAdmin, IsEstablishmentManager +from utils.permissions import IsCountryAdmin, IsEstablishmentManager, IsWineryReviewer from utils.views import CreateDestroyGalleryViewMixin from timetable.models import Timetable from rest_framework import status @@ -25,7 +24,8 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP """Establishment list/create view.""" filter_class = filters.EstablishmentFilter - permission_classes = [IsCountryAdmin | IsEstablishmentManager] + + permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager] queryset = models.Establishment.objects.all() serializer_class = serializers.EstablishmentListCreateSerializer @@ -33,13 +33,13 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView): queryset = models.Establishment.objects.all() serializer_class = serializers.EstablishmentRUDSerializer - permission_classes = [IsCountryAdmin | IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager] class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): """Establishment schedule RUD view""" serializer_class = ScheduleRUDSerializer - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer |IsEstablishmentManager] def get_object(self): """ @@ -64,21 +64,21 @@ class EstablishmentScheduleCreateView(generics.CreateAPIView): """Establishment schedule Create view""" serializer_class = ScheduleCreateSerializer queryset = Timetable.objects.all() - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class MenuListCreateView(generics.ListCreateAPIView): """Menu list create view.""" serializer_class = serializers.MenuSerializers queryset = models.Menu.objects.all() - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class MenuRUDView(generics.RetrieveUpdateDestroyAPIView): """Menu RUD view.""" serializer_class = serializers.MenuRUDSerializers queryset = models.Menu.objects.all() - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class SocialChoiceListCreateView(generics.ListCreateAPIView): @@ -116,14 +116,14 @@ class PlateListCreateView(generics.ListCreateAPIView): serializer_class = serializers.PlatesSerializers queryset = models.Plate.objects.all() pagination_class = None - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class PlateRUDView(generics.RetrieveUpdateDestroyAPIView): """Plate RUD view.""" serializer_class = serializers.PlatesSerializers queryset = models.Plate.objects.all() - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class PhonesListCreateView(generics.ListCreateAPIView): @@ -131,14 +131,14 @@ class PhonesListCreateView(generics.ListCreateAPIView): serializer_class = serializers.ContactPhoneBackSerializers queryset = models.ContactPhone.objects.all() pagination_class = None - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView): """Phones RUD view.""" serializer_class = serializers.ContactPhoneBackSerializers queryset = models.ContactPhone.objects.all() - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class EmailListCreateView(generics.ListCreateAPIView): @@ -146,14 +146,14 @@ class EmailListCreateView(generics.ListCreateAPIView): serializer_class = serializers.ContactEmailBackSerializers queryset = models.ContactEmail.objects.all() pagination_class = None - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class EmailRUDView(generics.RetrieveUpdateDestroyAPIView): """Email RUD view.""" serializer_class = serializers.ContactEmailBackSerializers queryset = models.ContactEmail.objects.all() - permission_classes = [IsEstablishmentManager] + permission_classes = [IsWineryReviewer | IsEstablishmentManager] class EmployeeListCreateView(generics.ListCreateAPIView): diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 30055c44..432c653b 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -7,7 +7,8 @@ from rest_framework_simplejwt.tokens import AccessToken from account.models import UserRole, Role from authorization.models import JWTRefreshToken from utils.tokens import GMRefreshToken - +from establishment.models import EstablishmentSubType +from location.models import Address class IsAuthenticatedAndTokenIsValid(permissions.BasePermission): """ @@ -56,8 +57,9 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly): """ Object-level permission to only allow owners of an object to edit it. """ - + SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') def has_permission(self, request, view): + rules = [ request.user.is_superuser, request.method in permissions.SAFE_METHODS @@ -306,7 +308,6 @@ class IsEstablishmentManager(IsStandardUser): rules = [ # special! super().has_permission(request, view) - # super().has_object_permission(request, view, obj) ] role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ @@ -319,7 +320,6 @@ class IsEstablishmentManager(IsStandardUser): ).exists(), # special! super().has_permission(request, view) - # super().has_object_permission(request, view, obj) ] return any(rules) @@ -368,7 +368,7 @@ class IsRestaurantReviewer(IsStandardUser): # and request.user.email_confirmed, if hasattr(request.data, 'user') and hasattr(request.data, 'object_id'): role = Role.objects.filter(role=Role.RESTAURANT_REVIEWER) \ - .first() # 'Comments moderator' + .first() rules = [ UserRole.objects.filter(user=request.user, role=role, @@ -394,3 +394,45 @@ class IsRestaurantReviewer(IsStandardUser): ] return any(rules) + + +class IsWineryReviewer(IsStandardUser): + + def has_permission(self, request, view): + rules = [ + super().has_permission(request, view) + ] + + if 'type_id' in request.data and 'address_id' in request.data and request.user: + countries = Address.objects.filter(id=request.data['address_id']) + + est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id']) + if est.exists(): + role = Role.objects.filter(establishment_subtype_id__in=[type.id for type in est], + role=Role.WINERY_REVIEWER, + country_id__in=[country.id for country in countries]) \ + .first() + + rules.append( + UserRole.objects.filter(user=request.user, role=role).exists() + ) + + return any(rules) + + def has_object_permission(self, request, view, obj): + rules = [ + super().has_object_permission(request, view, obj) + ] + if hasattr(obj, 'type_id'): + est = EstablishmentSubType.objects.filter(establishment_type_id=obj.type_id) + role = Role.objects.filter(role=Role.WINERY_REVIEWER, + establishment_subtype_id__in=[id for type.id in est], + country_id=obj.country_id).first() + + rules = [ + UserRole.objects.filter(user=request.user, role=role, + establishment_id=obj.object_id + ).exists(), + super().has_object_permission(request, view, obj) + ] + return any(rules) \ No newline at end of file From af7aef6d233408cad387a4e319328ec06c374f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 9 Dec 2019 09:38:18 +0300 Subject: [PATCH 12/45] Add winery reviewer --- apps/utils/permissions.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 432c653b..0d21c096 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -423,15 +423,28 @@ class IsWineryReviewer(IsStandardUser): rules = [ super().has_object_permission(request, view, obj) ] - if hasattr(obj, 'type_id'): - est = EstablishmentSubType.objects.filter(establishment_type_id=obj.type_id) + # AttributeError: 'Establishment' object has no attribute 'object_id' + if hasattr(obj, 'type_id') or hasattr(obj, 'establishment_type_id'): + type_id: int + if hasattr(obj, 'type_id'): + type_id = obj.type_id + else: + type_id = obj.establishment_type_id + + est = EstablishmentSubType.objects.filter(establishment_type_id=type_id) role = Role.objects.filter(role=Role.WINERY_REVIEWER, establishment_subtype_id__in=[id for type.id in est], country_id=obj.country_id).first() + object_id: int + if hasattr(obj, 'object_id'): + object_id = obj.object_id + else: + object_id = obj.establishment_id + rules = [ UserRole.objects.filter(user=request.user, role=role, - establishment_id=obj.object_id + establishment_id=object_id ).exists(), super().has_object_permission(request, view, obj) ] From 5cac9659e5d54ebcdfde01aab85c927d65e1dfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 9 Dec 2019 09:38:58 +0300 Subject: [PATCH 13/45] Clean code --- apps/utils/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 0d21c096..498f5932 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -423,7 +423,7 @@ class IsWineryReviewer(IsStandardUser): rules = [ super().has_object_permission(request, view, obj) ] - # AttributeError: 'Establishment' object has no attribute 'object_id' + if hasattr(obj, 'type_id') or hasattr(obj, 'establishment_type_id'): type_id: int if hasattr(obj, 'type_id'): From 38839f75f9d63da93cf15ba69823858978561149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 9 Dec 2019 09:59:17 +0300 Subject: [PATCH 14/45] Fix account test --- apps/account/serializers/back.py | 1 + apps/account/urls/back.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index 699210e7..15e0684e 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -33,6 +33,7 @@ class BackUserSerializer(serializers.ModelSerializer): 'email_confirmed', 'newsletter', 'roles', + 'password' ) extra_kwargs = { 'password': {'write_only': True} diff --git a/apps/account/urls/back.py b/apps/account/urls/back.py index 630a4cb9..30f21573 100644 --- a/apps/account/urls/back.py +++ b/apps/account/urls/back.py @@ -8,6 +8,6 @@ app_name = 'account' urlpatterns = [ path('role/', views.RoleLstView.as_view(), name='role-list-create'), path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'), - path('user/', views.UserLstView.as_view(), name='user-list-create'), + path('user/', views.UserLstView.as_view(), name='user-create-list'), path('user//', views.UserRUDView.as_view(), name='user-rud'), ] From 3e0fad21391560488e679ba31edd05ee45427d95 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Mon, 9 Dec 2019 12:14:55 +0300 Subject: [PATCH 15/45] add merge of migrations --- .../product/migrations/0020_merge_20191209_0911.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/product/migrations/0020_merge_20191209_0911.py diff --git a/apps/product/migrations/0020_merge_20191209_0911.py b/apps/product/migrations/0020_merge_20191209_0911.py new file mode 100644 index 00000000..b8c83246 --- /dev/null +++ b/apps/product/migrations/0020_merge_20191209_0911.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.7 on 2019-12-09 09:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0019_auto_20191204_1420'), + ('product', '0019_product_serial_number'), + ] + + operations = [ + ] From dc7a6a911aadc1156fe8867ba462eab708164db7 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Dec 2019 14:29:54 +0300 Subject: [PATCH 16/45] refactoring --- apps/advertisement/models.py | 4 ++++ apps/advertisement/serializers/common.py | 5 ----- apps/advertisement/views/common.py | 5 ++++- apps/advertisement/views/web.py | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/advertisement/models.py b/apps/advertisement/models.py index 920e3389..66cd0024 100644 --- a/apps/advertisement/models.py +++ b/apps/advertisement/models.py @@ -22,6 +22,10 @@ class AdvertisementQuerySet(models.QuerySet): """Filter Advertisement by page type.""" return self.filter(page_type__name=page_type) + def by_country(self, code: str): + """Filter Advertisement by country code.""" + return self.filter(sites__country__code=code) + def by_locale(self, locale): """Filter by locale.""" return self.filter(target_languages__locale=locale) diff --git a/apps/advertisement/serializers/common.py b/apps/advertisement/serializers/common.py index 0adb74f9..9caee0c2 100644 --- a/apps/advertisement/serializers/common.py +++ b/apps/advertisement/serializers/common.py @@ -11,14 +11,11 @@ from main.models import SiteSettings class AdvertisementBaseSerializer(serializers.ModelSerializer): """Base serializer for model Advertisement.""" - languages = LanguageSerializer(many=True, read_only=True, - source='target_languages') target_languages = serializers.PrimaryKeyRelatedField( queryset=Language.objects.all(), many=True, write_only=True ) - sites = SiteShortSerializer(many=True, read_only=True) target_sites = serializers.PrimaryKeyRelatedField( queryset=SiteSettings.objects.all(), many=True, @@ -33,9 +30,7 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer): 'uuid', 'url', 'block_level', - 'languages', 'target_languages', - 'sites', 'target_sites', 'start', 'end', diff --git a/apps/advertisement/views/common.py b/apps/advertisement/views/common.py index 43c6e965..02c61873 100644 --- a/apps/advertisement/views/common.py +++ b/apps/advertisement/views/common.py @@ -28,5 +28,8 @@ class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView) product_type = self.kwargs.get('page_type') qs = super(AdvertisementPageTypeListView, self).get_queryset() if product_type: - return qs.by_page_type(product_type) + return qs.by_page_type(product_type) \ + .by_country(self.request.country_code) \ + .by_locale(self.request.locale) \ + .distinct('id') return qs.none() diff --git a/apps/advertisement/views/web.py b/apps/advertisement/views/web.py index db1cfde8..a9e527a0 100644 --- a/apps/advertisement/views/web.py +++ b/apps/advertisement/views/web.py @@ -7,3 +7,4 @@ class AdvertisementPageTypeWebListView(AdvertisementPageTypeListView): """Advertisement mobile list view.""" serializer_class = AdvertisementPageTypeWebListSerializer + From ea760fc674784e520e16501ac7b0d19d43d7f7ae Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 9 Dec 2019 15:00:15 +0300 Subject: [PATCH 17/45] rename attrs --- apps/search_indexes/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index a43ebaf7..3b5561fa 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -243,8 +243,8 @@ class WineOriginSerializer(serializers.Serializer): class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): """Establishment document serializer.""" - establishment_type = EstablishmentTypeSerializer() - establishment_subtypes = EstablishmentTypeSerializer(many=True) + type = EstablishmentTypeSerializer(source='establishment_type') + subtypes = EstablishmentTypeSerializer(many=True, source='establishment_subtypes') address = AddressDocumentSerializer(allow_null=True) tags = TagsDocumentSerializer(many=True, source='visible_tags') restaurant_category = TagsDocumentSerializer(many=True, allow_null=True) @@ -280,8 +280,8 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): 'wine_origins', # 'works_now', # 'collections', - # 'establishment_type', - # 'establishment_subtypes', + 'type', + 'subtypes', ) From 912431690d5c12384542c5834b5df3c4b05d89cd Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 9 Dec 2019 15:21:39 +0300 Subject: [PATCH 18/45] rename attrs #2 --- apps/establishment/serializers/common.py | 4 ++-- apps/transfer/serializers/guide.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 52d7ae69..5c77ccbf 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -450,13 +450,13 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): address = AddressDetailSerializer(read_only=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True) - establishment_type = EstablishmentTypeGeoSerializer() + type = EstablishmentTypeGeoSerializer(source='establishment_type') artisan_category = TagBaseSerializer(many=True, allow_null=True) class Meta(EstablishmentBaseSerializer.Meta): fields = EstablishmentBaseSerializer.Meta.fields + [ 'schedule', - 'establishment_type', + 'type', 'artisan_category', ] diff --git a/apps/transfer/serializers/guide.py b/apps/transfer/serializers/guide.py index f0cddd02..8a083e55 100644 --- a/apps/transfer/serializers/guide.py +++ b/apps/transfer/serializers/guide.py @@ -68,7 +68,7 @@ class GuideSerializer(TransferSerializerMixin): class GuideFilterSerializer(TransferSerializerMixin): id = serializers.IntegerField() year = serializers.CharField(allow_null=True) - establishment_type = serializers.CharField(allow_null=True) + type = serializers.CharField(allow_null=True, source='establishment_type') countries = serializers.CharField(allow_null=True) regions = serializers.CharField(allow_null=True) subregions = serializers.CharField(allow_null=True) @@ -86,7 +86,7 @@ class GuideFilterSerializer(TransferSerializerMixin): fields = ( 'id', 'year', - 'establishment_type', + 'type', 'countries', 'regions', 'subregions', From 188f8877b485f12d205cbdbf05e87c94db4c5a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=D1=85?= Date: Mon, 9 Dec 2019 16:35:38 +0300 Subject: [PATCH 19/45] Fix test --- apps/establishment/tests.py | 52 ------------------------------------- 1 file changed, 52 deletions(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index c68bf029..f0c26abe 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -88,58 +88,6 @@ class BaseTestCase(APITestCase): ) -class WineryBackTests(BaseTestCase): - def setUp(self): - super().setUp() - self.user_role.delete() - self.role.delete() - - def test_establishment_CRUD(self): - params = {'page': 1, 'page_size': 1, } - response = self.client.get('/api/back/establishments/', params, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.establishment_subtype = EstablishmentSubType.objects.create( - name={"en-GB":"some text"}, - index_name='Index name', - establishment_type_id=self.establishment_type.id - ) - - self.establishment_subtype.save() - self.role = Role.objects.create(role=Role.WINERY_REVIEWER, - establishment_subtype_id=self.establishment_subtype.id) - self.role.save() - self.establishment.add_establishment_subtype(self.establishment_subtype) - - data = { - 'name': 'Test establishment', - 'type_id': self.establishment_type.id, - 'is_publish': True, - 'slug': 'test-establishment-slug', - 'tz': py_tz('Europe/Moscow').zone, - 'address_id': self.address.id - } - - response = self.client.post('/api/back/establishments/', data=data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - response = self.client.get(f'/api/back/establishments/{self.establishment.id}/', format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - update_data = { - 'name': 'Test new establishment' - } - - response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/', - data=update_data) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/', - format='json') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - - class EstablishmentBackTests(BaseTestCase): def test_establishment_CRUD(self): params = {'page': 1, 'page_size': 1, } From 0f11a47a790c0b49dbd9b306eaff90644d0a8431 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 9 Dec 2019 16:47:46 +0300 Subject: [PATCH 20/45] rest category & rest cuisine for favs (cherry picked from commit 79a70fb) --- apps/establishment/serializers/common.py | 6 +++++- apps/favorites/views.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 5c77ccbf..19b4b764 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -451,13 +451,17 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): address = AddressDetailSerializer(read_only=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True) type = EstablishmentTypeGeoSerializer(source='establishment_type') - artisan_category = TagBaseSerializer(many=True, allow_null=True) + artisan_category = TagBaseSerializer(many=True, allow_null=True, read_only=True) + restaurant_category = TagBaseSerializer(many=True, allow_null=True, read_only=True) + restaurant_cuisine = TagBaseSerializer(many=True, allow_null=True, read_only=True) class Meta(EstablishmentBaseSerializer.Meta): fields = EstablishmentBaseSerializer.Meta.fields + [ 'schedule', 'type', 'artisan_category', + 'restaurant_category', + 'restaurant_cuisine', ] diff --git a/apps/favorites/views.py b/apps/favorites/views.py index bee25ced..4faa3e07 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -30,6 +30,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView): """Override get_queryset method""" return Establishment.objects.filter(favorites__user=self.request.user) \ .order_by('-favorites').with_base_related() \ + .with_certain_tag_category_related('category', 'restaurant_category') \ + .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ .with_certain_tag_category_related('shop_category', 'artisan_category') From a6728e3148e4c5c272af33503ca778d0ea3b1a7c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 9 Dec 2019 16:59:40 +0300 Subject: [PATCH 21/45] wine_origins for favorites establishments --- apps/product/serializers/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index d794ca93..51ff8e1e 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -6,7 +6,8 @@ from comment.models import Comment from comment.serializers import CommentSerializer from establishment.serializers import EstablishmentProductShortSerializer from establishment.serializers.common import _EstablishmentAddressShortSerializer -from location.serializers import WineOriginRegionBaseSerializer, WineOriginBaseSerializer +from location.serializers import WineOriginRegionBaseSerializer,\ + WineOriginBaseSerializer, EstablishmentWineOriginBaseSerializer from main.serializers import AwardSerializer from product import models from review.serializers import ReviewShortSerializer @@ -95,6 +96,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): preview_image_url = serializers.URLField(allow_null=True, read_only=True) in_favorites = serializers.BooleanField(allow_null=True) + wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True) class Meta: """Meta class.""" From d12ad93fe1c258631f68d317335fcfb92c54a4cb Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 9 Dec 2019 17:19:29 +0300 Subject: [PATCH 22/45] try to fix issue w/ news tag binding --- apps/search_indexes/documents/tag_category.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/search_indexes/documents/tag_category.py b/apps/search_indexes/documents/tag_category.py index ed69c8b7..3c335cdf 100644 --- a/apps/search_indexes/documents/tag_category.py +++ b/apps/search_indexes/documents/tag_category.py @@ -2,6 +2,7 @@ from django.conf import settings from django_elasticsearch_dsl import Document, Index, fields from tag import models +from news.models import News TagCategoryIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'tag_category')) TagCategoryIndex.settings(number_of_shards=2, number_of_replicas=2) @@ -26,7 +27,7 @@ class TagCategoryDocument(Document): 'public', 'value_type' ) - related_models = [models.Tag] + related_models = [models.Tag, News] def get_queryset(self): From be793eed73658727fb66da7e75b2284634247e35 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 9 Dec 2019 17:40:05 +0300 Subject: [PATCH 23/45] fix issue w/ tags binding --- apps/search_indexes/documents/tag_category.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/search_indexes/documents/tag_category.py b/apps/search_indexes/documents/tag_category.py index 3c335cdf..cd2c8a90 100644 --- a/apps/search_indexes/documents/tag_category.py +++ b/apps/search_indexes/documents/tag_category.py @@ -32,3 +32,11 @@ class TagCategoryDocument(Document): def get_queryset(self): return super().get_queryset().with_base_related() + + def get_instances_from_related(self, related_instance): + """If related_models is set, define how to retrieve the Car instance(s) from the related model. + The related_models option should be used with caution because it can lead in the index + to the updating of a lot of items. + """ + if isinstance(related_instance, News): + return related_instance.tags \ No newline at end of file From 6af7a7da5bb6bbe211025ad86f053c52d11ca687 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Mon, 9 Dec 2019 17:47:37 +0300 Subject: [PATCH 24/45] add footer migrations and api --- apps/main/management/commands/add_footers.py | 32 ++++++++++++++++++++ apps/main/migrations/0040_footer.py | 29 ++++++++++++++++++ apps/main/models.py | 9 ++++++ apps/main/serializers.py | 30 ++++++++++++++++++ apps/main/urls/back.py | 2 ++ apps/main/views/back.py | 16 +++++++++- apps/transfer/models.py | 14 +++++++++ make_data_migration.sh | 3 ++ 8 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 apps/main/management/commands/add_footers.py create mode 100644 apps/main/migrations/0040_footer.py diff --git a/apps/main/management/commands/add_footers.py b/apps/main/management/commands/add_footers.py new file mode 100644 index 00000000..5982fd43 --- /dev/null +++ b/apps/main/management/commands/add_footers.py @@ -0,0 +1,32 @@ +from django.core.management.base import BaseCommand +from tqdm import tqdm + +from main.models import SiteSettings, Footer +from transfer.models import Footers + + +class Command(BaseCommand): + help = '''Add footers from legacy DB.''' + + def handle(self, *args, **kwargs): + objects = [] + deleted = 0 + footers_list = Footers.objects.all() + + for old_footer in tqdm(footers_list, desc='Add footers'): + site = SiteSettings.objects.filter(old_id=old_footer.site_id).first() + if site: + if site.footers.exists(): + site.footers.all().delete() + deleted += 1 + footer = Footer( + site=site, + about_us=old_footer.about_us, + copyright=old_footer.copyright, + created=old_footer.created_at, + modified=old_footer.updated_at + ) + objects.append(footer) + Footer.objects.bulk_create(objects) + self.stdout.write( + self.style.WARNING(f'Created {len(objects)}/Deleted {deleted} footer objects.')) diff --git a/apps/main/migrations/0040_footer.py b/apps/main/migrations/0040_footer.py new file mode 100644 index 00000000..688e76d1 --- /dev/null +++ b/apps/main/migrations/0040_footer.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.7 on 2019-12-09 13:21 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0039_sitefeature_old_id'), + ] + + operations = [ + migrations.CreateModel( + name='Footer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('about_us', models.TextField(verbose_name='about_us')), + ('copyright', models.TextField(verbose_name='copyright')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='footers', to='main.SiteSettings', verbose_name='footer')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index f9c1225f..0869169e 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -351,3 +351,12 @@ class PageType(ProjectBaseMixin): def __str__(self): """Overridden dunder method.""" return self.name + + +class Footer(ProjectBaseMixin): + site = models.ForeignKey( + 'main.SiteSettings', related_name='footers', verbose_name=_('footer'), + on_delete=models.PROTECT + ) + about_us = models.TextField(_('about_us')) + copyright = models.TextField(_('copyright')) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index a41a643f..033da976 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -22,6 +22,7 @@ class FeatureSerializer(serializers.ModelSerializer): 'site_settings', ) + class CurrencySerializer(ProjectModelSerializer): """Currency serializer.""" @@ -36,6 +37,33 @@ class CurrencySerializer(ProjectModelSerializer): ] +class FooterSerializer(serializers.ModelSerializer): + """Footer serializer.""" + + class Meta: + model = models.Footer + fields = [ + 'id', + 'about_us', + 'copyright', + 'created', + 'modified', + ] + + +class FooterBackSerializer(FooterSerializer): + site_id = serializers.PrimaryKeyRelatedField( + queryset=models.SiteSettings.objects.all(), + source='site' + ) + + class Meta: + model = models.Footer + fields = FooterSerializer.Meta.fields + [ + 'site_id' + ] + + class SiteFeatureSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source='feature.id') slug = serializers.CharField(source='feature.slug') @@ -68,6 +96,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer): country_name = serializers.CharField(source='country.name_translated', read_only=True) time_format = serializers.CharField(source='country.time_format', read_only=True) + footers = FooterSerializer(many=True, read_only=True) class Meta: """Meta class.""" @@ -87,6 +116,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer): 'published_features', 'currency', 'country_name', + 'footers', ] diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 609e61f7..a9e55311 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -18,6 +18,8 @@ urlpatterns = [ name='site-feature-list-create'), path('site-feature//', views.SiteFeatureRUDBackView.as_view(), name='site-feature-rud'), + path('footer/', views.FooterBackView.as_view(), name='footer-list-create'), + path('footer//', views.FooterRUDBackView.as_view(), name='footer-rud'), ] diff --git a/apps/main/views/back.py b/apps/main/views/back.py index 1faf79a0..3d73f88c 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -4,7 +4,7 @@ from rest_framework import generics, permissions from main import serializers from main.filters import AwardFilter -from main.models import Award +from main.models import Award, Footer from main.views import SiteSettingsView, SiteListView @@ -67,3 +67,17 @@ class SiteSettingsBackOfficeView(SiteSettingsView): class SiteListBackOfficeView(SiteListView): """Site settings View.""" serializer_class = serializers.SiteSerializer + + +class FooterBackView(generics.ListCreateAPIView): + """Footer back list/create view.""" + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + serializer_class = serializers.FooterBackSerializer + queryset = Footer.objects.all() + + +class FooterRUDBackView(generics.RetrieveUpdateDestroyAPIView): + """Footer back RUD view.""" + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + serializer_class = serializers.FooterBackSerializer + queryset = Footer.objects.all() diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 019f38aa..d8268d6d 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -1208,3 +1208,17 @@ class NewsletterSubscriber(MigrateMixin): class Meta: managed = False db_table = 'newsletter_subscriptions' + + +class Footers(MigrateMixin): + using = 'legacy' + + about_us = models.TextField(blank=True, null=True) + copyright = models.TextField(blank=True, null=True) + site = models.ForeignKey('Sites', models.DO_NOTHING, blank=True, null=True) + created_at = models.DateTimeField() + updated_at = models.DateTimeField() + + class Meta: + managed = False + db_table = 'footers' diff --git a/make_data_migration.sh b/make_data_migration.sh index 50be7fc4..965eac19 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -8,9 +8,12 @@ ./manage.py transfer --fill_city_gallery ./manage.py transfer -l ./manage.py transfer --product +# Утеряна четкая связь между последовательностью миграций для импорта тегов продуктов, +# что может привести к удалению уже импортированных тегов командой выше. ./manage.py transfer --souvenir ./manage.py transfer --establishment_note ./manage.py transfer --product_note +./manage.py transfer --check_serial_number ./manage.py transfer --wine_characteristics ./manage.py transfer --inquiries ./manage.py transfer --assemblage From 7910c98c8a8e9ec69857422dddc6dec9612d2279 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 9 Dec 2019 18:20:23 +0300 Subject: [PATCH 25/45] add wine_origins --- apps/product/serializers/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 51ff8e1e..e0617e63 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -115,6 +115,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): 'wine_regions', 'wine_colors', 'in_favorites', + 'wine_origins', ] From a2395b807e6d6bb6f1a80365b22dcc89a9a202a0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 9 Dec 2019 20:42:47 +0300 Subject: [PATCH 26/45] User locale & city reading --- apps/account/models.py | 4 ++++ apps/account/serializers/back.py | 8 +++++--- apps/utils/middleware.py | 11 ++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index 8f6c6233..70a3cd69 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -107,6 +107,10 @@ class User(AbstractUser): email_confirmed = models.BooleanField(_('email status'), default=False) newsletter = models.NullBooleanField(default=True) old_id = models.IntegerField(null=True, blank=True, default=None) + locale = models.CharField(max_length=10, blank=True, default=None, null=True, + verbose_name=_('User last used locale')) + city = models.TextField(default=None, blank=True, null=True, + verbose_name=_('User last visited from city')) EMAIL_FIELD = 'email' USERNAME_FIELD = 'username' diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index 15e0684e..01889411 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -33,12 +33,14 @@ class BackUserSerializer(serializers.ModelSerializer): 'email_confirmed', 'newsletter', 'roles', - 'password' + 'password', + 'city', + 'locale', ) extra_kwargs = { - 'password': {'write_only': True} + 'password': {'write_only': True}, } - read_only_fields = ('old_password', 'last_login', 'date_joined') + read_only_fields = ('old_password', 'last_login', 'date_joined', 'city', 'locale') def create(self, validated_data): user = super().create(validated_data) diff --git a/apps/utils/middleware.py b/apps/utils/middleware.py index 1a421322..fa9f9950 100644 --- a/apps/utils/middleware.py +++ b/apps/utils/middleware.py @@ -1,8 +1,9 @@ -"""Custom middleware.""" +"""Custom middlewares.""" from django.utils import translation, timezone -from account.models import User +from account.models import User from configuration.models import TranslationSettings +from main.methods import determine_user_city from translation.models import Language @@ -18,7 +19,11 @@ def user_last_visit(get_response): def middleware(request): response = get_response(request) if request.user.is_authenticated: - User.objects.filter(pk=request.user.pk).update(last_login=timezone.now()) + User.objects.filter(pk=request.user.pk).update(**{ + 'last_login': timezone.now(), + 'locale': request.locale, + 'city': determine_user_city(request), + }) return response return middleware From ed18d1c44d7890a6da80371012a98cd15c8a1463 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 10 Dec 2019 09:24:32 +0300 Subject: [PATCH 27/45] forgotten migrations --- .../migrations/0025_auto_20191210_0623.py | 23 +++++++++++++++++++ make_data_migration.sh | 3 ++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 apps/account/migrations/0025_auto_20191210_0623.py diff --git a/apps/account/migrations/0025_auto_20191210_0623.py b/apps/account/migrations/0025_auto_20191210_0623.py new file mode 100644 index 00000000..464e9b20 --- /dev/null +++ b/apps/account/migrations/0025_auto_20191210_0623.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.7 on 2019-12-10 06:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0024_role_establishment_subtype'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='city', + field=models.TextField(blank=True, default=None, null=True, verbose_name='User last visited from city'), + ), + migrations.AddField( + model_name='user', + name='locale', + field=models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='User last used locale'), + ), + ] diff --git a/make_data_migration.sh b/make_data_migration.sh index 965eac19..b16c0b2d 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -18,4 +18,5 @@ ./manage.py transfer --inquiries ./manage.py transfer --assemblage ./manage.py transfer --purchased_plaques -./manage.py rm_empty_images \ No newline at end of file +./manage.py rm_empty_images +./manage.py add_artisan_subtype # добавляет подтипы для заведений артизанов \ No newline at end of file From ce5e5f6cb79012f64ec0cbdad985803ea48a24f3 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 12:42:42 +0300 Subject: [PATCH 28/45] 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 29/45] 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 4ec812d617e9d2aa5c27feb5632f47e5360c61a9 Mon Sep 17 00:00:00 2001 From: dormantman Date: Tue, 10 Dec 2019 14:27:16 +0300 Subject: [PATCH 30/45] Created new category_test url --- apps/tag/serializers.py | 48 ++++++++++++-- apps/tag/urls/web.py | 1 + apps/tag/views.py | 134 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 176 insertions(+), 7 deletions(-) diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index eb73291f..2da41d5c 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -2,11 +2,14 @@ from rest_framework import serializers from rest_framework.fields import SerializerMethodField -from establishment.models import (Establishment, EstablishmentType) -from news.models import News, NewsType +from establishment.models import Establishment +from establishment.models import EstablishmentType +from news.models import News +from news.models import NewsType from tag import models -from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound, - RemovedBindingObjectNotFound) +from utils.exceptions import BindingObjectNotFound +from utils.exceptions import ObjectAlreadyAdded +from utils.exceptions import RemovedBindingObjectNotFound from utils.serializers import TranslatedField @@ -95,6 +98,43 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer): return TagBaseSerializer(instance=tags, many=True, read_only=True).data +class TestBaseSerializer(serializers.ModelSerializer): + """Serializer for model TagCategory.""" + + label_translated = TranslatedField() + tags = SerializerMethodField() + + class Meta: + """Meta class.""" + + model = models.TagCategory + fields = ( + 'id', + 'transliterated_name', + 'name', + 'tags', + ) + + def get_filters(self, obj): + query_params = dict(self.context['request'].query_params) + + if len(query_params) > 1: + return [] + + params = {} + if 'establishment_type' in query_params: + params = { + 'establishments__isnull': False, + } + elif 'product_type' in query_params: + params = { + 'products__isnull': False, + } + + tags = obj.tags.filter(**params).distinct() + return TagBaseSerializer(instance=tags, many=True, read_only=True).data + + class TagCategoryShortSerializer(serializers.ModelSerializer): """Serializer for model TagCategory.""" diff --git a/apps/tag/urls/web.py b/apps/tag/urls/web.py index f83c593a..7ea45b17 100644 --- a/apps/tag/urls/web.py +++ b/apps/tag/urls/web.py @@ -7,6 +7,7 @@ app_name = 'tag' router = SimpleRouter() router.register(r'categories', views.TagCategoryViewSet) +router.register(r'categories_test', views.TestTagCategoryViewSet) router.register(r'chosen_tags', views.ChosenTagsView) urlpatterns = [ diff --git a/apps/tag/views.py b/apps/tag/views.py index 4a2f2613..886d4f9f 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -1,11 +1,25 @@ """Tag views.""" from django.conf import settings +from django_elasticsearch_dsl_drf import constants +from django_elasticsearch_dsl_drf.filter_backends import FilteringFilterBackend +from elasticsearch_dsl import TermsFacet +from rest_framework import generics +from rest_framework import mixins from rest_framework import permissions -from rest_framework import viewsets, mixins, status, generics +from rest_framework import status +from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response -from tag import filters, models, serializers +from search_indexes import utils +from search_indexes.documents import EstablishmentDocument +from search_indexes.filters import CustomFacetedSearchFilterBackend +from search_indexes.filters import CustomSearchFilterBackend +from search_indexes.serializers import EstablishmentDocumentSerializer +from tag import filters +from tag import models +from tag import serializers +from utils.pagination import ESDocumentPagination class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet): @@ -36,7 +50,8 @@ class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet): serializer = self.get_serializer(queryset, many=True) result_list = serializer.data if request.query_params.get('type') and (settings.ESTABLISHMENT_CHOSEN_TAGS or settings.NEWS_CHOSEN_TAGS): - ordered_list = settings.ESTABLISHMENT_CHOSEN_TAGS if request.query_params.get('type') == 'establishment' else settings.NEWS_CHOSEN_TAGS + ordered_list = settings.ESTABLISHMENT_CHOSEN_TAGS if request.query_params.get( + 'type') == 'establishment' else settings.NEWS_CHOSEN_TAGS result_list = sorted(result_list, key=lambda x: ordered_list.index(x['index_name'])) return Response(result_list) @@ -53,6 +68,119 @@ class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = serializers.TagCategoryBaseSerializer +# User`s views & viewsets +class TestTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """ViewSet for TagCategory model.""" + + filterset_classes = [ + filters.TagCategoryFilterSet, + FilteringFilterBackend, + CustomSearchFilterBackend, + CustomFacetedSearchFilterBackend, + ] + + document = EstablishmentDocument + pagination_class = ESDocumentPagination + permission_classes = (permissions.AllowAny,) + queryset = models.TagCategory.objects.with_tags().with_base_related(). \ + distinct() + serializer_class = serializers.TestBaseSerializer + + faceted_search_fields = { + 'works_at_weekday': { + 'field': 'works_at_weekday', + 'facet': TermsFacet, + 'enabled': True, + }, + 'toque_number': { + 'field': 'toque_number', + 'enabled': True, + 'facet': TermsFacet, + }, + 'works_noon': { + 'field': 'works_noon', + 'facet': TermsFacet, + 'enabled': True, + }, + 'works_evening': { + 'field': 'works_evening', + 'facet': TermsFacet, + 'enabled': True, + }, + 'works_now': { + 'field': 'works_now', + 'facet': TermsFacet, + 'enabled': True, + }, + 'tag': { + 'field': 'tags.id', + 'facet': TermsFacet, + 'enabled': True, + 'options': { + 'size': utils.FACET_MAX_RESPONSE, + }, + } + } + + search_fields = { + 'name': { + 'fuzziness': 'auto:2,5', + 'boost': 4 + }, + 'transliterated_name': { + 'fuzziness': 'auto:2,5', + 'boost': 3 + }, + 'description': {'fuzziness': 'auto:2,5'}, + } + translated_search_fields = ( + 'description', + ) + filter_fields = { + 'slug': 'slug', + 'tag': { + 'field': 'tags.id', + 'lookups': [constants.LOOKUP_QUERY_IN] + }, + 'toque_number': { + 'field': 'toque_number', + 'lookups': [ + constants.LOOKUP_FILTER_RANGE, + constants.LOOKUP_QUERY_GT, + constants.LOOKUP_QUERY_GTE, + constants.LOOKUP_QUERY_LT, + constants.LOOKUP_QUERY_LTE, + constants.LOOKUP_QUERY_IN, + ] + }, + + 'works_noon': { + 'field': 'works_noon', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + ], + }, + 'works_at_weekday': { + 'field': 'works_at_weekday', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + ], + }, + 'works_evening': { + 'field': 'works_evening', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + ], + }, + 'works_now': { + 'field': 'works_now', + 'lookups': [ + constants.LOOKUP_FILTER_TERM, + ] + }, + } + + # BackOffice user`s views & viewsets class BindObjectMixin: """Bind object mixin.""" From bec4e9be96b9ad4944145d507ca40f1319ec721b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 15:31:57 +0300 Subject: [PATCH 31/45] 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 32/45] 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 33/45] 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 34/45] 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 35/45] 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 096d5dab1829d6f4f1b0f3d34c17355fca9ee32a Mon Sep 17 00:00:00 2001 From: dormantman Date: Tue, 10 Dec 2019 19:24:56 +0300 Subject: [PATCH 36/45] Added tags/filters method --- apps/tag/serializers.py | 45 ++++++++-- apps/tag/urls/web.py | 2 +- apps/tag/views.py | 191 +++++++++++++++++----------------------- 3 files changed, 120 insertions(+), 118 deletions(-) diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index 2da41d5c..2fea5ad0 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -98,11 +98,13 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer): return TagBaseSerializer(instance=tags, many=True, read_only=True).data -class TestBaseSerializer(serializers.ModelSerializer): +class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer): """Serializer for model TagCategory.""" label_translated = TranslatedField() - tags = SerializerMethodField() + filters = SerializerMethodField() + param_name = SerializerMethodField() + type = SerializerMethodField() class Meta: """Meta class.""" @@ -110,17 +112,44 @@ class TestBaseSerializer(serializers.ModelSerializer): model = models.TagCategory fields = ( 'id', - 'transliterated_name', - 'name', - 'tags', + 'label_translated', + 'index_name', + 'param_name', + 'type', + 'filters', ) + def get_type(self, obj): + return obj in ['open_now', ] + + def get_param_name(self, obj): + if obj == 'service': + return 'tags_id__in' + + elif obj == 'pop': + return 'tags_id__in' + + elif obj == 'open_now': + return 'open_now' + + elif obj == 'wine_region': + return 'wine_region_id__in' + + return '%s__in' % obj.index_name + + def get_fields(self, *args, **kwargs): + fields = super(FiltersTagCategoryBaseSerializer, self).get_fields() + + if self.get_type(self): + fields.pop('filters', None) + else: + fields.pop('type', None) + + return fields + def get_filters(self, obj): query_params = dict(self.context['request'].query_params) - if len(query_params) > 1: - return [] - params = {} if 'establishment_type' in query_params: params = { diff --git a/apps/tag/urls/web.py b/apps/tag/urls/web.py index 7ea45b17..23298d3d 100644 --- a/apps/tag/urls/web.py +++ b/apps/tag/urls/web.py @@ -7,7 +7,7 @@ app_name = 'tag' router = SimpleRouter() router.register(r'categories', views.TagCategoryViewSet) -router.register(r'categories_test', views.TestTagCategoryViewSet) +router.register(r'filters', views.FiltersTagCategoryViewSet) router.register(r'chosen_tags', views.ChosenTagsView) urlpatterns = [ diff --git a/apps/tag/views.py b/apps/tag/views.py index 886d4f9f..fc870fe9 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -11,15 +11,10 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response -from search_indexes import utils -from search_indexes.documents import EstablishmentDocument -from search_indexes.filters import CustomFacetedSearchFilterBackend -from search_indexes.filters import CustomSearchFilterBackend -from search_indexes.serializers import EstablishmentDocumentSerializer +from location.models import WineRegion from tag import filters from tag import models from tag import serializers -from utils.pagination import ESDocumentPagination class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet): @@ -69,116 +64,94 @@ class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): # User`s views & viewsets -class TestTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): +class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): """ViewSet for TagCategory model.""" - filterset_classes = [ - filters.TagCategoryFilterSet, - FilteringFilterBackend, - CustomSearchFilterBackend, - CustomFacetedSearchFilterBackend, - ] - - document = EstablishmentDocument - pagination_class = ESDocumentPagination + filterset_class = filters.TagCategoryFilterSet + pagination_class = None permission_classes = (permissions.AllowAny,) queryset = models.TagCategory.objects.with_tags().with_base_related(). \ distinct() - serializer_class = serializers.TestBaseSerializer + serializer_class = serializers.FiltersTagCategoryBaseSerializer - faceted_search_fields = { - 'works_at_weekday': { - 'field': 'works_at_weekday', - 'facet': TermsFacet, - 'enabled': True, - }, - 'toque_number': { - 'field': 'toque_number', - 'enabled': True, - 'facet': TermsFacet, - }, - 'works_noon': { - 'field': 'works_noon', - 'facet': TermsFacet, - 'enabled': True, - }, - 'works_evening': { - 'field': 'works_evening', - 'facet': TermsFacet, - 'enabled': True, - }, - 'works_now': { - 'field': 'works_now', - 'facet': TermsFacet, - 'enabled': True, - }, - 'tag': { - 'field': 'tags.id', - 'facet': TermsFacet, - 'enabled': True, - 'options': { - 'size': utils.FACET_MAX_RESPONSE, - }, - } - } + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + serializer = self.get_serializer(queryset, many=True) - search_fields = { - 'name': { - 'fuzziness': 'auto:2,5', - 'boost': 4 - }, - 'transliterated_name': { - 'fuzziness': 'auto:2,5', - 'boost': 3 - }, - 'description': {'fuzziness': 'auto:2,5'}, - } - translated_search_fields = ( - 'description', - ) - filter_fields = { - 'slug': 'slug', - 'tag': { - 'field': 'tags.id', - 'lookups': [constants.LOOKUP_QUERY_IN] - }, - 'toque_number': { - 'field': 'toque_number', - 'lookups': [ - constants.LOOKUP_FILTER_RANGE, - constants.LOOKUP_QUERY_GT, - constants.LOOKUP_QUERY_GTE, - constants.LOOKUP_QUERY_LT, - constants.LOOKUP_QUERY_LTE, - constants.LOOKUP_QUERY_IN, - ] - }, + result_list = serializer.data + query_params = request.query_params - 'works_noon': { - 'field': 'works_noon', - 'lookups': [ - constants.LOOKUP_QUERY_IN, - ], - }, - 'works_at_weekday': { - 'field': 'works_at_weekday', - 'lookups': [ - constants.LOOKUP_QUERY_IN, - ], - }, - 'works_evening': { - 'field': 'works_evening', - 'lookups': [ - constants.LOOKUP_QUERY_IN, - ], - }, - 'works_now': { - 'field': 'works_now', - 'lookups': [ - constants.LOOKUP_FILTER_TERM, - ] - }, - } + if 'toque_number__in' in query_params: + toques = { + "index_name": "toque_number", + "label_translated": "Toques", + "param_name": "toque_number__in", + "filters": [{ + "id": toque_id, + "index_name": "toque_%d" % toque_id, + "label_translated": "Toque %d" % toque_id + } for toque_id in range(6)] + } + result_list.append(toques) + + if 'wine_region_id__in' in query_params: + try: + wine_region_id = int(query_params['wine_region_id__in']) + + wine_regions = { + "index_name": "wine_region", + "label_translated": "Wine region", + "param_name": "wine_region_id__in", + "filters": [{ + "id": obj.id, + "index_name": obj.name.lower().replace(' ', '_'), + "label_translated": obj.name + } for obj in WineRegion.objects.filter(id=wine_region_id)] + } + + result_list.append(wine_regions) + + except ValueError: + pass + + if 'works_noon__in' in query_params: + week_days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") + works_noon = { + "index_name": "works_noon", + "label_translated": "Open noon", + "param_name": "works_noon__in", + "filters": [{ + "id": weekday, + "index_name": week_days[weekday].lower(), + "label_translated": week_days[weekday] + } for weekday in range(7)] + } + result_list.append(works_noon) + + if 'works_evening__in' in query_params: + week_days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") + works_evening = { + "index_name": "works_evening", + "label_translated": "Open noon", + "param_name": "works_evening__in", + "filters": [{ + "id": weekday, + "index_name": week_days[weekday].lower(), + "label_translated": week_days[weekday] + } for weekday in range(7)] + } + result_list.append(works_evening) + + if 'works_now' in query_params: + works_now = { + "index_name": "open_now", + "label_translated": "Open now", + "param_name": "open_now", + "type": True + } + result_list.append(works_now) + + return Response(result_list) # BackOffice user`s views & viewsets @@ -271,4 +244,4 @@ class TagCategoryBackOfficeViewSet(mixins.CreateModelMixin, if obj_type == self.bind_object_serializer_class.ESTABLISHMENT_TYPE: tag_category.establishment_types.remove(related_object) elif obj_type == self.bind_object_serializer_class.NEWS_TYPE: - tag_category.news_types.remove(related_object) + tag_category.news_types.remove(related_object) \ No newline at end of file From c90f8302ee2f397b488929e498f01f449a0f6b94 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 10 Dec 2019 19:29:11 +0300 Subject: [PATCH 37/45] 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 38/45] 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 39/45] 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 40/45] 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 41/45] 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 42/45] 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): From 907a9365e1b73abaa0f55f1cf14ec6fcd0ef22d3 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 11 Dec 2019 12:14:23 +0300 Subject: [PATCH 43/45] fix multislug news update --- apps/news/serializers.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 846bc31a..f22c7129 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -177,13 +177,23 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): '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 + def create(self, validated_data): + slugs = validated_data.get('slugs') + if slugs: + if models.News.objects.filter( + slugs__values__contains=list(slugs.values()) + ).exists(): + raise serializers.ValidationError({'slugs': _('News with this slug already exists.')}) + return super().create(validated_data) + + def update(self, instance, validated_data): + slugs = validated_data.get('slugs') + if slugs: + if models.News.objects.filter( + slugs__values__contains=list(slugs.values()) + ).exclude(pk=instance.pk).exists(): + raise serializers.ValidationError({'slugs': _('News with this slug already exists.')}) + return super().update(instance, validated_data) class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, From fb5a9d4302581a356258626a0be12a3eefb88ca0 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 11 Dec 2019 12:26:22 +0300 Subject: [PATCH 44/45] refactored ads --- apps/advertisement/admin.py | 5 +++ apps/advertisement/models.py | 8 ++--- apps/advertisement/serializers/back.py | 30 +++++----------- apps/advertisement/serializers/common.py | 25 ++++++------- apps/advertisement/serializers/mobile.py | 12 +++---- apps/advertisement/serializers/web.py | 12 +++---- apps/advertisement/urls/back.py | 8 ++--- apps/advertisement/views/back.py | 35 +++++++++++-------- apps/advertisement/views/common.py | 6 ++-- apps/main/admin.py | 3 ++ .../migrations/0041_auto_20191211_0631.py | 18 ++++++++++ apps/main/models.py | 3 +- apps/main/serializers.py | 20 +++++++++-- apps/main/urls/back.py | 2 ++ apps/main/views/back.py | 10 +++++- 15 files changed, 121 insertions(+), 76 deletions(-) create mode 100644 apps/main/migrations/0041_auto_20191211_0631.py diff --git a/apps/advertisement/admin.py b/apps/advertisement/admin.py index 3754dca9..2728f4f9 100644 --- a/apps/advertisement/admin.py +++ b/apps/advertisement/admin.py @@ -14,3 +14,8 @@ class PageInline(admin.TabularInline): class AdvertisementModelAdmin(admin.ModelAdmin): """Admin model for model Advertisement""" inlines = (PageInline, ) + list_display = ('id', '__str__', 'block_level', + 'start', 'end', 'page_type') + list_filter = ('url', 'block_level', 'start', 'end', 'page_type', + 'pages__source') + date_hierarchy = 'created' diff --git a/apps/advertisement/models.py b/apps/advertisement/models.py index 66cd0024..b86a6168 100644 --- a/apps/advertisement/models.py +++ b/apps/advertisement/models.py @@ -71,11 +71,11 @@ class Advertisement(ProjectBaseMixin): return super().delete(using, keep_parents) @property - def mobile_page(self): + def mobile_pages(self): """Return mobile page""" - return self.pages.by_platform(Page.MOBILE).first() + return self.pages.by_platform(Page.MOBILE) @property - def web_page(self): + def web_pages(self): """Return web page""" - return self.pages.by_platform(Page.WEB).first() + return self.pages.by_platform(Page.WEB) diff --git a/apps/advertisement/serializers/back.py b/apps/advertisement/serializers/back.py index 9dc8b029..2ca5a18f 100644 --- a/apps/advertisement/serializers/back.py +++ b/apps/advertisement/serializers/back.py @@ -1,26 +1,14 @@ """Serializers for back office app advertisements""" -from main.serializers import PageBaseSerializer +from advertisement.serializers import AdvertisementBaseSerializer +from main.serializers import PageExtendedSerializer -class AdvertisementPageBaseSerializer(PageBaseSerializer): - """Base serializer for linking page w/ advertisement.""" +class AdvertisementDetailSerializer(AdvertisementBaseSerializer): + """Advertisement serializer for back office.""" + pages = PageExtendedSerializer(many=True, read_only=True) - class Meta(PageBaseSerializer.Meta): + class Meta(AdvertisementBaseSerializer.Meta): """Meta class.""" - - PageBaseSerializer.Meta.extra_kwargs.update({ - 'advertisement': {'write_only': True}, - 'image_url': {'required': True}, - 'width': {'required': True}, - 'height': {'required': True}, - }) - - -class AdvertisementPageListCreateSerializer(AdvertisementPageBaseSerializer): - """Serializer for linking page w/ advertisement.""" - - def create(self, validated_data): - """Overridden create method.""" - - validated_data['advertisement'] = self.context.get('view').get_object() - return super().create(validated_data) + fields = AdvertisementBaseSerializer.Meta.fields + [ + 'pages', + ] diff --git a/apps/advertisement/serializers/common.py b/apps/advertisement/serializers/common.py index 9caee0c2..b673475e 100644 --- a/apps/advertisement/serializers/common.py +++ b/apps/advertisement/serializers/common.py @@ -2,15 +2,15 @@ from rest_framework import serializers from advertisement import models -from translation.serializers import LanguageSerializer -from main.serializers import SiteShortSerializer, PageBaseSerializer -from translation.models import Language from main.models import SiteSettings +from main.serializers import PageTypeBaseSerializer +from translation.models import Language class AdvertisementBaseSerializer(serializers.ModelSerializer): """Base serializer for model Advertisement.""" - + page_type_detail = PageTypeBaseSerializer(read_only=True, + source='page_type') target_languages = serializers.PrimaryKeyRelatedField( queryset=Language.objects.all(), many=True, @@ -34,16 +34,17 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer): 'target_sites', 'start', 'end', + 'page_type', + 'page_type_detail', ] + extra_kwargs = { + 'page_type': {'required': True, 'write_only': True} + } -class AdvertisementPageTypeCommonListSerializer(AdvertisementBaseSerializer): - """Serializer for AdvertisementPageTypeCommonView.""" - - page = PageBaseSerializer(source='common_page', read_only=True) - +class AdvertisementSerializer(AdvertisementBaseSerializer): + """Serializer for model Advertisement.""" class Meta(AdvertisementBaseSerializer.Meta): """Meta class.""" - fields = AdvertisementBaseSerializer.Meta.fields + [ - 'page', - ] + fields = AdvertisementBaseSerializer.Meta.fields.copy() + fields.pop(fields.index('page_type_detail')) diff --git a/apps/advertisement/serializers/mobile.py b/apps/advertisement/serializers/mobile.py index 80a19b82..37d810b4 100644 --- a/apps/advertisement/serializers/mobile.py +++ b/apps/advertisement/serializers/mobile.py @@ -1,15 +1,15 @@ """Serializers for mobile app advertisements""" -from advertisement.serializers import AdvertisementBaseSerializer +from advertisement.serializers import AdvertisementSerializer from main.serializers import PageBaseSerializer -class AdvertisementPageTypeMobileListSerializer(AdvertisementBaseSerializer): +class AdvertisementPageTypeMobileListSerializer(AdvertisementSerializer): """Serializer for AdvertisementPageTypeMobileView.""" - page = PageBaseSerializer(source='mobile_page', read_only=True) + pages = PageBaseSerializer(many=True, source='mobile_pages', read_only=True) - class Meta(AdvertisementBaseSerializer.Meta): + class Meta(AdvertisementSerializer.Meta): """Meta class.""" - fields = AdvertisementBaseSerializer.Meta.fields + [ - 'page', + fields = AdvertisementSerializer.Meta.fields + [ + 'pages', ] diff --git a/apps/advertisement/serializers/web.py b/apps/advertisement/serializers/web.py index 175f1875..a78c1f53 100644 --- a/apps/advertisement/serializers/web.py +++ b/apps/advertisement/serializers/web.py @@ -1,15 +1,15 @@ """Serializers for web app advertisements""" -from advertisement.serializers import AdvertisementBaseSerializer +from advertisement.serializers import AdvertisementSerializer from main.serializers import PageBaseSerializer -class AdvertisementPageTypeWebListSerializer(AdvertisementBaseSerializer): +class AdvertisementPageTypeWebListSerializer(AdvertisementSerializer): """Serializer for AdvertisementPageTypeWebView.""" - page = PageBaseSerializer(source='web_page', read_only=True) + pages = PageBaseSerializer(many=True, source='web_pages', read_only=True) - class Meta(AdvertisementBaseSerializer.Meta): + class Meta(AdvertisementSerializer.Meta): """Meta class.""" - fields = AdvertisementBaseSerializer.Meta.fields + [ - 'page', + fields = AdvertisementSerializer.Meta.fields + [ + 'pages', ] diff --git a/apps/advertisement/urls/back.py b/apps/advertisement/urls/back.py index 2502da0d..7a5cb133 100644 --- a/apps/advertisement/urls/back.py +++ b/apps/advertisement/urls/back.py @@ -9,10 +9,10 @@ app_name = 'advertisements' urlpatterns = [ path('', views.AdvertisementListCreateView.as_view(), name='list-create'), path('/', views.AdvertisementRUDView.as_view(), name='rud'), - path('/pages/', views.AdvertisementPageListCreateView.as_view(), - name='page-list-create'), - path('/pages//', views.AdvertisementPageRUDView.as_view(), - name='page-rud') + path('/pages/', views.AdvertisementPageCreateView.as_view(), + name='ad-page-create'), + path('/pages//', views.AdvertisementPageUDView.as_view(), + name='ad-page-update-destroy') ] urlpatterns += common_urlpatterns diff --git a/apps/advertisement/views/back.py b/apps/advertisement/views/back.py index a2973589..27a016a2 100644 --- a/apps/advertisement/views/back.py +++ b/apps/advertisement/views/back.py @@ -1,19 +1,19 @@ """Back office views for app advertisement""" -from rest_framework import generics +from rest_framework import generics, status +from rest_framework.response import Response from rest_framework import permissions from django.shortcuts import get_object_or_404 +from main.serializers import PageExtendedSerializer from advertisement.models import Advertisement -from rest_framework.response import Response -from rest_framework import status from advertisement.serializers import (AdvertisementBaseSerializer, - AdvertisementPageBaseSerializer, - AdvertisementPageListCreateSerializer) + AdvertisementDetailSerializer) class AdvertisementBackOfficeViewMixin(generics.GenericAPIView): """Base back office advertisement view.""" + pagination_class = None permission_classes = (permissions.IsAuthenticated, ) def get_queryset(self): @@ -31,14 +31,14 @@ class AdvertisementRUDView(AdvertisementBackOfficeViewMixin, generics.RetrieveUpdateDestroyAPIView): """Retrieve|Update|Destroy advertisement page view.""" - serializer_class = AdvertisementBaseSerializer + serializer_class = AdvertisementDetailSerializer -class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, - generics.ListCreateAPIView): - """Retrieve|Update|Destroy advertisement page view.""" +class AdvertisementPageCreateView(AdvertisementBackOfficeViewMixin, + generics.CreateAPIView): + """Create advertisement page view.""" - serializer_class = AdvertisementPageListCreateSerializer + serializer_class = PageExtendedSerializer def get_object(self): """Returns the object the view is displaying.""" @@ -56,12 +56,19 @@ class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, """Overridden get_queryset method.""" return self.get_object().pages.all() + def create(self, request, *args, **kwargs): + """Overridden create method.""" + request.data.update({'advertisement': self.get_object().pk}) + super().create(request, *args, **kwargs) + return Response(status=status.HTTP_200_OK) -class AdvertisementPageRUDView(AdvertisementBackOfficeViewMixin, - generics.RetrieveUpdateDestroyAPIView): - """Create|Retrieve|Update|Destroy advertisement page view.""" - serializer_class = AdvertisementPageBaseSerializer +class AdvertisementPageUDView(AdvertisementBackOfficeViewMixin, + generics.UpdateAPIView, + generics.DestroyAPIView): + """Update|Destroy advertisement page view.""" + + serializer_class = PageExtendedSerializer def get_object(self): """Returns the object the view is displaying.""" diff --git a/apps/advertisement/views/common.py b/apps/advertisement/views/common.py index 02c61873..12702d4b 100644 --- a/apps/advertisement/views/common.py +++ b/apps/advertisement/views/common.py @@ -3,8 +3,7 @@ from rest_framework import generics from rest_framework import permissions from advertisement.models import Advertisement -from advertisement.serializers import AdvertisementBaseSerializer, \ - AdvertisementPageTypeCommonListSerializer +from advertisement.serializers import AdvertisementBaseSerializer class AdvertisementBaseView(generics.GenericAPIView): @@ -16,8 +15,7 @@ class AdvertisementBaseView(generics.GenericAPIView): def get_queryset(self): """Overridden get queryset method.""" - return Advertisement.objects.with_base_related() \ - .by_locale(self.request.locale) + return Advertisement.objects.with_base_related() class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView): diff --git a/apps/main/admin.py b/apps/main/admin.py index 4b7038e7..315d1c2b 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -51,3 +51,6 @@ class PageTypeAdmin(admin.ModelAdmin): @admin.register(models.Page) class PageAdmin(admin.ModelAdmin): """Page admin.""" + list_display = ('id', '__str__', 'advertisement') + list_filter = ('advertisement__url', 'source') + date_hierarchy = 'created' diff --git a/apps/main/migrations/0041_auto_20191211_0631.py b/apps/main/migrations/0041_auto_20191211_0631.py new file mode 100644 index 00000000..973f4afd --- /dev/null +++ b/apps/main/migrations/0041_auto_20191211_0631.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-12-11 06:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('advertisement', '0008_auto_20191116_1135'), + ('main', '0040_footer'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='page', + unique_together={('advertisement', 'source')}, + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index 0869169e..ae4168ea 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -305,7 +305,7 @@ class PageQuerySet(models.QuerySet): def by_platform(self, platform: int): """Filter by platform.""" - return self.filter(source=platform) + return self.filter(source__in=[Page.ALL, platform]) class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin): @@ -325,6 +325,7 @@ class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin): """Meta class.""" verbose_name = _('page') verbose_name_plural = _('pages') + unique_together = ('advertisement', 'source') def __str__(self): """Overridden dunder method.""" diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 033da976..d586d71a 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -152,8 +152,6 @@ class SiteShortSerializer(serializers.ModelSerializer): ] - - class AwardBaseSerializer(serializers.ModelSerializer): """Award base serializer.""" @@ -234,10 +232,26 @@ class PageBaseSerializer(serializers.ModelSerializer): 'advertisement', ] extra_kwargs = { - 'establishment': {'write_only': True} + 'advertisement': {'write_only': True}, + 'image_url': {'required': True}, + 'width': {'required': True}, + 'height': {'required': True}, } +class PageExtendedSerializer(PageBaseSerializer): + """Extended serializer for model Page.""" + source_display = serializers.CharField(read_only=True, + source='get_source_display') + + class Meta(PageBaseSerializer.Meta): + """Meta class.""" + fields = PageBaseSerializer.Meta.fields + [ + 'source', + 'source_display', + ] + + class PageTypeBaseSerializer(serializers.ModelSerializer): """Serializer fro model PageType.""" diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index a9e55311..b4e196a3 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -20,6 +20,8 @@ urlpatterns = [ name='site-feature-rud'), path('footer/', views.FooterBackView.as_view(), name='footer-list-create'), path('footer//', views.FooterRUDBackView.as_view(), name='footer-rud'), + path('page-types/', views.PageTypeListCreateView.as_view(), + name='page-types-list-create') ] diff --git a/apps/main/views/back.py b/apps/main/views/back.py index 3d73f88c..f6f987af 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -4,7 +4,7 @@ from rest_framework import generics, permissions from main import serializers from main.filters import AwardFilter -from main.models import Award, Footer +from main.models import Award, Footer, PageType from main.views import SiteSettingsView, SiteListView @@ -81,3 +81,11 @@ class FooterRUDBackView(generics.RetrieveUpdateDestroyAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) serializer_class = serializers.FooterBackSerializer queryset = Footer.objects.all() + + +class PageTypeListCreateView(generics.ListCreateAPIView): + """PageType back office view.""" + permission_classes = (permissions.IsAuthenticatedOrReadOnly, ) + pagination_class = None + serializer_class = serializers.PageTypeBaseSerializer + queryset = PageType.objects.all() From 19c42ab1d7f39c87ea37e1ade639b4d9c567197c Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 11 Dec 2019 13:03:31 +0300 Subject: [PATCH 45/45] Added page types --- apps/tag/views.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/tag/views.py b/apps/tag/views.py index fc870fe9..db3970e5 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -1,8 +1,5 @@ """Tag views.""" from django.conf import settings -from django_elasticsearch_dsl_drf import constants -from django_elasticsearch_dsl_drf.filter_backends import FilteringFilterBackend -from elasticsearch_dsl import TermsFacet from rest_framework import generics from rest_framework import mixins from rest_framework import permissions @@ -81,7 +78,9 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): result_list = serializer.data query_params = request.query_params - if 'toque_number__in' in query_params: + params_type = query_params['type'] + + if params_type == 'restaurant' and 'toque_number__in' in query_params: toques = { "index_name": "toque_number", "label_translated": "Toques", @@ -94,7 +93,7 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): } result_list.append(toques) - if 'wine_region_id__in' in query_params: + if params_type == 'winery' and 'wine_region_id__in' in query_params: try: wine_region_id = int(query_params['wine_region_id__in']) @@ -114,7 +113,7 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): except ValueError: pass - if 'works_noon__in' in query_params: + if params_type == 'restaurant' and 'works_noon__in' in query_params: week_days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") works_noon = { "index_name": "works_noon", @@ -128,11 +127,11 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): } result_list.append(works_noon) - if 'works_evening__in' in query_params: + if params_type == 'restaurant' and 'works_evening__in' in query_params: week_days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") works_evening = { "index_name": "works_evening", - "label_translated": "Open noon", + "label_translated": "Open evening", "param_name": "works_evening__in", "filters": [{ "id": weekday, @@ -142,7 +141,7 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): } result_list.append(works_evening) - if 'works_now' in query_params: + if params_type in ('restaurant', 'artisan') and 'works_now' in query_params: works_now = { "index_name": "open_now", "label_translated": "Open now", @@ -151,6 +150,11 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): } result_list.append(works_now) + if 'tags_id__in' in query_params: + # filtering by params_type and tags id + # todo: result_list.append( filtering_data ) + pass + return Response(result_list) @@ -244,4 +248,4 @@ class TagCategoryBackOfficeViewSet(mixins.CreateModelMixin, if obj_type == self.bind_object_serializer_class.ESTABLISHMENT_TYPE: tag_category.establishment_types.remove(related_object) elif obj_type == self.bind_object_serializer_class.NEWS_TYPE: - tag_category.news_types.remove(related_object) \ No newline at end of file + tag_category.news_types.remove(related_object)